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)
PoC verification
Evidence pack
findings/poc/DF-1215 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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:
- Identify the physical address of a victim kernel object (
proc0.p_ucred, astruct ucred, or a function-pointer-bearing struct) via DMI/E820 or known DMA-consistent regions. - Point
video.addrat it viaMETEORSVIDEO. - Trigger
METEORCAPTURto DMA a color-bar pattern whose bytes overwrite the victim'scr_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.diffapplies cleanly to /usr/src viapatch -p1 --forward(both hunks succeed).make -j6 nativekernel KERNCONF=X86_64_GENERICbuilds the patched source withcc 8.3 -Werror, rc=0, no warnings onbktr_core.c.- The patched kernel installs via
make installkerneland boots cleanly (kern.versionβ6.5-DEVELOPMENT #1: Mon Jul 20 11:01:52 UTC 2026). nm /boot/kernel/kernelshowsT caps_priv_check_tdlinked 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 isfix_status: not_testableβ the diff applies, compiles, boots, and a source read confirms the previously-unchecked path now gates oncaps_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_testablenot_testable live (HW absent); fix.diff applies cleanly + kernel builds rc=0.
fix.diff applies, kernel build rc=0, symbol present in patched kernel.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- v
- i
- d
- e
- o
- /
- b
- k
- t
- r
- /
- b
- k
- t
- r
- _
- c
- o
- r
- e
- .
- c
- :
- 1
- 3
- 9
- 6
- s
- y
- s
- /
- d
- e
- v
- /
- v
- i
- d
- e
- o
- /
- b
- k
- t
- r
- /
- b
- k
- t
- r
- _
- c
- o
- r
- e
- .
- c
- :
- 2
- 6
- 7
- 0
- s
- y
- s
- /
- d
- e
- v
- /
- v
- i
- d
- e
- o
- /
- b
- k
- t
- r
- /
- b
- k
- t
- r
- _
- o
- s
- .
- c
- :
- 3
- 2
- 5
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.
No comments yet.