Merge pull request #113 from maxjivi05/feat/performance-fixes-2

This commit is contained in:
Kurt Himebauch authored and GitHub committed 2026-10-01 17:08:17 -04:00
commit aa05ab03fa
19 files changed
+1626 -2

No files matched your search

+4 -1
View File
@@ -157,7 +157,7 @@ jobs:
uses: actions/cache@v4
with:
path: app/src/main/assets/linuxfs
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'tools/linuxfs/preload/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env', 'tools/wlroots/release.env', 'tools/mangoapp/*') }}
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'tools/linuxfs/preload/*.c', 'tools/proot/fastpath/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env', 'tools/wlroots/release.env', 'tools/mangoapp/*') }}
- name: Build libfakeinput.so and libblsession.so (glibc aarch64)
if: steps.preloads.outputs.cache-hit != 'true'
@@ -176,6 +176,9 @@ jobs:
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
-o "$D/libblsession.so" tools/linuxfs/preload/*.c -ldl
aarch64-linux-gnu-strip --strip-unneeded "$D/libblsession.so"
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
-o "$D/libblfastpath.so" tools/proot/fastpath/fastpath.c -ldl
aarch64-linux-gnu-strip --strip-unneeded "$D/libblfastpath.so"
for script in tools/linuxfs/overlay/usr/local/bin/bannerlator-*; do
install -Dm644 "$script" "$D/usr/local/bin/$(basename "$script")"
done
@@ -137,6 +137,7 @@ class MainActivity : ComponentActivity() {
private var gamescopeRealtime by mutableStateOf(false)
private var gpuClockPin by mutableStateOf(false)
private var prootNoSeccomp by mutableStateOf(false)
private var prootFastPath by mutableStateOf(true)
private var guestHostname by mutableStateOf(SessionPrefs.DEFAULT_GUEST_HOSTNAME)
private var phantomWarning by mutableStateOf<String?>(null)
private var phantomProcessStatus by mutableStateOf(PhantomProcessStatus.NOT_APPLICABLE)
@@ -1052,7 +1053,7 @@ class MainActivity : ComponentActivity() {
tuSysmem = tuSysmem, zinkLazy = zinkLazy, glThread = glThread, noGlError = noGlError, noXalia = noXalia,
gamescopeRealtime = gamescopeRealtime,
gpuClockPin = gpuClockPin,
prootNoSeccomp = prootNoSeccomp, guestHostname = guestHostname, phantomWarning = phantomWarning,
prootNoSeccomp = prootNoSeccomp, prootFastPath = prootFastPath, guestHostname = guestHostname, phantomWarning = phantomWarning,
onClientOverride = { on -> SessionPrefs.setClientCpusOverride(this, on); clientOverride = on },
onTuSysmem = { on -> SessionPrefs.setTuSysmem(this, on); tuSysmem = on },
onZinkLazy = { on -> SessionPrefs.setZinkLazy(this, on); zinkLazy = on },
@@ -1062,6 +1063,7 @@ class MainActivity : ComponentActivity() {
onGamescopeRealtime = { on -> SessionPrefs.setGamescopeRealtime(this, on); gamescopeRealtime = on },
onGpuClockPin = { on -> SessionPrefs.setGpuClockPin(this, on); gpuClockPin = on },
onProotNoSeccomp = { on -> SessionPrefs.setProotNoSeccomp(this, on); prootNoSeccomp = on },
onProotFastPath = { on -> SessionPrefs.setProotFastPath(this, on); prootFastPath = on },
onGuestHostname = { name -> SessionPrefs.setGuestHostname(this, name) },
onClientCore = { core, on ->
clientCores = if (on) clientCores + core else clientCores - core
@@ -1162,6 +1164,7 @@ class MainActivity : ComponentActivity() {
gamescopeRealtime = SessionPrefs.gamescopeRealtime(this)
gpuClockPin = SessionPrefs.gpuClockPin(this)
prootNoSeccomp = SessionPrefs.prootNoSeccomp(this)
prootFastPath = SessionPrefs.prootFastPath(this)
guestHostname = SessionPrefs.guestHostname(this)
refreshPhantomStatus()
}
@@ -154,6 +154,7 @@ object DeviceReport {
k("Skip xalia", SessionPrefs.noXalia(context))
k("gamescope realtime", SessionPrefs.gamescopeRealtime(context))
k("proot without seccomp", SessionPrefs.prootNoSeccomp(context))
k("proot fast path", SessionPrefs.prootFastPath(context))
k("Guest host name", SessionPrefs.guestHostname(context))
k("DirectAudio for games", SessionPrefs.directAudio(context))
k("Stretch games to fill", SessionPrefs.forceFullscreen(context))
@@ -0,0 +1,45 @@
package com.droiddeck.launcher.runtime
import android.content.Context
import com.droiddeck.launcher.session.SessionPrefs
import java.io.File
import java.security.MessageDigest
/**
* proot's fast path: the session's path lookups answered inside each process by libblfastpath
* (tools/proot/fastpath), which issues the translated call from a trampoline page proot's seccomp
* filter lets through (tools/proot/patches/0014). A stat or an open costs about a microsecond
* instead of a 15-60 us round trip through the tracer.
*
* The library maps guest paths to host paths itself, so it has to be told the rootfs and the
* binds exactly as proot was given them. Both sides carry a key derived from that view - proot
* PROOT_FASTPATH=<key>, the guest PROOT_FP_KEY=<key> - and the library answers nothing unless the
* process that traces it has the same key: a program that inherited the variables but runs under
* another proot (a Flatpak sandbox, a one-off command) leaves every call to its own proot.
*
* Needs proot's seccomp filter, so "Run proot without seccomp" turns it off as well.
*/
object ProotFastPath {
const val LIBRARY = "/usr/local/lib/libblfastpath.so"
fun enabled(context: Context): Boolean =
SessionPrefs.prootFastPath(context) && !SessionPrefs.prootNoSeccomp(context)
/** The key for this rootfs and these binds, or null when the library cannot be told them. */
fun key(root: File, binds: List<String>): String? {
if (binds.any { it.contains('|') } || root.path.contains('|')) return null
val digest = MessageDigest.getInstance("SHA-256")
.digest((root.path + "\n" + binds.joinToString("\n")).toByteArray())
return digest.take(12).joinToString("") { "%02x".format(it) }
}
/** What proot's own environment carries. */
fun hostEnv(key: String): Pair<String, String> = "PROOT_FASTPATH" to key
/** The guest's assignments (for `env -i`), describing the view proot was given. */
fun guestEnv(root: File, binds: List<String>, key: String): List<String> = listOf(
"PROOT_FP_ROOT=" + root.path,
"PROOT_FP_BINDS=" + binds.joinToString("|"),
"PROOT_FP_KEY=$key",
)
}
@@ -5,6 +5,7 @@ import android.os.Environment
import android.util.Log
import com.droiddeck.launcher.core.FileUtils
import com.droiddeck.launcher.runtime.LinuxRuntime
import com.droiddeck.launcher.runtime.ProotFastPath
import java.io.File
/** Everything the session needs written into the runtime before it starts. */
@@ -29,6 +30,7 @@ object SessionFiles {
"usr/local/bin/bannerlator-game-env" to "usr/local/bin/bannerlator-game-env",
"libblsession.so" to "usr/local/lib/libblsession.so",
"libfakeinput.so" to "usr/local/lib/libfakeinput.so",
"libblfastpath.so" to "usr/local/lib/libblfastpath.so",
"usr/local/bin/bannerlator-session" to "usr/local/bin/bannerlator-session",
"usr/local/bin/bannerlator-steam-compat" to "usr/local/bin/bannerlator-steam-compat",
"usr/local/bin/bannerlator-steam-install" to "usr/local/bin/bannerlator-steam-install",
@@ -151,6 +153,9 @@ object SessionFiles {
if (!File(Environment.getExternalStorageDirectory(), NO_PAD_SWITCH).exists()) {
preload.append("/usr/local/lib/libfakeinput.so\n")
}
// Last, so the others' opens and stats reach it through RTLD_NEXT. It stays idle in any
// process its proot did not describe to it (ProotFastPath).
if (ProotFastPath.enabled(context)) preload.append(ProotFastPath.LIBRARY + "\n")
val etc = File(root, "etc").apply { mkdirs() }
val staged = File(etc, "ld.so.preload.staged")
if (!FileUtils.writeString(staged, preload.toString())
@@ -302,6 +302,17 @@ object SessionPrefs {
prefs(context).edit().putBoolean("prootNoSeccomp", on).apply()
}
/**
* Whether the session's path lookups take proot's fast path (ProotFastPath): answered inside
* each process instead of a round trip through the tracer. On by default; it needs proot's
* seccomp filter, so it is off whenever proot runs without one.
*/
fun prootFastPath(context: Context): Boolean = prefs(context).getBoolean("prootFastPath", true)
fun setProotFastPath(context: Context, on: Boolean) {
prefs(context).edit().putBoolean("prootFastPath", on).apply()
}
const val DEFAULT_GUEST_HOSTNAME = "DroidDeck"
@JvmStatic
@@ -36,6 +36,7 @@ import com.droiddeck.launcher.core.HostProcess
import com.droiddeck.launcher.input.FakeInputWriter
import com.droiddeck.launcher.runtime.LinuxNetworkLinkComponent
import com.droiddeck.launcher.runtime.LinuxRuntime
import com.droiddeck.launcher.runtime.ProotFastPath
import com.droiddeck.launcher.wayland.WaylandCompositor
import java.io.File
import java.text.SimpleDateFormat
@@ -278,6 +279,8 @@ class SessionService : Service() {
// same set-up as a Steam session: it gets what the client and its games are started with.
val steamHere = SessionState.mode == MODE_STEAM || SessionState.mode == MODE_DESKTOP
addClientEnvironment(guest, steamHere)
// Where the fast path's description of proot's view goes, once the binds are known.
val fastPathAt = guest.size
val pulse = startAudio(guest, sessionDir)
@@ -363,6 +366,19 @@ class SessionService : Service() {
FileUtils.clear(File(cacheDir, "shm"))
val binds = sessionBinds(controllersOn, fakeInputDir)
// The fast path is told exactly the rootfs and binds proot is given (ProotFastPath).
val fastPathKey = if (ProotFastPath.enabled(this)) {
val prootBinds = LinuxRuntime.binds(
this, sessionRoot, runtimeDir, Environment.getExternalStorageDirectory(), binds,
)
val root = LinuxRuntime.rootDir(this)
ProotFastPath.key(root, prootBinds)?.also { key ->
val env = ProotFastPath.guestEnv(root, prootBinds, key)
guest.addAll(fastPathAt, env)
shellGuest.addAll(fastPathAt, env)
Log.i(TAG, "proot: fast path on (${prootBinds.size} binds)")
}
} else null
val command = LinuxRuntime.command(
this, sessionRoot, runtimeDir, Environment.getExternalStorageDirectory(), binds, guest,
@@ -379,6 +395,7 @@ class SessionService : Service() {
hostEnv["PROOT_NO_SECCOMP"] = "1"
Log.i(TAG, "proot: seccomp acceleration off by request")
}
fastPathKey?.let { ProotFastPath.hostEnv(it).let { (k, v) -> hostEnv[k] = v } }
val prootLibs = LinuxRuntime.prootLibraryPath(this)
if (prootLibs.isNotEmpty()) hostEnv["LD_LIBRARY_PATH"] = prootLibs
@@ -535,6 +552,10 @@ class SessionService : Service() {
// opened once and read and written with pread/pwrite, which proot never sees. Mesa removes
// the old folder itself once it has gone a week untouched.
guest.add("MESA_DISK_CACHE_DATABASE=1")
// glibc's per-thread rseq registration is refused by Android's app seccomp policy, so
// every thread start paid a SIGSYS that proot answers; the malloc top pad grows the heap
// 16 MB at a time instead of 128 KB, and every brk(2) is a proot stop too.
guest.add("GLIBC_TUNABLES=glibc.pthread.rseq=0:glibc.malloc.top_pad=16777216")
if (SessionState.mode == MODE_STEAM) guest.add("BL_STEAMDECK=" + (if (SessionPrefs.steamDeckMode(this)) "1" else "0"))
if (SessionState.mode == MODE_STEAM) guest.add("BL_MANGOAPP=" + (if (SessionPrefs.mangoapp(this)) "1" else "0"))
if (steamHere) guest.add("BL_STEAM_CHANNEL=" + SessionPrefs.steamChannel(this))
@@ -40,6 +40,7 @@ fun PerformancePage(
gamescopeRealtime: Boolean,
gpuClockPin: Boolean,
prootNoSeccomp: Boolean,
prootFastPath: Boolean,
guestHostname: String,
phantomWarning: String?,
onClientOverride: (Boolean) -> Unit,
@@ -51,6 +52,7 @@ fun PerformancePage(
onGamescopeRealtime: (Boolean) -> Unit,
onGpuClockPin: (Boolean) -> Unit,
onProotNoSeccomp: (Boolean) -> Unit,
onProotFastPath: (Boolean) -> Unit,
onGuestHostname: (String) -> Unit,
onClientCore: (Int, Boolean) -> Unit,
onGameCore: (Int, Boolean) -> Unit,
@@ -127,6 +129,12 @@ fun PerformancePage(
"May fix missing syscall errors on some kernels, but can reduce performance.",
prootNoSeccomp, onChange = onProotNoSeccomp,
)
ToggleRow(
host, "fastpath", "proot fast path",
if (prootNoSeccomp) "Needs proot's seccomp filter: off while proot runs without seccomp."
else "Answers file lookups inside each program instead of through proot. Faster loading and less stutter; turn off if a program cannot find its files.",
prootFastPath && !prootNoSeccomp, enabled = !prootNoSeccomp, onChange = onProotFastPath,
)
}
SettingsGroup("Session identity") {
var draft by remember(guestHostname) { mutableStateOf(guestHostname) }
+310
View File
@@ -0,0 +1,310 @@
# proot, glibc and where the time goes
Research notes for `feat/performance-fixes-2` (2026-09-30). The question was whether Steam and the
games should keep running under proot, run directly on glibc, or a mix of the two, and what the
fastest correct setup is. Everything below was measured on a Snapdragon 8 Elite Gen 5 (SM8850, kernel
6.12.38-android16, Android 16) over adb, unless it says otherwise.
## Short answer
- **Don't drop proot.** On Android it does two jobs. It translates paths. It also answers every
syscall that Android's app seccomp policy blocks (`rseq`, `set_robust_list`, `faccessat2`,
`fchmodat2`, `openat2`, the `set*id` family, `futex_waitv`, ...).
- Stock Arch glibc without a tracer is killed by `SIGSYS` on its first `pthread_create`. That
stays true even with an in-process `SIGSYS` handler, because glibc makes those calls with every
signal blocked.
- Going "pure glibc" means building Termux-style glibc (relocated prefix, blocked syscalls
rewritten), an exec shim for every ELF Steam self-updates, a `/proc/self/exe` emulator for
Chromium, path redirects for `/tmp`, `/dev/shm`, `/mnt/*` and the fake `/proc` and `/sys`
files, and Wine's hard-coded `/tmp/.wine-<uid>`.
- That is a project of its own, and its failure mode is a crash instead of a slow call.
- **During gameplay, proot already costs almost nothing**, thanks to patches 0004–0013. GPU submits
(`ioctl`), `futex`, `read`/`write`, `mmap`, `ppoll`/`epoll`, `sendmsg`/`recvmsg` and
`clock_gettime` (via the vDSO) all run at native speed under the filter.
- **What proot still costs** is every *path* syscall, `execve`, `brk`, thread and process creation,
and every Android-blocked syscall: **15–60 µs each**, against ~0.5 µs natively. All of it goes
through **one tracer thread**, which tops out around 75–95k traps/s. Steam start-up, game loading
and asset streaming, Proton's Python, and any burst on several threads at once all hit this.
- **Most of that cost is not proot's code but the two cross-core wake-ups per trap.** Any
out-of-process mechanism pays them: ptrace, and seccomp user-notification too (measured the same).
The only real fix is to not leave the process.
- **Recommendation: proot plus an in-process fast path.** A preloaded library answers the common
path calls inside the process. It sends them through a trampoline page that proot's filter lets
through, and it falls back to proot for anything it can't prove proot would answer the same way.
- The prototype is in this branch: `tools/proot/fastpath/` and `patches/0014`.
- Speed: `stat` 25 µs → 0.6 µs, `open` 25 µs → 0.9 µs, missing-file lookups 23 µs → 1.6 µs.
Eight threads doing `stat` went from 89k/s to 3.6M/s.
- Correctness: an equivalence suite of awkward paths produced byte-identical results.
- It only uses syscalls the app policy allows.
## How a session runs today
```
SessionService ─HostProcess─▶ libproot.so (ONE tracer process, nice -6 after 2 s)
└─ /usr/bin/env -i … bannerlator-session steam
└─ gamescope ─▶ bannerlator-session (BL_INSIDE) ─▶ steamrtarm64/steam (+ steamwebhelper/CEF tree)
└─ reaper ─▶ bannerlator-game-env ─▶ bannerlator-proton ─▶ Valve ARM64 Proton (python)
└─ wine (arm64ec, FEX loaded in-process as a DLL) ─▶ wineserver, game.exe, …
```
- Every process in the tree, from the session script to the game, is a tracee of the same proot.
- The options are `--kill-on-exit`, `--kernel-release=…` (kompat, for the hostname), `-i uid:uid`
(fake_id0, for Xwayland's `setgid`/`setuid`), and about 25 binds: `/dev`, `/proc`, `/sys`, fake
`/proc` and `/sys` files, the GPU node, `/dev/shm`, storage and the game libraries.
- `/etc/ld.so.preload` loads `libblsession.so` (the `tools/linuxfs/preload/*.c` shims) into every
guest process.
- The Flatpak sandboxes (`BwrapSpawner`) already run in **separate** proot instances, started from
the Android side.
### What proot does per traced syscall
The tracee runs into proot's seccomp filter, which returns `SECCOMP_RET_TRACE`. Then:
1. The kernel stops the tracee and wakes the tracer: a cross-core IPI, and possibly an exit from a
deep idle state.
2. The tracer reads the registers (`PTRACE_GETREGSET`) and reads the path (`process_vm_readv`).
3. It canonicalises the path against the rootfs and bindings: an `lstat` per component, or one
`O_PATH` open plus a `/proc/self/fd` readlink with patch 0002.
4. It writes the host path into a scratch area in the tracee (`process_vm_writev`) and updates the
registers.
5. It resumes the tracee, which is another cross-core wake-up.
Calls flagged `FILTER_SYSEXIT` stop a second time on the way out, for example to translate the
result of `readlink` or `getcwd`. Android-blocked syscalls arrive as a `SIGSYS` signal stop instead,
and proot emulates them (`src/tracee/seccomp.c`). Fork, clone, exec and thread exit each add
`PTRACE_EVENT_*` stops.
## Measurements
The tools are in `tools/proot/bench/`; `run-device.sh` explains the device layout.
**Caveat:** `adb shell` is *not* under the app seccomp filter. Mechanism costs are the same, but
syscalls that Android blocks only for apps (see below) can't be reproduced there.
**Caveat:** the phone in Doze (screen off) runs everything about 7× slower. All numbers below were
taken with the screen awake.
### Per call, as a session runs proot today
`sysbench` in the patched (`patched`) and unpatched (`vanilla`) columns, using the session's own
proot options and a similar set of binds.
| call | direct glibc | termux proot (vanilla) | this repo's proot (0001–0013) | + fast path (0014) |
|---|---|---|---|---|
| getppid / futex / ioctl / pread (untraced) | 0.1–0.4 µs | futex **16.6**, fstat **32.8**, ioctl **30.5** µs | 0.2–0.4 µs | 0.2–0.4 µs |
| stat, deep absolute path | 0.8 µs | 41.7 | 25.3 | **0.6** |
| open + close | 1.7 µs | 40.4 | 25.3 | **0.9** |
| access | | 27.2 | 26.2 | **1.0** |
| fstatat relative to a dirfd | | 45.5 | 26.8 | **1.9** |
| stat of a missing file | | 38.9 | 22.9 | **1.6** |
| readlink /proc/self/exe | | 42.8 | 38–43 | (proot) |
| getcwd / uname / brk | | 31–35 | 30–33 | (proot) |
| memfd_create | | 20.4 | 18.6 | (proot) |
| pthread create+join | | 58 | 48 | 48 |
| fork + exec + wait | ~1.1 ms | 1.16 ms | 0.70–0.78 ms | +0.2–0.4 ms (see open items) |
The patches already in the repo pay off: kompat (0011) and fake_id0 (0012) took `futex`, `fstat` and
`sendmsg` off the trap list, and 0013 did the same for `ioctl`.
### What one trap costs, by mechanism (`mechbench`; the handler does nothing)
| mechanism | cost |
|---|---|
| native syscall | 0.1 µs |
| seccomp filter of 0 / 50 / 150 / 300 compares, allowed syscall | **no difference** (kernel ≥5.11 caches constant decisions per syscall number) |
| ptrace + seccomp, tracer and tracee on the **same core** | 6.2 µs (6.7 µs with register get/set) |
| ptrace + seccomp, **unpinned** (as the app runs it) | 15.5 µs |
| ptrace + seccomp, different cores (same cluster / across clusters) | 34–36 / 42–61 µs |
| ptrace with a spinning tracer, different cores | 20 µs (the tracee's side still has to wake) |
| seccomp user-notification (the "modern proot" design) | 5.7 µs same core, 34–58 µs across, 43 µs unpinned; `SYNC_WAKE_UP` doesn't help |
| **in-process SIGSYS** (`SECCOMP_RET_TRAP`) | **0.83 µs** |
| openat2 `RESOLVE_IN_ROOT` (kernel-side chroot lookup) | 1.0 µs, but **blocked in apps** |
Takeaways:
- The tracer's own work is about 8 µs of the ~14 µs same-core trap. Everything above that is
scheduling.
- Moving to seccomp user-notification would be a rewrite for no gain.
- The filter's length doesn't matter. What matters is not breaking the kernel's cache:
- **Any check on an argument or the instruction pointer must come after the syscall-number
dispatch.** The first version of 0014 checked the address first. That made every syscall in
the session run the whole filter, and cost +0.5 ms per exec.
- Patches 0004 and 0013 already follow this rule.
### One tracer for everything (`parstat`: N threads doing `stat`)
| | 1 thread | 4 threads | 8 threads |
|---|---|---|---|
| one proot | 37k/s, 27 µs | 74–95k/s, 42–54 µs | 68–89k/s, **90–118 µs** each |
| two proots, 2 threads each | | 187k/s total, 21 µs | |
| proot + fast path | 1.64M/s, 0.6 µs | 2.76M/s | **3.60M/s, 2.2 µs** |
A burst of path calls on several threads therefore queues behind the single tracer. CEF loading
assets, Proton's Python, a game streaming files on worker threads and the shader-cache threads all
produce such bursts.
### CPU placement
The workload ran with `taskset`; no fast path. Times are per traced call, and the last column is one
fork + exec.
| affinity | stat | exec |
|---|---|---|
| default | 25.8 µs | 750 µs |
| one core | 14.0 µs | 370 µs |
| the two prime cores | 14.6 µs | |
| the six-core cluster | 25.9 µs | |
The scheduler's wake-affine placement helps when it can put the tracer and the tracee together, and
spreading them apart hurts. **Never pin the tracer away from its tracees.** Today `raiseTracer`
only renices it, which is correct.
### Whole programs
Times are in ms, each with proot's ~40 ms start-up already subtracted.
| | direct glibc | vanilla proot | this repo's proot | + fast path |
|---|---|---|---|---|
| python3: 10 stdlib imports | 75–89 | ~130 | ~110 | ~90 (878 of 879 path calls answered in-process) |
| tar of 2.7k files | 10–40 | ~180 | ~80 | ~20 |
| 200 × fork+exec of `true` | 230 | ~600 | ~500 | ~500 |
| find over 2.7k files | 16–23 | noisy | noisy | unchanged: glibc's fts calls internal entry points |
## Android's app seccomp policy decides more than proot does
The app filter is generated from bionic's `SYSCALLS.TXT`, minus the blocklists, plus the
allowlists. It is the same for every app whatever its `targetSdk`, and anything not listed gets
`SECCOMP_RET_TRAP`, a `SIGSYS`. Filters stack, and the strictest one wins: a blocked call never
reaches proot's `RET_TRACE`. It arrives as a `SIGSYS` stop, and proot answers it (usually `ENOSYS`).
Checked against bionic `android16-release`:
| syscall | app policy | who calls it |
|---|---|---|
| `rseq`, `set_robust_list` | **blocked** | glibc, at the start of every thread, while all signals are blocked |
| `faccessat2` | **blocked** | glibc's `faccessat()`, which tries it on **every** call before falling back, so 2 round trips (`access()` calls the plain `faccessat` syscall directly) |
| `fchmodat2`, `openat2` | **blocked** | newer glibc, Flatpak/libglnx |
| `setuid`, `setgid` and the rest of the family | **blocked** | Xwayland (hence `-i`) |
| `futex_waitv`, `io_uring_*`, `landlock_*` | **blocked** | Proton's fsync, some engines |
| `clone3` | blocked before Android 15 | glibc `pthread_create` |
| `close_range` | blocked on Android 12 | |
| `statx`, `process_vm_readv`/`writev`, `memfd_create`, `pidfd_*`, `seccomp` | allowed | |
On top of the ptrace round trips, every one of these costs a signal round trip through proot.
## Every traced syscall: what it needs and whether it can bypass proot
This is the filter of termux proot at `source.env` plus patches 0001–0013. "Fast path" means the
call can be answered in-process with the 0014 design.
| syscalls | why proot traps them | can it bypass proot? |
|---|---|---|
| `openat`/`open`/`creat`, `newfstatat`/`stat`/`lstat`, `statx`, `faccessat`/`access`, `readlinkat` (exit), `mkdirat`, `unlinkat`, `renameat(2)`, `symlinkat`, `linkat`, `fchmodat`, `fchownat`, `utimensat`, `truncate`, `*xattr`, `inotify_add_watch`, `name_to_handle_at` | path translation (guest → host), plus exit fixups for `readlink` and `rename` | **Yes, through libc wrappers.** The prototype covers open, stat, statx, access, readlink, opendir and fopen; the write family is next. Raw `svc` and glibc's internal callers (ld.so, fts, realpath, nss) still go to proot. |
| `chdir`/`fchdir`, `getcwd` (exit) | proot emulates the cwd; until 0014 the kernel's cwd never moved | With 0014 the kernel's cwd follows the guest's. `getcwd` stays in proot (cheap, rare). |
| `execve`/`execveat` | runs the loader, maps `PT_INTERP` inside the rootfs, shebangs, tracks `/proc/self/exe` | **No.** This is the core of what proot is for. Costs 0.4–0.8 ms per exec; the session scripts already avoid exec-heavy loops (`nap`). |
| `brk` (entry and exit) | heap emulation: programs are mapped by proot's loader, so the kernel's brk area is the loader's | No (`PR_SET_MM` needs `CAP_SYS_RESOURCE`). **Fewer calls:** `GLIBC_TUNABLES=glibc.malloc.top_pad=…` grows the heap in larger steps. |
| `bind`, `connect`, `accept(4)`, `getsockname`, `getpeername` | `sun_path` of Unix sockets | Same-path binds (runtime dir, files dir) need no translation, but the filter can't see the address. Possible fast-path candidate. |
| `wait4`/`waitpid`, `ptrace` | ptrace emulation inside the guest (breakpad, gdb) | Could become opt-in; then `wait4` is free (one stop per wait today). |
| `prctl(PR_SET_DUMPABLE)`, `setrlimit`/`prlimit64(RLIMIT_STACK)` | loader and stack fixups | Already narrowed to these arguments (0004). |
| `ioctl(TCSETSF, termios2, FICLONE)` | Android pty policy, FICLONE `EACCES` | Already narrowed to these requests; GPU ioctls are free (0013). |
| `uname`, `sethostname`, `setdomainname` | the `DroidDeck` hostname (kompat) | Rare. Keep. |
| `set*id`, `*setxattr` | fake_id0 identity mode (0012) | Rare. Keep. |
| `memfd_create` | Qt JIT and php workarounds (string argument) | Every Wayland `wl_shm` buffer and Chromium shared memory pays one stop. Could be dropped if the DroidDeck runtime doesn't need those workarounds. |
| `statfs` (exit) | fakes tmpfs for `/dev/shm` | Rare. |
| `open_tree`, `move_mount`, `fspick`, `mount_setattr`, `openat2` | answered `ENOSYS` (0008, 0010) | Fine. |
| Android-blocked calls (previous table) | `SIGSYS` emulation | **Avoidable at the source:** `GLIBC_TUNABLES=glibc.pthread.rseq=0` removes one stop per thread. The rest need a patched glibc. |
| clone/fork/vfork/exec/exit events | tracking tracees | Needed while proot traces. |
## Options weighed
1. **Pure glibc, no proot.** Rejected as the main path, for the reasons in the short answer.
- Closest prior art: huntergdavis/steamclienttermux, which runs `steamrtarm64` on a patched
Termux glibc 2.44 with an exec shim and a `/tmp` and `/dev/shm` redirect preload.
- They report Steam's first window 7× faster and about 5% more FPS, with proot having used 60–65%
of a core.
- **They still run Proton and the games under proot.**
- DroidDeck also has four package names (`tools/release/variants.txt`), so a relocated prefix
would need four builds or Winlator-style padded path rewriting.
2. **Hybrid by process (Steam in proot, game on glibc, or the reverse).** The game side is the
*harder* half. Wine re-execs itself and `wineserver`, which hits `PT_INTERP`. Its server
directory is hard-coded under `/tmp`, and Proton's esync and fsync use `shm_open` (`/dev/shm`)
and `futex_waitv`, which is blocked. A game's steady state is already mostly untraced, so the
gain is in loading. Not worth it before the fast path.
3. **Seccomp user-notification instead of ptrace.** It measures the same as ptrace, and it can't
rewrite arguments, so every call would have to be emulated. No.
4. **In-process `SIGSYS` for everything** (0.8 µs). The kernel resets a blocked `SIGSYS` to the
default action and kills the process (`force_sig_seccomp`, `HANDLER_CURRENT`). glibc and Wine
block all signals around exactly these calls. Unsafe without also trapping `rt_sigprocmask`. No.
5. **proot plus an in-process fast path (this branch).** Recommended. Details below.
6. **A proot-aware glibc**, the follow-up to 5. Rebuild the rootfs's own glibc (same version, same
Arch package) with two changes:
- its internal path entry points (`__open_nocancel`, `__fstatat64`, ld.so's opens) go through the
fast path, which covers ld.so, fts, realpath and nss;
- it never makes the Android-blocked calls, as Termux's `fakesyscall.json` does.
This removes the remaining library-loading traps and all the `SIGSYS` round trips. The rootfs is
versioned (`linuxfs-rN`), so glibc rebuilds stay under control.
7. **On the tracer side, with no in-process changes:**
- **A second proot for the game tree.** Measured 2.5× trap throughput, and Steam/CEF bursts no
longer stall the game. `BwrapSpawner` already shows the pattern: a stand-in process in the
session relays stdio, the exit status and signals to a proot started from the app.
- Leave the tracer unpinned (above).
- Set `GLIBC_TUNABLES=glibc.pthread.rseq=0:glibc.malloc.top_pad=16777216` in the session
environment.
## The fast path (prototype)
The code is `tools/proot/fastpath/fastpath.c`, preloaded into every guest process. On the proot side
it needs `patches/0014`, enabled with `PROOT_FASTPATH=1`.
- **Trampoline.** The library maps a 4 KiB executable page at the fixed address `0xffff00000`. It
holds `mov x8,x0 … svc #0; ret`. With `PROOT_FASTPATH`, proot's filter checks the caller's address
just before each `RET_TRACE` and allows syscalls coming from that page.
- **Mapping a path.** The library takes the literal guest path and finds the longest binding whose
guest path is a prefix of it; otherwise the path is under the rootfs.
- It opens the *parent* host directory `O_PATH` and requires `/proc/self/fd` to name exactly that
path. That proves no component on the way was a symlink, so proot would have walked the same
directories.
- The last component is never followed: a symlink there goes to proot.
- **Missing files.** For a missing file, the deepest existing ancestor is verified the same way, and
the next component must not exist even as a symlink. Then `ENOENT` is exact.
- **What always goes to proot:**
- `..`, a trailing `/`, `/proc`, `/dev/fd` and `/dev/std*`;
- `O_TMPFILE`, `AT_EACCESS`, and any flag not handled;
- any doubt at all. "Go to proot" means calling the real libc function, so proot decides.
- **Relative paths.** Patch 0014 keeps the kernel's cwd at the guest cwd's host directory. Relative
and dirfd lookups are mapped through `/proc/self/{cwd,fd/N}`.
- **Verified-parent cache.** Verified parents are cached for 2 s (`PROOT_FP_TTL_MS`). That is the
same trade-off `preload/pathcache.c` already makes for the Steam client. A parent directory turned
into a symlink inside that window would be missed; `PROOT_FP_TTL_MS=0` verifies every call, at
2.0 µs instead of 0.6 µs.
- **Allowed syscalls only.** It uses only `openat`, `newfstatat`, `statx`, `faccessat`, `readlinkat`
and `close`, all allowed for apps. A first version used `openat2 RESOLVE_IN_ROOT`, which the app
policy blocks.
- **Equivalence check.** `bench/equiv.py` runs stat, lstat, readlink, read, listdir and access over
awkward paths, from several working directories and through a dirfd. Its output is byte-identical
with and without the fast path, at TTL 0 and at TTL 2000. The paths include:
- absolute and relative symlinks into binds, `..` symlinks, dangling links and loops;
- files inside binds, nested binds (`/dev/shm` inside `/dev`) and `/proc`;
- missing paths behind symlinks.
### Open items before shipping it
- **Validate inside the app**, not just from adb. Three things to check:
- The app domain allows an anonymous `PROT_EXEC` page (`execmem`). If not, map a page of the
library file itself at the fixed address instead, as proot's loader does for every program.
- Nothing claims `0xffff00000` first.
- The stub's syscalls pass Android's filter. They should: all are allowed.
- **Integrate into `libblsession.so`** rather than a second preload. `opens.c` and `pathcache.c`
already define `open` and `access`, so their "real" call should become the fast path. That also
removes the second library's load cost, about 0.2–0.4 ms per exec that is still unexplained (see
the exec row above).
- **Wire the launcher.** The app already knows the binds (`LinuxRuntime.binds`):
- set `PROOT_FASTPATH=1` in proot's environment;
- pass `PROOT_FP_ROOT` and `PROOT_FP_BINDS` to the guest;
- add a Performance toggle next to "Run proot without seccomp", with the fast path off whenever
seccomp is off.
- **Cover the write family** (mkdir, unlink, rename, symlink, chmod, utimens) and Unix-socket
`connect`/`bind` for same-path binds. Then measure a real Steam start in the app, a game load,
and `PROOT_FP_STATS` hit rates for steam, steamwebhelper, wine and wineserver.
- **FEX**, inside Proton as a DLL, translates x86 path syscalls through glibc's `syscall()`, not the
wrappers, so they still trap. A `syscall()` hook could route `openat` and `newfstatat` there too.
+3
View File
@@ -117,6 +117,9 @@ docker run --rm --platform linux/amd64 \
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
-o "$d/libblsession.so" tools/linuxfs/preload/*.c -ldl
aarch64-linux-gnu-strip --strip-unneeded "$d/libblsession.so"
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
-o "$d/libblfastpath.so" tools/proot/fastpath/fastpath.c -ldl
aarch64-linux-gnu-strip --strip-unneeded "$d/libblfastpath.so"
for script in tools/linuxfs/overlay/usr/local/bin/bannerlator-*; do
install -Dm644 "$script" "$d/usr/local/bin/$(basename "$script")"
done
+12
View File
@@ -80,3 +80,15 @@ Added by DroidDeck:
The `faccessat2` exit stop (termux 5ba8b95, for glibc's ENOSYS fallback on kernels before 5.8) and
the `statx` one (emulation on kernels before 4.11) are dropped when the running kernel is newer.
Host build: `ioctl` 31.6 -> 0.5 us; `stat`/`statx`/`faccessat2` about 44 -> 28 us (one stop).
Prototype (inert unless `PROOT_FASTPATH` is set; see `docs/development/proot-performance.md`):
- `0014-fastpath-trampoline.patch` - with `PROOT_FASTPATH`, every `SECCOMP_RET_TRACE` in the filter
is preceded by a check of the caller's address, and a syscall made from the fast path's trampoline
page (`fastpath/fastpath.c`, `0xffff00000`) runs without a stop: the tracee already translated it.
The check comes after the syscall-number dispatch, so untraced syscalls stay constant-ALLOW and keep
the kernel's seccomp action cache (checking first cost every syscall the full filter: +0.5 ms an
exec). `chdir`/`fchdir`, still emulated, also move the kernel's cwd to the host directory, and the
first tracee starts at `-w`'s, so relative lookups inside the tracee resolve where proot would.
SM8850 (adb shell): `stat` 25.3 -> 0.6 us, `open+close` 25.3 -> 0.9 us, ENOENT 22.9 -> 1.6 us,
8 threads `stat`ing 89k -> 3.6M/s; equivalence suite (`bench/equiv.py`) byte-identical to proot.
+32
View File
@@ -0,0 +1,32 @@
import os, sys
paths = ["/etc/os-release", "/root/abs-link", "/root/to-bind", "/root/to-bind/inbind.txt", "/root/rel-to-bind",
"/root/rel-to-bind/inbind.txt", "/root/dotdot", "/root/dotdot/libc.so.6", "/root/dangling", "/root/loop",
"/usr/lib/../lib/libc.so.6", "/usr//lib/./libc.so.6", "/lib", "/lib/libc.so.6", "/bin/env", "/nope", "/nope/x",
"/etc/os-release/x", "/mnt/droiddeck-sd", "/mnt/droiddeck-sd/inbind.txt", "/mnt/droiddeck-sd/../bannerlator",
"/dev/null", "/dev/shm", "/dev/shm/inbind.txt", "/dev/random", "/proc/self/exe", "/proc/version", "/proc",
"/sys/class/thermal/thermal_zone28/temp", "/sdcard", "/", "/tmp", "/root/in-root-abs-dir/os-release",
"/data/local/tmp/dd/tmp/inbind.txt", "/data/local/tmp/dd/mini/etc/os-release", "/root/to-bind/missing/x", "/root/to-bind/inbind.txt/x", "/usr/lib/nope/deeper/x.so", "/mnt/droiddeck-sd/nope/x", "/root/dangling/x", "/root/abs-link/x", "/nope/a/b/c", "/sdcard/nope/x"]
rels = ["os-release", "inbind.txt", "../etc/os-release", "abs-link", "to-bind/inbind.txt", "rel-to-bind",
"shm/inbind.txt", "null", "lib/libc.so.6", "mnt/droiddeck-sd/inbind.txt", "proc/version", "nope", "."]
def probe(p):
out = []
for name, fn in [("stat", lambda: (lambda s: (oct(s.st_mode), s.st_size if not os.path.isdir(p) else 0))(os.stat(p))),
("lstat", lambda: oct(os.lstat(p).st_mode)),
("readlink", lambda: os.readlink(p)),
("read", lambda: open(p, "rb").read(12)),
("list", lambda: sorted(x for x in os.listdir(p) if not x.startswith("proot-"))[:6]),
("access", lambda: (os.access(p, os.R_OK), os.access(p, os.W_OK), os.access(p, os.X_OK)))]:
try: out.append(f"{name}={fn()!r}")
except OSError as e: out.append(f"{name}=E{e.errno}")
return " ".join(out)
for p in paths: print(p, probe(p))
for cwd in ["/", "/root", "/etc", "/mnt/droiddeck-sd", "/dev", "/usr/lib", "/data/local/tmp/dd/tmp", "/root/to-bind", "/root/dotdot"]:
try: os.chdir(cwd)
except OSError as e: print("chdir", cwd, e.errno); continue
print("== cwd", cwd, os.getcwd())
for r in rels: print(" ", r, probe(r))
d = os.open(".", os.O_RDONLY)
for r in rels[:6]:
try: print(" at", r, oct(os.stat(r, dir_fd=d).st_mode))
except OSError as e: print(" at", r, "E", e.errno)
os.close(d)
+274
View File
@@ -0,0 +1,274 @@
/* Cost of the syscall-interception mechanisms available to an Android app, measured on the
* trapped call getppid() (the tracer/handler does no work, so this is pure mechanism cost).
*
* mechbench filter <n_linear> allowed syscall through a filter of n JEQs (linear, proot-style)
* mechbench tree allowed syscall through a binary-search filter (~150 entries)
* mechbench ptrace [spin] [regs] seccomp RET_TRACE + ptrace tracer (proot's mechanism)
* mechbench sigsys seccomp RET_TRAP + in-process SIGSYS handler
* mechbench notif [sync] [spin] seccomp RET_USER_NOTIF + supervisor thread
* mechbench openat2 <root> <path> openat2(RESOLVE_IN_ROOT) vs openat on the host path
* Env CPU_T / CPU_S pin the trapped thread / the tracer-supervisor to a cpu.
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/audit.h>
#include <linux/filter.h>
#include <linux/openat2.h>
#include <linux/seccomp.h>
#include <pthread.h>
#include <sched.h>
#include <signal.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/ptrace.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/uio.h>
#include <sys/wait.h>
#include <time.h>
#include <ucontext.h>
#include <unistd.h>
#include <linux/elf.h>
#ifndef SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP
#define SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP (1UL << 0)
#endif
#ifndef SECCOMP_IOCTL_NOTIF_SET_FLAGS
#define SECCOMP_IOCTL_NOTIF_SET_FLAGS SECCOMP_IOW(4, __u64)
#endif
#define TRAPPED SYS_getppid
static int N = 100000;
static double now(void) {
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return t.tv_sec * 1e9 + t.tv_nsec;
}
static void pin(const char *env) {
const char *v = getenv(env);
if (!v) return;
cpu_set_t s;
CPU_ZERO(&s);
CPU_SET(atoi(v), &s);
sched_setaffinity(0, sizeof s, &s);
}
static double time_trapped(void) {
for (int i = 0; i < N / 10; i++) syscall(TRAPPED);
double t0 = now();
for (int i = 0; i < N; i++) syscall(TRAPPED);
return (now() - t0) / N;
}
/* A filter: arch check, then `n` JEQs on numbers that never match (like proot's list of ~150
* traced syscalls seen by an allowed syscall), then `action` for TRAPPED, else ALLOW. */
static int install(int n_linear, uint32_t action, unsigned flags) {
struct sock_filter *f = calloc(n_linear + 16, sizeof *f);
int k = 0;
f[k++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch));
f[k++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_AARCH64, 1, 0);
f[k++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW);
f[k++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr));
for (int i = 0; i < n_linear; i++) /* numbers far above any real syscall */
f[k++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 5000 + i, 0, 0);
f[k++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, TRAPPED, 0, 1);
f[k++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, action);
f[k++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW);
struct sock_fprog p = {.len = k, .filter = f};
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
return syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, flags, &p);
}
/* ~150 traced numbers as a balanced binary search (what libseccomp's optimize=2 emits). */
static int emit_tree(struct sock_filter *f, int *k, int lo, int hi) {
if (lo > hi) { f[(*k)++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW); return 0; }
int mid = (lo + hi) / 2;
f[(*k)++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 5000 + mid, 0, 1);
f[(*k)++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_TRACE);
f[(*k)++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JGT | BPF_K, 5000 + mid, 0, 1);
int ja = (*k)++;
int left = *k;
emit_tree(f, k, lo, mid - 1);
f[ja] = (struct sock_filter)BPF_STMT(BPF_JMP | BPF_JA, *k - left);
emit_tree(f, k, mid + 1, hi);
return 0;
}
static void *sigsys_unused;
static void on_sigsys(int sig, siginfo_t *si, void *ctx) {
(void)sig; (void)si;
ucontext_t *uc = ctx;
uc->uc_mcontext.regs[0] = 4242; /* the emulated result */
}
struct notif_arg { int fd; int spin; };
static void *supervisor(void *a) {
struct notif_arg *na = a;
pin("CPU_S");
struct seccomp_notif *req = calloc(1, 256);
struct seccomp_notif_resp *resp = calloc(1, 256);
for (;;) {
memset(req, 0, sizeof *req);
if (ioctl(na->fd, SECCOMP_IOCTL_NOTIF_RECV, req) < 0) {
if (errno == EINTR) continue;
return NULL;
}
resp->id = req->id;
resp->val = 4242;
resp->error = 0;
resp->flags = 0;
ioctl(na->fd, SECCOMP_IOCTL_NOTIF_SEND, resp);
}
}
static int notif_fd;
static int notif_flags_sync;
static void *notified_worker(void *a) {
(void)a;
pin("CPU_T");
notif_fd = install(0, SECCOMP_RET_USER_NOTIF, SECCOMP_FILTER_FLAG_NEW_LISTENER);
return NULL;
}
int main(int argc, char **argv) {
if (getenv("N")) N = atoi(getenv("N"));
const char *mode = argc > 1 ? argv[1] : "filter";
if (!strcmp(mode, "filter")) {
pin("CPU_T");
printf("no filter getppid %6.0f ns\n", time_trapped());
int n = argc > 2 ? atoi(argv[2]) : 150;
if (install(n, SECCOMP_RET_ALLOW, 0) < 0) { perror("seccomp"); return 1; }
printf("linear %4d JEQs getppid %6.0f ns\n", n, time_trapped());
return 0;
}
if (!strcmp(mode, "tree")) {
pin("CPU_T");
printf("no filter getppid %6.0f ns\n", time_trapped());
struct sock_filter f[2048];
int k = 0;
f[k++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr));
emit_tree(f, &k, 0, 149);
struct sock_fprog p = {.len = k, .filter = f};
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
if (syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, 0, &p) < 0) { perror("seccomp"); return 1; }
printf("tree of 150 (%d insns) getppid %6.0f ns\n", k, time_trapped());
return 0;
}
if (!strcmp(mode, "sigsys")) {
pin("CPU_T");
struct sigaction sa = {0};
sa.sa_sigaction = on_sigsys;
sa.sa_flags = SA_SIGINFO;
sigaction(SIGSYS, &sa, NULL);
if (install(0, SECCOMP_RET_TRAP, 0) < 0) { perror("seccomp"); return 1; }
long r = syscall(TRAPPED);
printf("sigsys trap (result %ld) %6.0f ns\n", r, time_trapped());
(void)sigsys_unused;
return 0;
}
if (!strcmp(mode, "notif")) {
int sync = argc > 2 && strstr(argv[2], "sync");
pthread_t w, s;
pthread_create(&w, NULL, notified_worker, NULL);
pthread_join(w, NULL);
if (notif_fd < 0) { perror("NEW_LISTENER"); return 1; }
if (sync) {
if (ioctl(notif_fd, SECCOMP_IOCTL_NOTIF_SET_FLAGS, SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP) < 0) perror("SYNC_WAKE_UP");
else printf("(sync wake-up on)\n");
}
/* The filter lives on thread w, which has exited; install again on a fresh worker that
* does the timing. */
struct notif_arg na = {.fd = -1};
(void)na;
printf("note: user-notif is measured in 'notif2'\n");
return 0;
}
if (!strcmp(mode, "notif2")) {
/* Supervisor = child thread (unfiltered), timed thread = main after installing. */
int sync = argc > 2 && !strcmp(argv[2], "sync");
pin("CPU_T");
int fd = install(0, SECCOMP_RET_USER_NOTIF, SECCOMP_FILTER_FLAG_NEW_LISTENER);
if (fd < 0) { perror("NEW_LISTENER"); return 1; }
if (sync && ioctl(fd, SECCOMP_IOCTL_NOTIF_SET_FLAGS, SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP, 0) < 0) perror("SYNC_WAKE_UP");
/* The supervisor thread is created AFTER the filter, so it inherits it - but it only
* makes ioctl calls, which the filter allows. */
static struct notif_arg na;
na.fd = fd;
pthread_t s;
pthread_create(&s, NULL, supervisor, &na);
long r = syscall(TRAPPED);
printf("user_notif%s (result %ld) %6.0f ns\n", sync ? "+sync" : "", r, time_trapped());
return 0;
}
if (!strcmp(mode, "ptrace")) {
int spin = 0, regs = 0;
for (int i = 2; i < argc; i++) { if (!strcmp(argv[i], "spin")) spin = 1; if (!strcmp(argv[i], "regs")) regs = 1; }
pid_t c = fork();
if (c == 0) {
pin("CPU_T");
ptrace(PTRACE_TRACEME, 0, 0, 0);
raise(SIGSTOP);
if (install(0, SECCOMP_RET_TRACE, 0) < 0) { perror("seccomp"); _exit(1); }
printf("ptrace+seccomp%s%s %6.0f ns\n", spin ? " spin" : "", regs ? " +getregs+setregs" : "", time_trapped());
fflush(stdout);
_exit(0);
}
pin("CPU_S");
int st;
waitpid(c, &st, 0);
ptrace(PTRACE_SETOPTIONS, c, 0, PTRACE_O_TRACESECCOMP | PTRACE_O_EXITKILL);
ptrace(PTRACE_CONT, c, 0, 0);
for (;;) {
pid_t p;
if (spin) { do p = waitpid(c, &st, WNOHANG | __WALL); while (p == 0); }
else p = waitpid(c, &st, __WALL);
if (p < 0 || WIFEXITED(st) || WIFSIGNALED(st)) break;
int sig = 0;
if ((st >> 8) == (SIGTRAP | (PTRACE_EVENT_SECCOMP << 8))) {
if (regs) {
struct user_regs { uint64_t r[31], sp, pc, pstate; } ur;
struct iovec io = {&ur, sizeof ur};
ptrace(PTRACE_GETREGSET, c, NT_PRSTATUS, &io);
ptrace(PTRACE_SETREGSET, c, NT_PRSTATUS, &io);
}
} else if (WIFSTOPPED(st) && WSTOPSIG(st) != SIGTRAP) sig = WSTOPSIG(st);
ptrace(PTRACE_CONT, c, 0, sig);
}
return 0;
}
if (!strcmp(mode, "openat2")) {
const char *root = argv[2], *path = argv[3];
char host[4096];
snprintf(host, sizeof host, "%s%s", root, path);
int rfd = open(root, O_PATH | O_DIRECTORY);
struct open_how how = {.flags = O_RDONLY | O_CLOEXEC, .resolve = RESOLVE_IN_ROOT};
long fd = syscall(SYS_openat2, rfd, path, &how, sizeof how);
if (fd < 0) { perror("openat2 RESOLVE_IN_ROOT"); return 1; }
close(fd);
double t0 = now();
for (int i = 0; i < N; i++) close(open(host, O_RDONLY | O_CLOEXEC));
printf("openat host path %6.0f ns\n", (now() - t0) / N);
t0 = now();
for (int i = 0; i < N; i++) close(syscall(SYS_openat2, rfd, path, &how, sizeof how));
printf("openat2 RESOLVE_IN_ROOT %6.0f ns\n", (now() - t0) / N);
struct stat st;
t0 = now();
for (int i = 0; i < N; i++) stat(host, &st);
printf("stat host path %6.0f ns\n", (now() - t0) / N);
struct open_how ph = {.flags = O_PATH | O_CLOEXEC, .resolve = RESOLVE_IN_ROOT | RESOLVE_NO_MAGICLINKS};
t0 = now();
for (int i = 0; i < N; i++) { int f = syscall(SYS_openat2, rfd, path, &ph, sizeof ph); fstat(f, &st); close(f); }
printf("openat2(O_PATH)+fstat+close %6.0f ns\n", (now() - t0) / N);
return 0;
}
fprintf(stderr, "unknown mode\n");
return 2;
}
+16
View File
@@ -0,0 +1,16 @@
/* T threads each stat()ing a path for D seconds: aggregate trapped-syscall throughput. */
#define _GNU_SOURCE
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <time.h>
static const char *path; static volatile int stop; static long counts[64];
static void *run(void *a) { long i = (long)a, n = 0; struct stat st; while (!stop) { stat(path, &st); n++; } counts[i] = n; return 0; }
int main(int c, char **v) {
path = v[1]; int T = atoi(v[2]); double D = c > 3 ? atof(v[3]) : 2;
pthread_t t[64]; for (long i = 0; i < T; i++) pthread_create(&t[i], 0, run, (void *)i);
struct timespec ts = {(time_t)D, (long)((D - (long)D) * 1e9)}; nanosleep(&ts, 0); stop = 1;
long tot = 0; for (int i = 0; i < T; i++) { pthread_join(t[i], 0); tot += counts[i]; }
printf("threads=%d %8.0f stat/s total %6.1f us/stat per thread\n", T, tot / D, T * D * 1e6 / tot);
return 0; }
+26
View File
@@ -0,0 +1,26 @@
#!/system/bin/sh
# Runs a guest command on an adb-connected device the way a DroidDeck session runs proot (same
# options, a similar number of binds), as the shell user from /data/local/tmp/dd. Layout there:
# mini/ a glibc rootfs subset (glibc, bash, coreutils, python) + the bench binaries
# proot, loader this repo's proot (tools/proot/build.sh) -> variant "patched"
# proot-vanilla termux proot at source.env with no patches -> variant "vanilla"
# proot-fp this repo's proot incl. 0014 -> "fastpath" / "fpoff"
# mini/usr/lib/libfastpath.so tools/proot/fastpath/fastpath.c
# NOTE: adb shell is NOT under the app seccomp policy; see docs/development/proot-performance.md.
#
# run-device.sh <patched|vanilla|noseccomp|bare|fastpath|fpoff|direct> cmd...
D=/data/local/tmp/dd
V=$1; shift
U=$(id -u)
REL="\\Linux\\DroidDeck\\$(uname -r)\\$(uname -v)\\aarch64\\localdomain\\-1\\"
BINDS="-b /dev -b /proc -b /sys -b /dev/urandom:/dev/random -b /proc/self/fd:/dev/fd -b /proc/self/fd/0:/dev/stdin -b /proc/self/fd/1:/dev/stdout -b /proc/self/fd/2:/dev/stderr -b $D/tmp:/dev/shm -b $D -b /sdcard -b /data/local/tmp/dd/mini/etc/os-release:/proc/version -b $D/mini/etc/os-release:/proc/loadavg -b $D/mini/etc/os-release:/proc/uptime -b /sys/class/thermal/thermal_zone0/temp:/sys/class/thermal/thermal_zone28/temp -b $D/tmp:/mnt/droiddeck-sd -b $D/tmp:/mnt/bannerlator"
export PROOT_TMP_DIR=$D/tmp
case $V in
patched) export PROOT_LOADER=$D/loader; exec $D/proot --kill-on-exit --kernel-release="$REL" -i $U:$U -r $D/mini -w /root $BINDS /usr/bin/env -i HOME=/root PATH=/usr/bin LANG=C.UTF-8 "$@" ;;
noseccomp) export PROOT_LOADER=$D/loader PROOT_NO_SECCOMP=1; exec $D/proot --kill-on-exit --kernel-release="$REL" -i $U:$U -r $D/mini -w /root $BINDS /usr/bin/env -i HOME=/root PATH=/usr/bin LANG=C.UTF-8 "$@" ;;
vanilla) export PROOT_LOADER=$D/loader-vanilla; exec $D/proot-vanilla --kill-on-exit --kernel-release="$REL" -i $U:$U -r $D/mini -w /root $BINDS /usr/bin/env -i HOME=/root PATH=/usr/bin LANG=C.UTF-8 "$@" ;;
bare) export PROOT_LOADER=$D/loader; exec $D/proot --kill-on-exit -r $D/mini -w /root $BINDS /usr/bin/env -i HOME=/root PATH=/usr/bin LANG=C.UTF-8 "$@" ;;
fastpath) export PROOT_LOADER=$D/loader-fp PROOT_FASTPATH=bench; FPB=$(echo "$BINDS" | sed 's/-b //g; s/^ *//; s/ *$//; s/ */|/g'); exec $D/proot-fp --kill-on-exit --kernel-release="$REL" -i $U:$U -r $D/mini -w /root $BINDS /usr/bin/env -i HOME=/root PATH=/usr/bin LANG=C.UTF-8 PROOT_FP_ROOT=$D/mini "PROOT_FP_BINDS=$FPB" PROOT_FP_KEY=bench LD_PRELOAD=/usr/lib/libfastpath.so $FPEXTRA "$@" ;;
fpoff) export PROOT_LOADER=$D/loader-fp; exec $D/proot-fp --kill-on-exit --kernel-release="$REL" -i $U:$U -r $D/mini -w /root $BINDS /usr/bin/env -i HOME=/root PATH=/usr/bin LANG=C.UTF-8 "$@" ;;
direct) cd $D/mini/root; exec $D/mini/usr/lib/ld-linux-aarch64.so.1 --library-path $D/mini/usr/lib "$@" ;;
esac
+78
View File
@@ -0,0 +1,78 @@
/* Per-syscall latency under whatever runs this program (native, proot, ...).
* Usage: sysbench <deep-file> [iterations]
* Prints "name ns/op" lines. */
#define _GNU_SOURCE
#include <fcntl.h>
#include <linux/futex.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
static double now(void) {
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return t.tv_sec * 1e9 + t.tv_nsec;
}
#define BENCH(name, n, body) \
do { \
for (int _w = 0; _w < (n) / 10 + 1; _w++) { body; } \
double _t0 = now(); \
for (int _i = 0; _i < (n); _i++) { body; } \
printf("%-28s %10.0f ns\n", name, (now() - _t0) / (n)); \
fflush(stdout); \
} while (0)
static void *thread_fn(void *a) { return a; }
int main(int argc, char **argv) {
const char *deep = argc > 1 ? argv[1] : "/usr/lib/python3.14/os.py";
int n = argc > 2 ? atoi(argv[2]) : 20000;
struct stat st;
char buf[4096];
int pfd[2];
pipe(pfd);
int fd = open(deep, O_RDONLY);
if (fd < 0) { perror(deep); return 1; }
int futexword = 0;
char *slash = strrchr(deep, '/');
char dir[1024];
snprintf(dir, sizeof dir, "%.*s", (int)(slash - deep), deep);
int dfd = open(dir, O_RDONLY | O_DIRECTORY);
BENCH("getppid (untraced)", n, syscall(SYS_getppid));
BENCH("futex wake (untraced)", n, syscall(SYS_futex, &futexword, FUTEX_WAKE_PRIVATE, 1, 0, 0, 0));
BENCH("clock_gettime (vdso)", n, { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); });
BENCH("fstat fd (untraced)", n, fstat(fd, &st));
BENCH("pread 4k (untraced)", n, pread(fd, buf, sizeof buf, 0));
BENCH("ioctl FIONREAD (untraced*)", n, { int v; ioctl(pfd[0], FIONREAD, &v); });
BENCH("fstatat relative dirfd", n, fstatat(dfd, slash + 1, &st, 0));
BENCH("stat deep abs path", n, stat(deep, &st));
BENCH("lstat deep abs path", n, lstat(deep, &st));
BENCH("statx deep abs path", n, { struct statx sx; statx(AT_FDCWD, deep, 0, STATX_BASIC_STATS, &sx); });
BENCH("access deep abs path", n, access(deep, R_OK));
BENCH("open+close deep abs", n, close(open(deep, O_RDONLY)));
BENCH("stat ENOENT", n, stat("/usr/lib/does/not/exist.so", &st));
BENCH("readlink /proc/self/exe", n, readlink("/proc/self/exe", buf, sizeof buf));
BENCH("getcwd", n, getcwd(buf, sizeof buf));
BENCH("uname", n, { struct utsname_dummy { char b[65 * 6]; } u; syscall(SYS_uname, &u); });
BENCH("memfd_create+close", n / 4, close(memfd_create("x", MFD_CLOEXEC)));
BENCH("brk(0) query", n, syscall(SYS_brk, 0));
BENCH("mmap+munmap 64k", n, munmap(mmap(0, 65536, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0), 65536));
BENCH("pthread create+join", n / 20, { pthread_t t; pthread_create(&t, 0, thread_fn, 0); pthread_join(t, 0); });
BENCH("fork+_exit+wait", n / 100, { pid_t p = fork(); if (!p) _exit(0); waitpid(p, 0, 0); });
if (argc > 3) {
char *const av[] = {argv[3], NULL};
BENCH("fork+execve true+wait", n / 200, { pid_t p = fork(); if (!p) { execv(argv[3], av); _exit(127); } waitpid(p, 0, 0); });
}
return 0;
}
+26
View File
@@ -0,0 +1,26 @@
#!/system/bin/sh
# workloads: direct glibc (patched PT_INTERP) vs proot variants
D=/data/local/tmp/dd; cd $D
t() { local s=$(date +%s%N); "$@" >/dev/null 2>&1; local e=$(date +%s%N); echo $(( (e - s) / 1000000 )); }
base() { t ./run.sh $1 /usr/bin/true; }
W1='import json,email.parser,http.client,asyncio,decimal,sqlite3,argparse,subprocess,logging,xml.etree.ElementTree'
for v in direct vanilla fpoff fastpath; do
input keyevent KEYCODE_WAKEUP
for rep in 1 2 3; do
if [ $v = direct ]; then
export LD_LIBRARY_PATH=$D/mini/usr/lib PYTHONHOME=$D/mini/usr PYTHONDONTWRITEBYTECODE=1
a=$(t $D/dbin/python3.14 -c "$W1")
b=$(t $D/dbin/find $D/mini/usr/lib/python3.14 -type f)
c=$(t $D/dbin/bash -c "for i in \$($D/dbin/seq 200); do $D/dbin/true; done")
d=$(t $D/dbin/tar cf /dev/null $D/mini/usr/lib/python3.14)
unset LD_LIBRARY_PATH PYTHONHOME
else
a=$(t ./run.sh $v /usr/bin/env PYTHONDONTWRITEBYTECODE=1 /usr/bin/python3.14 -c "$W1")
b=$(t ./run.sh $v /usr/bin/find /usr/lib/python3.14 -type f)
c=$(t ./run.sh $v /usr/bin/bash -c 'for i in $(seq 200); do /usr/bin/true; done')
d=$(t ./run.sh $v /usr/bin/tar cf /dev/null /usr/lib/python3.14)
fi
[ $v = direct ] && z=0 || z=$(base $v)
echo "$v rep$rep: proot-start=${z}ms python-imports=${a}ms find-8k-files=${b}ms 200x-exec=${c}ms tar-80MB=${d}ms"
done
done
+545
View File
@@ -0,0 +1,545 @@
/*
* proot fast path: path syscalls answered inside the process, without a ptrace round trip.
*
* proot traps every path syscall into its tracer: 15-60 us a call on a Snapdragon, most of it
* the two cross-core wake-ups between tracee and tracer. This library, preloaded into every guest
* process, answers the common calls itself and issues the translated syscall from a trampoline
* page at a fixed address, which proot's seccomp filter lets through without a stop
* (PROOT_FASTPATH, tools/proot/patches/0014).
*
* It decides nothing proot would decide differently. The guest path is mapped literally - the
* longest binding whose guest path is a prefix, else the rootfs - and the parent directory is
* opened O_PATH and must come back from /proc/self/fd as exactly that host path, which proves no
* component on the way was a symlink: proot would then have walked the same directories. The
* last component is looked at without following it, and a symlink there goes to proot. So does
* "..", a trailing "/", anything under /proc, any flag not handled here and any failure to
* decide: the real libc call runs, proot traps it and answers as it always has.
*
* Only syscalls Android's app seccomp policy allows are used (openat, newfstatat, statx,
* faccessat, readlinkat, close) - not openat2 or faccessat2, which it answers with SIGSYS.
*
* Verified parents are remembered for PROOT_FP_TTL_MS (default 2000; 0 = verify every call): a
* parent directory replaced by a symlink within that window would be missed, the same trade the
* session's pathcache.c makes for the Steam client.
*
* Config (the launcher sets it): PROOT_FP_ROOT=<host rootfs>, PROOT_FP_BINDS=<host:guest|...>;
* PROOT_FP_KEY=<key>, with PROOT_FASTPATH=<key> in proot's own environment: nothing is answered
* here unless the tracer is that proot (traced_by_match).
* PROOT_FP_OFF=1 disables it; PROOT_FP_STATS=1 reports hits and misses at exit.
*/
#define _GNU_SOURCE
#include <dirent.h>
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <pthread.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/sysmacros.h>
#include <time.h>
#include <unistd.h>
#define FP_STUB_ADDR 0xffff00000UL /* must match FASTPATH_STUB_ADDR in proot's seccomp.c */
#define FP_STUB_SIZE 4096UL
#define MAX_BINDS 64
#define FP_SLOW (-100000L)
typedef long (*stub_fn)(long nr, long a, long b, long c, long d, long e, long f);
static stub_fn fp_sys;
static int fp_on;
struct bind { char *host; size_t hlen; char *guest; size_t glen; int canon; };
static struct bind binds[MAX_BINDS];
static int nbinds;
static char root[PATH_MAX];
static size_t rootlen;
static long long ttl_ns = 2000000000LL;
static unsigned long hits, misses;
static long sc(long nr, long a, long b, long c, long d, long e) { return fp_sys(nr, a, b, c, d, e, 0); }
static void fp_init(void);
static pthread_once_t init_once = PTHREAD_ONCE_INIT;
/* Set up on the first call that could use it: a process that only execs on pays nothing. */
static inline int fp_ready(void) {
pthread_once(&init_once, fp_init);
return fp_on;
}
static int under(const char *path, const char *dir, size_t dlen) {
if (dlen == 1 && dir[0] == '/') return path[0] == '/';
return strncmp(path, dir, dlen) == 0 && (path[dlen] == 0 || path[dlen] == '/');
}
/* ---------------------------------------------------------------- setup */
static void stats(void) {
char line[160];
int n = snprintf(line, sizeof line, "fastpath[%d] %s: %lu fast, %lu to proot\n", getpid(),
program_invocation_short_name, hits, misses);
if (write(2, line, n) < 0) {}
}
/* The canonical form of a host path (bindings may name /sdcard, /data/user/0, ...). */
static void canonical(char *path, size_t size) {
long fd = sc(SYS_openat, AT_FDCWD, (long)path, O_PATH | O_CLOEXEC, 0, 0);
if (fd < 0) return;
char link[40], out[PATH_MAX];
snprintf(link, sizeof link, "/proc/self/fd/%ld", fd);
long n = sc(SYS_readlinkat, AT_FDCWD, (long)link, (long)out, sizeof out - 1, 0);
sc(SYS_close, fd, 0, 0, 0, 0);
if (n > 0 && (size_t)n < size) { out[n] = 0; memcpy(path, out, n + 1); }
}
static pthread_mutex_t cache_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t bind_lock;
static void after_fork(void) { pthread_mutex_init(&cache_lock, NULL); pthread_mutex_init(&bind_lock, NULL); }
/*
* The process is traced by a proot that lets the trampoline through and was given the same
* rootfs and bindings: its own environment carries PROOT_FASTPATH=<PROOT_FP_KEY> (the launcher
* derives the key from them) and no PROOT_NO_SECCOMP. A process that inherited the variables but
* runs under another proot, or none, must not translate paths itself: a proot that traps the
* trampoline would translate the host path a second time.
*/
static int traced_by_match(const char *key) {
static char buf[32768]; /* under init_once */
char path[48], want[160];
long fd = sc(SYS_openat, AT_FDCWD, (long)"/proc/self/status", O_RDONLY | O_CLOEXEC, 0, 0);
if (fd < 0) return 0;
long n = sc(SYS_read, fd, (long)buf, 4095, 0, 0);
sc(SYS_close, fd, 0, 0, 0, 0);
if (n <= 0) return 0;
buf[n] = 0;
const char *line = strstr(buf, "\nTracerPid:");
if (!line) return 0;
long tracer = strtol(line + 11, NULL, 10);
if (tracer <= 0) return 0;
snprintf(path, sizeof path, "/proc/%ld/environ", tracer);
if (snprintf(want, sizeof want, "PROOT_FASTPATH=%s", key) >= (int)sizeof want) return 0;
fd = sc(SYS_openat, AT_FDCWD, (long)path, O_RDONLY | O_CLOEXEC, 0, 0);
if (fd < 0) return 0;
size_t have = 0;
for (long r; have < sizeof buf - 1 && (r = sc(SYS_read, fd, (long)buf + have, sizeof buf - 1 - have, 0, 0)) > 0;) have += r;
sc(SYS_close, fd, 0, 0, 0, 0);
buf[have] = 0;
int found = 0;
for (size_t i = 0; i < have; i += strlen(buf + i) + 1) {
if (strcmp(buf + i, want) == 0) found = 1;
if (strncmp(buf + i, "PROOT_NO_SECCOMP=", 17) == 0) return 0;
}
return found;
}
static void fp_init(void) {
const char *r = getenv("PROOT_FP_ROOT"), *b = getenv("PROOT_FP_BINDS"), *t = getenv("PROOT_FP_TTL_MS");
const char *key = getenv("PROOT_FP_KEY");
if (!r || !*r || !key || !*key || getenv("PROOT_FP_OFF")) return;
void *p = mmap((void *)FP_STUB_ADDR, FP_STUB_SIZE, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0);
if (p != (void *)FP_STUB_ADDR) {
if (p != MAP_FAILED) { munmap(p, FP_STUB_SIZE); return; }
if (errno != EEXIST) return; /* mapped already: this library loaded twice */
} else {
static const uint32_t code[] = {
0xAA0003E8, /* mov x8, x0 */ 0xAA0103E0, /* mov x0, x1 */ 0xAA0203E1, /* mov x1, x2 */
0xAA0303E2, /* mov x2, x3 */ 0xAA0403E3, /* mov x3, x4 */ 0xAA0503E4, /* mov x4, x5 */
0xAA0603E5, /* mov x5, x6 */ 0xD4000001, /* svc #0 */ 0xD65F03C0, /* ret */
};
memcpy(p, code, sizeof code);
__builtin___clear_cache((char *)p, (char *)p + sizeof code);
if (mprotect(p, FP_STUB_SIZE, PROT_READ | PROT_EXEC) != 0) { munmap(p, FP_STUB_SIZE); return; }
}
fp_sys = (stub_fn)FP_STUB_ADDR;
if (!traced_by_match(key)) { fp_sys = NULL; return; }
if (t) ttl_ns = atoll(t) * 1000000LL;
snprintf(root, sizeof root, "%s", r);
canonical(root, sizeof root);
rootlen = strlen(root);
for (const char *s = b; s && *s && nbinds < MAX_BINDS;) {
const char *end = strchr(s, '|');
size_t len = end ? (size_t)(end - s) : strlen(s);
char spec[2 * PATH_MAX];
if (len > 0 && len < sizeof spec) {
memcpy(spec, s, len);
spec[len] = 0;
char *colon = strchr(spec, ':');
if (colon) *colon = 0;
struct bind *bd = &binds[nbinds];
/* Canonicalised on first use: some (/sdcard) are FUSE, slow to look up at every exec.
* Static storage: malloc here would grow the heap with brk(2), which proot traps. */
static char hosts[MAX_BINDS][PATH_MAX], guests[MAX_BINDS][512];
const char *g = colon ? colon + 1 : spec;
if (strlen(g) >= sizeof guests[0]) { s = end ? end + 1 : NULL; continue; }
bd->host = hosts[nbinds];
bd->guest = guests[nbinds];
snprintf(bd->guest, sizeof guests[0], "%s", g);
snprintf(bd->host, PATH_MAX, "%.*s", PATH_MAX - 1, spec);
bd->hlen = strlen(bd->host);
bd->glen = strlen(bd->guest);
if (bd->guest[0] == '/' && bd->glen > 1 && bd->hlen > 0) nbinds++;
}
s = end ? end + 1 : NULL;
}
pthread_atfork(NULL, NULL, after_fork); /* registered once per exec */
fp_on = 1;
if (getenv("PROOT_FP_STATS")) atexit(stats);
}
static pthread_mutex_t bind_lock = PTHREAD_MUTEX_INITIALIZER;
static const struct bind *canon(struct bind *bd) {
if (__atomic_load_n(&bd->canon, __ATOMIC_ACQUIRE)) return bd;
if (pthread_mutex_trylock(&bind_lock) != 0) return NULL;
if (!bd->canon) {
canonical(bd->host, PATH_MAX);
bd->hlen = strlen(bd->host);
__atomic_store_n(&bd->canon, 1, __ATOMIC_RELEASE);
}
pthread_mutex_unlock(&bind_lock);
return bd;
}
/* ---------------------------------------------------------------- verified parents */
#define SLOTS 256
#define SLOT_PATH 240 /* longer directories are verified every time */
struct slot { long long stamp; unsigned hash; char path[SLOT_PATH]; };
static struct slot cache[SLOTS];
static long long now_ns(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC_COARSE, &ts);
return ts.tv_sec * 1000000000LL + ts.tv_nsec;
}
static unsigned hash_of(const char *s) {
unsigned h = 2166136261u;
while (*s) h = (h ^ (unsigned char)*s++) * 16777619u;
return h;
}
static int cached(const char *dir, unsigned h) {
if (ttl_ns <= 0 || pthread_mutex_trylock(&cache_lock) != 0) return 0;
struct slot *e = &cache[h % SLOTS];
int hit = e->stamp && e->hash == h && now_ns() - e->stamp < ttl_ns && strcmp(e->path, dir) == 0;
pthread_mutex_unlock(&cache_lock);
return hit;
}
static void remember(const char *dir, unsigned h) {
if (ttl_ns <= 0 || strlen(dir) >= SLOT_PATH || pthread_mutex_trylock(&cache_lock) != 0) return;
struct slot *e = &cache[h % SLOTS];
e->hash = h;
snprintf(e->path, sizeof e->path, "%s", dir);
e->stamp = now_ns();
pthread_mutex_unlock(&cache_lock);
}
/* The host directory `dir` is reached by the kernel without crossing a symlink. */
static long canonical_dir(const char *dir) {
unsigned h = hash_of(dir);
if (cached(dir, h)) return 1;
long fd = sc(SYS_openat, AT_FDCWD, (long)dir, O_PATH | O_DIRECTORY | O_CLOEXEC, 0, 0);
if (fd < 0) return fd;
char link[40], out[PATH_MAX];
snprintf(link, sizeof link, "/proc/self/fd/%ld", fd);
long n = sc(SYS_readlinkat, AT_FDCWD, (long)link, (long)out, sizeof out - 1, 0);
sc(SYS_close, fd, 0, 0, 0, 0);
if (n <= 0) return 0;
out[n] = 0;
if (strcmp(out, dir) != 0) return 0;
remember(dir, h);
return 1;
}
/* ---------------------------------------------------------------- guest -> host */
/* Guest paths never answered here: proot emulates /proc (self, exe, fd and cwd links). */
static int reserved(const char *g) {
return under(g, "/proc", 5) || under(g, "/dev/fd", 7) || under(g, "/dev/stdin", 10)
|| under(g, "/dev/stdout", 11) || under(g, "/dev/stderr", 11);
}
/* The guest path of a host directory (the cwd, a dirfd): the longest host prefix wins. */
static int host_to_guest(const char *h, char *g, size_t size) {
const struct bind *best = NULL;
for (int i = 0; i < nbinds; i++)
if (under(h, binds[i].host, binds[i].hlen) && (!best || binds[i].hlen > best->hlen)) best = &binds[i];
if (under(h, root, rootlen) && (!best || rootlen >= best->hlen))
return snprintf(g, size, "%s", h[rootlen] ? h + rootlen : "/") < (int)size ? 0 : -1;
if (best) return snprintf(g, size, "%s%s", best->guest, h + best->hlen) < (int)size ? 0 : -1;
return -1;
}
/*
* The host path `path` (relative to dirfd, as the guest means it) names, whose parent directory
* is verified; 0, or FP_SLOW when proot must decide.
*/
static long resolve(int dirfd, const char *path, char *host) {
char guest[PATH_MAX], base[PATH_MAX];
if (!path || !path[0] || !fp_ready()) return FP_SLOW;
size_t plen = strlen(path);
if (path[plen - 1] == '/' && plen > 1) return FP_SLOW;
if (path[0] == '/') {
if (plen >= sizeof guest) return FP_SLOW;
memcpy(guest, path, plen + 1);
} else {
char link[40], h[PATH_MAX];
if (dirfd == AT_FDCWD) snprintf(link, sizeof link, "/proc/self/cwd");
else snprintf(link, sizeof link, "/proc/self/fd/%d", dirfd);
long n = sc(SYS_readlinkat, AT_FDCWD, (long)link, (long)h, sizeof h - 1, 0);
if (n <= 0) return FP_SLOW;
h[n] = 0;
if (host_to_guest(h, base, sizeof base) < 0) return FP_SLOW;
if (snprintf(guest, sizeof guest, "%s/%s", strcmp(base, "/") ? base : "", path) >= (int)sizeof guest) return FP_SLOW;
}
/* Normalise "//" and "/./"; ".." goes to proot (it is not lexical once symlinks are involved). */
char norm[PATH_MAX];
size_t o = 0;
for (const char *c = guest; *c;) {
while (*c == '/') c++;
if (!*c) break;
const char *e = strchrnul(c, '/');
size_t len = e - c;
if (len == 1 && c[0] == '.') { c = e; continue; }
if (len == 2 && c[0] == '.' && c[1] == '.') return FP_SLOW;
if (o + len + 2 >= sizeof norm) return FP_SLOW;
norm[o++] = '/';
memcpy(norm + o, c, len);
o += len;
c = e;
}
if (o == 0) norm[o++] = '/';
norm[o] = 0;
if (reserved(norm)) return FP_SLOW;
struct bind *bd = NULL;
for (int i = 0; i < nbinds; i++)
if (under(norm, binds[i].guest, binds[i].glen) && (!bd || binds[i].glen > bd->glen)) bd = &binds[i];
if (bd && !canon(bd)) return FP_SLOW;
size_t base_len = bd ? bd->hlen : rootlen;
int n = bd ? snprintf(host, PATH_MAX, "%s%s", bd->host, norm + bd->glen)
: snprintf(host, PATH_MAX, "%s%s", root, strcmp(norm, "/") ? norm : "");
if (n >= PATH_MAX) return FP_SLOW;
char *slash = strrchr(host, '/');
if (!slash || slash == host) return FP_SLOW;
*slash = 0;
long ok = canonical_dir(host);
*slash = '/';
if (ok == 1) return 0;
if (ok != -ENOENT) return FP_SLOW;
/* The parent is missing. The path is missing for proot too if the deepest directory that does
* exist is canonical and the next component below it is not there at all - not even as a
* symlink proot would follow. Only within the binding (or rootfs) the path was mapped into. */
char probe[PATH_MAX];
memcpy(probe, host, n + 1);
for (char *cut = strrchr(probe, '/'); cut && (size_t)(cut - probe) > base_len; ) {
*cut = 0;
char *up = strrchr(probe, '/');
if (!up || (size_t)(up - probe) < base_len) break;
*up = 0;
long c = canonical_dir(probe[0] ? probe : "/");
*up = '/';
if (c == 1) {
struct stat st;
if (sc(SYS_newfstatat, AT_FDCWD, (long)probe, (long)&st, AT_SYMLINK_NOFOLLOW, 0) == -ENOENT) return -ENOENT;
return FP_SLOW;
}
if (c != -ENOENT) return FP_SLOW;
cut = up;
}
return FP_SLOW;
}
/* The last component, not followed: 0 something that is not a symlink, 1 a symlink, <0 -errno. */
static long kind(const char *host, struct stat *st) {
long r = sc(SYS_newfstatat, AT_FDCWD, (long)host, (long)st, AT_SYMLINK_NOFOLLOW, 0);
if (r < 0) return r;
return S_ISLNK(st->st_mode) ? 1 : 0;
}
static int ret(long r) {
if (r < 0) { errno = (int)-r; return -1; }
return (int)r;
}
#define REAL(name, type) \
static __typeof__(type) real_##name; \
if (!real_##name) real_##name = (type)dlsym(RTLD_NEXT, #name)
/* ---------------------------------------------------------------- open */
static long fp_open(int dirfd, const char *path, int flags, mode_t mode) {
char host[PATH_MAX];
if ((flags & __O_TMPFILE) == __O_TMPFILE) return FP_SLOW;
long rs = resolve(dirfd, path, host);
if (rs != 0) return rs;
/* The last component is never followed here: a symlink there is proot's to resolve. */
long fd = sc(SYS_openat, AT_FDCWD, (long)host, flags | O_NOFOLLOW, (flags & O_CREAT) ? mode : 0, 0);
if (fd == -ELOOP && !(flags & O_NOFOLLOW)) return FP_SLOW;
if (fd >= 0 && (flags & O_PATH) && !(flags & O_NOFOLLOW)) {
struct stat st;
if (sc(SYS_fstat, fd, (long)&st, 0, 0, 0) != 0 || S_ISLNK(st.st_mode)) {
sc(SYS_close, fd, 0, 0, 0, 0);
return FP_SLOW;
}
}
return fd;
}
static int do_openat(int dirfd, const char *path, int flags, mode_t mode, int (*real)(int, const char *, int, ...)) {
long r = fp_open(dirfd, path, flags, mode);
if (r != FP_SLOW) { hits++; return ret(r); }
misses++;
return real(dirfd, path, flags, mode);
}
int openat(int dirfd, const char *path, int flags, ...) {
REAL(openat, int (*)(int, const char *, int, ...));
mode_t mode = 0;
if (flags & (O_CREAT | __O_TMPFILE)) { va_list ap; va_start(ap, flags); mode = va_arg(ap, mode_t); va_end(ap); }
return do_openat(dirfd, path, flags, mode, real_openat);
}
int openat64(int dirfd, const char *path, int flags, ...) __attribute__((alias("openat")));
int open(const char *path, int flags, ...) {
REAL(openat, int (*)(int, const char *, int, ...));
mode_t mode = 0;
if (flags & (O_CREAT | __O_TMPFILE)) { va_list ap; va_start(ap, flags); mode = va_arg(ap, mode_t); va_end(ap); }
return do_openat(AT_FDCWD, path, flags, mode, real_openat);
}
int open64(const char *path, int flags, ...) __attribute__((alias("open")));
int __open_2(const char *path, int flags) { return open(path, flags); }
int __open64_2(const char *path, int flags) { return open(path, flags); }
int __openat_2(int d, const char *path, int flags) { return openat(d, path, flags); }
int __openat64_2(int d, const char *path, int flags) { return openat(d, path, flags); }
/* ---------------------------------------------------------------- stat family */
static long fp_stat(int dirfd, const char *path, int flags, struct stat *st) {
char host[PATH_MAX];
if (flags & ~AT_SYMLINK_NOFOLLOW) return FP_SLOW;
long rs = resolve(dirfd, path, host);
if (rs != 0) return rs;
long k = kind(host, st);
if (k == 1 && !(flags & AT_SYMLINK_NOFOLLOW)) return FP_SLOW;
return k < 0 ? k : 0;
}
static int stat_common(int dirfd, const char *path, struct stat *st, int flags,
int (*real)(int, const char *, struct stat *, int)) {
long r = fp_stat(dirfd, path, flags, st);
if (r != FP_SLOW) { hits++; return ret(r); }
misses++;
return real(dirfd, path, st, flags);
}
int fstatat(int dirfd, const char *path, struct stat *st, int flags) {
REAL(fstatat, int (*)(int, const char *, struct stat *, int));
return stat_common(dirfd, path, st, flags, real_fstatat);
}
int fstatat64(int d, const char *p, struct stat64 *st, int f) { return fstatat(d, p, (struct stat *)st, f); }
int stat(const char *path, struct stat *st) {
REAL(fstatat, int (*)(int, const char *, struct stat *, int));
return stat_common(AT_FDCWD, path, st, 0, real_fstatat);
}
int stat64(const char *p, struct stat64 *st) { return stat(p, (struct stat *)st); }
int lstat(const char *path, struct stat *st) {
REAL(fstatat, int (*)(int, const char *, struct stat *, int));
return stat_common(AT_FDCWD, path, st, AT_SYMLINK_NOFOLLOW, real_fstatat);
}
int lstat64(const char *p, struct stat64 *st) { return lstat(p, (struct stat *)st); }
int statx(int dirfd, const char *path, int flags, unsigned mask, struct statx *out) {
REAL(statx, int (*)(int, const char *, int, unsigned, struct statx *));
char host[PATH_MAX];
long rs = (flags & ~(AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT | AT_STATX_SYNC_TYPE)) ? FP_SLOW : resolve(dirfd, path, host);
if (rs == -ENOENT) { hits++; return ret(rs); }
if (rs == 0) {
long r = sc(SYS_statx, AT_FDCWD, (long)host, flags | AT_SYMLINK_NOFOLLOW, mask | STATX_TYPE, (long)out);
if (!(r == 0 && S_ISLNK(out->stx_mode) && !(flags & AT_SYMLINK_NOFOLLOW))) { hits++; return ret(r); }
}
misses++;
return real_statx(dirfd, path, flags, mask, out);
}
/* ---------------------------------------------------------------- access */
static int access_common(int dirfd, const char *path, int mode, int flags) {
REAL(faccessat, int (*)(int, const char *, int, int));
char host[PATH_MAX];
struct stat st;
/* faccessat(2) has no flags: AT_EACCESS and AT_SYMLINK_NOFOLLOW stay with glibc and proot. */
long rs = flags == 0 ? resolve(dirfd, path, host) : FP_SLOW;
if (rs == -ENOENT) { hits++; return ret(rs); }
if (rs == 0) {
long k = kind(host, &st);
if (k != 1) {
hits++;
if (k < 0) return ret(k);
return ret(sc(SYS_faccessat, AT_FDCWD, (long)host, mode, 0, 0));
}
}
misses++;
return real_faccessat(dirfd, path, mode, flags);
}
int faccessat(int dirfd, const char *path, int mode, int flags) { return access_common(dirfd, path, mode, flags); }
int access(const char *path, int mode) { return access_common(AT_FDCWD, path, mode, 0); }
/* ---------------------------------------------------------------- readlink */
/* The link's own text needs no translation outside /proc: proot stores and returns it as is. */
static ssize_t readlink_common(int dirfd, const char *path, char *buf, size_t size) {
REAL(readlinkat, ssize_t (*)(int, const char *, char *, size_t));
char host[PATH_MAX];
long rs = resolve(dirfd, path, host);
if (rs == -ENOENT) { hits++; return ret(rs); }
if (rs == 0) {
hits++;
long r = sc(SYS_readlinkat, AT_FDCWD, (long)host, (long)buf, size, 0);
return r < 0 ? ret(r) : r;
}
misses++;
return real_readlinkat(dirfd, path, buf, size);
}
ssize_t readlinkat(int dirfd, const char *path, char *buf, size_t size) { return readlink_common(dirfd, path, buf, size); }
ssize_t readlink(const char *path, char *buf, size_t size) { return readlink_common(AT_FDCWD, path, buf, size); }
/* ---------------------------------------------------------------- libc calls that open internally */
DIR *opendir(const char *path) {
REAL(opendir, DIR *(*)(const char *));
long fd = fp_open(AT_FDCWD, path, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NONBLOCK, 0);
if (fd == FP_SLOW) { misses++; return real_opendir(path); }
hits++;
if (fd < 0) { errno = (int)-fd; return NULL; }
DIR *d = fdopendir((int)fd);
if (!d) close((int)fd);
return d;
}
static FILE *fopen_common(const char *path, const char *m, FILE *(*real)(const char *, const char *)) {
int flags;
switch (m[0]) {
case 'r': flags = strchr(m, '+') ? O_RDWR : O_RDONLY; break;
case 'w': flags = (strchr(m, '+') ? O_RDWR : O_WRONLY) | O_CREAT | O_TRUNC; break;
case 'a': flags = (strchr(m, '+') ? O_RDWR : O_WRONLY) | O_CREAT | O_APPEND; break;
default: return real(path, m);
}
if (strchr(m, 'e')) flags |= O_CLOEXEC;
if (strchr(m, 'x')) flags |= O_EXCL;
long fd = fp_open(AT_FDCWD, path, flags, 0666);
if (fd == FP_SLOW) { misses++; return real(path, m); }
hits++;
if (fd < 0) { errno = (int)-fd; return NULL; }
FILE *f = fdopen((int)fd, m);
if (!f) close((int)fd);
return f;
}
FILE *fopen(const char *path, const char *m) { REAL(fopen, FILE *(*)(const char *, const char *)); return fopen_common(path, m, real_fopen); }
FILE *fopen64(const char *path, const char *m) { REAL(fopen64, FILE *(*)(const char *, const char *)); return fopen_common(path, m, real_fopen64); }
@@ -0,0 +1,205 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: DroidDeck <droiddeck@users.noreply.github.com>
Date: Wed, 30 Sep 2026 00:00:00 +0000
Subject: [PATCH] fastpath: let translated syscalls from the tracee's trampoline
page through, and keep the kernel's cwd at the guest's
With PROOT_FASTPATH set (and only then), every SECCOMP_RET_TRACE in the filter
is preceded by a check of the caller's address: a syscall made from the fast
path's trampoline page (tools/proot/fastpath, FASTPATH_STUB_ADDR) was already
translated inside the tracee and runs without a stop. The check follows the
syscall-number dispatch, so untraced syscalls stay constant-ALLOW and keep the
kernel's seccomp action cache. chdir/fchdir, emulated as before, also move the
kernel's cwd to the host directory (host "/" where the kernel cannot follow),
and the first tracee starts there, so relative lookups made inside the tracee
resolve where proot resolves them. Without PROOT_FASTPATH nothing changes.
---
--- a/src/syscall/enter.c
+++ b/src/syscall/enter.c
@@ -20,7 +20,8 @@
* 02110-1301 USA.
*/
-#include <errno.h> /* errno(3), E* */
+#include <errno.h>
+#include <stdlib.h> /* errno(3), E* */
#include <talloc.h> /* talloc_*, */
#include <sys/un.h> /* struct sockaddr_un, */
#include <linux/net.h> /* SYS_*, */
@@ -198,6 +199,20 @@
if ((statl.st_mode & S_IXUSR) == 0)
return -EACCES;
+ /* With the fast path, the kernel's cwd follows the guest's
+ * (at its host path), so relative lookups made inside the
+ * tracee resolve where proot would resolve them. */
+ if (getenv("PROOT_FASTPATH") != NULL) {
+ int fd = open(path, O_PATH | O_DIRECTORY | O_CLOEXEC);
+
+ /* Where the kernel could not follow, it goes to the
+ * host's "/", which no binding names: the fast path
+ * then sends every relative lookup to proot. */
+ strcpy(oldpath, fd >= 0 ? path : "/");
+ if (fd >= 0)
+ close(fd);
+ }
+
/* Sadly this method doesn't detranslate statefully,
* this means that there's an ambiguity when several
* bindings are from the same host path:
@@ -229,6 +244,14 @@
tracee->fs->cwd = tmp;
talloc_set_name_const(tracee->fs->cwd, "$cwd");
+ if (getenv("PROOT_FASTPATH") != NULL) {
+ /* fchdir(2) runs as it is; chdir(2) on the host path. */
+ status = syscall_number == PR_chdir
+ ? set_sysarg_path(tracee, oldpath, SYSARG_1)
+ : 0;
+ break;
+ }
+
set_sysnum(tracee, PR_void);
status = 0;
break;
--- a/src/syscall/seccomp.c
+++ b/src/syscall/seccomp.c
@@ -35,6 +35,7 @@
#include <talloc.h> /* talloc_*, */
#include <errno.h> /* E*, */
#include <string.h> /* memcpy(3), */
+#include <stdlib.h> /* getenv(3), */
#include <stddef.h> /* offsetof(3), */
#include <stdint.h> /* uint*_t, UINT*_MAX, */
#include <stdbool.h>
@@ -77,9 +78,95 @@
* items). This function returns -errno if an error occurred,
* otherwise 0.
*/
+/* The fast path's trampoline page (tools/proot/fastpath): a traced syscall made from it was
+ * already translated inside the tracee and is let through without a stop. With PROOT_FASTPATH
+ * set, every SECCOMP_RET_TRACE is preceded by a check of the caller's address. The check sits
+ * after the syscall-number dispatch, so syscalls proot never traces stay constant-ALLOW and keep
+ * the kernel's seccomp action cache. FASTPATH_STUB_ADDR must match FP_STUB_ADDR in fastpath.c. */
+#define FASTPATH_STUB_ADDR 0xffff00000ULL
+#define FASTPATH_STUB_SIZE 0x1000ULL
+#define LENGTH_FASTPATH_TAIL 7
+
+static bool fastpath_enabled(void)
+{
+ static int enabled = -1;
+ if (enabled < 0)
+ enabled = getenv("PROOT_FASTPATH") != NULL;
+ return enabled;
+}
+
+static bool is_ret_trace(const struct sock_filter *statement)
+{
+ return statement->code == (BPF_RET + BPF_K)
+ && (statement->k & SECCOMP_RET_ACTION_FULL) == SECCOMP_RET_TRACE;
+}
+
+static int add_statements_raw(struct sock_fprog *program, size_t nb_statements,
+ struct sock_filter *statements);
+
+/* Append @statements, each RET_TRACE expanded into the trampoline check when the fast path is
+ * on, every jump within (or out of) the block adjusted for the expansion. */
static int add_statements(struct sock_fprog *program, size_t nb_statements,
struct sock_filter *statements)
{
+ struct sock_filter out[64];
+ size_t position[33];
+ size_t i, o;
+
+ if (!fastpath_enabled())
+ return add_statements_raw(program, nb_statements, statements);
+
+ assert(nb_statements <= 32);
+ for (i = 0, o = 0; i < nb_statements; i++) {
+ position[i] = o;
+ o += is_ret_trace(&statements[i]) ? LENGTH_FASTPATH_TAIL : 1;
+ }
+ position[nb_statements] = o;
+
+#define NEW_TARGET(index) (((index) <= nb_statements) ? position[(index)] : position[nb_statements] + (index) - nb_statements)
+
+ for (i = 0, o = 0; i < nb_statements; i++) {
+ struct sock_filter statement = statements[i];
+
+ if (BPF_CLASS(statement.code) == BPF_JMP) {
+ if (BPF_OP(statement.code) == BPF_JA)
+ statement.k = NEW_TARGET(i + 1 + statement.k) - position[i] - 1;
+ else {
+ statement.jt = NEW_TARGET(i + 1 + statement.jt) - position[i] - 1;
+ statement.jf = NEW_TARGET(i + 1 + statement.jf) - position[i] - 1;
+ }
+ }
+
+ if (!is_ret_trace(&statement)) {
+ out[o++] = statement;
+ continue;
+ }
+
+ {
+ const size_t ip = offsetof(struct seccomp_data, instruction_pointer);
+ const uint32_t hi = (uint32_t) (FASTPATH_STUB_ADDR >> 32);
+ const uint32_t lo = (uint32_t) FASTPATH_STUB_ADDR;
+ struct sock_filter tail[LENGTH_FASTPATH_TAIL] = {
+ BPF_STMT(BPF_LD + BPF_W + BPF_ABS, ip + sizeof(uint32_t)),
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, hi, 0, 4),
+ BPF_STMT(BPF_LD + BPF_W + BPF_ABS, ip),
+ BPF_JUMP(BPF_JMP + BPF_JGE + BPF_K, lo, 0, 2),
+ BPF_JUMP(BPF_JMP + BPF_JGE + BPF_K, lo + (uint32_t) FASTPATH_STUB_SIZE, 1, 0),
+ BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
+ statement,
+ };
+ memcpy(&out[o], tail, sizeof(tail));
+ o += LENGTH_FASTPATH_TAIL;
+ }
+ }
+#undef NEW_TARGET
+
+ return add_statements_raw(program, o, out);
+}
+
+static int add_statements_raw(struct sock_fprog *program, size_t nb_statements,
+ struct sock_filter *statements)
+{
size_t length;
void *tmp;
size_t i;
@@ -211,8 +298,16 @@
return add_statements(program, LENGTH_TRACE_IOCTL, statements);
}
+static size_t trace_length_raw(Sysnum sysnum);
+
static size_t trace_length(Sysnum sysnum)
{
+ /* Each trace block has exactly one RET_TRACE (see add_statements). */
+ return trace_length_raw(sysnum) + (fastpath_enabled() ? LENGTH_FASTPATH_TAIL - 1 : 0);
+}
+
+static size_t trace_length_raw(Sysnum sysnum)
+{
switch (sysnum) {
case PR_prctl:
case PR_setrlimit:
--- a/src/tracee/event.c
+++ b/src/tracee/event.c
@@ -98,6 +98,15 @@
if (getenv("PROOT_NO_SECCOMP") == NULL)
(void) enable_syscall_filtering(tracee);
+ /* The fast path resolves relative paths against the
+ * kernel's cwd: start it at the guest's (see chdir in
+ * syscall/enter.c). */
+ if (getenv("PROOT_FASTPATH") != NULL) {
+ char host_cwd[PATH_MAX];
+ if (translate_path(tracee, host_cwd, AT_FDCWD, tracee->fs->cwd, true) >= 0)
+ (void) chdir(host_cwd);
+ }
+
/* Now process is ptraced, so the current rootfs is already the
* guest rootfs. Note: Valgrind can't handle execve(2) on
* "foreign" binaries (ENOEXEC) but can handle execvp(3) on such