Skip to content

EPIC-11: allocation failures are survivable - #12

Merged
diegoparrilla merged 4 commits into
release/v1.1.0from
epic/11-allocation-failures
Sep 16, 2026
Merged

diegoparrilla merged 4 commits into
release/v1.1.0from
epic/11-allocation-failures

Conversation

@diegoparrilla

Copy link
Copy Markdown
Contributor

Fourth epic of v1.1.0. A failed allocation used to call panic("Out of memory") and reboot the RP, which defeated every library written to handle NULL. The worst case: a card without the app folder made the device reboot about every 0.3 s, because creating a folder asks FatFs for 32 KB and the whole heap is 23 KB in a debug build.

What changes

  • malloc returns NULL instead of panicking. PICO_MALLOC_PANIC 0, ported from Booster's booster/src/CMakeLists.txt with its comment, and placed above pico_sdk_init(): the SDK's own malloc.c is compiled from there, so setting it later compiles only our sources with it and does nothing. The shipped v1.0.1beta image contains the Out of memory string; neither build type contains it now.
  • FatFs out-of-memory is reported, not fatal. The HTTP layer had 24 copies of the same generic 500 disk_error; they go through one write_fs_error(), which answers 503 insufficient_memory for FR_NOT_ENOUGH_CORE (retryable) and is unchanged otherwise. GEMDRIVE maps it to GEMDOS ENSMEM (-39) in Dcreate, Ddelete, Fdelete and Frename instead of EINTRN.
  • A failed settings save is visible. The seven settings_save calls in emul.c ignored their result. They go through saveAppSettings(), which stops the countdown and puts "Saving the settings failed: the change was not stored." on the menu's status line, so a lost setting shows on the ST. A failed callback registration in chandler_addCB traces instead of returning silently.
  • The settings flash-parameter checks exist in shipping builds. settings_init guarded its flash size, offset and default-entry count with assert(), and both build types define NDEBUG, so those checks were in no shipping firmware while they guard the erase and program ranges of the config sectors Booster shares. They are runtime checks that trace and return -1; both callers already treat that as "settings not initialised" and jump to Booster.
  • A debug-only heap-hold hook (DEVHOOKS_APP_HEAP_HOLD, EPIC-17's mailbox) holds heap in steps so low-memory behaviour can be tested; swd.py app heap_hold 4, 0 frees it. Release builds contain none of it.

The audit found the rest already correct: the four allocations in settings.c check and return -1, and the only right() caller falls back to the untruncated value.

release is 72 bytes smaller than before.

Verified on hardware

Check Result
Boot with the app folder renamed away, debug and release Folder created, menu up, no reboot loop — the crash loop that opened this epic
Dcreate from the ST, API folder create fr=0 and 201 on the 29.1 GB FAT32 card (32 KB clusters, read over SWD)
Heap squeezed to 3,528 bytes free Listing 200, folder create 201, and the ST wrote an 83 KB file through GEMDRIVE; no crash
Changing a setting at 3,528 bytes free Unable to allocate padding buffer, and the menu showed "Saving the settings failed: the change was not stored."; a later save cleared it
10 minutes of traffic, then a folder create 526 rounds of 64 KB upload, download and listing, 0 errors, heap unchanged, 201
Boot with no SD card at all, release Menu up, crashes 0, no crash loop

An 8 KB hold was refused while 13,808 bytes were free, so the heap is already fragmented at idle: input for EPIC-12.

Found, not fixed here

  • A 256 KB upload sent in one fast burst by curl has its connection closed with no reply; the same happens on 9a2c0dc from before this epic and with a full heap, so it is the fast-upload path (EPIC-13 STORY-02).
  • With no card, volume answers 503 but listings and folder creates answer 500 disk_error, and the menu shows nothing about the missing card (EPIC-15 STORY-02, which also now carries Diego's requirement that the app must refuse to launch without a valid card).

PICO_MALLOC_PANIC defaults to 1, which turns a failed allocation into
panic("Out of memory") and defeats every library written to handle NULL.
FatFs's dir_clear() is the case that bites: creating a folder asks for up
to 32 KB and halves the request until it fits, so on this device, whose
whole heap is 23 KB in debug builds, the first request killed the RP.
Booting with a card that has no app folder was a reboot loop.

The block is ported from Booster's booster/src/CMakeLists.txt, comment
included, and must stay above pico_sdk_init(): the SDK's own malloc.c is
compiled as a subdirectory from there, so setting it later would compile
only our sources with it and do nothing.

The shipped v1.0.1beta image contains the "Out of memory" string; neither
build type contains it now. Release is 72 bytes smaller.
With the malloc panic off, FatFs returns FR_NOT_ENOUGH_CORE and
settings_save returns an error where the device used to die. Both were
invisible.

FatFs failures on the HTTP side went through 24 copies of the same
generic "500 disk_error". They now go through one write_fs_error(),
which reports FR_NOT_ENOUGH_CORE as 503 insufficient_memory (retryable)
and everything else as before. GEMDRIVE maps it to GEMDOS ENSMEM (-39)
in Dcreate, Ddelete, Fdelete and Frename instead of EINTRN.

The seven settings_save calls in emul.c ignored their result. They go
through saveAppSettings(), which stops the countdown and puts "Saving
the settings failed: the change was not stored." on the menu's status
line, so a lost setting is visible on the ST. A failed callback
registration in chandler_addCB now traces instead of returning silently;
it would leave GEMDRIVE or the Runner unreachable.

The audit found the remaining allocation sites already correct: the four
in settings.c check and return -1, and the only right() caller falls
back to the untruncated value.
…-04)

settings_init guarded its flash size, offset and default-entry count with
assert(), and both build types define NDEBUG, so the checks existed in no
shipping firmware. They guard the erase and program ranges of the config
sectors, which Booster shares.

They are runtime checks now: each traces what was wrong and returns -1.
Both callers already treat a negative result as "settings not
initialised" and let main.c jump to Booster, so nothing else changes.
…STORY-05)

DEVHOOKS_APP_HEAP_HOLD holds the given number of KB in the debug
mailbox, in as many steps as the test needs, and frees it all when asked
for 0 KB; swd.py app now takes payload words, so it is
`swd.py app heap_hold 4`. Debug builds only.

With it the firmware was squeezed to 3,528 bytes free: a folder listing
and a folder creation still worked, and changing a setting from the menu
reported "Error: Unable to allocate padding buffer" and put "Saving the
settings failed: the change was not stored." on the ST's menu instead of
panicking. Releasing the heap and saving again cleared it.
@diegoparrilla
diegoparrilla merged commit fdeea8b into release/v1.1.0 Sep 16, 2026
2 checks passed
@diegoparrilla
diegoparrilla deleted the epic/11-allocation-failures branch September 16, 2026 15:07
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