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

uint8_t loop-counter wrap in uvc_ctrl_init_dev causes unbounded kernel heap overflow from malicious USB descriptor

Field Value
ID DF-1046
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
CWE CWE-787 Out-of-bounds Write; CWE-190 Integer Overflow or Wraparound
File sys/bus/u4b/uvc/uvc_ctrls.c
Lines 917-978 (esp. 925, 964, 968-973); counting fn at 899-915
Area bus/u4b/uvc (USB Video Class control parser)
Confidence certain
Discovered 2026-07-14
Reported pending
Known CVE none
CVE match dfly_specific

Summary

uvc_ctrl_init_dev() declares its bitmap-scan loop counter as uint8_t (uvc_ctrls.c:925) but iterates bCtrlSize * 8 times (line 964). bCtrlSize is an attacker-controlled descriptor byte (up to ~245, validated only by bLength fit in uvc_drv.c:2288-2292). When bCtrlSize >= 32 the product exceeds 255 and the uint8_t counter wraps 255β†’0, making the loop condition permanently true. The allocation at line 957 is correctly sized by uvc_ctrl_count_control() (which uses int i, line 902) for the true number of set bits, but the wrapping init loop re-processes the same set bits every 256 iterations and advances ctrl++ (line 973) past the allocation indefinitely β€” writing fully-initialized struct uvc_control entries (containing pointers and triggering nested kmallocs for uvc_data/sub_infos) into adjacent kernel heap. The result is an unbounded kernel heap overflow at device-attach time, triggerable by any malicious USB webcam.

Root cause

Type mismatch between the counting function and the initialization loop:

  1. uvc_ctrl_count_control() (line 902): int i = 0; β€” correctly iterates all bCtrlSize * 8 bits (up to 2040), returns the TRUE count of set bits. This count sizes the allocation.

  2. uvc_ctrl_init_dev() (line 925): uint8_t i = 0; β€” WRONG. At line 964: for (i = 0; i < bCtrlSize * 8; i++). Since i is uint8_t, when bCtrlSize * 8 > 255 (i.e., bCtrlSize >= 32), i wraps from 255 to 0 and the comparison i < (int)(bCtrlSize * 8) (promoted) is always true. The loop never terminates.

  3. Each iteration that finds a set bit executes (lines 968-973): c ctrl->topo_node = topo_node; ctrl->index = i; uvc_ctrl_initialize_control(ctrl); /* allocates uvc_data, sub_infos, populates info */ ctrl++; After the first full sweep of [0,255] writes exactly nctrls entries (correct), every subsequent wrap writes nctrls MORE entries past topo_node->controls (allocated for nctrls entries at line 957). This is an unbounded heap overflow.

  4. bCtrlSize is read directly from attacker-controlled USB descriptors: - PU: uvc_drv.c:2288 ctrls_mask_size = pu_desc->bControlSize; - CT: uvc_drv.c:2209 copies byte at offset 14 of it_desc. - XU: uvc_drv.c:2333-2334 reads from descriptor.

The only validation (uvc_drv.c:2289) is bLength < ctrls_mask_size + p (p=9 or 10), which allows bControlSize up to ~245 when bLength=255.

  1. uvc_ctrl_initialize_control() (line 971, defined at 809) on each OOB slot reads ctrl->topo_node (which was just assigned the valid topo_node pointer at line 968) and ctrl->index (the wrapped i), then for known PU/CT bit indices (0-18) allocates uvc_data via kmalloc and builds full sub_info mappings β€” so the overflow doesn't just scribble, it plants fully-valid-looking control structures with live pointers into adjacent heap.

Threat model & preconditions

  • Attacker position: Anyone who can attach a USB device to the target system (physical USB plug-in, compromised internal webcam firmware, USB passthrough in a VM, or a malicious dock/hub). No privileges, no authentication, no user interaction beyond plug-in.
  • Privileges gained or impact: Unbounded kernel heap corruption with reliable kernel panic (DoS) and potential local privilege escalation β€” the overwritten adjacent heap objects may contain function pointers or credential structures; struct uvc_control planted into adjacent slots contains live sub_info pointers whose get/set fields and v4l2_id fields could be weaponized with heap grooming. The XU path additionally means the attacker controls bControlSize without needing a spec-valid camera terminal.
  • Required config or capabilities: Default kernel with uvc configured (the default for any DragonFlyBSD install that supports USB video). USB port access.
  • Reachability: uvc_drv_attach β†’ uvc_ctrl_init_dev at device enumeration time. The bug fires on plug-in; no userspace action is needed.

Proof of concept

PoC source: findings/poc/DF-1046/malicious_uvc_descriptor.bin and findings/poc/DF-1046/README.md

Required descriptor payload (VideoControl interface, Processing Unit):

