Skip to content

Repository files navigation

RootThread

An Android library for executing arbitrary code in a privileged root process via Binder IPC, with first-class support for both Java (Future, ExecutorService) and Kotlin (coroutines, Flow, DSL).


Table of Contents


How It Works

┌─────────────────────────────────┐ ┌──────────────────────────────────┐
│ App Process │ │ Root Process │
│ │ │ │
│ RootCallable ──► Kryo ──► pipe ├──────► │ pipe ──► Kryo ──► RootCallable │
│ │ IPC │ │ │
│ result ◄── Kryo ◄── pipe ◄┤ │ call() │
│ │ │ │ │
│ │ │ result ──► Kryo ──► pipe ──► │
└─────────────────────────────────┘ └──────────────────────────────────┘
  1. The caller serializes a RootCallable via Kryo into a ParcelFileDescriptor write pipe.
  2. The read-end of that pipe and the write-end of a result pipe are handed to RootThreadService over Binder.
  3. The root service deserializes and executes the callable on a daemon thread.
  4. The result is serialized back into the result pipe.
  5. The caller reads the result pipe and resumes.

Parcelable objects are serialized using Android's own Parcel mechanism instead of Kryo to avoid cross-process reference-ID divergence.


Setup

JitPack

Add the JitPack repository to your settings file:

// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven("https://jitpack.io")
}
}

Dependencies

// build.gradle.kts
dependencies {
implementation("com.github.MMRLApp.RootThread:thread:<version>")
// KSP code generation (optional — see KSP Code Generation below)
ksp("com.github.MMRLApp.RootThread:thread-ksp:<version>")
}

Lifecycle binding

The simplest setup — attach the observer once in onCreate:

// KotlinclassMainActivity : AppCompatActivity() {
overridefunonCreate(savedInstanceState:Bundle?) {
super.onCreate(savedInstanceState)
addRootThread(this) // binds onStart, unbinds onStop
}
}
// JavapublicclassMainActivityextendsComponentActivity {
@OverrideprotectedvoidonCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
getLifecycle().addObserver(newRootThreadLifecycleObserver(this));
}
}

KSP Code Generation

The optional thread-ksp artifact provides a KSP processor that generates boilerplate-free RootCallable wrappers from annotated functions.

KSP Setup

// build.gradle.kts
plugins {
id("com.google.devtools.ksp") version "<ksp-version>"
}
dependencies {
implementation("com.github.MMRLApp.RootThread:thread:<version>")
ksp("com.github.MMRLApp.RootThread:thread-ksp:<version>")
}

@RootFunction

Annotate any top-level function or companion object function that should run in the root process. If the function accepts a RootOptions parameter, it is automatically injected from call(options) at runtime and excluded from the constructor.

// Top-level
@RootFunction
funloadModules(): List<Module> {
returnFile("/data/adb/modules").listFiles()
?.filter { it.isDirectory }
?.mapNotNull { parseModule(it) }
.orEmpty()
}
// With RootOptions (if you use RootOptions, always place it first)
@RootFunction
funreadFile(options:RootOptions, path:String): String {
returnFile(path).readText()
}
// Inside a companion objectclassModulesRepository {
companionobject {
@RootFunction
funloadModules(): List<Module> { ... }
}
}

Generated API

For each annotated function the processor generates a file under dev.mmrlx.threading:

// Generated: dev/mmrlx/threading/RootedLoadModules.ktpublicclassRootedLoadModules : RootCallable<List<Module>>, Serializable {
overridefuncall(options:RootOptions): List<Module> = loadModules()
}
// Extension on RootScope — the public API surfacefun RootScope.loadModules(): RootCallable<List<Module>> =RootedLoadModules()

Usage:

// Suspend call via companionval modules =RootedLoadModules().asThread()
// As a Flow via companion (if you compose)val modules by RootedLoadModules().asFlow().collectAsState(emptyList())

Core Concepts

RootCallable

RootCallable<T> is a @FunctionalInterface (usable as a lambda in both Java and Kotlin) that represents work to execute in the root process.

val callable =RootCallable<String> {
File("/proc/version").readText()
}
RootCallable<String> callable = options -> newFile("/proc/version").readText();

RootConsumer

RootConsumer<T, R> is a receiver-scoped variant — it receives a typed object from the caller's process and returns a result. Used by the rootBlocking receiver extension and RootThreadExtensions.rootBlocking.

val consumer =RootConsumer<PackageManager, List<PackageInfo>> { pm ->
pm.getInstalledPackages(0)
}

Serialisation rules

Because callables are serialized across a process boundary, they must be Kryo-compatible:

✅ Safe to capture❌ Never capture
Primitives (Int, Boolean, String, …)Context / Activity / Fragment
Parcelable objectsView or any UI object
Plain data classesNon-serialisable lambdas or anonymous classes
EnumsViewModel, LiveData, Flow
Serializable objectsBinder objects (other than via Parcel)

API Reference — Kotlin

rootThread { }

Suspends the coroutine, executes the block in the root process, and resumes with the result. Dispatches onto Dispatchers.IO automatically.

// In any suspend function:val kernel = rootThread { File("/proc/version").readText() }
val hasSu = rootThread { File("/system/bin/su").exists() }

Signature:

suspendfun <T> rootThread(block:RootCallable<T>): T

Throws:IOException on IPC or remote failure.


T.rootThread { }

Receiver-scoped variant. Passes this into the root process as the first argument of the callable.

val packages = packageManager.rootThread { pm ->
pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
}

Signature:

suspendfun <T, R> T.rootThread(block:RootConsumer<T, R>): R

Throws:IOException on IPC or remote failure.


rootLaunch / rootAsync

Launch-style wrappers for use inside a CoroutineScope. Exceptions propagate through the scope's job like any other coroutine failure.

// Fire and forget
viewModelScope.rootLaunch {
Runtime.getRuntime().exec("chmod 777 /data/local/tmp/file")
}
// With a result via Deferredval deferred = viewModelScope.rootAsync { readRootDatabase() }
val rows = deferred.await()

Signatures:

fun CoroutineScope.rootLaunch(block:RootCallable<Unit>): Jobfun <T> CoroutineScope.rootAsync(block:RootCallable<T>): Deferred<T>

rootFlow { }

Returns a cold Flow<T> that executes the callable on each collection and emits a single value.

rootFlow { File("/proc/version").readText() }
.onEach { version -> textView.text = version }
.launchIn(lifecycleScope)
// Combine with other operators
rootFlow { getPrivilegedData() }
.map { it.transform() }
.catch { e -> showError(e) }
.flowOn(Dispatchers.IO)
.collect { result -> updateUi(result) }

Signature:

fun <T> rootFlow(block:RootCallable<T>): Flow<T>

rootBlocking { }

Executes the block in the root process, blocking the calling thread. Must not be called on the main thread.

// On a background thread / Worker / HandlerThread:val exists = rootBlocking { File("/system/bin/su").exists() }

Signature:

@Throws(IOException::class, InterruptedException::class)
fun <T> rootBlocking(block:RootCallable<T>): T?

T.rootBlocking { }

Receiver-scoped blocking variant.

val packages = packageManager.rootBlocking { pm ->
pm.getInstalledPackages(0)
}

Signature:

@Throws(IOException::class, InterruptedException::class)
fun <T, R> T.rootBlocking(block:RootConsumer<T, R>): R?

rootBlocking with timeout

Blocking execution with a deadline. Throws TimeoutException if the root process does not respond in time.

val result = rootBlocking(5, TimeUnit.SECONDS) { readHeavyRootFile() }

Signature:

@Throws(IOException::class, InterruptedException::class, TimeoutException::class)
fun <T> rootBlocking(timeout:Long, unit:TimeUnit, block:RootCallable<T>): T?

rootBlock DSL

Groups multiple root calls into a structured block. Each exec { } call is an independent IPC round-trip but they share a readable sequential scope.

val data = rootBlock {
val hasSu = exec { File("/system/bin/su").exists() }
val kernel = exec { File("/proc/version").readText() }
val modules = exec { File("/data/adb/modules").listFiles()?.size ?:0 }
mapOf(
"hasSu" to hasSu,
"kernel" to kernel,
"modules" to modules,
)
}

Signatures:

suspendfun <T> rootBlock(block:suspendRootBlockScope.() ->T): TclassRootBlockScope {
suspendfun <T> exec(block:RootCallable<T>): T
}

rootThreadCatching / rootBlockingCatching

Result-wrapped variants for railway-oriented error handling. Never throw — failures are delivered as Result.failure.

// Suspend
rootThreadCatching { riskyRootOperation() }
.onSuccess { result -> updateUi(result) }
.onFailure { error ->Log.e(TAG, "Root failed", error) }
// Blocking (off main thread)val result = rootBlockingCatching { File("/proc/version").readText() }
if (result.isSuccess) {
textView.text = result.getOrNull()
}

Signatures:

suspendfun <T> rootThreadCatching(block:RootCallable<T>): Result<T>
fun <T> rootBlockingCatching(block:RootCallable<T>): Result<T>

RootThread { } invoke syntax

Syntactic sugar allowing RootThread to be called like a function inside any suspend context.

// Equivalent to rootThread { ... }val result =RootThread { doPrivilegedWork() }

Future.awaitRoot()

Suspends a coroutine until a Future<T> (returned by RootThread.submit()) completes. Implemented with suspendCancellableCoroutineno kotlinx-coroutines-jdk8 dependency required.

  • Runs Future.get() on Dispatchers.IO so the main thread is never blocked.
  • Cancels the Future if the coroutine is cancelled.
  • Unwraps ExecutionException so callers see the real cause.
val future =RootThread.submit<String> { readPrivilegedFile() }
// Cancel if needed:
future.cancel(true)
// Or await in a coroutine:val result = future.awaitRoot()

Signature:

suspendfun <T> Future<T>.awaitRoot(): T

API Reference — Java

RootThread.submit()

Submits a callable to the root process and returns a Future<T> immediately. The future resolves with the result or fails with an IOException.

Future<Boolean> future = RootThread.submit(() ->
newFile("/system/bin/su").exists()
);
// Optional cancellationfuture.cancel(true);
// Join elsewhere (not on main thread)booleanresult = future.get(5, TimeUnit.SECONDS);

Signature:

publicstatic <T> Future<T> submit(@NonNullRootCallable<T> callable)

RootThread.executeBlocking()

Submits a callable and blocks the calling thread until the result is available. Must not be called on the main thread.

executorService.execute(() -> {
try {
Stringkernel = RootThread.executeBlocking(
() -> newString(Files.readAllBytes(Paths.get("/proc/version")))
);
runOnUiThread(() -> textView.setText(kernel));
} catch (IOException | InterruptedExceptione) {
Log.e(TAG, "Root IPC failed", e);
}
});

Signature:

publicstatic <T> TexecuteBlocking(@NonNullRootCallable<T> callable)
throwsIOException, InterruptedException

RootThread.executeBlocking() with timeout

try {
Booleanexists = RootThread.executeBlocking(
() -> newFile("/system/bin/su").exists(),
5, TimeUnit.SECONDS
);
} catch (TimeoutExceptione) {
Log.e(TAG, "Root process timed out");
} catch (IOException | InterruptedExceptione) {
Log.e(TAG, "IPC error", e);
}

Signature:

publicstatic <T> TexecuteBlocking(
@NonNullRootCallable<T> callable,
longtimeout,
@NonNullTimeUnitunit
) throwsIOException, InterruptedException, TimeoutException

RootThreadExtensions.rootLaunch()

Async fire-and-forget with an optional callback delivered on a specified Executor (or the main thread by default).

// Callback on main thread (default)RootThreadExtensions.rootLaunch(
() -> readRootData(),
newRootThreadExtensions.RootCallback<String>() {
@OverridepublicvoidonSuccess(Stringresult) {
textView.setText(result); // main thread
}
@OverridepublicvoidonFailure(Throwableerror) {
Log.e(TAG, "Failed", error);
}
}
);
// Callback on a custom executorExecutordbExecutor = Executors.newSingleThreadExecutor();
RootThreadExtensions.rootLaunch(
() -> readRootDatabase(),
newRootThreadExtensions.RootCallback<List<Row>>() {
@OverridepublicvoidonSuccess(List<Row> rows) {
dao.insertAll(rows); // already on dbExecutor
}
@OverridepublicvoidonFailure(Throwablee) { /* handle */ }
},
dbExecutor
);

Signatures:

// Callback on main threadpublicstatic <T> Future<T> rootLaunch(
@NonNullRootCallable<T> callable,
@NullableRootCallback<T> callback
)
// Callback on custom executorpublicstatic <T> Future<T> rootLaunch(
@NonNullRootCallable<T> callable,
@NullableRootCallback<T> callback,
@NonNullExecutorexecutor
)
// Fire and forget, no callbackpublicstaticFuture<Void> rootLaunch(@NonNullRootCallable<Void> callable)

RootThreadExtensions.rootBlocking()

Receiver-scoped blocking execution. Equivalent to the Kotlin T.rootBlocking { } extension.

PackageManagerpm = getPackageManager();
executorService.execute(() -> {
try {
List<PackageInfo> packages = RootThreadExtensions.rootBlocking(
pm,
manager -> manager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
);
runOnUiThread(() -> adapter.setData(packages));
} catch (IOException | InterruptedExceptione) {
Log.e(TAG, "Failed", e);
}
});

Signatures:

publicstatic <T, R> RrootBlocking(
@NonNullTreceiver,
@NonNullRootConsumer<T, R> block
) throwsIOException, InterruptedExceptionpublicstatic <T, R> RrootBlocking(
@NonNullTreceiver,
@NonNullRootConsumer<T, R> block,
longtimeout,
@NonNullTimeUnitunit
) throwsIOException, InterruptedException, TimeoutException

RootThreadExtensions.addRootThread()

Lifecycle-aware bind/unbind as a static method (Java equivalent of the Kotlin extension).

RootThreadExtensions.addRootThread(this, context);

Lifecycle

RootThreadLifecycleObserver

The preferred Java approach. Stores applicationContext internally to prevent leaks.

// ActivitygetLifecycle().addObserver(newRootThreadLifecycleObserver(this));
// FragmentgetViewLifecycleOwner().getLifecycle()
.addObserver(newRootThreadLifecycleObserver(requireContext()));
// Kotlin extension — equivalent one-liner
addRootThread(requireContext())

Manual bind / unbind

For cases where lifecycle integration is not appropriate (services, background components):

RootThread.bind(context); // call when readyRootThread.unbind(); // call when done
RootThread.bind(context)
RootThread.unbind()

Threading model

LayerThread
Caller (Kotlin)Any — dispatched to Dispatchers.IO internally
Caller (Java async)RootThread cached executor (RootThread-IPC threads)
Caller (Java blocking)Caller's thread — must not be main thread
Root serviceBinder thread (returns immediately); work on RootThread-Worker daemon thread

The root service spawns a new named daemon thread per call so the Binder thread is never parked, eliminating ANR risk.


FD ownership contract

createPipe() → [callableRead, callableWrite]
createPipe() → [resultRead, resultWrite ]
Caller:
write callable → callableWrite → (AutoCloseOutputStream closes it, sends EOF)
svc.execute(callableRead, resultWrite) ← service owns these two from here
read result ← resultRead ← caller owns this until done
On error before execute():
caller closes all four FDs

Serialisation internals

KryoManager is a pre-configured Kryo instance:

