DF-2298 / verdict.json
{ "finding_id": "DF-2298", "status": "reproduced", "reproduced": true, "impact": "memcorrupt: demonstrated OOB-read-to-arbitrary-write through production fw_write (write target = groom-controlled pointer read 8 bytes past a 32-byte kmalloc; content = attacker iso payload, length up to u16 65535 per packet); fw_read is the arbitrary-read mirror; ungroomed firing = wild write/panic. uid=0 chain not completed -- entry requires a FireWire controllers /dev/fw* node (0660 root:operator), absent in QEMU; same evidence-class standard as DF-5413/DF-5548/DF-5613.", "confidence": "certain", "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_allocbufs 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.", "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": "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", "kernel_refs": [ "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" ], "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 findings 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 runs write HAD landed (victim+4 showed the rotated pattern in dmesg) but the checker missed it; both fixed and re-run clean.", "attempts": 2, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 4200, "guest_dirty": 0, "build_cmd": "sh build.sh (cd /root/df229x && make; compiles the unmodified production firewire.c+fwdma.c+fwdev.c into df229x.ko)", "run_cmd": "kldload /root/df229x/df229x.ko; sysctl kern.df229x_test=2; sysctl kern.df229x_status (test 4 = trivial npacket=0 variant; fix validation: apply fix.diff to /usr/src, rebuild module, sh run_fix.sh)", "code_hash": "6a11c862c5cec3c0fc16c7e8f2e39fc09c8ea7b46792cb29a2cdc98fd00f2e4a", "notes": "Reconciled against the pass-2 negative note: pass 2 killed only the size_t-wrap sub-variant (sizeof(fwdma_seg)*nseg unreachable on 64-bit, 32-bit-only). The findings primary int-product-overflow-to-zero route is 64-bit reachable and was reproduced on x86-64 production code; additionally npacket=0 reaches the identical n=0 state with no overflow at all (fix-validated as DF-2298b err=22). Zero-seg am returns SUCCESS (STSTREAM err=0), so no error is visible to userspace before the OOB fire. Related but distinct: DF-5413 (crafted nchunk*npacket write-what-where through VALID seg arrays), DF-5613 (claim/teardown lifetime races); this finding is specifically the zero-length-seg[] OOB.", "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.", "fix_status": "fixed", "fix_baseline_reproduced": true, "fix_patched_reproduced": false, "fix_verdict": "fix.diff (fwdev.c fwdev_allocbuf validation incl. the nchunk*npacket 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 0x10000*0x10000 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_evidence": "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)" } |