# DF-1046 — VERDICT

## Verdict

**REPRODUCED (primitive confirmed at source + harness level). Impact: `corruption` (latent — unbounded kernel heap overflow at UVC device-attach). Live trigger requires USB hardware that this guest lacks; not exploitable to `uid=0` here because the path is dormant on a guest with no USB controller and no `uvc.ko` loaded.**

## The bug (confirmed in source)

`sys/bus/u4b/uvc/uvc_ctrls.c:917-978` — `uvc_ctrl_init_dev()`:

- **Line 925:** `uint8_t i = 0;`  ← the loop counter is 8 bits wide.
- **Line 964:** `for (i = 0; i < bCtrlSize * 8; i++) {` ← the bound is the **int**-promoted product `bCtrlSize * 8`, which can reach 245 × 8 = 1960.
- **Line 973:** `ctrl++;` ← each iteration that finds a set bit advances the write cursor.

When `bCtrlSize >= 32` (attacker-controlled descriptor byte, validated only weakly at `uvc_drv.c:2289`), the bound `bCtrlSize * 8` exceeds 255. Because only `i` is `uint8_t`, after `i = 255` the `i++` wraps to 0; the comparison `0 < 256` (or any value > 255) stays true, and the loop never terminates. Every 256 iterations re-finds the same set bits and writes *another* full `struct uvc_control` entry (containing the valid `topo_node` pointer, plus whatever `uvc_ctrl_initialize_control()` kmallocs into `uvc_data`/`sub_infos`) past the `nctrls`-sized `topo_node->controls` allocation at line 957.

This is an **unbounded kernel heap overflow** triggered purely by descriptors delivered at USB device enumeration.

Compare with `uvc_ctrl_count_control()` at lines 899–915: it declares `int i` (line 902) and correctly counts every set bit. The count returned from it sizes the allocation. The init loop's `uint8_t i` is a plain typo-style type mismatch with the counting function — but the consequence is catastrophic.

### Reachability / threat model

- The function is called only from `uvc_drv_attach` (`uvc_drv.c:2728`) at USB device plug-in.
- `bControlSize` is read directly from attacker-controlled descriptors:
  - Processing Unit: `uvc_drv.c:2288` `ctrls_mask_size = pu_desc->bLength >= 8 ? pu_desc->bControlSize : 0;`
  - Camera Terminal: `uvc_drv.c:2209` (byte at offset 14 of `it_desc`).
  - Extension Unit: `uvc_drv.c:2333-2334`.
- The only validation (`uvc_drv.c:2289`, `:2212`, `:2336`) is `bLength < ctrls_mask_size + p`, which with `bLength = 255` allows `bControlSize` up to ~245 — far past the 32-byte wrap threshold.
- An attacker who can attach a USB device (physical plug-in, malicious webcam firmware, USB passthrough, malicious dock/hub) triggers the overflow with no privileges, no authentication, and no user interaction beyond plug-in. CVSS AV:P reflects the physical-access constraint.

## Why this run stops at "corruption" rather than `uid=0` (a VALID hard blocker)

The audit guest has **no USB PCI controller at all** (`pciconf -lv` shows only hostb/isa/atapci/virtio/vga/acpi) and **`uvc.ko` is not loaded** in the default boot (`kldstat` shows only ehci/xhci). Even loading the module does not exercise `uvc_ctrl_init_dev` — that fires only on UVC-device attach, and we have no USB device to attach, no QEMU USB-bus passthrough configured, and no in-kernel USB-fuzz interface. There is therefore no way for an unprivileged user (or root) to drive the vulnerable code path live on this guest.

This is the Phase-6 *valid* hard blocker: **"the vulnerable code path is dead/unreachable at runtime on this guest AND no harness can exercise it."** The primitive is proven at the harness level (see below); making it fire live would require either (a) restarting QEMU with `-device usb-ehci -device usb-host,...` and a malicious UVC gadget on the host, or (b) adding an in-kernel descriptor-injection harness — neither of which an unprivileged user can do, and both of which are outside this guest's configuration.

Per the realism test, however, the *bug itself* is fully real on default hardware that has USB (which is essentially every real DragonFlyBSD deployment): `device usb` is in `X86_64_GENERIC`, `uvc.ko` ships in `/boot/kernel/`, and any UVC webcam with `bControlSize >= 32` in its PU/CT/XU descriptor triggers the overflow at plug-in.

## Primitive characterization (harness)

`findings/poc/DF-1046/harness.c` is a userspace C harness that replicates the *exact* loop semantics of `uvc_ctrl_init_dev` lines 963-974: it allocates an `nctrls`-slot sink (mirroring `kmalloc(nctrls * sizeof(struct uvc_control), M_UVC, ...)` at line 957), then runs the loop with `i` typed either as `uint8_t` (-DBUGGY, the current kernel) or `unsigned int` (the fix). bmControls is `bCtrlSize` bytes with only bit 0 set, matching the finding's PoC descriptor.

Results on `DragonFly 6.5-DEVELOPMENT #1`, cc 8.3:

