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

USB_TEMP_PHONE gadget template is compiled and advertised but unreachable: missing case in usb_temp_setup_by_index()

Summary

USB_TEMP_PHONE=7 is a valid selection-enum value and usb_template_phone.c is compiled+linked into the shipped usb_template.ko (nm shows the exported descriptor) but the dispatcher has no case for it - index 7 falls to default USB_ERR_INVAL with the misleading 'maybe the module has not been loaded' hint. Guest-verified via KLD: setup_by_index(7) err=3 while direct usb_temp_setup(&usb_template_phone) succeeds FULL/HIGH and sweeps clean - the template is functional, only the dispatch case is missing. Add-the-case (or delete like upstream) fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5353 Β· 13 files
FileTypeDescriptionSize
usb_template_fuzz.c β€” 15.1 KB view raw
Makefile β€” 209 B ↓ download
build.sh β€” 178 B view raw
run.sh β€” 160 B view raw
build.log β€” 5.7 KB view raw
run.log β€” 2.8 KB view raw
run.2.log β€” 5.9 KB view raw
run.initial-expectation-fixes.log β€” 2.9 KB view raw
VERDICT.md β€” 6.9 KB ↓ raw
env.txt β€” 195 B view raw
fix.diff β€” 434 B view raw
manifest.json β€” 1.2 KB view raw
verdict.json β€” 4.5 KB view raw
VERDICT.md
↓ download raw

VERDICT β€” DF-5353 (usb_template.c pass 2)

Bottom line: sys/bus/u4b/template/usb_template.c is memory-safe against the malicious-host threat model β€” a 187-million-request KLD sweep of the real descriptor demux (all descriptor types Γ— indices Γ— langids Γ— request types Γ— 9 templates Γ— 3 speeds) produced zero anomalies, zero panics, and the two-pass descriptor builder proved byte-exact (independently recomputed sizing == sum of served wTotalLengths). One Info-level functional defect was reproduced: the USB_TEMP_PHONE gadget template is compiled into usb_template.ko and advertised by the usb_ioctl.h enum, but usb_temp_setup_by_index() has no case for it, so template selection 7 always fails. One additional Info hardening note (scratch-union aliasing) is filed without a pack.

What was verified on the guest and how

KLD harness (usb_template_fuzz.c, this directory) driving the shipped usb_template.ko against a synthetic struct usb_device:

Claim Result Evidence
Dispatch indices bounded (config index vs bNumConfigurations, string id vs sibling tables, langid) Holds β€” 0 anomalies in 187,272,018 crafted requests run.log, run.2.log anom=0
Served lengths consistent with served buffers (len==buf[0], config walk == wTotalLength, device 18B, qualifier 10B, string ≀255B even) Holds check_response()/walk_config() in harness; 0 anomalies
Two-pass build (size pass == fill pass) Holds β€” sum(wTotalLength)+sizeof(usb_temp_data) == independent recomputation for every template/speed sweep_template() size check; no SIZE MISMATCH lines
Invalid template indices rejected Holds for 9..1023, 0x8000, 0xFF00, 0xFFFF (USB_ERR_INVAL, no state left behind) run logs
USB_TEMP_PHONE (7) unreachable Reproduced β€” setup_by_index(7) β†’ USB_ERR_INVAL (err=3), while direct usb_temp_setup(udev, &usb_template_phone) succeeds at FULL/HIGH and serves 42 valid descriptors (run.2.log phone lines, anom=0) run.initial-expectation-fixes.log line 2; run.2.log phone sweep lines

Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0 ... x86_64, usb compiled into kernel, usb_template.ko from /boot/kernel. Three full sweep runs (initial + 2 confirmation), no panic, guest still up; modules unloaded and /tmp/utfz removed afterwards (guest returned to kldstat baseline).

Why the phone finding is only Info

usb_template_phone is dead code: it cannot be selected (usb_temp_setup_by_index switch, sys/bus/u4b/template/usb_template.c:1346-1373, has cases for MSC/CDCE/MTP/MODEM/AUDIO/KBD/MOUSE/SERIALNET but not PHONE), so an operator setting hw.usb.usb_template=7 gets a silent failure ("maybe the USB template module has not been loaded", usb_device.c:1729). No memory-safety, information-disclosure, or privilege impact: the error path is a clean USB_ERR_INVAL. Fix is one line (add the case) or delete the template (what upstream FreeBSD eventually did).