Field Value Meaning
bDescriptorType 0x24 CS_INTERFACE
bDescriptorSubtype 0x05 UDESCSUB_VC_PROCESSING_UNIT
bLength 0xFF 255 (satisfies bLength >= ctrls_mask_size + 9)
bControlSize 0x20 32 β€” THE TRIGGER: β‰₯32 causes uint8_t wrap
bmControls[0] 0x01 bit 0 set = Brightness control present
bmControls[1..31] 0x00 rest zero
pad to bLength β€” fill remaining bytes to reach bLength=255

With bControlSize=32 and 1 bit set: uvc_ctrl_count_control returns nctrls=1. kmalloc allocates 1 Γ— sizeof(struct uvc_control) (~150 bytes). The init loop iterates i=0..255 (finds bit 0, writes 1 entry in-bounds), then i wraps to 0, finds bit 0 again, writes a SECOND entry out-of-bounds, ctrl++ advances. Repeat forever: each 256-iteration sweep writes 1 more OOB entry. Within seconds the heap is massively corrupted and the kernel panics.

Build & run

No userspace binary needed β€” the bug is in the kernel driver triggered at attach. Delivery options:

  • Option A (QEMU): Create a USB device configfs gadget on a Linux host with the above descriptor, pass through to the DF guest via -device usb-host.
  • Option B (hardware): Flash a GreatFET One or similar USB emulator with a UVC gadget descriptor having bControlSize=32.
  • Option C (syzkaller): Use syzkaller's USB fuzzing (vusb) interface.

Expected output

uvc0: <malicious USB camera> at usbus0
uvc0: Processing Unit: bControlSize=32 bmControls=0x01...
Fatal trap 12: page fault while in kernel mode
fault virtual address   = 0x<address past topo_node->controls allocation>
uvc_ctrl_init_dev(...) at uvc_ctrls.c:973    (ctrl++ past end)
uvc_drv_attach(...) at uvc_drv.c:...
device_probe_and_attach(...) at subr_bus.c:...

A DEBUG/INVARIANTS kernel will additionally emit slab corruption warnings from adjacent freed/allocation metadata.

Impact

Any USB device that presents a UVC descriptor with bControlSize >= 32 triggers an unbounded kernel heap overflow at attach. The overflow is in M_UVC slab but the unbounded nature quickly crosses into adjacent slabs. With heap grooming the planted struct uvc_control entries (containing live pointers in topo_node, uvc_data, sub_infos fields) can be turned into a code-execution primitive. Without grooming, the result is a deterministic kernel panic at device plug-in.

This is a "kernel memory corruption" finding per the AGENT.md severity rubric, hence High even though the CVSS vector reflects the physical-access constraint (AV:P).

Change the loop counter from uint8_t to unsigned int so it can represent the full range [0, bCtrlSize * 8) without wrap. Also add a defensive bound on ctrl to prevent any future mismatch between the count and the loop from causing an overflow.

--- a/sys/bus/u4b/uvc/uvc_ctrls.c
+++ b/sys/bus/u4b/uvc/uvc_ctrls.c
@@ -922,2 +922,2 @@ uvc_ctrl_init_dev(struct uvc_softc *sc, struct uvc_drv_ctrl *ctrls)
    struct uvc_topo_node *topo_node, *tmp;
    struct uvc_control *ctrl = NULL;
-   uint8_t bCtrlSize = 0;
+   unsigned int bCtrlSize = 0;
    uint32_t nctrls = 0;
    uint8_t *bmCtrls = NULL;
-   uint8_t i = 0;
+   unsigned int i = 0;
+   uint32_t ctrl_idx = 0;
    struct uvc_xu_node_info *node_info_xu = NULL;
    struct uvc_pu_node_info *node_info_pu = NULL;
    struct uvc_ct_node_info *node_info_ct = NULL;
@@ -963,4 +963,8 @@ uvc_ctrl_init_dev(struct uvc_softc *sc, struct uvc_drv_ctrl *ctrls)

        ctrl = topo_node->controls;
        for (i = 0; i < bCtrlSize * 8; i++) {
            if (uvc_test_bit(bmCtrls, i) == 0)
                continue;
+
+           if (ctrl_idx >= nctrls) {
+               kprintf("%s: bitmap/count mismatch (bit %u, "
+                   "ctrl_idx %u >= nctrls %u)\n",
+                   __func__, i, ctrl_idx, nctrls);
+               break;
+           }
+
            ctrl->topo_node = topo_node;
            ctrl->index = i;

            uvc_ctrl_initialize_control(ctrl);

            ctrl++;
+           ctrl_idx++;
        }
    }