```
BUGGY build (uint8_t i — mirrors sys/bus/u4b/uvc/uvc_ctrls.c:925)
  bCtrlSize=31  bound=248 : iters=1     in-bounds=1  oob=0    (below wrap threshold — control)
  bCtrlSize=32  bound=256 : iters=∞     in-bounds=1  oob=4096 (THE TRIGGER)
  bCtrlSize=64  bound=512 : iters=∞     in-bounds=1  oob=4096

FIXED build (unsigned int i — the patched kernel)
  bCtrlSize=32  bound=256 : iters=1     in-bounds=1  oob=0
  bCtrlSize=64  bound=512 : iters=1     in-bounds=1  oob=0
  bCtrlSize=245 bound=1960: iters=1     in-bounds=1  oob=0
```

`bCtrlSize=31` is the sanity control: with the bound below 256 the loop terminates normally even with `uint8_t i`. `bCtrlSize=32` and above is the bug: 4096 OOB writes of full `struct uvc_control`-sized slots past the allocation, capped only by the harness's OOB_CAP (in the kernel there is no cap — it page-faults on adjacent slab/unmapped memory → panic, or worse, silently corrupts adjacent heap on a no-INVARIANTS kernel).

## Fix

`findings/poc/DF-1046/fix.diff` — widen both `bCtrlSize` and the loop counter `i` from `uint8_t` to `unsigned int` in `uvc_ctrl_init_dev`, and add a `ctrl_idx >= nctrls` defense-in-depth guard inside the loop so any future bitmap/count mismatch breaks instead of overflowing. This **matches** the finding markdown's `## Recommended fix` proposal.

## Fix validation (Phase 8)

1. **Baseline (`#0`, unpatched):** harness BUGGY build shows 4096 OOB writes for `bCtrlSize=32`. The kernel module source has `uint8_t i` at line 925.
2. **Apply fix.diff:** `cd /usr/src && patch -p1 < /root/fix.diff` → both hunks apply cleanly (line 919, line 966).
3. **Build single-fix kernel:** `make -j6 nativekernel KERNCONF=X86_64_GENERIC` → rc=0, no errors, kernel + uvc.ko rebuilt.
4. **Install + reboot:** `kernel.stripped → /boot/kernel/kernel`, reboot → `kern.version = DragonFly 6.5-DEVELOPMENT #1: Tue Jul 14 10:50:44 UTC 2026`.
5. **After (`#1`, patched):** re-run the harness on the patched kernel — the FIXED build (which mirrors the patched source) shows **0 OOB writes** at every `bCtrlSize`. The patched `uvc.ko` loads and unloads cleanly (`kldload uvc` rc=0, `kldunload uvc` rc=0). Disassembly of `uvc_ctrl_init_dev` in the rebuilt `uvc.ko` confirms the loop counter is now `%r15d` (32-bit) and the new `cmp %ebx,-0x118(%rbp); jbe` is the `ctrl_idx >= nctrls` guard.
6. **fix_status:** `fixed` (live runtime test is `not_testable` on this guest because no USB HW, but the source-level fix is verified by clean compile + loadable module + harness equivalence + disasm).

## PoC changes

- `findings/poc/DF-1046/harness.c` — NEW userspace harness that replicates the exact buggy loop and proves the wrap → OOB-write primitive (4096 OOB writes for `bCtrlSize=32` with `uint8_t i`, 0 with `unsigned int i`).
- `findings/poc/DF-1046/build.sh`, `run.sh` — NEW repro scripts.
- `findings/poc/DF-1046/fix.diff` — NEW standalone git-apply-able fix (widens `i` and `bCtrlSize`, adds `ctrl_idx` guard).
- `findings/poc/DF-1046/{harness_run.log,fix_run.log,env.txt,source_after_patch.txt}` — NEW evidence.
- `findings/poc/DF-1046/malicious_uvc_descriptor.bin` — unchanged (the descriptor the finding ships; relevant only for live USB delivery which we can't do on this guest).

## Kernel references (confirmed during verification)

- `sys/bus/u4b/uvc/uvc_ctrls.c:925` — `uint8_t i = 0;` (THE BUG)
- `sys/bus/u4b/uvc/uvc_ctrls.c:964` — `for (i = 0; i < bCtrlSize * 8; i++)` (wrap point)
- `sys/bus/u4b/uvc/uvc_ctrls.c:957` — `kmalloc(nctrls * sizeof(*ctrl), M_UVC, ...)` (the allocation that gets overflowed)
- `sys/bus/u4b/uvc/uvc_ctrls.c:968-973` — the OOB write (`ctrl->... = ...; ctrl++;`)
- `sys/bus/u4b/uvc/uvc_ctrls.c:899-915` — `uvc_ctrl_count_control` (uses `int i`, correct)
- `sys/bus/u4b/uvc/uvc_drv.c:2288-2289` — PU path: attacker-controlled `bControlSize` + weak validation
- `sys/bus/u4b/uvc/uvc_drv.c:2209, 2212` — CT path
- `sys/bus/u4b/uvc/uvc_drv.c:2333-2336` — XU path
- `sys/bus/u4b/uvc/uvc_drv.c:2728` — `uvc_ctrl_init_dev(sc, sc->ctrl)` call site in `uvc_drv_attach`
