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

struct tlabel leaked on every split-transaction timeout (firewire_xfer_timeout unlinks without kfree)

Summary

The timeout STAILQ_REMOVE_HEADs the tlabel without kfree and fw_tl_free only frees entries still in the list - every timed-out split transaction leaks one 16-byte M_FW allocation (operator user + non-answering node = ~1MB/day sustained, no ceiling). Guest-measured via harness malloc-type counters: 5000/5000 leaked (80,000 bytes); fix validated delta 0/0. kfree-after-remove fix (carried in the DF-5321 diff).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5322 Β· 13 files
FileTypeDescriptionSize
df5322.c β€” 16.7 KB view raw
Makefile β€” 1.3 KB ↓ download
build.sh β€” 161 B view raw
run.sh β€” 172 B view raw
run.log β€” 1.4 KB view raw
fix.diff β€” 6.8 KB view raw
fix_build.log β€” 6.5 KB view raw
fix_run.log β€” 662 B view raw
env.txt β€” 417 B view raw
VERDICT.md β€” 3.0 KB ↓ raw
README.md β€” 2.1 KB ↓ raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 3.1 KB view raw

DF-5322 β€” struct tlabel leaked on every split-transaction timeout

File: sys/bus/firewire/firewire.c:348 (firewire_xfer_timeout()) Severity: Low Β· Bucket: kernleak Β· Confidence: certain Status: reproduced (5000 objects / 80000 bytes leaked over 5000 timeouts) on the QEMU guest via the production code path; fix validated (delta 0).

Root cause

firewire_xfer_timeout() unlinks an expired tlabel with STAILQ_REMOVE_HEAD(&fc->tlabels[i], link) (firewire.c:348) but never frees it. The only kfree of a struct tlabel in the file is in fw_tl_free() (firewire.c:900), which frees an entry only when it still finds it in the list. After the timeout removed it:

  • kernel-internal xfers: fw_xfer_done() -> fw_asy_callback_free -> fw_xfer_free -> fw_xfer_unload -> fw_tl_free() finds nothing;
  • /dev/fw FW_ASYREQ xfers: the waiter's fw_xfer_free_buf() -> same fw_tl_free() β€” finds nothing.

So every split-transaction timeout leaks exactly one sizeof(struct tlabel) (= 16 bytes on x86_64) allocation from M_FW.

Trigger

Any local user permitted to use /dev/fwN.M can create these at will: FW_ASYREQ a read to a node that never answers (or pull the cable) β€” each transaction times out after 200 ms and leaks 16 bytes (~1 MB/day at the natural retry rate; unbounded in aggregate with kernel-internal fwmem traffic added on busy buses).

What to run here (guest, root)

./build.sh      # df5322.ko: compiles the PRODUCTION firewire.c in
sh run.sh       # mode 0: 5000 timeouts through the production
                # firewire_xfer_timeout() + fw_xfer_free() paths

Baseline output (run.log):

DF5322: 5000 split-transaction timeouts
DF5322: M_FW inuse delta = 5000 objects, memuse delta = 80000 bytes
DF5322: LEAK REPRODUCED: ~1 tlabel leaked per timeout

Fixed (fix.diff applied to /usr/src, module rebuilt, fix_run.log):

DF5322: M_FW inuse delta = 0 objects, memuse delta = 0 bytes

fix.diff is shared with DF-5321 (the timeout now kfrees the tlabel it unlinks, plus the tlabel-lock/CAS rework); see ../DF-5321/VERDICT.md for the full fix-validation narrative.

VERDICT.md
↓ download raw

DF-5322 β€” verdict

Status: reproduced (measured kernel memory leak, production path) Impact: leak (unbounded, slow kernel memory exhaustion) Confidence: certain

Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0 X86_64_GENERIC (6 vCPU, no FireWire hardware). Evidence class: KLD harness (DF-5230/DF-5302 pattern) compiling the unmodified production firewire.c into the module; mode 0 drives exactly the production sequence of a split-transaction timeout:

