Skip to content

epoll_pwait2: issue raw syscall, add Android gate and disable flag - #32490

Merged
Jarred-Sumner merged 10 commits into
mainfrom
farm/e256f127/epoll-pwait2-raw-syscall
Jun 21, 2026
Merged

epoll_pwait2: issue raw syscall, add Android gate and disable flag#32490
Jarred-Sumner merged 10 commits into
mainfrom
farm/e256f127/epoll-pwait2-raw-syscall

Conversation

@robobun

@robobun robobun commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Fixes #32489.

What happened

A linux-arm64 glibc build running on Android (via Termux + glibc-runner + a patched ELF interpreter) segfaults at 0x0 inside sys_epoll_pwait2 the first time the event loop waits:

Segmentation fault at address 0x00000000
  linux.rs:38                  sys_epoll_pwait2
  epoll_kqueue.c:142           bun_epoll_pwait2
  epoll_kqueue.c:391           us_loop_run_bun_tick
  src/uws_sys/Loop.rs:249      tick
  src/http/HTTPThread.rs:1372  HttpThread::process_events

Android's per-app seccomp policy only whitelists epoll_pwait, not epoll_pwait2 (syscall 441). The existing runtime fallback in bun_epoll_pwait2 handles ENOSYS/EPERM/EOPNOTSUPP/EACCES returns correctly (verified under a seccomp SECCOMP_RET_ERRNO | ENOSYS filter on stock Linux), but in this environment the fault happens inside glibc's syscall(2) error path before a return value can be checked, so the fallback never runs. The reporter's LD_PRELOAD shim that intercepts syscall() and redirects 441 to epoll_pwait() fixes the binary end to end, which confirms the call site.

Cause

The Zig implementation of sys_epoll_pwait2 issued a raw syscall via std.os.linux.syscall6 (inline asm, kernel returns -errno in the result register, no libc touched). The Rust port changed this to libc::syscall(SYS_epoll_pwait2, ...), which on failure writes thread-local errno inside glibc and returns -1; the port then re-reads errno and 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__isEpollPwait2SupportedOnLinuxKernel only 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_pwait2 now issues the syscall via per-arch inline asm (syscall on x86_64, svc #0 on aarch64) and returns the raw kernel value directly. No libc::syscall, no errno read. This restores exact parity with the Zig reference and removes the encode_raw_errno helper.
  • src/analytics/lib.rs: Bun__isEpollPwait2SupportedOnLinuxKernel now also returns 0 when the uname(2) release string contains -android (runtime detection; a glibc build running on Android is compiled with target_os = "linux" so a cfg gate cannot catch it), or when BUN_FEATURE_FLAG_DISABLE_EPOLL_PWAIT2 is set. Same pattern as the existing WSL detection a few lines below.
  • src/bun_core/env_var.rs: add BUN_FEATURE_FLAG_DISABLE_EPOLL_PWAIT2 as an escape hatch for other seccomp-restricted environments that block 441 without returning a checkable errno.

The existing ENOSYS/EPERM/EOPNOTSUPP/EACCES fallback in epoll_kqueue.c is unchanged and still handles the common case.

Verification

test/regression/issue/32489.test.ts compiles a seccomp helper that installs a SECCOMP_RET_KILL_PROCESS filter on __NR_epoll_pwait2, sets BUN_FEATURE_FLAG_DISABLE_EPOLL_PWAIT2=1, and runs both a main-loop timer and an HTTP-thread fetch() (the exact loop the crash report shows). If epoll_pwait2 is 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_pwait2 is attempted).
After: both exit 0 via the epoll_pwait path.

Also checked that the raw-syscall path still drives the event loop correctly on a host where epoll_pwait2 is available (Bun.serve + fetch + timer, no flag set), and that the pre-existing fs-stat-seccomp-linux test still passes. cargo check -p bun_platform --target aarch64-unknown-linux-gnu passes for the aarch64 inline asm.

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
@robobun

robobun commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 8:53 PM PT - Jun 20th, 2026

@robobun, your commit 7554c24b064a1dccbf3183f0ee803e379861b639 passed in Build #63712! 🎉


🧪   To try this PR locally:

bunx bun-pr 32490

That installs a local version of the PR into your bun-32490 executable, so you can run:

bun-32490 --bun

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a BUN_FEATURE_FLAG_DISABLE_EPOLL_PWAIT2 boolean env var that forces the event loop to use epoll_pwait instead of epoll_pwait2. The kernel support check gains an early-return guard for this flag. sys_epoll_pwait2 is refactored to use a new inline-asm raw_syscall6 helper instead of the glibc wrapper plus encode_raw_errno. A seccomp-based regression test validates the full path.

Changes

epoll_pwait2 Disable Flag and Syscall Refactor

Layer / File(s) Summary
Feature flag declaration and kernel support gating
src/bun_core/env_var.rs, src/analytics/lib.rs
Declares BUN_FEATURE_FLAG_DISABLE_EPOLL_PWAIT2 as a boolean env var (default false), then gates Bun__isEpollPwait2SupportedOnLinuxKernel with an early return of 0 when the flag is set.
Inline-asm raw_syscall6 and sys_epoll_pwait2 refactor
src/platform/linux.rs
Replaces libc::syscall + encode_raw_errno with a new raw_syscall6 helper using arch-specific inline asm! (x86_64/aarch64) that returns the kernel's raw in-band -errno result; sys_epoll_pwait2 is updated to call it directly.
Seccomp regression test
test/regression/issue/32489.test.ts
Adds a Linux-only test suite that compiles a C seccomp guard (kills on epoll_pwait2 invocation), defines helper functions for building and running Bun under the guard with BUN_FEATURE_FLAG_DISABLE_EPOLL_PWAIT2=1, and executes two test scenarios—main-loop timer path and HTTP server thread path—asserting both exit cleanly with expected output.

