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

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) where ifgr.ifgr_len is 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.

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
FileTypeDescriptionSize
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
README.md readme build/run/expected + one-liner mechanism
โ†“ download 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 lo0 from a fresh SSH session hangs forever (verified โ€” timeout 4 ifconfig cannot 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.

VERDICT.md verdict full narrative: line-by-line trace, privilege model, why the wedge is total, recovery
โ†“ download raw

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:

  1. From within the PoC: the second SIOCGIFGROUP ioctl (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).
  2. From a separate fresh SSH session after the PoC exits the syscall: ifconfig lo0 (which dispatches through ifioctl() and reaches ifnet_lock()) blocks forever in D-state; timeout 4 ifconfig cannot 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 needed
  • ioctl(s, SIOCGIFGROUP, &ifgr) with ifgr.ifgr_len set to any value that does not match the live group count (the PoC probes the real length first with len=0, then sends len=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 real ifgr_len for lo0 with len=0, then sends len=real+1 to force the EINVAL branch and leak ifnet_mtx. Forks a child that immediately issues a second SIOCGIFGROUP on 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 bare return (error) inside the locked switch region to break so they fall through to ifnet_unlock() at line 2450.

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.