SettingValue
Registration requiredfalse (class names are written to the stream)
Referencestrue (handles cyclic graphs in non-Parcelable objects)
Instantiation strategyDefaultInstantiatorStrategy + StdInstantiatorStrategy (no-arg constructor not required)
Parcelable serialiserCustom ParcelableSerializer — uses Parcel.marshall() / unmarshall()

A freshKryoManager instance is used for each write and each read, keeping reference tables completely independent across the pipe boundary.


Error handling

Error scenarioBehaviour
Remote callable throwsException is serialised and re-thrown as IOException("Remote exception", cause)
IPC write failsIOException("IPC write/execute failed", cause)
Deserialisation fails in rootIOException("Deserialisation failed in root process", cause)
Root service disconnectsCompletableFuture is replaced; next call blocks until reconnect
Coroutine cancelledFuture.cancel(true) is called; CancellationException propagates normally
InterruptedExceptionThread interrupt flag is restored; wrapped as CancellationException in coroutine context

Rules and gotchas

Serialisation

  • RootCallable and RootConsumer lambdas must be Kryo-serializable. Do not capture Context, View, or any non-serializable object.
  • Prefer capturing primitive values or Parcelable objects. For complex objects, pass them as the receiver via T.rootThread { } or rootBlocking(receiver) { }.

Threading

  • Never call executeBlocking or rootBlocking on the main thread — they block the calling thread.
  • Prefer rootThread { } (Kotlin suspend) or rootLaunch (Java async) in UI code.

Lifecycle

  • Always use RootThreadLifecycleObserver or addRootThread() to ensure the service is unbound when the component stops. Failing to unbind leaks the root process connection.
  • RootThreadLifecycleObserver stores applicationContext internally — passing an Activity context is safe.

Cancellation

  • rootLaunch / rootAsync respect coroutine cancellation: the underlying Future is canceled and the root worker thread is interrupted.
  • rootFlow is cold — collection starts a new IPC round-trip each time.

About

An Android library for executing arbitrary code in a privileged root process via Binder IPC, with first-class support for both Java (Future, ExecutorService) and Kotlin (coroutines, Flow, DSL).

Resources

Stars

13 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - MMRLApp/RootThread: An Android library for executing arbitrary code in a privileged root process via Binder IPC, with first-class support for both Java (Future, ExecutorService) and Kotlin (coroutines, Flow, DSL). · GitHub
Skip to content

Repository files navigation

RootThread

An Android library for executing arbitrary code in a privileged root process via Binder IPC, with first-class support for both Java (Future, ExecutorService) and Kotlin (coroutines, Flow, DSL).


Table of Contents


How It Works

┌─────────────────────────────────┐ ┌──────────────────────────────────┐
│ App Process │ │ Root Process │
│ │ │ │
│ RootCallable ──► Kryo ──► pipe ├──────► │ pipe ──► Kryo ──► RootCallable │
│ │ IPC │ │ │
│ result ◄── Kryo ◄── pipe ◄┤ │ call() │
│ │ │ │ │
│ │ │ result ──► Kryo ──► pipe ──► │
└─────────────────────────────────┘ └──────────────────────────────────┘
  1. The caller serializes a RootCallable via Kryo into a ParcelFileDescriptor write pipe.
  2. The read-end of that pipe and the write-end of a result pipe are handed to RootThreadService over Binder.
  3. The root service deserializes and executes the callable on a daemon thread.
  4. The result is serialized back into the result pipe.
  5. The caller reads the result pipe and resumes.

Parcelable objects are serialized using Android's own Parcel mechanism instead of Kryo to avoid cross-process reference-ID divergence.


Setup

JitPack

Add the JitPack repository to your settings file:

// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven("https://jitpack.io")
}
}

Dependencies

// build.gradle.kts
dependencies {
implementation("com.github.MMRLApp.RootThread:thread:<version>")
// KSP code generation (optional — see KSP Code Generation below)
ksp("com.github.MMRLApp.RootThread:thread-ksp:<version>")
}

Lifecycle binding

The simplest setup — attach the observer once in onCreate:

// KotlinclassMainActivity : AppCompatActivity() {
overridefunonCreate(savedInstanceState:Bundle?) {
super.onCreate(savedInstanceState)
addRootThread(this) // binds onStart, unbinds onStop
}
}
// JavapublicclassMainActivityextendsComponentActivity {
@OverrideprotectedvoidonCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
getLifecycle().addObserver(newRootThreadLifecycleObserver(this));
}
}

KSP Code Generation

The optional thread-ksp artifact provides a KSP processor that generates boilerplate-free RootCallable wrappers from annotated functions.

KSP Setup

// build.gradle.kts
plugins {
id("com.google.devtools.ksp") version "<ksp-version>"
}
dependencies {
implementation("com.github.MMRLApp.RootThread:thread:<version>")
ksp("com.github.MMRLApp.RootThread:thread-ksp:<version>")
}

@RootFunction

Annotate any top-level function or companion object function that should run in the root process. If the function accepts a RootOptions parameter, it is automatically injected from call(options) at runtime and excluded from the constructor.

// Top-level
@RootFunction
funloadModules(): List<Module> {
returnFile("/data/adb/modules").listFiles()
?.filter { it.isDirectory }
?.mapNotNull { parseModule(it) }
.orEmpty()
}
// With RootOptions (if you use RootOptions, always place it first)
@RootFunction
funreadFile(options:RootOptions, path:String): String {
returnFile(path).readText()
}
// Inside a companion objectclassModulesRepository {
companionobject {
@RootFunction
funloadModules(): List<Module> { ... }
}
}

Generated API

For each annotated function the processor generates a file under dev.mmrlx.threading:

// Generated: dev/mmrlx/threading/RootedLoadModules.ktpublicclassRootedLoadModules : RootCallable<List<Module>>, Serializable {
overridefuncall(options:RootOptions): List<Module> = loadModules()
}
// Extension on RootScope — the public API surfacefun RootScope.loadModules(): RootCallable<List<Module>> =RootedLoadModules()

Usage:

// Suspend call via companionval modules =RootedLoadModules().asThread()
// As a Flow via companion (if you compose)val modules by RootedLoadModules().asFlow().collectAsState(emptyList())

Core Concepts

RootCallable

RootCallable<T> is a @FunctionalInterface (usable as a lambda in both Java and Kotlin) that represents work to execute in the root process.

val callable =RootCallable<String> {
File("/proc/version").readText()
}
RootCallable<String> callable = options -> newFile("/proc/version").readText();

RootConsumer

RootConsumer<T, R> is a receiver-scoped variant — it receives a typed object from the caller's process and returns a result. Used by the rootBlocking receiver extension and RootThreadExtensions.rootBlocking.

val consumer =RootConsumer<PackageManager, List<PackageInfo>> { pm ->
pm.getInstalledPackages(0)
}

Serialisation rules

Because callables are serialized across a process boundary, they must be Kryo-compatible:

✅ Safe to capture❌ Never capture
Primitives (Int, Boolean, String, …)Context / Activity / Fragment
Parcelable objectsView or any UI object
Plain data classesNon-serialisable lambdas or anonymous classes
EnumsViewModel, LiveData, Flow
Serializable objectsBinder objects (other than via Parcel)

API Reference — Kotlin

rootThread { }

Suspends the coroutine, executes the block in the root process, and resumes with the result. Dispatches onto Dispatchers.IO automatically.

// In any suspend function:val kernel = rootThread { File("/proc/version").readText() }
val hasSu = rootThread { File("/system/bin/su").exists() }

Signature:

suspendfun <T> rootThread(block:RootCallable<T>): T

Throws:IOException on IPC or remote failure.


T.rootThread { }

Receiver-scoped variant. Passes this into the root process as the first argument of the callable.

val packages = packageManager.rootThread { pm ->
pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
}

Signature:

suspendfun <T, R> T.rootThread(block:RootConsumer<T, R>): R

Throws:IOException on IPC or remote failure.


rootLaunch / rootAsync

Launch-style wrappers for use inside a CoroutineScope. Exceptions propagate through the scope's job like any other coroutine failure.

// Fire and forget
viewModelScope.rootLaunch {
Runtime.getRuntime().exec("chmod 777 /data/local/tmp/file")
}
// With a result via Deferredval deferred = viewModelScope.rootAsync { readRootDatabase() }
val rows = deferred.await()

Signatures:

fun CoroutineScope.rootLaunch(block:RootCallable<Unit>): Jobfun <T> CoroutineScope.rootAsync(block:RootCallable<T>): Deferred<T>

rootFlow { }

Returns a cold Flow<T> that executes the callable on each collection and emits a single value.

rootFlow { File("/proc/version").readText() }
.onEach { version -> textView.text = version }
.launchIn(lifecycleScope)
// Combine with other operators
rootFlow { getPrivilegedData() }
.map { it.transform() }
.catch { e -> showError(e) }
.flowOn(Dispatchers.IO)
.collect { result -> updateUi(result) }

Signature:

fun <T> rootFlow(block:RootCallable<T>): Flow<T>

rootBlocking { }

Executes the block in the root process, blocking the calling thread. Must not be called on the main thread.

// On a background thread / Worker / HandlerThread:val exists = rootBlocking { File("/system/bin/su").exists() }

Signature:

@Throws(IOException::class, InterruptedException::class)
fun <T> rootBlocking(block:RootCallable<T>): T?

T.rootBlocking { }

Receiver-scoped blocking variant.

val packages = packageManager.rootBlocking { pm ->
pm.getInstalledPackages(0)
}

Signature:

@Throws(IOException::class, InterruptedException::class)
fun <T, R> T.rootBlocking(block:RootConsumer<T, R>): R?

rootBlocking with timeout

Blocking execution with a deadline. Throws TimeoutException if the root process does not respond in time.

val result = rootBlocking(5, TimeUnit.SECONDS) { readHeavyRootFile() }

Signature:

@Throws(IOException::class, InterruptedException::class, TimeoutException::class)
fun <T> rootBlocking(timeout:Long, unit:TimeUnit, block:RootCallable<T>): T?

rootBlock DSL

Groups multiple root calls into a structured block. Each exec { } call is an independent IPC round-trip but they share a readable sequential scope.

val data = rootBlock {
val hasSu = exec { File("/system/bin/su").exists() }
val kernel = exec { File("/proc/version").readText() }
val modules = exec { File("/data/adb/modules").listFiles()?.size ?:0 }
mapOf(
"hasSu" to hasSu,
"kernel" to kernel,
"modules" to modules,
)
}

Signatures:

suspendfun <T> rootBlock(block:suspendRootBlockScope.() ->T): TclassRootBlockScope {
suspendfun <T> exec(block:RootCallable<T>): T
}

rootThreadCatching / rootBlockingCatching

Result-wrapped variants for railway-oriented error handling. Never throw — failures are delivered as Result.failure.

// Suspend
rootThreadCatching { riskyRootOperation() }
.onSuccess { result -> updateUi(result) }
.onFailure { error ->Log.e(TAG, "Root failed", error) }
// Blocking (off main thread)val result = rootBlockingCatching { File("/proc/version").readText() }
if (result.isSuccess) {
textView.text = result.getOrNull()
}

Signatures:

suspendfun <T> rootThreadCatching(block:RootCallable<T>): Result<T>
fun <T> rootBlockingCatching(block:RootCallable<T>): Result<T>

RootThread { } invoke syntax

Syntactic sugar allowing RootThread to be called like a function inside any suspend context.

// Equivalent to rootThread { ... }val result =RootThread { doPrivilegedWork() }

Future.awaitRoot()

Suspends a coroutine until a Future<T> (returned by RootThread.submit()) completes. Implemented with suspendCancellableCoroutineno kotlinx-coroutines-jdk8 dependency required.

  • Runs Future.get() on Dispatchers.IO so the main thread is never blocked.
  • Cancels the Future if the coroutine is cancelled.
  • Unwraps ExecutionException so callers see the real cause.
val future =RootThread.submit<String> { readPrivilegedFile() }
// Cancel if needed:
future.cancel(true)
// Or await in a coroutine:val result = future.awaitRoot()

Signature:

suspendfun <T> Future<T>.awaitRoot(): T

API Reference — Java

RootThread.submit()

Submits a callable to the root process and returns a Future<T> immediately. The future resolves with the result or fails with an IOException.

Future<Boolean> future = RootThread.submit(() ->
newFile("/system/bin/su").exists()
);
// Optional cancellationfuture.cancel(true);
// Join elsewhere (not on main thread)booleanresult = future.get(5, TimeUnit.SECONDS);

Signature:

publicstatic <T> Future<T> submit(@NonNullRootCallable<T> callable)

RootThread.executeBlocking()

Submits a callable and blocks the calling thread until the result is available. Must not be called on the main thread.

executorService.execute(() -> {
try {
Stringkernel = RootThread.executeBlocking(
() -> newString(Files.readAllBytes(Paths.get("/proc/version")))
);
runOnUiThread(() -> textView.setText(kernel));
} catch (IOException | InterruptedExceptione) {
Log.e(TAG, "Root IPC failed", e);
}
});

Signature:

publicstatic <T> TexecuteBlocking(@NonNullRootCallable<T> callable)
throwsIOException, InterruptedException

RootThread.executeBlocking() with timeout

try {
Booleanexists = RootThread.executeBlocking(
() -> newFile("/system/bin/su").exists(),
5, TimeUnit.SECONDS
);
} catch (TimeoutExceptione) {
Log.e(TAG, "Root process timed out");
} catch (IOException | InterruptedExceptione) {
Log.e(TAG, "IPC error", e);
}

Signature:

publicstatic <T> TexecuteBlocking(
@NonNullRootCallable<T> callable,
longtimeout,
@NonNullTimeUnitunit
) throwsIOException, InterruptedException, TimeoutException

RootThreadExtensions.rootLaunch()

Async fire-and-forget with an optional callback delivered on a specified Executor (or the main thread by default).

// Callback on main thread (default)RootThreadExtensions.rootLaunch(
() -> readRootData(),
newRootThreadExtensions.RootCallback<String>() {
@OverridepublicvoidonSuccess(Stringresult) {
textView.setText(result); // main thread
}
@OverridepublicvoidonFailure(Throwableerror) {
Log.e(TAG, "Failed", error);
}
}
);
// Callback on a custom executorExecutordbExecutor = Executors.newSingleThreadExecutor();
RootThreadExtensions.rootLaunch(
() -> readRootDatabase(),
newRootThreadExtensions.RootCallback<List<Row>>() {
@OverridepublicvoidonSuccess(List<Row> rows) {
dao.insertAll(rows); // already on dbExecutor
}
@OverridepublicvoidonFailure(Throwablee) { /* handle */ }
},
dbExecutor
);

Signatures:

// Callback on main threadpublicstatic <T> Future<T> rootLaunch(
@NonNullRootCallable<T> callable,
@NullableRootCallback<T> callback
)
// Callback on custom executorpublicstatic <T> Future<T> rootLaunch(
@NonNullRootCallable<T> callable,
@NullableRootCallback<T> callback,
@NonNullExecutorexecutor
)
// Fire and forget, no callbackpublicstaticFuture<Void> rootLaunch(@NonNullRootCallable<Void> callable)

RootThreadExtensions.rootBlocking()

Receiver-scoped blocking execution. Equivalent to the Kotlin T.rootBlocking { } extension.

PackageManagerpm = getPackageManager();
executorService.execute(() -> {
try {
List<PackageInfo> packages = RootThreadExtensions.rootBlocking(
pm,
manager -> manager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
);
runOnUiThread(() -> adapter.setData(packages));
} catch (IOException | InterruptedExceptione) {
Log.e(TAG, "Failed", e);
}
});

