zephyr-cp: don't build the heap control structure in an undersized region - #11230
Conversation
b3fafd4 to
fa10182
Compare
tannewt
left a comment
There was a problem hiding this comment.
Could we do this in the Python build code instead? It makes the list of ram regions.
fa10182 to
5e840ab
Compare
|
Hardware update, and it raises the stakes on this one: current main does not boot on the SiWx917-DK2605A without this patch. Tested today on two DK2605A boards, one on macOS and one on Ubuntu 24.04, each with its own J-Link. Build main at 8caf4ca, flash, and the core ends up halted: No REPL on the VCOM and nothing on RTT, because the panic happens before either is up. Same result on both boards. Cherry-pick this PR onto the same main and reflash the same board, and it boots: So the board definition in #11218 landed without the region guard it depends on, and CI could not catch it because CI only builds. That is my fault for splitting them, not a problem with the review. Nothing in the patch changed for this; just posting the boot evidence, since "the board panics without it" is a stronger reason to take it than the out-of-bounds write argument alone. |
|
Yes, and I think you are right that it belongs there. Here is why it got through the filter that already exists.
MINIMUM_RAM_SIZE = 1024
...
if size >= MINIMUM_RAM_SIZE:and both problem regions are exactly
So they pass by one byte on a The floor is also just too low. TLSF sizes its control structure from One thing I would keep in the C: I will rework this to filter in |
zephyr2cp.py already filtered RAM regions by size, but MINIMUM_RAM_SIZE was 1024 and the comparison is >=, so a region of exactly 0x400 bytes passed. The SiWx917 has two of them, /memory@0 (reserved for the network processor) and /memory-dma@24061c00, and both reached ram_bounds. That matters because TLSF sizes its control structure from the maximum heap size rather than from the region it is given: tlsf_create_with_pool() forwards max_bytes to tlsf_create(), which performs both of its size checks against the maximum. With an 8 MB maximum the structure is 2412 bytes, so handing it a 1 KB region overruns that region by 1388 bytes and still returns success. On a SiWx917-DK2605A the result is a kernel panic during port_heap_init(). Raise MINIMUM_RAM_SIZE to 8192 so such regions never enter the list. The build script only sees the devicetree size, while the usable size is DT_REG_ADDR + DT_REG_SIZE minus a linker symbol, so it cannot predict a region the linker has filled. port.c keeps a check for that case, now by testing tlsf_create_with_pool() for NULL rather than assuming it succeeded. Verified on two SiWx917-DK2605A boards: before, ram_bounds carried four regions and the board panicked at boot on current main; after, the two 1 KB regions are gone from the generated board.c and both boards boot to the REPL.
5e840ab to
745461c
Compare
|
Moved it into the build code as you suggested. Generated board.c drops from four regions to two. The one check left in port.c is for regions the linker fills, which the build script cannot predict because it only sees the devicetree size. Both boards boot. |
What
tlsf_create_with_pool()can write TLSF's control structure past the end of theregion it is given. This makes
port_heap_init()skip a region that is toosmall to host it instead of handing it over.
Why
tlsf_create()passes that argument tocontrol_construct(), which dimensionsthe 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. Whenport_heap_init()calls this withcircuitpy_max_ram_sizeand asmall 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_MALLOCwithARENA_SIZE=-1), so thefirst candidate is a 1 KB DMA buffer:
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 = 0x24061c00andtlsf_get_pool(heap) = 0x2406256c, a difference of0x96c= 2412.Scope of the change
The bound applies only to the region that hosts the control structure:
size < 1024guard is unchanged, so smaller regions are stilleligible as additional pools.
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:
Before:
heapis inside the 1 KB DMA buffer, andpools[2] = 0x4is the NWP-reservedregion. After:
The control structure is in the 8 MB PSRAM region.
Hardware tested
Two SiWx917-DK2605A boards (Zephyr board
siwx917_dk2605a, SoCSiWG917M111MGTBA), 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, now merged.
heap=0x24061c00heap=0x0a000000heap=0x24061c00heap=0x0a000000Board B's unpatched image was also caught halted in
arch_system_haltwithreason=4(K_ERR_KERNEL_PANIC), reached fromport_heap_init()viatlsf_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-constructedinstance. 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.