Skip to content

Make malloc return NULL when SAMD boards run out of RAM - #399

Merged
mikeysklar merged 5 commits into
adafruit:masterfrom
mikeysklar:checked-sbrk
Oct 17, 2026
Merged

mikeysklar merged 5 commits into
adafruit:masterfrom
mikeysklar:checked-sbrk

Conversation

@mikeysklar

Copy link
Copy Markdown

What

malloc now returns NULL when RAM runs out.

Why

Today it hands out stack memory, so code that checks for NULL hangs.

Source @matthijskooijman's two commits from arduino#681, unchanged
cores/arduino/sbrk.c New _sbrk that fails 64 bytes below the stack pointer at call time
cores/arduino/main.cpp Makes the linker use it instead of the newlib stub
Added here License header, and sbrk(0) never fails
Cost +36 to +44 bytes flash, up to +12 bytes RAM

3 runs per row, same output every run, arm-none-eabi-gcc 9-2019q4.

Board Core 1 KB mallocs malloc(768000) GFX arduino#518 800x480 canvas
Feather M0 master 28, last overlaps stack 0x20008150, past end of RAM no output for 10 s
Feather M0 this PR 27, then NULL NULL 0x0, sketch finishes
Feather M4 master 188, last overlaps stack no output for 6 s no output for 10 s
Feather M4 this PR 186, then NULL NULL 0x0, sketch finishes

Not tested: other boards, or sketches using FreeRTOS.

Tested with help from Claude Code; I checked the hardware runs myself.

🤖 Generated with Claude Code

matthijskooijman and others added 5 commits October 15, 2026 19:04
When allocating memory with malloc, this calls _sbrk() to expand the
heap. The current implementation always succeeds, which makes malloc
always succeed, potentially returning memory that overlaps the stack, or
is even outside of the available RAM.

This commit fixes this by replacing the _sbrk() function by one which
fails rather than allocating memory too close to the stack (with
configurable margin).

= Background & history =
The malloc implementation used by this core is provided by newlib, which
is shipped along gcc in the toolchain package and is a libc
implementation intended for embedded systems. It implements the entire
user-facing API, but relies on a number of "syscalls", to be provided by
the underlying system to do the real work.

See https://sourceware.org/newlib/libc.html#Syscalls

These syscalls mostly concern handling of processes, and files, which
are not applicable here, so dummy implementations that simply returned
failure were originally provided by syscalls.c in this core.

In addition, the _sbrk() syscall *is* relevant, since it is used to
expand the heap when malloc runs out of memory to (re)use. Originally,
the samd core provided a very simple version that always succeeded,
which was later changed to fail when trying to allocate beyond the end
of memory (but would still happily overwrite the stack).

In commit 93c6355 (Resolving syscalls/_sbrk issue), the syscalls were
removed, and (among a lot of other flag changes) `--specs=nosys.specs`
was passed to the linker. Unlike what you might think, this "nosys" does
not instruct the linker to omit any system library, but it tells the
linker that no syscalls will be provided and makes it include the
libgloss "nosys" library.

This "nosys" library also provides dummy syscall implementations that
always fail, except for _sbrk(), which always succeeds (like the
original samd implementation).

So this left the samd core with a malloc that *always* succeeds without
regard for the stack or the end of RAM.

= Solution =
This is fixed by adding a _sbrk() implmentation that does check the
current stack location and fails allocation if that would overwrite the
stack.

This implementation of _sbrk() is based on the STM32 core, except with
some different variable and type names and with the minimum stack size
check omitted (since the linker scripts do not define such a minimum
size):

https://github.com/stm32duino/Arduino_Core_STM32/blob/616258269f7e5a2ef8b2f71bb2a55616b25d07db/libraries/SrcWrapper/src/syscalls.c#L29-L51

Because libgloss nosys defines its dummy _sbrk() as a weak symbol, it
can be overridden by a non-weak symbol in the samd core. However,
because the core is linked through an archive file, it will only be
included in the link if there is an unresolved reference to it (or
anything else in the same file, which is nothing) at the time the core
is linked. Normally, this undefined reference to _sbrk() is only
introduced when linking newlib at the end of the link, which is then
satisfied by libgloss (which is linked after newlib) rather than the
samd core.

To ensure that the samd core version is included instead, an artificial
unresolved reference is created in main.c (which *is* always included
because it defines main). This ensures that sbrk.c is pulled into the
link instead of the libgloss version.
The _sbrk() implementation added previously refuses to overwrite the
stack, but will still happily allocate all other memory, leaving none to
expand the stack into. Since the stack can expand over the heap
undetected, this changes sbrk() to leave a safety margin between the
heap and the stack (at the time of the malloc/_sbrk call). The size of
this margin can be configured (by the sketch) by changing the
`__malloc_margin` global variable, it defaults to 64 bytes.

This approach (both having a margin and making it configurable with the
`__malloc_margin`) is copied from the avr-libc malloc implementation,
see:

https://onlinedocs.microchip.com/pr/GUID-317042D4-BCCE-4065-BB05-AC4312DBC2C4-en-US-2/index.html?GUID-27757112-8BE1-49C2-B023-CD94AD06C5E2

avr-libc uses a default margin of 32 bytes, but given the bigger integer
and pointer size on ARM, this is doubled for this core.
Use the LGPL 2.1 block the other core files carry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
sbrk(0) only reads the current break, so it should never fail. Before
this, it returned -1 once the heap was within __malloc_margin of the
stack, which breaks free-memory helpers that subtract sbrk(0) from a
stack address.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
samd.h only covers SAMD parts. On SAME51 boards like the Feather M4 CAN
it pulls in no device header, so __get_MSP is never declared and the
link fails. sam.h picks the right header for every family, the same as
cortex_handlers.c. The generated code for SAMD21 and SAMD51 is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikeysklar

Copy link
Copy Markdown
Author

Fixed the feather_m4_can build failure.

sbrk.c included samd.h, which has no SAME51 parts, so __get_MSP was undeclared.

It now includes sam.h, like cortex_handlers.c does.

The compiled _sbrk is unchanged on SAMD21 and SAMD51.

@mikeysklar

Copy link
Copy Markdown
Author

@ladyada this is ready for your review.

Without it, malloc never returns NULL on SAMD boards.

That means adafruit/Adafruit-GFX-Library#518 cannot catch a failed canvas allocation on M0 or M4.

@mikeysklar
mikeysklar merged commit 65b6206 into adafruit:master Oct 17, 2026
12 checks passed
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.

4 participants