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()atif_tap.c:426tapread()atif_tap.c:870tapwrite()atif_tap.c:980tapifioctl()atif_tap.c:538/738(viaifconfig)tapifstart()on the next TXtapifstop()
โ 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=1and 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 intapopen()atif_tap.c:323-327: with the defaultnet.link.tap.user_open=0, opening requirescaps_priv_check(SYSCAP_RESTRICTEDROOT)(root); withuser_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)withti.type != IFT_ETHER. Concrete impact: 1. The offending fd cannot be closed (tapcloseblocks atif_tap.c:426), so the process hangs in uninterruptible state on exit. 2.ifconfig tapNfrom root hangs (tapifioctlserialize). 3. Any RX/TX on the interface hangs (tapifstart/tapifinputpath). 4. Module unload fails forever (taprefcntnever decrements to 0 becausetapcloseis 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.
Recommended fix
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.cTAPSIFINFOearly 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.