pmap_inval_smp()/pmap_inval_smp_cmpset() shortcut samples pmap->pm_active outside the CPULOCK_EXCL interlock (and takes none for non-PMAP_MULTI pmaps); originator also returns before target done-bits drain
Summary
The single-cpu shortcut decides the invalidation set from pm_active BEFORE pmap_inval_init acquires CPULOCK_EXCL (and takes no interlock at all when PMAP_MULTI is clear): a cpu concurrently switching into the pmap that slips its EXCL test into the gap caches the old translation the shortcut's local-only invlpg never evicts - persistent stale TLB for an unmapped page (stale-TLB UAF/info-leak class). pm_active is also not a sound no-live-TLB indicator (descheduled cpu's bit clears while TLB state persists), and the originator returns after clearing only its own done bit while targets' trailing invlpgs are outstanding. Unpriv munmap-vs-activation race on SMP. Rig fired 0.2-18/min phantom TLB accesses on KVM, but the four-control verification campaign (shortcut off / all-cpu broadcast / branch instrumentation / broadcast+drain exhaustion-fix kernel) pinned the residual on the KVM virtual TLB - honest not_reproduced for kernel impact on this platform; the ordering flaws stand on protocol-construction proof. Interlock-before-sample + no-user-shortcut + smp_active_mask + drain-targets fix (validated to build/boot).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5420 Β· 21 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 5.3 KB | β raw | |
| VERDICT.md | β | 6.2 KB | β raw | |
| stale_tlb_race.c | β | 12.1 KB | view raw | |
| build.sh | β | 105 B | view raw | |
| run.sh | β | 102 B | view raw | |
| instrument.diff | β | 5.7 KB | view raw | |
| fix.diff | β | 4.7 KB | view raw | |
| run.base.log | β | 3.6 KB | view raw | |
| run.ctrl.log | β | 5.7 KB | view raw | |
| run.ctrl2.log | β | 8.6 KB | view raw | |
| run.probe4.log | β | 1.2 KB | view raw | |
| rootprobe.log | β | 5.3 KB | view raw | |
| rootinstr.log | β | 2.8 KB | view raw | |
| rootinstr2.log | β | 491 B | view raw | |
| rootinstr3.log | β | 492 B | view raw | |
| fixrun1.log | β | 75.5 KB | view raw | |
| fix_build.log | β | 230 B | view raw | |
| instrument_ring_hit.txt | β | 1.5 KB | view raw | |
| env.txt | β | 490 B | view raw | |
| manifest.json | β | 1.3 KB | view raw | |
| verdict.json | β | 4.8 KB | view raw |
DF-5420 β pmap_inval_smp()/pmap_inval_smp_cmpset() single-cpu shortcut samples pmap->pm_active outside the CPULOCK_EXCL interlock (+ early return before target done-bits drain)
What (source-level defects, pass 2)
Three related ordering/protocol weaknesses in the SMP TLB-invalidation engine,
all in sys/platform/pc64/x86_64/pmap_inval.c:
- Shortcut TOCTOU (primary).
pmap_inval_smp()line 282 (andpmap_inval_smp_cmpset()line 460) evaluatesCPUMASK_CMPMASKEQ(pmap->pm_active, gd->gd_cpumask)beforepmap_inval_init()acquiresCPULOCK_EXCL(which the shortcut only does at line 289, and only whenPMAP_MULTIis set β non-PMAP_MULTI pmaps take no interlock at all). The activation protocol inswtch.scpu_heavy_restore(OR pm_active bit at :408, then test CPULOCK_EXCL at :410-412, then unconditionally load %cr3 at :445) only guarantees a concurrently-activating cpu is caught if the invalidator samples pm_active under the interlock. Sampling before it (or without it) leaves a window where the activating cpu ORs its bit after the sample, slips its EXCL test in before the acquire, loads %cr3, returns to user mode, and caches the old translation β then the shortcut swaps the PTE and invalidates only the local cpu. That cpu keeps a live TLB entry for a page the kernel has unmapped (use-after-free / info-leak class primitive). - pm_active is not a sound "no live TLB" indicator. The full path samples
tmpmask = pmap->pm_activeat :374 (also :524). A descheduled cpu's bit is cleared at switch-out (swtch.s:200) while its TLB state persists; safety rests entirely on the reactivation %cr3 reload (swtch.s:436-445, deliberately unconditional per the "XXX Temporary cludge" comment β thejeoptimization is commented out). The invalidation engine hangs PTE teardown correctness on an invariant it does not enforce itself. - Early return. The originator returns after clearing only its own
done bit (:418-429, :552-558). Targets' trailing
invlpgs (executed atpmap_inval_intr:743-767 after the originator's swap) may not have run yet β a real-hardware window where*ptep == 0but target TLBs still hold the old translation.
Verified on the guest β and the honest outcome
The rig (stale_tlb_race.c) reproduces phantom TLB accesses (successful
user-mode load of a va whose PTE is provably 0 β kernel copyin of the same
va EFAULTs; the phantom dies at the first syscall) at 0.2β18/min as an
unprivileged user. A kernel-instrumentation build (see instrument.diff,
ring dumps in instrument_ring_hit.txt) showed the full-path state machine
executing correctly (quiesce β originator swap β target invlpg β done bits
drain) while hits continued, including under:
- machdep.pmap_inval_force_nonopt=1 (shortcut disabled) β 30+ hits,
- machdep.pmap_inval_force_allcpus=1 (broadcast to every cpu) β 46 hits,
- a fix kernel (fix.diff: interlock hoist + no user-ptep shortcut +
broadcast + drain-wait for all target done bits) β 185 hits.
Since the fix kernel guarantees (by construction, with the runs completing
i.e. every done bit drained) that every active cpu executed invlpg(va)
before munmap() returned, surviving stale entries are architecturally
impossible on real hardware β the residual phenomenon is attributable to
the virtualization layer (QEMU 7.2/KVM virtual TLB losing guest flushes;
host VPID state uninspectable β no host root). The source-level defects above
remain real, but could not be demonstrated above this artifact noise floor.
Build
cc -O2 -pthread -o stale_tlb_race stale_tlb_race.c
Run (unprivileged)
./stale_tlb_race 60 1 # 60 s, with stretcher
./stale_tlb_race 60 1 0 1 2 # pinned T0=cpu0 T1=cpu1 Q=cpu2 (root only:
# usched_set(USCHED_SET_CPUMASK) is EPERM
# for unprivileged users; rig continues
# unpinned)
Expected output
Phantom hits print e.g.
[ts] STALE-TLB-HIT type=A(unmapped-load-ok) val=0x... (s1=1 s2=1 ...) probe[...]: frozen=1 phase=1 (1=UNMAPPED) kernel-copyin write()=-1 errno=14 probe[...]: post-syscall user read #1: FAULT
type=A = successful load while the PTE is provably absent (both phase
snapshots bracket the load inside the unmapped window). Exit code 42 when any
hit was recorded. On real multi-core hardware this rig is expected to be
silent on a correct kernel; on this QEMU/KVM guest it fires due to the
virtualization artifact described in VERDICT.md β the run is therefore not
evidence of a kernel bug by itself.
Files
stale_tlb_race.c the rig (T0 munmap hammer, T1 sleeping reader + detectors,
Q stretcher process, frozen phantom probe)
instrument.diff kernel instrumentation (branch-level trace of every
invalidation of the rig's fixed va) β audit-only
fix.diff validated-attempt fix (interlock hoist + user-ptep
broadcast + drain wait)
run.*.log baseline / control / probe / instrumented / fixed runs
instrument_ring_hit.txt kernel ring dump around a hit
env.txt guest identity and sysctls
DF-5420 β VERDICT
Status: not_reproduced (as a DragonFly kernel defect on the reference guest).
The rig reproduces phantom TLB accesses, but exhaustive kernel-side
instrumentation and controls attribute the surviving signal to a QEMU/KVM
virtual-TLB artifact. The source-level ordering flaws are real and remain
likely-confidence hardening findings.
What was run
- Guest: DragonFly 6.5-DEVELOPMENT (master DEV, kernel #0 Thu Jul 2 06:02:54 UTC 2026, identical /usr/src to the audit tree β md5 verified), QEMU 7.2.22
- KVM,
-smp 6,machdep.meltdown_mitigation=1. - Rig:
stale_tlb_race.cβ unprivileged userland; T0 hammers munmap/mmap/MAGIC writes on a fixed page XADDR; T1 sleeps (jittered 2-80 Β΅s) and guarded-reads XADDR; optional Q stretcher process. Two false-positive- proof detectors (see README). - 14 recorded runs across stock / sysctl-control / instrumented / fixed kernels (run..log, root.log, fixrun1.log).
Phenomenon (certain, guest level)
Successful user-mode load of XADDR while the PTE is provably 0:
phase==PH_UNMAPPED on both sides of the load brackets it inside the
munmapβremap window (anon mmap is lazy; the fault-in store happens under
PH_WARM/PH_MAPPED), and the frozen probe shows kernel-mode write(XADDR,8)
returning -1/EFAULT at the same moment. The phantom dies at T1's next
syscall. Rates: 18 hits/60 s (stock, unpriv), 12/60 s (root-pinned), 30+/120 s
(shortcut disabled), 46/90 s (all-cpu broadcast), 185/180 s (fix kernel).
Attribution work
- Shortcut disabled (
machdep.pmap_inval_force_nonopt=1) β hits continue β not (only) the shortcut. - All-cpu broadcast (
machdep.pmap_inval_force_allcpus=1) β hits continue β not (only) the pm_active mask. - Kernel instrumentation (
instrument.diff: branch-level trace of every invalidation of XADDR β shortcut entry/exit, publish, slave skip/quiesce/wait/invlpg, originator phases, completion). Ring dumps around hits show the full-path state machine operating correctly (quiesce β swap β target invlpg β done drained, e.g. idx 430815-430853 ininstrument_ring_hit.txt) while the last command before a dump window was frequently the single-cpu shortcut withpm_active == {cpu0}β the victim cpu was descheduled (bit clear, TLB retained). - Exhaustion fix (
fix.diff= interlock hoisted before sampling + no shortcut for user-pmap PTE ops + mask = smp_active_mask for those ops + drain-wait until all target done bits clear). Runs completed (no hang β every done bit drained β every active cpu executedinvlpg(XADDR)beforemunmapreturned) β and hits persisted (185/180 s). On real hardware this configuration makes a surviving entry architecturally impossible (same-cpu program order + IPI-acknowledged invlpg). Therefore the residual signal is lost below the guest architectural level: the QEMU/KVM virtual TLB (VPID-tagged) dropping guestinvlpg/mov cr3flush effects under this cross-vCPU workload. No host root was available to disable/inspect host VPID state or re-run under TCG.
Verdict on the source-level defects
- Leg 1 (shortcut samples pm_active outside the interlock, pmap_inval.c:282/289 and :460; non-PMAP_MULTI takes no interlock at all) is a genuine ordering flaw by protocol construction: the swtch.s activation protocol (OR bit :408 β test EXCL :410 β unconditional cr3 :445) only closes the race if the invalidator samples under EXCL; the full path does (sample at :374 after init at :331), the shortcut does not. Not separately demonstrable on this guest (the artifact noise floor is 2-3 orders of magnitude above the shortcut window's expected hit rate; the instrumented traces never caught a hit whose timing matched the TOCTOU β the victim's bit was consistently long clear, i.e. descheduled, not mid-activation).
- Leg 2 (pm_active β "no live TLB") β real; the protocol leans on the reactivation %cr3 reload (whose same-value load flush is exactly what the virtualization layer drops).
- Leg 3 (early return before target done-bits drain, :418-429/:552-558) β real on any hardware; a Β΅s-scale window with PTE==0 and target TLBs stale; the fix kernel's drain-wait closes it.
Impact assessment
If any leg fires on real hardware: an unprivileged user's thread retains a read/write TLB mapping to a page the kernel has unmapped and freed β a stale-TLB use-after-free primitive (page reuse β cross-process data access; page-table-page reuse β potential privilege escalation). Not demonstrated to uid=0 (no exploitable instance on the reference guest: the observed phantoms die at the first syscall and the attribution is virtualization-side).
Recommended fix
fix.diff (validated to build/boot; efficacy untestable on this guest for
the reasons above): hoist pmap_inval_init() before any pm_active sample,
drop the single-cpu shortcut for user-pmap PTE-modifying operations, compute
their mask from smp_active_mask, and drain target done-bits before
returning. The minimal upstream subset (hoist + drain) fixes legs 1 and 3;
fully closing leg 2 upstream wants a "recently used this pmap" cpu mask that
is not cleared on deschedule (e.g. pm_tlbmask), since trusting the
reactivation %cr3 flush has now been shown fragile.
Kernel references
- sys/platform/pc64/x86_64/pmap_inval.c:282-292 (shortcut decision before interlock)
- sys/platform/pc64/x86_64/pmap_inval.c:317-318, 469-474 (PMAP_MULTI-gated init/done)
- sys/platform/pc64/x86_64/pmap_inval.c:374-390 (full-path mask sample)
- sys/platform/pc64/x86_64/pmap_inval.c:418-429, 552-558 (early return, own done bit only)
- sys/platform/pc64/x86_64/pmap_inval.c:460-477 (cmpset shortcut)
- sys/platform/pc64/x86_64/swtch.s:408-445 (activation ORβEXCL testβunconditional cr3)
- sys/platform/pc64/x86_64/swtch.s:176-201 (switch-out clears pm_active bit)
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_testablefix.diff built and booted (kernel #4). Baseline phantom rate ~12-18/min; under the fix the rate was 185/180s (~1/s, higher). Because the fix guarantees by construction that every active cpu executed invlpg(va) before munmap returned (drain completed; runs did not hang), the surviving entries cannot be kernel misses on real hardware β fix efficacy is untestable on this guest because the measurable baseline signal itself is dominated by the KVM virtual-TLB artifact. The fix remains the correct hardening for the three source-level legs.
fixrun1.log (fixed kernel, 185 hits); compare run.base.log (stock kernel); fix.diff (git apply --check verified against pristine sys/)
Confirmed kernel references
- sys/platform/pc64/x86_64/pmap_inval.c:282
- sys/platform/pc64/x86_64/pmap_inval.c:289
- sys/platform/pc64/x86_64/pmap_inval.c:374
- sys/platform/pc64/x86_64/pmap_inval.c:418
- sys/platform/pc64/x86_64/pmap_inval.c:427
- sys/platform/pc64/x86_64/pmap_inval.c:460
- sys/platform/pc64/x86_64/pmap_inval.c:524
- sys/platform/pc64/x86_64/pmap_inval.c:552
- sys/platform/pc64/x86_64/swtch.s:408
- sys/platform/pc64/x86_64/swtch.s:445
Detail
Evidence (decisive lines)
run.base.log (18 hits/60s stock unpriv), run.ctrl.log / run.ctrl2.log (hits persist with shortcut disabled and with all-cpu broadcast), rootprobe.log (root-pinned hits), run.probe4.log + rootinstr.log (frozen probe: kernel copyin -1/EFAULT while user read succeeds; phantom gone after one syscall), instrument_ring_hit.txt (kernel ring: full-path command completing correctly on both cpus; last command before dump = shortcut with pm_active={cpu0}), fixrun1.log (185 hits/180s on the exhaustion fix kernel), VERDICT.md (full attribution narrative)
PoC changes
Evolved the seed rig through: correct DFly cpumask size (32-byte for usched_set), Q stretcher initial-map fix, freeze-in-unmapped-hold probe, kernel-side ring dump via sysctl at hit time, dual false-positive-proof detectors (phase-bracketed load + magic comparison). The orchestrator seed's syscall numbers were N/A (pure userland rig).
Verified recommended fix
Hoist pmap_inval_init() before any pm_active sample, drop the single-cpu shortcut for user-pmap PTE-modifying ops, broadcast those to smp_active_mask, and drain target done-bits before returning (see fix.diff)
Verdict
Pass-2 finding on the pmap_inval.c invalidation engine: the single-cpu shortcut in pmap_inval_smp()/pmap_inval_smp_cmpset() samples pmap->pm_active BEFORE acquiring (or without acquiring) the CPULOCK_EXCL interlock, the mask source pm_active is not a sound no-live-TLB indicator (descheduled cpus keep TLB state until a reactivation %cr3 reload the engine does not enforce), and the originator returns after clearing only its own done bit while targets' trailing invlpgs are still outstanding. The unprivileged rig reproduces phantom TLB accesses (successful user load of a va whose PTE is provably 0; kernel copyin EFAULTs; phantom dies at the first syscall) at 0.2-18/min on the QEMU/KVM guest, but kernel-level branch instrumentation shows the full-path state machine executing correctly, and the phenomenon SURVIVES shortcut-disabled, all-cpu-broadcast, and a fix kernel that drains every target done bit before munmap returns β an outcome architecturally impossible on real hardware. The residual signal is therefore attributed to the virtualization layer (QEMU 7.2/KVM virtual TLB dropping guest invlpg/mov-cr3 flush effects; host uninspectable without root). The three source-level ordering flaws remain real (protocol-construction proof for the shortcut TOCTOU) but were not demonstrable above the artifact noise floor; severity downgraded to hardening-class Medium.
No comments yet.