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)
PoC verification
Evidence pack
findings/poc/DF-3408 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
DF-3408 β VERDICT
Status: REPRODUCED (deterministic resource leak, +1 mbuf per faulted write); fix validated.
Baseline
- Guest: DragonFly 6.5-DEVELOPMENT
#0X86_64_GENERIC. - PoC
tun_eault_leak.c:open("/dev/tun"), then repeated 8192-bytewrite()s of a buffer whose second page ismprotect(PROT_NONE). The first page copies throughuiomove(); the fault on page 2 returns EFAULT withuio_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
fixedfix.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
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).
No comments yet.