The type change alone (uint8_t β†’ unsigned int for both bCtrlSize and i) closes the wrap. The added ctrl_idx guard is defense-in-depth: uvc_ctrl_count_control and the init loop now use the same-width counters, and the explicit bound prevents writing past the allocation even if the bitmap is somehow mutated between count and init. The same widening should be applied to the XU bControlSize path for consistency.

References

  • USB Device Class Definition for Video Devices Rev 1.5, Β§2.4.2 (Processing Unit) and Β§2.4.1 (Camera Terminal) β€” bControlSize field
  • sys/bus/u4b/uvc/uvc_ctrls.c:899-915 β€” uvc_ctrl_count_control (correct, uses int i)
  • sys/bus/u4b/uvc/uvc_ctrls.c:917-978 β€” uvc_ctrl_init_dev (wrong, uses uint8_t i)
  • sys/bus/u4b/uvc/uvc_drv.c:2209, 2288-2289, 2333-2334 β€” attacker-controlled bControlSize
  • CWE-190 Integer Overflow or Wraparound
  • CWE-787 Out-of-bounds Write

Timeline

  • 2026-07-14 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1046 Β· 13 files
FileTypeDescriptionSize
harness.c trigger-source userspace harness replicating uvc_ctrl_init_dev loop semantics; -DBUGGY=uint8_t i, default=unsigned int i 4.6 KB view raw
build.sh build-script cc -DBUGGY/-default into harness_buggy/harness_fixed 615 B view raw
run.sh run-script runs both variants at bCtrlSize=32 529 B view raw
malicious_uvc_descriptor.bin descriptor-blob 255-byte UVC Processing-Unit descriptor with bControlSize=32 (for live USB delivery, not exercisable on this guest) 255 B ↓ download
harness_run.log run-log full harness output on guest, buggy vs fixed, multiple bCtrlSize values 1.5 KB view raw
fix_run.log fix-validation harness re-run on patched #1 kernel: BUGGY still 4096 OOB, FIXED 0 OOB 995 B view raw
env.txt environment uname, cc version, sysctls, kldstat, pciconf (NO USB HW) 445 B view raw
source_after_patch.txt source-diff grep of patched source showing the new kprintf guard at line 970 126 B view raw
fix.diff suggested-fix git-apply-able: widen bCtrlSize+i to unsigned int, add ctrl_idx>=nctrls guard 932 B view raw
VERDICT.md verdict full narrative: mechanism, primitive, harness proof, fix, fix-validation 8.6 KB ↓ raw
README.md readme original PoC readme (USB-gadget delivery path) 5.2 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme original PoC readme (USB-gadget delivery path)
↓ download raw

DF-1046 PoC β€” UVC bControlSize uint8_t wrap β†’ unbounded heap overflow

Trigger

A malicious USB Video Class device descriptor with bControlSize >= 32 in its Processing Unit (PU), Camera Terminal (CT), or Extension Unit (XU) descriptor. The kernel's uvc_ctrl_init_dev declares its bitmap-scan loop counter as uint8_t, so the loop wraps 255β†’0 and never terminates when bCtrlSize * 8 > 255. Each wrap writes another full struct uvc_control entry past the kmalloc'd array, corrupting adjacent kernel heap until the kernel page-faults.

Malicious Processing Unit descriptor (minimal)

Field Offset Value Meaning
bLength 0 0xFF 255 (satisfies bLength >= ctrls_mask_size + 9)
bDescriptorType 1 0x24 CS_INTERFACE
bDescriptorSubtype 2 0x05 UDESCSUB_VC_PROCESSING_UNIT
bUnitID 3 0x02 arbitrary, must be unique within VC interface
bSourceID 4 0x01 references Camera Terminal (or any other entity)
wMaxMultiplier 5..6 0x00 not used
bControlSize 7 0x20 32 β€” THE TRIGGER (>= 32 causes uint8_t wrap)
bmControls[0..31] 8..39 0x01, 0x00 Γ— 31 bit 0 set (Brightness), rest zero
bmVideoStandards 40 0x00 not used
pad 41.. 0x00 fill to reach bLength=255

The descriptor must be embedded in a valid UVC VideoControl interface with at least one Camera Terminal and one Input Terminal so topology construction succeeds. The Linux configfs USB gadget subsystem is the most convenient way to deliver it.

Build a USB gadget on a Linux host (configfs)

#!/bin/sh
# Run as root on a Linux host with dwc2/dummy_hcd or a real USB controller
# in gadget mode.
MOD=gadget-zero-df1046
GADGET=/sys/kernel/config/usb_gadget/$MOD

mkdir -p $GADGET
echo 0x1d6b > $GADGET/idVendor      # Linux Foundation
echo 0x0104 > $GADGET/idProduct     # Multifunction Composite Gadget
echo 0x0100 > $GADGET/bcdDevice
echo 0x0200 > $GADGET/bcdUSB

