docs/signal-handler-chaining.md
FastLED uses signal handler chaining to provide both:
This is achieved through a pass-through handler design where the internal handler dumps first, then uninstalls itself and re-raises the signal for external processing.
Test crashes → Signal raised (SIGSEGV, SIGABRT, etc.)
↓
Internal handler catches signal
↓
Print internal stack trace
↓
Uninstall handler (signal(sig, SIG_DFL))
↓
Re-raise signal (raise(sig))
↓
Signal goes to default handler or external debugger
↓
Process terminates
Test hangs → External watchdog detects (no output for 10s)
↓
Attach lldb/gdb to process
↓
Debugger dumps all thread stacks
↓
Kill process
Test crashes → Internal handler dumps stack
↓
Handler uninstalls itself
↓
Handler re-raises signal
↓
External debugger (if attached) catches signal
↓
Debugger dumps additional context
↓
Process terminates
inline LONG WINAPI windows_exception_handler(EXCEPTION_POINTERS* ExceptionInfo) {
// Prevent recursion
static volatile LONG already_dumping = 0;
if (InterlockedExchange(&already_dumping, 1) != 0) {
return EXCEPTION_CONTINUE_SEARCH; // Recursive - bail out
}
printf("\n=== INTERNAL EXCEPTION HANDLER ===\n");
print_stacktrace_windows(); // Internal dump
printf("=== END INTERNAL HANDLER ===\n\n");
fflush(stdout);
// CHAINING: Remove our handler
SetUnhandledExceptionFilter(NULL);
// Return EXCEPTION_CONTINUE_SEARCH to pass to next handler
return EXCEPTION_CONTINUE_SEARCH;
}
Key Points:
InterlockedExchange for thread-safe recursion preventionEXCEPTION_CONTINUE_SEARCH to pass exception to next handlerSetUnhandledExceptionFilter(NULL)inline void crash_handler(int sig) {
// Prevent recursion
static volatile sig_atomic_t already_dumping = 0;
if (already_dumping) {
signal(sig, SIG_DFL);
raise(sig);
return;
}
already_dumping = 1;
fprintf(stderr, "\n=== INTERNAL CRASH HANDLER ===\n");
print_stacktrace(); // Internal dump
fprintf(stderr, "=== END INTERNAL HANDLER ===\n\n");
fflush(stderr);
// CHAINING: Restore default handler
signal(sig, SIG_DFL);
// Re-raise signal for external debugger
raise(sig);
// Fallback exit if raise doesn't terminate
exit(1);
}
Key Points:
sig_atomic_t for signal-safe recursion checksignal(sig, SIG_DFL)raise(sig) for external handlersAll handlers include recursion guards to prevent infinite loops if the handler itself crashes:
static volatile sig_atomic_t already_dumping = 0;
if (already_dumping) {
signal(sig, SIG_DFL);
raise(sig);
return;
}
already_dumping = 1;
This ensures:
| Approach | Internal Dumps | External Dumps | Config Required | Test Changes |
|---|---|---|---|---|
| Signal Chaining ⭐ | ✅ Yes | ✅ Yes | ❌ No | ❌ No |
| Disable Handler | ❌ No | ✅ Yes | ✅ Yes (env var) | ❌ No |
| Time-Based Uninstall | ⚠️ <3s only | ✅ Yes | ✅ Yes (timeout) | ❌ No |
| Watchdog Thread | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes (heartbeat) |
=== INTERNAL CRASH HANDLER (SIGNAL 11) ===
Stack trace (libunwind):
#0 0x00007f8b9c123456 in MyClass::crash_me() at test.cpp:42
#1 0x00007f8b9c123789 in test_case_1() at test.cpp:67
#2 0x00007f8b9c124000 in main at main.cpp:12
=== END INTERNAL HANDLER ===
Uninstalling crash handler and re-raising signal 11 for external debugger...
Segmentation fault (core dumped)
Running test: my_test (timeout: 10.0s)
[doctest] TEST CASE: test_infinite_loop
================================================================================
TEST HUNG: my_test
Exceeded timeout of 10.0s
================================================================================
📍 Attaching lldb to hung process (PID 12345)...
Note: Crash handlers use signal chaining (internal dump → external debugger)
THREAD STACK TRACES:
--------------------------------------------------------------------------------
* thread #1, name = 'my_test', stop reason = signal SIGSTOP
* frame #0: 0x00007ff6b9a41234 test.dll`infinite_loop(...) at test.cpp:12
frame #1: 0x00007ff6b9a41456 test.dll`test_case() at test.cpp:23
--------------------------------------------------------------------------------
🔪 Killed hung process (PID 12345)
If you want ONLY external dumps (no internal dumps), set:
export FASTLED_DISABLE_CRASH_HANDLER=1
This completely disables internal handlers, allowing pure external debugger control.
SetUnhandledExceptionFilterEXCEPTION_CONTINUE_SEARCH to chainsignal() / sigaction()raise(sig) to chainAll platforms handle:
SIGABRT - Abort (assertion failures)SIGFPE - Floating point exceptionsSIGILL - Illegal instructionsSIGINT - Interrupts (Ctrl+C)SIGSEGV - Segmentation faultsSIGTERM - Termination requestsFL_TEST_CASE("test_crash") {
int* ptr = nullptr;
*ptr = 42; // SIGSEGV - should see internal dump then terminate
}
Expected:
=== INTERNAL CRASH HANDLER (SIGNAL 11) ===
Stack trace: ...
=== END INTERNAL HANDLER ===
Uninstalling crash handler and re-raising signal 11...
Segmentation fault
FL_TEST_CASE("test_hang") {
volatile bool keep_running = true;
while (keep_running) {
// Infinite loop - should trigger external lldb after 10s
}
}
Expected:
TEST HUNG: test_hang
Exceeded timeout of 10.0s
📍 Attaching lldb to hung process (PID ...)
THREAD STACK TRACES: ...
🔪 Killed hung process
Signal handler chaining provides the ideal solution for test diagnostics:
This gives agents complete visibility into both crashes and hangs without requiring any test modifications or environment configuration!