The fundamental idea is not that different from proot. In fact, it's inspiried by proot -- both can intercept system calls through ptrace() and simulate the chroot() system call so that we can chroot into a different Linux Distro on a non-rooted phone.
The main difference: This solution is multithreaded, while proot itself is single threaded. (And yes, I know they also have a Rust version)
- Multithreading mattered to me, more than the ptrace overhead. Because proot has a hardtime supporting heavy I/O from a multithreaded JVM running game servers, for example, to host Minecraft.
- This project was an exploration of whether multithreading helps with heavy I/O. But I've been sidetracked by other issues like supporting apt-get.
Most likely, you should just use PRoot instead. It has a history of proven stability and success.
My project is still in its early stage. And it barely works right now. Basic shell commands work but apt-get is broken.
Eventually, my goal is to be able to run Docker container on any mobile device, without needing root, by creating a configuration file that tells the ptrace how to glue the file system back together. But there is a long way to go.
In Cargo.toml, the Rust compiler edition and many dependencies are outdated. (I used very old versions of libc and nix. And there are a lot of dirty workarounds in my code to fix missing syscall constants)
The following designs are outdated:
- The "mods" crate is outdated.
- The "procfs" crate is an empty placeholder for procfs simulation. It is not being used at all.
Regarding "mods" crate, the idea was to implement optional features and logics here. In reality, this proved unfit. Dynamically loading mods slows down Rust because it involves dyn pointers.
To actually achieve "turning on/off features at runtime", we should just create a better config schema so that we can customize "sysaug" crate behavior with a descriptive json config that's passed in during pcontainer initialization
By default, pcontainer will try to run ptrace() syscalls on dedicated threads (one thread per tracee process)
But this requires the permission for PTRACE_ATTACH. And on some systems, this permission is blocked, and tracer can only attach to their direct children from main threads.
In that case, pcontainer will try to cumulate ptrace() syscalls on main thread from all tracee processes, and offload each tracee's own event loop and calculations to other threads. (Main thread is busy executing ptrace() calls while other threads queue ptrace actions)
Both sysaug and mods crates are supposed to modify the behavior of system calls. But here are the differences:
Sysaug is a backend while mods are the frontend.
sysaugis a backend, low level ability to modify specific syscalls (i.e. remapping path of openat())modsare the fronted, high level features (provides chroot / root, without having root)
Mods are dynamic but sysaug isn't.
modscan be imported, enabled, and disabled dynamically. Disabling mods should improve efficiency.sysaugis not dynamic and cannot be turned on/off or imported without rebuilding the binary, but frontend mods should eventually be able to.
Within sysaug crate, there are two major parts:
aug_*.rsdefines Augments which have separate concerns based on the type of syscalls they augmenthandler.rsdefines the core "state machine" that translates TraceeHandlerStates and various trackers of tracee's stack and hacky mmap injection addresses, into how exactly to rewrite every syscalls + followup on them in multi-step algorithms.
Additionally,
ptracecrate is responsible for abstracting away the low level pointer safety of translating tracee pointers to/from ptrace callsexecutorcrate is responsible for abstracting away the low level thread safety of running ptrace calls across threads- And,
executorcrate also implements a basic Thread-Per-Core "async" runtime that fits my realtime tracer needs better:PtraceAsyncRuntime
This PtraceAsyncRuntime is mostly an enabler of an anti-pattern: I chose to write the state machine of a tracer using async syntax sugar, instead of manually writing out the state machine as literal switch case listing and migrating between all checkpoint states. Another added benefit of PtraceAsyncRuntime is that all logics within it are forced to run on the same thread, so I can avoid Arc<Mutex<>> and use RefCell instead.
Caveat: this refactor from "synchronous spaghetti" to "async as a hacky state machine syntax" is still ongoing. You will see two hacky logics live next to each other. The synchronous logics use a ton of Arc<Mutex<>> types. And the async logics are always Pinned, not truly "async", and are more of a hacky use of the underlying state machine than a use of true async events
Right now the repo lives in a very bad state and has a lot more runtime overhead than needed. I intend to move fully into async in hope that removing Arc will fix some of the overhead. But I'm starting to think I misunderstood how Rust handles async, and how heavy it truly is.
Copyright (c) 2026 Zhongzhi Yu
This project is licensed under the GNU Lesser General Public License v3.0 (LGPLv3) - see COPYING for details
How to debug problems:
RUST_LOG=TRACE RUST_BACKTRACE=1 cargo run -- --chroot xxx --root |& ansi2txt | tee ~/logfile | grep -v TRACE | grep -v DEBUG
Overhead: Tested on Android Termux:
proot slows down git status to about 5x its original run time.
- original total wall time: about 10ms
- proot total wall time: about 50ms
As long as our parallel proot doesn't slow down the tracee by more than 5x. It should be fine.
How to cross-compile for Android:
- Install
arm-linux-*-gccandaarch64-linux-*-gcc- Depending on the license of these softwares, the
*portion might differ - We prefer Musl libc toolchains.
- Depending on the license of these softwares, the
- Update your
~/.cargo/config:[target.armv7-unknown-linux-musleabihf] rustflags = ["-C", "target-feature=+crt-static"] linker = "arm-linux-foobar-gcc" [target.armv7-unknown-linux-musl] rustflags = ["-C", "target-feature=+crt-static"] linker = "aarch64-linux-foobar-gcc" - Run
cargo build --target=armv7-unknown-linux-musleabihf --release - Or run
cargo build --target=aarch64-unknown-linux-musl --release - Android permissions
- The terminal emulator must request specific permissions to unlock the ability to execute
./dockify. The exact permission name is unknown. - Older Android versions work better with https://f-droid.org/en/packages/org.galexander.sshd/
- Android 10 and above require executables to be codesigned
- Termux is the only solution that works well in this situation. Here is a page from their discussion.
- But apparently, IT'S EASIER IF
dockifyIS CODE SIGNED AS part of the readonly APK.
- The terminal emulator must request specific permissions to unlock the ability to execute
