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

fwe_stop() frees RX bulkxfer chunks and their mbufs while fwohci_rbuf_update()/fwe_as_input() still manipulate or walk them β€” teardown race causes UAF write/read and mbuf double-free

Summary

fwe_stop (under if_serializer) sleeps up to 1s inside irx_disable with FWXFERQ_OPEN/HANDLER still set, then m_freem's every chunk mbuf and kfrees bulkxfer; fwohci_rbuf_update runs in irq context with crit_enter only, moves the same chunks and calls hand=fwe_as_input which takes the serializer only inside itself - no common lock covers the transition (serializers non-recursive). Losing interleavings: STAILQ ops on freed bulkxfer (UAF write), mtod on freed mbuf, m_freem double-free of clusters, freed-mbuf delivery into if_input. Remote FW peer times the traffic; root teardown races it. FireWire-hw-gated (QEMU cannot emulate OHCI-1394). Flag-reorder + slz serializer + hand revalidation fix authored.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4762 Β· 11 files
FileTypeDescriptionSize
README.md β€” 1.6 KB ↓ raw
VERDICT.md β€” 6.7 KB ↓ raw
fwe_trigger.c β€” 2.7 KB view raw
build.sh β€” 89 B view raw
run.sh β€” 211 B view raw
build.log β€” 139 B view raw
run.log β€” 847 B view raw
env.txt β€” 904 B view raw
fix.diff β€” 3.5 KB view raw
verdict.json β€” 4.0 KB view raw
manifest.json β€” 1.0 KB view raw

DF-4762 β€” fwe_stop() RX teardown races fwohci_rbuf_update()/fwe_as_input(): kfree(bulkxfer) + m_freem() while chunks are still being queued/walked β†’ UAF write/read + mbuf double-free

File: sys/dev/netif/fwe/if_fwe.c (pass 2). Related: sys/bus/firewire/fwohci.c.

Build / Run / Expected

build.sh   # cc -O -o fwe_trigger fwe_trigger.c     (on the audit guest)
run.sh     # as root: probe fwe0 + document the ifconfig-down race driver

On hardware with an OHCI-1394 controller (fwohci0 β†’ fwe0), the full reproducer is:

  1. Peer node streams isochronous packets on the fwe stream channel (default hw.firewire.fwe.stream_ch=1), e.g. another machine doing IP-over-FireWire traffic to us.
  2. Root loop: while :; do ifconfig fwe0 up; ifconfig fwe0 down; done.
  3. Expected on a vulnerable kernel: panic (page fault in fwe_as_input walking a freed bulkxfer chunk, or m_freem() double-free of an mbuf cluster freed by fwe_stop) within seconds-to-minutes of racing.

On the audit QEMU guest there is no FireWire controller (QEMU has no OHCI-1394 device model at all), so fwe0 cannot exist β€” see run.log (exit 2, "Device not configured") and env.txt. Runtime verification is therefore blocked by absent hardware; the pack carries the full source-level proof instead (VERDICT.md).

Verdict (see VERDICT.md, verdict.json)

Not runtime-testable on this guest (hardware absent). Source-level analysis: certain race window, no common lock across the freed state; severity Medium (admin-triggered teardown racing remote peer traffic; kernel memory corruption when hit).

VERDICT.md
↓ download raw

DF-4762 VERDICT β€” fwe_stop() vs RX-completion race β†’ UAF/double-free

Status: NOT RUNTIME-TESTABLE on the audit guest (hardware absent)

The audit guest is QEMU/virtio-only (env.txt: pciconf lists no class 0x0c0010 OHCI-1394 device; ifconfig -a has no fwe0). QEMU provides no IEEE-1394 host controller model, so fwe(4) β€” which only attaches as a child of fwohci(4) (DRIVER_MODULE(fwe, firewire, ...), sys/dev/netif/fwe/if_fwe.c:619) β€” can never be instantiated here. The PoC compiles and runs on the guest and exits 2 with "fwe0 not present" (run.log), i.e. the trigger itself is sound; only the target device is missing. Per the run contract this is not_testable; persisted into the DB enum as inconclusive.

The bug (source-level proof)