fw_xfer_alloc()                    /* the FW_ASYREQ/fwmem xfer        */
fw_get_tlabel()                    /* inserts the tlabel (M_FW)       */
firewire_xfer_timeout()            /* 10 Hz watchdog: unlinks the
                                       tlabel at firewire.c:348
                                       WITHOUT kfree, completes xfer  */
fw_xfer_free()                     /* owner-side free: fw_tl_free()
                                       scans the list, finds nothing,
                                       never kfrees the tlabel        */

The harness measures the renamed-but-identical M_FW malloc type in-kernel (ks_use[cpu].inuse/memuse, summed over all CPUs) across 5000 iterations (zones prewarmed; no other M_FW activity).

Result (baseline, unpatched production source)

DF5322: 5000 split-transaction timeouts
DF5322: M_FW inuse delta = 5000 objects, memuse delta = 80000 bytes
        (each leaked struct tlabel = 16 bytes)
DF5322: LEAK REPRODUCED: ~1 tlabel leaked per timeout

Exactly one leaked 16-byte struct tlabel per timed-out transaction β€” matching the static analysis: the only kfree site is fw_tl_free() (firewire.c:900) and it requires list membership, which firewire_xfer_timeout() destroyed at firewire.c:348.

An independent confirmation is the panic in the sibling DF-5321 runs: the watchdog dereferenced tlabels freed by other paths, proving these objects' lifetime is not tracked anywhere after the timeout unlinks them.

Exploitability

Memory-exhaustion DoS only (16 bytes per timed-out transaction, attacker-paced, M_FW unbounded). No information disclosure, no corruption from this defect alone.

Fix validation

fix.diff (shared with DF-5321) adds kfree(tl, M_FW) for the timeout-unlinked tlabel inside the new fc->tlabel_lock critical region. Applied to the guest /usr/src, module rebuilt, identical trigger:

DF5322: 5000 split-transaction timeouts
DF5322: M_FW inuse delta = 0 objects, memuse delta = 0 bytes

fix_status: fixed.

Bottom line

Certain, production-path, unbounded-but-slow kernel memory leak, reproduced and measured on the audit guest, fixed by a one-line kfree (carried inside the DF-5321 serialization fix).

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

Baseline: 5000 objects / 80000 bytes leaked over 5000 timeouts via the production path. Patched (fix.diff on /usr/src, module rebuilt, identical trigger): delta 0 objects / 0 bytes.

fix_run.log, fix_build.log, fix.diff
↓ fix.diffper-fix-DF-5322

Confirmed kernel references

Detail

Exploit chain

open /dev/fwN.M (operator group) -> FW_ASYREQ to a non-answering node (or kernel fwmem traffic on a busy bus) -> each 200 ms split timeout leaks one M_FW tlabel -> unbounded slow kernel memory exhaustion.

Evidence (decisive lines)

run.log (baseline: 'M_FW inuse delta = 5000 objects, memuse delta = 80000 bytes ... LEAK REPRODUCED'), fix_run.log ('delta = 0 objects, memuse delta = 0 bytes'), VERDICT.md

PoC changes

Harness is the DF-5321 module (df5322.c byte-identical to df5321.c; KMOD renamed df5322, sysctl names unchanged 'df5321_*'); mode 0 measures the renamed M_FW malloc type in-kernel (ks_use[cpu].inuse/memuse summed) around the production timeout+free sequence with prewarmed zones.

Verified recommended fix

kfree(tl, M_FW) after STAILQ_REMOVE_HEAD in firewire_xfer_timeout() (carried inside the DF-5321 fix.diff serialization rework).

Verdict

Reproduced and measured on the QEMU guest with the production firewire.c compiled into a KLD harness: every split-transaction timeout leaks exactly one 16-byte struct tlabel because firewire_xfer_timeout() (firewire.c:348) unlinks the tlabel without kfree and the only free site, fw_tl_free() (firewire.c:900), requires list membership. 5000 timeouts -> M_FW inuse delta 5000 objects / 80000 bytes (baseline); 0/0 with fix.diff applied to /usr/src and the module rebuilt. Unbounded attacker-paced kernel memory exhaustion (DoS); no disclosure or corruption from this defect alone.