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

METEORSETGEO grow path installs an unzeroed frame buffer β€” contigmalloc pages recycled from PQ_FREE/PQ_CACHE disclose stale kernel memory via mmap()/read() (attach path bzero's; grow path doesn't)

Summary

The attach path zeroes the initial buffer but the grow path installs a fresh contigmalloc buffer with no M_ZERO/bzero; the pages come from free/cache queues with contents intact and bktr_mmap maps them into userspace before any capture overwrites - in-bounds disclosure of recycled kernel heap/page-cache contents via one unpriv ioctl+mmap. Distinct from DF-1216/3789 (OOB reads). bzero-at-install fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4673 Β· 15 files
FileTypeDescriptionSize
poc_stalebuf.c β€” 5.6 KB view raw
spray.c β€” 1.6 KB view raw
bktrig2.c β€” 10.6 KB view raw
run_4673b.sh β€” 1.0 KB view raw
build.sh β€” 154 B view raw
run.sh β€” 216 B view raw
run.log β€” 10.0 KB view raw
control_rounds.log β€” 4.6 KB view raw
fix_run.log β€” 1.1 KB view raw
fix.diff β€” 4.4 KB view raw
env.txt β€” 1.2 KB view raw
VERDICT.md β€” 4.7 KB ↓ raw
README.md β€” 1.1 KB ↓ raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 3.9 KB view raw

DF-4673 β€” METEORSETGEO grow installs an unzeroed frame buffer β†’ stale-memory disclosure via mmap

Status: REPRODUCED (leak, cross-privilege). 14,117 of 16,444 mapped pages carried ROOT's sprayed memory markers, read back by uid 1001 through the production grow path + verbatim bktr_mmap. Fix validated (zeroed on the fixed kernel).

Build

./build.sh    # poc_stalebuf (unpriv) + spray (root groomer); rig as in DF-4672

Run

./run.sh      # preconditions: rig loaded, reserve drained (DF-4672 storm);
              # root sprays 2600 MiB marked pages and exits; maxx grows+mmaps+reads

Expected (baseline / stock kernel)

grow -> alloc_pages=384 wire_delta=+960 (SCAN-path)  << RECYCLED PAGES
  ROOT-SECRET page 0 (tag=41 spraypage=121355):
    page 0: 524f4f5453454352 0000000000000029 000000000001da0b 004446343637332d ...
 -> scanned 384 pages: 383 nonzero, 0 kptr, 383 ROOT-SECRET
== RESULT ==
root-spray secret pages disclosed to unprivileged user: 14117
KERNEL MEMORY DISCLOSURE CONFIRMED (cross-privilege)

On the fixed kernel (DF467XFIX): every grown buffer all-zero, 0 secrets.

VERDICT.md
↓ download raw

DF-4673 β€” VERDICT (verify run, 2026-10-15)

Verdict

REPRODUCED (impact: leak β€” cross-privilege kernel-memory disclosure). Fix behaviorally VALIDATED on a rebuilt kernel (fixed).

The grow path of the production, unmodified bktr.ko (video_ioctl @0xffffffff82606190) installed an unzeroed contigmalloc'd frame buffer, and the verbatim bktr_mmap fault path (bktr_os.c:656-685, copied line-for-line into the rig because bktr_mmap is a local symbol, nm: t bktr_mmap @0x7840) mapped its recycled pages read-only into an unprivileged process (uid 1001), which read root's memory contents back.

