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).