Signatures:

publicstatic <T, R> RrootBlocking(
@NonNullTreceiver,
@NonNullRootConsumer<T, R> block
) throwsIOException, InterruptedExceptionpublicstatic <T, R> RrootBlocking(
@NonNullTreceiver,
@NonNullRootConsumer<T, R> block,
longtimeout,
@NonNullTimeUnitunit
) throwsIOException, InterruptedException, TimeoutException

RootThreadExtensions.addRootThread()

Lifecycle-aware bind/unbind as a static method (Java equivalent of the Kotlin extension).

RootThreadExtensions.addRootThread(this, context);

Lifecycle

RootThreadLifecycleObserver

The preferred Java approach. Stores applicationContext internally to prevent leaks.

// ActivitygetLifecycle().addObserver(newRootThreadLifecycleObserver(this));
// FragmentgetViewLifecycleOwner().getLifecycle()
.addObserver(newRootThreadLifecycleObserver(requireContext()));
// Kotlin extension — equivalent one-liner
addRootThread(requireContext())

Manual bind / unbind

For cases where lifecycle integration is not appropriate (services, background components):

RootThread.bind(context); // call when readyRootThread.unbind(); // call when done
RootThread.bind(context)
RootThread.unbind()

Threading model

LayerThread
Caller (Kotlin)Any — dispatched to Dispatchers.IO internally
Caller (Java async)RootThread cached executor (RootThread-IPC threads)
Caller (Java blocking)Caller's thread — must not be main thread
Root serviceBinder thread (returns immediately); work on RootThread-Worker daemon thread

The root service spawns a new named daemon thread per call so the Binder thread is never parked, eliminating ANR risk.


FD ownership contract

createPipe() → [callableRead, callableWrite]
createPipe() → [resultRead, resultWrite ]
Caller:
write callable → callableWrite → (AutoCloseOutputStream closes it, sends EOF)
svc.execute(callableRead, resultWrite) ← service owns these two from here
read result ← resultRead ← caller owns this until done
On error before execute():
caller closes all four FDs

Serialisation internals

KryoManager is a pre-configured Kryo instance:

SettingValue
Registration requiredfalse (class names are written to the stream)
Referencestrue (handles cyclic graphs in non-Parcelable objects)
Instantiation strategyDefaultInstantiatorStrategy + StdInstantiatorStrategy (no-arg constructor not required)
Parcelable serialiserCustom ParcelableSerializer — uses Parcel.marshall() / unmarshall()

A freshKryoManager instance is used for each write and each read, keeping reference tables completely independent across the pipe boundary.


Error handling

Error scenarioBehaviour
Remote callable throwsException is serialised and re-thrown as IOException("Remote exception", cause)
IPC write failsIOException("IPC write/execute failed", cause)
Deserialisation fails in rootIOException("Deserialisation failed in root process", cause)
Root service disconnectsCompletableFuture is replaced; next call blocks until reconnect
Coroutine cancelledFuture.cancel(true) is called; CancellationException propagates normally
InterruptedExceptionThread interrupt flag is restored; wrapped as CancellationException in coroutine context

Rules and gotchas

Serialisation

  • RootCallable and RootConsumer lambdas must be Kryo-serializable. Do not capture Context, View, or any non-serializable object.
  • Prefer capturing primitive values or Parcelable objects. For complex objects, pass them as the receiver via T.rootThread { } or rootBlocking(receiver) { }.

Threading

  • Never call executeBlocking or rootBlocking on the main thread — they block the calling thread.
  • Prefer rootThread { } (Kotlin suspend) or rootLaunch (Java async) in UI code.

Lifecycle

  • Always use RootThreadLifecycleObserver or addRootThread() to ensure the service is unbound when the component stops. Failing to unbind leaks the root process connection.
  • RootThreadLifecycleObserver stores applicationContext internally — passing an Activity context is safe.

Cancellation

  • rootLaunch / rootAsync respect coroutine cancellation: the underlying Future is canceled and the root worker thread is interrupted.
  • rootFlow is cold — collection starts a new IPC round-trip each time.

About

An Android library for executing arbitrary code in a privileged root process via Binder IPC, with first-class support for both Java (Future, ExecutorService) and Kotlin (coroutines, Flow, DSL).

Resources

Stars

13 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - MMRLApp/RootThread: An Android library for executing arbitrary code in a privileged root process via Binder IPC, with first-class support for both Java (Future, ExecutorService) and Kotlin (coroutines, Flow, DSL). · GitHub
Skip to content

Repository files navigation

RootThread

An Android library for executing arbitrary code in a privileged root process via Binder IPC, with first-class support for both Java (Future, ExecutorService) and Kotlin (coroutines, Flow, DSL).


Table of Contents


How It Works

┌─────────────────────────────────┐ ┌──────────────────────────────────┐
│ App Process │ │ Root Process │
│ │ │ │
│ RootCallable ──► Kryo ──► pipe ├──────► │ pipe ──► Kryo ──► RootCallable │
│ │ IPC │ │ │
│ result ◄── Kryo ◄── pipe ◄┤ │ call() │
│ │ │ │ │
│ │ │ result ──► Kryo ──► pipe ──► │
└─────────────────────────────────┘ └──────────────────────────────────┘
  1. The caller serializes a RootCallable via Kryo into a ParcelFileDescriptor write pipe.
  2. The read-end of that pipe and the write-end of a result pipe are handed to RootThreadService over Binder.
  3. The root service deserializes and executes the callable on a daemon thread.
  4. The result is serialized back into the result pipe.
  5. The caller reads the result pipe and resumes.

Parcelable objects are serialized using Android's own Parcel mechanism instead of Kryo to avoid cross-process reference-ID divergence.


Setup

JitPack

Add the JitPack repository to your settings file:

// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven("https://jitpack.io")
}
}

Dependencies

// build.gradle.kts
dependencies {
implementation("com.github.MMRLApp.RootThread:thread:<version>")
// KSP code generation (optional — see KSP Code Generation below)
ksp("com.github.MMRLApp.RootThread:thread-ksp:<version>")
}

Lifecycle binding

The simplest setup — attach the observer once in onCreate:

// KotlinclassMainActivity : AppCompatActivity() {
overridefunonCreate(savedInstanceState:Bundle?) {
super.onCreate(savedInstanceState)
addRootThread(this) // binds onStart, unbinds onStop
}
}
// JavapublicclassMainActivityextendsComponentActivity {
@OverrideprotectedvoidonCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
getLifecycle().addObserver(newRootThreadLifecycleObserver(this));
}
}

KSP Code Generation

The optional thread-ksp artifact provides a KSP processor that generates boilerplate-free RootCallable wrappers from annotated functions.

KSP Setup

// build.gradle.kts
plugins {
id("com.google.devtools.ksp") version "<ksp-version>"
}
dependencies {
implementation("com.github.MMRLApp.RootThread:thread:<version>")
ksp("com.github.MMRLApp.RootThread:thread-ksp:<version>")
}

@RootFunction

Annotate any top-level function or companion object function that should run in the root process. If the function accepts a RootOptions parameter, it is automatically injected from call(options) at runtime and excluded from the constructor.

// Top-level
@RootFunction
funloadModules(): List<Module> {
returnFile("/data/adb/modules").listFiles()
?.filter { it.isDirectory }
?.mapNotNull { parseModule(it) }
.orEmpty()
}
// With RootOptions (if you use RootOptions, always place it first)
@RootFunction
funreadFile(options:RootOptions, path:String): String {
returnFile(path).readText()
}
// Inside a companion objectclassModulesRepository {
companionobject {
@RootFunction
funloadModules(): List<Module> { ... }
}
}

Generated API

For each annotated function the processor generates a file under dev.mmrlx.threading:

// Generated: dev/mmrlx/threading/RootedLoadModules.ktpublicclassRootedLoadModules : RootCallable<List<Module>>, Serializable {
overridefuncall(options:RootOptions): List<Module> = loadModules()
}
// Extension on RootScope — the public API surfacefun RootScope.loadModules(): RootCallable<List<Module>> =RootedLoadModules()

Usage:

// Suspend call via companionval modules =RootedLoadModules().asThread()
// As a Flow via companion (if you compose)val modules by RootedLoadModules().asFlow().collectAsState(emptyList())

Core Concepts

RootCallable

RootCallable<T> is a @FunctionalInterface (usable as a lambda in both Java and Kotlin) that represents work to execute in the root process.

val callable =RootCallable<String> {
File("/proc/version").readText()
}
RootCallable<String> callable = options -> newFile("/proc/version").readText();

RootConsumer

RootConsumer<T, R> is a receiver-scoped variant — it receives a typed object from the caller's process and returns a result. Used by the rootBlocking receiver extension and RootThreadExtensions.rootBlocking.

val consumer =RootConsumer<PackageManager, List<PackageInfo>> { pm ->
pm.getInstalledPackages(0)
}

Serialisation rules

Because callables are serialized across a process boundary, they must be Kryo-compatible:

✅ Safe to capture❌ Never capture
Primitives (Int, Boolean, String, …)Context / Activity / Fragment
Parcelable objectsView or any UI object
Plain data classesNon-serialisable lambdas or anonymous classes
EnumsViewModel, LiveData, Flow
Serializable objectsBinder objects (other than via Parcel)

API Reference — Kotlin

rootThread { }

Suspends the coroutine, executes the block in the root process, and resumes with the result. Dispatches onto Dispatchers.IO automatically.

// In any suspend function:val kernel = rootThread { File("/proc/version").readText() }
val hasSu = rootThread { File("/system/bin/su").exists() }

Signature:

suspendfun <T> rootThread(block:RootCallable<T>): T

Throws:IOException on IPC or remote failure.


T.rootThread { }

Receiver-scoped variant. Passes this into the root process as the first argument of the callable.

val packages = packageManager.rootThread { pm ->
pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
}

Signature:

suspendfun <T, R> T.rootThread(block:RootConsumer<T, R>): R

Throws:IOException on IPC or remote failure.


rootLaunch / rootAsync

Launch-style wrappers for use inside a CoroutineScope. Exceptions propagate through the scope's job like any other coroutine failure.

// Fire and forget
viewModelScope.rootLaunch {
Runtime.getRuntime().exec("chmod 777 /data/local/tmp/file")
}
// With a result via Deferredval deferred = viewModelScope.rootAsync { readRootDatabase() }
val rows = deferred.await()

Signatures:

fun CoroutineScope.rootLaunch(block:RootCallable<Unit>): Jobfun <T> CoroutineScope.rootAsync(block:RootCallable<T>): Deferred<T>

rootFlow { }

Returns a cold Flow<T> that executes the callable on each collection and emits a single value.

rootFlow { File("/proc/version").readText() }
.onEach { version -> textView.text = version }
.launchIn(lifecycleScope)
// Combine with other operators
rootFlow { getPrivilegedData() }
.map { it.transform() }
.catch { e -> showError(e) }
.flowOn(Dispatchers.IO)
.collect { result -> updateUi(result) }

Signature:

fun <T> rootFlow(block:RootCallable<T>): Flow<T>

rootBlocking { }

Executes the block in the root process, blocking the calling thread. Must not be called on the main thread.

// On a background thread / Worker / HandlerThread:val exists = rootBlocking { File("/system/bin/su").exists() }

Signature:

@Throws(IOException::class, InterruptedException::class)
fun <T> rootBlocking(block:RootCallable<T>): T?

T.rootBlocking { }

Receiver-scoped blocking variant.

val packages = packageManager.rootBlocking { pm ->
pm.getInstalledPackages(0)
}

Signature:

@Throws(IOException::class, InterruptedException::class)
fun <T, R> T.rootBlocking(block:RootConsumer<T, R>): R?

rootBlocking with timeout

Blocking execution with a deadline. Throws TimeoutException if the root process does not respond in time.

val result = rootBlocking(5, TimeUnit.SECONDS) { readHeavyRootFile() }

Signature:

@Throws(IOException::class, InterruptedException::class, TimeoutException::class)
fun <T> rootBlocking(timeout:Long, unit:TimeUnit, block:RootCallable<T>): T?

rootBlock DSL

Groups multiple root calls into a structured block. Each exec { } call is an independent IPC round-trip but they share a readable sequential scope.

val data = rootBlock {
val hasSu = exec { File("/system/bin/su").exists() }
val kernel = exec { File("/proc/version").readText() }
val modules = exec { File("/data/adb/modules").listFiles()?.size ?:0 }
mapOf(
"hasSu" to hasSu,
"kernel" to kernel,
"modules" to modules,
)
}

Signatures:

suspendfun <T> rootBlock(block:suspendRootBlockScope.() ->T): TclassRootBlockScope {
suspendfun <T> exec(block:RootCallable<T>): T
}

rootThreadCatching / rootBlockingCatching

Result-wrapped variants for railway-oriented error handling. Never throw — failures are delivered as Result.failure.

// Suspend
rootThreadCatching { riskyRootOperation() }
.onSuccess { result -> updateUi(result) }
.onFailure { error ->Log.e(TAG, "Root failed", error) }
// Blocking (off main thread)val result = rootBlockingCatching { File("/proc/version").readText() }
if (result.isSuccess) {
textView.text = result.getOrNull()
}

Signatures:

suspendfun <T> rootThreadCatching(block:RootCallable<T>): Result<T>
fun <T> rootBlockingCatching(block:RootCallable<T>): Result<T>

RootThread { } invoke syntax

Syntactic sugar allowing RootThread to be called like a function inside any suspend context.

// Equivalent to rootThread { ... }val result =RootThread { doPrivilegedWork() }

Future.awaitRoot()

Suspends a coroutine until a Future<T> (returned by RootThread.submit()) completes. Implemented with suspendCancellableCoroutineno kotlinx-coroutines-jdk8 dependency required.

  • Runs Future.get() on Dispatchers.IO so the main thread is never blocked.
  • Cancels the Future if the coroutine is cancelled.
  • Unwraps ExecutionException so callers see the real cause.
val future =RootThread.submit<String> { readPrivilegedFile() }
// Cancel if needed:
future.cancel(true)
// Or await in a coroutine:val result = future.awaitRoot()

Signature:

suspendfun <T> Future<T>.awaitRoot(): T

API Reference — Java

RootThread.submit()

Submits a callable to the root process and returns a Future<T> immediately. The future resolves with the result or fails with an IOException.

Future<Boolean> future = RootThread.submit(() ->
newFile("/system/bin/su").exists()
);
// Optional cancellationfuture.cancel(true);
// Join elsewhere (not on main thread)booleanresult = future.get(5, TimeUnit.SECONDS);

Signature:

publicstatic <T> Future<T> submit(@NonNullRootCallable<T> callable)

RootThread.executeBlocking()

Submits a callable and blocks the calling thread until the result is available. Must not be called on the main thread.

executorService.execute(() -> {
try {
Stringkernel = RootThread.executeBlocking(
() -> newString(Files.readAllBytes(Paths.get("/proc/version")))
);
runOnUiThread(() -> textView.setText(kernel));
} catch (IOException | InterruptedExceptione) {
Log.e(TAG, "Root IPC failed", e);
}
});

Signature:

publicstatic <T> TexecuteBlocking(@NonNullRootCallable<T> callable)
throwsIOException, InterruptedException

RootThread.executeBlocking() with timeout

