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

udav_attach() failure paths call udav_detach() on a never-attached usb_ether -> deterministic kernel panic at evil-USB plug-in (lockmgr through NULL ue_lock; latent ether_ifdetach-on-unattached ifnet corruption on the second branch)

Summary

Sixth instance of the DF-5334 uether class: both attach failure branches goto the full detach with ue fields assigned only AFTER the failing call - the device-controlled usbd_transfer_setup failure (evil adapter with a udav_devs[] VID:PID missing the bulk endpoints) runs uether_ifdetach on an entirely-zeroed usb_ether -> lockmgr(NULL) Fatal trap 12 VA 0; the uether_ifattach-failure branch ether_ifdetaches a never-attached ifnet (ifindex2ifnet[0]/ifnet-list corruption). KLD-harness-verified: exact post-failure state replay panics at lockmgr_exclusive+0x5d 1/1; detach_usb rerouting + class-wide ue_lock guard fix validated (SURVIVED 2/2, clean loads).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5411 Β· 15 files
FileTypeDescriptionSize
README.md β€” 4.4 KB ↓ raw
VERDICT.md β€” 6.6 KB ↓ raw
udav_attach_poc.c β€” 5.3 KB view raw
Makefile β€” 149 B ↓ download
build.log β€” 5.8 KB view raw
panic.txt β€” 1.0 KB view raw
run.log β€” 1.9 KB view raw
fix_run.log β€” 1.9 KB view raw
fix_uether_build.log β€” 7.3 KB view raw
fix_udav_build.log β€” 894 B view raw
fix_build.log β€” 8.2 KB view raw
env.txt β€” 686 B view raw
fix.diff β€” 1.7 KB view raw
manifest.json β€” 1.4 KB view raw
verdict.json β€” 7.6 KB view raw

DF-5411 β€” if_udav.c attach-failure DF-5334-class panic (goto-detach-on-unattached)

What

udav_attach() (sys/bus/u4b/net/if_udav.c) routes its device-controlled failure branch through the full udav_detach() on a usb_ether that was never attached β€” identical class to DF-5334 (if_cdce), DF-5384 (if_aue), DF-5395 (if_axge), DF-5397 / DF-5399 (if_urndis / if_mos):

  • usbd_transfer_setup() fails (missing bulk endpoints on an otherwise matching DM9601-class device) β†’ if_udav.c:265-268 goto detach (:267) β†’ if_udav.c:293-294 udav_detach(dev).
  • At that point ue->ue_sc/ue_dev/ue_udev/ue_methods/ue_lock are ALL still zero β€” they are assigned only at if_udav.c:280-283, after the failing call (the lockinit(&sc->sc_lock) at :260 initializes sc_lock, not ue->ue_lock).
  • udav_detach() (if_udav.c:298-309) calls uether_ifdetach(ue) (if_udav.c:305) β†’ sys/bus/u4b/net/usb_ethernet.c:268-305: usb_proc_drain() no-ops on the zeroed ue_tq (usb_process.c:401-412); uether_getifp() returns the embedded arpcom ifnet (usb_ethernet.c:134), never NULL β€” the if (ifp != NULL) guard is dead code; then UE_LOCK(ue) (usb_ethernet.c:281, macro at :58) = lockmgr(NULL, LK_EXCLUSIVE) β†’ Fatal trap 12, fault VA 0x0.

The second failure branch (uether_ifattach() error, if_udav.c:285-288 β€” reachable only on usb_proc_create() ENOMEM, not attacker-timed) leaves ue_lock valid but a never-if_attach()ed ifnet, and reaches ether_ifdetach() (usb_ethernet.c:294) β†’ if_detach() on an unattached ifnet β†’ ifindex2ifnet[0] nulling + TAILQ_REMOVE of a never-inserted ifnet from the global ifnet list β€” latent corruption pre-empted in branch 1 by the NULL-write fault.