mkdir -p $GADPT/strings/0x409
echo "DF1046" > $GADGET/strings/0x409/serialnumber
echo "DFPoC"  > $GADGET/strings/0x409/manufacturer
echo "MaliciousUVC" > $GADGET/strings/0x409/product

mkdir -p $GADGET/configs/c.1/strings/0x409
echo "Config 1" > $GADGET/configs/c.1/strings/0x409/configuration

# Build the UVC function β€” most fields are the kernel UVC gadget defaults,
# but the Processing Unit bmControls mask must be expanded to 32 bytes with
# at least one bit set.  Linux's uvc-gadget configfs exposes this via
# functions/uvc.0/control/header/h/bmControls when extended.
# (For a minimal PoC we patch the gadget's descriptor table at runtime; a
# real maintainer reproduction can also use a custom GreatFET/Rubber Ducky
# device that emits the raw descriptor bytes above.)

mkdir -p $GADGET/functions/uvc.0
# ... configure Processing Unit with bControlSize=32 ...

ln -s $GADGET/functions/uvc.0 $GADGET/configs/c.1/

# Bind to a UDC
UDC=$(ls /sys/class/udc | head -1)
echo "$UDC" > $GADGET/UDC

For a self-contained PoC, use a GreatFET One or ** facedancer** with the byte stream above. The 256-byte descriptor payload is in malicious_uvc_descriptor.bin in this directory.

Run under QEMU

# Boot a DFly guest, pass the host gadget through.
qemu-system-x86_64 -enable-kvm -m 1G \
    -device usb-host,vendorid=0x1d6b,productid=0x0104 \
    -drive file=dragonfly.img,format=raw

Or with the GreatFET plugged into the host, just usb-host its real VID/PID.

Expected output

uvc0: <Malicious UVC camera> at usbus0
uvc0:  Processing Unit: bControlSize=32 bmControls=0x01...
Fatal trap 12: page fault while in kernel mode
fault virtual address   = 0x<address past topo_node->controls allocation>
cpuid = 0; apic id = 00000000
Trace:
uvc_ctrl_init_dev() at uvc_ctrls.c:973      (ctrl++ past end)
uvc_drv_attach() at uvc_drv.c:...
device_probe_and_attach() at subr_bus.c:...
...

Static verification fallback

If hardware/QEMU is impractical, both of these are static-verification wins:

  1. Confirm uvc_ctrl_count_control at uvc_ctrls.c:902 uses int i.
  2. Confirm uvc_ctrl_init_dev at uvc_ctrls.c:925 uses uint8_t i.
  3. Confirm the loop at uvc_ctrls.c:964 is for (i = 0; i < bCtrlSize * 8; i++).

The type mismatch is the bug; bCtrlSize * 8 can reach 245*8=1960, far beyond uint8_t's range.

Kernel references

VERDICT.md verdict full narrative: mechanism, primitive, harness proof, fix, fix-validation
↓ download raw

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)

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: harness BUGGY=4096 OOB writes; FIXED=0. Patched uvc.ko compiles+loads+disasm confirms 32-bit counter. Live not_testable (no USB HW).

BEFORE: 4096 OOB writes. AFTER: 0 OOB writes (3 bCtrlSize values tested). uvc.ko kldload rc=0.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Tue Jul 14 10:50:44 UTC 2026

Confirmed kernel references

Detail

Exploit chain

BLOCKED by valid hard blocker: no USB controller on guest, uvc.ko not loaded. Path reachable only at USB device-attach. Primitive proven at harness level: unbounded heap overflow with live pointers. Escalation needs USB HW passthrough.

Evidence (decisive lines)

BUGGY: 4096 OOB writes, 2147487745 iterations. FIXED: 0 OOB, 1 iteration. No USB HW on guest (pciconf).

PoC changes

Authored: harness.c (uint8_t vs unsigned int loop comparison), build.sh, run.sh, fix.diff (widen i+bCtrlSize to unsigned int + ctrl_idx>=nctrls guard), VERDICT.md, manifest.json.

Verified recommended fix

Change uint8_t i and bCtrlSize to unsigned int at uvc_ctrls.c:925; add ctrl_idx>=nctrls break guard. Matches finding proposal. Full diff in findings/poc/DF-1046/fix.diff.

Verdict

REPRODUCED (harness). uvc_ctrl_init_dev uint8_t i loop counter wraps at bCtrlSize*8>=256 (uvc_ctrls.c:925/964). Harness BUGGY: 4096 OOB writes (struct uvc_control with live pointers) past nctrls-sized alloc. FIXED (unsigned int): 0 OOB. Live requires USB HW absent on guest.