Add bun.sys.sigaction with bionic-correct layout for Android#30389
Conversation
std.c.Sigaction and std.c.sigset_t on .linux assume the glibc/musl
layout (handler, 128-byte mask, flags). bionic LP64 is
{ int sa_flags; sa_handler; sigset_t sa_mask; sa_restorer } with
sigset_t = unsigned long. Passing the glibc-shaped struct to bionic
sigaction() makes it read sa_handler from offset 8 — Zig's mask[0].
A zeroed mask silently installs SIG_DFL; a mask with SIGCHLD set
(WaiterThread.reloadHandlers) installs 0x10000 and segfaults on
delivery.
Add bun.sys.{Sigaction,sigset_t,sigemptyset,sigaddset,sigaction}: the
bionic extern struct on Android, transparent aliases to std.posix
elsewhere. Replace every std.posix.sigaction callsite.
Includes a comptime tripwire that fires once std.c.Sigaction gains a
bionic case upstream, and a zig build check-android step (gated on
-Dandroid_ndk_sysroot like check-freebsd).
|
Updated 2:12 AM PT - May 8th, 2026
❌ @robobun, your commit bd939db has 3 failures in
🧪 To try this PR locally: bunx bun-pr 30389That installs a local version of the PR into your bun-30389 --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:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughRefactors POSIX signal handling to use an Android-aware bun.sys abstraction, migrates many sigaction usages to bun.sys, adds a TestingAPI + JS binding and POSIX tests to validate sigaction layout, and guards Android build checks on NDK presence. ChangesSignal Handling Abstraction Migration
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
| pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; | ||
| pub const sigaction_fn = *const fn (i32, *const posix.siginfo_t, ?*anyopaque) callconv(.c) void; | ||
|
|
||
| flags: c_uint, |
There was a problem hiding this comment.
pretty sure this should be c_int instead of c_uint
There was a problem hiding this comment.
bionic does declare int sa_flags, but SA_RESETHAND = 0x80000000 — Zig refuses to coerce that into a c_int literal, so .flags = SA.SIGINFO | SA.RESTART | SA.RESETHAND in crash_handler.zig / filter_run.zig / multi_run.zig would stop compiling on Android. c_uint is ABI-identical (same size/align) and is what std.c.Sigaction uses for every other Linux libc for the same reason. Added a comment at the field explaining this.
Installs a known SIGUSR2 handler via bun.sys.sigaction, reads it back via sigaction(sig, NULL, &old), and asserts the handler pointer and SA_RESTART flag survive the trip through libc. That holds iff bun.sys.Sigaction's field offsets agree with the host libc's struct sigaction — the property that is violated by std.posix.Sigaction on Android bionic.
raiseIgnoringPanicHandler re-raises a child's terminating signal, which can be SIGKILL (bun run after kill -9). std.posix.sigaction does 'else => unreachable' on the EINVAL that follows, so the previous non-Android path was a reachable-unreachable regression. Call std.c.sigaction directly and discard the result instead. Also: - comptime tripwire now compares @offsetof(flags)/@offsetof(handler) instead of @sizeof - note why sa_flags stays c_uint (SA_RESETHAND = 0x80000000 doesn't fit c_int; ABI-identical, matches std.c.Sigaction on other Linux) - add test/cli/run/run-propagate-sigkill.test.ts to pin the bun run -> SIGKILL re-raise path
A static import throws at module load when the export is absent, which the junit reporter records as zero test failures. require() inside the test lets expect(...).toBeFunction() fail the test properly.
…#30389) ## Problem `std.c.Sigaction` / `std.c.sigset_t` for `.linux` assume the glibc/musl layout: `{ handler, mask[128B], flags, restorer }`. bionic LP64 is `{ int sa_flags; sa_handler; sigset_t sa_mask; sa_restorer }` where `sigset_t` is a single `unsigned long`. When Bun calls `std.posix.sigaction()` on Android, bionic reads `sa_handler` from offset 8 — which is Zig's `mask[0]`: * Every handler installed with `mask = sigemptyset()` (SIGPIPE/SIGXFSZ in `main`, the crash handler, SIGINT in repl/filter_run/multi_run/Coordinator) silently becomes `SIG_DFL`. Broken-pipe kills the process, no crash trace on SEGV, Ctrl+C isn't caught. * `WaiterThread.reloadHandlers()` sets `mask[0] = 1<<16` (SIGCHLD), so bionic installs the handler `0x10000`. When SIGCHLD fires, the kernel jumps there → `SEGV_MAPERR`, `rip=0x10000`, `rdi=17`. Reproduces 100% on a full Android emulator (not under qemu-user). ## Fix Add `bun.sys.{Sigaction, sigset_t, sigemptyset, sigaddset, sigaction}` in `src/sys/sys.zig`: * **Android**: an `extern struct` matching bionic `bits/signal_types.h` (`__LP64__`) and an `@extern` to libc `sigaction` that takes it. `sigset_t = c_ulong`. * **Everywhere else**: transparent aliases of `std.posix.*`, so nothing changes. Replace all nine callsites (`main.zig`, `crash_handler.zig`, `process.zig`, `repl.zig`, `filter_run.zig`, `multi_run.zig`, `Coordinator.zig`, `Global.zig`). A comptime tripwire fires if `@sizeOf(std.c.Sigaction)` ever shrinks to the bionic 32 bytes, so the workaround can be dropped once the Zig stdlib bug is fixed upstream. Also adds `zig build check-android[-debug]`, gated on `-Dandroid_ndk_sysroot` like `check-freebsd`, so the Android-only struct gets compile-checked. ## Verification ``` $ zig build check-android-debug -Dandroid_ndk_sysroot=…/sysroot check-android-debug success compile obj bun-debug Debug x86_64-linux-android success compile obj bun-debug Debug aarch64-linux-android success ``` `bun run zig:check-all` still passes all 16 targets. Host smoke tests (`spawn/exit-code`, `spawn/spawn-signal`, `spawn/spawn-kill-signal`, plus `BUN_FEATURE_FLAG_FORCE_WAITER_THREAD=1` to hit the modified SIGCHLD path) pass. No runtime test is included because the misbehaviour only surfaces on a real Android kernel, which CI does not run. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Signed-off-by: Sisyphus <sisyphus@ohos-bun.dev>
Problem
std.c.Sigaction/std.c.sigset_tfor.linuxassume the glibc/musl layout:{ handler, mask[128B], flags, restorer }. bionic LP64 is{ int sa_flags; sa_handler; sigset_t sa_mask; sa_restorer }wheresigset_tis a singleunsigned long.When Bun calls
std.posix.sigaction()on Android, bionic readssa_handlerfrom offset 8 — which is Zig'smask[0]:mask = sigemptyset()(SIGPIPE/SIGXFSZ inmain, the crash handler, SIGINT in repl/filter_run/multi_run/Coordinator) silently becomesSIG_DFL. Broken-pipe kills the process, no crash trace on SEGV, Ctrl+C isn't caught.WaiterThread.reloadHandlers()setsmask[0] = 1<<16(SIGCHLD), so bionic installs the handler0x10000. When SIGCHLD fires, the kernel jumps there →SEGV_MAPERR,rip=0x10000,rdi=17. Reproduces 100% on a full Android emulator (not under qemu-user).Fix
Add
bun.sys.{Sigaction, sigset_t, sigemptyset, sigaddset, sigaction}insrc/sys/sys.zig:extern structmatching bionicbits/signal_types.h(__LP64__) and an@externto libcsigactionthat takes it.sigset_t = c_ulong.std.posix.*, so nothing changes.Replace all nine callsites (
main.zig,crash_handler.zig,process.zig,repl.zig,filter_run.zig,multi_run.zig,Coordinator.zig,Global.zig).A comptime tripwire fires if
@sizeOf(std.c.Sigaction)ever shrinks to the bionic 32 bytes, so the workaround can be dropped once the Zig stdlib bug is fixed upstream.Also adds
zig build check-android[-debug], gated on-Dandroid_ndk_sysrootlikecheck-freebsd, so the Android-only struct gets compile-checked.Verification
bun run zig:check-allstill passes all 16 targets. Host smoke tests (spawn/exit-code,spawn/spawn-signal,spawn/spawn-kill-signal, plusBUN_FEATURE_FLAG_FORCE_WAITER_THREAD=1to hit the modified SIGCHLD path) pass.No runtime test is included because the misbehaviour only surfaces on a real Android kernel, which CI does not run.