tunwrite leaks mbuf chain on unsupported address family (m_freem(m) vs m_freem(top))
| Field | Value |
|---|---|
| ID | DF-0588 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:C/C:N/I:N/A:H |
| CWE | CWE-401 Missing Release of Memory after Effective Lifetime |
| File | sys/net/tun/if_tun.c |
| Lines | 875-882, 952-954 |
| Area | net (tun/tap driver) |
| Confidence | certain |
| Discovered | 2026-07-02 |
| Reported | pending |
Summary
In tunwrite, when TUN_IFHEAD mode is enabled and the user-supplied 4-byte
address family is anything other than AF_INET/AF_INET6, the default case
of the family switch calls m_freem(m) on the last mbuf of the chain
instead of m_freem(top) on the head. Because m_freem() walks m_next
starting from its argument and the trailing mbuf has m_next == NULL, only
that single trailing mbuf is freed β the chain head top and every
intermediate mbuf leak. The leak is unbounded and repeatable from a privileged
caller, exhausting kernel mbuf memory system-wide (including across jail /
chroot boundaries) until the kernel panics or wedges networking for all
tenants.
Root cause
tunwrite builds an mbuf chain in the loop at sys/net/tun/if_tun.c:875-882:
873: top = NULL;
874: mp = ⊤
875: while (error == 0 && uio->uio_resid > 0) {
876: m->m_len = (int)szmin(MHLEN, uio->uio_resid);
877: error = uiomove(mtod(m, caddr_t), (size_t)m->m_len, uio);
878: *mp = m;
879: mp = &m->m_next;
880: if (uio->uio_resid > 0)
881: MGET(m, M_WAITOK, MT_DATA);
882: }
Each iteration links one mbuf via *mp = m and advances mp = &m->m_next.
On the final iteration, after the last uiomove brings uio_resid to 0, the
conditional MGET at line 880-881 is not taken, so the local variable m
ends up pointing at the last mbuf linked into the chain (whose m_next is
NULL). The chain head is top.
At line 941 the code switches on the parsed address family:
941: switch (family) {
942:#ifdef INET
943: case AF_INET:
944: isr = NETISR_IP;
945: break;
946:#endif
947:#ifdef INET6
948: case AF_INET6:
949: isr = NETISR_IPV6;
950: break;
951:#endif
952: default:
953: m_freem(m); /* BUG: should be top */
954: return (EAFNOSUPPORT);
955: }
The default case at :952-955 does m_freem(m). Per
sys/kern/uipc_mbuf.c:1469, m_freem(m) walks m->m_next from its argument;
since m is the tail mbuf (m_next == NULL), only that single trailing mbuf
is freed. The chain head top and every intermediate mbuf are reachable only
through the local top, which goes out of scope on return without being freed
β a classic leak. The intended call is m_freem(top); the earlier error path
at :883-888 correctly uses m_freem(top), confirming the convention.
With uio_resid up to 65539 bytes (TUNMRU + 4, see :861-867) and MHLEN ~200
bytes per mbuf, a single write leaks up to ~326 mbufs.
Threat model & preconditions
- Attacker position: privileged local user.
tunopenat sys/net/tun/if_tun.c:283 gates withcaps_priv_check(SYSCAP_RESTRICTEDROOT)β per sys/sys/caps.h:123-132 this is always disabled in jails/chroots and amounts to genuine host root. - Privileges gained or impact: kernel memory exhaustion / DoS. Impact is
system-scoped (
S:Cin CVSS): mbuf exhaustion in the kernel affects every process and every jail on the host, not just the attacker's context, so a single privileged misbehaving process can take down networking for the whole machine. - Required config or capabilities: host root with an open
fdon/dev/tunN. Default kernel (INET compiled in). - Reachability:
open("/dev/tunN", O_RDWR)βioctl(fd, TUNSIFHEAD, &one)(sys/net/tun/if_tun.c:716, enablesTUN_IFHEAD) β repeatedlywrite()a 4-byte family βAF_INET/AF_INET6(e.g.AF_UNSPEC=0,AF_IPX, etc.) followed by up to 65531 bytes of payload. Each write hits thedefaultcase and leaks ~326 mbufs.
Proof of concept
PoC source: findings/poc/DF-0588/tun_leak.c
Build & run
cc -O2 -o tun_leak findings/poc/DF-0588/tun_leak.c ./tun_leak # as host root # in another terminal: netstat -m # watch mbuf count climb monotonically vmstat -z | grep mbuf
Expected output
netstat -m / vmstat -z mbuf count climbing monotonically (the freed tail
mbuf is recycled but the ~325 earlier mbufs per write are not). After enough
writes, the kernel reports mbuf-zone exhaustion and either panics or wedges
networking for all tenants:
mbuf zone exhausted panic: ...
Reproduces 100% of the time on a default-config DragonFly kernel.
Impact
- Blast radius: any DragonFly system that exposes
/dev/tun*to any privileged process (containers/jails on the host, VPN daemons running as root, etc.). The mbuf exhaustion is global and takes down networking for the entire host, not just the misbehaving tenant β including processes in other jails that share the host's network stack. - Severity rationale: Medium. Reliable and trivially triggerable by a privileged caller, system-scoped DoS impact; no code execution or info leak.
- Reliability: 100% β the leak is on a straight-line code path with no timing dependency.
Recommended fix
Free the chain head top in the default case, exactly as the earlier
error path at line 885 does.
--- a/sys/net/tun/if_tun.c
+++ b/sys/net/tun/if_tun.c
@@ -950,7 +950,7 @@ tunwrite(struct dev_write_args *ap)
break;
#endif
default:
- m_freem(m);
+ m_freem(top);
return (EAFNOSUPPORT);
}
top is the chain head set at :873/:878 and is the correct argument.
(Optional cleanup: also bump IFNET_STAT_INC(ifp, ierrors, 1) before returning
for parity with the other drop paths at :886, but that is accounting
cosmetics, not a security fix.)
References
- FreeBSD
if_tun.cusesm_freem(top)in the equivalent default case β the correct reference behavior. - sys/kern/uipc_mbuf.c:1469 (
m_freemsemantics β walksm_nextfrom its argument).
Timeline
- 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
- 2026-07-02 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0588 Β· 19 files| File | Type | Description | Size | |
|---|---|---|---|---|
| tun_leak.c | trigger-source | minimal tunwrite mbuf-chain-leak trigger; vendored TUNSIFINFO/TUNMRU; opens /dev/tun clone; configurable write count; ENOBUFS detection | 3.6 KB | view raw |
| build.sh | build-script | cc -Wall -O2 -o tun_leak tun_leak.c | 188 B | view raw |
| run.sh | run-script | measure-before/after harness (awk quoting for vm.sh) | 930 B | view raw |
| run_local.sh | run-script | phase-8 measure harness (self-contained, runs in /root on guest); reports leaked mbufs/write | 655 B | view raw |
| build.log | build-log | final successful PoC build (clean) | 31 B | view raw |
| run.log | run-log | 50-write demo on #0: 4 -> 13203 mbufs (~264/write) | 1.1 KB | view raw |
| run.2.log | run-log | 100-write continuation on #0: 13204 -> 39603 (unreclaimed) | 1.0 KB | view raw |
| run.stress.log | run-log | stress to exhaustion on #0: 39603 -> 76247, 12 memory denials | 1.0 KB | view raw |
| exhaustion.txt | panic-signature | boot.log 'Warning: objcache(mbuf) exhausted on cpu1!' | 51 B | view raw |
| baseline_repro.log | run-log | PHASE 8 BEFORE: clean #0 baseline repro, 7->13207 (50w) then 13207->39607 (100w), ~264/write, monotonic/unreclaimed | 1.0 KB | view raw |
| fix.diff | suggested-fix | m_freem(m) -> m_freem(top) at if_tun.c:953 (free chain head not tail); patch -p1 --forward: Hunk #1 succeeded at 950 | 224 B | view raw |
| fix_build.log | build-log | PHASE 8 single-fix kernel build: make -j6 nativekernel KERNCONF=X86_64_GENERIC on patched /usr/src, rc=0, if_tun.c recompiled, kernel.stripped relinked | 5.6 MB | β download |
| fix_run.log | run-log | PHASE 8 AFTER: same PoC on #1 patched kernel, 50/50/200 writes all keep mbufs flat at 7 (delta=0). Leak gone. | 1.3 KB | view raw |
| fix_env.txt | environment | PHASE 8 patched #1 kernel env: uname, kern.version (#1 06:56:05), cc 8.3, nmbclusters=33296, /dev/tun 0600 uucp:dialer, baseline 7 mbufs, live if_tun.c:953 = m_freem(top) | 608 B | view raw |
| env.txt | environment | original verification env (prior #1 build): uname, cc 8.3, kldstat (tun built-in), /dev/tun perms, nmbclusters, baseline mbufs | 979 B | view raw |
| VERDICT.md | verdict | REPRODUCED + PHASE 8 fix-validated: mechanism, measured leak table (#0), flat-mbuf table (#1), build-install-reboot notes | 7.8 KB | β raw |
| README.md | readme | build/run/expected + root-cause + reachability + verifier notes | 4.3 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0588 β PoC evidence pack
Reproduces the mbuf-chain leak in tunwrite() at sys/net/tun/if_tun.c. A
confirmed privileged local resource-exhaustion DoS that leaks ~263 mbufs
per write and drives the kernel mbuf zone to exhaustion system-wide.
Root cause (confirmed in source)
tunwrite() builds an mbuf chain in the loop at sys/net/tun/if_tun.c:875-882:
869: /* get a header mbuf */
870: MGETHDR(m, M_WAITOK, MT_DATA); /* m = chain head */
...
874: mp = ⊤
875: while (error == 0 && uio->uio_resid > 0) {
876: m->m_len = (int)szmin(MHLEN, uio->uio_resid);
877: error = uiomove(mtod(m, caddr_t), (size_t)m->m_len, uio);
878: *mp = m; /* append m */
879: mp = &m->m_next;
880: if (uio->uio_resid > 0)
881: MGET(m, M_WAITOK, MT_DATA); /* get NEXT mbuf only if more data */
882: }
On the last iteration uio->uio_resid reaches 0, so the MGET at line 881 is
not taken and m is left pointing at the last (tail) mbuf of the chain,
while top is the head. Then, when TUN_IFHEAD is set and the user-supplied
4-byte address family is anything other than AF_INET/AF_INET6, the default
case at lines 952-955:
952: default:
953: m_freem(m); /* BUG: frees only the tail mbuf */
954: return (EAFNOSUPPORT);
955: }
calls m_freem(m) β freeing only the trailing mbuf β and leaks the chain head
top plus all intermediate mbufs (since m->m_next == NULL at the tail,
m_freem(m) frees exactly one mbuf). The chain is reachable only from the
local top, which goes out of scope on return, so the leak is permanent.
A 65539-byte write (max, TUNMRU+4) assembles ~264 mbufs, freeing 1 and
leaking ~263 per write (measured).
Trigger
open("/dev/tun", O_RDWR)(clone β fresh tunN).ioctl(fd, TUNSIFHEAD, &one)β enable the IFHEAD address-family prefix.write(fd, buf, 65539)where the first 4 bytes are a family β AF_INET(2)/ AF_INET6(28) (e.g.0xffffffff).write()returnsEAFNOSUPPORTafter leaking ~263 mbufs.- Loop. The mbuf count climbs monotonically and unreclaimed until the objcache is exhausted system-wide.
Reachability / privilege (verified)
/dev/tunclone node is0600UID_UUCP/GID_DIALER(if_tun.c:155-157).tunopen()(if_tun.c:283) requirescaps_priv_check(SYSCAP_RESTRICTEDROOT)with nouser_openescape (unlike tap). So this is host-root-only.maxx(uid 1001) cannot open/dev/tunβ matches CVSS PR:H.- Impact is S:C: the mbuf zone is kernel-global, so a privileged trigger exhausts mbufs for every tenant / network stack on the box.
Files
tun_leak.cβ minimal reproducer (vendoredTUNSIFINFO/TUNMRU; opens the clone/dev/tun; configurable write count).build.sh/run.shβ build & measure-before/after harness.build.log,run.log(50-write demo),run.2.log(100-write continuation, proves unreclaimed),run.stress.log(200-write exhaustion),exhaustion.txt(kernelobjcache(mbuf) exhaustedwarning).env.txt,VERDICT.md,fix.diff,manifest.json.
Build & run (as host root on the DragonFly guest)
./build.sh ./run.sh 50 # ~16k leaked mbufs, safe demo ./run.sh 1000000 # stress: drive to mbuf exhaustion (guest will wedge)
Expected result (bug present)
Controlled demo (50 writes):
[baseline] mbufs in use: 4 ... tun_leak: 50 writes ... [after] mbufs in use: 13203 [delta] leaked mbufs: 13199 over 50 writes (~263 mbufs/write)
Stress to exhaustion:
76247/72904 mbufs in use (current/max): <-- over nominal max 12 requests for memory denied boot.log: Warning: objcache(mbuf) exhausted on cpu1!
After exhaustion the guest's networking is degraded system-wide (new mbuf
allocations denied). Recovery: vm.sh reset (reboot).
Notes for the per-PoC verifier (done)
- tun is built into
X86_64_GENERIC(nokldloadneeded;net.link.tunsysctl present,/dev/tunexists on clean boot). - The seeded PoC opened
/dev/tun3(non-existent) and#included<net/if_tun.h>(not shipped in guest/usr/include); both fixed. - The leak is straight-line, no race; reproduces 100 % of the time.
- After the fix (
m_freem(top)), the same workload keeps the mbuf count flat.
DF-0588 β VERDICT
Verdict: REPRODUCED (privileged local resource-exhaustion DoS, S:C)
The bug, confirmed in source
tunwrite() in sys/net/tun/if_tun.c assembles an mbuf chain whose head is
top. The loop at lines 875-882 only allocates the next mbuf (line 881) when
more data remains, so on the final iteration β when uio->uio_resid hits 0 β
the MGET is not taken and the local m is left pointing at the last
(tail) mbuf of the chain, not the head:
870: MGETHDR(m, M_WAITOK, MT_DATA); /* m = chain head initially */
874: mp = ⊤
875: while (error == 0 && uio->uio_resid > 0) {
...
878: *mp = m; /* append m to chain */
879: mp = &m->m_next;
880: if (uio->uio_resid > 0)
881: MGET(m, M_WAITOK, MT_DATA); /* NOT taken on last iter */
882: }
After the loop: top = chain head; m = chain tail (m->m_next == NULL).
When TUN_IFHEAD is set and the user 4-byte family is not AF_INET/AF_INET6,
the default case frees the wrong variable:
952: default:
953: m_freem(m); /* BUG: frees only the tail (1 mbuf) */
954: return (EAFNOSUPPORT);
955: }
m_freem(m) walks m->m_next, which is NULL at the tail, so it frees exactly
one mbuf. The chain head top and every intermediate mbuf are leaked. top is
a local that goes out of scope on return, so the leak is permanent.
Trigger & proof
open("/dev/tun", O_RDWR)β clone creates a tun interface.ioctl(fd, TUNSIFHEAD, &one)β enable IFHEAD.write(fd, buf, 65539)withbuf[0..3] = 0xffffffff(family β AF_INET/6) βEAFNOSUPPORT, ~263 mbufs leaked.- Loop; watch
netstat -m.
Measured (mbufs-in-use, netstat -m):
| run | writes | before | after | Ξ | mbufs/write |
|---|---|---|---|---|---|
| run #1 | 50 | 4 | 13203 | 13199 | ~264 |
| run #2 | 100 | 13204 | 39603 | 26399 | ~264 |
| stress | ~137 | 39603 | 76247 | 36644 | ~267 |
Run #2 starting at 13204 (the leftover from run #1, not reclaimed) and climbing by the same ~264/write proves the leak is monotonic and unreclaimed β the kernel never frees the leaked chain heads.
Stress to exhaustion:
76247/72904 mbufs in use (current/max): <- over the nominal max 12 requests for memory denied boot.log: Warning: objcache(mbuf) exhausted on cpu1!
The objcache(mbuf) exhausted kernel warning + requests for memory denied
confirm system-wide mbuf exhaustion: a privileged local user can drain the
global mbuf zone and degrade networking for every tenant β the S:C impact
in the CVSS vector.
Impact
- Class: CWE-401 (resource leak / missing release) β resource-exhaustion DoS.
- Effect: each unsupported-family write leaks ~263 mbufs; ~280 such writes
exhaust a default
nmbclusters=16912/ 72904-mbuf system, after which new mbuf allocations are denied system-wide (objcache(mbuf) exhausted). The triggeringwrite()itself eventually blocks inM_WAITOKonce the zone is empty. Recovery requires a reboot. - Privilege: host-root only.
tunopen()(if_tun.c:283) requirescaps_priv_check(SYSCAP_RESTRICTEDROOT)with no escape;/dev/tunis0600 uucp:dialer. Matches CVSSPR:H. (UnprivilegedmaxxgetsEACCES.) - No memory corruption (no overflow/UAF) β no exploit chain beyond DoS.
Impact is
dos(system-scoped resource exhaustion).
PoC changes from the seeded version
The seeded tun_leak.c had two bugs that prevented it from working:
1. It opened /dev/tun3, which does not exist (tun uses the /dev/tun clone
device). Fixed to open /dev/tun.
2. It #include <net/if_tun.h>, which is not shipped in the guest's
/usr/include. Vendored TUNSIFINFO (_IOW('t', 96, int)) and TUNMRU
(65535) directly.
Also added a configurable write count (default 50 for a safe demo), an
ENOBUFS/ENOMEM detection branch for the stress-to-exhaustion case, and a
run.sh that measures netstat -m before/after to quantify the leak per write.
Recommended fix
Free the chain head top, not the tail m. One line at if_tun.c:953:
default:
m_freem(top); /* was: m_freem(m) */
return (EAFNOSUPPORT);
This frees the entire assembled chain. See fix.diff (git apply --check
passes). After the fix, the same 50/100/200-write workload keeps the mbuf count
flat.
PHASE 8 β Fix validation on a single-fix kernel (verified)
The m_freem(m) β m_freem(top) fix was validated end-to-end by building a
single-fix kernel from the audited /usr/src tree (warm obj), installing
it, rebooting, and re-running the identical PoC workload.
Setup
- Base guest: DragonFly 6.5-DEVELOPMENT
#0(unpatched audit-source kernel,with-srcsnapshot β full/usr/src+ warm/usr/obj/usr/src/sys/X86_64_GENERIC). - Patch applied: ONLY
findings/poc/DF-0588/fix.diffviacd /usr/src && patch -p1 --forward < /root/fix.diffβHunk #1 succeeded at 950.(verified:sed -n 952,954pshowsm_freem(top)). - Build:
make -j6 nativekernel KERNCONF=X86_64_GENERICβrc=0,>>> Kernel build for X86_64_GENERIC completed. Onlyif_tun.crecompiled (warm obj); full build log infix_build.log. - Install: copied
kernel.strippedβ/boot/kernel/kernel(the file the loader actually boots β/boot/kernel/kernel.strippedalone is not loaded), pluskernel.debug. (First boot attempt used only.stripped/.debugand silently kept the old#0; the loader boots the bare namekernel.) - Booted kernel:
DragonFly 6.5-DEVELOPMENT #1: Thu Jul 2 06:56:05 UTC 2026(build timestamp matches the rebuiltkernel.debug).
Before (unpatched #0) β baseline_repro.log
| run | writes | before | after | Ξ | mbufs/write |
|---|---|---|---|---|---|
| A | 50 | 7 | 13207 | 13200 | 264 |
| B (cont) | 100 | 13207 | 39607 | 26400 | 264 |
Leak is monotonic and unreclaimed (run B starts from run A's terminal count).
After (single-fix #1) β fix_run.log
| run | writes | before | after | Ξ | mbufs/write |
|---|---|---|---|---|---|
| A | 50 | 7 | 7 | 0 | 0 |
| B | 50 | 7 | 7 | 0 | 0 |
| C | 200 | 7 | 7 | 0 | 0 |
Mbuf count stays perfectly flat across 300 total writes. The leak is gone.
Classification
fix_status = fixedβ baseline climbed at 264 mbufs/write; patched kernel leaks 0 across 50/50/200-write runs (identical workload, identical PoC binary).fix_kernel_unameβDragonFly 6.5-DEVELOPMENT #1: Thu Jul 2 06:56:05 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64.- The fix is the exact one-line change in the finding markdown's
## Recommended fix; the runner'sfix.diffmatches the finding proposal (no supersede needed). - No regressions observed: patched kernel boots clean, networking normal, no
boot.logwarnings or panics during the test workload.
Notes for the build-install-reboot workflow (template for future findings)
- The loader boots
/boot/kernel/kernel, NOTkernel.stripped. A single-fix install must overwrite/boot/kernel/kernel(copykernel.strippedthere). kern.versionis the authoritative "did the new kernel boot" check β the#Nsuffix and build timestamp change with each link.- Warm-obj
nativekernelfor a.c-only fix is fast (~3-4 min wall here despite a full module sweep); background it (&) and pollkill -0 $(cat nk.pid)+grep NK_DONE, because the backgrounded child holds the ssh channel open.
Fix verification
fixedFIXED. The single-fix kernel (m_freem(m) -> m_freem(top) at if_tun.c:953, only change) eliminates the leak: baseline #0 climbed 7 -> 13207 -> 39607 (~264 mbufs/write, monotonic); patched #1 stayed flat at 7 mbufs across 50+50+200 writes (delta=0 each). Same PoC binary, same workload, opposite result. fix.diff matches the finding markdown proposal; git apply --check and patch --dry-run both pass.
BEFORE (#0): [baseline] 7 -> [after 50w] 13207 (delta 13200, ~264/w); continuation [baseline] 13207 -> [after 100w] 39607 (delta 26400, ~264/w). AFTER (#1, kern.version 'DragonFly 6.5-DEVELOPMENT #1: Thu Jul 2 06:56:05 UTC 2026'): RUN A 50w 7->7 delta=0; RUN B 50w 7->7 delta=0; RUN C 200w 7->7 delta=0. Patched if_tun.c:953 live = m_freem(top). Build: make -j6 nativekernel rc=0, if_tun.o rebuilt 06:54, kernel.stripped relinked 06:56. boot.log clean (no panic/warning).
Confirmed kernel references
Detail
Exploit chain
none - this is a pure resource-exhaustion DoS (CWE-401), not memory corruption. No overflow/UAF/type-confusion, so there is no primitive to chain to code execution or uid0. Impact ceiling is system-scoped mbuf-zone exhaustion: a privileged (host-root, tunopen requires SYSCAP_RESTRICTEDROOT) caller drains the global mbuf zone (~264 mbufs/write, ~280 writes to exhaust 72904-mbuf default) after which new mbuf allocations are denied system-wide (objcache(mbuf) exhausted), degrading networking for every tenant on the host. Recovery requires a reboot.
Evidence (decisive lines)
BEFORE (#0 unpatched, baseline_repro.log): 7 -> 13207 mbufs over 50 writes (~264/write); continuation 13207 -> 39607 over 100 more (monotonic/unreclaimed). AFTER (#1 single-fix kernel, fix_run.log): kern.version='DragonFly 6.5-DEVELOPMENT #1: Thu Jul 2 06:56:05 UTC 2026'; RUN A 50w: 7->7 delta=0; RUN B 50w: 7->7 delta=0; RUN C 200w: 7->7 delta=0. Boot log clean, no panics/warnings on either kernel. fix.diff: m_freem(m) -> m_freem(top) at if_tun.c:953; git apply --check + patch --dry-run both pass.
PoC changes
No changes to the trigger (tun_leak.c) or fix.diff - both were already correct from the prior runner. Added run_local.sh (self-contained measure harness that survives vm.sh's awk-quoting), baseline_repro.log (clean #0 BEFORE evidence), fix_build.log (full single-fix nativekernel build), fix_run.log (#1 AFTER evidence, 50/50/200-write flat-mbuf runs), fix_env.txt (#1 kernel env), and a PHASE 8 section in VERDICT.md. Updated manifest.json with fix_status/fix_baseline_reproduced/fix_patched_reproduced/fix_kernel_uname and the new artifacts.
Verified recommended fix
matches finding proposal. Change m_freem(m) -> m_freem(top) at sys/net/tun/if_tun.c:953 in the default case of the family switch, freeing the chain head assembled at :873/:878 instead of the tail m (whose m_next==NULL, so m_freem(m) freed exactly one mbuf and leaked the rest). Verified correct against the audited source (line 953 confirmed) and proven effective: the single-fix kernel keeps mbufs flat where the unpatched kernel leaks ~264/write. The full git-apply-able diff lives in findings/poc/DF-0588/fix.diff (git apply --check + patch --dry-run pass).
Verdict
REPRODUCED on the unpatched #0 audit-source kernel and FIX VALIDATED on a single-fix #1 kernel built from the same tree. Bug: in tunwrite() at sys/net/tun/if_tun.c:953, with TUN_IFHEAD set and a family != AF_INET/AF_INET6, the default case calls m_freem(m) on the chain TAIL (m->m_next==NULL after the loop at :875-882) instead of m_freem(top) on the head, leaking the entire chain head + intermediates. Measured on #0: mbufs climb 7 -> 13207 over 50 writes (~264 leaked/write) and monotonically 13207 -> 39607 over 100 more (unreclaimed), reproducible 100%. The one-line fix (m_freem(m) -> m_freem(top) at :953, exactly the finding proposal) was applied via patch -p1 (Hunk #1 succeeded at 950), a single-fix kernel was built (make -j6 nativekernel, rc=0, warm obj), installed, and booted as #1 06:56:05 UTC 2026. The identical PoC workload on #1 keeps mbufs perfectly flat (7 -> 7 across 50/50/200-write runs, delta=0). fix_status=fixed.
No comments yet.