# VERDICT — DF-5553

**Finding:** `usb_make_raw_desc()`'s CDC descriptor renumbering heuristic
misclassifies the phone template's USB-Audio class-specific descriptors and
silently rewrites two bytes of the generated configuration descriptors.

**Status: reproduced (structural class; deterministic). Impact: none**
(no kernel memory-safety consequence; descriptor-level corruption only, and
the template itself is not selectable in-tree — DF-5353).

## Root cause (path:line)

The core patcher discriminates CDC descriptors by type+subtype only:

- `sys/bus/u4b/template/usb_template.c:135-146` — CDC **union**: `bLength >= 5
  && raw[1] == UDESC_CS_INTERFACE (0x24) && raw[2] == UDESCSUB_CDC_UNION (6)`
  → `bMasterInterface += ifnum; bSlaveInterface[0] += ifnum`
- `sys/bus/u4b/template/usb_template.c:162-171` — CDC **call management**:
  `bLength >= 5 && raw[1] == 0x24 && raw[2] == UDESCSUB_CDC_CM (1)` →
  `bDataInterface += ifnum`

USB Audio uses the *same* 0x24 type byte with an overlapping subtype space:
`AS_GENERAL` is subtype 0x01 and feature units are subtype 0x06. The phone
template therefore feeds the patcher five audio descriptors that satisfy the
CDC predicates:

| descriptor | file:line | shape | interface | delta applied |
|---|---|---|---|---|
| `phone_raw_desc_0`  | usb_template_phone.c:107 | AC header, 10 B, subtype 1 | 0 | `+= 0` (benign) |
| `phone_raw_desc_5`  | usb_template_phone.c:132 | feature unit, 11 B, subtype 6 | 0 | `+= 0` (benign) |
| `phone_raw_desc_6`  | usb_template_phone.c:137 | feature unit, 11 B, subtype 6 | 0 | `+= 0` (benign) |
| `phone_raw_desc_20` | usb_template_phone.c:162 | AS_GENERAL, 7 B, subtype 1 | 1 | **byte[4] += 1** |
| `phone_raw_desc_30` | usb_template_phone.c:229 | AS_GENERAL, 7 B, subtype 1 | 2 | **byte[4] += 2** |

Interface numbers come from the core's own numbering walk
(`usb_template.c:379` starts `bInterfaceNumber = 0xFF`;
`usb_template.c:309-314` pre-increments per non-alt interface before raw
descriptors are scanned, so iface 0's descriptors are patched with 0, iface
1's with 1, iface 2's with 2).

`phone_raw_desc_20 = {0x07, 0x24, 0x01, 0x04, 0x01, 0x01, 0x00}` and
`phone_raw_desc_30 = {0x07, 0x24, 0x01, 0x02, 0x01, 0x01, 0x00}` are audio
`AS_GENERAL` descriptors: byte[4] is **bDelay = 1**. The CM patch rewrites it
as `bDataInterface`, producing `bDelay = 2` (playback interface 1) and
`bDelay = 3` (record interface 2) in the generated configuration handed to a
USB host. All patched offsets (3,4) are inside the copied `bLength`-sized
descriptors in the heap config buffer (`usb_template.c:128-171` patches
`dst`, a copy — the const source tables are never written), so there is **no
out-of-bounds write and no read of the wrong object**: pure descriptor
corruption.

## Why impact is "none" (honestly)

1. The write is in-bounds of the generated buffer; lengths and table
   structure are unaffected (harness checks S1–S2).
2. `bDelay` is an 8-bit delay hint; a host receiving 2 or 3 instead of 1 has
   no security-relevant behavior.
3. The phone template cannot be activated in-tree at all:
   `usb_temp_setup_by_index()` (`usb_template.c:1346-1373`) has no
   `USB_TEMP_PHONE` case (default → `USB_ERR_INVAL`), and template selection
   is privileged anyway (`USB_SET_TEMPLATE` → `caps_priv_check_self`,
   usb_dev.c:1718-1721). Filed as DF-5353 against the core — not re-reported
   here. An out-of-tree KLD calling the exported `usb_temp_setup(udev,
   &usb_template_phone)` directly would hit this deterministically at
   descriptor-build time.

## Verification (what ran, where)

