# DF-5378 VERDICT

**Status: reproduced** (deterministic, production-code UAF with both a
**1-byte controlled write** into a freed kernel chunk and a **31-byte
kernel-heap disclosure per query** through the unprivileged
VIDIOC_QUERYCTRL/VIDIOC_QUERYMENU handlers; plus an uncontrolled variant that
panics the kernel). **Impact: memcorrupt (UAF write + kheap leak; panic
variant)**, confidence: certain.

## Root cause (path:line)

- `sys/bus/u4b/uvc/uvc_drv.c:2503-2511` — `uvc_drv_detach()` frees the whole
  control graph via `uvc_drv_destroy_ctrl(sc->ctrl)` at **2507** and only
  calls `uvc_v4l2_unreg(sc->video)` (→ `destroy_dev()`, uvc_v4l2.c:756-769)
  at **2511**, *after* the free. `uvc_drv_destroy_ctrl()`
  (uvc_drv.c:2453-2474) walks the graph **without taking `c->mtx`** and frees
  `sub_infos[]`, `menus[]`, `uvc_data`, `controls[]`, the `uvc_topo_node`
  list (`uvc_free_topo_node`, uvc_drv.c:1992-2011) and `sc->ctrl` itself
  (uvc_drv.c:2472).
- `/dev/videoN` is created **0666** (`uvc_v4l2.c:783-785`), so any local user
  can hold it open. A device disconnect (USB unplug — or an evil webcam whose
  firmware disconnects itself on demand) triggers that detach while a
  VIDIOC_QUERYCTRL/VIDIOC_QUERYMENU is in flight (or enters before
  destroy_dev() drains it).
- The in-flight ioctl then executes, on freed memory, inside the audited
  file:
  - `uvc_ctrls.c:1107` — `UVC_LOCK(&video->ctrl->mtx)` lockmgr on the freed
    `uvc_drv_ctrl` chunk;
  - `uvc_ctrls.c:1090` — STAILQ walk of freed `topo_nodes`;
  - `uvc_ctrls.c:1065` — **`ctrl->info.cached_sub_info_idx = j;`** a 1-byte
    write (value 0..4, attacker-influenced through the v4l2 id it searches)
    at a fixed offset of the freed `struct uvc_control` array chunk;
  - `uvc_ctrls.c:1001` / `uvc_ctrls.c:1150` — **`strlcpy()` of the freed
    `sub_info->v4l2_name` into the user-visible `query->name`/`qm->name`**:
    31 bytes of whatever now occupies the freed chunk are copied to
    userspace, repeatable at will.

Distinct from DF-2353 (that finding's victim is `sc->data`, freed at
uvc_drv.c:2506 and consumed by the format/frame paths in uvc_drv.c); the
victim here is `sc->ctrl`'s control graph (freed at 2507), consumed by the
two query handlers of uvc_ctrls.c, with different primitives (write + name
disclosure).

## Reproduction (KLD harness with production code, the DF-5305/5306 pattern)

The harness `#include`s the four real uvc driver sources, builds a real
control graph with the real `uvc_drv_init_ctrl()` + `uvc_drv_parse_ctrl()` +
`uvc_ctrl_init_dev()` from a crafted VideoControl-only descriptor blob
(camera terminal with bits 1 and 11: AE_MODE + PANTILT_ABSOLUTE), runs the
real `uvc_query_v4l2_ctrl()`, then executes the real
`uvc_drv_destroy_ctrl()` and reclaims every freed chunk **at its exact
address** (what a heap-grooming attacker does in the guaranteed race window).