try {
Booleanexists = RootThread.executeBlocking(
() -> newFile("/system/bin/su").exists(),
5, TimeUnit.SECONDS
);
} catch (TimeoutExceptione) {
Log.e(TAG, "Root process timed out");
} catch (IOException | InterruptedExceptione) {
Log.e(TAG, "IPC error", e);
}

Signature:

publicstatic <T> TexecuteBlocking(
@NonNullRootCallable<T> callable,
longtimeout,
@NonNullTimeUnitunit
) throwsIOException, InterruptedException, TimeoutException

RootThreadExtensions.rootLaunch()

Async fire-and-forget with an optional callback delivered on a specified Executor (or the main thread by default).

// Callback on main thread (default)RootThreadExtensions.rootLaunch(
() -> readRootData(),
newRootThreadExtensions.RootCallback<String>() {
@OverridepublicvoidonSuccess(Stringresult) {
textView.setText(result); // main thread
}
@OverridepublicvoidonFailure(Throwableerror) {
Log.e(TAG, "Failed", error);
}
}
);
// Callback on a custom executorExecutordbExecutor = Executors.newSingleThreadExecutor();
RootThreadExtensions.rootLaunch(
() -> readRootDatabase(),
newRootThreadExtensions.RootCallback<List<Row>>() {
@OverridepublicvoidonSuccess(List<Row> rows) {
dao.insertAll(rows); // already on dbExecutor
}
@OverridepublicvoidonFailure(Throwablee) { /* handle */ }
},
dbExecutor
);

Signatures:

// Callback on main threadpublicstatic <T> Future<T> rootLaunch(
@NonNullRootCallable<T> callable,
@NullableRootCallback<T> callback
)
// Callback on custom executorpublicstatic <T> Future<T> rootLaunch(
@NonNullRootCallable<T> callable,
@NullableRootCallback<T> callback,
@NonNullExecutorexecutor
)
// Fire and forget, no callbackpublicstaticFuture<Void> rootLaunch(@NonNullRootCallable<Void> callable)

RootThreadExtensions.rootBlocking()

Receiver-scoped blocking execution. Equivalent to the Kotlin T.rootBlocking { } extension.

PackageManagerpm = getPackageManager();
executorService.execute(() -> {
try {
List<PackageInfo> packages = RootThreadExtensions.rootBlocking(
pm,
manager -> manager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
);
runOnUiThread(() -> adapter.setData(packages));
} catch (IOException | InterruptedExceptione) {
Log.e(TAG, "Failed", e);
}
});

Signatures:

publicstatic <T, R> RrootBlocking(
@NonNullTreceiver,
@NonNullRootConsumer<T, R> block
) throwsIOException, InterruptedExceptionpublicstatic <T, R> RrootBlocking(
@NonNullTreceiver,
@NonNullRootConsumer<T, R> block,
longtimeout,
@NonNullTimeUnitunit
) throwsIOException, InterruptedException, TimeoutException

RootThreadExtensions.addRootThread()

Lifecycle-aware bind/unbind as a static method (Java equivalent of the Kotlin extension).

RootThreadExtensions.addRootThread(this, context);

Lifecycle

RootThreadLifecycleObserver

The preferred Java approach. Stores applicationContext internally to prevent leaks.

// ActivitygetLifecycle().addObserver(newRootThreadLifecycleObserver(this));
// FragmentgetViewLifecycleOwner().getLifecycle()
.addObserver(newRootThreadLifecycleObserver(requireContext()));
// Kotlin extension — equivalent one-liner
addRootThread(requireContext())

Manual bind / unbind

For cases where lifecycle integration is not appropriate (services, background components):

RootThread.bind(context); // call when readyRootThread.unbind(); // call when done
RootThread.bind(context)
RootThread.unbind()

Threading model

LayerThread
Caller (Kotlin)Any — dispatched to Dispatchers.IO internally
Caller (Java async)RootThread cached executor (RootThread-IPC threads)
Caller (Java blocking)Caller's thread — must not be main thread
Root serviceBinder thread (returns immediately); work on RootThread-Worker daemon thread

The root service spawns a new named daemon thread per call so the Binder thread is never parked, eliminating ANR risk.


FD ownership contract

createPipe() → [callableRead, callableWrite]
createPipe() → [resultRead, resultWrite ]
Caller:
write callable → callableWrite → (AutoCloseOutputStream closes it, sends EOF)
svc.execute(callableRead, resultWrite) ← service owns these two from here
read result ← resultRead ← caller owns this until done
On error before execute():
caller closes all four FDs

Serialisation internals

KryoManager is a pre-configured Kryo instance:

SettingValue
Registration requiredfalse (class names are written to the stream)
Referencestrue (handles cyclic graphs in non-Parcelable objects)
Instantiation strategyDefaultInstantiatorStrategy + StdInstantiatorStrategy (no-arg constructor not required)
Parcelable serialiserCustom ParcelableSerializer — uses Parcel.marshall() / unmarshall()

A freshKryoManager instance is used for each write and each read, keeping reference tables completely independent across the pipe boundary.


Error handling

Error scenarioBehaviour
Remote callable throwsException is serialised and re-thrown as IOException("Remote exception", cause)
IPC write failsIOException("IPC write/execute failed", cause)
Deserialisation fails in rootIOException("Deserialisation failed in root process", cause)
Root service disconnectsCompletableFuture is replaced; next call blocks until reconnect
Coroutine cancelledFuture.cancel(true) is called; CancellationException propagates normally
InterruptedExceptionThread interrupt flag is restored; wrapped as CancellationException in coroutine context

Rules and gotchas

Serialisation

  • RootCallable and RootConsumer lambdas must be Kryo-serializable. Do not capture Context, View, or any non-serializable object.
  • Prefer capturing primitive values or Parcelable objects. For complex objects, pass them as the receiver via T.rootThread { } or rootBlocking(receiver) { }.

Threading

  • Never call executeBlocking or rootBlocking on the main thread — they block the calling thread.
  • Prefer rootThread { } (Kotlin suspend) or rootLaunch (Java async) in UI code.

Lifecycle

  • Always use RootThreadLifecycleObserver or addRootThread() to ensure the service is unbound when the component stops. Failing to unbind leaks the root process connection.
  • RootThreadLifecycleObserver stores applicationContext internally — passing an Activity context is safe.

Cancellation

  • rootLaunch / rootAsync respect coroutine cancellation: the underlying Future is canceled and the root worker thread is interrupted.
  • rootFlow is cold — collection starts a new IPC round-trip each time.

About

An Android library for executing arbitrary code in a privileged root process via Binder IPC, with first-class support for both Java (Future, ExecutorService) and Kotlin (coroutines, Flow, DSL).

Resources

Stars

13 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - MMRLApp/RootThread: An Android library for executing arbitrary code in a privileged root process via Binder IPC, with first-class support for both Java (Future, ExecutorService) and Kotlin (coroutines, Flow, DSL). · GitHub
Skip to content

Repository files navigation

RootThread

An Android library for executing arbitrary code in a privileged root process via Binder IPC, with first-class support for both Java (Future, ExecutorService) and Kotlin (coroutines, Flow, DSL).


Table of Contents


How It Works

┌─────────────────────────────────┐ ┌──────────────────────────────────┐
│ App Process │ │ Root Process │
│ │ │ │
│ RootCallable ──► Kryo ──► pipe ├──────► │ pipe ──► Kryo ──► RootCallable │
│ │ IPC │ │ │
│ result ◄── Kryo ◄── pipe ◄┤ │ call() │
│ │ │ │ │
│ │ │ result ──► Kryo ──► pipe ──► │
└─────────────────────────────────┘ └──────────────────────────────────┘
  1. The caller serializes a RootCallable via Kryo into a ParcelFileDescriptor write pipe.
  2. The read-end of that pipe and the write-end of a result pipe are handed to RootThreadService over Binder.
  3. The root service deserializes and executes the callable on a daemon thread.
  4. The result is serialized back into the result pipe.
  5. The caller reads the result pipe and resumes.

Parcelable objects are serialized using Android's own Parcel mechanism instead of Kryo to avoid cross-process reference-ID divergence.


Setup

JitPack

Add the JitPack repository to your settings file:

// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven("https://jitpack.io")
}
}

Dependencies

// build.gradle.kts
dependencies {
implementation("com.github.MMRLApp.RootThread:thread:<version>")
// KSP code generation (optional — see KSP Code Generation below)
ksp("com.github.MMRLApp.RootThread:thread-ksp:<version>")
}

Lifecycle binding

The simplest setup — attach the observer once in onCreate:

// KotlinclassMainActivity : AppCompatActivity() {
overridefunonCreate(savedInstanceState:Bundle?) {
super.onCreate(savedInstanceState)
addRootThread(this) // binds onStart, unbinds onStop
}
}
// JavapublicclassMainActivityextendsComponentActivity {
@OverrideprotectedvoidonCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
getLifecycle().addObserver(newRootThreadLifecycleObserver(this));
}
}

KSP Code Generation

The optional thread-ksp artifact provides a KSP processor that generates boilerplate-free RootCallable wrappers from annotated functions.

KSP Setup

// build.gradle.kts
plugins {
id("com.google.devtools.ksp") version "<ksp-version>"
}
dependencies {
implementation("com.github.MMRLApp.RootThread:thread:<version>")
ksp("com.github.MMRLApp.RootThread:thread-ksp:<version>")
}

@RootFunction

Annotate any top-level function or companion object function that should run in the root process. If the function accepts a RootOptions parameter, it is automatically injected from call(options) at runtime and excluded from the constructor.

// Top-level
@RootFunction
funloadModules(): List<Module> {
returnFile("/data/adb/modules").listFiles()
?.filter { it.isDirectory }
?.mapNotNull { parseModule(it) }
.orEmpty()
}
// With RootOptions (if you use RootOptions, always place it first)
@RootFunction
funreadFile(options:RootOptions, path:String): String {
returnFile(path).readText()
}
// Inside a companion objectclassModulesRepository {
companionobject {
@RootFunction
funloadModules(): List<Module> { ... }
}
}

Generated API

For each annotated function the processor generates a file under dev.mmrlx.threading:

// Generated: dev/mmrlx/threading/RootedLoadModules.ktpublicclassRootedLoadModules : RootCallable<List<Module>>, Serializable {
overridefuncall(options:RootOptions): List<Module> = loadModules()
}
// Extension on RootScope — the public API surfacefun RootScope.loadModules(): RootCallable<List<Module>> =RootedLoadModules()

Usage:

// Suspend call via companionval modules =RootedLoadModules().asThread()
// As a Flow via companion (if you compose)val modules by RootedLoadModules().asFlow().collectAsState(emptyList())

Core Concepts

RootCallable

RootCallable<T> is a @FunctionalInterface (usable as a lambda in both Java and Kotlin) that represents work to execute in the root process.

val callable =RootCallable<String> {
File("/proc/version").readText()
}
RootCallable<String> callable = options -> newFile("/proc/version").readText();

RootConsumer

RootConsumer<T, R> is a receiver-scoped variant — it receives a typed object from the caller's process and returns a result. Used by the rootBlocking receiver extension and RootThreadExtensions.rootBlocking.

val consumer =RootConsumer<PackageManager, List<PackageInfo>> { pm ->
pm.getInstalledPackages(0)
}

Serialisation rules

Because callables are serialized across a process boundary, they must be Kryo-compatible:

✅ Safe to capture❌ Never capture
Primitives (Int, Boolean, String, …)Context / Activity / Fragment
Parcelable objectsView or any UI object
Plain data classesNon-serialisable lambdas or anonymous classes
EnumsViewModel, LiveData, Flow
Serializable objectsBinder objects (other than via Parcel)

API Reference — Kotlin

rootThread { }

Suspends the coroutine, executes the block in the root process, and resumes with the result. Dispatches onto Dispatchers.IO automatically.

// In any suspend function:val kernel = rootThread { File("/proc/version").readText() }
val hasSu = rootThread { File("/system/bin/su").exists() }

Signature:

suspendfun <T> rootThread(block:RootCallable<T>): T

Throws:IOException on IPC or remote failure.


T.rootThread { }

Receiver-scoped variant. Passes this into the root process as the first argument of the callable.

val packages = packageManager.rootThread { pm ->
pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
}

Signature:

suspendfun <T, R> T.rootThread(block:RootConsumer<T, R>): R

Throws:IOException on IPC or remote failure.


rootLaunch / rootAsync

Launch-style wrappers for use inside a CoroutineScope. Exceptions propagate through the scope's job like any other coroutine failure.

// Fire and forget
viewModelScope.rootLaunch {
Runtime.getRuntime().exec("chmod 777 /data/local/tmp/file")
}
// With a result via Deferredval deferred = viewModelScope.rootAsync { readRootDatabase() }
val rows = deferred.await()

Signatures:

fun CoroutineScope.rootLaunch(block:RootCallable<Unit>): Jobfun <T> CoroutineScope.rootAsync(block:RootCallable<T>): Deferred<T>

rootFlow { }

Returns a cold Flow<T> that executes the callable on each collection and emits a single value.

rootFlow { File("/proc/version").readText() }
.onEach { version -> textView.text = version }
.launchIn(lifecycleScope)
// Combine with other operators
rootFlow { getPrivilegedData() }
.map { it.transform() }
.catch { e -> showError(e) }
.flowOn(Dispatchers.IO)
.collect { result -> updateUi(result) }

Signature:

fun <T> rootFlow(block:RootCallable<T>): Flow<T>

rootBlocking { }

Executes the block in the root process, blocking the calling thread. Must not be called on the main thread.

// On a background thread / Worker / HandlerThread:val exists = rootBlocking { File("/system/bin/su").exists() }

Signature:

@Throws(IOException::class, InterruptedException::class)
fun <T> rootBlocking(block:RootCallable<T>): T?

T.rootBlocking { }

Receiver-scoped blocking variant.

val packages = packageManager.rootBlocking { pm ->
pm.getInstalledPackages(0)
}

Signature:

@Throws(IOException::class, InterruptedException::class)
fun <T, R> T.rootBlocking(block:RootConsumer<T, R>): R?

rootBlocking with timeout

Blocking execution with a deadline. Throws TimeoutException if the root process does not respond in time.

val result = rootBlocking(5, TimeUnit.SECONDS) { readHeavyRootFile() }

Signature:

@Throws(IOException::class, InterruptedException::class, TimeoutException::class)
fun <T> rootBlocking(timeout:Long, unit:TimeUnit, block:RootCallable<T>): T?

rootBlock DSL

Groups multiple root calls into a structured block. Each exec { } call is an independent IPC round-trip but they share a readable sequential scope.

val data = rootBlock {
val hasSu = exec { File("/system/bin/su").exists() }
val kernel = exec { File("/proc/version").readText() }
val modules = exec { File("/data/adb/modules").listFiles()?.size ?:0 }
mapOf(
"hasSu" to hasSu,
"kernel" to kernel,
"modules" to modules,
)
}

Signatures:

suspendfun <T> rootBlock(block:suspendRootBlockScope.() ->T): TclassRootBlockScope {
suspendfun <T> exec(block:RootCallable<T>): T
}

rootThreadCatching / rootBlockingCatching

Result-wrapped variants for railway-oriented error handling. Never throw — failures are delivered as Result.failure.

// Suspend
rootThreadCatching { riskyRootOperation() }
.onSuccess { result -> updateUi(result) }
.onFailure { error ->Log.e(TAG, "Root failed", error) }
// Blocking (off main thread)val result = rootBlockingCatching { File("/proc/version").readText() }
if (result.isSuccess) {
textView.text = result.getOrNull()
}