Object lifetime: fwe_init() (if_fwe.c:315-357) allocates xferq->bulkxfer = kmalloc(sizeof(struct fw_bulkxfer) * bnchunk) (line 340) — an array whose entries themselves are linked onto the xferq chunk queues: STAILQ_INSERT_TAIL(&xferq->stfree, &xferq->bulkxfer[i], link) (line 352), each carrying an mbuf (bulkxfer[i].mbuf, line 349). fwohci_irx_enable() (fwohci.c:1311) moves them stfree→stdma under crit_enter() only; fwohci_rbuf_update() (fwohci.c:2132, called from the fwohci interrupt/poll context at fwohci.c:1818) moves them stdma→stvalid (fwohci.c:2167-2168) and then invokes the registered handler ir->hand(ir) (fwohci.c:2184) = fwe_as_input(), which only inside itself does lwkt_serialize_enter(ifp->if_serializer) (if_fwe.c:543) and then walks STAILQ_FIRST(&xferq->stvalid), dereferences sxfer->mbuf, writes sxfer->mbuf = m_getcl(...) (if_fwe.c:552), m_freem(m) (562), and hands m to if_input (586).

The teardown fwe_stop() (if_fwe.c:244-282) runs under the same serializer (entered by fwe_detach at 289-291, or by the SIOCSIFFLAGS ioctl path) and:

  1. line 262: fc->irx_disable(fc, fwe->dma_ch) β†’ fwohci_irx_disable (fwohci.c:1281) stops the DMA and tsleeps up to 1 s β€” but the FWXFERQ_OPEN/FWXFERQ_HANDLER guards checked by the interrupt path (fwohci.c:1813, 2183) are cleared only after it returns (if_fwe.c:263-266), so during that whole second the door is open;
  2. line 269: m_freem(xferq->bulkxfer[i].mbuf) β€” frees every chunk mbuf;
  3. line 270: kfree(xferq->bulkxfer, M_FWE) β€” frees the array itself, while stvalid/stdma (queue heads in the still-live xferq) point into it and an in-flight fwohci_rbuf_update()/blocked fwe_as_input() still references those entries.

No lock is held in common across step 2/3 and the RX-completion path: fwohci_rbuf_update() runs with only crit_enter() (a critical section, not a serializer), and fwe_as_input() acquires the serializer only after fwohci_rbuf_update() has already been manipulating the freed chunks. lwkt serializers are non-recursive (sys/sys/serialize.h:42), which is why fwe_as_input cannot be called with the serializer already held by rbuf_update β€” the code structure itself proves there is no common exclusion.

Concrete losing interleavings (either suffices):

  • (a) fwohci_rbuf_update passes the FWXFERQ_OPEN check (fwohci.c:1813) during irx_disable's 1 s sleep, executes STAILQ_REMOVE_HEAD/INSERT_TAIL on chunk->link (fwohci.c:2167-2168) concurrently with fwe_stop's kfree at if_fwe.c:270 β†’ UAF write into freed bulkxfer memory; then calls hand (2184) which blocks on the serializer held by fwe_stop, resumes after the kfree and walks stvalid (if_fwe.c:544-586): mtod(sxfer->mbuf) on a freed mbuf, sxfer->mbuf = m_getcl(...) UAF write (552), m->m_data += ... UAF write into the freed mbuf (567), m_freem(m) double-free (562 β€” the same mbuf was already freed by fwe_stop:269), and if_input(ifp, m) feeding a freed mbuf chain into the entire network stack.
  • (b) fwe_as_input is already blocked at lwkt_serialize_enter (543) while fwe_stop frees everything (269-270); on resume it walks the freed chunks β€” same UAF/double-free as (a).

Impact ceiling: double-free of mbuf clusters (system-wide allocator metadata corruption) plus controlled-offset UAF writes (sxfer->mbuf/m->m_data into freed zone memory). Triggering requires an admin action (ifconfig fwe0 down, SIOCSIFFLAGS β€” if.c gates it on root β€” or driver detach) racing inbound isochronous traffic from a peer node; the peer (attacker) fully controls the traffic timing, so the race win probability is a function of how often teardown happens under load. Not unprivileged-only β†’ severity Medium (mirrors the DF-4497 sbsh deactivate-race family).

Why not false-positive

