-
Notifications
You must be signed in to change notification settings - Fork 5k
fuzzilli: chain crash signal handlers to JSC/ASAN instead of SIG_DFL #30092
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e71477b
3cbb692
9394842
696a1f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
| #include <cstdlib> | ||
| #include <cstring> | ||
| #include <fcntl.h> | ||
| #include <mutex> | ||
| #include <sanitizer/asan_interface.h> | ||
| #include <sys/mman.h> | ||
| #include <sys/stat.h> | ||
|
|
@@ -19,20 +20,49 @@ | |
|
|
||
| extern "C" { | ||
|
|
||
| // Signal handler to ensure output is flushed before crash | ||
| static void fuzzilliSignalHandler(int sig) | ||
| // Previous handlers (JSC's jscSignalHandler, which itself chains to ASAN's | ||
| // handler). We flush stdio so Fuzzilli captures buffered output, then forward | ||
| // to the prior handler so JSC's VMTraps/WASM fault handling keeps working and | ||
| // ASAN can print its report. Using signal()+SIG_DFL here used to swallow both, | ||
| // leaving crash reports with just "TERMSIG: 11" and no stack. | ||
| static struct sigaction fuzzilliOldActions[NSIG]; | ||
|
|
||
| static void fuzzilliSignalHandler(int sig, siginfo_t* info, void* ucontext) | ||
| { | ||
| // Flush all output | ||
| fflush(stdout); | ||
| fflush(stderr); | ||
| fsync(STDOUT_FILENO); | ||
| fsync(STDERR_FILENO); | ||
|
|
||
| // Re-raise the signal with default handler | ||
| struct sigaction& old = fuzzilliOldActions[sig]; | ||
| if (old.sa_flags & SA_SIGINFO) { | ||
| if (old.sa_sigaction) { | ||
| old.sa_sigaction(sig, info, ucontext); | ||
| return; | ||
| } | ||
| } else if (old.sa_handler && old.sa_handler != SIG_DFL && old.sa_handler != SIG_IGN) { | ||
| old.sa_handler(sig); | ||
| return; | ||
| } | ||
|
|
||
| // No previous handler: re-raise with default handler. | ||
| signal(sig, SIG_DFL); | ||
| raise(sig); | ||
| } | ||
|
|
||
| static void installFuzzilliSignalHandler(int sig) | ||
| { | ||
| struct sigaction action; | ||
| action.sa_sigaction = fuzzilliSignalHandler; | ||
| sigfillset(&action.sa_mask); | ||
| // SA_ONSTACK so stack-overflow faults are delivered on the sigaltstack | ||
| // ASAN/JSC already set up; otherwise the kernel can't push the frame and | ||
| // the process dies with a bare SIGSEGV before we reach the chain. | ||
| action.sa_flags = SA_SIGINFO | SA_ONSTACK; | ||
| sigaction(sig, &action, &fuzzilliOldActions[sig]); | ||
| } | ||
|
Comment on lines
+54
to
+64
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, verify the file exists and read the relevant section
head -70 src/bun.js/bindings/FuzzilliREPRL.cpp | tail -25Repository: oven-sh/bun Length of output: 1030 🏁 Script executed: # Search for sigaction usage in the codebase to see error handling patterns
rg "sigaction" --type cpp -B2 -A5Repository: oven-sh/bun Length of output: 10135 🏁 Script executed: # Check for similar error handling patterns with system calls in C++ files
rg "if.*!=.*0\)" src/bun.js/bindings --type cpp -B2 -A2 | head -40Repository: oven-sh/bun Length of output: 2960 🏁 Script executed: # Look for struct initialization patterns in the file
rg "struct.*=" src/bun.js/bindings/FuzzilliREPRL.cpp --type cppRepository: oven-sh/bun Length of output: 153 Initialize The function leaves Proposed fix static void installFuzzilliSignalHandler(int sig)
{
- struct sigaction action;
+ struct sigaction action = {};
action.sa_sigaction = fuzzilliSignalHandler;
sigfillset(&action.sa_mask);
// SA_ONSTACK so stack-overflow faults are delivered on the sigaltstack
// ASAN/JSC already set up; otherwise the kernel can't push the frame and
// the process dies with a bare SIGSEGV before we reach the chain.
action.sa_flags = SA_SIGINFO | SA_ONSTACK;
- sigaction(sig, &action, &fuzzilliOldActions[sig]);
+ if (sigaction(sig, &action, &fuzzilliOldActions[sig]) != 0) {
+ fprintf(stderr, "[fuzzilli] sigaction(%d) failed: %s\n", sig, strerror(errno));
+ std::abort();
+ }
}🤖 Prompt for AI Agents |
||
|
|
||
| // Implementation of the global fuzzilli() function for Bun | ||
| // This function is used by Fuzzilli to: | ||
| // 1. Test crash detection with fuzzilli('FUZZILLI_CRASH', type) | ||
|
|
@@ -259,12 +289,20 @@ void Bun__REPRL__registerFuzzilliFunctions(Zig::GlobalObject* globalObject) | |
| { | ||
| JSC::VM& vm = globalObject->vm(); | ||
|
|
||
| // Install signal handlers to ensure output is flushed before crashes | ||
| // This is important for ASAN output to be captured | ||
| signal(SIGABRT, fuzzilliSignalHandler); | ||
| signal(SIGSEGV, fuzzilliSignalHandler); | ||
| signal(SIGILL, fuzzilliSignalHandler); | ||
| signal(SIGFPE, fuzzilliSignalHandler); | ||
| // Install signal handlers to ensure output is flushed before crashes. | ||
| // Chain to the previous handler (JSC's jscSignalHandler → ASAN) so | ||
| // VMTraps/WASM fault handling keeps working and ASAN reports are printed. | ||
| // Signal dispositions are process-wide; this function runs once per | ||
| // GlobalObject (main thread, macros, and every Worker), so guard with a | ||
| // once-flag — re-installing would save ourselves into fuzzilliOldActions | ||
| // and recurse on the next signal. | ||
| static std::once_flag installOnce; | ||
| std::call_once(installOnce, [] { | ||
| installFuzzilliSignalHandler(SIGABRT); | ||
| installFuzzilliSignalHandler(SIGSEGV); | ||
| installFuzzilliSignalHandler(SIGILL); | ||
| installFuzzilliSignalHandler(SIGFPE); | ||
| }); | ||
|
|
||
| globalObject->putDirectNativeFunction( | ||
| vm, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,90 @@ | ||||||||||||||||||||||
| // The fuzzilli REPRL setup installs SIGSEGV/SIGILL/SIGFPE/SIGABRT handlers so | ||||||||||||||||||||||
| // buffered stdio is flushed before a crash. Those handlers must chain to the | ||||||||||||||||||||||
| // previously-installed handler (WTF::jscSignalHandler → ASAN) instead of | ||||||||||||||||||||||
| // re-raising with SIG_DFL, otherwise: | ||||||||||||||||||||||
| // • ASAN never prints a report for null derefs / wild accesses, so every | ||||||||||||||||||||||
| // signal-based fuzzer crash shows up as a bare "TERMSIG: 11". | ||||||||||||||||||||||
| // • JSC's signal-based VMTraps and WASM fault handling are broken, turning | ||||||||||||||||||||||
| // intentional JIT trap breakpoints into hard process crashes. | ||||||||||||||||||||||
| // | ||||||||||||||||||||||
| // Only runs when the binary under test was built with FUZZILLI_ENABLED (the | ||||||||||||||||||||||
| // `fuzzilli()` global exists). Normal debug/release builds skip. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import { expect, test } from "bun:test"; | ||||||||||||||||||||||
| import { bunEnv, bunExe } from "harness"; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| declare const fuzzilli: unknown; | ||||||||||||||||||||||
| const isFuzzilliBuild = typeof fuzzilli === "function"; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Skip symbolization so ASAN writes its report and exits immediately instead | ||||||||||||||||||||||
| // of shelling out to llvm-symbolizer for every frame (several seconds on the | ||||||||||||||||||||||
| // fuzz binary). The presence of "AddressSanitizer: SEGV" is enough to prove | ||||||||||||||||||||||
| // the handler chain reached ASAN. allow_user_segv_handler keeps JSC from | ||||||||||||||||||||||
| // disabling its own fault handling when it sees ASAN_OPTIONS is set. | ||||||||||||||||||||||
| const fastCrashEnv = { | ||||||||||||||||||||||
| ...bunEnv, | ||||||||||||||||||||||
| ASAN_OPTIONS: [bunEnv.ASAN_OPTIONS, "allow_user_segv_handler=1", "symbolize=0"].filter(Boolean).join(":"), | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| test.skipIf(!isFuzzilliBuild)("fuzzilli crash signal handler chains to JSC/ASAN for SIGSEGV", async () => { | ||||||||||||||||||||||
| // FUZZILLI_CRASH type 5 writes to a volatile null pointer. With a working | ||||||||||||||||||||||
| // handler chain the fault reaches jscSignalHandler → ASAN and we get an | ||||||||||||||||||||||
| // "AddressSanitizer: SEGV" report on stderr; with signal()+SIG_DFL the | ||||||||||||||||||||||
| // process dies with stderr containing only the [COV] banner. | ||||||||||||||||||||||
| await using proc = Bun.spawn({ | ||||||||||||||||||||||
| cmd: [bunExe(), "-e", 'fuzzilli("FUZZILLI_CRASH", 5);'], | ||||||||||||||||||||||
| env: fastCrashEnv, | ||||||||||||||||||||||
| stdout: "pipe", | ||||||||||||||||||||||
| stderr: "pipe", | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const [stdout, stderr] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| expect(stdout).toContain("FUZZILLI_CRASH: 5"); | ||||||||||||||||||||||
| expect(stderr).toContain("AddressSanitizer: SEGV"); | ||||||||||||||||||||||
| // ASAN aborts after printing; a bare SIGSEGV would surface as signalCode | ||||||||||||||||||||||
| // "SIGSEGV" with nothing useful on stderr. | ||||||||||||||||||||||
| expect(proc.signalCode).not.toBe("SIGSEGV"); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| test.skipIf(!isFuzzilliBuild)("fuzzilli crash signal handler survives Worker global creation", async () => { | ||||||||||||||||||||||
| // Bun__REPRL__registerFuzzilliFunctions runs for every GlobalObject (main, | ||||||||||||||||||||||
| // macros, Workers). Without a once-guard the Worker's call re-installs the | ||||||||||||||||||||||
| // handler, saving fuzzilliSignalHandler itself into fuzzilliOldActions and | ||||||||||||||||||||||
| // turning the next SIGSEGV into infinite self-recursion → bare TERMSIG 11. | ||||||||||||||||||||||
| await using proc = Bun.spawn({ | ||||||||||||||||||||||
| cmd: [ | ||||||||||||||||||||||
| bunExe(), | ||||||||||||||||||||||
| "-e", | ||||||||||||||||||||||
| 'const w = new Worker("data:text/javascript,postMessage(0)"); await new Promise(r => (w.onmessage = r)); w.terminate(); fuzzilli("FUZZILLI_CRASH", 5);', | ||||||||||||||||||||||
| ], | ||||||||||||||||||||||
| env: fastCrashEnv, | ||||||||||||||||||||||
| stdout: "pipe", | ||||||||||||||||||||||
| stderr: "pipe", | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const [stdout, stderr] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| expect(stdout).toContain("FUZZILLI_CRASH: 5"); | ||||||||||||||||||||||
| expect(stderr).toContain("AddressSanitizer: SEGV"); | ||||||||||||||||||||||
| expect(proc.signalCode).not.toBe("SIGSEGV"); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| test.skipIf(!isFuzzilliBuild)("fuzzilli crash signal handler still terminates for SIGABRT", async () => { | ||||||||||||||||||||||
| // FUZZILLI_CRASH type 0 is std::abort(). No JSC/ASAN handler is registered | ||||||||||||||||||||||
| // for SIGABRT by default, so the chain falls through to SIG_DFL and the | ||||||||||||||||||||||
| // process terminates with SIGABRT — Fuzzilli's crash detection relies on | ||||||||||||||||||||||
| // this. | ||||||||||||||||||||||
| await using proc = Bun.spawn({ | ||||||||||||||||||||||
| cmd: [bunExe(), "-e", 'fuzzilli("FUZZILLI_CRASH", 0);'], | ||||||||||||||||||||||
| env: fastCrashEnv, | ||||||||||||||||||||||
| stdout: "pipe", | ||||||||||||||||||||||
| stderr: "pipe", | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| expect(stdout).toContain("FUZZILLI_CRASH: 0"); | ||||||||||||||||||||||
| expect(exitCode).not.toBe(0); | ||||||||||||||||||||||
| expect(proc.signalCode).toBe("SIGABRT"); | ||||||||||||||||||||||
|
Comment on lines
+85
to
+89
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make the exit-code assertion last in this subprocess test. Please move Proposed fix expect(stdout).toContain("FUZZILLI_CRASH: 0");
- expect(exitCode).not.toBe(0);
expect(proc.signalCode).toBe("SIGABRT");
+ expect(exitCode).not.toBe(0);
});Based on learnings: in 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| }); | ||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
Isfflushasync-signal-safe according to POSIX, and what is the recommended way to ensure crash output is visible from a signal handler?💡 Result:
No, fflush is not async-signal-safe according to POSIX. The official POSIX list of async-signal-safe functions does not include fflush or any stdio functions like printf. Instead, use low-level write to STDERR_FILENO (fd 2) or STDOUT_FILENO (fd 1) for output in signal handlers, followed by fsync if needed to ensure visibility, then _exit. This avoids stdio buffering issues and ensures the output is flushed to the kernel without relying on process termination cleanup, which may not occur reliably on crashes like SIGSEGV.
Citations:
Replace
fflush()calls with async-signal-safewrite()to avoid deadlock in signal handler.The
fflush(stdout)andfflush(stderr)calls at lines 31–35 are not async-signal-safe. If the signal interrupts code that holds the stdio lock, the handler will deadlock before the chained JSC/ASAN handler runs, defeating the purpose of this patch. Usewrite(STDOUT_FILENO, ...)andwrite(STDERR_FILENO, ...)instead; thefsync()calls that follow are async-signal-safe and can remain.Current code
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
fflush()calls are pre-existing (present before this PR) and intentional — the whole point of this handler is to drain stdio's userspace buffers so Fuzzilli captures buffered output before the process dies.write(STDOUT_FILENO, …)can only push bytes we already hold; it cannot flushFILE*buffers, so swapping them in would make the handler a no-op for its stated purpose.The async-signal-safety risk (deadlock if we crash while holding stdio's lock) is accepted here: the process is terminating regardless, and the next handler in the chain (
WTF::jscSignalHandler, ASAN's reporter) is already non-signal-safe — they allocate, walk stacks, and write through stdio themselves. This is the standard crash-reporting trade-off.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@robobun, that's a fair and well-reasoned explanation. You're right thatwrite()cannot drainFILE*userspace buffers, so substituting it would silently drop exactly the buffered output Fuzzilli needs to see. And the signal-safety concern is moot in practice here because the process is already on its way out and the downstream handlers (WTF::jscSignalHandler, ASAN) are themselves non-signal-safe by necessity. This is the standard crash-reporter trade-off. Happy to dismiss this comment.(
・ω・)っ🐇✏️ Learnings added
🧠 Learnings used