METEORSETGEO grow path frees the contigmalloc'd frame buffer with bare kmem_free() β wired vm_pages are never returned: permanent unprivileged wired-memory leak (plus stale kernel PTEs / mistabulated resident_count)
Summary
get_bktr_mem allocates via contigmalloc (wired, object-less kmem entry); the resize path releases with kmem_free alone, which for object-less entries neither frees the pages nor removes the stale PTEs - exactly the misuse vm_kern.c warns about. Every strictly-larger SETGEO permanently leaks the entire previous buffer as unreclaimable wired memory (up to ~16k pages per step, cumulative, repeat across opens). Unpriv 0444 node; hardware-gated. contigfree fix (guards for the first-boot 0 case).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4672 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc_wireleak.c | β | 6.2 KB | view raw | |
| bktrig2.c | β | 10.6 KB | view raw | |
| poc_race.c | β | 15.2 KB | view raw | |
| build.sh | β | 178 B | view raw | |
| run.sh | β | 148 B | view raw | |
| run.log | β | 2.1 KB | view raw | |
| fix_run.log | β | 1.2 KB | view raw | |
| fix.diff | β | 4.4 KB | view raw | |
| env.txt | β | 1.2 KB | view raw | |
| VERDICT.md | β | 4.9 KB | β raw | |
| README.md | β | 975 B | β raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 4.0 KB | view raw |
DF-4672 β METEORSETGEO grow path leaks the old frame buffer's pages (wired / DMA-reserve)
Status: REPRODUCED (dos). Unprivileged uid 1001 β +203.7 MiB permanent
wired leak (and silent 256 MiB DMA-reserve drain) via production
video_ioctl on the stock unfixed bktr.ko, driven through the DF-1215
KLD rig on the hardware-less audit guest. Fix validated on a rebuilt
kernel (leak gone).
Build
./build.sh # cc poc_wireleak.c + KLD rig (needs /usr/src + /usr/obj headers)
Run
./run.sh # root: rig load + attach-mirror init; maxx: the grow storm
Expected (baseline / stock kernel)
wire before grows: 155684 grow #40 -> wire_delta=+1511 >>> WIRE CLIMB PHASE BEGINS grow #64 -> wire_delta=+28234 == RESULT == wire delta (permanent) : 52137 pages (203.7 MiB) WIRED-MEMORY LEAK CONFIRMED (pages wired by the grow path are never returned)
On the fixed kernel (DF467XFIX): wire delta β 0 for the same grow storm.
DF-4672 β VERDICT (verify run, 2026-10-15)
Verdict
REPRODUCED (impact: dos β permanent unprivileged wired-memory leak).
Fix behaviorally VALIDATED on a rebuilt kernel (fixed).
The METEORSETGEO grow block of the production, unmodified bktr.ko
(video_ioctl, resolved at 0xffffffff82606190 from the stock module)
was driven by an unprivileged user (uid 1001) through the DF-1215-style
KLD rig (bktrig2.ko β /dev/bktrig0, 0666 β real /dev/bktr0 is 0444,
both world-openable; no priv_check exists anywhere in the driver).
Baseline (stock kernel #0, unfixed bktr.ko), run.log:
- grow storm: 14 strictly-larger
METEORSETGEOioctls (128-page steps) β buffers churn (KVA visible changing), all rc=0 as uid 1001. vm.stats.vm.v_wire_count: 155,684 β 207,846 = +52,137 pages (203.7 MiB) permanently wired, never returned (unchanged after 15 s settle; still 207,846 at the end of the run).- The wire-count trace shows the two phases of the same leak on
DragonFly:
contigmallocserves from the 256 MiB low-phys DMA reserve first (vm_page.c:2765vm_page_alloc_contigβvm_contig_alist); reserve pages are pre-wired so the drain is silent (wire flat, grows #1-#32); once the reserve's contiguous blocks are exhausted the 3-pass PQ_FREE/PQ_CACHE scan wires normal pages (vm_contig.c:447vm_page_wire) and the wire count climbs per grow and never comes back (grow #40 onward: +1511, +28234, +22392...). - Re-confirmed on a fresh guest after
vm.sh reset with-src: +107,297 pages (419.1 MiB) permanent wire growth from 15 unpriv grows (wire climbed from grow #10), exhaustion (Cannot allocate memoryfromget_bktr_mem) reached during the DF-4673 runs of the same boot.
Root cause re-confirmed in source, line by line
- grow block: bktr_core.c:1667-1673 β
get_bktr_mem()(contigmalloc, bktr_os.c:399-407) then barekmem_free(kernel_map, bigbuf, size). - contigmalloc: vm_contig.c:560-582 β
vm_contig_pg_alloc(wires the pages, vm_contig.c:447; for reserve servicevm_page_alloc_contig, vm_page.c:2765) +vm_contig_pg_kmap(object-lesskmem_alloc_pageableentry +pmap_kenter_noinval, vm_contig.c:527-531). - the ONLY correct release:
contigfree()vm_contig.c:588-603 (pmap_qremove+kmem_free+vm_page_free_contig). - bare
kmem_free: vm_map.c:3300-3370 β for an object-less VM_MAPTYPE_NORMAL entry withwired_count == 0NONE of the pmap_remove branches fire and no page release happens; the vm_kern.c:311-313 warning describes this misuse verbatim. - Reserve variant:
vm_page_free_contig(vm_page.c:2858-2877) is what returns blocks tovm_contig_alistβ barekmem_freenever does, so every grow permanently drains the DMA reserve (256 MiB, vm_page.c:426-428) as well.
Rig fidelity (what is production, what is synthetic)
| Element | Status |
|---|---|
| METEORSETGEO handler incl. grow block | production, unmodified (stock bktr.ko video_ioctl) |
| get_bktr_mem / contigmalloc / kmem_free | production (stock bktr.ko + kernel) |
| initial buffer installed by RIG_INIT | production get_bktr_mem + bzero β mirrors common_bktr_attach (bktr_core.c:393-446); size 32 pages instead of BROOKTREE_ALLOC_PAGES=868 to keep the leak measurable on the 4 GB guest (grow path is size-agnostic) |
| softc | synthetic zeroed struct bktr_softc + fake PCI register page for the SETGEO OUTW/OUTL (x86_64 bus_space = direct deref, bus_dma.h:139-141) |
| device node | rig 0666 vs real 0444 β both unpriv-openable |
| bt848 hardware / DMA | absent in QEMU (irrelevant for this finding β no DMA involved) |
Fix validation (behavioral A/B)
fix.diff (in this pack): grow block uses contigfree() for the old
buffer, installs new buffer before freeing, zeroes it. Built as
DF467XFIX = X86_64_GENERIC + device bktr + fix (make -j6
nativekernel, rc=0; installkernel; reboot). With the same rig (nodep
variant, production symbols now kernel globals) and the same unpriv
attack: wire delta β 0 pages across the same grow storm (see
fix_run.log), buffer contents zero after every grow. Baseline bad
behavior GONE β fix_status: fixed.
Files
| File | Purpose |
|---|---|
poc_wireleak.c |
phase-aware unpriv PoC (reserve drain β wire climb β exhaustion) |
bktrig2.c |
KLD rig: production video_ioctl + production get_bktr_mem init |
run.log |
decisive baseline run (full untrimmed output) |
fix_run.log |
same attack on the DF467XFIX kernel |
fix.diff |
verified fix (never applied to the read-only sys/ tree) |
env.txt, manifest.json, verdict.json |
environment + machine records |
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
fixedfixed
fix_run.log: DF467XFIX kernel (bktr built in, fix applied, KERN_RC=0/INST_RC=0, booted #0 Oct 15 00:54:24 UTC 2026): same unpriv grow storm, 85.3 MiB of frame buffers allocated, permanent wire delta 22 pages (== idle noise of 288) vs +52,137 pages baseline; reserve no longer drains (grow KVA reuse safe).
Confirmed kernel references
- sys/dev/video/bktr/bktr_core.c:1667
- sys/dev/video/bktr/bktr_core.c:1669
- sys/dev/video/bktr/bktr_os.c:399
- sys/dev/video/bktr/bktr_os.c:403
- sys/vm/vm_contig.c:447
- sys/vm/vm_contig.c:527
- sys/vm/vm_contig.c:588
- sys/vm/vm_kern.c:311
- sys/vm/vm_map.c:3300
- sys/vm/vm_page.c:2765
- sys/vm/vm_page.c:2858
- sys/vm/vm_page.c:426
Detail
Exploit chain
open(/dev/bktr0 O_RDONLY) [0444, unpriv] -> ioctl(METEORSETGEO, strictly larger geometry) xN [no priv_check in driver] -> every grow get_bktr_mem()s a new buffer and bare-kmem_free()s the old one -> reserve blocks + wired pages permanently lost -> repeat until the 256 MiB DMA reserve and/or all reclaimable memory is wired -> system-wide allocation exhaustion DoS (verified ENOMEM from get_bktr_mem after 266 MiB of unpriv growth).
Evidence (decisive lines)
run.log: 'wire delta (permanent): 52137 pages (203.7 MiB)' + flat->climb transition lines ('WIRE CLIMB PHASE BEGINS', '+28234'); re-confirm on fresh guest in VERDICT.md; fix_run.log on DF467XFIX kernel: same attack, 85.3 MiB allocated, wire delta 22 pages == noise (leak gone).
PoC changes
Seed bktr_wireleak.c expected an immediate wire climb and hard-coded /dev/bktr0; rewritten as phase-aware rig PoC: /dev/bktrig0, kernel-verified strictly-larger grows, per-grow wire deltas, two-phase detection (reserve drain then PQ wire climb), exact leak prediction. Discovered en route: DragonFly contigmalloc serves from a 256 MiB low-phys DMA reserve first (vm_page.c:2765), so the seed's one-phase model was wrong on this kernel.
Verified recommended fix
Release the grow-path buffer with contigfree() (not bare kmem_free), install the new buffer before freeing the old one, and bzero the new buffer (see fix.diff).
Verdict
REPRODUCED. Unprivileged (uid 1001) METEORSETGEO grows through the production, unmodified video_ioctl of the stock bktr.ko (driven via the DF-1215-style KLD rig /dev/bktrig0 on the hardware-less guest) permanently leaked 52,137 wired pages (203.7 MiB) in one run and 107,297 pages (419.1 MiB) on a fresh guest, with the two DragonFly leak phases visible: silent 256 MiB low-phys DMA-reserve alist drain (wire flat), then PQ-scan allocations wired per grow and never returned (wire climbs, never drops, survives kldunload), ending in get_bktr_mem/contigmalloc exhaustion (ENOMEM). Bare kmem_free of the contigmalloc'd object-less region returns neither the wired pages nor the reserve block nor removes the kernel PTEs -- exactly the misuse sys/vm/vm_kern.c:311-313 warns about.
No comments yet.