usb_make_raw_desc() CDC renumbering misclassifies the phone template's USB-Audio CS_INTERFACE descriptors: bDelay bytes rewritten 1->2 and 1->3 in generated descriptors
Summary
The template core's CDC union/call-management renumbering discriminates only on bLength>=5 + type 0x24 + subtype in {1,6}, which collides with USB Audio's CS_INTERFACE subtype space (AS_GENERAL=1, feature unit=6): five phone-template descriptors satisfy the CDC predicates and the two AS_GENERAL descriptors in non-zero interfaces get bDelay 1->2 and 1->3 in the generated heap copy (in-bounds - descriptor corruption, not memory corruption; the const source tables are never written). Template unselectable in-tree (DF-5353; root-only selection), latent for out-of-tree KLDs and the same predicate fires on usb_template_audio.c. Structural harness extracted the audited file verbatim (sha256 == guest /usr/src == audit tree) and replayed the exact predicate + numbering walk: 2 CORRUPTED misfires deterministic x2; class-gated fix drops misfires to 0. Seven structural check families S1-S7 all PASS on the deployed source (weight to the negative result).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5553 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.5 KB | β raw | |
| VERDICT.md | β | 7.7 KB | β raw | |
| table_check.c | β | 16.5 KB | view raw | |
| build.sh | β | 92 B | view raw | |
| run.sh | β | 116 B | view raw | |
| build.log | β | 252 B | view raw | |
| run.log | β | 5.7 KB | view raw | |
| run.2.log | β | 5.7 KB | view raw | |
| env.txt | β | 322 B | view raw | |
| fix.diff | β | 3.2 KB | view raw | |
| verdict.json | β | 6.2 KB | view raw |
DF-5553 β usb_template_phone.c structural misfire (CDC patcher vs audio CS_INTERFACE descriptors)
File: sys/bus/u4b/template/usb_template_phone.c (416 LOC, pass 2)
Evidence class: structural β production tables extracted verbatim at build
time and classified with the exact predicate of the in-tree core. Gadget-mode
QEMU activation is impossible for this template (it is not selectable,
DF-5353), so per the pass-2 family standard the verdict rests on the
table-termination/classification replay below, run on the audit guest against
the guest's own /usr/src.
Build
./build.sh # extracts /usr/src/.../usb_template_phone.c body -> phone_body.inc,
# sha256s the source, builds table_check + table_check_fixed
Requires nothing but a C compiler; SRC=<path> overrides the source file.
Run
./run.sh
Runs the baseline classification replay (misfires = the finding, structural
checks S1βS7 must PASS) and the fix-logic variant (-DFIX_CLASSGATE, CDC
patching gated on CDC interface classes; misfires must drop to 0). Exits 0
iff both behave exactly as expected.
Expected output
build.log: source sha2566dac1cedβ¦1942(identical to the audited tree), 359 extracted lines,BUILD_OK.run.log/run.2.log: fiveMISFIRElines β- interface 0 (AC header
subtype 0x01, two feature unitssubtype 0x06): deltas+= 0(benign), - interface 1:
phone_raw_desc_20byte[4]+= 1 <-- CORRUPTED, - interface 2:
phone_raw_desc_30byte[4]+= 2 <-- CORRUPTED; ALL STRUCTURAL CHECKS PASSED(S1βS7);MISFIRE lines under FIX_CLASSGATE: 0 (expected 0).- Exit code 0, both runs identical (deterministic).
What byte[4] is
phone_raw_desc_20/_30 are USB Audio AS_GENERAL descriptors
({bLength=7, 0x24, subtype 0x01, bTerminalLink, bDelay=1, wFormatTag=0x0001});
byte[4] is bDelay. The core's CDC call-management patcher
(usb_template.c:162-171) reads it as bDataInterface and adds the containing
interface number, so the generated gadget descriptors advertise
bDelay = 2 (playback, iface 1) and bDelay = 3 (record, iface 2) instead of
the authored 1. Writes are in-bounds inside the generated config buffer β
this is descriptor corruption, not memory corruption.
Artifacts
table_check.cβ harness (includes the audited file's body verbatim)build.sh,run.shbuild.log,run.log,run.2.log,env.txtfix.diffβ class-gated CDC renumbering (validated bytable_check_fixed)VERDICT.md,manifest.json,verdict.json
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] += ifnumsys/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)
- The write is in-bounds of the generated buffer; lengths and table structure are unaffected (harness checks S1βS2).
bDelayis an 8-bit delay hint; a host receiving 2 or 3 instead of 1 has no security-relevant behavior.- The phone template cannot be activated in-tree at all:
usb_temp_setup_by_index()(usb_template.c:1346-1373) has noUSB_TEMP_PHONEcase (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 exportedusb_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-171over 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) β thememcpy(dst, raw, raw[0])inusb_make_raw_desccannot 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_DESCobjects andusb_string_lang_enare self-sized (bLength == sizeof(struct), even payload), so the EP0len = 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 β thestring_index < INDEX_PHONE_MAXbound 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)== thewDescriptorLengthembedded inphone_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, exactlyfix.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.
Fix verification
not_testableFix LOGIC validated: table_check_fixed (identical harness with fix.diff's class gate compiled in) drops misfires from 5 (two with nonzero bDelay deltas) to 0 while all S1-S7 checks still pass. A kernel-level A/B build is not honestly testable: the phone template cannot be activated on any in-tree kernel (no USB_TEMP_PHONE case - DF-5353; USB_SET_TEMPLATE is caps-privileged), so no observable kernel behavior exists to compare baseline vs patched; with the class gate the generated descriptors equal the authored bytes by construction.
run.log (baseline: 5 MISFIRE, two '<-- CORRUPTED' += 1 / += 2, then 'MISFIRE lines under FIX_CLASSGATE: 0 (expected 0)'); fix.diff; VERDICT.md 'Fix and validation'
Confirmed kernel references
- sys/bus/u4b/template/usb_template.c:135
- sys/bus/u4b/template/usb_template.c:162
- sys/bus/u4b/template/usb_template.c:307
- sys/bus/u4b/template/usb_template.c:379
- sys/bus/u4b/template/usb_template_phone.c:107
- sys/bus/u4b/template/usb_template_phone.c:132
- sys/bus/u4b/template/usb_template_phone.c:162
- sys/bus/u4b/template/usb_template_phone.c:229
- sys/bus/u4b/template/usb_template_phone.c:287
- sys/bus/u4b/template/usb_template_phone.c:379
- sys/bus/u4b/template/usb_template_phone.c:397
- sys/bus/u4b/template/usb_template_phone.c:412
- sys/bus/u4b/usb_handle_request.c:744
Detail
Evidence (decisive lines)
run.log / run.2.log (decisive: 5 MISFIRE lines with the two '<-- CORRUPTED' nonzero deltas, ALL STRUCTURAL CHECKS PASSED, 'MISFIRE lines under FIX_CLASSGATE: 0'); build.log (guest /usr/src source sha256 6dac1ced... == audited tree; 359 lines extracted verbatim into phone_body.inc, BUILD_OK); VERDICT.md (full path:line narrative incl. byte layouts); fix.diff; table_check.c (harness: verbatim-extracted body + faithful core predicate replay)
PoC changes
No seed PoC existed (pass 2, no prior finding). Built the evidence from scratch: a harness that #includes the audited file's body extracted verbatim at build time (sed from '^enum {' to EOF) against a minimal shim of usb.h/usb_template.h/usb_cdc.h constants, replicating usb_make_raw_desc()'s classification predicate and usb_make_interface_desc()'s numbering walk; plus seven structural check families (S1-S7) covering the whole pass-2 template-family checklist, and a -DFIX_CLASSGATE variant validating fix.diff's logic (misfires 5 -> 0). Two build-environment fixes were needed: DragonFly uses 'sha256' not 'sha256sum', and the guest's /tmp/df5553 needed mode 1777 after root created it.
Verified recommended fix
Thread the containing interface's bInterfaceClass into usb_make_raw_desc() and gate the CDC union/call-management renumbering on UICLASS_CDC/UICLASS_CDC_DATA interfaces so audio CS_INTERFACE descriptors are emitted byte-exact (fix.diff).
Verdict
Structural (pass-2 family class, guest-run table replay): usb_make_raw_desc()'s CDC renumbering heuristic (usb_template.c:135-146,162-171) discriminates CDC union/call-management descriptors only by (bLength>=5, type==0x24, subtype==6/1), which collides with USB-Audio CS_INTERFACE subtypes; five of the phone template's descriptors are misclassified. Three (AC header subtype 1, two feature units subtype 6, all in interface 0) get delta 0; two AS_GENERAL descriptors in non-zero interfaces get real deltas: phone_raw_desc_20 byte[4] (bDelay) 1->2 at interface 1 (usb_template_phone.c:162-170) and phone_raw_desc_30 byte[4] 1->3 at interface 2 (usb_template_phone.c:229-237). All writes land in-bounds inside the generated heap config buffer (the const source tables are never written; offsets 3-4 < bLength 7/10/11), so this is deterministic descriptor corruption, not memory corruption. Impact ceiling is a malformed bDelay hint presented to a USB host, and the template cannot even be activated in-tree (no USB_TEMP_PHONE case in usb_temp_setup_by_index, DF-5353; selection is root-only via USB_SET_TEMPLATE), hence impact=none. Everything else the family checklist hunts is verified negative by the same harness (S1-S7: bLength==sizeof for all 14 raw tables, all 11 pointer tables NULL-terminated without holes, all string descriptors self-sized, 256-index callback sweep clean, vendor-desc length contract exact at 39==wDescriptorLength, HID interface hardcode==3, speed tables in-bounds/fail-closed).
No comments yet.