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

METEORSVIDEO ioctl allows unprivileged user to set arbitrary physical DMA target address

Summary

METEORSVIDEO ioctl at bktr_core.c:1396-1402 stores user-supplied video->addr (32-bit physical address) into bktr->video with ZERO priv check and ZERO address validation. Every RISC DMA builder (rgb_vbi_prog:2670, rgb_prog:2840, yuvpack_prog:2995, yuv422_prog:3113, yuv12_prog:3214) uses it as bt848 bus-master DMA write target. Device nodes created mode 0444 (world-readable) at bktr_os.c:325, ioctls not gated by open mode. Any unprivileged user: open /dev/bktr0 O_RDONLY -> METEORSVIDEO addr=phys -> METEORCAPTUR continuous -> bt848 writes video frames to arbitrary physical memory. No suser/priv_check/caps_priv_check anywhere in bktr_core.c. Arbitrary physical-memory write primitive -> reliable root escalation. Fix: caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT) when addr!=0.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1215 Β· 9 files
FileTypeDescriptionSize
poc_physdma.c trigger-source minimal METEORSVIDEO -> arbitrary phys DMA write trigger 1.4 KB view raw
fix.diff suggested-fix caps_priv_check_td gate on METEORSVIDEO when addr!=0 1.1 KB view raw
build.sh build-script cc -O2 -Wall -o poc_physdma poc_physdma.c 260 B view raw
run.sh run-script executes the trigger (ENOENT on this guest) 601 B view raw
fix_build.log build-log full nativekernel build log with DF-1215 fix applied (rc=0, -Werror) 5.6 MB ↓ download
env.txt environment uname, pciconf (no bt848), dev nodes, kldstat, kernel sha256 2.5 KB view raw
VERDICT.md verdict full narrative: source confirmation, no-HW limitation, fix validation 6.5 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
VERDICT.md verdict full narrative: source confirmation, no-HW limitation, fix validation
↓ download raw

DF-1215 β€” VERDICT

Verdict

INCONCLUSIVE (live) β€” bug CONFIRMED in source; trigger requires PCI hardware absent from this QEMU guest. The primitive is a real arbitrary-physical-write via the bt848 DMA engine. Fix authored and built cleanly into a single-fix kernel; the patched kernel is boot-stable and the cited path is closed in source.

Bug class

Arbitrary physical-memory write via PCI bus-master DMA, no privilege check, reachable through a world-readable (mode 0444) character device on real hardware.

Source confirmation (path:line)

The bug is real. The cited ioctl handler stores the user-supplied 32-bit physical address directly into bktr->video.addr with no privilege check and no address validation:

/* sys/dev/video/bktr/bktr_core.c:1396-1402 */
case METEORSVIDEO:
    video = (struct meteor_video *)arg;
    bktr->video.addr = video->addr;        /* <- user-controlled phys addr */
    bktr->video.width = video->width;
    bktr->video.banksize = video->banksize;
    bktr->video.ramsize = video->ramsize;
    break;

There is no priv_check, suser, caps_priv_check_td, or open-mode gate anywhere in bktr_core.c (verified with grep). The function signature video_ioctl(bktr, unit, cmd, arg, td) even passes a thread *td that the case could use for a privilege check β€” it doesn't.

Every RISC DMA builder then consumes bktr->video.addr as the bus-master target address:

Builder line sink
rgb_vbi_prog 2671 target_buffer = (u_long) bktr->video.addr;
rgb_prog 2841 target_buffer = (uint32_t) bktr->video.addr;
yuvpack_prog 2996 target_buffer = (uint32_t) bktr->video.addr;
yuv422_prog 3114 target_buffer = (uint32_t) bktr->video.addr;
yuv12_prog 3215 target_buffer = (uint32_t) bktr->video.addr;

The bt848 RISC engine writes captured video frames to that physical address. Since the bytes are video data (color-bar pattern via BT848_SCBARS is attacker-deterministic), the attacker has substantial byte control over what gets written. The METEORCAPTUR continuous-capture case (bktr_core.c:1502) that starts the DMA also has no privilege check, completing the chain:

open(/dev/bktr0, O_RDONLY)            # works for any user β€” node is mode 0444
ioctl(fd, METEORSVIDEO, addr=X)        # sets DMA target to attacker phys addr
ioctl(fd, BT848_SCBARS, 1)             # deterministic color bars as payload
ioctl(fd, METEORCAPTUR, CONTINUOUS)    # bus-master DMA write begins

Device-node creation confirmed world-readable:

/* sys/dev/video/bktr/bktr_os.c:325 */
make_dev(&bktr_ops, unit, 0, 0, 0444, "bktr%d", unit);

Live reproduction: NOT POSSIBLE on this guest