Signatures:

suspendfun <T> rootThreadCatching(block:RootCallable<T>): Result<T>
fun <T> rootBlockingCatching(block:RootCallable<T>): Result<T>

RootThread { } invoke syntax

Syntactic sugar allowing RootThread to be called like a function inside any suspend context.

// Equivalent to rootThread { ... }val result =RootThread { doPrivilegedWork() }

Future.awaitRoot()

Suspends a coroutine until a Future<T> (returned by RootThread.submit()) completes. Implemented with suspendCancellableCoroutineno kotlinx-coroutines-jdk8 dependency required.

  • Runs Future.get() on Dispatchers.IO so the main thread is never blocked.
  • Cancels the Future if the coroutine is cancelled.
  • Unwraps ExecutionException so callers see the real cause.
val future =RootThread.submit<String> { readPrivilegedFile() }
// Cancel if needed:
future.cancel(true)
// Or await in a coroutine:val result = future.awaitRoot()

Signature:

suspendfun <T> Future<T>.awaitRoot(): T

API Reference — Java

RootThread.submit()

Submits a callable to the root process and returns a Future<T> immediately. The future resolves with the result or fails with an IOException.

Future<Boolean> future = RootThread.submit(() ->
newFile("/system/bin/su").exists()
);
// Optional cancellationfuture.cancel(true);
// Join elsewhere (not on main thread)booleanresult = future.get(5, TimeUnit.SECONDS);

Signature:

publicstatic <T> Future<T> submit(@NonNullRootCallable<T> callable)

RootThread.executeBlocking()

Submits a callable and blocks the calling thread until the result is available. Must not be called on the main thread.

executorService.execute(() -> {
try {
Stringkernel = RootThread.executeBlocking(
() -> newString(Files.readAllBytes(Paths.get("/proc/version")))
);
runOnUiThread(() -> textView.setText(kernel));
} catch (IOException | InterruptedExceptione) {
Log.e(TAG, "Root IPC failed", e);
}
});

Signature:

publicstatic <T> TexecuteBlocking(@NonNullRootCallable<T> callable)
throwsIOException, InterruptedException

RootThread.executeBlocking() with timeout

try {
Booleanexists = RootThread.executeBlocking(
() -> newFile("/system/bin/su").exists(),
5, TimeUnit.SECONDS
);
} catch (TimeoutExceptione) {
Log.e(TAG, "Root process timed out");
} catch (IOException | InterruptedExceptione) {
Log.e(TAG, "IPC error", e);
}

Signature:

publicstatic <T> TexecuteBlocking(
@NonNullRootCallable<T> callable,
longtimeout,
@NonNullTimeUnitunit
) throwsIOException, InterruptedException, TimeoutException

RootThreadExtensions.rootLaunch()

Async fire-and-forget with an optional callback delivered on a specified Executor (or the main thread by default).

// Callback on main thread (default)RootThreadExtensions.rootLaunch(
() -> readRootData(),
newRootThreadExtensions.RootCallback<String>() {
@OverridepublicvoidonSuccess(Stringresult) {
textView.setText(result); // main thread
}
@OverridepublicvoidonFailure(Throwableerror) {
Log.e(TAG, "Failed", error);
}
}
);
// Callback on a custom executorExecutordbExecutor = Executors.newSingleThreadExecutor();
RootThreadExtensions.rootLaunch(
() -> readRootDatabase(),
newRootThreadExtensions.RootCallback<List<Row>>() {
@OverridepublicvoidonSuccess(List<Row> rows) {
dao.insertAll(rows); // already on dbExecutor
}
@OverridepublicvoidonFailure(Throwablee) { /* handle */ }
},
dbExecutor
);

Signatures:

// Callback on main threadpublicstatic <T> Future<T> rootLaunch(
@NonNullRootCallable<T> callable,
@NullableRootCallback<T> callback
)
// Callback on custom executorpublicstatic <T> Future<T> rootLaunch(
@NonNullRootCallable<T> callable,
@NullableRootCallback<T> callback,
@NonNullExecutorexecutor
)
// Fire and forget, no callbackpublicstaticFuture<Void> rootLaunch(@NonNullRootCallable<Void> callable)

RootThreadExtensions.rootBlocking()

Receiver-scoped blocking execution. Equivalent to the Kotlin T.rootBlocking { } extension.

PackageManagerpm = getPackageManager();
executorService.execute(() -> {
try {
List<PackageInfo> packages = RootThreadExtensions.rootBlocking(
pm,
manager -> manager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
);
runOnUiThread(() -> adapter.setData(packages));
} catch (IOException | InterruptedExceptione) {
Log.e(TAG, "Failed", e);
}
});

Signatures:

publicstatic <T, R> RrootBlocking(
@NonNullTreceiver,
@NonNullRootConsumer<T, R> block
) throwsIOException, InterruptedExceptionpublicstatic <T, R> RrootBlocking(
@NonNullTreceiver,
@NonNullRootConsumer<T, R> block,
longtimeout,
@NonNullTimeUnitunit
) throwsIOException, InterruptedException, TimeoutException

RootThreadExtensions.addRootThread()

Lifecycle-aware bind/unbind as a static method (Java equivalent of the Kotlin extension).

RootThreadExtensions.addRootThread(this, context);

Lifecycle

RootThreadLifecycleObserver

The preferred Java approach. Stores applicationContext internally to prevent leaks.

// ActivitygetLifecycle().addObserver(newRootThreadLifecycleObserver(this));
// FragmentgetViewLifecycleOwner().getLifecycle()
.addObserver(newRootThreadLifecycleObserver(requireContext()));
// Kotlin extension — equivalent one-liner
addRootThread(requireContext())

Manual bind / unbind

For cases where lifecycle integration is not appropriate (services, background components):

RootThread.bind(context); // call when readyRootThread.unbind(); // call when done
RootThread.bind(context)
RootThread.unbind()

Threading model

LayerThread
Caller (Kotlin)Any — dispatched to Dispatchers.IO internally
Caller (Java async)RootThread cached executor (RootThread-IPC threads)
Caller (Java blocking)Caller's thread — must not be main thread
Root serviceBinder thread (returns immediately); work on RootThread-Worker daemon thread

The root service spawns a new named daemon thread per call so the Binder thread is never parked, eliminating ANR risk.


FD ownership contract

createPipe() → [callableRead, callableWrite]
createPipe() → [resultRead, resultWrite ]
Caller:
write callable → callableWrite → (AutoCloseOutputStream closes it, sends EOF)
svc.execute(callableRead, resultWrite) ← service owns these two from here
read result ← resultRead ← caller owns this until done
On error before execute():
caller closes all four FDs

Serialisation internals

KryoManager is a pre-configured Kryo instance:

SettingValue
Registration requiredfalse (class names are written to the stream)
Referencestrue (handles cyclic graphs in non-Parcelable objects)
Instantiation strategyDefaultInstantiatorStrategy + StdInstantiatorStrategy (no-arg constructor not required)
Parcelable serialiserCustom ParcelableSerializer — uses Parcel.marshall() / unmarshall()

A freshKryoManager instance is used for each write and each read, keeping reference tables completely independent across the pipe boundary.


Error handling

Error scenarioBehaviour
Remote callable throwsException is serialised and re-thrown as IOException("Remote exception", cause)
IPC write failsIOException("IPC write/execute failed", cause)
Deserialisation fails in rootIOException("Deserialisation failed in root process", cause)
Root service disconnectsCompletableFuture is replaced; next call blocks until reconnect
Coroutine cancelledFuture.cancel(true) is called; CancellationException propagates normally
InterruptedExceptionThread interrupt flag is restored; wrapped as CancellationException in coroutine context

Rules and gotchas

Serialisation

  • RootCallable and RootConsumer lambdas must be Kryo-serializable. Do not capture Context, View, or any non-serializable object.
  • Prefer capturing primitive values or Parcelable objects. For complex objects, pass them as the receiver via T.rootThread { } or rootBlocking(receiver) { }.

Threading

  • Never call executeBlocking or rootBlocking on the main thread — they block the calling thread.
  • Prefer rootThread { } (Kotlin suspend) or rootLaunch (Java async) in UI code.

Lifecycle

  • Always use RootThreadLifecycleObserver or addRootThread() to ensure the service is unbound when the component stops. Failing to unbind leaks the root process connection.
  • RootThreadLifecycleObserver stores applicationContext internally — passing an Activity context is safe.

Cancellation

  • rootLaunch / rootAsync respect coroutine cancellation: the underlying Future is canceled and the root worker thread is interrupted.
  • rootFlow is cold — collection starts a new IPC round-trip each time.

About

An Android library for executing arbitrary code in a privileged root process via Binder IPC, with first-class support for both Java (Future, ExecutorService) and Kotlin (coroutines, Flow, DSL).

Resources

Stars

13 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - MMRLApp/RootThread: An Android library for executing arbitrary code in a privileged root process via Binder IPC, with first-class support for both Java (Future, ExecutorService) and Kotlin (coroutines, Flow, DSL). · GitHub
Skip to content

Repository files navigation

RootThread

An Android library for executing arbitrary code in a privileged root process via Binder IPC, with first-class support for both Java (Future, ExecutorService) and Kotlin (coroutines, Flow, DSL).


Table of Contents


How It Works

┌─────────────────────────────────┐ ┌──────────────────────────────────┐
│ App Process │ │ Root Process │
│ │ │ │
│ RootCallable ──► Kryo ──► pipe ├──────► │ pipe ──► Kryo ──► RootCallable │
│ │ IPC │ │ │
│ result ◄── Kryo ◄── pipe ◄┤ │ call() │
│ │ │ │ │
│ │ │ result ──► Kryo ──► pipe ──► │
└─────────────────────────────────┘ └──────────────────────────────────┘
  1. The caller serializes a RootCallable via Kryo into a ParcelFileDescriptor write pipe.
  2. The read-end of that pipe and the write-end of a result pipe are handed to RootThreadService over Binder.
  3. The root service deserializes and executes the callable on a daemon thread.
  4. The result is serialized back into the result pipe.
  5. The caller reads the result pipe and resumes.

Parcelable objects are serialized using Android's own Parcel mechanism instead of Kryo to avoid cross-process reference-ID divergence.


Setup

JitPack

Add the JitPack repository to your settings file:

// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven("https://jitpack.io")
}
}

Dependencies

// build.gradle.kts
dependencies {
implementation("com.github.MMRLApp.RootThread:thread:<version>")
// KSP code generation (optional — see KSP Code Generation below)
ksp("com.github.MMRLApp.RootThread:thread-ksp:<version>")
}

Lifecycle binding

The simplest setup — attach the observer once in onCreate:

// KotlinclassMainActivity : AppCompatActivity() {
overridefunonCreate(savedInstanceState:Bundle?) {
super.onCreate(savedInstanceState)
addRootThread(this) // binds onStart, unbinds onStop
}
}
// JavapublicclassMainActivityextendsComponentActivity {
@OverrideprotectedvoidonCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
getLifecycle().addObserver(newRootThreadLifecycleObserver(this));
}
}

KSP Code Generation

The optional thread-ksp artifact provides a KSP processor that generates boilerplate-free RootCallable wrappers from annotated functions.

KSP Setup

// build.gradle.kts
plugins {
id("com.google.devtools.ksp") version "<ksp-version>"
}
dependencies {
implementation("com.github.MMRLApp.RootThread:thread:<version>")
ksp("com.github.MMRLApp.RootThread:thread-ksp:<version>")
}

@RootFunction

Annotate any top-level function or companion object function that should run in the root process. If the function accepts a RootOptions parameter, it is automatically injected from call(options) at runtime and excluded from the constructor.

// Top-level
@RootFunction
funloadModules(): List<Module> {
returnFile("/data/adb/modules").listFiles()
?.filter { it.isDirectory }
?.mapNotNull { parseModule(it) }
.orEmpty()
}
// With RootOptions (if you use RootOptions, always place it first)
@RootFunction
funreadFile(options:RootOptions, path:String): String {
returnFile(path).readText()
}
// Inside a companion objectclassModulesRepository {
companionobject {
@RootFunction
funloadModules(): List<Module> { ... }
}
}

Generated API

For each annotated function the processor generates a file under dev.mmrlx.threading:

// Generated: dev/mmrlx/threading/RootedLoadModules.ktpublicclassRootedLoadModules : RootCallable<List<Module>>, Serializable {
overridefuncall(options:RootOptions): List<Module> = loadModules()
}
// Extension on RootScope — the public API surfacefun RootScope.loadModules(): RootCallable<List<Module>> =RootedLoadModules()

Usage:

// Suspend call via companionval modules =RootedLoadModules().asThread()
// As a Flow via companion (if you compose)val modules by RootedLoadModules().asFlow().collectAsState(emptyList())

Core Concepts

RootCallable

RootCallable<T> is a @FunctionalInterface (usable as a lambda in both Java and Kotlin) that represents work to execute in the root process.

val callable =RootCallable<String> {
File("/proc/version").readText()
}
RootCallable<String> callable = options -> newFile("/proc/version").readText();

RootConsumer

RootConsumer<T, R> is a receiver-scoped variant — it receives a typed object from the caller's process and returns a result. Used by the rootBlocking receiver extension and RootThreadExtensions.rootBlocking.

val consumer =RootConsumer<PackageManager, List<PackageInfo>> { pm ->
pm.getInstalledPackages(0)
}

Serialisation rules

Because callables are serialized across a process boundary, they must be Kryo-compatible:

✅ Safe to capture❌ Never capture
Primitives (Int, Boolean, String, …)Context / Activity / Fragment
Parcelable objectsView or any UI object
Plain data classesNon-serialisable lambdas or anonymous classes
EnumsViewModel, LiveData, Flow
Serializable objectsBinder objects (other than via Parcel)

API Reference — Kotlin

rootThread { }

Suspends the coroutine, executes the block in the root process, and resumes with the result. Dispatches onto Dispatchers.IO automatically.

// In any suspend function:val kernel = rootThread { File("/proc/version").readText() }
val hasSu = rootThread { File("/system/bin/su").exists() }

Signature:

suspendfun <T> rootThread(block:RootCallable<T>): T

Throws:IOException on IPC or remote failure.


T.rootThread { }

Receiver-scoped variant. Passes this into the root process as the first argument of the callable.

val packages = packageManager.rootThread { pm ->
pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
}

Signature:

suspendfun <T, R> T.rootThread(block:RootConsumer<T, R>): R

Throws:IOException on IPC or remote failure.


rootLaunch / rootAsync

Launch-style wrappers for use inside a CoroutineScope. Exceptions propagate through the scope's job like any other coroutine failure.

// Fire and forget
viewModelScope.rootLaunch {
Runtime.getRuntime().exec("chmod 777 /data/local/tmp/file")
}
// With a result via Deferredval deferred = viewModelScope.rootAsync { readRootDatabase() }
val rows = deferred.await()

Signatures:

fun CoroutineScope.rootLaunch(block:RootCallable<Unit>): Jobfun <T> CoroutineScope.rootAsync(block:RootCallable<T>): Deferred<T>

rootFlow { }

Returns a cold Flow<T> that executes the callable on each collection and emits a single value.

rootFlow { File("/proc/version").readText() }
.onEach { version -> textView.text = version }
.launchIn(lifecycleScope)
// Combine with other operators
rootFlow { getPrivilegedData() }
.map { it.transform() }
.catch { e -> showError(e) }
.flowOn(Dispatchers.IO)
.collect { result -> updateUi(result) }

