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

REQBUFS-while-streaming frees bq->mem without draining product/consumer queues or resetting descriptors: USB completion engine writes frame data into freed kernel memory (UAF write) and, after a frame-size shrink, stale vbuf.bytesused underflows the bulk maxlen giving a controlled-offset heap OOB write

Summary

VIDIOC_REQBUFS is accepted while the queue is live; free_bufs_locked kfrees bq->mem but drains neither STAILQ nor resets descriptors, and the re-init loop covers only the new count while fill_v4l2 never clears bytesused: (A) count-shrink leaves descriptors with stale mem pointing at freed memory remaining on the product queue - usbd_copy_out writes frame data into freed kernel memory; (B) len-shrink keeps stale bytesused > new length so maxlen = length - bytesused underflows and the copy lands at mem_new + stale_bytesused = controlled-offset OOB write. KLD-rig-proven on production code across 3 independent builds: 2048 marker bytes written into the freed/reclaimed block at the predicted offset; Fatal trap 12 supervisor WRITE at exactly the predicted VA inside usbd_copy_out. Unpriv (0666 cdev) with a streaming camera - semi-controlled write, fully controlled for a malicious device; groomed victim = credential/pointer overwrite. Distinct from DF-1065 (mmap side). Queue-drain + descriptor-reset + unsigned-clamp fix validated (0 pattern bytes, no fault, unpriv clamps).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5434 Β· 20 files
FileTypeDescriptionSize
bufharness.c β€” 15.0 KB view raw
trig5434.c β€” 3.5 KB view raw
Makefile β€” 151 B ↓ download
build.sh β€” 194 B view raw
run.sh β€” 301 B view raw
run_panic.sh β€” 253 B view raw
build.log β€” 828 B view raw
fix_build.log β€” 828 B view raw
run.modes123.log β€” 2.8 KB view raw
run.vuln2.log β€” 856 B view raw
run.vuln3.log β€” 1.9 KB view raw
panic.txt β€” 873 B view raw
fix_run.log β€” 2.9 KB view raw
fix_run_user.log β€” 1.1 KB view raw
env.txt β€” 308 B view raw
fix.diff β€” 1.8 KB view raw
README.md β€” 4.5 KB ↓ raw
VERDICT.md β€” 6.4 KB ↓ raw
manifest.json β€” 1.7 KB view raw
verdict.json β€” 7.4 KB view raw

DF-5434 β€” UVC buffer-queue realloc UAF/OOB write (uvc_buf.c)

Finding

VIDIOC_REQBUFS is accepted while the buffer queue is live (no streaming-state check at sys/bus/u4b/uvc/uvc_v4l2.c:613-630, none in uvc_buf_queue_req_bufs() at sys/bus/u4b/uvc/uvc_buf.c:537). The realloc path calls uvc_buf_queue_free_bufs_locked() (sys/bus/u4b/uvc/uvc_buf.c:517-525), which frees bq->mem but leaves

  • the product/consumer STAILQs undrained, and
  • every bq->buf[i] descriptor un-reset (mem, vbuf.bytesused, status, link pointers all stale).

The USB completion engine (uvc_bulkbuf_sell_buf / uvc_buf_sell_buf) walks STAILQ_FIRST(&bq->product) and copies frame data through those stale descriptors:

  • (A) count-shrink UAF write β€” descriptors with index >= new count keep mem pointing at the freed block while remaining product-head: usbd_copy_out() writes device frame data into freed kernel memory (uvc_buf.c:268-270, uvc_buf.c:346-348). Deterministic, no race.
  • (B) len-shrink OOB write β€” the re-inited descriptor keeps a stale vbuf.bytesused (uvc_buf_fill_v4l2(), uvc_buf.c:91-102, never clears it); maxlen = length - bytesused (uvc_buf.c:265) underflows, nbytes = min(maxlen, len) becomes len, and the copy lands at mem_new + stale_bytesused β€” an out-of-bounds heap write at a fully attacker/device-chosen offset.

Not the same as DF-1065 (userspace mmap of freed pages): the victim here is the kernel's own completion path writing through stale driver-internal descriptors.

How to reproduce (guest, root for rig setup; trigger itself is uid-1001-capable)

