epoll_pwait2: issue raw syscall, add Android gate and disable flag - #32490
Conversation
sys_epoll_pwait2 now issues the syscall via inline asm (matching the Zig reference's std.os.linux.syscall6) instead of routing through glibc's syscall(2) wrapper and re-reading thread-local errno. The C caller in epoll_kqueue.c decodes errno from the raw kernel return value, so no errno round-trip is needed. Bun__isEpollPwait2SupportedOnLinuxKernel now also returns 0 when BUN_FEATURE_FLAG_DISABLE_EPOLL_PWAIT2 is set, or when the uname release string contains -android. Android's per-app seccomp policy blocks epoll_pwait2, and on shimmed-glibc setups the blocked call can fault inside libc's error path before the runtime ENOSYS fallback gets a chance to run. Fixes #32489
|
Updated 8:53 PM PT - Jun 20th, 2026
✅ @robobun, your commit 7554c24b064a1dccbf3183f0ee803e379861b639 passed in 🧪 To try this PR locally: bunx bun-pr 32490That installs a local version of the PR into your bun-32490 --bun |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds a Changesepoll_pwait2 Disable Flag and Syscall Refactor
Possibly related issues
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/regression/issue/32489.test.ts`:
- Around line 119-128: Add a control run verification function (such as
guardWouldCatchEpollPwait2) that executes the same snippet without the
BUN_FEATURE_FLAG_DISABLE_EPOLL_PWAIT2 flag to prove the syscall would actually
be blocked by seccomp on the current system. Before each flagged assertion in
runUnderSeccomp and related test paths, call this control function to verify the
precondition exists on the host machine, and skip the test with a warning only
when the control run does not get killed by seccomp, ensuring the test fails for
the right reason rather than passing vacuously.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: d52013c7-3ac8-4452-b09d-64fa7aa0a1d1
📒 Files selected for processing (4)
src/analytics/lib.rssrc/bun_core/env_var.rssrc/platform/linux.rstest/regression/issue/32489.test.ts
Verifies the precondition (epoll_pwait2 would be issued and killed without the flag) before asserting on the flagged run, so the test cannot pass vacuously on hosts where the kernel-version gate or some other condition already disables epoll_pwait2.
The seccomp helper now sets RLIMIT_CORE=0 before exec so the control run (which is deliberately killed by SECCOMP_RET_KILL_PROCESS with SIGSYS) does not leave a core file for the CI runner to flag. Alpine lanes run with unlimited core size by default.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/regression/issue/32489.test.ts`:
- Around line 55-60: The setrlimit call to set RLIMIT_CORE to zero is not being
checked for success or failure before proceeding to the execvp call that
deliberately triggers SIGSYS. If setrlimit fails, the core dump limit will not
be applied, allowing the subsequent intentional crash to generate a core file
and cause CI failures. Add a check after the setrlimit(RLIMIT_CORE, &no_core)
call to verify its return value equals zero, and if it fails, either fail the
test or skip it before proceeding to execvp. This ensures the test is hermetic
and the control run cannot create unwanted core files.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: f017b51d-cd10-4067-8a2b-9418f14ddeb3
📒 Files selected for processing (1)
test/regression/issue/32489.test.ts
There was a problem hiding this comment.
I didn't find any bugs, but this swaps the event-loop's epoll_pwait2 hot path from libc::syscall to hand-written per-arch inline asm and the last CI snapshot still shows build-rust failures on the Android/FreeBSD targets — worth a human look before merging.
Extended reasoning...
Overview
This PR touches four files to fix #32489 (segfault in sys_epoll_pwait2 on Android via glibc-runner). The substantive change is in src/platform/linux.rs, where sys_epoll_pwait2 is rewritten from libc::syscall(...) + an encode_raw_errno helper to a new raw_syscall6 that issues the syscall directly via core::arch::asm! (syscall on x86_64, svc #0 on aarch64) and returns the raw kernel -errno-in-band result. src/analytics/lib.rs adds two early-return gates to Bun__isEpollPwait2SupportedOnLinuxKernel (the new BUN_FEATURE_FLAG_DISABLE_EPOLL_PWAIT2 flag and a runtime -android substring check on the cached uname release). src/bun_core/env_var.rs declares the flag. A new ~200-line seccomp-based regression test compiles a C helper that installs a SECCOMP_RET_KILL_PROCESS filter on __NR_epoll_pwait2, runs control + flagged subprocesses for both the main loop and HTTP-thread loop, and asserts the flag prevents the syscall.
Security risks
None apparent. The inline asm only invokes epoll_pwait2 with caller-supplied arguments (same as before); the feature flag and Android gate only ever narrow behavior to the older, already-supported epoll_pwait path. The seccomp test helper runs in a temp-dir subprocess and only restricts itself.
Level of scrutiny
High. sys_epoll_pwait2 is called from us_loop_run_bun_tick on every event-loop wait on Linux — it is one of the hottest code paths in the runtime. Replacing a libc trampoline with hand-rolled per-architecture inline assembly is exactly the kind of change where a subtle mistake (register assignment, clobber list, options(...) flags, sign-extension of epfd/maxevents) would not necessarily show up in a happy-path test but could corrupt state or mis-decode errno under load. The asm looks correct to me (x86_64: rax/rdi/rsi/rdx/r10/r8/r9 with rcx+r11 clobbered; aarch64: x8 + x0..x5; default memory side-effects since neither nomem nor readonly is set), but I'm not confident enough to waive human review on inline asm in this path.
Other factors
- The robobun CI comment (last updated for commit 3a8d571) reports build-rust failures on
x64-android,aarch64-android, and both FreeBSD targets; the only commit since (00075cd) touches the test file, so those build failures are likely still live and should be checked. - The two CodeRabbit findings (vacuous-pass control run, unchecked
setrlimit) were both addressed in d0b157f and 00075cd and are marked resolved. - No CODEOWNERS cover the changed files.
- The
compile_error!fallback inraw_syscall6for non-x86_64/aarch64 means any future Linux arch (e.g. riscv64) will fail to build rather than silently misbehave, which is the right trade-off but is a behavior change from the previouslibc::syscallpath.
|
Re the Android/FreeBSD build-rust failures mentioned above: those were from build #63345 which was cancelled mid-run when 00075cd was pushed (the job logs end with On the current build #63346 for 00075cd, all four of those lanes pass:
Verified locally as well with On the inline-asm scrutiny: agreed this is a hot path and worth eyes. The register assignments match the Linux syscall ABI for both arches (and the Zig reference at |
There was a problem hiding this comment.
I didn't find any bugs, but this swaps the event loop's hot-path epoll_pwait2 call for hand-written per-arch inline asm on x86_64 and aarch64 — the register/clobber lists look right to me, but this is the kind of change that deserves a human pass before merge.
Extended reasoning...
Overview
This PR touches four files to fix #32489 (segfault on Android when seccomp blocks epoll_pwait2):
src/platform/linux.rs: replaceslibc::syscall()+encode_raw_errno()with a newraw_syscall6helper implemented as per-arch inlineasm!(syscallon x86_64,svc #0on aarch64), called from the#[no_mangle]sys_epoll_pwait2thatepoll_kqueue.cinvokes on every loop tick.src/analytics/lib.rs: adds two early-return gates (env flag,-androidin the uname release string) toBun__isEpollPwait2SupportedOnLinuxKernel.src/bun_core/env_var.rs: declaresBUN_FEATURE_FLAG_DISABLE_EPOLL_PWAIT2.test/regression/issue/32489.test.ts: new seccomp-kill regression test with a control run.
Security risks
None identified. The change narrows the syscall surface (removes a glibc TLS dependency) rather than expanding it; the new env var only ever disables an optimization. The seccomp BPF in the test is self-contained in a child process.
Level of scrutiny
High. The raw_syscall6 inline asm is the primary event-loop wait on every Linux build. The register mappings (x86_64: rax/rdi/rsi/rdx/r10/r8/r9, rcx+r11 clobbered; aarch64: x8 nr, x0–x5 args, x0 return) and the implicit memory clobber (no nomem/readonly in options) match the Linux syscall ABI and the Zig std.os.linux.syscall6 reference, but a mistake here would corrupt every epoll wait rather than fail loudly. This is exactly the kind of low-level, two-arch, hot-path change that should be signed off by a human with platform expertise rather than auto-approved.
Other factors
- All CodeRabbit findings (vacuous-pass control run, unchecked
setrlimit) have been addressed and resolved. - The author confirmed the earlier Android/FreeBSD CI failures were a cancelled build, and the current build passes those lanes.
- The env-var and Android-gate additions are mechanical and follow existing patterns (mirrors the WSL detection a few lines below).
- No prior
claude[bot]review on this PR; this is my first pass.
Jarred-Sumner
left a comment
There was a problem hiding this comment.
We can detect android by using the cfg flag instead of runtime.
|
Switched to One note on the tradeoff: the crash in #32489 is from a
Happy to put the runtime |
|
CI on fe54171 (build #63385) is green for this diff. The one red lane is unrelated infrastructure:
|
|
@robobun if it gets EFAULT then we could manually set the disable env var to true. |
|
Agreed. With the raw syscall in place the kernel return comes back cleanly, so the existing I left |
|
@roboun no, there's no real way an efault happens and we should instead enable the fallback on efault. |
All pointer arguments to sys_epoll_pwait2 are well-formed stack/loop state, so EFAULT from the kernel here indicates the syscall is being rejected by the environment rather than a caller bug. Treat it the same as ENOSYS/EPERM/EOPNOTSUPP/EACCES and fall back to epoll_pwait.
|
Added |
The crash section framed both messages as a seccomp block. Only 'Bad system call' is seccomp (a syscall the filter rejects: statx on an Android 10 build, pidfd_open on newer native). 'Segmentation fault' / 'Bun has crashed' on newer phones is a null-pointer crash inside Termux's glibc-runner epoll_pwait2 shim that Bun 1.4 triggers at startup, not a blocked syscall. Fixed upstream in oven-sh/bun#32490. The shipped v2.9.2 CHANGELOG entry is left as historical record. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes #32489.
What happened
A linux-arm64 glibc build running on Android (via Termux + glibc-runner + a patched ELF interpreter) segfaults at
0x0insidesys_epoll_pwait2the first time the event loop waits:Android's per-app seccomp policy only whitelists
epoll_pwait, notepoll_pwait2(syscall 441). The existing runtime fallback inbun_epoll_pwait2handlesENOSYS/EPERM/EOPNOTSUPP/EACCESreturns correctly (verified under a seccompSECCOMP_RET_ERRNO | ENOSYSfilter on stock Linux), but in this environment the fault happens inside glibc'ssyscall(2)error path before a return value can be checked, so the fallback never runs. The reporter'sLD_PRELOADshim that interceptssyscall()and redirects 441 toepoll_pwait()fixes the binary end to end, which confirms the call site.Cause
The Zig implementation of
sys_epoll_pwait2issued a raw syscall viastd.os.linux.syscall6(inline asm, kernel returns-errnoin the result register, no libc touched). The Rust port changed this tolibc::syscall(SYS_epoll_pwait2, ...), which on failure writes thread-localerrnoinside glibc and returns-1; the port then re-readserrnoand re-encodes it into the raw-kernel convention for the C caller. That added a glibc TLS dependency to a hot-path call that previously had none, and is where the Android+glibc-runner process faults.Separately, the loop-init gate
Bun__isEpollPwait2SupportedOnLinuxKernelonly checks kernel version (>= 5.11). Android 13 on kernel 5.15 passes that check but seccomp blocks the syscall anyway.Fix
src/platform/linux.rs:sys_epoll_pwait2now issues the syscall via per-arch inline asm (syscallon x86_64,svc #0on aarch64) and returns the raw kernel value directly. Nolibc::syscall, noerrnoread. This restores exact parity with the Zig reference and removes theencode_raw_errnohelper.src/analytics/lib.rs:Bun__isEpollPwait2SupportedOnLinuxKernelnow also returns 0 when theuname(2)release string contains-android(runtime detection; a glibc build running on Android is compiled withtarget_os = "linux"so acfggate cannot catch it), or whenBUN_FEATURE_FLAG_DISABLE_EPOLL_PWAIT2is set. Same pattern as the existing WSL detection a few lines below.src/bun_core/env_var.rs: addBUN_FEATURE_FLAG_DISABLE_EPOLL_PWAIT2as an escape hatch for other seccomp-restricted environments that block 441 without returning a checkable errno.The existing
ENOSYS/EPERM/EOPNOTSUPP/EACCESfallback inepoll_kqueue.cis unchanged and still handles the common case.Verification
test/regression/issue/32489.test.tscompiles a seccomp helper that installs aSECCOMP_RET_KILL_PROCESSfilter on__NR_epoll_pwait2, setsBUN_FEATURE_FLAG_DISABLE_EPOLL_PWAIT2=1, and runs both a main-loop timer and an HTTP-threadfetch()(the exact loop the crash report shows). Ifepoll_pwait2is ever issued the process is killed by the kernel before any fallback can run.Before: both subprocesses die with
SIGSYS(the flag does not exist,epoll_pwait2is attempted).After: both exit 0 via the
epoll_pwaitpath.Also checked that the raw-syscall path still drives the event loop correctly on a host where
epoll_pwait2is available (Bun.serve + fetch + timer, no flag set), and that the pre-existingfs-stat-seccomp-linuxtest still passes.cargo check -p bun_platform --target aarch64-unknown-linux-gnupasses for the aarch64 inline asm.