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

TAPSIFINFO leaks the ifnet serializer on type mismatch (local DoS / kernel wedge)

Field Value
ID DF-0585
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
CWE CWE-667 Improper Locking (lock acquired but not released on error path)
File sys/net/tap/if_tap.c
Lines 738-832
Area net
Confidence certain
Discovered 2026-07-01
Reported pending

Summary

tapioctl() acquires the per-ifnet serializer with ifnet_serialize_all() at the top of the function and releases it only on the single fall-through exit path. The TAPSIFINFO case has an early return (EPROTOTYPE) when the caller-supplied type does not match the interface type, which returns while the serializer is still held. That orphaned lock then wedges every subsequent operation on the interface โ€” including close() of the very fd used to trigger it โ€” turning a single ioctl into a permanent local denial of service.

Root cause

In tapioctl() (sys/net/tap/if_tap.c:726) the serializer is acquired unconditionally at if_tap.c:738 (ifnet_serialize_all(ifp);) and released only at if_tap.c:832 (ifnet_deserialize_all(ifp);) on the normal return path. The TAPSIFINFO handler at if_tap.c:742-748 is:

case TAPSIFINFO:
    tapp = (struct tapinfo *)data;
    if (ifp->if_type != tapp->type)
        return (EPROTOTYPE);     /* <-- line 745: leaks the serializer */
    ifp->if_mtu = tapp->mtu;
    ifp->if_baudrate = tapp->baudrate;
    break;

ifp->if_type for a tap device is IFT_ETHER (set in tapcreate()). tapp->type is an attacker-controlled u_char taken verbatim from the ioctl argument (struct tapinfo, sys/net/tap/if_tap.h:46-51). Any value != IFT_ETHER drives the early return, leaving the ifnet serializer acquired with no matching release. The serializer is the lwkt serializer installed by ether_ifattach(); it is not auto-released across the syscall return boundary. Once orphaned, the next ifnet_serialize_all() on this ifp โ€” which happens in:

  • tapclose() at if_tap.c:426
  • tapread() at if_tap.c:870
  • tapwrite() at if_tap.c:980
  • tapifioctl() at if_tap.c:538/738 (via ifconfig)
  • tapifstart() on the next TX
  • tapifstop()

โ€” blocks indefinitely. There is exactly one such early return inside the serialized section of tapioctl(); tapifioctl() (the net-iface path) has no unbalanced returns, and all other early returns in tapread/tapwrite occur either before serialization or after an explicit deserialize.

Threat model & preconditions

  • Attacker position: any process holding an open tap fd (root by default; any user if net.link.tap.user_open=1 and node perms allow).
  • Privileges gained or impact: permanent local denial of service โ€” no integrity or confidentiality impact.
  • Required config or capabilities: open fd on /dev/tapN. The fd-open privilege gate is in tapopen() at if_tap.c:323-327: with the default net.link.tap.user_open=0, opening requires caps_priv_check(SYSCAP_RESTRICTEDROOT) (root); with user_open=1 (or on a node chmod'd world-writable) an unprivileged user can open it. In practice tap fds are routinely delegated to lower-privilege processes โ€” qemu/bhyve VM processes, VPN daemons (OpenVPN, WireGuard userspace), jails/containers with a passed tap fd โ€” and any of those compromised or malicious processes can wedge the kernel.
  • Reachability: single ioctl(fd, TAPSIFINFO, &ti) with ti.type != IFT_ETHER. Concrete impact: 1. The offending fd cannot be closed (tapclose blocks at if_tap.c:426), so the process hangs in uninterruptible state on exit. 2. ifconfig tapN from root hangs (tapifioctl serialize). 3. Any RX/TX on the interface hangs (tapifstart/tapifinput path). 4. Module unload fails forever (taprefcnt never decrements to 0 because tapclose is stuck). On a multi-process system the wedge can cascade as more threads touch the interface. Recovery requires a reboot.

Proof of concept

PoC source: findings/poc/DF-0585/leak_tap_lock.c

Build & run

# on a DragonFlyBSD host/guest with the tap module loaded
cc -I/sys/net/tap -o leak_tap_lock findings/poc/DF-0585/leak_tap_lock.c
./leak_tap_lock /dev/tap0

(If the kernel include path is awkward, vendor struct tapinfo and the TAPSIFINFO _IOW('t',91,struct tapinfo) definition into the source to avoid the kernel header dependency.)

Expected output

serializer orphaned; close() will now wedge

The binary prints the "close() will now wedge" line, then never prints the final "unreachable: fd closed" line โ€” it hangs in tapclose. A separate shell running ifconfig tap0 or cat /dev/tap0 also hangs, proving the interface serializer is orphaned. ps -axl | grep leak_tap_lock shows the process stuck in tapcls/ifser. Recovery requires a reboot.

Impact

Permanent local denial of service on any system that exposes a tap fd to a process that can be compromised or that is itself malicious. Default kernels require root to open /dev/tapN, but the common deployment pattern (VM processes, VPN daemons, jails) deliberately lowers that bar. A single buggy or malicious ioctl wedges the kernel: the offending process can never exit, the interface becomes unusable, and the module cannot unload.

Do not return from inside the serialized section; route all error exits through the single ifnet_deserialize_all() at the bottom, exactly like every other case in tapioctl() and like tapifioctl() does. Convert the early return into the standard error = โ€ฆ; break; pattern so control falls through to ifnet_deserialize_all(ifp) before returning. Callers still see EPROTOTYPE; behavior is otherwise unchanged.

--- a/sys/net/tap/if_tap.c
+++ b/sys/net/tap/if_tap.c
@@ -741,8 +741,9 @@ tapioctl(struct dev_ioctl_args *ap)
    switch (ap->a_cmd) {
    case TAPSIFINFO:
        tapp = (struct tapinfo *)data;
-       if (ifp->if_type != tapp->type)
-           return (EPROTOTYPE);
+       if (ifp->if_type != tapp->type) {
+           error = EPROTOTYPE;
+           break;
+       }
        ifp->if_mtu = tapp->mtu;
        ifp->if_baudrate = tapp->baudrate;
        break;

References

  • FreeBSD rS366310 (2020-10-02) โ€” same pattern (if_tap.c TAPSIFINFO early return vs serializer), historically addressed by routing all error exits through the single deserialize.
  • DragonFlyBSD ifnet_serialize_all(9) / lwkt serializer semantics: the serializer is a recursive-spinning token that is not released across syscall return; orphaning it deadlocks every subsequent acquire.

Timeline

  • 2026-07-01 Discovered during automated file-by-file audit of sys/net/tap/if_tap.c.
  • 2026-07-01 PoC source staged under findings/poc/DF-0585/; awaiting upstream report.