# build (NOTE: uvc_buf.c is #included by bufharness.c and NOT in SRCS β€”
# always force a full rebuild after touching it:  rm -f *.o *.ko)
sh build.sh                     # KLD from pristine /usr/src uvc sources

# baseline (vulnerable):
kldload /root/build5434/bufharness.ko
sysctl hw.bufharness=1          # mode A: UAF write proof (marker 0x5A)
sysctl hw.bufharness=2          # marker 0xA7
sysctl hw.bufharness=3          # marker 0xC3
sysctl hw.bufharness=8          # signed-count probe (ENOMEM fold)
sysctl hw.bufharness=4          # mode B: underflow OOB write -> PANIC
                                # (predicted fault VA printed first)

# fix validation:
cd /usr/src && patch -p0 < fix.diff
cd /root/build5434 && cp /usr/src/sys/bus/u4b/uvc/uvc_buf.c . && rm -f *.o *.ko && make
(reboot) ; kldload ...; sysctl hw.bufharness=1..4,8   # all clean, guest stays up
/tmp/trig5434                                        # unpriv: count clamps to 8,
                                                     # QUERYBUF/QBUF OOB -> EINVAL

Expected output (vulnerable)

  • mode 1/2/3: HARNESS DF-5434[A*]: UAF-WRITE PROVEN: found 2048 bytes of 0x?? pattern inside freed/reclaimed block ... (0x800 payload bytes found at OLDmem+0x31000) β€” kernel wrote through the stale descriptor.
  • mode 4: PREDICTED fault VA = 0xfffff801... then Fatal trap 12 ... fault virtual address = <same VA>, supervisor write data, Stopped at memcpy+0xfa (inside usbd_copy_out).

Expected output (fixed)

  • modes 1-3: buf3 NOT re-inited: mem=0 status=0 bytesused=0x0; product head=0 and no hit: found 0 bytes β€” descriptors reset, queues drained, no write into freed memory.
  • mode 4: buf0: length=0x1000 bytesused=STALE 0x0 β€” stale bytesused is gone; in-bounds write; no fault, guest stays up.
  • mode 8 / trig5434: REQBUFS(0x80000000) succeeds with count=0x8 (clamped); every out-of-bounds QUERYBUF/QBUF returns EINVAL.

Files

  • bufharness.c β€” KLD: real uvc sources compiled in (DRIVER_MODULE neutered), production uvc_v4l2_reg() creates a real 0666 /dev/video96, sysctl drives the production completion path (uvc_bulkbuf_sell_buf) with a fabricated usb_page_cache exactly as uvc_drv_bulkfill_buf() does from the xfer callback.
  • trig5434.c β€” unprivileged userland probe (uid 1001 used): the signed-count candidate REQBUFS(0x80000000) + OOB index sweep (negative evidence: on this toolchain the uninit mem read compiles to ENOMEM β€” see VERDICT.md).
  • run.modes123.log β€” vulnerable: 3Γ— UAF-WRITE PROVEN + ENOMEM fold.
  • panic.txt β€” vulnerable: mode B fault at the predicted VA.
  • run.vuln2.log, run.vuln3.log β€” two further vulnerable runs (independent builds/reboots): stale descriptors + UAF write, guest dies on the dangling state.
  • fix_run.log, fix_run_user.log β€” fixed build: all modes clean.
  • build.log, fix_build.log, env.txt, fix.diff.
VERDICT.md
↓ download raw

DF-5434 β€” VERDICT

Status: reproduced (kernel UAF write + OOB write / page fault). Fix validated.

What was run

bufharness.ko compiles the unmodified production sources (uvc_buf.c, uvc_v4l2.c, uvc_drv.c, uvc_ctrls.c from /usr/src/sys/bus/u4b/uvc/) into a KLD, registers the production character device via the real uvc_v4l2_reg() (make_dev(..., 0666, "video96")), and drives the production completion function uvc_bulkbuf_sell_buf() (uvc_buf.c:222-312) from kernel context with a fabricated usb_page_cache (pc->buffer set, zeroed otherwise β‡’ "Case 2 - Plain PIO" in usbd_get_page(), usb_busdma.c:118-123 β€” the exact copy primitive the USB callback uses). The only fabricated state is what a streaming camera would otherwise provide (frame payloads, FID toggle); every victim code path is production code.

