bugfix: integer underflow in PSRAM virtual-disk file import (OOB write from hostile FAT metadata) - #763
Conversation
scgbckbone
left a comment
There was a problem hiding this comment.
are you planning to re-open ?
|
I have sent an email 6 hours ago to security@coinkite.com with all info |
|
but i can reopen it right away without any problems if you give me the ok |
|
Ok, to reopen this PR... it's very not hidden at this point. This fix can be in the next release as it didn't make today's cutoff. |
psram_copy_file() and psram_mmap_file() trusted FAT metadata exposed over USB Mass Storage: a cluster chain covering more bytes than the directory entry's file size made 'len = actual_len - so_far' underflow to ~4GB, causing an out-of-bounds memcpy across PSRAM from a compromised USB host. - validate cluster-chain coverage against file size before each fragment - check destination bounds with 64-bit arithmetic *before* writing (the old post-memcpy check was too late, and dest+len+3 could wrap) Q1 shares this file via symlink; both platforms are fixed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
9e7150b to
9261095
Compare
scgbckbone
left a comment
There was a problem hiding this comment.
Two hostile FAT/link-map shapes can still bypass these guards and produce out-of-bounds PSRAM reads or mappings. The fragment count must be validated before it is narrowed by block_to_ptr(), and the declared final remainder must be checked against the final fragment's physical capacity.
Two residual hostile-FAT shapes bypassed the guards: - num_clusters (DWORD from the link map) was narrowed to uint16_t by block_to_ptr(); 65536 wrapped to 0 and passed the range check, mapping fragments outside the PSRAM window. Now validated as u32 (1..BLOCK_COUNT) before the call in both psram_copy_file() and psram_mmap_file(). - The declared final remainder was not checked against the final fragment's physical capacity: a 1024-byte directory entry backed by one 512-byte cluster made the final branch copy/map past the fragment. Now the fragment capacity is preserved and the final remainder must fit it (checked before the copy path's align-4). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
ACK cf3fe22 nit: the changelog only mentions oversized chains and OOB writes while the patch also prevents undersized final fragments and OOB reads/mappings - please update that wording and resolve the changelog conflict with master otherwise LGTM |
Review nit from scgbckbone. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Two findings from reviewing the current patch:
Non-blocking follow-ups: the PR currently conflicts with |
…head dec0158) into copilot branch Brings in the reviewed patch: integer-underflow fix in psram_copy_file/ psram_mmap_file plus review fixes (fragment-count validation as u32, final-remainder capacity check, 64-bit pre-copy bounds check) and the updated changelog wording. Conflicts: - releases/Next-ChangeLog.md (upstream reshuffled sections after 5.6.1) # Conflicts: # releases/Next-ChangeLog.md Co-authored-by: Amiga500 <16525337+Amiga500@users.noreply.github.com>
…ing fragments Finding 1: CREATE_LINKMAP reports run lengths in clusters, but the code treats them as 512-byte sectors; a hostile host can rewrite the BPB so FatFs mounts a filesystem with csize > 1, breaking block_to_ptr() and capacity math. Reject vfs.fatfs.csize != 1 right after f_mount() in both psram_mmap_file() and psram_copy_file() (the explicit-reject option). Finding 2: when preceding fragments exactly cover objsize, the final branch computed len == 0 and accepted the oversized chain, making mmap() return a spurious zero-length tuple. Now reject a final fragment whose remainder is < 1 (covers zero-size files with an allocated chain). Also merged PR Coldcard#763 head (dec0158) into this branch (changelog conflict resolved, keeping both entries) and updated the changelog wording. Validated with a python logic harness and a gcc-compiled C harness replicating both fixed loops: legit shapes pass, all hostile FAT shapes (csize>1, u16 wrap, oversized chains, trailing fragments, edge clusters) are rejected. Co-authored-by: Amiga500 <16525337+Amiga500@users.noreply.github.com>
|
Thanks for the careful re-review. Both findings addressed in 457acd8:
len = fp.obj.objsize - so_far; So when preceding fragments already cover objsize exactly (so_far == objsize), a remaining fragment is rejected as oversized-chain metadata instead of producing a spurious zero-length tuple from mmap(). This also covers the zero-size-file-with-allocated-chain case you mentioned (objsize == 0 with any link map). Follow-ups: Re-validated with the standalone harness extended for the new shapes: legit single/multi-fragment files (including normal over-allocation of the final cluster) pass; hostile shapes (csize 2/4, num_clusters u16 wrap, oversized chains, exact-coverage trailing fragment, zero-size file with chain, clusters past end of disk) are all rejected. CodeQL is clean on the diff. |
|
Move commit 457acd8 onto the branch backing the PR. The fixes currently exist only on copilot/amiga500-fix-psramdisk-fat-underflow, while PR 763 tracks amiga500-fix-psramdisk-fat-underflow. |
…ing fragments Finding 1: CREATE_LINKMAP reports run lengths in clusters, but the code treats them as 512-byte sectors; a hostile host can rewrite the BPB so FatFs mounts a filesystem with csize > 1, breaking block_to_ptr() and capacity math. Reject vfs.fatfs.csize != 1 right after f_mount() in both psram_mmap_file() and psram_copy_file() (the explicit-reject option). Finding 2: when preceding fragments exactly cover objsize, the final branch computed len == 0 and accepted the oversized chain, making mmap() return a spurious zero-length tuple. Now reject a final fragment whose remainder is < 1 (covers zero-size files with an allocated chain). Also merged PR Coldcard#763 head (dec0158) into this branch (changelog conflict resolved, keeping both entries) and updated the changelog wording. Validated with a python logic harness and a gcc-compiled C harness replicating both fixed loops: legit shapes pass, all hostile FAT shapes (csize>1, u16 wrap, oversized chains, trailing fragments, edge clusters) are rejected. Co-authored-by: Amiga500 <16525337+Amiga500@users.noreply.github.com>
…ing fragments Finding 1: CREATE_LINKMAP reports run lengths in clusters, but the code treats them as 512-byte sectors; a hostile host can rewrite the BPB so FatFs mounts a filesystem with csize > 1, breaking block_to_ptr() and capacity math. Reject vfs.fatfs.csize != 1 right after f_mount() in both psram_mmap_file() and psram_copy_file() (the explicit-reject option). Finding 2: when preceding fragments exactly cover objsize, the final branch computed len == 0 and accepted the oversized chain, making mmap() return a spurious zero-length tuple. Now reject a final fragment whose remainder is < 1 (covers zero-size files with an allocated chain). Also merged PR Coldcard#763 head (dec0158) into this branch (changelog conflict resolved, keeping both entries) and updated the changelog wording. Validated with a python logic harness and a gcc-compiled C harness replicating both fixed loops: legit shapes pass, all hostile FAT shapes (csize>1, u16 wrap, oversized chains, trailing fragments, edge clusters) are rejected. Co-authored-by: Amiga500 <16525337+Amiga500@users.noreply.github.com>
| # Shared Improvements - Both Mk and Q | ||
|
|
||
| - tbd | ||
| - Bugfix: Simulator crashed on Bless Firmware, due to a desynced LED pipe. Thanks to |
Latest review comment by scgbckbone on Coldcard#763: "remove" (line 7 of releases/Next-ChangeLog.md, the View TRNG Words entry). Note: PR 763's branch (amiga500-fix-psramdisk-fat-underflow) does not contain this entry; direct push to it is blocked by a repository rule, so this records the removal on the session branch. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Removed a bugfix note about simulator crash on Bless Firmware.
Summary
psram_copy_file()andpsram_mmap_file()(stm32/COLDCARD_MK4/psramdisk.c, shared by Q1 via symlink) trust FAT metadata of the PSRAM virtual disk, which is exposed over USB Mass Storage and therefore fully writable by a compromised USB host.The final fragment length is computed as:
where
actual_len = fp.obj.objsize(directory entry size field) andso_faris the sum of the bytes covered by previous cluster-chain fragments. FatFs'CREATE_LINKMAPfollows the cluster chain to EOC without validating it againstobjsize, so a crafted FAT (chain covering more bytes thanobjsize, e.g. on a.dfu/.psbtfile dropped by the host) makesso_far > actual_lenandlenunderflows to ~4 GB. The subsequentmemcpy(dest, spot, len)then writes attacker-influenced data out of bounds:dest + actual_len + 3 >= PSRAM_TOP_BASEis insufficient and can itself wrap around forobjsizenear 4 GB;dest >= PSRAM_TOP_BASEcheck happened after thememcpy— too late.Realistic impact: corruption of everything staged in PSRAM (PSBT/firmware staging areas) and device crash via BusFault. SRAM and flash sit below
0x90000000, so the forward-only copy does not reach them; no credible path to code execution was identified (hence moderate severity), but it is a deterministic host-triggered memory corruption reachable without user confirmation when Virtual Disk is in "Enable & Auto" mode.Fix
so_far > actual_len, and per-fragmentlen > remaining) before the subtraction — no more underflow.memcpy, eliminating both the post-hoc check and the wrap-around.psram_mmap_file(), which hands attacker-influenced (address, length) tuples to MicroPython.Verification
Logic validated with a standalone host harness replicating the fixed loop: legit single/multi-fragment files pass; hostile chains (oversized coverage,
objsizenear 4 GB, degenerate zero-size) are all rejected. A full firmware build was not possible in the authoring environment (no ARM toolchain / uninitialized submodules) — a run throughrepro-build.shbefore merge would be appreciated.