Threat model: attacker-supplied USB device (evil adapter / evil dock / VM USB attach) presenting one of the 6 VID:PID pairs in udav_devs[] (if_udav.c:205-218; e.g. ShanTou DM9601 0x0fe6:0x8101, Corega FEther USB-TXC 0x07aa:0x0004, Kontron JP1082) at config index 0 / iface 0 without the expected bulk-IN/OUT endpoints. Not remotely triggerable; requires physical/VM USB access.

Reproduce (evidence class: KLD harness driving production code)

QEMU has no DM9601 device model. The harness udav_attach_poc.c replays udav_detach() verbatim (its production calls, in order) on the exact state udav_attach()'s transfer-setup failure branch produces, using the production usbd_transfer_unsetup() and uether_ifdetach() (exported by uether.ko). The only non-production code is the static udav_attach/udav_detach frame itself (static in if_udav.c, reachable only via the goto with a physical device present).

# build (in guest, source pack at /root/df5411)
cd /root/df5411 && make -m /usr/src/share/mk

# BASELINE run (stock /boot/kernel/uether.ko auto-loaded by MODULE_DEPEND)
kldload -v ./udav_poc.ko
# expected: 3 marker lines, then
#   Fatal trap 12: page fault while in kernel mode
#   fault virtual address = 0x0
#   current process = ... (kldload)
#   Stopped at lockmgr_exclusive+0x5d: orl (%r14),%esi
# guest dead at db> β€” record: panic.txt

# PATCHED run (fix.diff applied to /usr/src, uether.ko rebuilt + installed)
kldload -v ./udav_poc.ko
# expected: 4th marker "udav_poc: SURVIVED uether_ifdetach()", RC=0,
#           0 Fatal traps, clean kldunload; patched if_udav.ko loads/unloads
# record: run.log

Fix validation

fix.diff (git-apply-able against clean sys/): (a) uether_ifdetach() early-returns when ue->ue_lock == NULL (class-wide, same as the DF-5334/5384 validated fix); (b) udav_attach() routes both pre-attach failures to a detach_usb: label doing only usbd_transfer_unsetup() + lockuninit() β€” (b) is required for the branch-2 ether_ifdetach-on-unattached corruption, which (a) does not cover, and it also removes the now-dead detach: label.

Both uether.ko and if_udav.ko rebuilt -Werror clean in-guest; patched uether.ko installed to /boot/kernel; identical harness run SURVIVED.

Files

  • udav_attach_poc.c + Makefile β€” the harness
  • build.log β€” harness build (final, -Werror clean)
  • panic.txt β€” baseline Fatal trap 12 record (serial console)
  • run.log / fix_run.log β€” patched run: SURVIVED, RC=0, 0 traps
  • fix_uether_build.log, fix_udav_build.log, fix_build.log β€” patched module builds
  • env.txt β€” guest uname, cc, kldstat, patch confirmation
  • fix.diff β€” the validated fix
  • VERDICT.md, manifest.json, verdict.json
VERDICT.md
↓ download raw

DF-5411 VERDICT β€” REPRODUCED (panic), fixed

Finding. udav_attach() (sys/bus/u4b/net/if_udav.c) has the DF-5334 "goto-detach-on-unattached" pattern: the usbd_transfer_setup() failure branch (if_udav.c:265-268) and the uether_ifattach() failure branch (if_udav.c:285-289) both jump to detach: (if_udav.c:293-295) and call the full udav_detach(), which calls uether_ifdetach(&sc->sc_ue) (if_udav.c:305) on a usb_ether that was never attached.