Possibly related issues

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: replacing libc-based syscalls with raw inline asm and adding a feature flag to disable epoll_pwait2.
Description check ✅ Passed The description comprehensively addresses both template sections: it clearly explains what the PR does (fixes segfault via raw syscall + feature flag) and verifies the fix via regression testing and manual verification on multiple platforms.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between eae8038 and cdc9ad5.

📒 Files selected for processing (4)
  • src/analytics/lib.rs
  • src/bun_core/env_var.rs
  • src/platform/linux.rs
  • test/regression/issue/32489.test.ts

Comment thread test/regression/issue/32489.test.ts Outdated
robobun added 2 commits June 18, 2026 16:29
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between d0b157f and 3a8d571.

📒 Files selected for processing (1)
  • test/regression/issue/32489.test.ts

Comment thread test/regression/issue/32489.test.ts

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 in raw_syscall6 for 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 previous libc::syscall path.

@robobun

robobun commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator Author

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 Received cancellation signal, interrupting after bun_platform had already compiled). They are not compile errors.

On the current build #63346 for 00075cd, all four of those lanes pass:

  • aarch64-android - build-rust
  • x64-android - build-rust
  • freebsd x64 - build-rust
  • freebsd aarch64 - build-rust

Verified locally as well with cargo check -p bun_platform -p bun_analytics --target {x86_64,aarch64}-linux-android and --target x86_64-unknown-freebsd.

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 src/platform/linux.zig:75 which used std.os.linux.syscall6). Neither options(nomem) nor options(readonly) is set, so the default memory clobber is in effect for the kernel writing through events.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: replaces libc::syscall() + encode_raw_errno() with a new raw_syscall6 helper implemented as per-arch inline asm! (syscall on x86_64, svc #0 on aarch64), called from the #[no_mangle] sys_epoll_pwait2 that epoll_kqueue.c invokes on every loop tick.
  • src/analytics/lib.rs: adds two early-return gates (env flag, -android in the uname release string) to Bun__isEpollPwait2SupportedOnLinuxKernel.
  • src/bun_core/env_var.rs: declares BUN_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 Jarred-Sumner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can detect android by using the cfg flag instead of runtime.

@robobun

robobun commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator Author

Switched to cfg(target_os) in fd6eb0a: the function now returns 0 for anything that isn't target_os = "linux", so android targets skip epoll_pwait2 at compile time.

One note on the tradeoff: the crash in #32489 is from a linux-arm64 glibc build running on Android via Termux's glibc-runner with a patched ELF interpreter, so that specific binary is compiled with target_os = "linux" and won't hit the cfg gate. For that setup this PR still helps via:

  • the raw-syscall change in sys_epoll_pwait2 (no more glibc errno hop, which is where their stack trace faults), and
  • BUN_FEATURE_FLAG_DISABLE_EPOLL_PWAIT2=1 as an explicit escape hatch.

Happy to put the runtime uname check back alongside the cfg gate if you'd rather have both.

Comment thread test/regression/issue/32489.test.ts
@robobun

robobun commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator Author

CI on fe54171 (build #63385) is green for this diff. The one red lane is unrelated infrastructure:

  • :darwin: 14 aarch64 - test-bun: test/js/web/websocket/autobahn.test.ts fails because the autobahn Docker image is linux/amd64 and the host is arm64 (exec /opt/pypy/bin/wstest: exec format error). Not touched by this PR.
  • x64-asan flaky warnings are bun install timeouts during test setup.

test/regression/issue/32489.test.ts passed on every Linux lane including the Alpine x64/aarch64 lanes that previously flagged the intentional control-run core dumps. All Android and FreeBSD build-rust lanes pass. Ready for re-review.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

@robobun if it gets EFAULT then we could manually set the disable env var to true.

@robobun

robobun commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

Agreed. With the raw syscall in place the kernel return comes back cleanly, so the existing ENOSYS/EPERM/EOPNOTSUPP/EACCES latch in bun_epoll_pwait2 should catch the normal seccomp-blocked case, and anyone still hitting trouble on a shimmed setup can set BUN_FEATURE_FLAG_DISABLE_EPOLL_PWAIT2=1.

I left EFAULT out of the latch list since that one can also mean a genuinely bad events/timeout pointer and I'd rather not mask that, but happy to add it if you'd prefer the latch to be wider.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

@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.
@robobun

robobun commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator Author

Added EFAULT to the bun_epoll_pwait2 fallback latch in e7af444. The pointer arguments (epoll fd, the static ready_polls array, stack timespec/sigset_t) are all well-formed, so an EFAULT here can only come from the environment rejecting the call; it now latches off the same as ENOSYS/EPERM/EOPNOTSUPP/EACCES.

Comment thread src/bun_core/env_var.rs
@Jarred-Sumner
Jarred-Sumner merged commit 1031c66 into main Jun 21, 2026
78 of 79 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/e256f127/epoll-pwait2-raw-syscall branch June 21, 2026 07:06
ferrumclaudepilgrim added a commit to ferrumclaudepilgrim/claude-code-android that referenced this pull request Jul 1, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

epoll_pwait2 has no ENOSYS fallback, causes segfault at 0x0 on Android (seccomp blocks the syscall)

2 participants