-
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 1 commit
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 |
|---|---|---|
|
|
@@ -19,20 +19,46 @@ | |
|
|
||
| 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); | ||
| action.sa_flags = SA_SIGINFO; | ||
|
Check warning on line 58 in src/bun.js/bindings/FuzzilliREPRL.cpp
|
||
|
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. 🟡 Consider adding Extended reasoning...What the bug is
For most faults that's fine. But when the SIGSEGV is caused by stack exhaustion (the guard page below the thread stack is hit), there is no room left on the current stack to push the signal frame. The kernel's attempt to set up the handler invocation itself faults, the kernel gives up and applies the default disposition, and the process dies with a bare Why the alt stack is already thereBoth layers this PR is chaining to already account for this:
So the alternate stack already exists at the point Step-by-step proof
Result: Fuzzilli logs a crash fingerprint with no output — indistinguishable from the bug class this PR is fixing. Why this isn't a regressionThe pre-PR code used FixOne token: action.sa_flags = SA_SIGINFO | SA_ONSTACK;(Optionally also Given the PR's stated goal is "if this fingerprint re-fires it will now come with an ASAN stack", and stack overflow is a common fuzzer finding, it seems worth folding in here.
Collaborator
Author
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. Added |
||
| 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 +285,13 @@ | |
| { | ||
| 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. | ||
| installFuzzilliSignalHandler(SIGABRT); | ||
| installFuzzilliSignalHandler(SIGSEGV); | ||
| installFuzzilliSignalHandler(SIGILL); | ||
| installFuzzilliSignalHandler(SIGFPE); | ||
|
Check failure on line 294 in src/bun.js/bindings/FuzzilliREPRL.cpp
|
||
|
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. 🔴 Extended reasoning...What the bug is
Code path that triggers it
JSC's Why nothing prevents itThere is no once-guard around the installation, and Step-by-step proof
ImpactAfter the first Worker is spawned in a REPRL session: (a) the JSC→ASAN chain is permanently lost, so crashes go back to bare FixEither guard the installation block with a static once-flag (signal handlers are process-wide, so installing once is correct), e.g.: static bool installed = false;
if (!std::exchange(installed, true)) {
installFuzzilliSignalHandler(SIGABRT);
installFuzzilliSignalHandler(SIGSEGV);
installFuzzilliSignalHandler(SIGILL);
installFuzzilliSignalHandler(SIGFPE);
}or, inside
Collaborator
Author
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. Good catch — confirmed: before the fix, |
||
|
|
||
| globalObject->putDirectNativeFunction( | ||
| vm, | ||
|
|
||
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