bktr_mem stored-address registry is never invalidated when the METEORSETGEO grow path frees the frame buffer β driver reload reinstalls the freed KVA, bzeros ~3.5 MB and RISC-DMAs video into reallocated kernel memory (write-after-free)
Summary
The registry can only store, never invalidate - the unpriv METEORSETGEO grow frees the old bigbuf without re-storing, so after any grow a bktr.ko reload retrieves stale freed KVA and bzeros 3.5MB of it + DMAs video into reallocated kernel objects. Unpriv plants it, root's mundane reload harvests it (hence Low). Hardware-gated (no Bt848 in QEMU); trigger built. Re-store-in-grow-path fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5162 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| grow_stale.c | β | 2.4 KB | view raw | |
| reload_stale.sh | β | 1010 B | view raw | |
| build.sh | β | 94 B | view raw | |
| run.sh | β | 144 B | view raw | |
| build.log | β | 112 B | view raw | |
| run.log | β | 146 B | view raw | |
| env.txt | β | 531 B | view raw | |
| fix.diff | β | 506 B | view raw | |
| README.md | β | 1.4 KB | β raw | |
| VERDICT.md | β | 6.2 KB | β raw | |
| manifest.json | β | 1.5 KB | view raw | |
| verdict.json | β | 4.1 KB | view raw |
DF-5162 β bktr_mem stored-address registry goes stale across METEORSETGEO grow β write-after-free on driver reload
PoC verdict: not_reproduced on audit guest (no Bt848 hardware) β defect certain at source level.
Reproduce (on hardware)
cc -O2 -Wall -o grow_stale grow_stale.c # build.sh ./grow_stale # unprivileged: METEORSETGEO grow sudo sh reload_stale.sh # root: kldunload bktr; kldload bktr
Expected (on hardware)
grow_staleprints that the grow ioctl was accepted β the bktr_mem registry for unit 0 now holds akmem_free()d KVA inBKTR_MEM_BUF.- After reload,
common_bktr_attach()reinstalls that pointer and executesbzero(bigbuf, 3.5 MB)on freed KVA (bktr_core.c:429-432); any capture then DMAs into it. Watch dmesg for the re-attach buffer line (buffer size ... addr 0x...β the address equals the old freed one, not a fresh contigmalloc), then start streaming and observe corruption of whatever kernel objects were reallocated into the range (typically panic or silent data corruption).
What was run here
Guest DragonFly 6.5-DEVELOPMENT X86_64_GENERIC: build OK; run blocked at
open(/dev/bktr0): ENOENT β no Brooktree PCI device in QEMU
(see env.txt). See VERDICT.md for the full source-level proof with
path:line citations and the fix (fix.diff).
DF-5162 β bktr_mem registry never invalidated when METEORSETGEO grow frees the frame buffer β freed KVA reinstalled and bzero()ed/DMA-written after driver reload
Verdict (short)
Not reproduced on the audit guest β the guest has no Bt848/Bt878 capture card
(NO_BT8XX_PCI_DEVICE, no /dev/bktr*), so the trigger cannot run. The
defect itself is proven at source level (certain): the grow path frees the
attach-time frame buffer without updating the bktr_mem registry, and the
registry has no invalidation mechanism at all. Every line of the chain is
cited below; on real hardware the reload deterministically writes ~3.5 MB of
kernel KVA that was returned to the kernel map by kmem_free().
File under audit
sys/dev/video/bktr/bktr_mem.c β the module whose entire purpose is to keep
DMA buffer addresses valid across bktr.ko unload/reload. The lifetime defect
lives in its (missing) invalidation contract and in its only consumer,
bktr_core.c, violating it.
The chain, line by line
-
Attach stores all five buffers β
common_bktr_attach(),sys/dev/video/bktr/bktr_core.c:415-419(under#if defined(KLD_MODULE)):bktr_store_address(unit, BKTR_MEM_BUF, buf)etc.bufhere is thecontigmalloc'dBROOKTREE_ALLOC(217*4 pages = 868 pages β 3.5 MB) buffer frombktr_core.c:404. -
Registry API can only set, never clear β
sys/dev/video/bktr/bktr_mem.c:103-138:bktr_store_address()setsaddresses_stored = 1(lines 115/119/123/127/131); no function in the file ever clears it or invalidates a stored pointer. (bktr_has_stored_addresses()bktr_mem.c:89-98,bktr_retrieve_address()bktr_mem.c:142-167 return the raw stored values.) -
Unprivileged grow frees the stored buffer β METEORSETGEO handler,
sys/dev/video/bktr/bktr_core.c:1655-1675:c buf = get_bktr_mem(unit, temp*PAGE_SIZE); /* 1667 */ if (buf != 0) { kmem_free(kernel_map, bktr->bigbuf, /* 1669 β OLD buffer freed */ (bktr->alloc_pages * PAGE_SIZE)); bktr->bigbuf = buf; /* 1672 */ bktr->alloc_pages = temp; /* 1673 */Nobktr_store_address(unit, BKTR_MEM_BUF, buf)follows. The registry entry forBKTR_MEM_BUFnow holds freed KVA withaddresses_stored == 1. Device node ismake_dev(&bktr_ops, unit, 0, 0, 0444, "bktr%d", unit)(sys/dev/video/bktr/bktr_os.c:325) β world-readable; ioctls are permitted on anO_RDONLYfd, so the grow is unprivileged (consistent with pass-1 findings DF-3789/DF-4671 which treat METEORSETGEO as unprivileged). -
Detach relies on the registry β
sys/dev/video/bktr/bktr_os.c:362-364: "We do not free memory for RISC programs, grab buffer, vbi buffers. The memory is retained by the bktr_mem module so we can unload and then reload the main bktr driver module." The invariant this comment depends on was broken in step 3. -
Reload reinstalls the freed pointer β
common_bktr_attach()again,sys/dev/video/bktr/bktr_core.c:382-390:bktr_has_stored_addresses(unit) == 1β retrieves all five addresses,need_to_allocate_memory = 0βbuf= the freed KVA. -
Immediate deterministic write-after-free β
sys/dev/video/bktr/bktr_core.c:429-432:c if ( buf != 0 ) { bktr->bigbuf = buf; /* freed pointer */ bktr->alloc_pages = BROOKTREE_ALLOC_PAGES; bzero((caddr_t) bktr->bigbuf, BROOKTREE_ALLOC); /* β3.5 MB zeroing of possibly-reallocated kernel KVA */Subsequently,read(),mmap()(bktr_os.c:669-690) and the RISC DMA program builders target the same freed range as soon as capture starts: the DMA writes attacker-shaped video data into whatever now occupies that KVA.
Threat model
- Step 3 (plant the stale entry): unprivileged local user with access to
/dev/bktr0(0444). - Steps 4-6 (harvest): requires root to
kldunload bktr && kldload bktrβ a mundane admin/driver-recycling operation, not an unusual privilege. There is no unprivileged-only path to the reload (attach only runs at module load / bus probe). - Requires a Bt848/878-family capture card (PCI).
Because the unprivileged user cannot complete the chain alone, this is Low severity, not a privesc: the payoff for the root actor is silent kernel-memory corruption of its own kernel. It is nonetheless a genuine use-after-free lifetime defect in the module whose sole job is lifetime management, with a deterministic multi-megabyte write sink.
Why not reproduced on the guest (and what was verified instead)
build.log/run.log: trigger compiles cleanly on the guest (DragonFly 6.5-DEVELOPMENT X86_64_GENERIC) and fails exactly atopen("/dev/bktr0") β ENOENT.env.txt:pciconf -lvshows no Brooktree/Bt87x PCI device;devinfohas no bktr;/boot/kernel/bktr.koandbktr_mem.koexist (the modules load, but no probe ever attaches, socommon_bktr_attachnever runs).- Fix validation is therefore not_testable on this guest (no baseline possible). The fix is a two-line source correction whose effect is provable by inspection: after it, the registry always holds either 0 or a live allocation.
Fix
fix.diff (against the read-only sys/ tree, never applied there):
re-store the new buffer in the grow path, and clear the stored BUF address
when the new allocation is bigger but the realloc fails/doesn't happen β
keeping memory_list[unit].buf always pointing at the live bktr->bigbuf.
(Related bookkeeping wart kept out of scope: on reload alloc_pages is
reset to BROOKTREE_ALLOC_PAGES regardless of a prior grow β harmless
over-allocation at worst, since grow only triggers when temp > alloc_pages.)
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
not_testableFix validation not testable on this guest: the baseline itself cannot reproduce (no Bt848 hardware -> common_bktr_attach never runs), so a patched kernel cannot be differentially exercised. fix.diff is source-verified (git apply --check clean) and provable by inspection: after it, memory_list[unit].buf always equals the live bktr->bigbuf at every point the registry is consulted.
fix.diff; VERDICT.md 'Fix' section; git apply --check output 'APPLIES_CLEAN' recorded in the audit trail
Confirmed kernel references
- sys/dev/video/bktr/bktr_mem.c:103
- sys/dev/video/bktr/bktr_mem.c:115
- sys/dev/video/bktr/bktr_mem.c:142
- sys/dev/video/bktr/bktr_core.c:415
- sys/dev/video/bktr/bktr_core.c:382
- sys/dev/video/bktr/bktr_core.c:1667
- sys/dev/video/bktr/bktr_core.c:1669
- sys/dev/video/bktr/bktr_core.c:429
- sys/dev/video/bktr/bktr_core.c:432
- sys/dev/video/bktr/bktr_os.c:325
- sys/dev/video/bktr/bktr_os.c:362
Detail
Exploit chain
unpriv: open /dev/bktr0 (mode 0444, bktr_os.c:325) -> METEORSETGEO rows=1024 cols=1024 frames=2 RGB16 (1026 pages > 868 default) -> grow path kmem_free()s original bigbuf, installs new buffer, registry keeps freed KVA with addresses_stored=1; root: kldunload bktr && kldload bktr -> common_bktr_attach retrieves stale KVA -> bktr_core.c:432 bzero(freed,3.5MB) + read()/mmap()/RISC-DMA target freed range -> corruption of whatever kernel objects were reallocated there. Chain needs root mid-way (driver reload), so impact is corruption-on-admin-action, not unpriv privesc.
Evidence (decisive lines)
VERDICT.md (full line-cited chain); run.log (guest: BUILD_OK then open(/dev/bktr0): ENOENT β hardware-gated); env.txt (no Brooktree PCI device, modules present); fix.diff (re-store after grow; git apply --check clean)
PoC changes
no orchestrator seed existed for this ID; trigger authored fresh in this pack (ioctl numbers/struct mirrored from sys/dev/video/meteor/ioctl_meteor.h:47-52,72)
Verified recommended fix
Re-store the live bigbuf in the METEORSETGEO grow path: bktr_store_address(unit, BKTR_MEM_BUF, bktr->bigbuf) after the if/else at bktr_core.c:1678-1681 (see fix.diff); longer term give bktr_mem an invalidate API.
Verdict
Source-proven use-after-free of the bktr_mem stored-address registry: the unprivileged METEORSETGEO grow path (bktr_core.c:1667-1673) frees the attach-time frame buffer without re-storing BKTR_MEM_BUF, the registry has no invalidation mechanism (bktr_mem.c:103-138 only ever sets addresses_stored=1), and the unload/reload design that detach relies on (bktr_os.c:362-364) reinstalls the freed KVA at reload (bktr_core.c:382-390) where bktr_core.c:429-432 immediately bzeros ~3.5 MB of possibly-reallocated kernel KVA and later DMAs video into it. Not reproduced on the audit guest because no Bt848/Bt878 PCI device exists there (env.txt: NO_BT8XX_PCI_DEVICE, /dev/bktr0 ENOENT in run.log); the trigger builds cleanly. Full unpriv-to-root escalation is NOT claimed: completing the chain requires a root kldunload/kldload of bktr.ko between the plant and the harvest, hence Low severity.
No comments yet.