# VERDICT — DF-5493 (uether_ifdetach() missing class-wide never-attached guard)

**status: reproduced / impact: panic / confidence: certain / fix: fixed**

## Root cause (line-accurate)

`sys/bus/u4b/net/usb_ethernet.c`:

- :267-305 `uether_ifdetach()` — no validation of the `usb_ether` state:
  - :273 `usb_proc_drain(&ue->ue_tq)` no-ops on an all-zero `ue_tq`
    (`sys/bus/u4b/usb_process.c:406-410` returns when `up->up_lock == NULL`).
  - :276 `ifp = uether_getifp(ue)` returns `&ue->ue_arpcom.ac_if`
    (:133-137) — an **embedded** member, never NULL.
  - :278 `if (ifp != NULL)` — dead guard, always true.
  - :281 `UE_LOCK(ue)` = `lockmgr(ue->ue_lock, LK_EXCLUSIVE)` (:61) —
    **branch A**: `ue->ue_lock == NULL` → `Fatal trap 12`, fault VA 0x0.
  - :294 `ether_ifdetach(ifp)` — **branch B**: ifnet never `if_attach()`ed
    → `if_down()`/`if_detach()` on a zeroed ifnet → `Fatal trap 12`,
    VA 0x0 (`if_down_dispatch+0x66`; deeper in, `sys/net/if.c:975`
    `ifindex2ifnet[0] = NULL` and :982 `TAILQ_REMOVE(&ifnetlist, ...)`
    through a NULL `tqe_prev`).
  - :297 `sysctl_ctx_free()` on a never-`sysctl_ctx_init()`ed ctx,
    :300 `devfs_clone_bitmap_put(..., ue_unit=0)` of a never-allocated
    unit (would double-issue "ue0") — latent behind the :294 crash.

- :161-189 `uether_ifattach()` returns error leaving `ue_lock` valid but
  `ue_tq` fully unwound to zero (`usb_process.c:208-219` error path calls
  `usb_proc_free()` which resets `up->up_lock = NULL` at :244) and
  `ue_attach_post_task` never queued (queued only at :181-185 after
  success) — this is branch B's exact state.

## Reachability (attacker model)

- Branch A: evil-USB / evil-dock / VM-attach — the 8 sibling findings
  (DF-5334 cdce, DF-5384 aue, DF-5395 axge, DF-5397 urndis, DF-5399 mos,
  DF-5411 udav, DF-5453 kue, DF-5486 cue) each proved a deterministic
  device-controlled attach failure reaching `goto detach` →
  `uether_ifdetach()` with a zeroed ue. All those driver modules are in
  the default module set and auto-load on device match.
- Branch B: any of the same 8 drivers when `usb_proc_create()` fails
  (kernel-thread/resource exhaustion — ENOMEM) — every driver has the
  `error = uether_ifattach(ue); if (error) goto detach;` branch.
- Fixed-address NULL derefs both arms (softc zeroed by newbus,
  `sys/kern/subr_bus.c:1953` M_ZERO); no attacker-controlled data reaches
  the fault → local DoS from physical/VM USB access, no uid0 path.
  Same tier as the 8 siblings: Medium.

## Evidence

- `panic_a_serial.txt` (baseline, stock uether.ko): 3 `ue_guard_a:`
  markers, `ue_lock=0`, `Fatal trap 12`, `fault virtual address = 0x0`,
  `Stopped at lockmgr_exclusive+0x5d: orl (%r14),%esi`, `db>` — guest dead.
- `panic_b_serial.txt` (baseline, stock uether.ko, fresh reset):
  `ue_guard_b:` markers with `ue_lock=0xffffffff826001e0 ue_tq.up_lock=0`,
  `Fatal trap 12`, VA 0x0, `current process = Idle`,
  `Stopped at if_down_dispatch+0x66: movq (%rax),%r13` — the crash is
  inside `ether_ifdetach()`→`if_down()` on the never-attached ifnet,
  proving the call sequence survives UE_LOCK (:281) and
  usb_callout_drain (:286) and dies exactly at :294's teardown.
- `run_patched.log` (fix validation): after `fix.diff` applied in-guest,
  `uether.ko` rebuilt `-Werror` clean and installed, rigs rebuilt against
  the patched header: **both** branches print `SURVIVED
  uether_ifdetach()`, `KLDLOAD_RC=0`, `traps: 0`, `UNLOAD_OK`.
- `build.log` / `build_b.log`: `-Werror` clean builds of both rigs.
- `fix.diff`: `git apply --check` clean against the read-only `sys/` tree;
  applied in-guest with `patch -p1` (all 4 hunks, Plan A).