Guest `DragonFly dfbsd 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026
(X86_64_GENERIC)`, cc 8.3. `build.sh` extracted lines 58–416 of the guest's
own `/usr/src/sys/bus/u4b/template/usb_template_phone.c` (sha256
`6dac1cede3a0c6a8ea7b3a8d0cf798d860e9dcfa22b9c0a0aa241693fcce1942` —
byte-identical to the audited tree) and compiled the harness over it.
`run.sh` (twice; `run.log`, `run.2.log` identical):

- **(1) Classification replay** — the exact predicate of
  `usb_template.c:135-171` over the real tables with the real numbering walk:
  5 misfires, 2 with nonzero deltas (`+= 1`, `+= 2`), exactly the byte
  positions and values predicted above.
- **Structural checks S1–S7 all PASS** — the file is otherwise clean:
  - S1: every raw descriptor's `bLength == sizeof(table)` (14/14) — the
    `memcpy(dst, raw, raw[0])` in `usb_make_raw_desc` cannot over-read;
  - S2: all 11 NULL-terminated pointer tables carry their terminator, no
    interior holes (early-truncation class dead);
  - S3: all 5 `USB_MAKE_STRING_DESC` objects and `usb_string_lang_en` are
    self-sized (`bLength == sizeof(struct)`, even payload), so the EP0
    `len = buf[0]` path (`usb_template.c:1199-1203`) cannot over-disclose
    adjacent kernel data;
  - S4: full 256-index sweep of `phone_get_string_desc()`: non-NULL exactly
    for {0} ∪ {1..5} @ 0x0409, NULL for every other (index, lang) pair —
    the `string_index < INDEX_PHONE_MAX` bound check
    (usb_template_phone.c:412) holds and the designated-initializer table
    (usb_template_phone.c:397-404) has no NULL hole;
  - S5: `phone_get_vendor_desc()` sets `*plen = 39 ==
    sizeof(phone_hid_descriptor)` == the `wDescriptorLength` embedded in
    `phone_hid_raw_desc_0` (usb_template_phone.c:287), and rejects
    wrong-interface/type/index variants;
  - S6: interface walk yields HID = #3, matching the hardcoded
    `req->wIndex[0] == 3` (usb_template_phone.c:379);
  - S7: endpoint speed tables nonzero at FULL/HIGH and in-bounds of
    `mps[USB_SPEED_MAX]`; other speeds → 0 → core fails closed
    (`usb_template.c:221-229`).
- **Fix-logic validation**: `table_check_fixed` (same harness with the CDC
  patch gated on CDC/CDC-data interface classes, exactly `fix.diff`):
  misfires drop to **0**.

EP0 clamp sanity (cited, core): `usb_handle_request.c:744-748` truncates
`max_len` to the request's remaining `wLength` before the copy/zero-copy
frames, so template-supplied lengths cannot over-send against a hostile
host's request regardless.

## Fix and validation

`fix.diff` threads the containing interface's `bInterfaceClass` into
`usb_make_raw_desc()` and gates the CDC union/CM renumbering on
`UICLASS_CDC (0x02) / UICLASS_CDC_DATA (0x0a)`; IAD renumbering stays
ungated (type 0x0B cannot collide with 0x24). Validated at the table level
(`table_check_fixed`: 0 misfires). A kernel-level A/B (rebuild + activate
template) is **not testable**: the template cannot be activated in-tree
(DF-5353), so no observable kernel behavior exists to compare — fix_status
`not_testable` is honest, not lazy. The fix also protects the audio template
(whose feature units live in interface 0 today and would corrupt silently if
ever rebased) and any future composite embedding of these tables.

## Negative result weight

Pass-2 classes considered and killed for this file: OOB read/write in
descriptor emission (S1, S2); string-index/EP0-index reachability (S4, core
clamp); uninitialized/sparse static tables (S3, S4); length-contract bugs in
the vendor callback (S5); interface-number TOCTOU-style hardcode drift (S6);
speed-table indexing and zero-mps handling (S7); sysctl-mutable string
fields — N/A, this tree's template core has no mutable vendor/product
strings; races/lifetimes — file has zero mutable state (all `static const`,
one never-written `static uint8_t` array), callbacks read immutable data
under the control-transfer engine. Nothing exploitable beyond the Info-level
misfire above.

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