Primitive A β€” deterministic kernel UAF write (modes 1/2/3)

Sequence (all production calls): uvc_buf_queue_req_bufs(4, 0x10000) β†’ uvc_buf_queue_queue_buf(index=3) β†’ bulk sells fill buf3 (ACTIVE, bytesused=0x1000, product head) β†’ uvc_buf_queue_req_bufs(1, 0x10000) while the queue is live β†’ uvc_buf_queue_free_bufs_locked() (uvc_buf.c:517-525) frees the 0x40000 block, sets buf_count=1, re-inits only descriptor 0, drains nothing β†’ descriptor 3 still holds mem=<freed>, status=ACTIVE, and is still STAILQ_FIRST(&bq->product) (log: product head=... == buf3? YES) β†’ next bulk payload: usbd_copy_out(pc, 0, mem_freed + 0x30000 + 0x1000, 0x800) (uvc_buf.c:268-270) writes 0x800 bytes of device-controlled frame data into freed kernel memory.

Observed (run.modes123.log, three independent rounds/markers):

A1: UAF-WRITE PROVEN: found 2048 bytes of 0xa5 pattern inside freed/reclaimed block 0xfffff80118760000
A2: UAF-WRITE PROVEN: found 2048 bytes of 0x58 pattern ... 0xfffff80118788000
A3: UAF-WRITE PROVEN: found 2048 bytes of 0x3c pattern ... 0xfffff80118788000

2048 = exactly the 0x800-byte payload written at the predicted offset OLDmem+0x31000. In two later independent vulnerable builds (run.vuln2.log, run.vuln3.log) the same sequence left the guest dead on the dangling state (Fatal trap 12 ... Stopped at mode_a+0x342 while touching the freed block) β€” the driver's own stale pointers.

No race is involved: everything runs under bq->mtx; the bug is the state left behind by the free path, not a locking window. An unprivileged user reaches it on any streaming UVC camera with plain V4L2 ioctls (open 0666 /dev/videoN β†’ S_FMT/REQBUFS (pri) β†’ STREAMON β†’ QBUF β†’ frames flow β†’ REQBUFS(smaller count) β€” the v4l2 layer at uvc_v4l2.c:613 never checks streaming state).

Primitive B β€” stale-bytesused underflow OOB write (mode 4)