mode 1 (controlled; guest stays up) — decisive lines from `run.log`:

    HARNESS DF-5378[baseline]: ret=0 name=<Tilt (Absolute)>
    HARNESS DF-5378: production uvc_drv_destroy_ctrl() done - ... DANGLING
    HARNESS DF-5378: all 11 freed chunks reclaimed at their original addresses
    HARNESS DF-5378[UAF query]: ret=0 name_len=31
      q.name = 5a 5a ... 5a 00      (31 marker bytes + NUL)
    *** UAF READ/LEAK CONFIRMED: 31 bytes of attacker-marked FREED-chunk
        content copied into the user-visible queryctrl.name ***
    *** UAF WRITE CONFIRMED: uvc_ctrls.c:1065 wrote cached_sub_info_idx=1
        into the freed-and-reclaimed controls[1] chunk (was 0xAA) ***
    HARNESS DF-5378[UAF menu]: ret=0, qm.name = 5b*31
    *** QUERYMENU disclosure confirmed (strlcpy from freed sub_info at
        uvc_ctrls.c:1150) ***

mode 2 (uncontrolled; guest dies) — `panic.txt`:

    HARNESS DF-5378[wild]: ctrl chunk poisoned; expect fatal trap reading
        ~0x5a5a5a5a5a5a5a5a+112 (topo_node->controls_num) ...
    Fatal trap 9: general protection fault while in kernel mode
    Stopped at uvc_search_control.isra.3+0x20: movl 0x70(%rax),%ecx

    i.e. the production walk dereferenced 0x5a5a5a5a5a5a5a5a + 0x70 — the
    freed ctrl chunk's poisoned `topo_nodes.stqh_first`. `vm.sh status`:
    down.

## Exploitability

- The window is not a narrow race in practice: the ioctl handlers are
  reachable until `destroy_dev()` drains (uvc_drv.c:2511), i.e. for the whole
  duration of `uvc_drv_destroy_ctrl()` plus unbound-open-fd time; an
  unprivileged user loops QUERYCTRL while the evil device disconnects.
- Demonstrated primitives: (a) fully attacker-controlled fake graph after
  same-address zone reclaim (the walk follows attacker pointers), (b) 1-byte
  write (value 0..4) at a fixed offset of a freed ~184-byte M_UVC chunk,
  (c) 31 bytes of kernel heap disclosed per query, (d) lockmgr on freed
  memory (contention on attacker-crafted lock words → further state
  corruption).
- uid=0 was not completed: the standard audit guest has no UVC device, so
  the real /dev/videoN path cannot be driven end-to-end here; the harness
  proves the primitives deterministically. On hardware, grooming the M_UVC
  size-class zones from the same 0666 device (other /dev/videoN ioctls
  allocate M_UVC: e.g. open/REQBUFS paths, cdev priv) is the route to
  turning the fake-graph control into arbitrary-pointer walk → write.

## Fix validation

`fix.diff` (applies with `git apply -p0` from /usr/src):
1. `uvc_drv_detach()`: destroy the cdev first (`uvc_v4l2_unreg()` drains
   in-flight ioctls) and NULL `video->ctrl` under the ctrl lock **before**
   `uvc_drv_destroy_data/destroy_ctrl`;
2. `uvc_query_v4l2_ctrl()`/`uvc_query_v4l2_menu()` (uvc_ctrls.c): return
   ENODEV when `video->ctrl == NULL`;
3. `uvc_drv_destroy_ctrl()`: hold `c->mtx` during teardown + `lockuninit()`.

Both patched files compile with `-Werror` in the harness build
(`fix_build.log`). Rebuilt against the patched sources and re-run
(`fix.log`):

    HARNESS DF-5378[fix baseline]: ret=0 name=<Tilt (Absolute)>
    HARNESS DF-5378[fix post-detach]: query ret=19 (ENODEV), NO freed-memory
    access, guest up

i.e. with the fixed detach sequence (NULL-out under lock, then destroy),
the production (patched) query handler refuses the dangling pointer instead
of walking freed memory, and the live-graph path is unaffected. Full-kernel
rebuild not run (harness pattern as in DF-5305/5306; the patch only touches
the two driver files, both compile-checked).

## Impact

Unprivileged local user + device disconnect ⇒ kernel use-after-free with
write primitive and repeatable kernel-heap disclosure (High). Panic variant
confirmed. Root-cause class matches the project's memcorrupt bucket.

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