# DF-5381 VERDICT — reproduced (engine primitive), fix validated

**File:** `sys/bus/u4b/usb_busdma.c`
**Finding:** The USB DMA copy engine performs no bounds enforcement
whatsoever. `usbd_get_page()` (usb_busdma.c:79-126) resolves any caller
offset against the page cache without consulting the loaded extent that
is already tracked in the same structure (`page_offset_end` /
`page_offset_buf`, set at usb_busdma.c:545/616-617 and 427-428), and
every copy primitive — `usbd_copy_in` (131), `usbd_copy_in_user` (159),
`usb_uiomove` (216), `usbd_copy_out` (248), `usbd_copy_out_user` (276),
`usbd_frame_zero` (304) — then `memcpy`/`copyin`/`copyout`/`uiomove`s
`min(res.length, len)` bytes to/from that unbounded address
(usb_busdma.c:140-147, 254-265).

## Reproduced? YES — three faces of the primitive, all on the standard guest

Baseline kernel: `DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026`
(x86_64, USB core compiled in; `nm /boot/kernel/kernel` shows
`T usbd_get_page/usbd_copy_in/usbd_copy_out/...`).

A root KLD (`df5381.c`) builds 32 fully legitimate 200-byte
`usb_page_cache` objects via the engine's own `usb_dma_tag_setup()` +
`usb_pc_alloc_mem()` API (the same path `usb_bus_mem_alloc_all` uses),
picks two heap-adjacent objects (KVA gap 512 = the kmalloc chunk), and
drives the engine exactly as a buggy driver would:

1. **Cross-object kernel-heap OOB WRITE (3/3 runs)** —
   `usbd_copy_in(pcA, 512, pattern, 16)` plants 16 bytes into pcB's
   allocation; read-back through pcB returns the planted bytes
   (run.log T2). This is precisely the DF-0998 / DF-2357 in-tree bug
   class (16-byte heap overwrites past USB DMA buffers that shipped in
   this tree), caught live.
2. **Cross-object kernel-heap OOB READ (3/3 runs)** —
   `usbd_copy_out(pcA, 512, ...)` returns pcB's contents (run.log T1);
   `usbd_frame_zero(pcA, 512, 16)` zeroes them (T3). In a real driver
   this is how adjacent kernel heap is exfiltrated through /dev/ugen*
   (the DF-1103 / DF-2608 leak pattern).
3. **Instant kernel panic (1/1)** — the same OOB read aimed at the page
   after a 2-page kmem buffer hit an unmapped kernel VA hole:
   `Fatal trap 12: page fault ... fault virtual address =
   0xfffff8011863e000 (= pcM.buffer+8192) ... Stopped at memcpy+0x55`
   from inside `usbd_copy_out` during `kldload` (panic.txt). Guest went
   down — a single miscalculated driver-side length is a kernel DoS.
4. **Unbounded pointers out of `usbd_get_page` (3/3)** —
   `get_page(pc, 0xFFFFFF00)` hands out `buffer + 0xFFFFFF00` with
   `length = UINT32_MAX` (T5); the multiseg page-array walk serves a
   stale/poisoned `physaddr` (0xdeadbeef0000) from 12 pages past a
   2-page load (T6) — i.e. HCDs would program DMA descriptors from
   garbage.

No escalation chain is claimed: no *live* unprivileged trigger exists in
the tree today. The one userspace-reachable client (`usb_generic.c`)
clamps `nFrames` (usb_generic.c:1069) and `sum(length) <=
max_data_length` (usb_generic.c:1166-1170), and `usbd_transfer_done`
truncates `actlen > sumlen` (usb_transfer.c:2952-2959). The finding is
therefore filed as **Info / defense-in-depth**: the engine is the single
chokepoint for ~120 call sites, at least four of which
(DF-0998, DF-2357, if_axge's device-derived `hdr_off`, if_urndis's
device-derived `rm_dataoffset`) have already depended on that arithmetic
being perfect everywhere.

## Bonus observation (same contract class)

`usb_pc_alloc_mem` dereferences `pc->tag_parent` without a NULL check
(usb_busdma.c:495 → `usb_dma_tag_find` → `movzbl 0x3a(%rdi)`, rdi=0):
an uninitialized page cache panics the kernel instantly (observed on the
guest during harness bring-up; `usb_transfer.c:842` is the only thing
that guarantees tag_parent is set). Kernel-driver-contract issue only.

## Fix validation — YES, `fixed`

`fix.diff` (git-apply-able against `sys/bus/u4b/usb_busdma.c`) adds:
- a bound in `usbd_get_page` Case 1: offset ≥ `page_offset_end -
  page_offset_buf` ⇒ return `length = 0`, `buffer = NULL`,
  `physaddr = 0`;
- `break` (or `EFAULT` for the _user/uiomove variants) on zero-length
  results in all six copy loops, so truncation terminates cleanly.

Applied to the guest's `/usr/src`, incremental `nativekernel` rebuild,
`installkernel`, reboot into `#1: Sat Oct 3 21:56:23 UTC 2026`, exact
same harness 3×:

| test | baseline (unpatched) | patched |
|------|----------------------|---------|
| T1 cross-object read  | PASS (sentinel read via pcA) | FAIL — clamp fired, no copy |
| T2 cross-object write | PASS (pattern in pcB)        | FAIL — pcB still holds sentinel ⇒ **not corrupted** |
| T3 cross-object zero  | PASS                         | FAIL — pcB untouched |
| T5 wild KVA/UINT32_MAX length | PASS            | length=0, no wild pointer |
| T6 stale physaddr served      | PASS            | physaddr=0, length=0 |
| guest | up | up (USB stack loads: ehci.ko, xhci.ko) |

In-extent engine copies keep working on the patched kernel (the T1 setup
plants and reads back the sentinel through the engine itself), so the
clamp does not break legitimate transfers. Two artifacts to not
mis-read on the patched runs (both are *proof the fix works*):
"T2b PASS" writes at offset 200 < the engine's own doubled 400-byte
allocation (safe by design of the usb_busdma.c:526-528 doubling), and
"T4 PASS" shows an *unwritten* destination buffer (the copy loop breaks
before touching it; the printed bytes are stale harness stack — note
they are byte-identical across runs, unlike a real read).

Residual gap (documented, out of scope of this diff): Case 2 ("Plain
PIO", usb_busdma.c:119-125) caches have no extent tracked at all; fully
closing them needs a new size field in `struct usb_page_cache`.

## Harness changes vs. the seeded idea

No seed PoC existed; the harness was authored from scratch. Iterations:
(1) first version panicked via the tag_parent NULL contract (kept as
evidence, harness now wires `tag_parent` like usb_transfer.c:842);
(2) second version's foreign-page read panicked on an unmapped hole
(kept as panic.txt, read now skipped); (3) final version stable 3/3.

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