## Rig honesty

The failing call sites (driver `attach()` statics; `usb_proc_create`
ENOMEM) cannot be linked from a KLD — replaying the exact post-failure
state + the single production `uether_ifdetach()` call (auto-loaded stock
`uether.ko`) is the strongest runnable evidence, the same evidence class
as the 8 sibling rigs this pass. One rig defect was caught and fixed
during verification: `CFLAGS+= -DBRANCH_A` in a kmod Makefile is dropped
by `bsd.kmod.mk`, so the first "branch A" binary actually contained
branch B code (accidentally producing the first branch-B reproduction);
rebuilt as two self-contained sources (`ue_guard_a.c` / `ue_guard_b.c`)
with distinct module names and a `cmp` anti-identity check in `build.sh`.

## Relationship to known findings

- Not DF-1082 (miibus device_t UAF via SIOCGIFMEDIA) — different code.
- Not DF-2341 (its branch C: `ue_attach_post_task` fails at
  `mii_phy_probe`/`attach_post_sub` → later `ether_ifdetach` crash) —
  same *symptom sink*, different trigger state; the central guard here
  additionally covers DF-2341's arm, and DF-2341's own per-arm handling is
  untouched. The 8 per-driver DF-5334-family findings own each driver's
  `goto detach`; this finding owns the missing central guard that would
  have neutralized the entire class in one place.

## Pass-2 negative results (file checklist, all killed)

- (a) `uether_ifattach` partial-failure unwind: only failure is
  `usb_proc_create` (branch B); the taskqueue-drain-then-read-ifp
  ordering (:273→:276) is correct for the success path (drain waits for
  the async attach task).
- (b) `uether_ifdetach` ordering under concurrency: flag-clear under
  ue_lock (:281-283) precedes `usb_callout_drain` (:286); a watchdog
  racing in the window queues nothing (`usb_proc_msignal` drops on
  `up_gone`, usb_process.c:268-269) and its re-arm (:453) is stopped by
  the drain; `ue_tick_task` early-returns on the cleared IFF_RUNNING
  (:464). Method-table audit: all 10 drivers set every pointer the core
  derefs unconditionally (`ue_attach_post` :202, `ue_setpromisc` :394,
  `ue_setmulti` :404, `ue_init` :341, `ue_stop` :361, `ue_start` :383);
  mii-less tables (cdce/cue/ipheth/kue/urndis/udav_nophy) make the core
  skip `mii_phy_probe` (:230-231) so `ue_ifmedia_task`'s unconditional
  `ue_mii_upd` (:437) is unreachable today (API hazard only, no finding).
- (c) `uether_rxbuf` (:589) clamps the destination
  (`len < ETHER_HDR_LEN || len > MCLBYTES - ETHER_ALIGN`) into a fresh
  MCLBYTES cluster — bounded. Source side `usbd_copy_out` (:598) is
  unbounded vs the page cache (`usb_busdma.c:108` sets
  `res.length = (usb_size_t)-1` for single-segment) but every live caller
  passes a constant offset (0 or 2) with `len ≤ actlen ≤ bufsize`.
  `uether_rxmbuf` (:563-578) has **no** clamp — filed as DF-5495 (Info);
  live instance is DF-5396 (urndis), other callers (cdce :1001/:1550,
  ipheth :498) are clamped.
- (d) Watchdog/queue machinery: `ue_watchdog` re-arm loop is bounded by
  the drain ordering above; `ue_queue_command`'s post-msignal field
  writes are safe under the held ue_lock; start/stop tasks are
  synchronous via `usb_proc_mwait` (:129-130). `uether_ifdetach` never
  drains `ue->ue_rxq` — filed as DF-5494 (Low; cdce `tr_stall` at
  if_cdce.c:1014 skips the :1026 flush, mbufs leak with the softc).
- (e) Device-supplied sizes reaching the core: only via the rx entry
  points above — no new live unbounded path beyond DF-5396.

## Fix validated

`fix.diff` (central): `ue_attaching` set in `uether_ifattach()` only after
`usb_proc_create()` succeeds; `ue_attached` set in `ue_attach_post_task()`
after the ifnet is published; `uether_ifdetach()` early-returns on
`ue_lock == NULL || !ue_attaching` and gates the whole teardown block on
`ue_attached` (the failed-attach task already returned its unit at :262).
Applied in-guest, `uether.ko` rebuilt `-Werror` clean, installed, both
baseline-panicking rigs re-run → SURVIVED, RC=0, 0 traps, clean unloads.
Guest reset `with-src` after validation.

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
