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)
PoC verification
Evidence pack
findings/poc/DF-5434 Β· 20 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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/consumerSTAILQs 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 countkeepmempointing 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)becomeslen, and the copy lands atmem_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...thenFatal 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=0andno 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 withcount=0x8(clamped); every out-of-bounds QUERYBUF/QBUF returns EINVAL.
Files
bufharness.cβ KLD: real uvc sources compiled in (DRIVER_MODULE neutered), productionuvc_v4l2_reg()creates a real 0666/dev/video96, sysctl drives the production completion path (uvc_bulkbuf_sell_buf) with a fabricatedusb_page_cacheexactly asuvc_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 uninitmemread compiles to ENOMEM β seeVERDICT.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.
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 bytesin 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 OOBQUERYBUF(8..23)/QBUF(16)returnsEINVAL(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
fixedfix.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).
Confirmed kernel references
- sys/bus/u4b/uvc/uvc_buf.c:517
- sys/bus/u4b/uvc/uvc_buf.c:537
- sys/bus/u4b/uvc/uvc_buf.c:554
- sys/bus/u4b/uvc/uvc_buf.c:569
- sys/bus/u4b/uvc/uvc_buf.c:91
- sys/bus/u4b/uvc/uvc_buf.c:265
- sys/bus/u4b/uvc/uvc_buf.c:268
- sys/bus/u4b/uvc/uvc_buf.c:346
- sys/bus/u4b/uvc/uvc_v4l2.c:613
- sys/bus/u4b/uvc/uvc_v4l2.c:620
- sys/bus/u4b/uvc/uvc_drv.c:1385
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.
No comments yet.