The Pedigree Project 0.1
tlb-regressions.cc
1/*
2 * Copyright (c) 2026, Pedigree Developers
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted.
6 */
7
8#include "pedigree/kernel/Atomic.h"
9#include "pedigree/kernel/LockGuard.h"
10#include "pedigree/kernel/Log.h"
11#include "pedigree/kernel/process/Process.h"
12#include "pedigree/kernel/process/Scheduler.h"
13#include "pedigree/kernel/process/Semaphore.h"
14#include "pedigree/kernel/process/Thread.h"
15#include "pedigree/kernel/processor/MemoryRegion.h"
16#include "pedigree/kernel/processor/PhysicalMemoryManager.h"
17#include "pedigree/kernel/processor/Processor.h"
18#include "pedigree/kernel/processor/VirtualAddressSpace.h"
19#include "pedigree/kernel/time/Time.h"
20
21namespace {
22constexpr uint64_t OldMarker = 0x13579BDF2468ACE0ULL;
23constexpr uint64_t NewMarker = 0x0FEDCBA987654321ULL;
24constexpr uint64_t WrittenMarker = 0xA55A123443215AA5ULL;
25constexpr uint64_t ReusedMarker = 0x76543210FEDCBA98ULL;
26
27struct TlbRemapContext {
28 explicit TlbRemapContext(void* virtualAddress)
29 : address(virtualAddress),
30 warmed(0),
31 remapped(false),
32 failures(0),
33 remoteReaders(0),
34 remoteSuccesses(0),
35 warmedProcessors(0),
36 successfulProcessors(0),
37 mutatorProcessor(static_cast<size_t>(-1)) {}
38
39 void* address;
40 Semaphore warmed;
41 Atomic<bool> remapped;
42 Atomic<size_t> failures;
43 Atomic<size_t> remoteReaders;
44 Atomic<size_t> remoteSuccesses;
45 Atomic<uint64_t> warmedProcessors;
46 Atomic<uint64_t> successfulProcessors;
47 Atomic<size_t> mutatorProcessor;
48};
49
50int warmRemoteTranslation(void* parameter) {
51 TlbRemapContext* context = reinterpret_cast<TlbRemapContext*>(parameter);
52 volatile uint64_t* value = reinterpret_cast<volatile uint64_t*>(context->address);
53 const size_t processorBefore = Processor::index();
54 if (*value != OldMarker) {
55 context->failures += 1;
56 }
57 context->warmedProcessors |= uint64_t(1) << processorBefore;
58 context->warmed.release();
59
60 bool remapped = false;
61 for (size_t poll = 0; poll < 10000000; ++poll) {
62 if (context->remapped.value()) {
63 remapped = true;
64 break;
65 }
67 }
68 if (!remapped) {
69 context->failures += 1;
70 return 1;
71 }
72
73 __atomic_thread_fence(__ATOMIC_ACQUIRE);
74 const uint64_t observedAfter = *value;
75 const size_t processorAfter = Processor::index();
76 if (observedAfter != NewMarker) {
77 context->failures += 1;
78 return 1;
79 }
80
81 // A migrated reader can still detect a bad value, but it does not prove
82 // that one warmed processor retained and then discarded its translation.
83 if (processorAfter != processorBefore) {
84 return 0;
85 }
86
87 if (processorBefore != context->mutatorProcessor.value()) {
88 context->remoteReaders += 1;
89 context->remoteSuccesses += 1;
90 }
91 context->successfulProcessors |= uint64_t(1) << processorBefore;
92 return 0;
93}
94
95struct PrivateTlbContext {
97 void* address;
98 size_t processor;
99 uint64_t pat = 0;
100 Semaphore resume{0};
101 Atomic<size_t> ready{0}, stage{0};
102 Atomic<bool> aborted{false}, passed{false};
103};
104
105#if X64
106uint64_t readPat() {
107 uint32_t low, high;
108 asm volatile("rdmsr" : "=a"(low), "=d"(high) : "c"(0x277));
109 return (uint64_t(high) << 32) | low;
110}
111#endif
112
113bool waitPrivateTlbStage(PrivateTlbContext& context, size_t stage) {
114 const auto deadline = Time::getTicks() + 5 * Time::Multiplier::Second;
115 while (!context.aborted && Time::getTicks() < deadline) {
116 if (context.stage == stage) {
117 return true;
118 }
120 }
121 return false;
122}
123
124bool privateTlbValue(PrivateTlbContext& context, uint64_t expected) {
125 return Processor::index() == context.processor &&
126 &Processor::information().getVirtualAddressSpace() == context.space &&
127 *reinterpret_cast<volatile uint64_t*>(context.address) == expected;
128}
129
130bool waitPrivateTlbReady(PrivateTlbContext& context, size_t ready) {
131 const auto deadline = Time::getTicks() + 5 * Time::Multiplier::Second;
132 while (context.ready != ready && Time::getTicks() < deadline) {
134 }
135 return context.ready == ready;
136}
137
138int readPrivateTranslations(void* parameter) {
139 auto& context = *static_cast<PrivateTlbContext*>(parameter);
140 const bool interrupts = Processor::getInterrupts();
142#if X64
143 uint64_t cr0, savedCr4;
144 asm volatile("mov %%cr0, %0" : "=r"(cr0));
145 asm volatile("mov %%cr4, %0" : "=r"(savedCr4));
146 const bool cacheControlsMatch = !(cr0 & (3ULL << 29)) && readPat() == context.pat;
147 const uint64_t withGlobalPages = savedCr4 | (1ULL << 7);
148 asm volatile("mov %0, %%cr4" : : "r"(withGlobalPages) : "memory");
149#endif
150 // A context switch would hide a missing remote invalidation. Keep IRQs
151 // masked while warmed; pause services pending TLB requests cooperatively.
152 do {
153#if X64
154 if (!cacheControlsMatch) {
155 ERROR("QEMU private TLB reader has inconsistent cache controls");
156 break;
157 }
158#endif
159 if (!privateTlbValue(context, OldMarker)) {
160 break;
161 }
162 context.ready = 1;
163 if (!waitPrivateTlbStage(context, 1) || !privateTlbValue(context, NewMarker)) {
164 break;
165 }
166 context.ready = 2;
167 if (!waitPrivateTlbStage(context, 2) || !privateTlbValue(context, NewMarker)) {
168 break;
169 }
170 *reinterpret_cast<volatile uint64_t*>(context.address) = WrittenMarker;
171 context.ready = 3;
172 Processor::setInterrupts(interrupts);
173 if (!context.resume.acquireForCompletion(1, 5) || context.aborted) {
174 break;
175 }
177 context.passed = privateTlbValue(context, ReusedMarker);
178 } while (false);
180#if X64
181 asm volatile("mov %0, %%cr4" : : "r"(savedCr4) : "memory");
182#endif
183 Processor::setInterrupts(interrupts);
184 if (!context.passed) {
185 context.aborted = true;
186 }
187 return context.passed ? 0 : 1;
188}
189
190struct KernelTlbWitness {
191 size_t processor;
192 Semaphore entered{0}, release{0};
193 Atomic<bool> passed{false};
194};
195
196int witnessKernelAddressSpace(void* parameter) {
197 auto& context = *static_cast<KernelTlbWitness*>(parameter);
198 context.passed = Processor::index() == context.processor &&
199 &Processor::information().getVirtualAddressSpace() ==
201 context.entered.release();
202 return context.release.acquireForCompletion(1, 5) ? 0 : 1;
203}
204
205bool privateTlbMutator() {
206 NOTICE("QEMU-CONCURRENCY-TEST: BEGIN private-tlb-shootdown-smp");
207 const size_t mutator = Processor::index();
208 const CpuAffinityMask online = Scheduler::onlineAffinity();
209 size_t remote = CpuAffinityMask::MaximumCpus;
210 for (size_t cpu = 0; cpu < CpuAffinityMask::MaximumCpus; ++cpu) {
211 if (cpu != mutator && online.contains(cpu)) {
212 remote = cpu;
213 break;
214 }
215 }
216 if (remote == CpuAffinityMask::MaximumCpus) {
217 return false;
218 }
219
220 const size_t pageSize = PhysicalMemoryManager::getPageSize();
221 MemoryRegion backing("QEMU private TLB backing");
222 if (!PhysicalMemoryManager::instance().allocateRegion(
224 return false;
225 }
226 auto* original = static_cast<volatile uint64_t*>(backing.virtualAddress());
227 auto* replacement = reinterpret_cast<volatile uint64_t*>(
228 reinterpret_cast<uintptr_t>(backing.virtualAddress()) + pageSize);
229 *original = OldMarker;
230 *replacement = NewMarker;
231 physical_uintptr_t originalPage = 0, replacementPage = 0;
232 size_t flags = 0;
234 if (!kernel.getMapping(backing.virtualAddress(), originalPage, flags) ||
235 !kernel.getMapping(const_cast<uint64_t*>(replacement), replacementPage, flags)) {
236 return false;
237 }
238
239 Process* process = new Process(Scheduler::instance().getKernelProcess(), true);
240 VirtualAddressSpace* space = process->getAddressSpace();
241 void* address = reinterpret_cast<void*>(space->getDynamicStart() + 64 * pageSize);
242 // A private lower-half supervisor mapping must not survive a CR3 switch as
243 // a global translation. The kernel aliases retain ownership of both pages.
245 const size_t writeFlags = readFlags | VirtualAddressSpace::Write;
246 if (reinterpret_cast<uintptr_t>(address) >= space->getKernelStart() || space->isMapped(address) ||
247 !space->map(originalPage, address, readFlags)) {
248 delete process;
249 return false;
250 }
251 PrivateTlbContext context{space, address, remote};
252#if X64
253 context.pat = readPat();
254#endif
255 ThreadPlacement placement;
256 placement.allowed.set(remote);
257 Thread* reader = new Thread(process, readPrivateTranslations, &context, nullptr, false, false,
258 true, &placement);
259 reader->setName("QEMU private TLB reader");
260 const bool started = reader->start();
261 bool passed = started && waitPrivateTlbReady(context, 1);
262 if (passed) {
263 space->unmap(address);
264 // Reuse the detached backing before allowing the remote reader to load.
265 *original = ReusedMarker;
266 passed = space->map(replacementPage, address, readFlags);
267 if (passed) {
268 context.stage = 1;
269 passed = waitPrivateTlbReady(context, 2);
270 }
271 }
272 if (passed) {
273 passed = space->trySetFlags(address, writeFlags);
274 if (passed) {
275 context.stage = 2;
276 passed = waitPrivateTlbReady(context, 3) && *replacement == WrittenMarker;
277 }
278 }
279
280 KernelTlbWitness witness{remote};
281 Thread* witnessThread = nullptr;
282 if (passed) {
283 bool sleeping = false;
284 const auto deadline = Time::getTicks() + 5 * Time::Multiplier::Second;
285 while (!sleeping && Time::getTicks() < deadline) {
286 {
287 LockGuard<Spinlock> guard(reader->getLock());
288 sleeping = reader->getStatus() == Thread::Sleeping;
289 }
290 if (!sleeping) {
292 }
293 }
294 passed = sleeping;
295 if (passed) {
296 witnessThread =
297 new Thread(Scheduler::instance().getKernelProcess(), witnessKernelAddressSpace, &witness,
298 nullptr, false, false, true, &placement);
299 witnessThread->setName("QEMU private TLB switch witness");
300 if (!witnessThread->start()) {
302 passed = false;
303 } else {
304 passed = witness.entered.acquireForCompletion(1, 5) && witness.passed;
305 }
306 }
307 }
308 if (passed) {
309 space->unmap(address);
310 passed = space->map(originalPage, address, writeFlags);
311 }
312 context.aborted = !passed;
313 context.resume.release();
314 witness.release.release();
315 if (!started) {
316 reader->setUnwindState(Thread::TerminateThread);
317 }
318 if (!reader->joinForCompletion() || (witnessThread && !witnessThread->joinForCompletion())) {
319 FATAL("QEMU private TLB regression could not retire its workers");
320 }
321 passed = passed && context.passed;
322 if (space->isMapped(address)) {
323 space->unmap(address);
324 }
325 delete process;
326 if (!passed) {
327 ERROR("QEMU private TLB regression failed at stage " << Dec << context.stage.value());
328 return false;
329 }
330 NOTICE("QEMU-CONCURRENCY-TEST: private-tlb-shootdown reader=" << Dec << remote
331 << ", mutator=" << mutator);
332 NOTICE("QEMU-CONCURRENCY-TEST: PASS private-tlb-shootdown-smp");
333 return true;
334}
335
336int privateTlbMutatorEntry(void* parameter) {
337 auto& passed = *static_cast<Atomic<bool>*>(parameter);
338 passed = privateTlbMutator();
339 return passed ? 0 : 1;
340}
341
342bool privateTlbShootdownRegression() {
343 Atomic<bool> passed{false};
344 ThreadPlacement placement;
345 placement.allowed.set(Processor::index());
346 Thread* mutator = new Thread(Scheduler::instance().getKernelProcess(), privateTlbMutatorEntry,
347 &passed, nullptr, false, false, true, &placement);
348 mutator->setName("QEMU private TLB mutator");
349 if (!mutator->start()) {
351 }
352 if (!mutator->joinForCompletion()) {
353 FATAL("QEMU private TLB regression could not retire its mutator");
354 }
355 return passed;
356}
357} // namespace
358
359bool runTlbShootdownConcurrencyRegression() {
360 NOTICE("QEMU-CONCURRENCY-TEST: BEGIN shared-kernel-tlb-shootdown-smp");
361 const size_t processorCount = Processor::getCount();
362 if (processorCount < 2 || processorCount > 64) {
363 ERROR("QEMU TLB shootdown regression requires at least two processors");
364 return false;
365 }
366
368 MemoryRegion reservation("QEMU TLB shootdown regression");
369 if (!memory.allocateRegion(reservation, 1, PhysicalMemoryManager::virtualOnly,
371 ERROR("QEMU TLB shootdown regression could not reserve a kernel address");
372 return false;
373 }
374 // The fixture owns both physical pages explicitly; the region only keeps
375 // its kernel virtual address unavailable to other allocators.
376 reservation.setNonRamMemory(true);
377 reservation.setForced(true);
378
379 const physical_uintptr_t originalPage = memory.allocatePage();
380 const physical_uintptr_t replacementPage = memory.allocatePage();
382 if (!originalPage || !replacementPage ||
383 !addressSpace.map(originalPage, reservation.virtualAddress(),
385 if (originalPage) {
386 memory.freePage(originalPage);
387 }
388 if (replacementPage) {
389 memory.freePage(replacementPage);
390 }
391 reservation.free();
392 ERROR("QEMU TLB shootdown regression could not prepare physical pages");
393 return false;
394 }
395 *reinterpret_cast<volatile uint64_t*>(reservation.virtualAddress()) = OldMarker;
396
397 TlbRemapContext context(reservation.virtualAddress());
398 Process* process = Scheduler::instance().getKernelProcess();
399 Thread* readers[64] = {};
400 const size_t readerTarget = processorCount <= 32 ? processorCount * 2 : processorCount;
401 size_t startedReaders = 0;
402 for (; startedReaders < readerTarget; ++startedReaders) {
403 readers[startedReaders] =
404 new Thread(process, warmRemoteTranslation, &context, nullptr, false, false, true);
405 readers[startedReaders]->setName("QEMU remote TLB reader");
406 if (!readers[startedReaders]->start()) {
407 break;
408 }
409 }
410
411 for (size_t i = 0; i < startedReaders; ++i) {
412 if (!context.warmed.acquireForCompletion()) {
413 context.failures += 1;
414 }
415 }
416
417 context.mutatorProcessor = Processor::index();
418 addressSpace.unmap(reservation.virtualAddress());
419 const bool replacementMapped =
420 addressSpace.map(replacementPage, reservation.virtualAddress(),
422 if (replacementMapped) {
423 *reinterpret_cast<volatile uint64_t*>(reservation.virtualAddress()) = NewMarker;
424 __atomic_thread_fence(__ATOMIC_RELEASE);
425 } else {
426 context.failures += 1;
427 }
428 context.remapped = true;
429
430 bool readersJoined = true;
431 for (size_t i = 0; i < startedReaders; ++i) {
432 readersJoined = readers[i]->joinForCompletion() && readersJoined;
433 }
434 const bool mapped = addressSpace.isMapped(reservation.virtualAddress());
435 if (mapped) {
436 addressSpace.unmap(reservation.virtualAddress());
437 }
438 memory.freePage(originalPage);
439 memory.freePage(replacementPage);
440 reservation.free();
441
442 const bool passed = startedReaders == readerTarget && readersJoined && replacementMapped &&
443 mapped && !context.failures && context.remoteReaders &&
444 context.remoteSuccesses == context.remoteReaders;
445 if (!passed) {
446 ERROR("QEMU TLB shootdown regression did not observe the replacement mapping");
447 return false;
448 }
449
450 NOTICE("QEMU-CONCURRENCY-TEST: shared-kernel-tlb-shootdown mask="
451 << Hex << context.successfulProcessors.value() << Dec
452 << ", mutator=" << static_cast<size_t>(context.mutatorProcessor));
453 NOTICE("QEMU-CONCURRENCY-TEST: PASS shared-kernel-tlb-shootdown-smp");
454 return privateTlbShootdownRegression();
455}
Special memory entity in the kernel's virtual address space.
virtual physical_uintptr_t allocatePage(size_t pageConstraints=0)=0
static PhysicalMemoryManager & instance()
virtual void freePage(physical_uintptr_t page)=0
virtual bool allocateRegion(MemoryRegion &Region, size_t cPages, size_t pageConstraints, size_t Flags, physical_uintptr_t start=-1)=0
VirtualAddressSpace * getAddressSpace()
Definition Process.h:530
static bool getInterrupts()
static ProcessorInformation & information()
static size_t getCount()
static void pause()
static void setInterrupts(bool bEnable)
static size_t index()
static Scheduler & instance()
Definition Scheduler.h:96
void yield()
Definition Scheduler.cc:236
void setUnwindState(UnwindType ut)
Definition Thread.cc:3580
@ TerminateThread
Exit only this thread during Process exit.
Definition Thread.h:519
bool joinForCompletion()
Definition Thread.cc:2750
bool start()
Definition Thread.cc:751
virtual bool map(physical_uintptr_t physicalAddress, void *virtualAddress, size_t flags)=0
virtual bool isMapped(void *virtualAddress)=0
static EXPORTED_PUBLIC VirtualAddressSpace & getKernelAddressSpace()
virtual uintptr_t getKernelStart() const =0
virtual MUST_USE_RESULT bool trySetFlags(void *virtualAddress, size_t newFlags)
virtual void unmap(void *virtualAddress)=0
@ Dec
Definition Log.h:126
@ Hex
Definition Log.h:124
Definition waits.c:9