Signature:

fun <T> rootFlow(block:RootCallable<T>): Flow<T>

rootBlocking { }

Executes the block in the root process, blocking the calling thread. Must not be called on the main thread.

// On a background thread / Worker / HandlerThread:val exists = rootBlocking { File("/system/bin/su").exists() }

Signature:

@Throws(IOException::class, InterruptedException::class)
fun <T> rootBlocking(block:RootCallable<T>): T?

T.rootBlocking { }

Receiver-scoped blocking variant.

val packages = packageManager.rootBlocking { pm ->
pm.getInstalledPackages(0)
}

Signature:

@Throws(IOException::class, InterruptedException::class)
fun <T, R> T.rootBlocking(block:RootConsumer<T, R>): R?

rootBlocking with timeout

Blocking execution with a deadline. Throws TimeoutException if the root process does not respond in time.

val result = rootBlocking(5, TimeUnit.SECONDS) { readHeavyRootFile() }

Signature:

@Throws(IOException::class, InterruptedException::class, TimeoutException::class)
fun <T> rootBlocking(timeout:Long, unit:TimeUnit, block:RootCallable<T>): T?

rootBlock DSL

Groups multiple root calls into a structured block. Each exec { } call is an independent IPC round-trip but they share a readable sequential scope.

val data = rootBlock {
val hasSu = exec { File("/system/bin/su").exists() }
val kernel = exec { File("/proc/version").readText() }
val modules = exec { File("/data/adb/modules").listFiles()?.size ?:0 }
mapOf(
"hasSu" to hasSu,
"kernel" to kernel,
"modules" to modules,
)
}

Signatures:

suspendfun <T> rootBlock(block:suspendRootBlockScope.() ->T): TclassRootBlockScope {
suspendfun <T> exec(block:RootCallable<T>): T
}

rootThreadCatching / rootBlockingCatching

Result-wrapped variants for railway-oriented error handling. Never throw — failures are delivered as Result.failure.

// Suspend
rootThreadCatching { riskyRootOperation() }
.onSuccess { result -> updateUi(result) }
.onFailure { error ->Log.e(TAG, "Root failed", error) }
// Blocking (off main thread)val result = rootBlockingCatching { File("/proc/version").readText() }
if (result.isSuccess) {
textView.text = result.getOrNull()
}

Signatures:

suspendfun <T> rootThreadCatching(block:RootCallable<T>): Result<T>
fun <T> rootBlockingCatching(block:RootCallable<T>): Result<T>

RootThread { } invoke syntax

Syntactic sugar allowing RootThread to be called like a function inside any suspend context.

// Equivalent to rootThread { ... }val result =RootThread { doPrivilegedWork() }

Future.awaitRoot()

Suspends a coroutine until a Future<T> (returned by RootThread.submit()) completes. Implemented with suspendCancellableCoroutineno kotlinx-coroutines-jdk8 dependency required.

  • Runs Future.get() on Dispatchers.IO so the main thread is never blocked.
  • Cancels the Future if the coroutine is cancelled.
  • Unwraps ExecutionException so callers see the real cause.
val future =RootThread.submit<String> { readPrivilegedFile() }
// Cancel if needed:
future.cancel(true)
// Or await in a coroutine:val result = future.awaitRoot()

Signature:

suspendfun <T> Future<T>.awaitRoot(): T

API Reference — Java

RootThread.submit()

Submits a callable to the root process and returns a Future<T> immediately. The future resolves with the result or fails with an IOException.

Future<Boolean> future = RootThread.submit(() ->
newFile("/system/bin/su").exists()
);
// Optional cancellationfuture.cancel(true);
// Join elsewhere (not on main thread)booleanresult = future.get(5, TimeUnit.SECONDS);

Signature:

publicstatic <T> Future<T> submit(@NonNullRootCallable<T> callable)

RootThread.executeBlocking()

Submits a callable and blocks the calling thread until the result is available. Must not be called on the main thread.

executorService.execute(() -> {
try {
Stringkernel = RootThread.executeBlocking(
() -> newString(Files.readAllBytes(Paths.get("/proc/version")))
);
runOnUiThread(() -> textView.setText(kernel));
} catch (IOException | InterruptedExceptione) {
Log.e(TAG, "Root IPC failed", e);
}
});

Signature:

publicstatic <T> TexecuteBlocking(@NonNullRootCallable<T> callable)
throwsIOException, InterruptedException

RootThread.executeBlocking() with timeout

try {
Booleanexists = RootThread.executeBlocking(
() -> newFile("/system/bin/su").exists(),
5, TimeUnit.SECONDS
);
} catch (TimeoutExceptione) {
Log.e(TAG, "Root process timed out");
} catch (IOException | InterruptedExceptione) {
Log.e(TAG, "IPC error", e);
}

Signature:

publicstatic <T> TexecuteBlocking(
@NonNullRootCallable<T> callable,
longtimeout,
@NonNullTimeUnitunit
) throwsIOException, InterruptedException, TimeoutException

RootThreadExtensions.rootLaunch()

Async fire-and-forget with an optional callback delivered on a specified Executor (or the main thread by default).

// Callback on main thread (default)RootThreadExtensions.rootLaunch(
() -> readRootData(),
newRootThreadExtensions.RootCallback<String>() {
@OverridepublicvoidonSuccess(Stringresult) {
textView.setText(result); // main thread
}
@OverridepublicvoidonFailure(Throwableerror) {
Log.e(TAG, "Failed", error);
}
}
);
// Callback on a custom executorExecutordbExecutor = Executors.newSingleThreadExecutor();
RootThreadExtensions.rootLaunch(
() -> readRootDatabase(),
newRootThreadExtensions.RootCallback<List<Row>>() {
@OverridepublicvoidonSuccess(List<Row> rows) {
dao.insertAll(rows); // already on dbExecutor
}
@OverridepublicvoidonFailure(Throwablee) { /* handle */ }
},
dbExecutor
);

Signatures:

// Callback on main threadpublicstatic <T> Future<T> rootLaunch(
@NonNullRootCallable<T> callable,
@NullableRootCallback<T> callback
)
// Callback on custom executorpublicstatic <T> Future<T> rootLaunch(
@NonNullRootCallable<T> callable,
@NullableRootCallback<T> callback,
@NonNullExecutorexecutor
)
// Fire and forget, no callbackpublicstaticFuture<Void> rootLaunch(@NonNullRootCallable<Void> callable)

RootThreadExtensions.rootBlocking()

Receiver-scoped blocking execution. Equivalent to the Kotlin T.rootBlocking { } extension.

PackageManagerpm = getPackageManager();
executorService.execute(() -> {
try {
List<PackageInfo> packages = RootThreadExtensions.rootBlocking(
pm,
manager -> manager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
);
runOnUiThread(() -> adapter.setData(packages));
} catch (IOException | InterruptedExceptione) {
Log.e(TAG, "Failed", e);
}
});

Signatures:

publicstatic <T, R> RrootBlocking(
@NonNullTreceiver,
@NonNullRootConsumer<T, R> block
) throwsIOException, InterruptedExceptionpublicstatic <T, R> RrootBlocking(
@NonNullTreceiver,
@NonNullRootConsumer<T, R> block,
longtimeout,
@NonNullTimeUnitunit
) throwsIOException, InterruptedException, TimeoutException

RootThreadExtensions.addRootThread()

Lifecycle-aware bind/unbind as a static method (Java equivalent of the Kotlin extension).

RootThreadExtensions.addRootThread(this, context);

Lifecycle

RootThreadLifecycleObserver

The preferred Java approach. Stores applicationContext internally to prevent leaks.

// ActivitygetLifecycle().addObserver(newRootThreadLifecycleObserver(this));
// FragmentgetViewLifecycleOwner().getLifecycle()
.addObserver(newRootThreadLifecycleObserver(requireContext()));
// Kotlin extension — equivalent one-liner
addRootThread(requireContext())

Manual bind / unbind

For cases where lifecycle integration is not appropriate (services, background components):

RootThread.bind(context); // call when readyRootThread.unbind(); // call when done
RootThread.bind(context)
RootThread.unbind()

Threading model

LayerThread
Caller (Kotlin)Any — dispatched to Dispatchers.IO internally
Caller (Java async)RootThread cached executor (RootThread-IPC threads)
Caller (Java blocking)Caller's thread — must not be main thread
Root serviceBinder thread (returns immediately); work on RootThread-Worker daemon thread

The root service spawns a new named daemon thread per call so the Binder thread is never parked, eliminating ANR risk.


FD ownership contract

createPipe() → [callableRead, callableWrite]
createPipe() → [resultRead, resultWrite ]
Caller:
write callable → callableWrite → (AutoCloseOutputStream closes it, sends EOF)
svc.execute(callableRead, resultWrite) ← service owns these two from here
read result ← resultRead ← caller owns this until done
On error before execute():
caller closes all four FDs

Serialisation internals

KryoManager is a pre-configured Kryo instance:

SettingValue
Registration requiredfalse (class names are written to the stream)
Referencestrue (handles cyclic graphs in non-Parcelable objects)
Instantiation strategyDefaultInstantiatorStrategy + StdInstantiatorStrategy (no-arg constructor not required)
Parcelable serialiserCustom ParcelableSerializer — uses Parcel.marshall() / unmarshall()

A freshKryoManager instance is used for each write and each read, keeping reference tables completely independent across the pipe boundary.


Error handling

Error scenarioBehaviour
Remote callable throwsException is serialised and re-thrown as IOException("Remote exception", cause)
IPC write failsIOException("IPC write/execute failed", cause)
Deserialisation fails in rootIOException("Deserialisation failed in root process", cause)
Root service disconnectsCompletableFuture is replaced; next call blocks until reconnect
Coroutine cancelledFuture.cancel(true) is called; CancellationException propagates normally
InterruptedExceptionThread interrupt flag is restored; wrapped as CancellationException in coroutine context

Rules and gotchas

Serialisation

  • RootCallable and RootConsumer lambdas must be Kryo-serializable. Do not capture Context, View, or any non-serializable object.
  • Prefer capturing primitive values or Parcelable objects. For complex objects, pass them as the receiver via T.rootThread { } or rootBlocking(receiver) { }.

Threading

  • Never call executeBlocking or rootBlocking on the main thread — they block the calling thread.
  • Prefer rootThread { } (Kotlin suspend) or rootLaunch (Java async) in UI code.

Lifecycle

  • Always use RootThreadLifecycleObserver or addRootThread() to ensure the service is unbound when the component stops. Failing to unbind leaks the root process connection.
  • RootThreadLifecycleObserver stores applicationContext internally — passing an Activity context is safe.

Cancellation

  • rootLaunch / rootAsync respect coroutine cancellation: the underlying Future is canceled and the root worker thread is interrupted.
  • rootFlow is cold — collection starts a new IPC round-trip each time.

About

An Android library for executing arbitrary code in a privileged root process via Binder IPC, with first-class support for both Java (Future, ExecutorService) and Kotlin (coroutines, Flow, DSL).

Resources

Stars

13 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - MMRLApp/RootThread: An Android library for executing arbitrary code in a privileged root process via Binder IPC, with first-class support for both Java (Future, ExecutorService) and Kotlin (coroutines, Flow, DSL). · GitHub
Skip to content

Repository files navigation

RootThread

An Android library for executing arbitrary code in a privileged root process via Binder IPC, with first-class support for both Java (Future, ExecutorService) and Kotlin (coroutines, Flow, DSL).


Table of Contents


How It Works

┌─────────────────────────────────┐ ┌──────────────────────────────────┐
│ App Process │ │ Root Process │
│ │ │ │
│ RootCallable ──► Kryo ──► pipe ├──────► │ pipe ──► Kryo ──► RootCallable │
│ │ IPC │ │ │
│ result ◄── Kryo ◄── pipe ◄┤ │ call() │
│ │ │ │ │
│ │ │ result ──► Kryo ──► pipe ──► │
└─────────────────────────────────┘ └──────────────────────────────────┘
  1. The caller serializes a RootCallable via Kryo into a ParcelFileDescriptor write pipe.
  2. The read-end of that pipe and the write-end of a result pipe are handed to RootThreadService over Binder.
  3. The root service deserializes and executes the callable on a daemon thread.
  4. The result is serialized back into the result pipe.
  5. The caller reads the result pipe and resumes.

Parcelable objects are serialized using Android's own Parcel mechanism instead of Kryo to avoid cross-process reference-ID divergence.


Setup

JitPack

Add the JitPack repository to your settings file:

// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven("https://jitpack.io")
}
}

Dependencies

// build.gradle.kts
dependencies {
implementation("com.github.MMRLApp.RootThread:thread:<version>")
// KSP code generation (optional — see KSP Code Generation below)
ksp("com.github.MMRLApp.RootThread:thread-ksp:<version>")
}

Lifecycle binding

The simplest setup — attach the observer once in onCreate:

// KotlinclassMainActivity : AppCompatActivity() {
overridefunonCreate(savedInstanceState:Bundle?) {
super.onCreate(savedInstanceState)
addRootThread(this) // binds onStart, unbinds onStop
}
}
// JavapublicclassMainActivityextendsComponentActivity {
@OverrideprotectedvoidonCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
getLifecycle().addObserver(newRootThreadLifecycleObserver(this));
}
}

KSP Code Generation

The optional thread-ksp artifact provides a KSP processor that generates boilerplate-free RootCallable wrappers from annotated functions.

KSP Setup

// build.gradle.kts
plugins {
id("com.google.devtools.ksp") version "<ksp-version>"
}
dependencies {
implementation("com.github.MMRLApp.RootThread:thread:<version>")
ksp("com.github.MMRLApp.RootThread:thread-ksp:<version>")
}

@RootFunction

Annotate any top-level function or companion object function that should run in the root process. If the function accepts a RootOptions parameter, it is automatically injected from call(options) at runtime and excluded from the constructor.

// Top-level
@RootFunction
funloadModules(): List<Module> {
returnFile("/data/adb/modules").listFiles()
?.filter { it.isDirectory }
?.mapNotNull { parseModule(it) }
.orEmpty()
}
// With RootOptions (if you use RootOptions, always place it first)
@RootFunction
funreadFile(options:RootOptions, path:String): String {
returnFile(path).readText()
}
// Inside a companion objectclassModulesRepository {
companionobject {
@RootFunction
funloadModules(): List<Module> { ... }
}
}

Generated API

For each annotated function the processor generates a file under dev.mmrlx.threading:

// Generated: dev/mmrlx/threading/RootedLoadModules.ktpublicclassRootedLoadModules : RootCallable<List<Module>>, Serializable {
overridefuncall(options:RootOptions): List<Module> = loadModules()
}
// Extension on RootScope — the public API surfacefun RootScope.loadModules(): RootCallable<List<Module>> =RootedLoadModules()

Usage:

// Suspend call via companionval modules =RootedLoadModules().asThread()
// As a Flow via companion (if you compose)val modules by RootedLoadModules().asFlow().collectAsState(emptyList())

Core Concepts

RootCallable

RootCallable<T> is a @FunctionalInterface (usable as a lambda in both Java and Kotlin) that represents work to execute in the root process.

val callable =RootCallable<String> {
File("/proc/version").readText()
}
RootCallable<String> callable = options -> newFile("/proc/version").readText();

RootConsumer

RootConsumer<T, R> is a receiver-scoped variant — it receives a typed object from the caller's process and returns a result. Used by the rootBlocking receiver extension and RootThreadExtensions.rootBlocking.

