⬢ DragonFlyBSD Kernel Audit
DF-5406 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-5406",
  "status": "reproduced",
  "verdict": "REPRODUCED: HID report-descriptor Push/Pop items drive the parsers uint8_t pushlevel out of [0,MAXPUSH) (unconditional ++/-- at usb_hid.c:431/:445); the next hid_get_item() call re-derives c = &s->cur[pushlevel] (usb_hid.c:248) with pushlevel in [4..255] and writes/reads through it past the 1088-byte kmalloc hid_data object. Proven on the guest against the production kernel symbols: deterministic cross-object write into an adjacent live heap chunk at the pre-computed offset (guard[7]+0x1DC = 0xc0de0010), arbitrary 32-bit write + 100-byte read-back at +24,436 bytes (cur[255], single Pop byte), and a kernel panic (Fatal trap 12, supervisor write, page not present, Stopped at hid_get_item+0x83: movq $0,0x58(%r10) -- the OOB hid_clear_local zero of c->loc.size) after ~32 hammer rounds with filesystem churn; guest wedged in DDB and went down. Reachable by any malicious USB device at HID-driver attach (descriptor fetched verbatim by usbd_req_get_hid_desc and parsed by hid_locate/hid_report_size/driver loops). Fix (guard Push at MAXPUSH-1, guard Pop at 0) validated on a rebuilt kernel: same trigger corrupts nothing (0/9 guards), 40-round hammer completes, guest stays up.",
  "reproduced": true,
  "impact": "panic",
  "confidence": "certain",
  "exploit_chain": "malicious USB device supplies report descriptor -> usbd_req_get_hid_desc fetch (usb_hid.c:801-843) -> driver attach probe loops hid_get_item (ukbd.c:1070+, ums.c:468+, uhid.c:784+, wsp.c:733) -> N Push bytes (or one Pop byte) leave pushlevel in [4..255] while an item returns -> next call: c = &cur[pushlevel] OOB (usb_hid.c:248) -> hid_clear_local zeros 48 bytes (inlined; the faulting instruction hid_get_item+0x83 movq $0,0x58(%r10)) -> subsequent Global items write device-chosen 32-bit dvals at cur[k]+{0,4,8,...} (logical/physical min/max, unit, string_*, designator_*) and *h = *c reads 100 OOB bytes into the driver stack -> chosen-offset (s+24+100k, k=4..255 -> +424..+25524), chosen-content kernel heap write into neighbouring ~1-2KB M_TEMP-class objects (guard run proves exact-offset hits into the adjacent chunk at +1152); with device-attach grooming this is a neighbour-object pointer/refcount/function-pointer overwrite -> kernel code execution on an unhardened guest (no SMAP/SMEP/KASLR). uid0 chain not weaponized end-to-end: trigger requires a malicious USB device (physical vector), so the deliverable here is the corruption primitive + panic, fully characterized.",
  "evidence": "findings/poc/DF-5406/run.log (guard proof: GUARD 7 base=0xfffff80089703680 +0x1dc: 0xc0de0010 <-- CROSS-OBJECT CORRUPTION; far write: item 2: logmin=0xfeed0001), panic.txt (Fatal trap 12 ... Stopped at hid_get_item+0x83: movq $0,0x58(%r10)), run.hammer.log (40-round driver, died ~32), fix_run.log (patched kernel: 0/9 guards, HAMMER_ALL_DONE, guest up), build.log, env.txt",
  "kernel_refs": [
    "sys/bus/u4b/usb_hid.c:248",
    "sys/bus/u4b/usb_hid.c:88",
    "sys/bus/u4b/usb_hid.c:274",
    "sys/bus/u4b/usb_hid.c:289",
    "sys/bus/u4b/usb_hid.c:345",
    "sys/bus/u4b/usb_hid.c:377",
    "sys/bus/u4b/usb_hid.c:402",
    "sys/bus/u4b/usb_hid.c:431",
    "sys/bus/u4b/usb_hid.c:445",
    "sys/bus/u4b/usb_hid.c:571",
    "sys/bus/u4b/usb_hid.c:617",
    "sys/bus/u4b/usb_hid.c:801"
  ],
  "poc_changes": "Built a KLD harness (hidrig.c) that calls the guest kernels own compiled-in hid_start_parse/hid_get_item symbols (no source copy); duplicated the file-private struct hid_data/MAXPUSH definition verbatim (usb_hid.c:67-95) for sizeof/offsetof math; iterated guard placement 5x (batch guards around freed parsers failed 0/24 and 0/48 due to magazine allocation order; final design: hid_start_parse a zeroed buffer FIRST so the chunk is live, allocate same-size-class guards after it until one sits above (measured +1152), compute index k from measured deltas, then write the trigger descriptor into the same buffer in place before driving the parser -- fully deterministic). Modes: 0=geometry, 3=guard proof, 1=far write cur[255] + allocator churn; panic captured via 40-round hammer with concurrent find/ churn.",
  "attempts": 7,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly v6.5.0.1712.g89e6a-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (baseline); fix validation on #1: Sun Oct 4 08:32:42 UTC 2026",
  "runtime_sec": 1560,
  "guest_dirty": 0,
  "build_cmd": "cd /root/hidrig && make   (KLD hidrig.ko; then fix kernel: cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_GENERIC && make installkernel)",
  "run_cmd": "kldload /root/hidrig/hidrig.ko; sysctl kern.hidrig_mode=3; sysctl kern.hidrig_fire=1; sysctl kern.hidrig_mode=1; sysctl kern.hidrig_fire=1; (panic leg) for i in $(seq 1 40); do sysctl -n kern.hidrig_fire=1 >/dev/null; done & find /usr/src -ls >/dev/null; find / -name *.conf -ls >/dev/null",
  "code_hash": "4c8312643c6e1826e0b8041e4128abfd6544ce8289249b8fb97500b2151ea940",
  "notes": "Adjacent-chunk stride measured 1152 bytes for the 1088-byte hid_data allocation. The panic instruction movq $0,0x58(%r10) zeroes c->loc.size (offsetof(hid_item,loc.size)=0x58=88) -- the inlined hid_clear_local -- through c=cur[255]. Baseline runs: guard strategy iterations 1-4 (0/24, no-guard-above, LIFO-miss, 0/48), run 5 deterministic hit; hammer panicked at round ~32 of 40. Negative results proven during audit: hid_get_data/hid_put_data are per-byte clamped (rpos<len, usb_hid.c:667-671/728-734); hid_switch_rid loops are MAXID-bounded with guarded stores (:133-167); usbd_req_get_hid_descs UGETW(descrs[0].wDescriptorLength) is in-bounds because USB_HID_DESCRIPTOR_SIZE(0)==9 exactly covers the packed 9-byte header+descrs[0] and usb_desc_foreach guarantees desc+bLength<=end (usb_parse.c:93-95); hid_report_size can overflow h.loc.pos+size*count (:583) but consumers are byte-clamped by the engine -- hardening only.",
  "recommended_fix": "Reject Push when pushlevel >= MAXPUSH-1 and Pop when pushlevel == 0 so pushlevel stays in [0,3] and the per-call c = &s->cur[pushlevel] re-derivation (usb_hid.c:248) is always in bounds (fix.diff).",
  "fix_status": "fixed",
  "fix_baseline_reproduced": true,
  "fix_patched_reproduced": false,
  "fix_verdict": "fix.diff applied to guest /usr/src, nativekernel rebuilt (BUILD_RC=0), installed, rebooted; identical harness and 40-round hammer workload: guard corruption gone (0/9 vs 1/9 at +0x1DC), far-write parses in-bounds, all hammer rounds complete, guest up with no traps -- the corrupted/panicking behavior is gone on the patched kernel.",
  "fix_evidence": "findings/poc/DF-5406/fix.diff; findings/poc/DF-5406/fix_run.log (patched-kernel run transcript); baseline comparison in run.log/panic.txt"
}