From 69d663626c3b4fe571ea3480b8491418575e3bf7 Mon Sep 17 00:00:00 2001 From: Yona Appletree Date: Wed, 29 Jul 2026 08:31:28 -0700 Subject: [PATCH] fix(fw-esp32c6): give catch_unwind recovery enough stack (heap 300K -> 250K) `catch_unwind`-based panic/OOM recovery has been non-functional on device: Test 1 of `--features test_oom` raised its panic, then unwinding tripped esp-hal's stack guard and the device never finished booting. Root cause is a stack budget failure, not a toolchain or esp-hal regression. The main stack is whatever DRAM is left after .bss, and .bss holds the esp_alloc heap, so every heap byte costs a stack byte. At 300_000 the stack was 32,392 B total and only 25,976 B were free at the panic site. Unwinding one panic needs ~41 KB, concentrated in two frames: ::from_context 20,736 B with_context::delegate::<..._Unwind_RaiseException::{c#0}> 8,448 B `from_context` builds a gimli `UnwindContext` on the stack per frame; `StoreOnStack` rounds MAX_REG_RULES (65) up to 128 register-rule slots, twice over in `[UnwindTableRow; 2]`, plus the cloned row. So the unwinder ran off the bottom of the stack and wrote `__stack_chk_guard`. Everything downstream followed from that. esp-hal's `ExceptionHandler` is `extern "C"` and therefore nounwind, so panicking inside it and unwinding hits `panic_cannot_unwind`; that panic re-enters esp_println's already-held esp-sync lock, producing an unbounded "lock is not reentrant" cascade. Dropping the heap to 250_000 leaves an 85,784-byte stack. Measured high-water for a boot-depth panic is 47,224 B, so ~38 KB remains for panics raised deeper in node render. Heap is not tight afterwards: 142 KB of 250 KB still free with the studio project loaded. Flash is unaffected -- the heap lives in .bss. Verified on an ESP32-C6 (XIAO, a0:f2:62:87:b4:8c) at this commit: [test_oom] Test 1 OK: simple panic caught [test_oom] Test 2 OK: OOM caught, recovery works [test_oom] Tests complete, continuing boot... Boot: auto-loaded project /projects/studio [RECOVERY] boot complete (first frame served) with recovery level green, safe_mode=false, and steady frame service. Refs docs/reports/2026-03-13-esp32-unwinding-implementation.md. Co-Authored-By: Claude Opus 5 --- lp-fw/fw-esp32c6/src/board/esp32c6/init.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lp-fw/fw-esp32c6/src/board/esp32c6/init.rs b/lp-fw/fw-esp32c6/src/board/esp32c6/init.rs index 99ac369be..42fa75fcd 100644 --- a/lp-fw/fw-esp32c6/src/board/esp32c6/init.rs +++ b/lp-fw/fw-esp32c6/src/board/esp32c6/init.rs @@ -28,7 +28,17 @@ pub fn init_board() -> ( // Allocate heap while leaving enough RAM for the main task stack. Project loading // and on-device shader compilation use deep filesystem/compiler call stacks; too // large a heap reservation shrinks that stack and corrupts execution before OOM. - esp_alloc::heap_allocator!(size: 300_000); + // + // The binding constraint is `catch_unwind` panic recovery, not ordinary call depth. + // The main stack is whatever RAM is left after .bss, so every heap byte is a stack + // byte. Unwinding one panic costs ~41 KB of stack: `Frame::from_context` alone has a + // 20,736-byte frame (a gimli `UnwindContext` with 128 register-rule slots, plus the + // cloned `UnwindTableRow`), and the `_Unwind_RaiseException` loop adds 8,448 more. + // At 300_000 the stack was 32,392 B total — unwinding overflowed it, tripped esp-hal's + // stack guard, and cascaded (see docs/reports/2026-03-13-esp32-unwinding-implementation.md). + // 250_000 leaves an 82,392-byte stack; measured high-water for a boot-depth panic is + // 47,224 B. Do not raise this without re-running `--features test_oom` on hardware. + esp_alloc::heap_allocator!(size: 250_000); // Extract peripherals we need before moving others let rmt = peripherals.RMT;