Negative results with weight (what was hunted and killed)

  1. OOB via host-controlled wValue[0] config index β€” usb_temp_get_config_desc() bounds the index against the generated bNumConfigurations (usb_template.c:1046-1049) and walks only kernel-written wTotalLength fields; empirically exercised for every index 0..254 Γ— every request type (0 anomalies).
  2. String/langid dispatch β€” usb_temp_get_string_desc() (usb_template.c:1095-1109) forwards host wIndex/wValue[0] to the per-template getters; all nine getters bounds-check string_index < *_MAX against designated arrays and gate langid == 0x0409 (e.g. usb_template_msc.c:170-193); MTP's 0xEE/MS-OS paths return fixed static descriptors. Full index/langid sweep clean.
  3. wTotalLength math / two-pass builder β€” sizing pass and fill pass walk identical const template tables; wTotalLength is written as the exact chunk size (usb_template.c:398-403); proven equal to an independent recomputation for all 27 template/speed combos.
  4. Registry/unload races β€” the publish-then-maybe-free window (usb_template_ptr set at usb_template.c:1288, freed at :1322 on resolve failure) is unreachable by concurrent control requests: gadget ep0 transfers are only started by uhub_explore β†’ usbd_ctrl_transfer_setup (usb_hub.c:563-565), i.e. after usb_alloc_device() (which contains the only usb_temp_setup_by_index_p call site, usb_device.c:1727) completes and registers the device. usb_temp_unload()'s 1-second tsleep grace (usb_dynamic.c:117-130) is a kldunload-time (root-only) hazard, not an attacker path.
  5. Fragmented control transfers β€” the consumer's max_len -= off (usb_handle_request.c:746) cannot underflow via template descriptors: control_rem is decremented by bytes actually sent (usb_transfer.c:1608-1626), and every fragment serves the same deterministic descriptor, so cumulative off ≀ served length. The subdev-handlers that can change demux results mid-transfer use the offset==0 ? payload : 0 pattern (e.g. gadget/g_modem.c), keeping max_len - off == 0 on later fragments.
  6. usb_hw_ep_get_needs bounds β€” ep_curr bounded by ep_end = ues->ep + USB_EP_MAX (usb_template.c:686,726-729); bmInAlloc/bmOutAlloc are 2 bytes and every index derives from UE_ADDR(0x0f) or best_n < USB_EP_MAX/2 (:586,642-651,760-786).
  7. Kernel-side writes from host data β€” none: usb_temp_get_desc() serves only READ requests; all SET requests stall to the vendor fallback (usb_template.c:1140-1216); MTP's vendor descriptor is gated on UT_READ_VENDOR_DEVICE + fixed wIndex values returning static data.

Known-fragile-but-benign (filed as separate Info hardening note)

union usb_device_scratch (usb_device.h:171-179) overlays struct usb_temp_setup (32 B) on usb_hw_ep_scratch_sub ep[0..1]; usb_hw_ep_resolve() memsets and repopulates the union (usb_template.c:915-922) inside usb_temp_setup(). Today this is safe only because uts->err is re-stored from the resolve return value (:1298,1311) before every read and uts->buf/size are never read after resolve begins β€” the harness's clean PASS runs (setup err==0 for all FULL/HIGH template builds) confirm the current ordering holds.

Cosmetic noise (no finding)

Every template-setup failure path calls usb_temp_unsetup() with usb_template_ptr == NULL β†’ usbd_free_config_desc: nullpointer console spew (usb_template.c:1322,1334-1339; usb_request.c:1318-1326). Observed 33Γ— during the runs. Log noise only.

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

Info-only functional fix; kernel rebuild not warranted. The fix was validated indirectly and empirically: the harness drives usb_temp_setup(udev, &usb_template_phone) directly - exactly what the added case invokes - and the phone template builds cleanly and serves valid descriptors at FULL/HIGH (run.2.log, anom=0), so the one-line case addition is proven behaviorally. fix.diff is git-apply-clean against sys/.

run.2.log: 'FUZZ: template phone speed 2 swept (calls=176868017 nonnull=355 ... anom=0)' and 'FUZZ: template phone speed 3 swept (... nonnull=380 ... anom=0)'; git apply --check findings/poc/DF-5353/fix.diff -> clean.
↓ fix.diffper-fix-DF-5353

Confirmed kernel references

Detail

Evidence (decisive lines)

run.initial-expectation-fixes.log line 2 ('setup_by_index(7) err=3'); run.2.log phone sweep lines + final 'FUZZ: PASS calls=187272018 nonnull=380 stalls=187271638 anomalies=0'; VERDICT.md table maps each claim to its decisive lines; fix.diff applies cleanly to sys/ (git apply --check).

PoC changes

No seed PoC existed - harness written from scratch for this pass-2 run. Iterations: (1) added generated opt_usb.h/opt_bus.h/bus_if.h/device_if.h for the out-of-tree KLD build; (2) mirrored usb_template.c's exact include order; (3) fixed variadic kprintf format under -Werror; (4) after run 1, corrected harness expectations: the template enum has NINE values (SERIALNET=8 is valid; my loop had assumed 0..7) and SUPER-speed setup legitimately fails USB_ERR_INVAL because template mps[] tables define FULL/HIGH only (usb_template.c:221-225); (5) added usb_template_phone to the direct-setup table for run 3 to prove the template itself is functional.

Verified recommended fix

Add the missing dispatch case: 'case USB_TEMP_PHONE: err = usb_temp_setup(udev, &usb_template_phone); break;' (or delete the dead phone template as upstream FreeBSD did).

Verdict

Info-level functional defect reproduced on the guest: usb_temp_setup_by_index() (sys/bus/u4b/template/usb_template.c:1346-1373) has no USB_TEMP_PHONE case, so template index 7 (enum value advertised in sys/bus/u4b/usb_ioctl.h:56-67; template compiled into usb_template.ko per template/Makefile and nm) always returns USB_ERR_INVAL - the shipped phone gadget is dead code. Verified empirically: setup_by_index(7) -> err=3 while a direct usb_temp_setup(udev, &usb_template_phone) succeeds at FULL/HIGH speed and serves internally consistent descriptors (run.2.log 'phone' sweeps, anom=0). Simultaneously, the pack is the pass-2 negative-evidence record for the whole file: 187,272,018 crafted host requests through the real descriptor demux (all descriptor types x indices x langids x request types x 9 templates x 3 speeds) yielded ZERO anomalies and no panic; the two-pass builder matched an independent size recomputation for every template/speed. No memory-safety, leak, or privilege impact; fix is a one-line case addition (fix.diff) or deleting the template as upstream FreeBSD did.