mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
Merge pull request #113 from maxjivi05/feat/performance-fixes-2
This commit is contained in:
19 files changed
+1626
-2
No files matched your search
@@ -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) }
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user