Why it faults. On the transfer-setup failure path, every ue field that uether_ifdetach() needs is still zero because they are assigned only at if_udav.c:280-283 β€” after the failing call at if_udav.c:263-264:

  • usb_proc_drain(&ue->ue_tq) (usb_ethernet.c:273) returns immediately (up->up_lock == NULL, usb_process.c:401-412 β€” its own comment says a zeroed struct makes it a no-op).
  • ifp = uether_getifp(ue) (usb_ethernet.c:276) returns &ue->ue_arpcom.ac_if β€” an embedded member, never NULL β€” so the if (ifp != NULL) guard at :278 is dead code (same as DF-5334/5384).
  • UE_LOCK(ue) (usb_ethernet.c:281, macro at :58) is lockmgr(ue->ue_lock /* NULL */, LK_EXCLUSIVE) β†’ read at VA 0x0 β†’ Fatal trap 12.

The second branch (uether_ifattach error) leaves ue_lock valid but the ifnet never if_attach()ed; uether_ifdetach() proceeds to ether_ifdetach() (usb_ethernet.c:294) β†’ if_detach() on if_index 0 β†’ nulls ifindex2ifnet[0] (lo0) and TAILQ_REMOVEs a never-inserted ifnet from the global ifnet list (sys/net/if.c) β€” latent list corruption pre-empted by the NULL fault in branch 1. That branch needs usb_proc_create() to fail (ENOMEM), so it is not attacker-timed; the transfer-setup branch is fully device-controlled: any USB device exposing one of the 6 udav_devs[] VID:PID pairs (if_udav.c:205-218; e.g. ShanTou DM9601 0x0fe6:0x8101, Corega FEther USB-TXC 0x07aa:0x0004, Kontron JP1082) at config 0 / iface 0 while omitting the bulk-IN/OUT endpoints makes usbd_transfer_setup() fail (USB_ERR_NO_ENDPOINT) at plug-in.

