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

tunwrite() leaks one mbuf per EFAULTing write: spare MGET allocated before the loop recheck is never freed (tap DF-3406 twin) β€” verified, deterministic

Summary

The mbuf-assembly loop in tunwrite pre-allocates the next mbuf after linking the current one. When uiomove faults with uio_resid still > 0, the freshly MGET'd mbuf is never linked and m_freem(top) cannot reach it - exactly one mbuf leaks per faulted write. VERIFIED deterministically: 30000 faulted writes moved mbufs-in-use 8 -> 30007 (+1/write); control with readable pages flat; patched kernel flat. Privileged (SYSCAP_RESTRICTEDROOT tunopen); exhausts the kernel mbuf pool system-wide with ~nmbclusters faulted writes, wedging networking for every tenant - same shape as the known DF-0588 in this file. Fix validated (m_freem(m) on the error path).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3408 Β· 12 files
FileTypeDescriptionSize
tun_eault_leak.c β€” 2.4 KB view raw
build.sh β€” 108 B view raw
run.sh β€” 137 B view raw
build.log β€” 77 B view raw
run.log β€” 219 B view raw
run.control.log β€” 120 B view raw
run.fix.log β€” 262 B view raw
env.txt β€” 654 B view raw
fix.diff β€” 234 B view raw
fix_kernel_info.txt β€” 1.1 KB view raw
VERDICT.md β€” 2.4 KB ↓ raw
verdict.json β€” 3.1 KB view raw
VERDICT.md
↓ download raw

DF-3408 β€” VERDICT

Status: REPRODUCED (deterministic resource leak, +1 mbuf per faulted write); fix validated.

Baseline

  • Guest: DragonFly 6.5-DEVELOPMENT #0 X86_64_GENERIC.
  • PoC tun_eault_leak.c: open("/dev/tun"), then repeated 8192-byte write()s of a buffer whose second page is mprotect(PROT_NONE). The first page copies through uiomove(); the fault on page 2 returns EFAULT with uio_resid > 0, which is precisely the leak condition.

Run 1 (run.log):

BEFORE: 8/146632 mbufs in use
writes: ok=0 faulted=30000
AFTER:  30007/146632 mbufs in use     (+29999 β‰ˆ +1/write)

Control (run.control.log): 20000 writes with both pages readable β€” mbuf count flat (30007 before and after). The leak is specific to the uiomove-EFAULT error path, not to write volume.

Root cause (line-accurate)

sys/net/tun/if_tun.c:875-882: the trailing MGET on :880-881 executes in the same iteration that linked the previous mbuf; on a faulting iteration the loop exits before the spare is ever linked by *mp = m (:878), and the error path m_freem(top) (:884-885) frees only the linked chain. In the error branch m is provably always the unlinked spare: uiomove cannot fail while driving uio_resid to 0, so error != 0 implies uio_resid > 0 implies the MGET ran in the final iteration. Identical shape to tap(4) DF-3406.

Impact

Unbounded kernel mbuf leak (memory-exhaustion DoS) repeatable by a privileged caller β€” tunopen() is gated by caps_priv_check(SYSCAP_RESTRICTEDROOT) (if_tun.c:283). With kern.ipc.nmbclusters=33296 on this guest, ~33k faulted writes exhaust the cluster pool; the leak is otherwise unbounded across the lifetime of the open fd. No unprivileged reachability, so no escalation chain β€” root-gated by construction.

Fix validation

fix.diff adds m_freem(m) (the spare) to the error branch. Combined into the same patched kernel as DF-3407's fix, rebuilt, rebooted, and the exact PoC re-run (run.fix.log): 30000 faulted writes leave the mbuf count flat. Baseline leak vs patched flat = fix confirmed.

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 (m_freem(m) of the unlinked spare in tunwrite's error branch) built into the validated kernel (kernel #1 09:05:29, same build as DF-3407 v3): 30000 faulted writes leave mbuf count flat (8 -> 7, noise) vs +29999 on baseline. Fix confirmed.

run.fix.log; fix.diff; fix_kernel_info.txt
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Sat Sep 12 09:05:29 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

root: open /dev/tun, write() a buffer whose second page is PROT_NONE -> uiomove EFAULT with resid>0 -> spare MGET leaked -> repeat ~kern.ipc.nmbclusters times -> mbuf/cluster pool exhaustion -> networking DoS system-wide.

Evidence (decisive lines)

['run.log β€” BEFORE 8/146632 mbufs, 30000 faulted writes, AFTER 30007/146632', 'run.control.log β€” control: 20000 successful writes, count flat 30007->30007', 'run.fix.log β€” patched kernel: 30000 faulted writes, count flat', 'fix.diff β€” one-line m_freem(m) for the unlinked spare', 'VERDICT.md β€” full narrative']

PoC changes

PoC written fresh for this finding (no seed). None needed after first build.

Verified recommended fix

Free the unlinked spare mbuf on the uiomove error path: add m_freem(m) beside m_freem(top) in tunwrite's error branch.

Verdict

Reproduced deterministically: 30000 EFAULTing 8KB writes to /dev/tun moved 'mbufs in use' from 8 to 30007 (exactly +1 mbuf per faulted write); a control run with both pages readable left the count flat (30007 before and after). Root cause: tunwrite's mbuf loop (if_tun.c:875-882) pre-allocates the next MGET (:880-881) which is never linked when uiomove faults with uio_resid>0, and the error path frees only the linked chain (m_freem(top), :884-885). Privileged-only (tunopen is SYSCAP_RESTRICTEDROOT-gated, if_tun.c:283): a root-triggered kernel mbuf-exhaustion primitive. fix.diff (m_freem(m) of the spare) built into the patched kernel and validated: same PoC leaves the mbuf count flat. Fix validated: same PoC on the patched kernel leaves the mbuf count flat (8->7 over 30000 faulted writes).