val consumer =RootConsumer<PackageManager, List<PackageInfo>> { pm ->
pm.getInstalledPackages(0)
}

Serialisation rules

Because callables are serialized across a process boundary, they must be Kryo-compatible:

✅ Safe to capture❌ Never capture
Primitives (Int, Boolean, String, …)Context / Activity / Fragment
Parcelable objectsView or any UI object
Plain data classesNon-serialisable lambdas or anonymous classes
EnumsViewModel, LiveData, Flow
Serializable objectsBinder objects (other than via Parcel)

API Reference — Kotlin

rootThread { }

Suspends the coroutine, executes the block in the root process, and resumes with the result. Dispatches onto Dispatchers.IO automatically.

// In any suspend function:val kernel = rootThread { File("/proc/version").readText() }
val hasSu = rootThread { File("/system/bin/su").exists() }

Signature:

suspendfun <T> rootThread(block:RootCallable<T>): T

Throws:IOException on IPC or remote failure.


T.rootThread { }

Receiver-scoped variant. Passes this into the root process as the first argument of the callable.

val packages = packageManager.rootThread { pm ->
pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
}

Signature:

suspendfun <T, R> T.rootThread(block:RootConsumer<T, R>): R

Throws:IOException on IPC or remote failure.


rootLaunch / rootAsync

Launch-style wrappers for use inside a CoroutineScope. Exceptions propagate through the scope's job like any other coroutine failure.

// Fire and forget
viewModelScope.rootLaunch {
Runtime.getRuntime().exec("chmod 777 /data/local/tmp/file")
}
// With a result via Deferredval deferred = viewModelScope.rootAsync { readRootDatabase() }
val rows = deferred.await()

Signatures:

fun CoroutineScope.rootLaunch(block:RootCallable<Unit>): Jobfun <T> CoroutineScope.rootAsync(block:RootCallable<T>): Deferred<T>

rootFlow { }

Returns a cold Flow<T> that executes the callable on each collection and emits a single value.

rootFlow { File("/proc/version").readText() }
.onEach { version -> textView.text = version }
.launchIn(lifecycleScope)
// Combine with other operators
rootFlow { getPrivilegedData() }
.map { it.transform() }
.catch { e -> showError(e) }
.flowOn(Dispatchers.IO)
.collect { result -> updateUi(result) }

Signature:

fun <T> rootFlow(block:RootCallable<T>): Flow<T>

rootBlocking { }

Executes the block in the root process, blocking the calling thread. Must not be called on the main thread.

// On a background thread / Worker / HandlerThread:val exists = rootBlocking { File("/system/bin/su").exists() }

Signature:

@Throws(IOException::class, InterruptedException::class)
fun <T> rootBlocking(block:RootCallable<T>): T?

T.rootBlocking { }

Receiver-scoped blocking variant.

val packages = packageManager.rootBlocking { pm ->
pm.getInstalledPackages(0)
}

Signature:

@Throws(IOException::class, InterruptedException::class)
fun <T, R> T.rootBlocking(block:RootConsumer<T, R>): R?

rootBlocking with timeout

Blocking execution with a deadline. Throws TimeoutException if the root process does not respond in time.

val result = rootBlocking(5, TimeUnit.SECONDS) { readHeavyRootFile() }

Signature:

@Throws(IOException::class, InterruptedException::class, TimeoutException::class)
fun <T> rootBlocking(timeout:Long, unit:TimeUnit, block:RootCallable<T>): T?

rootBlock DSL

Groups multiple root calls into a structured block. Each exec { } call is an independent IPC round-trip but they share a readable sequential scope.

val data = rootBlock {
val hasSu = exec { File("/system/bin/su").exists() }
val kernel = exec { File("/proc/version").readText() }
val modules = exec { File("/data/adb/modules").listFiles()?.size ?:0 }
mapOf(
"hasSu" to hasSu,
"kernel" to kernel,
"modules" to modules,
)
}

Signatures:

suspendfun <T> rootBlock(block:suspendRootBlockScope.() ->T): TclassRootBlockScope {
suspendfun <T> exec(block:RootCallable<T>): T
}

rootThreadCatching / rootBlockingCatching

Result-wrapped variants for railway-oriented error handling. Never throw — failures are delivered as Result.failure.

// Suspend
rootThreadCatching { riskyRootOperation() }
.onSuccess { result -> updateUi(result) }
.onFailure { error ->Log.e(TAG, "Root failed", error) }
// Blocking (off main thread)val result = rootBlockingCatching { File("/proc/version").readText() }
if (result.isSuccess) {
textView.text = result.getOrNull()
}

Signatures:

suspendfun <T> rootThreadCatching(block:RootCallable<T>): Result<T>
fun <T> rootBlockingCatching(block:RootCallable<T>): Result<T>

RootThread { } invoke syntax

Syntactic sugar allowing RootThread to be called like a function inside any suspend context.

// Equivalent to rootThread { ... }val result =RootThread { doPrivilegedWork() }

Future.awaitRoot()

Suspends a coroutine until a Future<T> (returned by RootThread.submit()) completes. Implemented with suspendCancellableCoroutineno kotlinx-coroutines-jdk8 dependency required.

  • Runs Future.get() on Dispatchers.IO so the main thread is never blocked.
  • Cancels the Future if the coroutine is cancelled.
  • Unwraps ExecutionException so callers see the real cause.
val future =RootThread.submit<String> { readPrivilegedFile() }
// Cancel if needed:
future.cancel(true)
// Or await in a coroutine:val result = future.awaitRoot()

Signature:

suspendfun <T> Future<T>.awaitRoot(): T

API Reference — Java

RootThread.submit()

Submits a callable to the root process and returns a Future<T> immediately. The future resolves with the result or fails with an IOException.

Future<Boolean> future = RootThread.submit(() ->
newFile("/system/bin/su").exists()
);
// Optional cancellationfuture.cancel(true);
// Join elsewhere (not on main thread)booleanresult = future.get(5, TimeUnit.SECONDS);

Signature:

publicstatic <T> Future<T> submit(@NonNullRootCallable<T> callable)

RootThread.executeBlocking()

Submits a callable and blocks the calling thread until the result is available. Must not be called on the main thread.

executorService.execute(() -> {
try {
Stringkernel = RootThread.executeBlocking(
() -> newString(Files.readAllBytes(Paths.get("/proc/version")))
);
runOnUiThread(() -> textView.setText(kernel));
} catch (IOException | InterruptedExceptione) {
Log.e(TAG, "Root IPC failed", e);
}
});

Signature:

publicstatic <T> TexecuteBlocking(@NonNullRootCallable<T> callable)
throwsIOException, InterruptedException

RootThread.executeBlocking() with timeout

try {
Booleanexists = RootThread.executeBlocking(
() -> newFile("/system/bin/su").exists(),
5, TimeUnit.SECONDS
);
} catch (TimeoutExceptione) {
Log.e(TAG, "Root process timed out");
} catch (IOException | InterruptedExceptione) {
Log.e(TAG, "IPC error", e);
}

Signature:

publicstatic <T> TexecuteBlocking(
@NonNullRootCallable<T> callable,
longtimeout,
@NonNullTimeUnitunit
) throwsIOException, InterruptedException, TimeoutException

RootThreadExtensions.rootLaunch()

Async fire-and-forget with an optional callback delivered on a specified Executor (or the main thread by default).

// Callback on main thread (default)RootThreadExtensions.rootLaunch(
() -> readRootData(),
newRootThreadExtensions.RootCallback<String>() {
@OverridepublicvoidonSuccess(Stringresult) {
textView.setText(result); // main thread
}
@OverridepublicvoidonFailure(Throwableerror) {
Log.e(TAG, "Failed", error);
}
}
);
// Callback on a custom executorExecutordbExecutor = Executors.newSingleThreadExecutor();
RootThreadExtensions.rootLaunch(
() -> readRootDatabase(),
newRootThreadExtensions.RootCallback<List<Row>>() {
@OverridepublicvoidonSuccess(List<Row> rows) {
dao.insertAll(rows); // already on dbExecutor
}
@OverridepublicvoidonFailure(Throwablee) { /* handle */ }
},
dbExecutor
);

Signatures:

// Callback on main threadpublicstatic <T> Future<T> rootLaunch(
@NonNullRootCallable<T> callable,
@NullableRootCallback<T> callback
)
// Callback on custom executorpublicstatic <T> Future<T> rootLaunch(
@NonNullRootCallable<T> callable,
@NullableRootCallback<T> callback,
@NonNullExecutorexecutor
)
// Fire and forget, no callbackpublicstaticFuture<Void> rootLaunch(@NonNullRootCallable<Void> callable)

RootThreadExtensions.rootBlocking()

Receiver-scoped blocking execution. Equivalent to the Kotlin T.rootBlocking { } extension.

PackageManagerpm = getPackageManager();
executorService.execute(() -> {
try {
List<PackageInfo> packages = RootThreadExtensions.rootBlocking(
pm,
manager -> manager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
);
runOnUiThread(() -> adapter.setData(packages));
} catch (IOException | InterruptedExceptione) {
Log.e(TAG, "Failed", e);
}
});

Signatures:

publicstatic <T, R> RrootBlocking(
@NonNullTreceiver,
@NonNullRootConsumer<T, R> block
) throwsIOException, InterruptedExceptionpublicstatic <T, R> RrootBlocking(
@NonNullTreceiver,
@NonNullRootConsumer<T, R> block,
longtimeout,
@NonNullTimeUnitunit
) throwsIOException, InterruptedException, TimeoutException

RootThreadExtensions.addRootThread()

Lifecycle-aware bind/unbind as a static method (Java equivalent of the Kotlin extension).

RootThreadExtensions.addRootThread(this, context);

Lifecycle

RootThreadLifecycleObserver

The preferred Java approach. Stores applicationContext internally to prevent leaks.

// ActivitygetLifecycle().addObserver(newRootThreadLifecycleObserver(this));
// FragmentgetViewLifecycleOwner().getLifecycle()
.addObserver(newRootThreadLifecycleObserver(requireContext()));
// Kotlin extension — equivalent one-liner
addRootThread(requireContext())

Manual bind / unbind

For cases where lifecycle integration is not appropriate (services, background components):

RootThread.bind(context); // call when readyRootThread.unbind(); // call when done
RootThread.bind(context)
RootThread.unbind()

Threading model

LayerThread
Caller (Kotlin)Any — dispatched to Dispatchers.IO internally
Caller (Java async)RootThread cached executor (RootThread-IPC threads)
Caller (Java blocking)Caller's thread — must not be main thread
Root serviceBinder thread (returns immediately); work on RootThread-Worker daemon thread

The root service spawns a new named daemon thread per call so the Binder thread is never parked, eliminating ANR risk.


FD ownership contract

createPipe() → [callableRead, callableWrite]
createPipe() → [resultRead, resultWrite ]
Caller:
write callable → callableWrite → (AutoCloseOutputStream closes it, sends EOF)
svc.execute(callableRead, resultWrite) ← service owns these two from here
read result ← resultRead ← caller owns this until done
On error before execute():
caller closes all four FDs

Serialisation internals

KryoManager is a pre-configured Kryo instance:

SettingValue
Registration requiredfalse (class names are written to the stream)
Referencestrue (handles cyclic graphs in non-Parcelable objects)
Instantiation strategyDefaultInstantiatorStrategy + StdInstantiatorStrategy (no-arg constructor not required)
Parcelable serialiserCustom ParcelableSerializer — uses Parcel.marshall() / unmarshall()

A freshKryoManager instance is used for each write and each read, keeping reference tables completely independent across the pipe boundary.


Error handling

Error scenarioBehaviour
Remote callable throwsException is serialised and re-thrown as IOException("Remote exception", cause)
IPC write failsIOException("IPC write/execute failed", cause)
Deserialisation fails in rootIOException("Deserialisation failed in root process", cause)
Root service disconnectsCompletableFuture is replaced; next call blocks until reconnect
Coroutine cancelledFuture.cancel(true) is called; CancellationException propagates normally
InterruptedExceptionThread interrupt flag is restored; wrapped as CancellationException in coroutine context

Rules and gotchas

Serialisation

  • RootCallable and RootConsumer lambdas must be Kryo-serializable. Do not capture Context, View, or any non-serializable object.
  • Prefer capturing primitive values or Parcelable objects. For complex objects, pass them as the receiver via T.rootThread { } or rootBlocking(receiver) { }.

Threading

  • Never call executeBlocking or rootBlocking on the main thread — they block the calling thread.
  • Prefer rootThread { } (Kotlin suspend) or rootLaunch (Java async) in UI code.

Lifecycle

  • Always use RootThreadLifecycleObserver or addRootThread() to ensure the service is unbound when the component stops. Failing to unbind leaks the root process connection.
  • RootThreadLifecycleObserver stores applicationContext internally — passing an Activity context is safe.

Cancellation

  • rootLaunch / rootAsync respect coroutine cancellation: the underlying Future is canceled and the root worker thread is interrupted.
  • rootFlow is cold — collection starts a new IPC round-trip each time.

About

An Android library for executing arbitrary code in a privileged root process via Binder IPC, with first-class support for both Java (Future, ExecutorService) and Kotlin (coroutines, Flow, DSL).

Resources

Stars

13 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GitHub - MMRLApp/RootThread: An Android library for executing arbitrary code in a privileged root process via Binder IPC, with first-class support for both Java (Future, ExecutorService) and Kotlin (coroutines, Flow, DSL). · GitHub
Skip to content

Repository files navigation

RootThread

An Android library for executing arbitrary code in a privileged root process via Binder IPC, with first-class support for both Java (Future, ExecutorService) and Kotlin (coroutines, Flow, DSL).


Table of Contents


How It Works

┌─────────────────────────────────┐ ┌──────────────────────────────────┐
│ App Process │ │ Root Process │
│ │ │ │
│ RootCallable ──► Kryo ──► pipe ├──────► │ pipe ──► Kryo ──► RootCallable │
│ │ IPC │ │ │
│ result ◄── Kryo ◄── pipe ◄┤ │ call() │
│ │ │ │ │
│ │ │ result ──► Kryo ──► pipe ──► │
└─────────────────────────────────┘ └──────────────────────────────────┘
  1. The caller serializes a RootCallable via Kryo into a ParcelFileDescriptor write pipe.
  2. The read-end of that pipe and the write-end of a result pipe are handed to RootThreadService over Binder.
  3. The root service deserializes and executes the callable on a daemon thread.
  4. The result is serialized back into the result pipe.
  5. The caller reads the result pipe and resumes.

Parcelable objects are serialized using Android's own Parcel mechanism instead of Kryo to avoid cross-process reference-ID divergence.


Setup

JitPack

Add the JitPack repository to your settings file:

// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven("https://jitpack.io")
}
}

Dependencies

// build.gradle.kts
dependencies {
implementation("com.github.MMRLApp.RootThread:thread:<version>")
// KSP code generation (optional — see KSP Code Generation below)
ksp("com.github.MMRLApp.RootThread:thread-ksp:<version>")
}

Lifecycle binding

The simplest setup — attach the observer once in onCreate:

// KotlinclassMainActivity : AppCompatActivity() {
overridefunonCreate(savedInstanceState:Bundle?) {
super.onCreate(savedInstanceState)
addRootThread(this) // binds onStart, unbinds onStop
}
}
// JavapublicclassMainActivityextendsComponentActivity {
@OverrideprotectedvoidonCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
getLifecycle().addObserver(newRootThreadLifecycleObserver(this));
}
}