req_bufs(2, 0x2000000) β†’ sells accumulate buf0.vbuf.bytesused = 0x2000000 (mid-frame, ACTIVE) β†’ req_bufs(2, 0x1000) (smaller frame size: in production len = UGETDW(v->req.dwMaxFrameSize), uvc_v4l2.c:620, which VIDIOC_S_PARM refreshes from the device's probe response mid-stream β€” uvc_drv.c:1385-1393 probe = v->req; uvc_drv_probe_video(); v->req = probe; β€” or a malicious device controls directly) β†’ descriptor 0 is re-inited with length=0x1000 but bytesused stays 0x2000000 (uvc_buf_fill_v4l2(), uvc_buf.c:91-102, never clears it) β†’ next payload: maxlen = 0x1000 - 0x2000000 = 0xE0001000 (uvc_buf.c:265, underflow) β†’ nbytes = min(maxlen, len) = len β†’ usbd_copy_out writes at mem2 + 0x2000000 in a 0x2000-byte allocation.

Observed (panic.txt) β€” the harness prints the predicted VA before triggering:

PREDICTED fault VA = 0xfffff8011a7cc000 (mem2 + stale bytesused 0x2000000)
Fatal trap 12: page fault while in kernel mode
fault virtual address   = 0xfffff8011a7cc000      <-- EXACT MATCH
fault code              = supervisor write data, page not present
Stopped at      memcpy+0xfa:    repe movsq        <-- inside usbd_copy_out

Offset and extent of the OOB write are chosen by the previous session's accumulated bytesused (device/frame-size controlled) β€” a controlled kernel heap overflow primitive, not just a crash.

Killed candidate (documented, Info-level, NOT the High finding)

The signed-count path (num = *count as int, uvc_buf.c:549; clamp at :550 signed; alloc loop at :558 skipped for num<0; mem uninitialized at :564) was hypothesized to leave a wild bq->mem + sign-extended buf_count. Guest objdump of the built module proves the shipping gcc 8.3 folds the uninitialized read into a deterministic ENOMEM (172a: movslq %r12d,%rdi; jg 1635 … 1688: mov $0xc,%r12d), and mode 8 confirms at runtime: req_bufs(0x80000000) -> ENOMEM ... bq->mem=0 buf_count=0 unchanged. No memory-safety impact on this toolchain; filed separately as hardening (the fix.diff clamp removes the UB).

Fix validation (fix.diff applied to guest /usr/src, KLD force-rebuilt)

  • modes 1/2/3: buf3 NOT re-inited: mem=0 status=0 bytesused=0x0; product head=0 β†’ no hit: found 0 bytes in all three rounds (fix_run.log).
  • mode 4: buf0: length=0x1000 bytesused=STALE 0x0 β€” the stale bytesused is gone; write is in-bounds; no fault; guest up.
  • mode 8 + unpriv trig5434 (uid 1001): REQBUFS(0x80000000) ret=0 count=0x8 (clamped, V4L2-correct), every OOB QUERYBUF(8..23)/QBUF(16) returns EINVAL (fix_run_user.log).
  • vm.sh status β‡’ up after the full fixed sequence.

Baseline bad behaviors (UAF pattern bytes, predicted-VA fault) are gone.

Exploitability (honest ceiling)

Write primitive: device-frame-data content (semi-controlled: a camera looking at a controlled pattern; fully controlled for a malicious USB device) at a chosen offset into freshly freed M_UVC memory of attacker-selected size class (count Γ— frame size). Classic path to uid=0 is slab-grooming a victim object into the freed block and overwriting a function pointer / cred-adjacent structure; not developed to uid0 here because the guest has no UVC hardware β€” the KLD rig proves the kernel-side primitive with production code, which is the same code a real camera exercises. Requires a UVC device present and hw.usb.v4l2. v4l2_not_allowed == 0 (default).

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to guest /usr/src (patch -p0, 5 hunks clean); KLD force-rebuilt from the patched tree (rm -f .o .ko); modes 1/2/3: descriptors re-inited (mem=0 status=0 bytesused=0x0), product queue drained (head=0), 0 pattern bytes found in the freed block in all three rounds; mode 4: stale bytesused gone (0x0), write in-bounds, no fault; mode 8 + unpriv trig5434: REQBUFS(0x80000000) clamps to count=8 and every OOB QUERYBUF/QBUF returns EINVAL; guest stays up (vm.sh status up). Baseline bad behavior gone.

fix_run.log ('no hit: found 0 bytes' x3, 'product head=0', 'bytesused=STALE 0x0'); fix_run_user.log ('REQBUFS(0x80000000) ret=0 ... count=0x8', 'QUERYBUF(8..23) ret=-1 errno=25', 'QBUF(16) ret=-1 errno=25'); fix_build.log (clean -Werror rebuild); env.txt (guest up, kldstat).
↓ fix.diffper-fix-DF-5434

Confirmed kernel references

Detail

Exploit chain

unpriv user + streaming UVC camera (0666 /dev/videoN, v4l2_not_allowed=0 default): open -> S_FMT/REQBUFS (acquires pri) -> STREAMON -> QBUF -> frames flow -> REQBUFS(smaller count) [permitted: uvc_v4l2.c:613 checks only type/memory/pri] -> free_bufs_locked frees the N*frame-size M_UVC block while old-indexed descriptors stay linked as product head -> next USB frame payload is memcpy'd into the freed block at a chosen offset (device-controlled content; malicious USB device = fully controlled) -> slab-groom a victim kernel object into the freed block -> overwrite function pointer / cred-adjacent structure -> uid=0. The len-shrink variant (reachable via S_PARM refreshing v->req.dwMaxFrameSize from the device probe response mid-stream, uvc_drv.c:1385-1393, or a malicious device) gives a direct controlled-offset OOB write past the new small allocation. uid0 chain not developed on the guest (no UVC hardware; KLD rig proves the kernel-side primitive with production code).

Evidence (decisive lines)

run.modes123.log: 'UAF-WRITE PROVEN: found 2048 bytes of 0xa5/0x58/0x3c pattern inside freed/reclaimed block' x3 with 'product head=... == buf3? YES' and stale descriptor dump; panic.txt: 'PREDICTED fault VA = 0xfffff8011a7cc000' followed by 'fault virtual address = 0xfffff8011a7cc000, supervisor write data' + 'Stopped at memcpy+0xfa: repe movsq'; run.vuln2.log/run.vuln3.log: two more independent vulnerable builds, guest dies on the dangling state; fix_run.log: 'buf3 NOT re-inited: mem=0 status=0 bytesused=0x0; product head=0' + 'no hit: found 0 bytes' x3 + 'bytesused=STALE 0x0' + count clamp 0x8; fix_run_user.log: uid=1001 REQBUFS(0x80000000) -> count=0x8, QUERYBUF/QBUF OOB -> EINVAL; guest up.

PoC changes

No seed sketch existed (pass-2 discovery). Harness written fresh on the DF-5427 rig pattern: production uvc sources #included with DRIVER_MODULE neutered, production uvc_v4l2_reg() cdev (0666 /dev/video96), completion path driven via real uvc_bulkbuf_sell_buf() with a fabricated Plain-PIO usb_page_cache (usbd_get_page Case 2). Gotchas fixed during iteration: (1) fake uvc_drv_data with num_altsetting=1 so the production close path (uvc_drv_stop_video) is safe on the rig; (2) per-round reset of v->bulk.skip_payload/header_size/payload_size to mirror a fresh stream; (3) uvc_buf.c is #included and NOT in KLD SRCS - 'make' will not rebuild after touching it; every rebuild must 'rm -f .o .ko' (a stale module silently invalidated one fix run, caught by behavior diff); (4) the signed-count candidate (count>=0x80000000 -> uninit mem) was killed by objdump evidence: shipping gcc 8.3 folds the uninitialized read into ENOMEM (172a: movslq/jg; 1688: mov $0xc,%r12d) - documented as Info hardening, not the High.

Verified recommended fix

Drain both STAILQs and reset all bq->buf[] descriptors in uvc_buf_queue_free_bufs_locked(); fully re-init each descriptor (uvc_buf_queue_init_qbuf) in the req_bufs alloc loop so stale bytesused cannot survive; clamp *count with unsigned arithmetic (see fix.diff).

Verdict

VIDIOC_REQBUFS is accepted while the UVC buffer queue is live (no streaming check at uvc_v4l2.c:613 or uvc_buf.c:537); uvc_buf_queue_free_bufs_locked() (uvc_buf.c:517-525) then frees bq->mem without draining the product/consumer STAILQs and without resetting the bq->buf[] descriptors. The USB completion engine copies frame data through those stale descriptors: (A) with a shrunk count, descriptors with index >= new count keep mem pointing at freed memory while remaining product head, so uvc_bulkbuf_sell_buf()/uvc_buf_sell_buf() usbd_copy_out() writes 0x800 bytes of controlled payload into freed kernel memory - proven deterministically 3x on the guest (exactly 2048 marker bytes at the predicted offset, no race, all under bq->mtx); (B) with a shrunk frame size, uvc_buf_fill_v4l2() (uvc_buf.c:91) never clears vbuf.bytesused, so maxlen = length - bytesused (uvc_buf.c:265) underflows and the copy lands at mem_new + stale_bytesused - proven by Fatal trap 12, supervisor WRITE, at the exact VA predicted from the stale bytesused (0xfffff8011a7cc000), Stopped at memcpy+0xfa inside usbd_copy_out. Distinct from DF-1065 (userspace mmap of freed pages): the victim here is the kernel's own completion path. fix.diff (drain+reset in free_bufs_locked, full descriptor re-init in the req_bufs loop, unsigned count clamp) validated on a force-rebuilt KLD from the patched tree: 0 pattern bytes in all rounds, stale bytesused 0x0, no fault, guest up.