diff --git a/README.md b/README.md index 3bcd46c8..be18ef11 100644 --- a/README.md +++ b/README.md @@ -188,8 +188,6 @@ Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded * (since API 30) `Lcom/android/server/wifi/WifiContext;->ACTION_RESOURCES_APK:Ljava/lang/String;` * (since API 29) `Lcom/android/server/wifi/p2p/WifiP2pServiceImpl;->ANONYMIZED_DEVICE_ADDRESS:Ljava/lang/String;` * (since API 30) `Lcom/android/server/SystemServer;->TETHERING_CONNECTOR_CLASS:Ljava/lang/String;` -* (since API 29) `Ldalvik/system/VMRuntime;->getRuntime()Ldalvik/system/VMRuntime;,core-platform-api,unsupported` -* (since API 29) `Ldalvik/system/VMRuntime;->setHiddenApiExemptions([Ljava/lang/String;)V,blocked,core-platform-api` * (since API 26) `Ljava/lang/invoke/MethodHandles$Lookup;->(Ljava/lang/Class;I)V,unsupported` * (since API 26) `Ljava/lang/invoke/MethodHandles$Lookup;->ALL_MODES:I,lo-prio,max-target-o` * (prior to API 29) `Ljava/net/InetAddress;->parseNumericAddress(Ljava/lang/String;)Ljava/net/InetAddress;,core-platform-api,max-target-p` diff --git a/build.gradle.kts b/build.gradle.kts index df1dec62..e9691b94 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,10 +10,10 @@ buildscript { dependencies { classpath(kotlin("gradle-plugin", "1.7.10")) - classpath("com.android.tools.build:gradle:7.3.0-beta05") - classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.1") + classpath("com.android.tools.build:gradle:7.4.0-beta01") + classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.2") classpath("com.google.android.gms:oss-licenses-plugin:0.10.5") - classpath("com.google.gms:google-services:4.3.13") + classpath("com.google.gms:google-services:4.3.14") } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index aa991fce..ae04661e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/mobile/build.gradle.kts b/mobile/build.gradle.kts index 2d9e3599..fc28d2df 100644 --- a/mobile/build.gradle.kts +++ b/mobile/build.gradle.kts @@ -69,25 +69,26 @@ android { } dependencies { - val lifecycleVersion = "2.5.0-rc01" - val roomVersion = "2.5.0-alpha02" + val lifecycleVersion = "2.5.1" + val roomVersion = "2.5.0-alpha03" - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.0") + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.0") kapt("androidx.room:room-compiler:$roomVersion") implementation(kotlin("stdlib-jdk8")) implementation("androidx.browser:browser:1.4.0") - implementation("androidx.core:core-ktx:1.8.0") - implementation("androidx.fragment:fragment-ktx:1.5.0") + implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.fragment:fragment-ktx:1.5.3") implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion") implementation("androidx.preference:preference:1.2.0") implementation("androidx.room:room-ktx:$roomVersion") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") + implementation("be.mygod.librootkotlinx:librootkotlinx:1.0.0") implementation("com.android.billingclient:billing-ktx:5.0.0") implementation("com.google.android.gms:play-services-oss-licenses:17.0.0") - implementation("com.google.android.material:material:1.7.0-alpha03") - implementation("com.google.firebase:firebase-analytics-ktx:21.1.0") - implementation("com.google.firebase:firebase-crashlytics:18.2.11") + implementation("com.google.android.material:material:1.7.0-rc01") + implementation("com.google.firebase:firebase-analytics-ktx:21.1.1") + implementation("com.google.firebase:firebase-crashlytics:18.2.13") implementation("com.google.zxing:core:3.5.0") implementation("com.jakewharton.timber:timber:5.0.1") implementation("com.linkedin.dexmaker:dexmaker:2.28.3") diff --git a/mobile/src/main/java/be/mygod/librootkotlinx/AppProcess.kt b/mobile/src/main/java/be/mygod/librootkotlinx/AppProcess.kt deleted file mode 100644 index c8f2a792..00000000 --- a/mobile/src/main/java/be/mygod/librootkotlinx/AppProcess.kt +++ /dev/null @@ -1,131 +0,0 @@ -package be.mygod.librootkotlinx - -import android.os.Build -import android.os.Debug -import android.os.Process -import androidx.annotation.RequiresApi -import java.io.File -import java.io.IOException - -object AppProcess { - /** - * Based on: https://android.googlesource.com/platform/bionic/+/aff9a34/linker/linker.cpp#3397 - */ - @get:RequiresApi(28) - val genericLdConfigFilePath: String get() { - "/system/etc/ld.config.$currentInstructionSet.txt".let { if (File(it).isFile) return it } - if (Build.VERSION.SDK_INT >= 30) "/linkerconfig/ld.config.txt".let { - if (File(it).isFile) return it - Logger.me.w("Failed to find generated linker configuration from \"$it\"") - } - if (isVndkLite) { - "/system/etc/ld.config.vndk_lite.txt".let { if (File(it).isFile) return it } - } else when (vndkVersion) { - "", "current" -> { } - else -> "/system/etc/ld.config.$vndkVersion.txt".let { if (File(it).isFile) return it } - } - return "/system/etc/ld.config.txt" - } - - /** - * Based on: https://android.googlesource.com/platform/bionic/+/30f2f05/linker/linker_config.cpp#182 - */ - @RequiresApi(26) - fun findLinkerSection(lines: Sequence, binaryRealPath: String): String { - for (untrimmed in lines) { - val line = untrimmed.substringBefore('#').trim() - if (line.isEmpty()) continue - if (line[0] == '[' && line.last() == ']') break - if (line.contains("+=")) continue - val chunks = line.split('=', limit = 2) - if (chunks.size < 2) { - Logger.me.w("warning: couldn't parse invalid format: $line (ignoring this line)") - continue - } - var (name, value) = chunks.map { it.trim() } - if (!name.startsWith("dir.")) { - Logger.me.w("warning: unexpected property name \"$name\", " + - "expected format dir. (ignoring this line)") - continue - } - if (value.endsWith('/')) value = value.dropLast(1) - if (value.isEmpty()) { - Logger.me.w("warning: property value is empty (ignoring this line)") - continue - } - try { - value = File(value).canonicalPath - } catch (e: IOException) { - Logger.me.i("warning: path \"$value\" couldn't be resolved: ${e.message}") - } - if (binaryRealPath.startsWith(value) && binaryRealPath[value.length] == '/') return name.substring(4) - } - throw IllegalArgumentException("No valid linker section found") - } - - val myExe get() = "/proc/${Process.myPid()}/exe" - val myExeCanonical get() = try { - File("/proc/self/exe").canonicalPath - } catch (e: IOException) { - Logger.me.i("warning: couldn't resolve self exe: ${e.message}") - "/system/bin/app_process" - } - - /** - * Try to guess whether enabling relocation would work best. - * It seems some Android 5-7 devices give random permission denials without relocation. - * See also VPNHotspot#173. - */ - val shouldRelocateHeuristics get() = Build.VERSION.SDK_INT < 26 || myExeCanonical.startsWith("/data/") - - /** - * To workaround Samsung's stupid kernel patch that prevents exec, we need to relocate exe outside of /data. - * See also: https://github.com/Chainfire/librootjava/issues/19 - * - * @return The script to be executed to perform relocation and the relocated binary path. - */ - fun relocateScript(token: String): Pair { - val script = StringBuilder() - val (baseDir, relocated) = if (Build.VERSION.SDK_INT < 29) "/dev" to "/dev/app_process_$token" else { - val apexPath = "/apex/$token" - script.appendLine("[ -d $apexPath ] || " + - "mkdir $apexPath && " + - // we need to mount a new tmpfs to override noexec flag - "mount -t tmpfs -o size=1M tmpfs $apexPath || exit 1") - // unfortunately native ld.config.txt only recognizes /data,/system,/system_ext as system directories; - // to link correctly, we need to add our path to the linker config too - val ldConfig = "$apexPath/etc/ld.config.txt" - val masterLdConfig = genericLdConfigFilePath - val section = try { - File(masterLdConfig).useLines { findLinkerSection(it, myExeCanonical) } - } catch (e: Exception) { - Logger.me.w("Failed to locate system section", e) - "system" - } - script.appendLine("[ -f $ldConfig ] || " + - "mkdir -p $apexPath/etc && " + - "echo dir.$section = $apexPath >$ldConfig && " + - "cat $masterLdConfig >>$ldConfig || exit 1") - "$apexPath/bin" to "$apexPath/bin/app_process" - } - script.appendLine("[ -f $relocated ] || " + - "mkdir -p $baseDir && " + - "cp $myExe $relocated && " + - "chmod 700 $relocated || exit 1") - return script to relocated - } - - /** - * Compute the shell script line that exec into the corresponding [clazz]. - * Extra params can be simply appended to the string. - */ - fun launchString(packageCodePath: String, clazz: String, appProcess: String, niceName: String? = null): String { - val debugParams = if (Debug.isDebuggerConnected()) when (Build.VERSION.SDK_INT) { - in 29..Int.MAX_VALUE -> "-XjdwpProvider:adbconnection" - 28 -> "-XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y -Xcompiler-option --debuggable" - else -> "-Xrunjdwp:transport=dt_android_adb,suspend=n,server=y -Xcompiler-option --debuggable" - } else "" - val extraParams = if (niceName != null) " --nice-name=$niceName" else "" - return "CLASSPATH=$packageCodePath exec $appProcess $debugParams /system/bin$extraParams $clazz" - } -} diff --git a/mobile/src/main/java/be/mygod/librootkotlinx/Logger.kt b/mobile/src/main/java/be/mygod/librootkotlinx/Logger.kt deleted file mode 100644 index 9207a290..00000000 --- a/mobile/src/main/java/be/mygod/librootkotlinx/Logger.kt +++ /dev/null @@ -1,27 +0,0 @@ -package be.mygod.librootkotlinx - -import android.util.Log - -interface Logger { - companion object { - /** - * Override this variable to change default behavior, - * which is to print to [android.util.Log] under tag "RootServer" except for [d]. - */ - @JvmStatic - var me = object : Logger { } - - private const val TAG = "RootServer" - } - - fun d(m: String?, t: Throwable? = null) { } - fun e(m: String?, t: Throwable? = null) { - Log.e(TAG, m, t) - } - fun i(m: String?, t: Throwable? = null) { - Log.i(TAG, m, t) - } - fun w(m: String?, t: Throwable? = null) { - Log.w(TAG, m, t) - } -} diff --git a/mobile/src/main/java/be/mygod/librootkotlinx/RootServer.kt b/mobile/src/main/java/be/mygod/librootkotlinx/RootServer.kt deleted file mode 100644 index 4c4b99aa..00000000 --- a/mobile/src/main/java/be/mygod/librootkotlinx/RootServer.kt +++ /dev/null @@ -1,503 +0,0 @@ -package be.mygod.librootkotlinx - -import android.content.Context -import android.os.Build -import android.os.Looper -import android.os.Parcelable -import android.os.RemoteException -import android.system.ErrnoException -import android.system.Os -import android.system.OsConstants -import androidx.collection.LongSparseArray -import androidx.collection.valueIterator -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* -import java.io.* -import java.util.* -import java.util.concurrent.CountDownLatch -import kotlin.system.exitProcess - -class RootServer { - private sealed class Callback(private val server: RootServer, private val index: Long, - protected val classLoader: ClassLoader?) { - var active = true - - abstract fun cancel() - abstract fun shouldRemove(result: Byte): Boolean - abstract operator fun invoke(input: DataInputStream, result: Byte) - fun sendClosed() = server.execute(CancelCommand(index)) - - private fun initException(targetClass: Class<*>, message: String): Throwable { - @Suppress("NAME_SHADOWING") - var targetClass = targetClass - while (true) { - try { - // try to find a message constructor - return targetClass.getDeclaredConstructor(String::class.java).newInstance(message) as Throwable - } catch (_: ReflectiveOperationException) { } - targetClass = targetClass.superclass - } - } - private fun makeRemoteException(cause: Throwable, message: String? = null) = - if (cause is CancellationException) cause else RemoteException(message).initCause(cause) - protected fun DataInputStream.readException(result: Byte) = when (result.toInt()) { - EX_GENERIC -> { - val message = readUTF() - val name = message.split(':', limit = 2)[0] - makeRemoteException(initException(try { - classLoader?.loadClass(name) - } catch (_: ClassNotFoundException) { - null - } ?: Class.forName(name), message), message) - } - EX_PARCELABLE -> makeRemoteException(readParcelable(classLoader) as Throwable) - EX_SERIALIZABLE -> makeRemoteException(readSerializable(classLoader) as Throwable) - else -> throw IllegalArgumentException("Unexpected result $result") - } - - class Ordinary(server: RootServer, index: Long, classLoader: ClassLoader?, - private val callback: CompletableDeferred) : Callback(server, index, classLoader) { - override fun cancel() = callback.cancel() - override fun shouldRemove(result: Byte) = true - override fun invoke(input: DataInputStream, result: Byte) { - if (result.toInt() == SUCCESS) callback.complete(input.readParcelable(classLoader)) - else callback.completeExceptionally(input.readException(result)) - } - } - - class Channel(server: RootServer, index: Long, classLoader: ClassLoader?, - private val channel: SendChannel) : Callback(server, index, classLoader) { - val finish: CompletableDeferred = CompletableDeferred() - override fun cancel() = finish.cancel() - override fun shouldRemove(result: Byte) = result.toInt() != SUCCESS - override fun invoke(input: DataInputStream, result: Byte) { - when (result.toInt()) { - SUCCESS -> channel.trySend(input.readParcelable(classLoader)).onClosed { - active = false - sendClosed() - finish.completeExceptionally(it - ?: ClosedSendChannelException("Channel was closed normally")) - return - }.onFailure { throw it!! } // the channel we are supporting should never block - CHANNEL_CONSUMED -> finish.complete(Unit) - else -> finish.completeExceptionally(input.readException(result)) - } - } - } - } - - class LaunchException(cause: Throwable) : RuntimeException("Failed to launch root daemon", cause) - class UnexpectedExitException : RemoteException("Root process exited unexpectedly") - - private lateinit var process: Process - /** - * Thread safety: needs to be protected by callbackLookup. - */ - private lateinit var output: DataOutputStream - - @Volatile - var active = false - private var counter = 0L - private var callbackListenerExit: Deferred? = null - private val callbackLookup = LongSparseArray() - - private fun readUnexpectedStderr(): String? { - if (!this::process.isInitialized) return null - Logger.me.d("Attempting to read stderr") - var available = process.errorStream.available() - return if (available <= 0) null else String(ByteArrayOutputStream().apply { - try { - while (available > 0) { - val bytes = ByteArray(available) - val len = process.errorStream.read(bytes) - if (len < 0) throw EOFException() // should not happen - write(bytes, 0, len) - available = process.errorStream.available() - } - } catch (e: IOException) { - Logger.me.w("Reading stderr was cut short", e) - } - }.toByteArray()) - } - - private fun BufferedReader.lookForToken(token: String) { - while (true) { - val line = readLine() ?: throw EOFException() - if (line.endsWith(token)) { - val extraLength = line.length - token.length - if (extraLength > 0) Logger.me.w(line.substring(0, extraLength)) - break - } - Logger.me.w(line) - } - } - private fun doInit(context: Context, shouldRelocate: Boolean, niceName: String) { - try { - val (reader, writer) = try { - process = ProcessBuilder("su").start() - val token1 = UUID.randomUUID().toString() - val writer = DataOutputStream(process.outputStream.buffered()) - writer.writeBytes("echo $token1\n") - writer.flush() - val reader = process.inputStream.bufferedReader() - reader.lookForToken(token1) - Logger.me.d("Root shell initialized") - reader to writer - } catch (e: Exception) { - throw NoShellException(e) - } - try { - val token2 = UUID.randomUUID().toString() - writer.writeBytes(if (shouldRelocate) { - val persistence = File(context.codeCacheDir, ".librootkotlinx-uuid") - val uuid = context.packageName + '@' + try { - persistence.readText() - } catch (_: FileNotFoundException) { - UUID.randomUUID().toString().also { persistence.writeText(it) } - } - val (script, relocated) = AppProcess.relocateScript(uuid) - script.appendLine(AppProcess.launchString(context.packageCodePath, RootServer::class.java.name, - relocated, niceName) + " $token2") - script.toString() - } else { - AppProcess.launchString(context.packageCodePath, RootServer::class.java.name, AppProcess.myExe, - niceName) + " $token2\n" - }) - writer.flush() - reader.lookForToken(token2) // wait for ready signal - } catch (e: Exception) { - throw LaunchException(e) - } - output = writer - require(!active) - active = true - Logger.me.d("Root server initialized") - } finally { - try { - readUnexpectedStderr()?.let { Logger.me.e(it) } - } catch (e: IOException) { - Logger.me.e("Failed to read from stderr", e) // avoid the real exception being swallowed - } - } - } - - private fun callbackSpin() { - val input = DataInputStream(process.inputStream.buffered()) - while (active) { - val index = try { - input.readLong() - } catch (_: EOFException) { - break - } - val result = input.readByte() - val callback = synchronized(callbackLookup) { - if (active) (callbackLookup[index] ?: error("Empty callback #$index")).also { - if (it.shouldRemove(result)) { - callbackLookup.remove(index) - it.active = false - } - } else null - } ?: break - Logger.me.d("Received callback #$index: $result") - callback(input, result) - } - } - - /** - * Initialize a RootServer synchronously, can throw a lot of exceptions. - * - * @param context Any [Context] from the app. - * @param shouldRelocate Whether app process should be copied first. See also [AppProcess.shouldRelocateHeuristics]. - * @param niceName Name to call the rooted Java process. - */ - suspend fun init(context: Context, shouldRelocate: Boolean = false, - niceName: String = "${context.packageName}:root") { - withContext(Dispatchers.IO) { doInit(context, shouldRelocate, niceName) } - callbackListenerExit = GlobalScope.async(Dispatchers.IO) { - val errorReader = async(Dispatchers.IO) { - try { - process.errorStream.bufferedReader().forEachLine(Logger.me::w) - } catch (_: IOException) { } - } - try { - callbackSpin() - if (active) throw UnexpectedExitException() - } catch (e: Throwable) { - process.destroy() - throw e - } finally { - Logger.me.d("Waiting for exit") - withContext(NonCancellable) { errorReader.await() } - process.waitFor() - closeInternal(true) - } - } - } - - /** - * Caller should check for active. - */ - private fun sendLocked(command: Parcelable) { - try { - output.writeParcelable(command) - output.flush() - } catch (e: IOException) { - if (e.isEBADF) throw CancellationException().initCause(e) else throw e - } catch (e: ErrnoException) { - if (e.errno == OsConstants.EPIPE) throw CancellationException().initCause(e) else throw e - } - Logger.me.d("Sent #$counter: $command") - counter++ - } - - fun execute(command: RootCommandOneWay) = synchronized(callbackLookup) { if (active) sendLocked(command) } - @Throws(RemoteException::class) - suspend inline fun execute(command: RootCommand) = - execute(command, T::class.java.classLoader) - @Throws(RemoteException::class) - suspend fun execute(command: RootCommand, classLoader: ClassLoader?): T { - val future = CompletableDeferred() - val callback = synchronized(callbackLookup) { - @Suppress("UNCHECKED_CAST") - val callback = Callback.Ordinary(this, counter, classLoader, future as CompletableDeferred) - if (active) { - callbackLookup.append(counter, callback) - sendLocked(command) - } else future.cancel() - callback - } - try { - return future.await() - } finally { - if (callback.active) callback.sendClosed() - callback.active = false - } - } - - @ExperimentalCoroutinesApi - @Throws(RemoteException::class) - inline fun create(command: RootCommandChannel, scope: CoroutineScope) = - create(command, scope, T::class.java.classLoader) - @ExperimentalCoroutinesApi - @Throws(RemoteException::class) - fun create(command: RootCommandChannel, scope: CoroutineScope, - classLoader: ClassLoader?) = scope.produce( - SupervisorJob(), command.capacity.also { - when (it) { - Channel.UNLIMITED, Channel.CONFLATED -> { } - else -> throw IllegalArgumentException("Unsupported channel capacity $it") - } - }) { - val callback = synchronized(callbackLookup) { - @Suppress("UNCHECKED_CAST") - val callback = Callback.Channel(this@RootServer, counter, classLoader, this as SendChannel) - if (active) { - callbackLookup.append(counter, callback) - sendLocked(command) - } else callback.finish.cancel() - callback - } - try { - callback.finish.await() - } finally { - if (callback.active) callback.sendClosed() - callback.active = false - } - } - - private fun closeInternal(fromWorker: Boolean = false) = synchronized(callbackLookup) { - if (active) { - active = false - Logger.me.d(if (fromWorker) "Shutting down from worker" else "Shutting down from client") - try { - sendLocked(Shutdown()) - output.close() - process.outputStream.close() - } catch (_: CancellationException) { - } catch (e: IOException) { - Logger.me.w("send Shutdown failed", e) - } - Logger.me.d("Client closed") - } - if (fromWorker) { - for (callback in callbackLookup.valueIterator()) callback.cancel() - callbackLookup.clear() - } - } - /** - * Shutdown the instance gracefully. - */ - suspend fun close() { - closeInternal() - val callbackListenerExit = callbackListenerExit ?: return - try { - withTimeout(10000) { callbackListenerExit.await() } - } catch (e: TimeoutCancellationException) { - Logger.me.w("Closing the instance has timed out", e) - if (Build.VERSION.SDK_INT < 26) process.destroy() else if (process.isAlive) process.destroyForcibly() - } catch (e: UnexpectedExitException) { - Logger.me.w(e.message) - } - } - - companion object { - private const val SUCCESS = 0 - private const val EX_GENERIC = 1 - private const val EX_PARCELABLE = 2 - private const val EX_SERIALIZABLE = 4 - private const val CHANNEL_CONSUMED = 3 - - private fun DataInputStream.readByteArray() = ByteArray(readInt()).also { readFully(it) } - - private inline fun DataInputStream.readParcelable( - classLoader: ClassLoader? = T::class.java.classLoader) = readByteArray().toParcelable(classLoader) - private fun DataOutputStream.writeParcelable(data: Parcelable?, parcelableFlags: Int = 0) { - val bytes = data.toByteArray(parcelableFlags) - writeInt(bytes.size) - write(bytes) - } - - private fun DataInputStream.readSerializable(classLoader: ClassLoader?) = - object : ObjectInputStream(ByteArrayInputStream(readByteArray())) { - override fun resolveClass(desc: ObjectStreamClass) = Class.forName(desc.name, false, classLoader) - }.readObject() - - @JvmStatic - fun main(args: Array) { - Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> - Logger.me.e("Uncaught exception from $thread", throwable) - throwable.printStackTrace() // stderr will be read by listener - exitProcess(1) - } - rootMain(args) - exitProcess(0) // there might be other non-daemon threads - } - - private fun DataOutputStream.pushThrowable(callback: Long, e: Throwable) { - writeLong(callback) - if (e is Parcelable) { - writeByte(EX_PARCELABLE) - writeParcelable(e) - } else try { - val bytes = ByteArrayOutputStream().apply { - ObjectOutputStream(this).use { it.writeObject(e) } - }.toByteArray() - writeByte(EX_SERIALIZABLE) - writeInt(bytes.size) - write(bytes) - } catch (_: NotSerializableException) { - writeByte(EX_GENERIC) - writeUTF(e.stackTraceToString()) - } - flush() - } - private fun DataOutputStream.pushResult(callback: Long, result: Parcelable?) { - writeLong(callback) - writeByte(SUCCESS) - writeParcelable(result) - flush() - } - - private fun rootMain(args: Array) { - require(args.isNotEmpty()) - val mainInitialized = CountDownLatch(1) - val main = Thread({ - @Suppress("DEPRECATION") - Looper.prepareMainLooper() - mainInitialized.countDown() - Looper.loop() - }, "main") - main.start() - val job = Job() - val defaultWorker by lazy { - mainInitialized.await() - CoroutineScope(Dispatchers.Main.immediate + job) - } - val callbackWorker by lazy { - mainInitialized.await() - Dispatchers.IO.limitedParallelism(1) - } - // access to cancellables shall be wrapped in defaultWorker - val cancellables = LongSparseArray<() -> Unit>() - - // thread safety: usage of output should be guarded by callbackWorker - val output = DataOutputStream(FileOutputStream(Os.dup(FileDescriptor.out)).buffered().apply { - // prevent future write attempts to System.out, possibly from Samsung changes (again) - Os.dup2(FileDescriptor.err, OsConstants.STDOUT_FILENO) - System.setOut(System.err) - val writer = writer() - writer.appendLine(args[0]) // echo ready signal - writer.flush() - }) - // thread safety: usage of input should be in main thread - val input = DataInputStream(System.`in`.buffered()) - var counter = 0L - Logger.me.d("Server entering main loop") - loop@ while (true) { - val command = try { - input.readParcelable(RootServer::class.java.classLoader) - } catch (_: EOFException) { - break - } - val callback = counter - Logger.me.d("Received #$callback: $command") - when (command) { - is CancelCommand -> defaultWorker.launch { cancellables[command.index]?.invoke() } - is RootCommandOneWay -> defaultWorker.launch { - try { - command.execute() - } catch (e: Throwable) { - Logger.me.e("Unexpected exception in RootCommandOneWay ($command.javaClass.simpleName)", e) - } - } - is RootCommand<*> -> { - val commandJob = Job() - defaultWorker.launch(commandJob) { - cancellables.append(callback) { commandJob.cancel() } - val result = try { - val result = command.execute(); - { output.pushResult(callback, result) } - } catch (e: Throwable) { - val worker = { output.pushThrowable(callback, e) } - worker - } finally { - cancellables.remove(callback) - } - withContext(callbackWorker + NonCancellable) { result() } - } - } - is RootCommandChannel<*> -> defaultWorker.launch { - val result = try { - coroutineScope { - command.create(this).also { - cancellables.append(callback) { it.cancel() } - }.consumeEach { result -> - withContext(callbackWorker) { output.pushResult(callback, result) } - } - }; - @Suppress("BlockingMethodInNonBlockingContext") { - output.writeByte(CHANNEL_CONSUMED) - output.writeLong(callback) - output.flush() - } - } catch (e: Throwable) { - val worker = { output.pushThrowable(callback, e) } - worker - } finally { - cancellables.remove(callback) - } - withContext(callbackWorker + NonCancellable) { result() } - } - is Shutdown -> break@loop - else -> throw IllegalArgumentException("Unrecognized input: $command") - } - counter++ - } - job.cancel() - Logger.me.d("Clean up initiated before exit. Jobs: ${job.children.joinToString()}") - if (runBlocking { withTimeoutOrNull(5000) { job.join() } } == null) { - Logger.me.w("Clean up timeout: ${job.children.joinToString()}") - } else Logger.me.d("Clean up finished, exiting") - } - } -} diff --git a/mobile/src/main/java/be/mygod/librootkotlinx/RootSession.kt b/mobile/src/main/java/be/mygod/librootkotlinx/RootSession.kt deleted file mode 100644 index e9a3689b..00000000 --- a/mobile/src/main/java/be/mygod/librootkotlinx/RootSession.kt +++ /dev/null @@ -1,108 +0,0 @@ -package be.mygod.librootkotlinx - -import kotlinx.coroutines.* -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import java.util.concurrent.TimeUnit -import kotlin.coroutines.CoroutineContext - -/** - * This object manages creation of [RootServer] and times them out automagically, with default timeout of 5 minutes. - */ -abstract class RootSession { - protected abstract suspend fun initServer(server: RootServer) - /** - * Timeout to close [RootServer] in milliseconds. - */ - protected open val timeout get() = TimeUnit.MINUTES.toMillis(5) - protected open val timeoutContext: CoroutineContext get() = Dispatchers.Default - - private val mutex = Mutex() - private var server: RootServer? = null - private var timeoutJob: Job? = null - private var usersCount = 0L - private var closePending = false - - private suspend fun ensureServerLocked(): RootServer { - server?.let { - if (it.active) return it - usersCount = 0 - closeLocked() - } - check(usersCount == 0L) { "Unexpected $server, $usersCount" } - val server = RootServer() - try { - initServer(server) - this.server = server - return server - } catch (e: Throwable) { - try { - server.close() - } catch (eClose: Throwable) { - e.addSuppressed(eClose) - } - throw e - } - } - - private suspend fun closeLocked() { - closePending = false - val server = server - this.server = null - server?.close() - } - private fun startTimeoutLocked() { - check(timeoutJob == null) - timeoutJob = GlobalScope.launch(timeoutContext, CoroutineStart.UNDISPATCHED) { - delay(timeout) - mutex.withLock { - check(usersCount == 0L) - timeoutJob = null - closeLocked() - } - } - } - private fun haltTimeoutLocked() { - timeoutJob?.cancel() - timeoutJob = null - } - - suspend fun acquire() = withContext(NonCancellable) { - mutex.withLock { - haltTimeoutLocked() - closePending = false - ensureServerLocked().also { ++usersCount } - } - } - suspend fun release(server: RootServer) = withContext(NonCancellable) { - mutex.withLock { - if (this@RootSession.server != server) return@withLock // outdated reference - require(usersCount > 0) - when { - !server.active -> { - usersCount = 0 - closeLocked() - return@withLock - } - --usersCount > 0L -> return@withLock - closePending -> closeLocked() - else -> startTimeoutLocked() - } - } - } - suspend inline fun use(block: (RootServer) -> T): T { - val server = acquire() - try { - return block(server) - } finally { - release(server) - } - } - - suspend fun closeExisting() = mutex.withLock { - if (usersCount > 0) closePending = true else { - haltTimeoutLocked() - closeLocked() - } - } -} diff --git a/mobile/src/main/java/be/mygod/librootkotlinx/ServerCommands.kt b/mobile/src/main/java/be/mygod/librootkotlinx/ServerCommands.kt deleted file mode 100644 index f5978ba1..00000000 --- a/mobile/src/main/java/be/mygod/librootkotlinx/ServerCommands.kt +++ /dev/null @@ -1,47 +0,0 @@ -package be.mygod.librootkotlinx - -import android.os.Parcelable -import androidx.annotation.MainThread -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.ReceiveChannel -import kotlinx.parcelize.Parcelize - -interface RootCommand : Parcelable { - /** - * If a throwable was thrown, it will be wrapped in RemoteException only if it implements [Parcelable]. - */ - @MainThread - suspend fun execute(): Result -} - -typealias RootCommandNoResult = RootCommand - -/** - * Execute a command and discards its result, even if an exception occurs. - * - * If you want to catch exception, use e.g. [RootCommandNoResult] and return null. - */ -interface RootCommandOneWay : Parcelable { - @MainThread - suspend fun execute() -} - -interface RootCommandChannel : Parcelable { - /** - * The capacity of the channel that is returned by [create] to be used by client. - * Only [Channel.UNLIMITED] and [Channel.CONFLATED] is supported for now to avoid blocking the entire connection. - */ - val capacity: Int get() = Channel.UNLIMITED - - @MainThread - fun create(scope: CoroutineScope): ReceiveChannel -} - -@Parcelize -internal data class CancelCommand(val index: Long) : RootCommandOneWay { - override suspend fun execute() = error("Internal implementation") -} - -@Parcelize -internal class Shutdown : Parcelable diff --git a/mobile/src/main/java/be/mygod/librootkotlinx/Utils.kt b/mobile/src/main/java/be/mygod/librootkotlinx/Utils.kt deleted file mode 100644 index ee8c1a60..00000000 --- a/mobile/src/main/java/be/mygod/librootkotlinx/Utils.kt +++ /dev/null @@ -1,257 +0,0 @@ -@file:JvmName("Utils") - -package be.mygod.librootkotlinx - -import android.annotation.SuppressLint -import android.content.Context -import android.os.Parcel -import android.os.Parcelable -import android.system.ErrnoException -import android.system.OsConstants -import android.util.* -import androidx.annotation.RequiresApi -import kotlinx.parcelize.Parcelize -import java.io.IOException -import java.util.* - -class NoShellException(cause: Throwable) : Exception("Root missing", cause) - -internal val currentInstructionSet by lazy { - val classVMRuntime = Class.forName("dalvik.system.VMRuntime") - val runtime = classVMRuntime.getDeclaredMethod("getRuntime").invoke(null) - classVMRuntime.getDeclaredMethod("getCurrentInstructionSet").invoke(runtime) as String -} - -private val classSystemProperties by lazy { Class.forName("android.os.SystemProperties") } -@get:RequiresApi(26) -internal val isVndkLite by lazy { - classSystemProperties.getDeclaredMethod("getBoolean", String::class.java, Boolean::class.java).invoke(null, - "ro.vndk.lite", false) as Boolean -} -@get:RequiresApi(26) -internal val vndkVersion by lazy { - classSystemProperties.getDeclaredMethod("get", String::class.java, String::class.java).invoke(null, - "ro.vndk.version", "") as String -} - -val systemContext by lazy { - val classActivityThread = Class.forName("android.app.ActivityThread") - val activityThread = classActivityThread.getMethod("systemMain").invoke(null) - classActivityThread.getMethod("getSystemContext").invoke(activityThread) as Context -} - -@Parcelize -data class ParcelableByte(val value: Byte) : Parcelable - -@Parcelize -data class ParcelableShort(val value: Short) : Parcelable - -@Parcelize -data class ParcelableInt(val value: Int) : Parcelable - -@Parcelize -data class ParcelableLong(val value: Long) : Parcelable - -@Parcelize -data class ParcelableFloat(val value: Float) : Parcelable - -@Parcelize -data class ParcelableDouble(val value: Double) : Parcelable - -@Parcelize -data class ParcelableBoolean(val value: Boolean) : Parcelable - -@Parcelize -data class ParcelableString(val value: String) : Parcelable - -@Parcelize -data class ParcelableByteArray(val value: ByteArray) : Parcelable { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as ParcelableByteArray - - if (!value.contentEquals(other.value)) return false - - return true - } - - override fun hashCode(): Int { - return value.contentHashCode() - } -} - -@Parcelize -data class ParcelableIntArray(val value: IntArray) : Parcelable { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as ParcelableIntArray - - if (!value.contentEquals(other.value)) return false - - return true - } - - override fun hashCode(): Int { - return value.contentHashCode() - } -} - -@Parcelize -data class ParcelableLongArray(val value: LongArray) : Parcelable { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as ParcelableLongArray - - if (!value.contentEquals(other.value)) return false - - return true - } - - override fun hashCode(): Int { - return value.contentHashCode() - } -} - -@Parcelize -data class ParcelableFloatArray(val value: FloatArray) : Parcelable { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as ParcelableFloatArray - - if (!value.contentEquals(other.value)) return false - - return true - } - - override fun hashCode(): Int { - return value.contentHashCode() - } -} - -@Parcelize -data class ParcelableDoubleArray(val value: DoubleArray) : Parcelable { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as ParcelableDoubleArray - - if (!value.contentEquals(other.value)) return false - - return true - } - - override fun hashCode(): Int { - return value.contentHashCode() - } -} - -@Parcelize -data class ParcelableBooleanArray(val value: BooleanArray) : Parcelable { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as ParcelableBooleanArray - - if (!value.contentEquals(other.value)) return false - - return true - } - - override fun hashCode(): Int { - return value.contentHashCode() - } -} - -@Parcelize -data class ParcelableStringArray(val value: Array) : Parcelable { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as ParcelableStringArray - - if (!value.contentEquals(other.value)) return false - - return true - } - - override fun hashCode(): Int { - return value.contentHashCode() - } -} - -@Parcelize -data class ParcelableStringList(val value: List) : Parcelable - -@Parcelize -data class ParcelableSparseIntArray(val value: SparseIntArray) : Parcelable - -@Parcelize -data class ParcelableSparseLongArray(val value: SparseLongArray) : Parcelable - -@Parcelize -data class ParcelableSparseBooleanArray(val value: SparseBooleanArray) : Parcelable - -@Parcelize -data class ParcelableCharSequence(val value: CharSequence) : Parcelable - -@Parcelize -data class ParcelableSize(val value: Size) : Parcelable - -@Parcelize -data class ParcelableSizeF(val value: SizeF) : Parcelable - -@Parcelize -data class ParcelableArray(val value: Array) : Parcelable { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as ParcelableArray - - if (!value.contentEquals(other.value)) return false - - return true - } - - override fun hashCode(): Int { - return value.contentHashCode() - } -} - -@Parcelize -data class ParcelableList(val value: List) : Parcelable - -@SuppressLint("Recycle") -inline fun useParcel(block: (Parcel) -> T) = Parcel.obtain().run { - try { - block(this) - } finally { - recycle() - } -} - -fun Parcelable?.toByteArray(parcelableFlags: Int = 0) = useParcel { p -> - p.writeParcelable(this, parcelableFlags) - p.marshall() -} -inline fun ByteArray.toParcelable(classLoader: ClassLoader? = T::class.java.classLoader) = - useParcel { p -> - p.unmarshall(this, 0, size) - p.setDataPosition(0) - p.readParcelable(classLoader) - } - -// Stream closed caused in NullOutputStream -val IOException.isEBADF get() = (cause as? ErrnoException)?.errno == OsConstants.EBADF || - message?.lowercase(Locale.ENGLISH) == "stream closed"