The lock model is verifiable statically: the only serialization fwe_as_input has with fwe_stop is ifp->if_serializer, acquired at if_fwe.c:543 β€” after fwohci_rbuf_update has mutated the shared chunk queues (fwohci.c:2148-2168); fwe_stop's frees (if_fwe.c:269-270) are guarded by that serializer alone. Nothing prevents the interleavings above. Related-but-distinct from DF-1849 (unbounded read of stream.len at if_fwe.c:560-569): this finding is about freeing chunks/mbufs that completion processing still touches.

Fix validation

Not possible on this guest (no hardware): fix_status = not_testable. fix.diff is authored against the read-only tree (never applied):

  • fwe_stop(): clear FWXFERQ_OPEN|FWXFERQ_HANDLER|FWXFERQ_STREAM and NULL hand before calling irx_disable (closes the fwohci.c:1813/2183 check windows during the disable sleep).
  • firewirereg.h/fwohci.c: give fw_xferq an optional serializer (slz); fwohci_rbuf_update() moves chunks and snapshots hand under slz; fwe_init() sets xferq->slz = ifp->if_serializer.
  • fwe_as_input(): revalidate xferq->hand == fwe_as_input after acquiring the serializer and bail if torn down (protects callers that were already blocked at 543).

On hardware the validation procedure is: stream from a peer while looping ifconfig down/up; baseline kernel must panic/UAF within minutes; patched kernel must survive indefinitely (see run.sh).

At detach with in-flight TX xfers, fwe_stop frees only the idle xfers on fwe->xferlist (if_fwe.c:272-277); up to 127 in-flight xfers are later re-inserted into the (reinitialized or orphaned) list and leaked (bus_generic_detach does not free the softc β€” sys/kern/subr_bus.c:2867-2880 β€” so this is a leak, not a UAF).

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

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff authored against read-only sys/ after source verification; cannot build/validate on the guest (no OHCI-1394 hardware, fwe driver cannot attach).

fix.diff in this pack
↓ fix.diffper-fix-DF-4762

Confirmed kernel references

Detail

Exploit chain

peer streams on stream_ch while root ifconfig down: rbuf_update inserts freed bulkxfer chunk into stvalid (UAF write) / blocked fwe_as_input resumes after kfree -> m_freem() double-free of mbuf cluster (global allocator metadata corruption) plus controlled-offset UAF writes into freed zone memory; full uid0 chain not assessed - no hardware to develop it on.

Evidence (decisive lines)

VERDICT.md (interleavings + path:line proof); run.log (PoC builds BUILD_OK on guest, exits 2 'fwe0 not present'); env.txt (guest PCI/ifconfig inventory); fix.diff (3-file serializer fix)

PoC changes

Authored the trigger from scratch (no seed): if_nametoindex('fwe0') probe + SO_BROADCAST UDP flood for the TX half; the root half (ifconfig down loop) documented in run.sh. No code errors found in own draft; build succeeded first try on guest.

Verified recommended fix

Serialize RX chunk processing against teardown: clear FWXFERQ_OPEN/HANDLER and hand=NULL in fwe_stop() BEFORE irx_disable(); add fw_xferq.slz serializer taken by fwohci_rbuf_update() around chunk-queue mutation and hand snapshot; fwe_as_input() revalidates hand after acquiring if_serializer.

Verdict

NOT runtime-testable on the audit guest: fwe(4) only attaches under an OHCI-1394 FireWire controller and the QEMU guest has none (env.txt: no class 0x0c0010 PCI device; ifconfig -l has no fwe0; QEMU has no 1394 device model at all) - contract status not_testable, mapped to 'inconclusive' for the poc_results enum. Source-level proof stands: fwe_stop() (if_fwe.c:258-281) frees bulkxfer chunks and their mbufs under if_serializer only, while fwohci_rbuf_update() (fwohci.c:2148-2184, interrupt context, crit_enter() only) manipulates the same stdma/stvalid queues and then calls fwe_as_input(), which takes that serializer only after the mutation; FWXFERQ_OPEN/HANDLER guards are cleared AFTER fwohci_irx_disable()'s 1s sleep, so a completion racing ifconfig-down/detach walks freed chunks -> UAF write (sxfer->mbuf, m->m_data), m_freem double-free, and freed-mbuf delivery into if_input. Trigger needs admin teardown racing peer-controlled isochronous traffic -> Medium, memcorrupt bucket.