Decisive numbers (run.log, stock kernel, guest RAM 4 GB)

  • Root sprayed 2600 MiB of anonymous memory with per-page markers ("ROOTSECR", tag, spray page index) and exited (pages β†’ PQ_FREE, contents intact).
  • uid 1001 ran 24 adaptive METEORSETGEO grows; each grow's wire delta (0 = reserve-served, >0 = scan-served/recycled) was printed; every scan-served buffer was immediately mmap(PROT_READ)'d and scanned.
  • 8 scan-path buffers, 16,444 pages scanned: 14,117 pages carried the ROOT spray secret (e.g. page 0: 524f4f5453454352 0000000000000029 000000000001da0b 004446343637332d ... = MAGIC0, tag=41, spraypage=121355, MAGIC3, 0xAA fill) β€” disclosed to an unprivileged user across the privilege boundary.
  • The run ends with Cannot allocate memory β€” get_bktr_mem exhaustion (incidentally also demonstrating DF-4672's phase C).

Important DragonFly-specific precondition (honest scoping)

The seed's claim "pages come from the free/cache queues" is only the fallback path on this kernel: contigmalloc first serves from the 256 MiB low-phys DMA reserve (vm_page.c:2765) whose fictitious pages are pristine zero, so with a full reserve the grown buffer reads as all zero (demonstrated by the control rounds, control_rounds.log: 700 MiB and 2600 MiB sprays, all scans 0/… non-zero with the reserve intact β€” this also confirms the two-pool model). The disclosure becomes reachable the moment the reserve cannot satisfy the request β€” and the SAME unprivileged ioctl deterministically drains the reserve (that is DF-4672, +203.7 MiB verified in the same session). After the drain, the pfn-ascending first-fit scan takes sprayed PQ_FREE pages (small sprays lose to pristine low-pfn runs; ~85% coverage wins deterministically: P(clean 33-page run) β‰ˆ 0.15^33 β‰ˆ 1e-27).

Chain for an unprivileged attacker on real hardware: open /dev/bktr0 (0444) β†’ N SETGEO grows to drain the reserve (each leaks, DF-4672) β†’ one more grow β†’ mmap β†’ read recycled page-cache/heap/user pages of other users, including root. No privilege check anywhere in the path (rg priv_check sys/dev/video/bktr/ β†’ none).

Root cause re-confirmed in source

  • attach zeroes: bktr_core.c:432 bzero(bigbuf, BROOKTREE_ALLOC); grow path installs get_bktr_mem()'d buffer with no zeroing (bktr_core.c:1667-1673; get_bktr_mem passes only M_NOWAIT, bktr_os.c:403-407 β€” no M_ZERO).
  • scan-path pages come from PQ_FREE/PQ_CACHE with contents intact (vm_contig.c:308-323, 398-449; zeroing happens only on fault via vm_page_grab(VM_ALLOC_*ZERO) paths, not for contig pages).
  • bktr_mmap maps them at first fault with no exec-only restriction (device_pager.c:348-390 old_dev_pager_fault β†’ dev_dmmap).

Rig fidelity

Same table as DF-4672 (production ioctl+allocation path, verbatim production mmap, synthetic softc/fake register page, 0666 rig node vs 0444 real node β€” both unpriv-openable). The disclosure observed is strictly in-bounds mapped frame-buffer memory; no OOB read is involved.

Fix validation

fix.diff adds bzero(new buffer) at install (plus the contigfree and ordering fixes). On the DF467XFIX kernel, the same spray+grow+scan protocol yields all-zero buffers and 0 secret pages (fix_run.log). Baseline bad behavior GONE β‡’ fix_status: fixed.

Files

File Purpose
poc_stalebuf.c adaptive unpriv grow+mmap+scan PoC (wire-delta phase detection)
spray.c root-side page-queue groomer (marked anon pages β†’ PQ_FREE)
run.log decisive run (14,117/16,444 secret pages)
control_rounds.log control rounds (reserve intact β†’ all zero; two-pool proof)
fix_run.log same attack on the fixed kernel
fix.diff verified fix
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, 2600 MiB spray + one 127.6 MiB max-geometry grow (32,673 pages forced through the PQ scan path, exceeding every reserve fragment): all 32,673 pages zero, 0 ROOT-SECRET, 0 nonzero pages vs 14,117/16,444 secret pages baseline.
↓ fix.diffper-fix-DF-4673

Confirmed kernel references

Detail

Exploit chain

root (or any user) dirties ~2.6 GiB of anon memory with markers and exits (pages -> PQ_FREE, contents intact) -> uid 1001 opens /dev/bktr0 (0444), grows until the DMA reserve is exhausted (unpriv, DF-4672) -> next contigmalloc takes recycled PQ_FREE/PQ_CACHE pages (first-fit from pfn 0; ~85% spray coverage makes a clean run impossible: P(33-page clean run) ~ 1e-27) -> mmap(PROT_READ) of the frame buffer -> read other processes'/root's freed page contents (page cache, heap, anon).

Evidence (decisive lines)

run.log: 'ROOT-SECRET page 0 (tag=41 spraypage=121355): 524f4f5453454352 ...' and '== RESULT round 41 == ... ROOT-spray secret pages disclosed to unprivileged user: 14117; KERNEL MEMORY DISCLOSURE CONFIRMED (cross-privilege)'; control_rounds.log: reserve-intact rounds all-zero; fix_run.log: 127.6 MiB scan-path buffer on DF467XFIX, 0/32,673 nonzero, 0 secrets.

PoC changes

Seed assumed /dev/bktr0 + direct PQ sourcing. Rewritten: rig path, adaptive +32-page grows with per-grow wire deltas to distinguish reserve-served (zero, fictitious pages) from scan-served (recycled, contents intact) buffers, root-side 2600 MiB marker spray (23% sprays lose to pristine low-pfn runs -- measured; ~85% wins deterministically).

Verified recommended fix

bzero the newly grown buffer at install (grow path must match the attach path's bzero at bktr_core.c:432); see fix.diff.

Verdict

REPRODUCED (cross-privilege kernel-memory disclosure). After draining the low-phys DMA reserve with unprivileged METEORSETGEO grows (DF-4672's mechanism, same session), the grow path of the production unfixed bktr.ko installed unzeroed contigmalloc buffers whose pages come from the pfn-ascending PQ_FREE/PQ_CACHE scan; the verbatim bktr_mmap fault path mapped them PROT_READ into uid 1001, which read back ROOT's just-freed memory: 8 scan-served buffers scanned, 14,117 of 16,444 pages carried the ROOT-spray markers (magic, tag, spray page index). Control rounds with the reserve intact read all-zero (two-pool model proven). No OOB access involved -- purely the unzeroed in-bounds buffer.