β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5493

uether_ifdetach() performs no never-attached state validation β€” class-wide owner of the 8-instance DF-5334 goto-detach panic family (lockmgr through NULL ue_lock; ether_ifdetach() on a never-if_attach()ed ifnet)

Summary

The core tears down whatever ue it is handed with zero state checks: the drain no-ops on an all-zero ue_tq, the embedded-ifp getter defeats the NULL guard, lockmgr goes through NULL (branch A) or ether_ifdetach tears down a never-published ifnet (branch B - usb_proc_create ENOMEM, ue_tq unwound to zero, attach task never queued, plus sysctl_ctx_free on a never-init ctx and devfs_clone_bitmap_put of a never-allocated unit). Owner of the 8 driver instances proven this pass (DF-5334/5384/5395/5397/5399/5411/5453/5486). Both branches reproduced against the PRODUCTION uether_ifdetach (Fatal trap 12 at lockmgr_exclusive+0x5d and if_down_dispatch+0x66 respectively); two-flag central-guard fix validated (both SURVIVED, clean unloads).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5493 Β· 15 files
FileTypeDescriptionSize
ue_guard_a.c β€” 2.9 KB view raw
ue_guard_b.c β€” 3.8 KB view raw
Makefile.a β€” 163 B ↓ download
Makefile.b β€” 163 B ↓ download
build.sh β€” 165 B view raw
run.sh β€” 448 B view raw
build.log β€” 6.2 KB view raw
build_b.log β€” 6.2 KB view raw
panic_a_serial.txt β€” 2.2 KB view raw
panic_b_serial.txt β€” 2.2 KB view raw
run_patched.log β€” 2.6 KB view raw
env.txt β€” 362 B view raw
fix.diff β€” 2.6 KB view raw
VERDICT.md β€” 7.9 KB ↓ raw
README.md β€” 3.5 KB ↓ raw

DF-5493 β€” uether_ifdetach() missing never-attached guard (class-wide owner)

status: reproduced / impact: panic / confidence: certain / fix validated

What

uether_ifdetach() (sys/bus/u4b/net/usb_ethernet.c:267-305) validates nothing about the struct usb_ether it is handed:

  • usb_proc_drain() (:273) silently no-ops on an all-zero ue_tq (sys/bus/u4b/usb_process.c:409),
  • uether_getifp() (:276, :133-137) returns the embedded arpcom ifnet, so the if (ifp != NULL) guard (:278) is dead code β€” always true,
  • it then lockmgr()s through ue_lock (:281) and runs ether_ifdetach(ifp) (:294) unconditionally.

Eight drivers this pass (DF-5334/5384/5395/5397/5399/5411/5453/5486) call their detach β€” and thus uether_ifdetach() β€” on attach-failure paths where the ue was never attached. This file owns the class-wide defect and the central guard; the per-driver findings own each driver's goto detach.

