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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5553 Β· 11 files
FileTypeDescriptionSize
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 sha256 6dac1ced…1942 (identical to the audited tree), 359 extracted lines, BUILD_OK.
  • run.log / run.2.log: five MISFIRE lines β€”
  • interface 0 (AC header subtype 0x01, two feature units subtype 0x06): deltas += 0 (benign),
  • interface 1: phone_raw_desc_20 byte[4] += 1 <-- CORRUPTED,
  • interface 2: phone_raw_desc_30 byte[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.sh
  • build.log, run.log, run.2.log, env.txt
  • fix.diff β€” class-gated CDC renumbering (validated by table_check_fixed)
  • VERDICT.md, manifest.json, verdict.json
VERDICT.md
↓ download raw

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:

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.

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

Fix 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'
↓ fix.diffper-fix-DF-5553

Confirmed kernel references

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