# 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:

```c
// 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.

## 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).
```
