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)
PoC verification
Evidence pack
findings/poc/DF-4673 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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
METEORSETGEOgrows; each grow's wire delta (0 = reserve-served, >0 = scan-served/recycled) was printed; every scan-served buffer was immediatelymmap(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_memexhaustion (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 installsget_bktr_mem()'d buffer with no zeroing (bktr_core.c:1667-1673;get_bktr_mempasses onlyM_NOWAIT, bktr_os.c:403-407 β noM_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_mmapmaps them at first fault with no exec-only restriction (device_pager.c:348-390old_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
fixedfixed
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.
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.
No comments yet.