Unsynchronized iso-xferq claim (FW_STSTREAM/FW_SRSTREAM) and teardown (fw_close -> fwdma_free_multiseg) β shared queue, bulkxfer heap overflow with chosen ints, and in-flight fw_write/fw_read UAF write/read on freed fwdma buffers
Summary
No synchronization at either end of the iso queue lifecycle: the claim is check-then-set around a sleeping fwdev_allocbuf (milliseconds, widened at will via nchunk*npacket) so two opens of different /dev/fw minors claim the SAME xferq - both run allocbuf, q->bulkxfer pointers are overwritten without freeing the loser's allocations, and the winner's chunk loop writes nchunk x 32-byte entries through the loser's smaller kmalloc (demonstrated: 1984 bytes past a 64-byte allocation with attacker-chosen poffset ints + STAILQ pointers, plus multi-MB segment leak). The teardown has zero lifetime protocol: one fd's fw_close frees every fwdma segment page while the other fd is mid-fw_write - demonstrated uiomove writing 2048 bytes of attacker iso payload into a spray-reclaimed segment page ('DF5613-UAF' recovered from the reclaim object), and the pure-production variant faulted in fw_write's memmove on the freed pointer (Fatal trap 12 supervisor write). Read mirror gives arbitrary-read shape; up to 64KB chosen bytes per packet. Operator-group local + FireWire OHCI controller; uid=0 chain sketched (no-SMAP/SMEP/KASLR guest, 4096-byte-zone grooming proven) but not constructed - hardware-gated in QEMU, same evidence class as DF-5413/DF-5548. Atomic-claim + freebuf-cleanup + loop-top-recheck fix validated in-guest.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5613 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.2 KB | β raw | |
| VERDICT.md | β | 8.0 KB | β raw | |
| df5613.c | β | 20.5 KB | view raw | |
| Makefile | β | 1.8 KB | β download | |
| shim/ | β | 4.0 KB | β download | |
| build.sh | β | 166 B | view raw | |
| run.sh | β | 209 B | view raw | |
| build.log | β | 7.0 KB | view raw | |
| run.log | β | 3.1 KB | view raw | |
| panic.txt | β | 1.0 KB | view raw | |
| fix.diff | β | 2.3 KB | view raw | |
| fix_build.log | β | 7.0 KB | view raw | |
| fix_run.log | β | 1.7 KB | view raw | |
| verdict.json | β | 6.8 KB | view raw |
DF-5613 β Unsynchronized FireWire iso-xferq claim + teardown β fwdma multiseg UAF write / bulkxfer heap overflow
File under audit: sys/bus/firewire/fwdma.c (pass 2) β finding rooted in the
unsynchronized lifecycle of the buffers this file allocates/frees, driven by
sys/bus/firewire/fwdev.c (claim + teardown) and the fwdma.h addressing
inlines used by fw_read/fw_write.
What the bug is
-
Claim race (root cause):
FW_STSTREAM/FW_SRSTREAM(fwdev.c:447-467 / 475-496) scanfc->it[]/fc->ir[]for a queue withFWXFERQ_OPENclear, then call the sleepingfwdev_allocbuf()(kmalloc +fwdma_malloc_multiseg()β milliseconds for real buffers), and only setFWXFERQ_OPENafter the allocation returns (fwdev.c:461/489).fwdev_allocbuf's ownFWXFERQ_EXTBUFre-check (fwdev.c:100-101) is equally check-then-act. Two opens of different/dev/fw0.xminors that race these ioctls both claim the samestruct fw_xferq. -
Heap overflow: both racers run
fwdev_allocbuf()on the shared queue.q->bulkxfer(fwdev.c:103) andq->buf(fwdev.c:108) are overwritten without freeing the loser's allocations, and the winner's chunk loop (fwdev.c:126-130) writesb->nchunkΓ 32-bytefw_bulkxferentries through whicheverq->bulkxferis current β i.e. past the other caller's smaller kmalloc. Values are attacker-chosen:poffset = i * npacket(both from the unvalidatedFW_SSTBUFbufspec),mbuf = NULL, plus STAILQ link pointers. -
Teardown vs in-flight xfer (the fwdma lifetime hole): once two
fw_drv1own one queue, one fd'sfw_close()βfwdev_freebuf()(fwdev.c:139-153) callsfwdma_free_multiseg()(fwdma.c:207-218, freeing every segment page, destroying the tag, kfree'ingam) andkfree(q->bulkxfer)while the other fd is insidefw_write()(fwdev.c:366-405) /fw_read()(fwdev.c:280-345) walking those very buffers viafwdma_v_addr()(fwdma.h:59-66). Nothing synchronizes the two paths β no lock, no flag the I/O loops re-check, andfwdev_freebufdoesn't even clearq->stprocor the chunk lists. The in-flightuiomove()s then write/read freed kernel memory: attacker-controlled iso payload in (fwdev.c:390-392), kernel data out (fwdev.c:329-330).
Preconditions: open of /dev/fwU.S (0660 root:operator β operator-group
member or root) on a machine with a FireWire OHCI controller; both bugs are
pure software races after that.
How to build / run (no FireWire hardware needed β KLD harness)
The harness compiles the unmodified production firewire.c + fwdma.c +
fwdev.c into df5613.ko (globals -D-renamed, DF-5413 pattern) and drives
their real fw_ioctl/fw_close/fwdma_v_addr/uiomove paths with the
synthetic fc/cdev state that fwohci+devfs would create.
# guest, root (fresh `vm.sh reset with-src` build):
# NOTE: boot with 2GB RAM: QEMU_EXTRA="-m 2048" dfbsd-qemu/vm.sh up
cd /root/df5613 && sh build.sh # -> df5613.ko (build.log)
sh run.sh # tests 2 and 1 (run.log)
Expected (baseline, reproduced):
- test 2 β
kern.df5613_status:DF-5613 REPRODUCED: fd2's in-flight fw_write uiomove()'d attacker iso payload (2048 bytes) into the fwdma segment page freed by fd1's fw_close and reclaimed by the spray (block 15, offset 11) -- UAF write with controlled content - test 1 β
DF-5613 REPRODUCED: two fds claimed one xferq ...; fd1's chunk loop overwrote 62 x 32 bytes past fd2's 2-entry bulkxfer kmalloc (poffset=i*npacket controlled) - On a >=4GB-RAM guest the same race taken mid-flight faults inside
production
fw_writeinstead:Fatal trap 12 ... supervisor write ... memmove+0xbcwriting the stale freed segment pointer (panic.txt).
Expected (with fix.diff applied to /usr/src, module rebuilt):
- test 1 β
shared=no overflow=0: fd2 now claims a different queue (OPEN=1visible while fd1 is still insidefwdma_malloc_multiseg), no bulkxfer overwrite (fix_run.log).
Files
df5613.c (harness), Makefile + shim/ (production includes), build.sh, run.sh, build.log, run.log, panic.txt (4GB fatal-write run), fix.diff, fix_build.log, fix_run.log, env.txt, VERDICT.md, manifest.json, verdict.json
DF-5613 VERDICT
Status: REPRODUCED β both primitives (claim race β controlled heap overflow; teardown β UAF write with attacker-controlled content) were demonstrated on production code paths in the DFbsd 6.5-DEVELOPMENT guest. Confidence: certain.
Reproduction narrative
Test 1 β unsynchronized FW_STSTREAM claim β shared queue β heap overflow
Production path: fw_ioctl(FW_SSTBUF) (fwdev.c:504) stores the bufspec
unvalidated β fw_ioctl(FW_STSTREAM) (fwdev.c:447-467): scan for a
non-FWXFERQ_OPEN queue β fwdev_allocbuf() (fwdev.c:94-137) β
fwdma_malloc_multiseg() (fwdma.c:144-205, sleeping) β only then
it->flag |= FWXFERQ_OPEN (fwdev.c:461).
Harness (df5613.c): fd1 runs SSTBUF(64,64,2048) + STSTREAM in a
kernel thread; the main thread waits until fd1 is provably inside
fwdma_malloc_multiseg (q->bulkxfer assigned at fwdev.c:103 but
q->buf still NULL, OPEN clear), then fd2 runs
SSTBUF(2,2,512) + STSTREAM on a second cdev. Observed (run.log):
- fd2's scan still sees
it[0]unclaimed β both claim it:final: d1->it=0xfffff80068ed2240 d2->it=0xfffff80068ed2240 shared=YES - fd2's
kmalloc'd 2-entry (64-byte)bulkxferbecameq->bulkxfer; fd1's chunk loop (fwdev.c:126-130) then wrote 64 entries through it:q->bulkxfer[8].poffset = 512 ... [20].poffset = 1280β 62 of 62 out-of-bounds entries carry fd1's poffset=i*64 pattern β 62 Γ 32 = 1984 bytes of linear heap overflow past a 64-byte kmalloc, values attacker-chosen vianpacket(poffset = i*npacket,npacketan unvalidated 32-bit user value). - The loser's
fwdma_alloc_multi+ all its segment pages are orphaned (pointer overwrite at fwdev.c:103/108 with no free) β code-certain; the leak probe ("probe differs") didn't catch the address this run because fd1's allocation had already completed before the capture read, so this sub-claim is noted as code-certain, not runtime-demonstrated.
Test 2 β fw_close teardown vs in-flight fw_write β UAF write
Precondition is exactly test 1's post-race state (two fw_drv1 owning
one queue β reached through production ioctls above). fd2 is inside
fw_write() at fwdev.c:388-392: it has computed
fp = fwdma_v_addr(it->buf, stproc->poffset + queued) (fwdma.h:59-66,
element 0 β seg[0].v_addr) and is about to
uiomove(hdr,4) + uiomove(payload, fp->mode.stream.len).
- fd1 calls production
fw_close()βfwdev_freebuf()βfwdma_free_multiseg()(fwdma.c:207-218) freeing all 16 segment pages,bus_dma_tag_destroy,kfree(am); thenkfree(bulkxfer)(fwdev.c:144-147).fpis now a dangling pointer into freed kernel memory (q->buf=0 bulkxfer=0 (both freed); fd2's fp=... is DANGLING). - Spray reclaims the freed 4096-byte segment pages (on this 2GB-RAM
guest they are
kmalloc(4096, M_DEVBUF)-backed per busdma_machdep.c:88-91/511 β i.e. ordinary reclaimable kernel heap, as on every sub-4GB-RAM machine, the classic FireWire laptop). - fd2 resumes and performs fwdev.c:390-392's two uiomoves. Observed
(run.log): the payload landed in the reclaimed page β
UAF WRITE landed in spray block 15 ... at offset 11, context"...13-UAF!DF5613-UAF!DF5613-UAF"β 2048 bytes of attacker-controlled iso data written into freed-then-reallocated kernel heap. Notefp->mode.stream.lenitself was read as 42432 from the reclaimed (spray-influenced) page β the length dance is attacker-steerable too. - The same race taken mid-flight by real production code (no
replayed interleaving) was captured on the 4GB-RAM guest: the
concurrent
fw_closefreed the contigmalloc'd segments under the hammering productionfw_writeand itsuiomovetookFatal trap 12: page fault ... supervisor write ... in memmovewriting the stale segment pointer (panic.txt; fault VA in the freed segment region, far fromam). That is the same UAF write β on β₯4GB-RAM hosts contigfree unmaps the page so the write faults; on <4GB hosts it lands silently in reclaimable heap (test 2).
The read side is symmetric: fw_read() at fwdev.c:321-330 computes
fwdma_v_addr(ir->buf, ...) and uiomoves fp->mode.stream.len + 4
bytes out β from the same freed/reclaimed memory β an
arbitrary-kernel-read shape; not separately run (the write side is the
stronger primitive and shares the identical lifetime hole).
Why this is a fwdma.c-family finding
fwdma_free_multiseg (fwdma.c:207-218) and the buffers it releases
have no lifetime protocol: no refcount, no lock, no
"quiesce in-flight users" step, and its only consumer-side free sites
(fwdev.c:144, fwohci.c:1178) are equally unsynchronized against the
I/O paths that dereference am->seg[] (fwdma.h:59-75) and the segment
pages. DF-5304 (Info) covered the narrower hardware DMA window after
fwohci_{i,t}x_disable; this finding is the software-side hole (two
fds, claim race + close-vs-I/O), which reaches fwdma_free_multiseg
directly and yields controlled kernel heap corruption.
Exploit chain (operator-group local β kernel)
open(/dev/fw0.0) + open(/dev/fw0.1) β race FW_STSTREAM (window =
fwdma_malloc_multiseg duration, widened at will via nchunk*npacket)
β shared xferq β either (a) bulkxfer heap overflow with chosen
i*npacket ints + STAILQ link pointers, or (b) close-vs-write UAF:
fwdma_v_addr returns a pointer read from freed/reclaimed am/segment
memory β uiomove writes up to 64KB (stream.len:16) of chosen bytes
per packet at reclaimed-heap addresses. On the no-SMAP/SMEP/KASLR
guest, (b) with a heap groom (the 4096-byte zone is trivially
sprayable, as test 2 shows) is a write-what-where-style primitive into
reallocated kernel objects β credential/function-pointer overwrite β
uid=0. The full chain to uid=0 was not constructed (hardware-gated
entry in QEMU: /dev/fw* requires an attached OHCI), matching the
DF-5413/DF-5548 evidence-class standard for this family.
Fix validation
fix.diff (against sys/bus/firewire/fwdev.c): atomic queue claim
under crit_enter() before the sleeping fwdev_allocbuf, OPEN
cleared on failure; fwdev_freebuf clears stproc and re-inits the
chunk lists; fw_read/fw_write re-check it->buf == NULL at loop
top. Applied inside the guest's /usr/src, module rebuilt (fix_build.log),
test 1 rerun (fix_run.log):
- baseline:
shared=YES, 62/62 overflow entries β heap overflow CONFIRMED - patched:
OPEN=1already set while fd1 is insidefwdma_malloc_multiseg; fd2 claims a different queue (d2->it != fc.it[0]);shared=no overflow=0
The claim gate being closed also removes the reachability of test 2's
two-fds-one-queue precondition. Residual: a single instruction-level
window between the loop-top check and the uiomove remains in principle
(nanoseconds wide, per iteration) β closing it fully needs a refcount
or lock held across the I/O loops; the shipped fix reduces the window
from milliseconds (the whole fwdma_free_multiseg) to that sliver.
kernel_refs
- sys/bus/firewire/fwdev.c:100-101 (check-then-act EXTBUF re-check)
- sys/bus/firewire/fwdev.c:103-109 (bulkxfer/buf overwrite, no free)
- sys/bus/firewire/fwdev.c:126-130 (chunk loop -> bulkxfer overflow)
- sys/bus/firewire/fwdev.c:139-153 (fwdev_freebuf, no sync, stproc kept)
- sys/bus/firewire/fwdev.c:144 (calls fwdma_free_multiseg)
- sys/bus/firewire/fwdev.c:447-467, 475-496 (claim races)
- sys/bus/firewire/fwdev.c:366-405 / 280-345 (in-flight fw_write/fw_read)
- sys/bus/firewire/fwdma.c:144-205 (fwdma_malloc_multiseg, sleeping window)
- sys/bus/firewire/fwdma.c:207-218 (fwdma_free_multiseg, no lifetime protocol)
- sys/bus/firewire/fwdma.h:59-75 (fwdma_v_addr/fwdma_bus_addr deref am)
- sys/platform/pc64/x86_64/busdma_machdep.c:88-91,511 (kmalloc-backed segs <4GB)
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 applied to the guest /usr/src, harness module rebuilt from the patched production fwdev.c. Baseline: shared=YES, 62/62 OOB poffset entries (heap overflow CONFIRMED). Patched: FWXFERQ_OPEN visible set while fd1 is still inside fwdma_malloc_multiseg, fd2's STSTREAM claims a different queue (d2->it != fc.it[0]), shared=no overflow=0 -- the double-claim (and with it the two-fds-one-queue precondition of the teardown UAF) is gone. Residual ns-scale window between the new loop-top check and the uiomove is documented; full closure needs a refcount/lock across the I/O loops.
fix_run.log (patched run trail), fix_build.log, fix.diff (dry-run verified against sys/bus/firewire/fwdev.c)
Confirmed kernel references
- sys/bus/firewire/fwdev.c:100
- sys/bus/firewire/fwdev.c:103
- sys/bus/firewire/fwdev.c:108
- sys/bus/firewire/fwdev.c:126
- sys/bus/firewire/fwdev.c:139
- sys/bus/firewire/fwdev.c:144
- sys/bus/firewire/fwdev.c:366
- sys/bus/firewire/fwdev.c:388
- sys/bus/firewire/fwdev.c:447
- sys/bus/firewire/fwdev.c:461
- sys/bus/firewire/fwdev.c:475
- sys/bus/firewire/fwdev.c:489
- sys/bus/firewire/fwdma.c:144
- sys/bus/firewire/fwdma.c:193
- sys/bus/firewire/fwdma.c:207
- sys/bus/firewire/fwdma.h:59
- sys/platform/pc64/x86_64/busdma_machdep.c:88
Detail
Exploit chain
open /dev/fw0.0 + /dev/fw0.1 (operator group) -> race FW_SSTBUF+FW_STSTREAM claims (window = fwdma_malloc_multiseg, widened at will via nchunknpacket) -> shared fw_xferq -> (a) bulkxfer heap overflow: nchunk32 bytes past a kmalloc with poffset=inpacket chosen ints + STAILQ link pointers, or (b) close-vs-write UAF: fwdma_v_addr (fwdma.h:59-66) reads am->seg[] and returns pointers into freed segment pages; spray-reclaim the 4096-byte kmalloc zone; each fw_write packet uiomoves up to 64KB (stream.len:16, attacker-set) of chosen bytes into the reclaimed objects -> overwrite reallocated kernel object (function pointer / cred-adjacent) -> uid=0 on the no-SMAP/SMEP/KASLR guest. Full uid=0 chain not constructed (entry hardware-gated in QEMU: /dev/fw needs an attached OHCI), same evidence-class standard as DF-5413/DF-5548.
Evidence (decisive lines)
run.log (test 2: 'UAF WRITE landed in spray block 15 ... at offset 11', context "....13-UAF!DF5613-UAF!DF5613-UAF", len=42432 read from the reclaimed page; test 1: 'shared=YES', 'q->bulkxfer[8].poffset = 512 ... [20] = 1280', '62 of 62 ... heap overflow CONFIRMED'), panic.txt (4GB run: Fatal trap 12 supervisor write in memmove via production fw_write on the freed segment pointer), fix_run.log (OPEN=1 inside fwdma_malloc_multiseg, d2->it != fc.it[0], shared=no overflow=0), build.log/fix_build.log, env.txt
PoC changes
Harness written fresh on the DF-5413 KLD pattern (production firewire.c+fwdma.c+fwdev.c #included with -D-renamed globals, synthetic fc/devclass/cdevs): test 1 races two STSTREAM claims (poll for q->bulkxfer set && q->buf NULL to pin fd1 inside fwdma_malloc_multiseg), test 2 occupies the proven loser-thread interleaving (cached fwdma_v_addr element pointer across production fw_close, then the fwdev.c:390-392 uiomoves into spray-reclaimed segment pages). Two earlier fully-threaded test-2 designs were replaced: the pure speed race let the writer outrun fwdma_free_multiseg (no reclaim window) or corrupt the slab freelist (wedged guest); the 4GB-RAM run of that design produced the mid-flight Fatal trap 12 supervisor write kept as panic.txt. Guest must be booted with -m 2048 for the kmalloc-backed (reclaimable) segment variant.
Verified recommended fix
Serialize and atomicize the iso-queue lifecycle: claim FWXFERQ_OPEN under crit_enter() BEFORE the sleeping fwdev_allocbuf() (clear on failure) in FW_STSTREAM/FW_SRSTREAM; clear q->stproc and re-init stvalid/stfree/stdma in fwdev_freebuf; re-check it->buf==NULL at the fw_read/fw_write loop tops (fix.diff).
Verdict
Reproduced on the QEMU guest with the unmodified production firewire.c+fwdma.c+fwdev.c compiled into KLD df5613.ko (DF-5413 pattern). Test 1: two fds racing the unsynchronized FW_STSTREAM claim both own one fw_xferq (d1->it == d2->it); the winner's chunk loop (fwdev.c:126-130) then wrote 64 fw_bulkxfer entries through the loser's 2-entry 64-byte kmalloc -- 62/62 out-of-bounds entries carried the attacker-chosen poffset=i*npacket pattern (1984-byte heap overflow). Test 2: from that shared-queue state, fd1's production fw_close -> fwdev_freebuf -> fwdma_free_multiseg (fwdma.c:207-218) freed the segment pages under fd2's in-flight fw_write; after spray reclamation of the freed 4096-byte segment page, fd2's uiomove (fwdev.c:390-392) wrote 2048 bytes of attacker iso payload into the reclaimed page ('DF5613-UAF' found in spray block 15) -- UAF write with controlled content. On a 4GB-RAM guest the same race taken mid-flight by pure production code faulted writing the stale segment pointer: Fatal trap 12, supervisor write, in fw_write's memmove (panic.txt). fix.diff (atomic claim under crit + freebuf list/stproc cleanup + loop-top rechecks) validated: patched run shows OPEN set before the sleeping allocation, fd2 claims a different queue, shared=no overflow=0.
No comments yet.