Use-after-free on device disconnect: sc->data freed before /dev/videoX cdev is destroyed
Summary
uvc_drv_detach calls uvc_drv_destroy_data(sc->data) (freeing sc->data->fmt and sc->data) BEFORE uvc_v4l2_unreg(sc->video) (asynchronous destroy_dev). Several V4L2 ioctl handlers (VIDIOC_ENUM_FMT ENUM_FRAMESIZES ENUM_FRAMEINTERVALS G_FMT) read sc->video->data->fmt WITHOUT holding video->mtx. Concurrent ioctl issued while device being unplugged reads freed fmt buffer UAF. destroy_dev async (devfs_msg_send_dev) so /dev/videoX cdev remains live accepting ioctls until devfs thread processes destroy. Window extends until devfs async teardown. VIDIOC_ENUM_FMT reads kfreed memory yielding info leak or with heap grooming controlled read.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2353 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger_race.c | trigger-source | unprivileged ENUM_FMT/ENUM_FRAMESIZES hammer with freed-memory (odd description) detector | 3.3 KB | view raw |
| panic.txt | panic-proof | inline verbatim detach-sequence run: Fatal trap 12 va=0xfffff801186d0010, stopped at uvc_drv_enum_v4l2_fmt+0x32: testb $0x1,0x10(%r12) | 3.0 KB | view raw |
| panic.prod.txt | panic-proof | PRODUCTION uvc_drv_detach() run: identical signature (va=0xfffff8011878a010) | 2.5 KB | view raw |
| run.log | run-log | same decisive run (production-function variant) | 3.0 KB | view raw |
| fix_run.log | fix-run-log | patched rig: 3000 production-detach cycles vs 18024 unpriv ioctl iterations, 0 faults, 0 odd descriptions | 152 B | view raw |
| build.log | build-log | baseline (unpatched) uvc_rig.ko build, gcc 8.3 -Werror clean, rc=0 | 924 B | view raw |
| fix_build.log | fix-build-log | patched (fix.diff applied to guest /usr/src) uvc_rig.ko rebuild, -Werror clean | 859 B | view raw |
| fix.diff | suggested-fix | git-apply-able combined fix for DF-2351/2352/2353 (uvc_drv.c + uvc_drv.h) | 4.3 KB | view raw |
| env.txt | environment | guest uname 6.5-DEVELOPMENT, gcc 8.3, kldstat (ehci/xhci/uvc_rig), sysctls | 501 B | view raw |
| setup.sh | build-script | push rig + triggers to guest and build (idempotent after vm.sh reset with-src) | 947 B | view raw |
| build.sh | build-script | wrapper calling setup.sh | 65 B | view raw |
| run.sh | run-script | full end-to-end reproduction procedure | 89 B | view raw |
| README.md | readme | human-facing repro + expected output | 2.7 KB | β raw |
| VERDICT.md | verdict | full narrative, line-by-line trace, fix validation | 4.7 KB | β raw |
| verdict.json | verdict-json | machine verdict (persist_poc.py input) | 5.5 KB | view raw |
| boot.log.serial | serial-log | full guest serial console of the decisive session | 15.9 KB | β download |
DF-2353 β use-after-free on device disconnect (cdev outlives sc->data)
sys/bus/u4b/uvc/uvc_drv.c β uvc_drv_detach() (uvc_drv.c:2493-2518)
frees sc->data (uvc_drv_destroy_data, uvc_drv.c:2506, which kfree's
data->fmt and data) and later sc->video before destroying the
/dev/videoX character device (uvc_v4l2_unreg, uvc_drv.c:2511).
destroy_dev() is asynchronous (destroy_dev β devfs_destroy_dev β
devfs_msg_send_dev(DEVFS_DEVICE_DESTROY), kern_conf.c:346 /
devfs_core.c:757-763), and open file descriptors keep dispatching ioctls
into the driver until the devfs core processes the destroy. Several V4L2
handlers (uvc_drv_enum_v4l2_fmt, uvc_drv_enum_v4l2_framesizes,
uvc_drv_enum_v4l2_frameintervals, uvc_drv_get_v4l2_fmt) read
video->data->fmt with no mutex held β concurrent ioctls during unplug
read freed memory.
Evidence class
Production-handler KLD dispatch rig: unprivileged user hammers
VIDIOC_ENUM_FMT/VIDIOC_ENUM_FRAMESIZES (all production dispatch),
while the rig runs reg/detach cycles β the teardown is the actual
production uvc_drv_detach() function, invoked through a fabricated
device_t whose softc points at the rig's uvc_softc
(action 2353prod; an earlier variant 2353 replays the same five calls
inline β both panic identically).
The crafted device carries 250 discrete frames Γ 57 intervals, sizing
data->fmt at ~65 KB so that kfree() returns its pages to the VM and
post-free reads fault.
Build / run
./build.sh # push rig + hammer trigger, build uvc_rig.ko ./run.sh # hammer as user + 2000 production uvc_drv_detach cycles as root
Expected output (see panic.txt / panic.prod.txt)
RIG2353p: === DF-2353 via PRODUCTION uvc_drv_detach(), 2000 cycles === c0dc564a-uvc_v4l2_ioctl need to be implement <- hammer's live ioctls Fatal trap 12: page fault while in kernel mode fault virtual address = 0xfffff801186d0010 <- inside the freed data->fmt fault code = supervisor read data, page not present Stopped at uvc_drv_enum_v4l2_fmt+0x32: testb $0x1,0x10(%r12)
testb $0x1, 0x10(%r12) is fmt->flags & UVC_FMT_FLAG_COMPRESSED
(uvc_drv.c:831) β a read of the freed data->fmt array by the
unprivileged hammer. Panicked during the first detach cycle.
Fix validation
fix.diff reorders uvc_drv_detach(): uvc_v4l2_unreg() first, then
wait (up to 10 s) for video->users == 0, and only then
uvc_drv_destroy_video/data/ctrl + kfree(video). On the patched rig
(fix_run.log): 3000 production-detach cycles racing 18024 unprivileged
ioctl iterations β zero faults, zero odd descriptions, guest stayed up
and /dev/video* disappeared cleanly.
DF-2353 β VERDICT: REPRODUCED (UAF β page-fault panic; info-leak ceiling) β FIX VALIDATED
Bottom line
The claimed use-after-free is real and was demonstrated twice, each time
with the teardown driven by the production uvc_drv_detach() function
(action 2353prod, fabricated device_t.softc) or by the identical
inline call sequence (action 2353, uvc_drv.c:2503-2515 verbatim), while
an unprivileged user (uid 1001) hammered VIDIOC_ENUM_FMT /
VIDIOC_ENUM_FRAMESIZES on the production-registered /dev/videoN
nodes (0666). Result:
Fatal trap 12: page fault while in kernel mode fault virtual address = 0xfffff801186d0010 fault code = supervisor read data, page not present Stopped at uvc_drv_enum_v4l2_fmt+0x32: testb $0x1,0x10(%r12)
testb $0x1,0x10(%r12)=fmt->flags & UVC_FMT_FLAG_COMPRESSED(uvc_drv.c:831) insideuvc_drv_enum_v4l2_fmt.- The faulting address is inside the freed 65 KB
data->fmtallocation (the crafted device: 250 frames Γ 57 intervals β 96 + 250Β·32 + 14250Β·4 = 65096 bytes;kfree()returned its pages, so the post-free read hitpage not present). - Panicked in the FIRST detach cycle both runs β the window is not
merely the few instructions between
uvc_drv_destroy_data()anduvc_v4l2_unreg(), but extends across the asynchronousdestroy_dev()(kern_conf.c:346 β devfs_core.c:757-763devfs_msg_send_dev(DEVFS_DEVICE_DESTROY)), during which open fds keep dispatching ioctls (and after whichsi_drv1still pointed at the already-kfreedvideountil devfs processed the destroy).
Root cause (verified in source)
- uvc_drv.c:2503-2507 β
uvc_drv_destroy_video+uvc_drv_destroy_data(kfreedata->fmt,data) +uvc_drv_destroy_ctrl - uvc_drv.c:2509-2515 β only THEN
uvc_v4l2_unreg(sc->video)βdestroy_dev(async) andkfree(sc->video) - uvc_v4l2.c:548-553 β
VIDIOC_ENUM_FMTβuvc_drv_enum_v4l2_fmtreadsv->data->nfmt/v->data->fmtwith no lock (uvc_drv.c:826-833; same for ENUM_FRAMESIZES 799-816, ENUM_FRAMEINTERVALS 739-760, G_FMT) - uvc_v4l2.c:369-379 β
uvc_v4l2_openalso writesv->userson the freedvideo(a UAF write window exists during the async teardown as well).
Primitive characterization (adversarial read)
- Demonstrated primitive: kernel-side read of freed slab memory,
reached by an unprivileged ioctl; when the freed pages are unmapped
(large
data->fmt) it is a reliable panic (DoS). - When the freed memory is instead re-used by another slab allocation
(small
data->fmtdevices),VIDIOC_ENUM_FMTcopies up to 32 bytes of it (strlcpy(vfmt->description, fmt->name, 32), uvc_drv.c:833) back to userspace β an info-leak oracle whose content depends on what reclaims the buffer (thetrigger_race"ODD description" detector watches for exactly this; in the patched validation run it stayed 0). - Escalation ceiling: read-only disclosure + DoS from this handler set.
No attacker-controlled write through ENUM_* paths was identified
(the
v->usersUAF write at open time is a counter increment on freed memory β corrupting an adjacent allocation's first 8 bytes with +1 under narrow timing; noted for completeness, not developed).
Rig notes
2353prodcalls the in-treeuvc_drv_detach()directly (fakedevice_t;device_get_softcjust returnsdev->softc, subr_bus.c:1782) β the ordering bug exercised is the production code.- Hammer quirk:
trigger_race.c'sv4l2_frmsizeenumunion padding made its VIDIOC_ENUM_FRAMESIZES number mismatch the kernel's (c0dc564aβ console "need to be implement" noise). VIDIOC_ENUM_FMT β the handler that faulted β matched exactly. Kept as-built so the logs match this source (sha256 in verdict.json). - The guest has no emulated UVC webcam; the rig synthesizes the USB hardware layer only.
Fix validation
fix.diff reorders uvc_drv_detach(): uvc_v4l2_unreg() FIRST, then
drain video->users (β€10 s), then destroy/free everything. Patched rig,
same hammer: 3000 production detach cycles, 18024 ioctl iterations,
0 faults, 0 odd descriptions, guest up, nodes cleanly removed
(fix_run.log). Residual (documented, upstream-hardening): an ioctl
already in flight when its fd's dtor drops users to 0 is not
reference-counted; closing the practical window needs per-call refs or a
synchronous devfs drain primitive.
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
fixedWith fix.diff applied (guest /usr/src, module rebuilt -Werror clean): 3000 reg/detach cycles driving the patched production uvc_drv_detach raced by the same unprivileged hammer (18024 iterations, 18023 successful ENUM_FMT, 0 odd descriptions) β no fault, guest up, nodes cleanly removed. Baseline panicked in cycle 1; patched survived 3000 cycles.
fix_run.log: 'RIG2353p: cycle 2900/3000 done' ... 'RIG2353p: finished 3000 cycles, no kernel-side fault'; hammer log: 'trigger_race: done: 18024 iterations, 18023 ENUM_FMT ok, 0 odd descriptions'; fix_build.log: clean patched build.
Confirmed kernel references
Detail
Exploit chain
Unplug (or USB unbind) of a UVC camera while a local user hammers ioctls on /dev/videoN: uvc_drv_detach frees data->fmt/data/video BEFORE the async destroy_dev stops dispatch; lockless handlers (uvc_drv_enum_v4l2_fmt uvc_drv.c:826-833 strlcpy's fmt->name back to userspace) then read freed slab memory. Demonstrated primitive: freed-memory read -> page-fault panic when the freed pages are unmapped (large fmt); when reused, up to 32 bytes of reallocated kernel heap are disclosed per ENUM_FMT (info-leak oracle). Secondary: uvc_v4l2_open increments v->users on the freed video (a narrow UAF write of +1 on freed memory during the async window). Read/disclosure-focused; no strong write primitive identified via these handlers, so uid0 escalation was not pursued beyond characterization.
Evidence (decisive lines)
panic.prod.txt (production uvc_drv_detach variant): 'Fatal trap 12 ... fault virtual address = 0xfffff801186d0010 ... supervisor read data, page not present' 'Stopped at uvc_drv_enum_v4l2_fmt+0x32: testb $0x1,0x10(%r12)'; panic.txt (inline verbatim-sequence variant): identical signature at 0xfffff8011878a010. fix_run.log: 'RIG2353p: finished 3000 cycles, no kernel-side fault' + hammer summary 'done: 18024 iterations, 18023 ENUM_FMT ok, 0 odd descriptions'.
PoC changes
No PoC seed existed. Built from scratch: trigger_race.c (unprivileged hammer; note its v4l2_frmsizeenum union padding made VIDIOC_ENUM_FRAMESIZES's cmd number mismatch the kernel's β cosmetic ENOTTY console noise; VIDIOC_ENUM_FMT, the handler that faulted, matched exactly; source kept as-built so hashes match the logs). Rig variants: '2353' (inline verbatim detach sequence) and '2353prod' (actual production uvc_drv_detach via fabricated device_t.softc). Crafted device sizes data->fmt at ~65KB so kfree returns pages and the post-free read faults.
Verified recommended fix
Reorder uvc_drv_detach: uvc_v4l2_unreg (async destroy_dev) first, drain video->users to 0 before freeing video/data/ctrl
Verdict
REPRODUCED β use-after-free read of freed kernel memory, raced by an UNPRIVILEGED user against the actual production uvc_drv_detach() function. The rig registered /dev/videoN nodes via production uvc_v4l2_reg (0666) with a ~65KB data->fmt allocation, ran reg/detach cycles calling the in-tree uvc_drv_detach() through a fabricated device_t, while uid-1001 hammered VIDIOC_ENUM_FMT/ENUM_FRAMESIZES. Result (both with the production function, action 2353prod, and with the verbatim inline call sequence, action 2353): Fatal trap 12, fault virtual address 0xfffff801186d0010 = supervisor read, page not present, stopped at uvc_drv_enum_v4l2_fmt+0x32: testb $0x1,0x10(%r12) β the fmt->flags read (uvc_drv.c:831) on data->fmt freed by uvc_drv_destroy_data() (uvc_drv.c:2506) while the cdev still dispatched ioctls because destroy_dev() is asynchronous (kern_conf.c:346 -> devfs_core.c:757-763) and ran only later at uvc_drv.c:2511. Panicked in the FIRST detach cycle both runs. FIX VALIDATED: reordered detach (unreg + users drain before any free) survived 3000 production-detach cycles against 18024 unprivileged ioctl iterations with zero faults and zero freed-memory disclosures.
No comments yet.