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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4672 Β· 13 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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 METEORSETGEO ioctls (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: contigmalloc serves from the 256 MiB low-phys DMA reserve first (vm_page.c:2765 vm_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:447 vm_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 memory from get_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 bare kmem_free(kernel_map, bigbuf, size).
  • contigmalloc: vm_contig.c:560-582 β†’ vm_contig_pg_alloc (wires the pages, vm_contig.c:447; for reserve service vm_page_alloc_contig, vm_page.c:2765) + vm_contig_pg_kmap (object-less kmem_alloc_pageable entry + 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 with wired_count == 0 NONE 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 to vm_contig_alist β€” bare kmem_free never 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

fixed
baseline reproduced→ patch + rebuild →patched clean

fixed

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).
↓ fix.diffper-fix-DF-4672

Confirmed kernel references

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.