Two failure arms verified on the guest

  • Branch A (ue fields zero β€” all 8 sibling drivers' pre-uether_ifattach failures, e.g. if_cue.c:405-410): UE_LOCK(ue) β†’ lockmgr(NULL) β†’ Fatal trap 12, fault VA 0x0, Stopped at lockmgr_exclusive+0x5d (panic_a_serial.txt).
  • Branch B (uether_ifattach() itself failed at usb_proc_create() ENOMEM, usb_ethernet.c:173-178; ue_lock valid, ue_tq unwound to zero by usb_process.c:217-219,244; every driver's if (uether_ifattach(ue)) goto detach; branch): ether_ifdetach() β†’ if_down() on the never-if_attach()ed ifnet β†’ Fatal trap 12, fault VA 0x0, Stopped at if_down_dispatch+0x66 (panic_b_serial.txt). Behind this crash also lurk sysctl_ctx_free() on a never-init'd ctx (:297) and devfs_clone_bitmap_put(..., ue_unit=0) of a never-allocated unit (:300) which would hand unit 0 to the next adapter while a live ue0 exists.

Rig (KLD harness + production code)

ue_guard_a.c / ue_guard_b.c: each kmallocs a zeroed struct usb_ether, constructs the exact post-failure state, and calls the production uether_ifdetach() from the stock auto-loaded uether.ko (MODULE_DEPEND(uether)). The failing call sites are static driver-attach paths needing a physical evil-USB device (or a taskqueue-creation ENOMEM), so state + call-sequence replay is the strongest runnable evidence β€” same evidence class as the 8 sibling rigs (see findings/poc/DF-5486/).

Build / run (in guest)

cd /root/df5493 && sh ./build.sh        # builds ue_guard_a.ko + ue_guard_b.ko, -Werror
sh ./run.sh a                           # baseline: Fatal trap 12 VA 0x0 (lockmgr NULL)
# (reset with-src, rebuild)
sh ./run.sh b                           # baseline: Fatal trap 12 VA 0x0 (if_down_dispatch)

With fix.diff applied to /usr/src, uether.ko rebuilt and installed, and the rigs rebuilt against the patched header, both branches print SURVIVED uether_ifdetach(), KLDLOAD_RC=0, traps: 0, clean unload (run_patched.log).

Fix

fix.diff: adds ue_attaching / ue_attached state flags to struct usb_ether; uether_ifattach() sets ue_attaching only after usb_proc_create() succeeds; ue_attach_post_task() sets ue_attached after publishing the ifnet; uether_ifdetach() early-returns on ue_lock == NULL || !ue->ue_attaching and gates the teardown block on ue->ue_attached (the failed-attach task already returned the unit). Covers branch A, branch B, and DF-2341's branch C (async PHY-probe attach failure) in one central place. git apply --check clean on the read-only tree.

VERDICT.md
↓ download raw

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff (ue_attaching set in uether_ifattach() after usb_proc_create() success; 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 teardown block on ue_attached) applied in-guest via patch -p1 (4 hunks Plan A), uether.ko rebuilt -Werror clean, installed to /boot/kernel, both rigs rebuilt against the patched header (struct layout grew) and re-run on the IDENTICAL inputs: branch A and branch B both print SURVIVED uether_ifdetach(), KLDLOAD_RC=0, dmesg Fatal-trap count 0, clean kldunload of both modules. Baseline panics for the same inputs are in panic_a_serial.txt / panic_b_serial.txt. The guard also covers DF-2341's branch C and would have neutralized the entire 8-driver DF-5334 class centrally. git apply --check passes on the clean read-only sys/ tree.

run_patched.log (both SURVIVED markers + KLDLOAD_RC=0 + traps:0 + UNLOAD_OK x2); fix_uether build inside build.sh logs; fix.diff (git apply --check OK); panic_a_serial.txt + panic_b_serial.txt (baseline traps on identical inputs)
↓ fix.diffper-fix-DF-5493

Confirmed kernel references

Detail

Evidence (decisive lines)

panic_a_serial.txt (baseline branch A: 3 ue_guard_a markers, ue_lock=0, Fatal trap 12 fault VA 0x0, Stopped at lockmgr_exclusive+0x5d, db> prompt); panic_b_serial.txt (baseline branch B on fresh with-src reset: ue_lock=0xffffffff826001e0 ue_tq.up_lock=0 markers, Fatal trap 12 VA 0x0, current process=Idle, Stopped at if_down_dispatch+0x66); run_patched.log (fix validation: BOTH branches SURVIVED + KLDLOAD_RC=0 + traps:0 + UNLOAD_OK after patched uether.ko installed); build.log/build_b.log (-Werror clean); fix.diff (git apply --check OK on clean read-only sys/ tree; patch -p1 all hunks Plan A in guest); VERDICT.md (full narrative incl. rig-honesty note: first rig build lost -DBRANCH_A to bsd.kmod.mk CFLAGS handling -- accidentally produced the first branch-B reproduction; rebuilt as two self-contained sources with cmp anti-identity check)

PoC changes

No seeded PoC existed. Wrote the rig from scratch: ue_guard_a.c/ue_guard_b.c KLD harnesses (MODULE_DEPEND uether) that kmalloc a zeroed struct usb_ether, construct each exact post-failure state (A: ue_lock NULL; B: lockinit'd ue_lock + everything else zero, matching usb_proc_create's fully-unwound error state), print the state, and call the production uether_ifdetach() from the stock uether.ko. Build issues fixed: two missing kernel includes (sys/sysctl.h, sys/bus.h + sys/condvar.h) flagged by -Werror; then the decisive rig defect: CFLAGS+= -DBRANCH_A in a kmod Makefile is silently dropped by bsd.kmod.mk, so the 'branch A' binary contained branch B code (caught via identical .ko sizes; that accidental run was the first branch-B reproduction) -- rebuilt as two self-contained .c files with distinct module names and added a cmp anti-identity check to build.sh.

Verified recommended fix

Central guard in uether_ifdetach(): add ue_attaching/ue_attached state flags to struct usb_ether; early-return when ue_lock==NULL or !ue_attaching; gate the ifnet teardown block on ue_attached (see fix.diff)

Verdict

uether_ifdetach() (sys/bus/u4b/net/usb_ethernet.c:267-305) performs zero state validation on the usb_ether handed to it: usb_proc_drain() (:273) no-ops on an all-zero ue_tq (usb_process.c:409), uether_getifp() (:276/:133-137) returns the EMBEDDED arpcom ifnet so the ifp!=NULL guard (:278) is dead code, and it then lockmgr()s through ue_lock (:281) and runs ether_ifdetach() (:294) unconditionally. This file owns the class-wide defect instantiated 8 times this pass (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 driver's attach-failure goto-detach reaches uether_ifdetach() on a never-attached ue. Both arms verified on the guest with KLD rigs calling the PRODUCTION uether_ifdetach() from the stock auto-loaded uether.ko: branch A (zeroed ue, ue_lock NULL -- the 8-sibling state) -> Fatal trap 12, fault VA 0x0, Stopped at lockmgr_exclusive+0x5d (panic_a_serial.txt); branch B (ue_lock valid, ue_tq unwound to zero -- uether_ifattach()'s own usb_proc_create ENOMEM state, reached by every driver's if(uether_ifattach(ue)) goto detach branch) -> survives UE_LOCK and usb_callout_drain and dies at ether_ifdetach()/if_down() on the never-if_attach()ed ifnet: Fatal trap 12, VA 0x0, Stopped at if_down_dispatch+0x66 (panic_b_serial.txt); latent behind that crash are sysctl_ctx_free on a never-init'd ctx (:297) and devfs_clone_bitmap_put of unit 0 never allocated (:300). Fixed-address NULL derefs (softc zeroed by newbus M_ZERO), no attacker-controlled data at the fault -> panic/DoS from evil-USB plug-in, same Medium tier as the 8 siblings; no uid0 path. Central fix (fix.diff: ue_attaching/ue_attached state flags; early-return on ue_lock==NULL || !ue_attaching; teardown gated on ue_attached) applied in-guest, uether.ko rebuilt -Werror clean and installed, identical rigs re-run -> both branches SURVIVED, KLDLOAD_RC=0, 0 Fatal traps, clean unloads. The guard also neutralizes DF-2341's branch (async PHY-probe attach failure).