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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2353 Β· 16 files
FileTypeDescriptionSize
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
README.md readme human-facing repro + expected output
↓ download raw

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.

VERDICT.md verdict full narrative, line-by-line trace, fix validation
↓ download raw

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) inside uvc_drv_enum_v4l2_fmt.
  • The faulting address is inside the freed 65 KB data->fmt allocation (the crafted device: 250 frames Γ— 57 intervals β‡’ 96 + 250Β·32 + 14250Β·4 = 65096 bytes; kfree() returned its pages, so the post-free read hit page not present).
  • Panicked in the FIRST detach cycle both runs β€” the window is not merely the few instructions between uvc_drv_destroy_data() and uvc_v4l2_unreg(), but extends across the asynchronous destroy_dev() (kern_conf.c:346 β†’ devfs_core.c:757-763 devfs_msg_send_dev(DEVFS_DEVICE_DESTROY)), during which open fds keep dispatching ioctls (and after which si_drv1 still pointed at the already-kfreed video until devfs processed the destroy).

Root cause (verified in source)

  • uvc_drv.c:2503-2507 β€” uvc_drv_destroy_video + uvc_drv_destroy_data (kfree data->fmt, data) + uvc_drv_destroy_ctrl
  • uvc_drv.c:2509-2515 β€” only THEN uvc_v4l2_unreg(sc->video) β†’ destroy_dev (async) and kfree(sc->video)
  • uvc_v4l2.c:548-553 β€” VIDIOC_ENUM_FMT β†’ uvc_drv_enum_v4l2_fmt reads v->data->nfmt / v->data->fmt with 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_open also writes v->users on the freed video (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->fmt devices), VIDIOC_ENUM_FMT copies 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 (the trigger_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->users UAF 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

  • 2353prod calls the in-tree uvc_drv_detach() directly (fake device_t; device_get_softc just returns dev->softc, subr_bus.c:1782) β€” the ordering bug exercised is the production code.
  • Hammer quirk: trigger_race.c's v4l2_frmsizeenum union 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

fixed
baseline reproduced→ patch + rebuild →patched clean

With 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.
↓ fix.diffper-fix-DF-2353

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.