Skip to content

fix: no host crash or heap overflow on garbage or hostile firmware - #71

Merged
begeistert merged 2 commits into
masterfrom
fix/garbage-firmware
Oct 14, 2026
Merged

begeistert merged 2 commits into
masterfrom
fix/garbage-firmware

Conversation

@begeistert

Copy link
Copy Markdown
Collaborator

Problem

Embedders such as iCircuit must survive any firmware: corrupt, random, hostile, or a legitimate program with a stack or DMA bug. Several paths reachable from guest code crashed the host process or wrote past native buffers into the host heap.

Fixes

Bug Effect before Fix
Null fetch pointer after a fetch fault in a region with no fetch window NullReferenceException Poison the cached region id; lockup if the HardFault handler is unfetchable
Halfword/word accesses at the last 1–3 bytes of SRAM, flash or boot ROM host heap write 16-byte tail pad on every store
PUSH/POP/STMIA/LDMIA SRAM fast paths with SP or base near the top of SRAM host heap write SramBurstFits guard, falling back to the per-word path
Bootrom flash erase/program hooks: bounds check in int host heap write up to 2 GB (AccessViolation) Check in long
SSI erase starting past flash: end - start wrapped to ~4 GB AccessViolation Ignore such an erase
DMA CHAIN_TO 12–15, and two channels chained to each other IndexOutOfRange; host stack overflow Ignore out-of-range targets; stop a chain after 12 hops
USB BUFFER_ADDRESS + LENGTH past the 4 KB DPRAM ArgumentOutOfRange Clamp the IN copy and the OUT delivery

The first three bugs mirror fixes in RP2350Sharp (PyMCU/RP2350Sharp#26). The last four were found by the new fuzz tests.

Tests

tests/RP2040Sharp.Tests/Cpu/GarbageFirmwareTests.cs has 30 tests. Every bug has a reproducer that failed before its fix. The fuzz tests cover:

  • the bare core;
  • the full two-core machine;
  • raw MMIO pokes across every peripheral window;
  • every one of the 65536 16-bit opcodes against hostile registers.

The class runs in about 0.25 s, with no wall-clock assertions. Beyond the committed seeds, up to 20000 extra seeds ran clean.

  • Unit suite: 569 passed, stable over 10 consecutive runs plus 5 more.
  • Integration suite: 189 passed, 4 skipped.

Performance

  • CPU-bound MicroPython loop: +0.6 %, within noise.
  • Synthetic loop of only PUSH/POP/LDM/STM: +3 %.

Known, not fixed

DMA runs a transfer to completion synchronously, so a huge TRANS_COUNT stalls the caller. It does not crash or corrupt anything, and it is documented in the CHANGELOG.

Embedders (iCircuit) must survive any firmware. The fixes ported from
RP2350Sharp plus new fuzz tests (bare core, full two-core machine, raw
MMIO, every 16-bit opcode) found:

- Cortex-M0+: after an instruction-fetch fault from a region with no
  fetch window, the cached region id kept a null pointer; a handler in
  the same region read through null. The id is now poisoned (lockup if
  the HardFault handler is unfetchable).
- Memory: halfword/word accesses at the last 1-3 bytes of SRAM, flash
  or boot ROM spilled past the native buffer; every store now has a
  16-byte tail pad.
- PUSH/POP/STMIA/LDMIA SRAM fast paths masked only the start address
  and could write past SRAM; bursts that do not fit take the per-word
  path.
- Bootrom flash erase/program hooks bounds-checked offset + count in
  int, which wrapped: a host heap write of up to 2 GB. Now in long.
- SSI erase starting past the end of flash wrapped end - start to ~4 GB
  for InitBlock. Such an erase is now ignored.
- DMA CHAIN_TO 12-15 indexed past the 12 channels, and two enabled
  channels chained to each other recursed until a host stack overflow.
  Out-of-range targets are ignored and a chain stops after 12 hops.
- USB: BUFFER_ADDRESS + LENGTH could run past the 4 KB DPRAM array;
  the IN copy and the OUT delivery are clamped.
@begeistert
begeistert requested a review from lmSeryi as a code owner October 14, 2026 06:15
Second hardening pass (line-by-line audit plus PIO and wireless fuzz):

- PIO OUT EXEC / MOV EXEC could re-execute itself from inside itself
  (X = 0xA081, mov exec, x) until the host stack overflowed; forced
  execution is now nested at most 8 deep.
- PIO WAIT cleared IRQ flag (index & 7) on every wait: missing braces
  around the IRQ-clear pair since 1.1.0. Only WAIT IRQ clears now.
- DMA: a channel writing its own trigger register (AL1 TRANS_COUNT,
  MULTI_CHAN_TRIGGER) re-entered ExecuteChannel per beat until the host
  stack overflowed; chains and beat-triggered channels now share one
  depth limit of 12.
- DMA transfers run in slices of at most 2^20 beats per call (the DMA
  is now ticked), so a garbage TRANS_COUNT no longer stalls one bus
  write for minutes. Real transfers fit in one slice.
- VirtualNet: DHCP option 53 at the end of a frame and a TCP segment
  whose IP length exceeds the frame read past the buffer.
- Bounded memory: SSI tx/rx buffers, USB pending IN completions,
  CYW43439 SDPCM queue, backplane storage, gSPI buffer, VirtualNet and
  VirtualSwitch tables and per-connection receive bytes.
@begeistert

Copy link
Copy Markdown
Collaborator Author

Second hardening pass added (see the latest commit): PIO EXEC recursion and WAIT IRQ-flag bug, DMA self-trigger re-entrancy, DMA transfers sliced at 2^20 beats, VirtualNet out-of-bounds reads, and bounded buffers across SSI/USB/CYW43439. Unit 578 (stable 5/5), integration 197 passed / 4 skipped; MicroPython CPU-bound loop unchanged (4940 → 4932 ms median).

@begeistert
begeistert merged commit 11e28ed into master Oct 14, 2026
4 checks passed
@begeistert
begeistert deleted the fix/garbage-firmware branch October 16, 2026 01:12
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