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

cdce_attach() failure paths call cdce_detach() on a never-attached usb_ether -> kernel panic at device plug-in (NULL lockmgr, then latent ifindex/ifnetlist corruption)

Summary

The FreeBSD attach-failure detach pattern was imported unchanged, but DF's uether_getifp() returns the EMBEDDED ifnet (never NULL) so uether_ifdetach's guards are dead code: device-controlled attach failure (no data interface / no valid alt setting - the STOCK QEMU usb-net gadget's RNDIS config) -> lockmgr(ue->ue_lock==NULL) Fatal trap 12 at VA 0; had it not faulted, if_detach on the zeroed ifnet would NULL ifindex2ifnet[0] (destroying lo0) and corrupt the global ifnet list tail. REPRODUCED 3/3 with stock -device usb-net, no code needed, DDB backtrace pinned; fixed modules boot to ssh with the data interface attaching (ue0 works). ue_lock-NULL guard in uether_ifdetach (class-wide for all uether drivers) + detach_usb label fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5334 Β· 15 files
FileTypeDescriptionSize
README.md β€” 4.6 KB ↓ raw
VERDICT.md β€” 4.9 KB ↓ raw
trigger-attach.sh β€” 1.7 KB view raw
serial_drive.py β€” 1.4 KB view raw
build.sh β€” 281 B view raw
run.sh β€” 258 B view raw
build.log β€” 1.9 KB view raw
run.log β€” 12.3 KB view raw
run.serial.log β€” 12.3 KB view raw
run.2.log β€” 771 B view raw
panic.txt β€” 1.4 KB view raw
env.txt β€” 379 B view raw
fix.diff β€” 1.7 KB view raw
manifest.json β€” 1.6 KB view raw
verdict.json β€” 4.8 KB view raw

DF-5334 β€” cdce_attach failure path calls cdce_detach() on a

never-attached usb_ether -> kernel panic (NULL lockmgr / zeroed ifnet teardown)

What

sys/bus/u4b/net/if_cdce.c (USB CDC-Ethernet / NCM host driver). When a USB device is claimed by cdce_probe() but cdce_attach() fails early β€” e.g. the CDC union descriptor names a slave interface that does not exist ("no data interface found", if_cdce.c:610), or no alternate setting of the data interface admits a bulk IN/OUT transfer setup ("No valid alternate setting found", if_cdce.c:660) β€” the error path does goto detach; cdce_detach(dev); (if_cdce.c:731). At that point the usb_ether embedded in the softc was never initialised: ue->ue_lock is assigned only at if_cdce.c:720 (after all failure gotos), the ue_tq task queue is created by uether_ifattach() (never called), and the embedded ue_arpcom.ac_if was never if_attach()ed.

uether_ifdetach() then walks the FreeBSD-era teardown on this dead object and, on DragonFly, every guard is dead code:

  • usb_proc_drain() early-returns (zeroed up_lock) β€” safe;
  • ifp = uether_getifp(ue) returns the address of the embedded ue->ue_arpcom.ac_if (usb_ethernet.c:136) which on DragonFly is never NULL, so if (ifp != NULL) (usb_ethernet.c:278) is always true;
  • UE_LOCK(ue) β†’ lockmgr(ue->ue_lock == NULL) β†’ fault VA 0x0 (first observed fault, uether_ifdetach+0x2f).

Had the NULL ue_lock not faulted first, the teardown would proceed to ether_ifdetach() β†’ if_detach() on the zeroed embedded ifnet, which * sets ifindex2ifnet[0] = NULL β€” destroying the loopback interface's index slot (sys/net/if.c:966), and * executes TAILQ_REMOVE(&ifnetlist, ifp, if_link) (sys/net/if.c:982) on a zeroed if_link (tqe_prev == NULL) β†’ corrupts the global ifnet list tail and writes through NULL.

Reachability

An attacker supplies a crafted USB device (evil NIC / charging-cable / dock; "evil USB network adapter" is exactly this driver's threat model). cdce_probe() matches on interface class/subclass only (if_cdce.c:538-547), so a device presenting a CDC ECM/NCM/MDLM control interface whose data-interface alt settings never yield both bulk endpoints β€” or whose union descriptor slave number is out of range β€” deterministically hits the branch. No exploit payload is needed: the stock QEMU usb-net gadget (RNDIS+CDC dual config, as used to give VMs networking) triggers the panic at boot, because its RNDIS control interface matches cdce and its data interface fails the alternate-setting loop.

Reproduce (baseline β€” unpatched guest)

cd dfbsd-qemu
qemu-img snapshot -a with-src disk.qcow2          # clean disk, stock modules
QEMU_EXTRA="-device piix3-usb-uhci,id=uhci0 -netdev user,id=n1 \
            -device usb-net,bus=uhci0.0,netdev=n1" ./vm.sh up 240

Expected: vm.sh up times out (ssh never starts). Serial log shows:

cdce0: <QEMU USB Net RNDIS Control Interface> on usbus0
cdce0: No valid alternate setting found
Fatal trap 12: page fault while in kernel mode
fault virtual address        = 0x0
instruction pointer          = 0x8:0xffffffff80647bad
Stopped at      lockmgr_exclusive+0x5d: orl     (%r14),%esi
db> trace
lockmgr_exclusive() at lockmgr_exclusive+0x5d
uether_ifdetach() at uether_ifdetach+0x2f
cdce_detach() at cdce_detach+0x2a
cdce_attach() at cdce_attach+0x56b
device_doattach() at device_doattach+0x3af
usb_probe_and_attach() at usb_probe_and_attach+0x176
uhub_explore() at uhub_explore+0x231
usb_bus_explore() at usb_bus_explore+0xd0
usb_process() at usb_process+0x76

Full untrimmed logs: run.log (decisive run, incl. DDB trace + dumpsys), panic.txt (extract). Backtrace capture used serial_drive.py (drives the DDB prompt over the serial unix socket).

Fix validation (same QEMU config, patched modules)

# in guest, as root, with fix.diff applied to /usr/src:
cd /usr/src/sys/bus/u4b/net/uether && make && cp uether.ko /boot/kernel/
cd /usr/src/sys/bus/u4b/net/cdce   && make && cp if_cdce.ko /boot/kernel/
# reboot with the same -device usb-net attached:

Expected (observed): boot completes, SSH comes up, dmesg shows

cdce0: <QEMU USB Net RNDIS Control Interface> on usbus0
cdce0: No valid alternate setting found
device_probe_and_attach: cdce0 attach returned 6
cdce0: <QEMU USB Net Data Interface> on usbus0
cdce0: faking MAC address
ue0: <USB Ethernet> on cdce0

i.e. the same device that panicked the kernel now fails the control- interface claim cleanly (ENXIO), then successfully attaches to the data interface and yields a working ue0. dmesg | grep -c "Fatal trap" = 0. See run.2.log, build.log, fix.diff.

VERDICT.md
↓ download raw

DF-5334 β€” VERDICT

Status: reproduced (kernel panic, deterministic, 3 independent observations) β€” fix validated (patched modules boot clean with the same device attached).

Root cause (path:line)

  1. cdce_attach() initialises sc->sc_lock at sys/bus/u4b/net/if_cdce.c:580 but assigns ue->ue_lock, ue->ue_sc/dev/udev/methods only at if_cdce.c:717-721, i.e. after every failure goto.
  2. The failure branches reachable from device-controlled descriptors / device behaviour: * "no data interface found" β€” if_cdce.c:603-611: the CDC union descriptor's bSlaveInterface[0] (if_cdce.c:593, only checked for bLength >= sizeof(*ud) at :586) matches no interface number the device actually presented. * "No valid alternate setting found" β€” if_cdce.c:642-664: for every alt setting 0..31 either usbd_set_alt_interface_index() fails (device STALLs SET_INTERFACE) or usbd_transfer_setup() finds no bulk IN+OUT pair. Both do goto detach β†’ cdce_detach(dev) (if_cdce.c:730-732).
  3. cdce_detach() unconditionally calls uether_ifdetach(ue) (if_cdce.c:743).
  4. uether_ifdetach() (sys/bus/u4b/net/usb_ethernet.c:267-305): * usb_proc_drain(&ue->ue_tq) early-returns (zeroed up->up_lock) β€” sys/bus/u4b/usb_process.c. * ifp = uether_getifp(ue) returns &ue->ue_arpcom.ac_if β€” the embedded ifnet, never NULL on DragonFly (usb_ethernet.c:133-137), so the FreeBSD-era if (ifp != NULL) guard at :278 is dead code. * UE_LOCK(ue) at :281 β†’ lockmgr(NULL, LK_EXCLUSIVE) β†’ read/write through NULL β€” Fatal trap 12, fault VA 0x0 (observed: lockmgr_exclusive+0x5d: orl (%r14),%esi, r14=0, backtrace uether_ifdetach+0x2f).
  5. Latent second-stage corruption (proven statically, pre-empted by the first fault): ether_ifdetach() β†’ if_detach() on the zeroed ifnet would ifindex2ifnet[0] = NULL (destroying lo0's index slot, sys/net/if.c:966) and TAILQ_REMOVE(&ifnetlist, ifp, if_link) (sys/net/if.c:982) with tqe_prev == NULL β†’ sets ifnetlist.tqh_last = NULL (global list corruption) and writes through NULL.

Why it is DragonFly-specific

FreeBSD's uether_getifp() returns ue->ue_ifp, a pointer field that is NULL until if_alloc(). DragonFly embeds struct arpcom in usb_ether and uether_getifp() returns its address β€” the NULL guard can never fire. The failure-path cdce_detach() call pattern was imported from FreeBSD where it was harmless; on DragonFly it is a plug-time kernel panic.

Observed evidence

  • Baseline (stock /boot/kernel/if_cdce.ko, uether.ko), stock QEMU usb-net (RNDIS+CDC) on UHCI, boot-time enumeration: run.log / panic.txt β€” panic at attach, DDB backtrace captured via serial socket (serial_drive.py), crash dump written by call dumpsys. Guest never reaches ssh; vm.sh up times out.
  • Reproduced 3x: (a) qemu-xhci hot-plug device_add usb-net (guest died minutes after attach; log lost to reboot), (b) UHCI boot-time panic (vm.sh serial log), (c) UHCI boot-time panic with DDB trace (run.log). Deterministic β€” 3/3 boots with the device panicked.
  • Patched (fix.diff β†’ rebuilt if_cdce.ko, uether.ko, installed to /boot/kernel), identical QEMU command line: boots to ssh; dmesg: cdce0: No valid alternate setting found, device_probe_and_attach: cdce0 attach returned 6, then clean re-probe β†’ cdce0: <QEMU USB Net Data Interface>, ue0: <USB Ethernet> on cdce0 (run.2.log), dmesg | grep -c "Fatal trap" = 0.

Impact

Unauthenticated local DoS (kernel panic) from a crafted USB device at plug/enumeration time; also any VM configuration using QEMU's stock usb-net device on a DragonFly master guest panics at boot. No privilege escalation: the fault is a NULL-page access (write target fixed at VA 0), not a controllable overwrite. The latent if_detach() second stage (global ifnet list corruption + lo0 index destruction) would turn a survivable first fault into persistent networking-state corruption, but is pre-empted by the immediate NULL lockmgr fault.

Fix (fix.diff, module-only β€” both .ko rebuilt in-guest)

  • sys/bus/u4b/net/usb_ethernet.c: uether_ifdetach() bails out early when ue->ue_lock == NULL (never passed uether_ifattach()). Guards the entire uether driver class on DragonFly.
  • sys/bus/u4b/net/if_cdce.c: the two pre-attach failure branches take a new detach_usb label that only does usbd_transfer_unsetup() + lockuninit() instead of the full cdce_detach().

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 applied in-guest to /usr/src; if_cdce.ko and uether.ko rebuilt (-Werror clean) and installed into /boot/kernel; rebooted with the identical QEMU usb-net configuration that panicked 3/3 baseline boots: guest boots to ssh, dmesg shows 'cdce0: No valid alternate setting found' + 'device_probe_and_attach: cdce0 attach returned 6' followed by successful attach of the data interface and a working ue0; dmesg|grep -c 'Fatal trap' = 0. Baseline panic and patched-clean boot both on record in run.log / run.2.log.

run.2.log (patched boot serial: clean ENXUI then ue0 up, 'No core dumps found'); build.log (module rebuild + md5 match into /boot/kernel); fix.diff (unified, git-apply-able against clean sys/ tree)
↓ fix.diffper-fix-DF-5334

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log/run.serial.log (full serial: 'cdce0: No valid alternate setting found' -> Fatal trap 12 fault VA 0x0 -> DDB trace lockmgr_exclusive <- uether_ifdetach+0x2f <- cdce_detach+0x2a <- cdce_attach+0x56b <- device_doattach <- usb_probe_and_attach <- uhub_explore, plus dumpsys); panic.txt (extract); run.2.log (patched boot: clean 'attach returned 6' then 'ue0: <USB Ethernet> on cdce0', 'No core dumps found'); build.log (both fixed modules rebuilt -Werror clean, md5-matched into /boot/kernel); fix.diff (git-apply-able, module-only); env.txt (uname, cc 8.3, ue0 state, kldstat)

PoC changes

No compiled PoC was needed: the trigger is the stock QEMU 'usb-net' device (12 Mbps, CDC/RNDIS dual config) attached to a UHCI controller; initial attempts on qemu-xhci also panicked (hot-plug). Added serial_drive.py to drive the DDB prompt over a unix-socket serial to capture the backtrace and call dumpsys.

Verified recommended fix

uether_ifdetach(): return early when ue->ue_lock == NULL; cdce_attach(): route the two pre-attach failure branches to a detach_usb label doing only usbd_transfer_unsetup()+lockuninit() instead of full cdce_detach()

Verdict

Deterministic kernel panic at USB device enumeration: cdce_attach() failure branches (if_cdce.c:610 'no data interface found' / :660 'No valid alternate setting found') jump to cdce_detach() before the embedded usb_ether was initialised (ue_lock assigned only at :720, ue_tq/ifnet never created). uether_ifdetach() then lockmgr()s through the NULL ue_lock (fault VA 0x0) and would proceed to if_detach() on the zeroed embedded ifnet, NULLing ifindex2ifnet[0] (lo0) and corrupting the global ifnetlist via TAILQ_REMOVE - the NULL-guard it relies on is dead code on DragonFly because uether_getifp() returns the embedded arpcom ifnet which is never NULL. Reproduced 3/3 with the STOCK QEMU usb-net (RNDIS+CDC) device at boot and via hot-plug; backtrace captured through DDB over serial. Patched modules (fix.diff: early-out in uether_ifdetach when ue_lock==NULL + detach_usb label in cdce_attach) boot clean with the same device: ENXIO on the failing claim, driver then attaches the data interface and ue0 works; 0 Fatal traps.