KSP Code Generation

The optional thread-ksp artifact provides a KSP processor that generates boilerplate-free RootCallable wrappers from annotated functions.

KSP Setup

// build.gradle.kts
plugins {
id("com.google.devtools.ksp") version "<ksp-version>"
}
dependencies {
implementation("com.github.MMRLApp.RootThread:thread:<version>")
ksp("com.github.MMRLApp.RootThread:thread-ksp:<version>")
}

@RootFunction

Annotate any top-level function or companion object function that should run in the root process. If the function accepts a RootOptions parameter, it is automatically injected from call(options) at runtime and excluded from the constructor.

// Top-level
@RootFunction
funloadModules(): List<Module> {
returnFile("/data/adb/modules").listFiles()
?.filter { it.isDirectory }
?.mapNotNull { parseModule(it) }
.orEmpty()
}
// With RootOptions (if you use RootOptions, always place it first)
@RootFunction
funreadFile(options:RootOptions, path:String): String {
returnFile(path).readText()
}
// Inside a companion objectclassModulesRepository {
companionobject {
@RootFunction
funloadModules(): List<Module> { ... }
}
}

Generated API

For each annotated function the processor generates a file under dev.mmrlx.threading:

// Generated: dev/mmrlx/threading/RootedLoadModules.ktpublicclassRootedLoadModules : RootCallable<List<Module>>, Serializable {
overridefuncall(options:RootOptions): List<Module> = loadModules()
}
// Extension on RootScope — the public API surfacefun RootScope.loadModules(): RootCallable<List<Module>> =RootedLoadModules()

Usage:

// Suspend call via companionval modules =RootedLoadModules().asThread()
// As a Flow via companion (if you compose)val modules by RootedLoadModules().asFlow().collectAsState(emptyList())

Core Concepts

RootCallable

RootCallable<T> is a @FunctionalInterface (usable as a lambda in both Java and Kotlin) that represents work to execute in the root process.

val callable =RootCallable<String> {
File("/proc/version").readText()
}
RootCallable<String> callable = options -> newFile("/proc/version").readText();

RootConsumer

RootConsumer<T, R> is a receiver-scoped variant — it receives a typed object from the caller's process and returns a result. Used by the rootBlocking receiver extension and RootThreadExtensions.rootBlocking.

val consumer =RootConsumer<PackageManager, List<PackageInfo>> { pm ->
pm.getInstalledPackages(0)
}

Serialisation rules

Because callables are serialized across a process boundary, they must be Kryo-compatible:

✅ Safe to capture❌ Never capture
Primitives (Int, Boolean, String, …)Context / Activity / Fragment
Parcelable objectsView or any UI object
Plain data classesNon-serialisable lambdas or anonymous classes
EnumsViewModel, LiveData, Flow
Serializable objectsBinder objects (other than via Parcel)

API Reference — Kotlin

rootThread { }

Suspends the coroutine, executes the block in the root process, and resumes with the result. Dispatches onto Dispatchers.IO automatically.

// In any suspend function:val kernel = rootThread { File("/proc/version").readText() }
val hasSu = rootThread { File("/system/bin/su").exists() }

Signature:

suspendfun <T> rootThread(block:RootCallable<T>): T

Throws:IOException on IPC or remote failure.


T.rootThread { }

Receiver-scoped variant. Passes this into the root process as the first argument of the callable.

val packages = packageManager.rootThread { pm ->
pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
}

Signature:

suspendfun <T, R> T.rootThread(block:RootConsumer<T, R>): R

Throws:IOException on IPC or remote failure.


rootLaunch / rootAsync

Launch-style wrappers for use inside a CoroutineScope. Exceptions propagate through the scope's job like any other coroutine failure.

// Fire and forget
viewModelScope.rootLaunch {
Runtime.getRuntime().exec("chmod 777 /data/local/tmp/file")
}
// With a result via Deferredval deferred = viewModelScope.rootAsync { readRootDatabase() }
val rows = deferred.await()

Signatures:

fun CoroutineScope.rootLaunch(block:RootCallable<Unit>): Jobfun <T> CoroutineScope.rootAsync(block:RootCallable<T>): Deferred<T>

rootFlow { }

Returns a cold Flow<T> that executes the callable on each collection and emits a single value.

rootFlow { File("/proc/version").readText() }
.onEach { version -> textView.text = version }
.launchIn(lifecycleScope)
// Combine with other operators
rootFlow { getPrivilegedData() }
.map { it.transform() }
.catch { e -> showError(e) }
.flowOn(Dispatchers.IO)
.collect { result -> updateUi(result) }

Signature:

fun <T> rootFlow(block:RootCallable<T>): Flow<T>

rootBlocking { }

Executes the block in the root process, blocking the calling thread. Must not be called on the main thread.

// On a background thread / Worker / HandlerThread:val exists = rootBlocking { File("/system/bin/su").exists() }

Signature:

@Throws(IOException::class, InterruptedException::class)
fun <T> rootBlocking(block:RootCallable<T>): T?

T.rootBlocking { }

Receiver-scoped blocking variant.

val packages = packageManager.rootBlocking { pm ->
pm.getInstalledPackages(0)
}

Signature:

@Throws(IOException::class, InterruptedException::class)
fun <T, R> T.rootBlocking(block:RootConsumer<T, R>): R?

rootBlocking with timeout

Blocking execution with a deadline. Throws TimeoutException if the root process does not respond in time.

val result = rootBlocking(5, TimeUnit.SECONDS) { readHeavyRootFile() }

Signature:

@Throws(IOException::class, InterruptedException::class, TimeoutException::class)
fun <T> rootBlocking(timeout:Long, unit:TimeUnit, block:RootCallable<T>): T?

rootBlock DSL

Groups multiple root calls into a structured block. Each exec { } call is an independent IPC round-trip but they share a readable sequential scope.

val data = rootBlock {
val hasSu = exec { File("/system/bin/su").exists() }
val kernel = exec { File("/proc/version").readText() }
val modules = exec { File("/data/adb/modules").listFiles()?.size ?:0 }
mapOf(
"hasSu" to hasSu,
"kernel" to kernel,
"modules" to modules,
)
}

Signatures:

suspendfun <T> rootBlock(block:suspendRootBlockScope.() ->T): TclassRootBlockScope {
suspendfun <T> exec(block:RootCallable<T>): T
}

rootThreadCatching / rootBlockingCatching

Result-wrapped variants for railway-oriented error handling. Never throw — failures are delivered as Result.failure.

// Suspend
rootThreadCatching { riskyRootOperation() }
.onSuccess { result -> updateUi(result) }
.onFailure { error ->Log.e(TAG, "Root failed", error) }
// Blocking (off main thread)val result = rootBlockingCatching { File("/proc/version").readText() }
if (result.isSuccess) {
textView.text = result.getOrNull()
}

Signatures:

suspendfun <T> rootThreadCatching(block:RootCallable<T>): Result<T>
fun <T> rootBlockingCatching(block:RootCallable<T>): Result<T>

RootThread { } invoke syntax

Syntactic sugar allowing RootThread to be called like a function inside any suspend context.

// Equivalent to rootThread { ... }val result =RootThread { doPrivilegedWork() }

Future.awaitRoot()

Suspends a coroutine until a Future<T> (returned by RootThread.submit()) completes. Implemented with suspendCancellableCoroutineno kotlinx-coroutines-jdk8 dependency required.

  • Runs Future.get() on Dispatchers.IO so the main thread is never blocked.
  • Cancels the Future if the coroutine is cancelled.
  • Unwraps ExecutionException so callers see the real cause.
val future =RootThread.submit<String> { readPrivilegedFile() }
// Cancel if needed:
future.cancel(true)
// Or await in a coroutine:val result = future.awaitRoot()

Signature:

suspendfun <T> Future<T>.awaitRoot(): T

API Reference — Java

RootThread.submit()

Submits a callable to the root process and returns a Future<T> immediately. The future resolves with the result or fails with an IOException.

Future<Boolean> future = RootThread.submit(() ->
newFile("/system/bin/su").exists()
);
// Optional cancellationfuture.cancel(true);
// Join elsewhere (not on main thread)booleanresult = future.get(5, TimeUnit.SECONDS);

Signature:

publicstatic <T> Future<T> submit(@NonNullRootCallable<T> callable)

RootThread.executeBlocking()

Submits a callable and blocks the calling thread until the result is available. Must not be called on the main thread.

executorService.execute(() -> {
try {
Stringkernel = RootThread.executeBlocking(
() -> newString(Files.readAllBytes(Paths.get("/proc/version")))
);
runOnUiThread(() -> textView.setText(kernel));
} catch (IOException | InterruptedExceptione) {
Log.e(TAG, "Root IPC failed", e);
}
});

Signature:

publicstatic <T> TexecuteBlocking(@NonNullRootCallable<T> callable)
throwsIOException, InterruptedException

RootThread.executeBlocking() with timeout

try {
Booleanexists = RootThread.executeBlocking(
() -> newFile("/system/bin/su").exists(),
5, TimeUnit.SECONDS
);
} catch (TimeoutExceptione) {
Log.e(TAG, "Root process timed out");
} catch (IOException | InterruptedExceptione) {
Log.e(TAG, "IPC error", e);
}

Signature:

publicstatic <T> TexecuteBlocking(
@NonNullRootCallable<T> callable,
longtimeout,
@NonNullTimeUnitunit
) throwsIOException, InterruptedException, TimeoutException

RootThreadExtensions.rootLaunch()

Async fire-and-forget with an optional callback delivered on a specified Executor (or the main thread by default).

// Callback on main thread (default)RootThreadExtensions.rootLaunch(
() -> readRootData(),
newRootThreadExtensions.RootCallback<String>() {
@OverridepublicvoidonSuccess(Stringresult) {
textView.setText(result); // main thread
}
@OverridepublicvoidonFailure(Throwableerror) {
Log.e(TAG, "Failed", error);
}
}
);
// Callback on a custom executorExecutordbExecutor = Executors.newSingleThreadExecutor();
RootThreadExtensions.rootLaunch(
() -> readRootDatabase(),
newRootThreadExtensions.RootCallback<List<Row>>() {
@OverridepublicvoidonSuccess(List<Row> rows) {
dao.insertAll(rows); // already on dbExecutor
}
@OverridepublicvoidonFailure(Throwablee) { /* handle */ }
},
dbExecutor
);

Signatures:

// Callback on main threadpublicstatic <T> Future<T> rootLaunch(
@NonNullRootCallable<T> callable,
@NullableRootCallback<T> callback
)
// Callback on custom executorpublicstatic <T> Future<T> rootLaunch(
@NonNullRootCallable<T> callable,
@NullableRootCallback<T> callback,
@NonNullExecutorexecutor
)
// Fire and forget, no callbackpublicstaticFuture<Void> rootLaunch(@NonNullRootCallable<Void> callable)

RootThreadExtensions.rootBlocking()

Receiver-scoped blocking execution. Equivalent to the Kotlin T.rootBlocking { } extension.

PackageManagerpm = getPackageManager();
executorService.execute(() -> {
try {
List<PackageInfo> packages = RootThreadExtensions.rootBlocking(
pm,
manager -> manager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
);
runOnUiThread(() -> adapter.setData(packages));
} catch (IOException | InterruptedExceptione) {
Log.e(TAG, "Failed", e);
}
});

Signatures:

publicstatic <T, R> RrootBlocking(
@NonNullTreceiver,
@NonNullRootConsumer<T, R> block
) throwsIOException, InterruptedExceptionpublicstatic <T, R> RrootBlocking(
@NonNullTreceiver,
@NonNullRootConsumer<T, R> block,
longtimeout,
@NonNullTimeUnitunit
) throwsIOException, InterruptedException, TimeoutException

RootThreadExtensions.addRootThread()

Lifecycle-aware bind/unbind as a static method (Java equivalent of the Kotlin extension).

RootThreadExtensions.addRootThread(this, context);

Lifecycle

RootThreadLifecycleObserver

The preferred Java approach. Stores applicationContext internally to prevent leaks.

// ActivitygetLifecycle().addObserver(newRootThreadLifecycleObserver(this));
// FragmentgetViewLifecycleOwner().getLifecycle()
.addObserver(newRootThreadLifecycleObserver(requireContext()));
// Kotlin extension — equivalent one-liner
addRootThread(requireContext())

Manual bind / unbind

For cases where lifecycle integration is not appropriate (services, background components):

RootThread.bind(context); // call when readyRootThread.unbind(); // call when done
RootThread.bind(context)
RootThread.unbind()

Threading model

LayerThread
Caller (Kotlin)Any — dispatched to Dispatchers.IO internally
Caller (Java async)RootThread cached executor (RootThread-IPC threads)
Caller (Java blocking)Caller's thread — must not be main thread
Root serviceBinder thread (returns immediately); work on RootThread-Worker daemon thread

The root service spawns a new named daemon thread per call so the Binder thread is never parked, eliminating ANR risk.


FD ownership contract

createPipe() → [callableRead, callableWrite]
createPipe() → [resultRead, resultWrite ]
Caller:
write callable → callableWrite → (AutoCloseOutputStream closes it, sends EOF)
svc.execute(callableRead, resultWrite) ← service owns these two from here
read result ← resultRead ← caller owns this until done
On error before execute():
caller closes all four FDs

Serialisation internals

KryoManager is a pre-configured Kryo instance:

SettingValue
Registration requiredfalse (class names are written to the stream)
Referencestrue (handles cyclic graphs in non-Parcelable objects)
Instantiation strategyDefaultInstantiatorStrategy + StdInstantiatorStrategy (no-arg constructor not required)
Parcelable serialiserCustom ParcelableSerializer — uses Parcel.marshall() / unmarshall()

A freshKryoManager instance is used for each write and each read, keeping reference tables completely independent across the pipe boundary.


Error handling

Error scenarioBehaviour
Remote callable throwsException is serialised and re-thrown as IOException("Remote exception", cause)
IPC write failsIOException("IPC write/execute failed", cause)
Deserialisation fails in rootIOException("Deserialisation failed in root process", cause)
Root service disconnectsCompletableFuture is replaced; next call blocks until reconnect
Coroutine cancelledFuture.cancel(true) is called; CancellationException propagates normally
InterruptedExceptionThread interrupt flag is restored; wrapped as CancellationException in coroutine context

Rules and gotchas

Serialisation

  • RootCallable and RootConsumer lambdas must be Kryo-serializable. Do not capture Context, View, or any non-serializable object.
  • Prefer capturing primitive values or Parcelable objects. For complex objects, pass them as the receiver via T.rootThread { } or rootBlocking(receiver) { }.

Threading

  • Never call executeBlocking or rootBlocking on the main thread — they block the calling thread.
  • Prefer rootThread { } (Kotlin suspend) or rootLaunch (Java async) in UI code.

Lifecycle

  • Always use RootThreadLifecycleObserver or addRootThread() to ensure the service is unbound when the component stops. Failing to unbind leaks the root process connection.
  • RootThreadLifecycleObserver stores applicationContext internally — passing an Activity context is safe.

Cancellation

  • rootLaunch / rootAsync respect coroutine cancellation: the underlying Future is canceled and the root worker thread is interrupted.
  • rootFlow is cold — collection starts a new IPC round-trip each time.

About

An Android library for executing arbitrary code in a privileged root process via Binder IPC, with first-class support for both Java (Future, ExecutorService) and Kotlin (coroutines, Flow, DSL).

Resources

Stars

13 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages