β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5613

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5613 Β· 14 files
FileTypeDescriptionSize
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

  1. Claim race (root cause): FW_STSTREAM/FW_SRSTREAM (fwdev.c:447-467 / 475-496) scan fc->it[]/fc->ir[] for a queue with FWXFERQ_OPEN clear, then call the sleeping fwdev_allocbuf() (kmalloc + fwdma_malloc_multiseg() β€” milliseconds for real buffers), and only set FWXFERQ_OPEN after the allocation returns (fwdev.c:461/489). fwdev_allocbuf's own FWXFERQ_EXTBUF re-check (fwdev.c:100-101) is equally check-then-act. Two opens of different /dev/fw0.x minors that race these ioctls both claim the same struct fw_xferq.

  2. Heap overflow: both racers run fwdev_allocbuf() on the shared queue. q->bulkxfer (fwdev.c:103) and q->buf (fwdev.c:108) are overwritten without freeing the loser's allocations, and the winner's chunk loop (fwdev.c:126-130) writes b->nchunk Γ— 32-byte fw_bulkxfer entries through whichever q->bulkxfer is current β€” i.e. past the other caller's smaller kmalloc. Values are attacker-chosen: poffset = i * npacket (both from the unvalidated FW_SSTBUF bufspec), mbuf = NULL, plus STAILQ link pointers.

  3. Teardown vs in-flight xfer (the fwdma lifetime hole): once two fw_drv1 own one queue, one fd's fw_close() β†’ fwdev_freebuf() (fwdev.c:139-153) calls fwdma_free_multiseg() (fwdma.c:207-218, freeing every segment page, destroying the tag, kfree'ing am) and kfree(q->bulkxfer) while the other fd is inside fw_write() (fwdev.c:366-405) / fw_read() (fwdev.c:280-345) walking those very buffers via fwdma_v_addr() (fwdma.h:59-66). Nothing synchronizes the two paths β€” no lock, no flag the I/O loops re-check, and fwdev_freebuf doesn't even clear q->stproc or the chunk lists. The in-flight uiomove()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_write instead: Fatal trap 12 ... supervisor write ... memmove+0xbc writing 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=1 visible while fd1 is still inside fwdma_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

VERDICT.md
↓ download raw

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) bulkxfer became q->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 via npacket (poffset = i*npacket, npacket an 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); then kfree(bulkxfer) (fwdev.c:144-147). fp is 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. Note fp->mode.stream.len itself 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_close freed the contigmalloc'd segments under the hammering production fw_write and its uiomove took Fatal trap 12: page fault ... supervisor write ... in memmove writing the stale segment pointer (panic.txt; fault VA in the freed segment region, far from am). 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=1 already set while fd1 is inside fwdma_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

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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)
↓ fix.diffper-fix-DF-5613

Confirmed kernel references

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.