โฌข DragonFlyBSD Kernel Audit
โ† dashboard
DF-0588

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. tunopen at sys/net/tun/if_tun.c:283 gates with caps_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:C in 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 fd on /dev/tunN. Default kernel (INET compiled in).
  • Reachability: open("/dev/tunN", O_RDWR) โ†’ ioctl(fd, TUNSIFHEAD, &one) (sys/net/tun/if_tun.c:716, enables TUN_IFHEAD) โ†’ repeatedly write() 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 the default case 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.

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.c uses m_freem(top) in the equivalent default case โ€” the correct reference behavior.
  • sys/kern/uipc_mbuf.c:1469 (m_freem semantics โ€” walks m_next from its argument).

Timeline

  • 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
  • 2026-07-02 Reported to DragonFlyBSD security contact (pending).