Session stop: kill guest processes proot lost, and cap the session log

Twice today a session's Xwayland aborted from one thread ("The futex
facility returned an unexpected error code"), proot lost the tracee, and
the process lived on reparented to init: proot's seccomp filter, with no
tracer left, answers ENOSYS, so Xwayland's main loop logged
"WaitForSomething(): poll: Function not implemented" until something killed
it - 9 GB after Dolphin, 13 GB after PPSSPP. --kill-on-exit and the tree
sweep only reach what proot still traces, and the straggler sweep ran only
at the next session's start.

Now a session stop also kills every process of ours whose executable lives
in the Linux runtime, and bannerlator-session keeps only the first
BL_LOG_CAP bytes (256 MB) of session.log, draining the rest so a writer
that filled it is not killed by SIGPIPE.
This commit is contained in:
The412Banner committed 2026-09-27 16:52:51 -04:00
1 parent 9aed4e7a5c
commit 9d13b238bd
2 files changed
+29 -1

No files matched your search

@@ -741,6 +741,30 @@ class SessionService : Service() {
* behind, still holding the Wayland socket and the audio server the next session needs. They
* are our uid, so they are ours to kill.
*/
/**
* Every process of ours still running a program out of the Linux runtime once proot is gone.
* proot's --kill-on-exit and the tree sweep only reach what proot still traces; a tracee it
* lost - an Xwayland that aborted from one thread and then looped on a syscall proot's own
* seccomp filter, with no tracer left, answers ENOSYS - survives both, reparented to init,
* and wrote 9 GB of one line into the session log before anything killed it.
*/
private fun killGuestLeftovers() {
val me = android.os.Process.myPid()
val rootfs = try { File(filesDir, "linuxfs").canonicalPath } catch (e: Exception) { return } + "/"
val procs = File("/proc").listFiles { f -> f.name.all { it.isDigit() } } ?: return
var killed = 0
for (proc in procs) {
val pid = proc.name.toIntOrNull() ?: continue
if (pid == me) continue
// Another uid's exe is not ours to read; a process already gone has none.
val exe = try { File(proc, "exe").canonicalPath } catch (e: Exception) { continue }
if (!exe.startsWith(rootfs)) continue
android.os.Process.killProcess(pid)
killed++
}
if (killed > 0) Log.w(TAG, "killed $killed guest process(es) proot no longer tracked")
}
private fun killStragglers() {
val me = android.os.Process.myPid()
val procs = File("/proc").listFiles { f -> f.name.all { it.isDigit() } } ?: return
@@ -917,6 +941,7 @@ class SessionService : Service() {
Thread({
auxiliary.forEach { (pid, started) -> teardown(pid, started) }
if (prootPid > 1) teardown(prootPid)
killGuestLeftovers()
finishSessionStop(status)
}, "session-teardown").start()
} else {
@@ -19,7 +19,10 @@ if [ -n "${BL_LOG:-}" ] && [ -z "${BL_INSIDE:-}" ]; then
if [ "$(stat -c %s "$BL_LOG" 2>/dev/null || echo 0)" -gt 8388608 ]; then
mv -f "$BL_LOG" "$BL_LOG.1"
fi
exec >>"$BL_LOG" 2>&1
# A guest looping on an error writes gigabytes in minutes (an Xwayland proot had lost track of
# managed 9 GB of one line in 19 min): the log keeps its first BL_LOG_CAP bytes and the rest is
# drained, not refused - a writer that filled it must not die of SIGPIPE.
exec > >({ head -c "${BL_LOG_CAP:-268435456}" >>"$BL_LOG"; cat >/dev/null; }) 2>&1
echo "== bannerlator-session $(date) mode=$mode $*"
echo "== uid=$(id -u) size=${BL_WIDTH:-?}x${BL_HEIGHT:-?} fps=${BL_FPS:-0} icd=${VK_ICD_FILENAMES:-none}"
echo "== gamescope: $(gamescope --version 2>&1 | head -1) at $(command -v gamescope)"