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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5162 Β· 12 files
FileTypeDescriptionSize
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_stale prints that the grow ioctl was accepted β€” the bktr_mem registry for unit 0 now holds a kmem_free()d KVA in BKTR_MEM_BUF.
  • After reload, common_bktr_attach() reinstalls that pointer and executes bzero(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).

VERDICT.md
↓ download raw

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

  1. 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. buf here is the contigmalloc'd BROOKTREE_ALLOC (217*4 pages = 868 pages β‰ˆ 3.5 MB) buffer from bktr_core.c:404.

  2. Registry API can only set, never clear β€” sys/dev/video/bktr/bktr_mem.c:103-138: bktr_store_address() sets addresses_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.)

  3. 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 */ No bktr_store_address(unit, BKTR_MEM_BUF, buf) follows. The registry entry for BKTR_MEM_BUF now holds freed KVA with addresses_stored == 1. Device node is make_dev(&bktr_ops, unit, 0, 0, 0444, "bktr%d", unit) (sys/dev/video/bktr/bktr_os.c:325) β€” world-readable; ioctls are permitted on an O_RDONLY fd, so the grow is unprivileged (consistent with pass-1 findings DF-3789/DF-4671 which treat METEORSETGEO as unprivileged).

  4. 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.

  5. 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.

  6. 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 at open("/dev/bktr0") β†’ ENOENT.
  • env.txt: pciconf -lv shows no Brooktree/Bt87x PCI device; devinfo has no bktr; /boot/kernel/bktr.ko and bktr_mem.ko exist (the modules load, but no probe ever attaches, so common_bktr_attach never 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_testable
baseline no→ patch + rebuild →patched clean

Fix 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
↓ fix.diffper-fix-DF-5162

Confirmed kernel references

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.