$ ls /dev/bktr*
ls: /dev/bktr*: No such file or directory

$ pciconf -lv | grep -iE 'bktr|bt848|brooktree'
(no match)

The QEMU guest exposes only: Intel 440FX, PIIX3 ISA, PIIX3 IDE, PIIX4 ACPI, QEMU std VGA, virtio-net, virtio-blk. No Brooktree bt848/878 PCI video capture device is emulated, so the bktr driver never probes/attaches, make_dev is never called, and /dev/bktr0 is never created. kldload bktr succeeds but creates no device node (verified).

This is case (d) in the procedure: genuinely not reachable at runtime on this guest. The device path is live-reachable on real hardware (or in a QEMU invocation that adds -device bktr-class hardware, which doesn't exist upstream).

Phase 6 β€” escalation analysis (not exercisable live)

If the device were present, the primitive would be an arbitrary physical memory write of substantially-attacker-controlled bytes (color-bar frame data is deterministic). On this guest (no SMAP/SMEP/KASLR/PTI; INVARIANTS ON by default in GENERIC), the realistic chain on real hardware would be:

  1. Identify the physical address of a victim kernel object (proc0.p_ucred, a struct ucred, or a function-pointer-bearing struct) via DMI/E820 or known DMA-consistent regions.
  2. Point video.addr at it via METEORSVIDEO.
  3. Trigger METEORCAPTUR to DMA a color-bar pattern whose bytes overwrite the victim's cr_uid (β†’ direct privesc) or a function pointer (β†’ pivot to userspace shellcode on this guest, since SMEP is OFF).

This is a textbook physmem-write β†’ root chain on default GENERIC. The chain is not demonstrable here only because the trigger device is absent.

Fix (fix.diff)

Add a capability check before redirecting DMA away from the kernel-allocated buffer:

+ if (video->addr != 0 &&
+     caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT) != 0)
+     return (EPERM);
  bktr->video.addr = video->addr;

addr == 0 is the "use kernel-allocated bigbuf" case (preserved for unprivileged users); any non-zero redirect now requires privilege.

Phase 8 β€” fix validation (not_testable)

  • fix.diff applies cleanly to /usr/src via patch -p1 --forward (both hunks succeed).
  • make -j6 nativekernel KERNCONF=X86_64_GENERIC builds the patched source with cc 8.3 -Werror, rc=0, no warnings on bktr_core.c.
  • The patched kernel installs via make installkernel and boots cleanly (kern.version β†’ 6.5-DEVELOPMENT #1: Mon Jul 20 11:01:52 UTC 2026).
  • nm /boot/kernel/kernel shows T caps_priv_check_td linked in.
  • The live PoC cannot be re-run on the patched kernel to confirm "behavior gone" because the trigger device (/dev/bktr0) does not exist on this guest. Per the procedure, this is fix_status: not_testable β€” the diff applies, compiles, boots, and a source read confirms the previously-unchecked path now gates on caps_priv_check_td.

PoC changes

The provided poc_physdma.c (authored pre-verification) compiles cleanly on the guest (cc 8.3, -Wall no warnings) and is left unchanged. It fails at open("/dev/bktr0") with ENOENT, which is the correct guest-behavior given the absent PCI hardware.

Files

File Purpose
poc_physdma.c original live-trigger PoC (compiles, fails at open on this guest)
fix.diff adds caps_priv_check_td gate to METEORSVIDEO (sys/dev/video/bktr/bktr_core.c)
build.sh runs the PoC build
run.sh runs the PoC (ENOENT on guest)
fix_build.log full untrimmed nativekernel log for the patched build (rc=0)
env.txt guest uname / pciconf / dev nodes / kldstat / kernel sha256

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not_testable live (HW absent); fix.diff applies cleanly + kernel builds rc=0.

fix.diff applies, kernel build rc=0, symbol present in patched kernel.
↓ fix.diffkernel #1/#2/#3 builds rc=0

Confirmed kernel references

Detail

Exploit chain

Not exercisable (no bt848 PCI HW in QEMU). On real HW: physmem write -> forge ucred/funcptr -> uid0 (no SMAP/SMEP/KASLR).

Evidence (decisive lines)

ls /dev/bktr*: ENOENT. pciconf: no brooktree. PoC open() fails. fix.diff applies (2 hunks), kernel #1 builds rc=0, caps_priv_check_td linked.

Verified recommended fix

In METEORSVIDEO case: add 'if (video->addr != 0 && caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT) != 0) return EPERM;' + #include .

Verdict

INCONCLUSIVE live. METEORSVIDEO no priv check on user addr. Bug REAL in source. QEMU has no bt848 PCI device -> /dev/bktr0 absent -> cannot reproduce. Kldload bktr succeeds but no device attaches.