Skip to content

Heap-exhaustion diagnostics: /api/status instruments and resilient abort hook - #6

Merged
xymeow merged 5 commits into
mainfrom
codex/readme-docs-wiki
Jul 15, 2026
Merged

Heap-exhaustion diagnostics: /api/status instruments and resilient abort hook#6
xymeow merged 5 commits into
mainfrom
codex/readme-docs-wiki

Conversation

@xymeow

@xymeow xymeow commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Follow-up to #5, diagnosing last night's reboot recurrence.

Root cause (captured on serial): XS chunk-heap exhaustion after ~3.5 hours of match-day load —

04:24:43 # Chunk allocation: 40 bytes failed in fixed size heap
04:24:43 # Chunk allocation: 20 bytes failed ×15
04:24:45 XS abort: native stack overflow
04:24:45 rst:0xc (RTC_SW_CPU_RST)

lastAbort stayed empty because persisting the reason string itself needs heap. The 432KB XS arena was already 57% occupied at boot; the leak-or-watermark curve is being logged overnight to distinguish which.

This PR (mod side, degrades gracefully on stock hosts)

  • /api/status reports all instrumentation metrics (XS Chunk Heap Used, XS Slot Heap Used, sockets, timers, promises) under instruments when the host bundles the instrumentation module.
  • The abort hook falls back to persisting the bare status string when the full reason cannot be allocated.
  • Bilingual release-note addition in docs/releases/1.6.0.md.

Companion host change: xymeow/stack-chan matchday-host 812d093 grows the XS arena to 1.25MB (PSRAM-backed via CONFIG_SPIRAM_USE_MALLOC) and bundles the instrumentation module. The instrumented build is already flashed and under observation.

🤖 Generated with Claude Code

xymeow and others added 5 commits July 15, 2026 04:48
The 2026-07-15 reboot recurrence was XS chunk-heap exhaustion after ~3.5
hours of match-day load (serial: "Chunk allocation ... failed" storm, then
"XS abort: native stack overflow"). The abort hook could not persist the
reason because building the string itself needs heap — lastAbort stayed
empty. Now:

- /api/status reports all instrumentation metrics (XS chunk/slot heap used,
  sockets, timers, promises) when the host bundles the instrumentation
  module; degrades to omitting the field elsewhere
- the abort hook falls back to persisting the bare status string when the
  full reason cannot be allocated
- companion host change (xymeow/stack-chan matchday-host 812d093) grows the
  XS arena from 432KB (57% full at boot) to 1.25MB in PSRAM

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Under 1.6.0's concurrent HTTP handling, connection teardowns overlap
inbound data far more than the old serialized handler did, reopening a
window the arg-first NULL guard cannot cover: a tcpReceive already past
the guard and mid-tail-walk when xs_tcp_destructor frees tcp->buffers/tcp.
Observed as a LoadProhibited in tcpReceive (tcp.c:600) at ~25 min under
light polling. The destructor now clears callbacks via the marshaled
tcp_clear_callbacks_safe, which cannot run until an in-flight tcpReceive
on the tcpip thread has returned.

This revises the upstream note that the simpler arg-first variant was
sufficient (it was, pre-concurrency). Also documents a separate, unfixed
listener-accept race (pending socket RST before consumption) surfaced only
by pathological connection bursts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ve race

Deploying the marshaled destructor clear did not stop the crash: the device
crashed again at 27 min under real load with a healthy heap, same tcpReceive
backtrace. The fault address is a poison value (EXCVADDR=0xe2f61b44) with tcp
itself valid, so the UAF is a receive-buffer node freed while still linked,
not the tcp record my v5 change targeted. This is an unresolved SDK race
across tcpReceive / xs_tcp_read / destructor on two cores; escalating to the
maintainer with the poison-UAF evidence rather than guessing further.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Observed live 2026-07-15: a remote-TTS stream settled neither onDone nor
onError, leaving speakRemote's state.tts.busy stuck true, which made every
later utterance return "tts busy" — commentary went silent until a reboot,
with no crash.

RemoteTTS.stream now arms a progress-reset watchdog (15s): armed before the
first byte (so a dead TTS host is caught), reset on every onReady/onPlayed,
and it tears the stream down and rejects if progress stops. All completion
paths route through a single settle() that clears streaming and closes the
streamer/audio first, so busy is released even if close throws. A healthy
long utterance keeps resetting the timer and is never cut off.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tatic arena

The earlier note claimed a 1.25MB arena fixed the reboots; that edit was on
the wrong platform block (m5stack_core2) and did nothing. The real cause was
the CoreS3 chunk heap's incremental:0 hard-capping it at 256KB; the fix is
chunk { initial: 393216, incremental: 32768 } (stack-chan matchday-host
14d4d15), verified to grow past the old cap under load with zero aborts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@xymeow
xymeow merged commit 9b2a113 into main Jul 15, 2026
1 check passed
@xymeow
xymeow deleted the codex/readme-docs-wiki branch July 15, 2026 22:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant