# DF-5406 VERDICT — REPRODUCED (kernel heap OOB read/write via HID descriptor push/Pop level corruption; Fatal trap 12 inside hid_get_item)

## Bottom line

A malicious USB device's report descriptor drives the HID parser's item
stack level (`pushlevel`) out of range; on the next `hid_get_item()` call
the parser derives its working pointer **out of bounds** (`c =
&s->cur[s->pushlevel]`, usb_hid.c:248, `cur[MAXPUSH=4]` in a 1088-byte
`kmalloc` object) and then both **writes** device-supplied 32-bit values
and **reads back** 100-byte items through it. Reproduced three ways on the
QEMU guest against the production in-kernel functions (KLD harness calling
the kernel's own `hid_start_parse`/`hid_get_item` symbols):

1. deterministic cross-object write into a neighbouring live heap object at
   a pre-computed offset with pre-chosen content (guard scan),
2. controlled 32-bit write + read-back at **+24,436 bytes** past the object
   (`cur[255]`, single Pop byte),
3. **kernel panic**: `Fatal trap 12, supervisor write data, page not
   present, Stopped at hid_get_item+0x83: movq $0,0x58(%r10)` — the OOB
   zero-write of `c->loc.size` through `c = cur[255]` — after ~32 hammer
   rounds with filesystem churn. Guest wedged in DDB → `vm.sh status` = down.

## Root cause chain (path:line)

1. `sys/bus/u4b/usb_hid.c:88` — `uint8_t pushlevel` in `struct hid_data`
   (state is per-`hid_start_parse`, private, no locking needed — the bug is
   arithmetic, not races).
2. `:430-443 case 10 Push` — `s->pushlevel++` **unconditional**; only the
   copy to `s->cur[s->pushlevel]` is gated by `< MAXPUSH`. Four or more
   Push items leave `pushlevel ∈ [4..255]` while parsing continues and
   items are returned (`:278` top-block return, `:377` Collection,
   `:390` End collection).
3. `:444-461 case 11 Pop` — `s->pushlevel--` **unconditional**; one Pop at
   level 0 wraps 0 → **255** (uint8_t), the `else` branch merely prints.
   Parsing continues with the still-valid `c`, and the next Input item
   returns.
4. **Re-entry**: every caller loops `hid_get_item()` — `hid_locate`
   (:617), `hid_report_size` (:571), `hid_is_collection` (:751),
   `hid_is_mouse` (:869), and all HID drivers at attach (ukbd.c:1070+,
   ums.c:468+, uhid.c:784+, wsp.c:733, ucycom.c:243-245, uaudio.c:6110+).
   At `:248`, `c = &s->cur[pushlevel]` with pushlevel ∈ [4..255] → OOB.
5. Sinks through the OOB `c`:
   - `:289 hid_clear_local(c)` — 12 fields zeroed (48 bytes),
   - `:343/:369/:380 c->flags = dval`, `:345-346 c->loc.count/size`,
     `:402-405 logical_minimum/maximum = dval` (**arbitrary 32-bit**,
     4-byte items), `:399 _usage_page = dval<<16`, `:374 collevel++`,
     `:147 report_ID` via `hid_switch_rid`,
   - `:274/:376/:389 *h = *c` — **100-byte OOB read** into the caller's
     stack `hid_item`.
6. Threat: the report descriptor is fetched verbatim from the device
   (`usbd_req_get_hid_desc`, :801-843) and parsed at attach — a malicious
   USB device (or crafted device handed to the victim) controls every
   descriptor byte.

Primitive geometry (guest-verified, `mode 0`): `sizeof(struct hid_data) =
1088`, `offsetof(cur) = 24`, `sizeof(struct hid_item) = 100` → index k
writes at `s + 24 + 100k` (+4..+52 zeroed, +0/`_usage_page`, +4/+8
arbitrary dvals, …), k ∈ [4..255] chosen by Push/Pop bytes → **offsets
+424..+25524**, object ends at +1088. Same-zone neighbour chunks sit at
1152-byte stride (measured).

## Reproduction (baseline, unpatched kernel #0 Thu Jul 2 06:02:54)

- **Guard proof (deterministic cross-object corruption)**: harness
  `mode 3` — `hid_start_parse()` a zeroed 600-byte buffer (parser chunk
  now live and known), allocate same-size-class guards until one lands
  above it (measured delta 1152 = adjacent chunk), compute k so
  `cur[k].logical_minimum` (+24+100k+4) lands mid-guard, write the
  Push-spam trigger into the descriptor buffer the parser is anchored to
  (it has consumed nothing — exactly the device-attacker position), drive
  `hid_get_item()`:

  ```
  hidrig: live parser chunk s=0xfffff80089703200 (size 1088), 9 guards
  hidrig: target guard[7]=0xfffff80089703680 delta=1152 above s
  hidrig: target index k=16: marker 0xc0de0010 at s+1628 = guard[7]+476
  hidrig: firing 30-byte descriptor
  hidrig: parse done, 1 items returned
  hidrig: GUARD  7 base=0xfffff80089703680 +0x1dc: 0xc0de0010  <-- CROSS-OBJECT CORRUPTION
  ```

  The marker appears at the exact predicted guard offset (+0x1DC).
  (Earlier runs 0/24, 0/48: heap geometry — solved by allocating the
  target guard *after* the live parser chunk and rewriting the descriptor
  in place; no heap luck remains.)

- **Far write + read-back (`mode 1` phase A)**: descriptor `B4` (Pop @ 0)
  + item bytes; the second `hid_get_item()` call writes 0xFEED0001/0xFEED0002
  at `cur[255].logical_minimum/maximum` (s+25528/s+25532) and the trailing
  Input item copies them back: `item 2: logmin=0xfeed0001` — OOB write and
  OOB read round-trip at +24.4 KB.

- **Panic (hammer)**: 40 rounds of mode-1 (far write + free-list sweep +
  allocation churn) while two `find` walks generate kernel allocation
  traffic. Round ~32: `Fatal trap 12`, FVA 0xfffff80123ac008c,
  `supervisor write data, page not present`, `Stopped at
  hid_get_item+0x83: movq $0,0x58(%r10)` — the inlined
  `hid_clear_local()` zero store through `c = cur[255]` hitting an
  unmapped page. CPUs stopped, DDB, ssh dead, `vm.sh status` ⇒ down.
  (panic.txt, run.hammer.log.)

## Exploit-chain assessment

The primitive is a **relative kernel heap write**: chosen element index
(byte-offset 24+100k, k device-chosen ∈ [4..255]), chosen 32-bit content
(logical/physical min/max, unit, string_*, designator_* — full dval via
4-byte items; plus fixed zeroing of 12 fields and `_usage_page` dval<<16),
repeatable per parse pass, plus a 100-byte OOB read surfaced in driver
stack items. At real attach time the parser shares M_TEMP/~1-2KB size
classes with driver softc-adjacent allocations; overwriting a neighbour's
pointer/refcount/function-pointer field is the natural next step (the
guard run proves neighbour co-location at +1152 and exact-offset hits).
On this intentionally unhardened guest (no KASAN; no SMAP/SMEP/KASLR) the
remaining work is grooming victim objects at device-attach time —
mechanical for an attacker controlling the descriptor *and* the device
timing (multiple attach/detach cycles). Physical/USB vector, hence
High/CERT-metric 7.6 rather than Critical.

## Fix validation

`fix.diff`: reject Push when `pushlevel >= MAXPUSH-1`, reject Pop when
`pushlevel == 0` (nesting levels 1..3 preserved — all that ever worked).
Applied inside the guest `/usr/src`, `make -j6 nativekernel
KERNCONF=X86_64_GENERIC` (BUILD_RC=0), `make installkernel`, reboot into
kernel #1 Sun Oct 4 08:32:42 UTC 2026. Identical harness + workload:

- mode 3: same k=16 trigger → `guard scan done: 0/9 guards corrupted`
  (baseline: 1/9 at +0x1DC), parse completes;
- mode 1: churn completes, no trap;
- the full 40-round hammer + dual `find` churn: `round 1..40 done`,
  `HAMMER_ALL_DONE`, guest **up**, no trap, no dmesg anomalies.

Baseline behavior (guard corruption, panic) is GONE on the patched kernel.

## Attempts

7 trigger configurations (guard-placement strategy iterated 5×: batch
guards, LIFO-reuse, descending-magazine adaptation, live-parser+in-place
descriptor write — final: deterministic; then far-write, then hammer→panic).

## 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.
