Heap OOB read/write via zero-length segment array from integer overflow in n
Summary
fwdma_malloc_multiseg accepts untrusted int n without validating n>0 or checking caller nchunk*npacket product did not overflow. When product overflows to 0 (e.g. nchunk=65536 npacket=65536 -> 0 mod 2^32) nseg computes to 0 and function allocates struct fwdma_alloc_multi header with zero seg[] entries. Caller stores original non-overflowed nchunk/npacket in fw_xferq and inline accessors fwdma_v_addr/fwdma_bus_addr index am->seg[0] without bounds-checking against am->nseg reading 24 bytes past 32-byte allocation from adjacent slab object and dereferencing result. fw_write path writes user data through OOB-derived pointer (arbitrary kernel write). fw_read path reads kernel memory (info leak). Exploitation requires heap grooming to control adjacent chunk contents.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2298 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df229x.c | β | 19.9 KB | view raw | |
| Makefile | β | 1.8 KB | β download | |
| shim/ | β | 4.0 KB | β download | |
| build.sh | β | 208 B | view raw | |
| run_bootA.sh | β | 803 B | view raw | |
| run_fix.sh | β | 1.1 KB | view raw | |
| run.log | β | 2.0 KB | view raw | |
| fix_run.log | β | 2.6 KB | view raw | |
| baseline_build.log | β | 7.0 KB | view raw | |
| fix_build.log | β | 7.0 KB | view raw | |
| env.txt | β | 2.0 KB | view raw | |
| fix.diff | β | 1.4 KB | view raw | |
| README.md | β | 3.2 KB | β raw | |
| VERDICT.md | β | 5.5 KB | β raw | |
| manifest.json | β | 2.1 KB | view raw | |
| verdict.json | β | 7.0 KB | view raw |
DF-2298 β Heap OOB read/write via zero-length segment array from integer overflow in n
File: sys/bus/firewire/fwdma.c:145-166 + sys/bus/firewire/fwdma.h:59-75
Verdict: REPRODUCED β OOB read of a groom-controlled pointer + production
fw_write landing 2048 attacker-chosen bytes at the chosen kernel address.
Claim
FW_SSTBUF (no validation, fwdev.c:504-506) β fwdev_allocbuf
(fwdev.c:109) computes n = b->nchunk * b->npacket as an int multiply.
For nchunk = npacket = 0x10000 the product wraps to 0 mod 2^32.
fwdma_malloc_multiseg(fc, 4, 512, 0, ...) then computes
nseg = howmany(0, 4096/512) = 0, kmallocs only the 32-byte
struct fwdma_alloc_multi header (fwdma.c:162-163) and returns it as
success with a zero-length seg[] array. fwdev_allocbuf stores the
original nchunk/npacket in the queue and marks FWXFERQ_EXTBUF, so
production fw_write/fw_read reach fwdma_v_addr() (fwdma.h:59-66),
which reads am->seg[0].v_addr β 8..24 bytes past the 32-byte
allocation β and uiomove()s attacker iso data to that stale/groomed
pointer.
How to build / run (no FireWire hardware needed β KLD harness)
The harness compiles the unmodified production firewire.c + fwdma.c +
fwdev.c into df229x.ko (DF-5613 pattern) and drives the real
fw_ioctl / fw_write paths. Test 2:
- sprays the 32-byte kmalloc zone with chunks whose every qword is the victim pointer, then punches holes (every 2nd chunk freed);
FW_SSTBUF{tx:{0x10000, 0x10000, 512}} + FW_STSTREAMβ the zero-segamlands in a hole β the OOBam->seg[0].v_addr(atam+40) reads back the pointer we chose;- one production
fw_write()(fwdev.c:388-392) uiomoves the 4-byte iso header +len(2044) payload bytes of attacker data to that address.
# guest, root, fresh `vm.sh reset with-src`
cd /root/df229x && sh build.sh && kldload ./df229x.ko
sysctl kern.df229x_test=2 && sysctl kern.df229x_status
Expected (baseline, reproduced)
df2298/a1: am (32-byte kmalloc, nseg=0) -> OOB am->seg[0].v_addr = 0xfffff801187ea000
(read at am+40); groom target = 0xfffff801187ea000
df2298/a1: fw_write err=0; victim page now: "" / "DF2298-OOB-WRITE"
df2298/a1: CONTROLLED WRITE CONFIRMED: attacker iso payload ... landed at the
groom-chosen kernel address ... via the OOB-read seg[0].v_addr
kern.df229x_status: DF-2298 REPRODUCED: ... OOB-read-to-arbitrary-write
(Groom hit 4/4 attempts across two boots. Without the groom the same OOB
read hands fw_write an uncontrolled kernel pointer β a wild write/panic.)
Expected (fix.diff applied, module rebuilt β fix_run.log)
df2298/a*: STSTREAM err=22 -- bufspec rejected, no zero-seg allocation kern.df229x_status: DF-2298 nchunk=npacket=0x10000: STSTREAM err=22 (rejected, no write) kern.df229x_status: DF-2298b npacket=0: STSTREAM err=22 (trivial n=0 variant) kern.df229x_status: regression OK: legit bufspec still works
Files
df229x.c (harness; test 2 = this finding, test 4 = trivial npacket=0
variant), Makefile + shim/, build.sh, run_bootA.sh / run_fix.sh,
run.log (decisive baseline), fix_run.log, baseline_build.log /
fix_build.log, env.txt, fix.diff, VERDICT.md, manifest.json, verdict.json
DF-2298 β VERDICT
Status: REPRODUCED (impact: memcorrupt β OOB read of a heap-adjacent,
groom-controlled pointer feeding a production uiomove write of fully
attacker-chosen bytes: a demonstrated write-what-where class primitive).
Confidence: certain.
Reachability trace (production code, this tree, today)
fwdev.c:504-506βFW_SSTBUFcopiesstruct fw_isobufreq(userspaceunsigned inttriples,firewire.h:50-56) unvalidated.fwdev.c:103-109βfwdev_allocbuf:kmalloc(sizeof(fw_bulkxfer) * b->nchunk)(48 * 0x10000 = 3 MB, fine), thenfwdma_malloc_multiseg(fc, 4, roundup2(512,4), b->nchunk * b->npacket, ...). The element count is an int multiply:0x10000 * 0x10000 == 0 (mod 2^32).fwdma.c:157-166βesize=512 β€ PAGE_SIZEβssize = rounddown(4096,512) = 4096,nseg = howmany(0, 8) = 0;am = kmalloc(32 + 24*0)β only the 32-byte header βam->nseg = 0, the per-segment loop (fwdma.c:193-203) is skipped, andamis returned as success.fwdev.c:116-134β the originalbnchunk=0x10000, bnpacket=0x10000are stored, 65536fw_bulkxferentries initialized, queue flaggedFWXFERQ_EXTBUF. From userspace theioctlreturned 0: nothing looks wrong.fwdev.c:388-392βfw_write:fp = fwdma_v_addr(it->buf, poffset+queued);fwdma.h:59-66computesam->seg[0/4096].v_addrβseg[0]begins atam+32, i.e. entirely past the 32-byte allocation;v_addris the qword atam+40, read from the adjacent 32-byte slab chunk. Thenuiomove(fp, 4)+uiomove(fp->payload, fp->len)write attacker bytes to that pointer. (fw_read,fwdev.c:321-330, is the arbitrary-read mirror.)
What was run (decisive evidence, run.log)
Production path driven end-to-end with the OOB pointer proven controlled before any write fires:
- 512Γ
kmalloc(32, M_FW)groom chunks each pre-filled with the victim pointer, every 2nd freed βam'skmalloc(32, M_FW)(fwdma.c:162) lands in a hole; df2298/a1: STSTREAM OK (err=0) ... nseg=0 ssize=4096 esize=512 bnchunk=65536 bnpacket=65536β the overflow-to-zero state itself;OOB am->seg[0].v_addr = 0xfffff801187ea000 (read at am+40); groom target = 0xfffff801187ea000β the OOB read returns exactly the pointer we planted (groom hit 4/4 across two boots: 3/3 on the first run's attempts, 1/1 decisive);- production
fw_writeβerr=0; victim page now: "" / "DF2298-OOB-WRITE"β header atvictim[0..4)(NUL bytes print empty), payloadDF2298-OOB-WRITE!β¦atvictim+4; CONTROLLED WRITE CONFIRMED: attacker iso payload (header + 2044 bytes) landed at the groom-chosen kernel address.
Primitive characterization (write-capable bug β what it is)
- What: 4 semi-constrained bytes (iso stream header:
len:16bounds the following write) then up tolen(u16, β€65535) fully attacker-chosen bytes per packet, repeatable per chunk/packet. - Where: any address the attacker can plant in the adjacent 32-byte kmalloc chunk β i.e. arbitrary kernel address (demonstrated: the write landed precisely on the pre-registered victim page; the harness never fires the write unless the OOB read provably returned the planted pointer).
- Read mirror:
fw_read(fwdev.c:321-330) uiomoves out of the same OOB-derived pointer β arbitrary-read shape. - uid=0 route: on the no-KASLR/no-SMAP guest, spray the 32-byte zone with
the address of any known writable function pointer / ops table (fixed
kernel addresses), overwrite it via the iso payload, trigger the call β
or use the read mirror to walk
allprocβ ownp_ucredand zero the uids with the write. Full uid=0 chain not constructed this run: entry requires a FireWire controller's/dev/fw*node (0660 root:operator), which the QEMU guest does not have β same evidence-class standard as DF-5413/DF-5548/DF-5613 (hardware-gated entry, primitive proven on production code). - Without grooming, the same OOB read hands
uiomovean uncontrolled kernel qword β wild write β panic (trivially reachable DoS).
Reconciliation with the pass-2 negative note
Pass 2 killed only the other sub-variant ("size_t wrap of
sizeof(fwdma_seg)*nseg unreachable on 64-bit β¦ 32-bit-only"). The
finding's primary claim β int product nchunk*npacket wrapping to 0 β
zero-length seg[] β OOB access β is 64-bit reachable and reproduced
here on x86-64. Also note npacket=0 reaches the identical n=0 state
without any overflow (tested: rejected by the fix, DF-2298b err=22).
Fix validation (fix.diff, applied to guest /usr/src, module rebuilt)
- Overflow variant
0x10000Γ0x10000βSTSTREAM err=22, no zero-seg allocation, no write (it=0 buf=0 bulkxfer=0) β thefwdev_allocbufhunk rejectsnpacket > 0x7fffffff / nchunkbefore the multiply feedsfwdma_malloc_multiseg. - Trivial variant
npacket=0βerr=22. - Hard boundary: direct
fwdma_malloc_multiseg(esize=512, n=0)βNULL, no trap (thefwdma.chunk), so no caller can reach the zero-lengthseg[]state even if a future caller forgets validation. - Regression: legit
{4,8,2048}still allocates and streams. Guest alive (fix_run.log).
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedfix.diff (fwdev.c fwdev_allocbuf validation incl. the nchunknpacket overflow check + fwdma.c esize/n hard guard) applied to the guest /usr/src; harness module rebuilt from the patched production sources. Baseline: STSTREAM returns 0 with a 0-seg 32-byte am and the groomed production fw_write lands 2048 attacker bytes on the chosen address. Patched: the 0x100000x10000 vector is rejected (err=22, it=0 buf=0 bulkxfer=0, no write), the trivial npacket=0 variant is rejected (err=22), the direct fwdma_malloc_multiseg(n=0) probe returns NULL, and the legit {4,8,2048} regression still allocates and streams; guest alive after the full suite.
fix_run.log (patched suite incl. 'df2298/a*: STSTREAM err=22 -- bufspec rejected, no zero-seg allocation' and 'regression OK'), fix_build.log (patched build), fix.diff (git apply --check verified against the repo sys/ tree; patch(1) applied cleanly to guest /usr/src)
Confirmed kernel references
- sys/bus/firewire/fwdma.c:145
- sys/bus/firewire/fwdma.c:157
- sys/bus/firewire/fwdma.c:160
- sys/bus/firewire/fwdma.c:162
- sys/bus/firewire/fwdma.c:163
- sys/bus/firewire/fwdma.c:193
- sys/bus/firewire/fwdma.h:51
- sys/bus/firewire/fwdma.h:59
- sys/bus/firewire/fwdma.h:65
- sys/bus/firewire/fwdev.c:103
- sys/bus/firewire/fwdev.c:109
- sys/bus/firewire/fwdev.c:116
- sys/bus/firewire/fwdev.c:388
- sys/bus/firewire/fwdev.c:390
- sys/bus/firewire/fwdev.c:504
- sys/bus/firewire/firewire.h:50
Detail
Exploit chain
open /dev/fw0.0 (operator group) -> ioctl(FW_SSTBUF, {0x10000,0x10000,512}) -> ioctl(FW_STSTREAM) [n wraps to 0; queue left with 65536 chunks and a zero-seg 32-byte am] -> spray/groom the 32-byte kmalloc zone so the chunk following am contains the desired target address -> write()/fw_write iso packets: 4 semi-constrained header bytes + up to 65535 fully attacker-chosen bytes land at the target (repeatable per packet/chunk) -> on the no-KASLR/no-SMAP guest, target a known writable function pointer/ops table and trigger it (or use the fw_read OOB mirror to walk allproc to p_ucred and zero the uids) -> uid=0. Full uid=0 chain not constructed this run (device-gated entry; no /dev/fw* in QEMU), matching the DF-5413/DF-5548/DF-5613 evidence standard.
Evidence (decisive lines)
run.log (decisive baseline: 'STSTREAM OK ... nseg=0 ssize=4096 esize=512 bnchunk=65536 bnpacket=65536'; 'am (32-byte kmalloc, nseg=0) -> OOB am->seg[0].v_addr = 0xfffff801187ea000 ... groom target = 0xfffff801187ea000'; 'fw_write err=0; victim page now: "" / "DF2298-OOB-WRITE"'; 'CONTROLLED WRITE CONFIRMED: attacker iso payload (header + 2044 bytes ...) landed at the groom-chosen kernel address'), fix_run.log (patched: 'STSTREAM err=22 -- bufspec rejected, no zero-seg allocation (it=0 buf=0 bulkxfer=0)'; 'DF-2298b npacket=0: STSTREAM err=22'; 'regression OK'), baseline_build.log / fix_build.log, env.txt
PoC changes
No seed existed (poc_path was empty). Harness df229x.c written fresh on the DF-5613/DF-5413 KLD pattern; test 2 grooms the 32-byte kmalloc zone (planted victim pointer, every-2nd-chunk freed), drives the production SSTBUF+STSTREAM pair with the finding's exact 0x10000*0x10000 vector, and only then fires one production fw_write -- and only after proving the OOB-read pointer equals the planted one. Two harness bugs were fixed during iteration: (1) the payload fill started the 17-byte pattern at i%17 instead of (i-4)%17, rotating it by 4; (2) the scan signature 'DF2298-W' never occurs in 'DF2298-OOB-WRITE!' -- the first run's write HAD landed (victim+4 showed the rotated pattern in dmesg) but the checker missed it; both fixed and re-run clean.
Verified recommended fix
Validate in fwdev_allocbuf (EINVAL for psize==0/psize>0x7fffffff/nchunk==0/npacket==0/npacket > 0x7fffffff/nchunk) and hard-guard fwdma_malloc_multiseg (reject esize<=0 || n<=0 -> NULL so no zero-length seg[] can ever be returned) -- fix.diff in this pack.
Verdict
REPRODUCED on the QEMU guest with the unmodified production firewire.c+fwdma.c+fwdev.c compiled into KLD df229x.ko. FW_SSTBUF{tx:{nchunk=0x10000, npacket=0x10000, psize=512}} (no validation, fwdev.c:504-506) -> fwdev_allocbuf's int multiply nchunk*npacket wraps to 0 (fwdev.c:109) -> fwdma_malloc_multiseg computes nseg=howmany(0,8)=0, kmallocs ONLY the 32-byte fwdma_alloc_multi header (fwdma.c:162-163) and returns it as SUCCESS with a zero-length seg[] array; fwdev_allocbuf stores the ORIGINAL nchunk/npacket and flags FWXFERQ_EXTBUF, so the ioctl pair returns 0. After a 512-chunk groom of the 32-byte kmalloc zone (planted pointer + punched holes), the zero-seg am landed in a hole 4/4 times across two boots; fwdma_v_addr (fwdma.h:59-66) OOB-read am->seg[0].v_addr at am+40 returned EXACTLY the planted pointer, and production fw_write (fwdev.c:388-392) then uiomove()'d the 4-byte iso header + 2044 attacker bytes onto the chosen kernel address ('CONTROLLED WRITE CONFIRMED ... DF2298-OOB-WRITE'). The harness only fires fw_write when the OOB read provably returned the planted pointer, so the demonstration is a controlled write, not a lucky crash.
No comments yet.