Skip to content

zephyr-cp: don't build the heap control structure in an undersized region - #11230

Open
mikeysklar wants to merge 1 commit into
adafruit:mainfrom
mikeysklar:zephyr-heap/tlsf-largest-region
Open

zephyr-cp: don't build the heap control structure in an undersized region#11230
mikeysklar wants to merge 1 commit into
adafruit:mainfrom
mikeysklar:zephyr-heap/tlsf-largest-region

Conversation

@mikeysklar

@mikeysklar mikeysklar commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

What

tlsf_create_with_pool() can write TLSF's control structure past the end of the
region it is given. This makes port_heap_init() skip a region that is too
small to host it instead of handing it over.

Why

tlsf_t tlsf_create_with_pool(void* mem, size_t pool_bytes, size_t max_bytes)
{
    tlsf_t tlsf = tlsf_create(mem, max_bytes ? max_bytes : pool_bytes);
    ...

tlsf_create() passes that argument to control_construct(), which dimensions
the control structure from it and performs both of its size checks against it.
So the checks test the maximum heap size, not the space actually available in
mem. When port_heap_init() calls this with circuitpy_max_ram_size and a
small region, the structure is sized for the maximum and written into the small
region.

On a SiWx917-DK2605A the port skips the large SRAM region because Zephyr's
malloc arena owns it (CONFIG_COMMON_LIBC_MALLOC with ARENA_SIZE=-1), so the
first candidate is a 1 KB DMA buffer:

region 0  0x000310ca..0x00050000  126774 B  sram0        skipped, Zephyr malloc
region 1  0x24061c00..0x24062000    1024 B  DMA buffer   <- first candidate
region 2  0x00000000..0x00000400    1024 B  NWP-reserved
region 3  0x0a000000..0x0a800000  8388608 B PSRAM

With an 8 MB maximum the control structure is 2412 bytes, so it overran that
1 KB region by 1388 bytes. Measured on the board: heap = 0x24061c00 and
tlsf_get_pool(heap) = 0x2406256c, a difference of 0x96c = 2412.

Scope of the change

The bound applies only to the region that hosts the control structure:

  • Region order is unchanged.
  • The existing size < 1024 guard is unchanged, so smaller regions are still
    eligible as additional pools.
  • A board whose first region already hosts the control structure successfully
    sees no behavioural change at all.

The only boards affected are those where the current code would overrun the
region, which it does silently today.

How to reproduce

Flash, attach, and read the heap state. No test program needed:

$ commander flash zephyr.rps --device SiWG917M111MGTBA --serialno <sn>
$ JLinkGDBServer -device SiWG917M111MGTBA -if SWD -speed 1000 -select USB=<sn> -port 2331 &
$ arm-none-eabi-gdb -q zephyr.elf -ex 'target remote localhost:2331' -ex 'monitor halt' \
    -ex 'printf "count=%d heap=0x%08x pools=[0x%x,0x%x,0x%x,0x%x]\n", \
         valid_pool_count, heap, pools[0], pools[1], pools[2], pools[3]'

Before:

count=2 heap=0x24061c00 pools=[0x0,0x2406256c,0x4,0x0]

heap is inside the 1 KB DMA buffer, and pools[2] = 0x4 is the NWP-reserved
region. After:

count=1 heap=0x0a000000 pools=[0x0,0x0,0x0,0xa00096c]

The control structure is in the 8 MB PSRAM region.

Hardware tested

Two SiWx917-DK2605A boards (Zephyr board siwx917_dk2605a, SoC
SiWG917M111MGTBA), one on macOS 15 and one on Ubuntu 24.04, each with its own
J-Link. Built on top of the board definition from #11218, which is not yet
merged.

board unpatched patched
A 3/3 heap=0x24061c00 3/3 heap=0x0a000000
B 1/1 heap=0x24061c00 1/1 heap=0x0a000000

Board B's unpatched image was also caught halted in arch_system_halt with
reason=4 (K_ERR_KERNEL_PANIC), reached from port_heap_init() via
tlsf_add_pool() for the PSRAM region. That panic is not reliably reproducible;
the heap layout above is, on both boards.

Not tested on any other zephyr-cp board. I do not have the vendor blobs to build
the NXP or STM32 targets locally, which is part of why the change is scoped as
narrowly as it is.

Scope

8 KB is a round number comfortably above the 2412-byte structure measured here;
it is not computed from tlsf_size(), which needs an already-constructed
instance. Keeping the NWP-reserved and DMA regions out of the heap entirely is a
separate board-level concern and is not addressed here.

AI assistance

Written with Claude Code. I ran the before/after captures above myself on the
hardware listed.

…gion

tlsf_create_with_pool() forwards its max_bytes argument to tlsf_create(), so
control_construct() dimensions the control structure from the maximum heap size
and checks that maximum against itself, not against the region it is writing
into. A region too small for the structure is overrun rather than rejected.

On a SiWx917-DK2605A the port skips the large SRAM region because Zephyr's
malloc arena owns it (CONFIG_COMMON_LIBC_MALLOC with ARENA_SIZE=-1), leaving a
1 KB DMA buffer as the first candidate. With circuitpy_max_ram_size of 8 MB the
control structure is 2412 bytes, so it overran that region by 1388 bytes.

Require the region that hosts the control structure to be at least 8 KB, and
handle tlsf_create_with_pool() returning NULL by moving to the next region
instead of leaving heap NULL for the first allocation to trip over.

The bound applies only to the region hosting the control structure. Region
order is unchanged and smaller regions are still eligible as additional pools,
so boards whose first region already works are unaffected.
@mikeysklar
mikeysklar force-pushed the zephyr-heap/tlsf-largest-region branch from b3fafd4 to fa10182 Compare August 23, 2026 02:03
@mikeysklar mikeysklar changed the title zephyr-cp: build the Python heap in the largest RAM region zephyr-cp: don't build the heap control structure in an undersized region Aug 23, 2026
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