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

bktr_mmap is re-executed at page-fault time and reads bktr->bigbuf/alloc_pages with no lock while METEORSETGEO's grow path kmem_free()s and reassigns the buffer β€” fault in the window maps pages via a dangling KVA pointer (stale mappings; ceiling: RW kernel-memory alias)

Summary

The device pager re-queries d_mmap on every fault, so the offset check and vtophys(bigbuf)+offset run against live softc state with no sync; the unlocked METEORSETGEO grow block does get_bktr_mem -> kmem_free(old) -> reassign, and the bare kmem_free performs no pmap_remove - a fault in the window computes atop(vtophys(dangling KVA)) through surviving stale PTEs: deterministic orphaned-page mapping, and if the KVA is re-allocated within the window an RW alias of LIVE kernel memory (only PROT_EXEC refused; /dev/bktr0 is 0444 and METEORSETGEO needs no privilege). Ceiling user->root via kernel-image overwrite; hardware-gated. buf_lock serialization fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4671 Β· 21 files
FileTypeDescriptionSize
poc_race.c β€” 15.2 KB view raw
bktrig2.c β€” 10.6 KB view raw
bktr_mmap_race.c β€” 4.6 KB view raw
build.sh β€” 132 B view raw
run.sh β€” 194 B view raw
run.log β€” 2.2 KB view raw
race2.log β€” 19.5 KB view raw
race3.log β€” 181.1 KB view raw
race4.log β€” 89 B view raw
race5.log β€” 88 B view raw
race6.log β€” 0 B ↓ download
race11.log β€” 2.8 KB view raw
race12.log β€” 2.1 KB view raw
race_diag.log β€” 1.1 KB view raw
run_4671.log β€” 32.0 KB view raw
fix.diff β€” 4.4 KB view raw
env.txt β€” 1.2 KB view raw
VERDICT.md β€” 6.6 KB ↓ raw
README.md β€” 1.2 KB ↓ raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 4.6 KB view raw

DF-4671 β€” bktr_mmap fault-time re-query races the unlocked METEORSETGEO buffer swap

Status: NOT REPRODUCED as a race on this rig (source-proven mechanism; sub-Β΅s window not caught across ~1,700 grow windows / ~10⁴ genuine first-fault queries). Two discoveries documented: (1) the device pager's fake-page cache survives munmap (stale phys_addr re-served, d_mmap never re-queried for already-faulted pindexes); (2) a HELD mapping stays a live RW alias onto the orphaned pages after the grow path frees the buffer β€” deterministic, race-free (--holdcheck).

Build

./build.sh    # poc_race (unpriv; includes --seqcheck and --holdcheck modes)

Run

./run.sh      # seqcheck (confounder proof), holdcheck (deterministic alias),
              # then the hardened grow-vs-fault race as uid 1001

Expected

  • --seqcheck: 64/64 stale markers re-served with zero concurrency (pager fake-page cache survives munmap β€” disqualifies naive rigs).
  • --holdcheck: 64/64 markers alive + mapping still WRITABLE after the mapped buffer was kmem_free'd (orphaned-page alias).
  • hardened race: catches=0 (the in-window dangling resolution was not caught; see VERDICT.md for the full attempt accounting).
VERDICT.md
↓ download raw

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

Verdict

NOT REPRODUCED as a fault-time race on this rig β€” the mechanism is source-proven (and every instrumentation confounder was hunted down and eliminated, which is itself the bulk of this evidence pack), but the unsynchronized window between kmem_free(old bigbuf) and bktr->bigbuf = buf (bktr_core.c:1669-1672) proved empirically too narrow to catch with a userspace faulter across ~1,700 kernel-verified grow windows and roughly 10⁴ genuine first-fault d_mmap re-executions. Two important DISCOVERIES about the surrounding machinery are documented below; one of them (the held-mapping orphan alias) is a deterministic, race-free manifestation of the same missing buffer-lifetime bug and is reproduced (holdcheck).