Verification (evidence class: KLD harness driving production code β€” QEMU has no DM9601 device model; udav_attach/udav_detach are static and reachable only with hardware; aue DF-5384 / axge / mos precedents):

  • The harness (udav_attach_poc.c) constructs the exact post-failure state (zeroed struct udav_softc + lockinit(&sc->sc_lock) per if_udav.c:260 β€” the only softc initialisation before the failing call) and replays udav_detach() verbatim: the production usbd_transfer_unsetup() then the production uether_ifdetach().
  • Baseline (pristine with-src guest, stock /boot/kernel/uether.ko auto-loaded by MODULE_DEPEND): the three markers print, then Fatal trap 12: page fault while in kernel mode, fault virtual address = 0x0, current process = 1065 (kldload), Stopped at lockmgr_exclusive+0x5d: orl (%r14),%esi β€” guest dead at db>. Record: panic.txt. 1/1 run (deterministic NULL deref; DF-5334/5384 proved the same sink in-situ).
  • Patched (fix.diff applied to guest /usr/src; uether.ko + if_udav.ko rebuilt -Werror clean; patched uether.ko into /boot/kernel, md5-verified against the patched build): identical harness run prints udav_poc: SURVIVED uether_ifdetach() (the guard's early return), KLDLOAD_RC=0, dmesg | grep -c 'Fatal trap' = 0, kldunload clean, and the patched if_udav.ko driver module loads/unloads clean (2/2 runs). Records: run.log, fix_uether_build.log, fix_udav_build.log.

Impact. Plug-time kernel NULL dereference (panic/DoS) from a malicious USB device — same severity tier and threat model as DF-5334/5384/5395/5397/ 5399 (Medium, physical/VM USB access). NOT memory corruption with attacker-controlled data: the fault address is fixed VA 0; the only "dereference" is the lockmgr read of the lock word at NULL. No user→root route. The latent branch-2 second stage (ifindex/ifnet-list corruption) is likewise plug-time corruption at fixed addresses — DoS.

Why this is a separate finding from DF-5334/5384/5395/5397/5399. Those were filed against if_cdce.c / if_aue.c / if_axge.c / if_urndis.c / if_mos.c; their persisted fixes were applied in-guest for validation only and then reverted β€” the tree still carries the unguarded uether_ifdetach, and every uether driver's attach-failure path is an independent instance. This finding covers the if_udav.c instance (root cause identical, trigger device-controlled the same way).

Fix. fix.diff: (a) uether_ifdetach() early-returns when ue->ue_lock == NULL (class-wide, matches the DF-5334/5384 validated fix); (b) udav_attach() routes both pre-attach failures to a detach_usb: label doing only usbd_transfer_unsetup() + lockuninit() and drops the now-dead detach: label β€” (b) is required for the branch-2 ether_ifdetach-on-unattached corruption, which (a) does not cover. Validated in-guest: baseline panics, patched survives, driver module clean.

Pass-2 negative results (checked and killed). - RX pseudo-header/m_adj class (DF-5396 in urndis): udav BULK_DT_RD bufsize is MCLBYTES+3 (if_udav.c:124); actlen ≀ 2051, guard actlen < sizeof(stat)+ETHER_CRC_LEN (:662) ensures payload β‰₯ 4; len = min(actlen-3, le16toh(stat.pktlen)) - 4 ≀ MCLBYTES-4 = 2044 (:669-670). A device-supplied pktlen < 4 makes len negative β†’ passed to uether_rxbuf(unsigned) it wraps to β‰₯ 0xFFFFFFFC and is rejected by the usb_ethernet.c:589 guard (len > MCLBYTES-ETHER_ALIGN) β€” no OOB, the frame is just dropped. Copy lands at m_adj(ETHER_ALIGN) (=2) into a fresh 2048-byte cluster: 2+2044 = 2046 ≀ 2048. No DF-5396 instance. - TX: m->m_pkthdr.len clamped to MCLBYTES (if_udav.c:594-595), temp_len ≀ MCLBYTES+2 = bufsize (:114, :602-611), copy at offset 2 β†’ exact fit; usbd_frame_zero range within budget (:617-618). - MII/register helpers: reg & UDAV_EPAR_EROA_MASK (5-bit) at :813-814/ :853-854; multicast hash h = crc32>>26 ≀ 63 into 8-byte table (:521-523). (The udav_csr_read1() uninitialized-return-on-error at :395-402 is filed separately as DF-5412, Info β€” it feeds device register writes only.) - udav_reset() retry loop (:484-489): device can hold RST for 1000 Γ— hz/100 = 10 s under sc_lock β€” bounded plug-time stall, DoS ceiling too low to file (same code ships upstream).

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 (patch -p1, both files; first variant kept the dead detach: label and tripped -Werror=unused-label, final variant drops it); uether.ko and if_udav.ko rebuilt -Werror clean; patched uether.ko installed into /boot/kernel (md5 verified against patched build); identical harness re-run: 'udav_poc: SURVIVED uether_ifdetach()', KLDLOAD_RC=0, dmesg|grep -c 'Fatal trap' = 0, kldunload clean, and the patched if_udav.ko driver module loads/unloads clean (2/2 runs). Baseline panic record in panic.txt, patched survival in run.log/fix_run.log. fix.diff verified git-apply-able against the clean read-only sys/ tree.

run.log / fix_run.log (SURVIVED marker + KLDLOAD_RC=0 + kldstat + UNLOAD_OK + Fatal-trap count 0 + if_udav.ko LOAD/UNLOAD OK + md5 match of installed uether.ko); fix_uether_build.log + fix_udav_build.log + fix_build.log (-Werror clean); panic.txt (baseline trap on identical input); fix.diff (git apply --check OK on clean tree)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

panic.txt (baseline serial: 3 udav_poc markers -> Fatal trap 12, fault virtual address = 0x0, current process 1065 (kldload), Stopped at lockmgr_exclusive+0x5d: orl (%r14),%esi, db> prompt); run.log (patched run: SURVIVED marker, KLDLOAD_RC=0, kldstat, UNLOAD_OK, dmesg Fatal-trap count 0, patched if_udav.ko loads/unloads clean, md5 match /boot/kernel/uether.ko vs patched build); build.log + fix_uether_build.log + fix_udav_build.log (-Werror clean builds); env.txt (guest uname/cc/kldstat + in-guest patch line confirmation); fix.diff (git-apply-able against clean sys/, git apply --check OK)

PoC changes

No seeded PoC existed. Wrote udav_attach_poc.c from scratch (aue DF-5384 harness pattern): KLD harness (MODULE_DEPEND uether) that kmallocs a zeroed struct udav_softc, lockinit()s sc->sc_lock exactly as if_udav.c:260 does (the only softc initialisation before the failing usbd_transfer_setup() at :263), prints the zeroed ue state, then replays udav_detach() verbatim: production usbd_transfer_unsetup(sc->sc_xfer, UDAV_N_TRANSFER) (if_udav.c:304) + production uether_ifdetach(&sc->sc_ue) (if_udav.c:305). First build hit -Werror=cast-qual printing ue_methods with (void*) cast - fixed by printing the const pointer directly. Physical DM9601 emulation is infeasible in QEMU (no device model); static udav_attach/udav_detach cannot be linked against, so the branch-1 state + exact call-sequence replay is the strongest runnable evidence and uses only production code after the goto.

Verified recommended fix

udav_attach(): route both pre-attach failures to a detach_usb label doing only usbd_transfer_unsetup()+lockuninit() (drop the dead detach: label); uether_ifdetach(): early-return when ue->ue_lock==NULL (class-wide, same as DF-5334/5384 fix)

Verdict

Deterministic kernel panic at USB device plug-in: udav_attach()'s usbd_transfer_setup() failure branch (if_udav.c:265-268 'allocating USB transfers failed') goto-jumps into the full udav_detach() (if_udav.c:293-295) while the embedded usb_ether is still entirely zeroed (ue_sc/ue_dev/ue_udev/ue_methods/ue_lock are assigned only at if_udav.c:280-283, AFTER the failing call at :263-264; the lockinit at :260 initialises sc_lock, not ue->ue_lock). uether_ifdetach() (if_udav.c:305) then lockmgr()s through the NULL ue_lock (Fatal trap 12, fault VA 0x0, Stopped at lockmgr_exclusive+0x5d) - the same DF-5334 (if_cdce) class already proved for aue/axge/urndis/mos: usb_proc_drain() no-ops on the zeroed ue_tq (usb_process.c:401-412) and the ifp!=NULL guard at usb_ethernet.c:278 is dead code because uether_getifp() (usb_ethernet.c:134) returns the embedded arpcom ifnet. The trigger is fully device-controlled: any evil USB adapter presenting one of the 6 udav_devs[] VID:PID pairs (if_udav.c:205-218; e.g. ShanTou DM9601 0x0fe6:0x8101, Corega FEther USB-TXC 0x07aa:0x0004, Kontron JP1082) at config 0/iface 0 without the expected bulk-IN/OUT endpoints fails the transfer setup at plug-in. The second failure branch (uether_ifattach error, only on usb_proc_create ENOMEM) additionally runs ether_ifdetach() on a never-if_attach()ed ifnet -> ifindex2ifnet[0] nulling + global ifnet list corruption. Verified with a KLD harness replaying udav_detach() verbatim (its two production calls in order) on the exact branch-1 state, using production usbd_transfer_unsetup()+uether_ifdetach(): baseline on pristine with-src guest with stock uether.ko -> Fatal trap 12 VA 0x0, guest dead in DDB (panic.txt); with fix.diff ((a) uether_ifdetach early-return on NULL ue_lock + (b) udav_attach detach_usb label doing only unsetup+lockuninit, dead detach: label dropped, required for branch 2) both modules rebuilt -Werror clean and the identical harness run SURVIVED with KLDLOAD_RC=0, 0 Fatal traps, clean unload, patched if_udav.ko loads/unloads clean (2/2 runs). Not memory corruption with attacker-controlled data: fixed-address NULL deref at plug time; impact is panic/DoS from physical/VM USB access, same tier as DF-5334 (Medium).