What is source-proven (traced line by line this run)

  • The device pager re-executes d_mmap on the first fault of every page index of a cdev: device_pager.c:348-390 old_dev_pager_fault β†’ dev_dmmap β†’ bktr_mmap (bktr_os.c:656-685).
  • bktr_mmap reads alloc_pages (bktr_os.c:680) and bigbuf (:683) with no lock; the METEORSETGEO grow block mutates both with no lock (bktr_core.c:1664-1681; video_ioctl contains no locking at all β€” verified by reading the whole function).
  • During the window, vtophys(old KVA) still resolves: kmem_free β†’ vm_map_remove β†’ vm_map_delete (vm_map.c:3300-3370) performs no pmap_remove for the object-less VM_MAPTYPE_NORMAL entry and no page release (vm_map_entry_delete: unlink+dispose only), so the pmap_kenter'd PTEs survive until another contigmalloc reuses the KVA β€” a fault in the window maps the orphaned page. KVA reuse was observed directly (grow #32 reallocating the original RIG_INIT KVA 0xfffff80118aa0000; per-grow KVA/PA traces in race_diag.log).

Why it could not be caught here (measured, not hand-waved)

Two confounders had to be proven first β€” both invalidated naive rigs and both are interesting in their own right:

  1. The old-style device pager's fake-page cache survives munmap (--seqcheck, no concurrency at all): markers written into buffer generation 0 were re-served 64/64 pages after munmap + 10 purely sequential grows. Consequence: a page fault only executes the driver's d_mmap for a pindex never faulted before on that cdev; every later fault is served from the cached fake page with a stale phys_addr and never re-queries. All "catches" from early rig versions (race2/race3: 464 events) were exactly this artifact and were disqualified. (This also narrows the finding's premise: the "re-queried on every fault" model holds only per first fault of a pindex.)
  2. SETEORSETGEO returns success without growing when the requested geometry is not strictly larger, so a userspace epoch counter over-counts; fixed by keying all generation accounting on the kernel's alloc_pages (monotonic, changes only on real grows).

The final protocol (race11/race12): production grow path hammered by 1-2 grower threads (each grow kernel-verified via STATUS), 4 faulter threads faulting only never-faulted pindex groups across 8 rig cdevs (fresh pager objects per cdev β†’ genuine d_mmap executions), per-run nonces, marker generations sampled after faults (so marker.gen < pre-fault alloc_pages can only mean the fault resolved a page of an already-freed buffer β€” false-positive-proof). Result across all hardened runs: 0 catches. The window (the duration of one kmem_free + two stores, sub-Β΅s to low-Β΅s) against a first-fault rate of a few thousand per second and a per-buffer marked-page fraction of 1/8–1/4 gives an expected catch count of order 1 per several runs; we ran out of practical amplification (leaked memory budget, serial console spam from contigmalloc failures, faulter iteration cost) before landing one.

What IS deterministically demonstrated (consequence, not the race)

--holdcheck (no race, no threads): an RW mapping established on the live buffer stays a live, writable alias onto the orphaned pages after the grow path kmem_frees the buffer beneath it (64/64 marker pages intact and a test store read back after 10 grows). The device pager pins nothing, so with the stock code (pages never freed β€” DF-4672) a held mapping is a permanent RW window onto orphaned kernel pages; on real hardware the 0444 node limits an unprivileged holder to PROT_READ (vm_mmap.c:368-371 denies PROT_WRITE without FWRITE), i.e. a persistent read alias onto the old frame-buffer pages after they conceptually ceased to exist. (With the contigfree() fix applied, those pages instead return to the free queues while still aliased β€” see the residual note in fix.diff; a complete fix needs pager-referenced buffer lifetimes.)

Rig fidelity

Same as DF-4672/4673 (production video_ioctl grow path, production get_bktr_mem/contigmalloc/kmem_free; bktr_mmap verbatim copy β€” the only delta is the softc lookup, and the dropped minor-number decode on the rig's video-function node).

Honest classification

Per the audit table: expected marker absent, run exits 0, guest stays up β†’ not_reproduced, reproduced=0, impact=none for the race claim as filed. The finding remains source-certain (CWE-367 TOCTOU is real in the code); the practical severity hinges on a sub-microsecond window and on the first-fault-only re-query behavior, both now measured. The deterministic held-mapping alias above is the strongest demonstrated consequence and belongs to the same root cause cluster (no buffer lifetime tied to mappings).

Files

File Purpose
poc_race.c hardened race PoC + --seqcheck + --holdcheck modes
race2.log,race3.log disqualified runs (pager-cache artifact, 464 events)
race4.log,race5.log,race6.log,race_diag.log post-fix zero-catch runs, KVA/PA traces
race11.log,race12.log final amplified protocol (8 cdevs, 4 faulters, dual growers), zero catches
run.log consolidated record incl. seqcheck/holdcheck transcripts
bktr_mmap_race.c original pass-1 hardware PoC (kept for reference)
fix.diff consistent-sampling + fail-closed bktr_mmap hardening (validated to build; race not catchable so behavioral A/B limited to DF-4672/4673 aspects)
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

not_testable
baseline no→ patch + rebuild →patched clean

not_testable

The race itself never reproduced on the baseline, so no behavioral A/B of the race is possible; fix.diff's bktr_mmap hardening and the shared grow-block fixes were built and boot-validated on the DF467XFIX kernel (fix_build.log, fix_run.log in the DF-4672/4673 packs), and the fixed bktr_mmap compiles and serves mappings normally (4672/4673 A/B exercised the same grow+mmap paths).
↓ fix.diffper-fix-DF-4671

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (consolidated): race11.log/race12.log zero catches under the final protocol; race2/race3 logs show the 464 disqualified pager-cache artifacts; seqcheck + holdcheck transcripts in run.log; race_diag.log per-grow KVA/PA traces proving buffer churn and KVA reuse (stale-PTE window mechanics).

PoC changes

Complete rewrite of the seed: rig device path; per-run nonces; kernel-truth generation counters (SETGEO returns success without growing unless strictly larger -- fixed); never-faulted pindex groups only (8 cdevs for fresh pager objects after proving the fake-page cache survives munmap); marker generations sampled after faults making catches false-positive-proof; single/dual grower + 1-4 faulters; plus --seqcheck/--holdcheck diagnostic modes.

Verified recommended fix

Serialize the grow block against bktr_mmap (consistent sampling + install-before-free + vtophys==0 fail-closed as in fix.diff; complete fix needs pager-referenced buffer lifetimes).

Verdict

NOT REPRODUCED as a fault-time race on this rig. The TOCTOU is source-proven (device pager re-executes d_mmap on first fault of each page index, device_pager.c:348-390; bktr_mmap reads alloc_pages/bigbuf unlocked at bktr_os.c:680/683; the grow block frees and reassigns bigbuf with no lock at bktr_core.c:1669-1672; the bare kmem_free performs no pmap_remove for the object-less entry, vm_map.c:3300-3370, so stale PTEs resolve throughout the window), but across 12+ hardened runs (~1,700 kernel-verified grow windows, ~10^4 genuine first-fault d_mmap executions, 1-2 growers incl. kernel_map-contention dual-grower, 1-4 faulters over 8 cdevs with fresh pager objects) no dangling resolution was caught: the window is empirically sub-microsecond. Two measurement discoveries are documented: (1) the old-style device pager's fake-page cache SURVIVES munmap (--seqcheck: 64/64 stale markers re-served with zero concurrency), so d_mmap re-query happens only on first fault of a pindex -- this both disproved naive rigs (464 false 'catches' disqualified) and narrows the finding's premise; (2) a HELD mapping stays a live RW alias onto the orphaned pages after the grow frees the buffer beneath it (--holdcheck: 64/64 markers alive, still writable, deterministic, race-free) -- the same missing buffer-lifetime root cause, demonstrated without the race.