The Pedigree Project 0.1
memory-lock-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/Log.h"
9#include "pedigree/kernel/process/Process.h"
10#include "pedigree/kernel/process/Scheduler.h"
11#include "pedigree/kernel/process/Thread.h"
12#include "pedigree/kernel/processor/MemoryRegion.h"
13#include "pedigree/kernel/processor/PhysicalMemoryManager.h"
14#include "pedigree/kernel/processor/Processor.h"
15#include "pedigree/kernel/processor/UserMemoryPolicy.h"
16#include "pedigree/kernel/processor/VirtualAddressSpace.h"
17#include "pedigree/kernel/utilities/utility.h"
18
19#include "modules/system/vfs/File.h"
21
22namespace {
23bool check(bool condition, const char* detail) {
24 if (!condition)
25 ERROR("MEMORY-LOCK-TEST: FAIL " << detail);
26 return condition;
27}
28
29class TestAccount final : public MemoryLockAccount {
30 public:
31 bool permitsTotalPages(size_t total, bool) const override {
32 return total <= maximumPages;
33 }
34 size_t maximumPages = ~size_t(0);
35};
36
37class LockProbeFile final : public File {
38 public:
39 LockProbeFile()
40 : File(String("memory-lock-probe"), 0, 0, 0, 1, nullptr,
41 2 * PhysicalMemoryManager::getPageSize(), nullptr),
42 m_Storage("Memory Lock Probe") {
44 }
45 ~LockProbeFile() override {
46 shutdownFillCacheWriteback();
47 }
48
49 bool initialise() {
50 if (!PhysicalMemoryManager::instance().allocateRegion(
52 return false;
53 ByteSet(m_Storage.virtualAddress(), 0x49, getSize());
54 return true;
55 }
56 size_t getBlockSize() const override {
58 }
59 bool evict(size_t offset) {
60 return cacheState().fill.evict(offset);
61 }
62 size_t loans() {
63 return __atomic_load_n(&physicalPageLoans(), __ATOMIC_ACQUIRE);
64 }
65 bool backingUnchanged() {
66 auto* bytes = static_cast<const uint8_t*>(m_Storage.virtualAddress());
67 for (size_t n = 0; n < getSize(); ++n) {
68 if (bytes[n] != 0x49)
69 return false;
70 }
71 return true;
72 }
73
74 protected:
75 uintptr_t readBlock(uint64_t offset) override {
76 if (offset >= getSize())
77 return FILE_BAD_BLOCK;
78 return reinterpret_cast<uintptr_t>(m_Storage.virtualAddress()) + offset;
79 }
80 bool pinBlock(uint64_t offset) override {
81 return offset < getSize();
82 }
83 void unpinBlock(uint64_t) override {}
84
85 private:
86 MemoryRegion m_Storage;
87};
88
89bool compactUntilAbsent(MemoryMapManager& manager, VirtualAddressSpace& space, uintptr_t address) {
90 for (size_t attempt = 0; attempt < 256; ++attempt) {
91 // The pressure path refuses recursive entry; each call needs a fresh operation.
92 manager.compact();
93 if (!space.isMapped(reinterpret_cast<void*>(address)))
94 return true;
96 }
97 return false;
98}
99
100bool cleanupMappings(MemoryMapManager& manager, TestAccount& account, LockProbeFile& file) {
101 MemoryMapManager::OperationGuard operation(manager);
102 manager.unmapAll();
103 const auto charge = account.charge();
104 return check(!charge.managedPages && !charge.rawPages && !file.loans(),
105 "mapping teardown retained a lock charge or file loan");
106}
107
108bool compactorCase(MemoryLockMode mode, MemoryMapManager& manager, VirtualAddressSpace& space,
109 TestAccount& account) {
110 LockProbeFile file;
111 const size_t pageSize = PhysicalMemoryManager::getPageSize();
112 uintptr_t locked = 0;
113 uintptr_t control = 0;
114 physical_uintptr_t originalPhysical = 0;
115 bool passed = check(file.initialise(), "compactor backing allocation");
116 if (passed) {
117 passed = [&]() {
118 {
119 MemoryMapManager::OperationGuard operation(manager);
120 if (!check(manager.mapFile(&file, locked, pageSize, MemoryMappedObject::Read, 0, false) &&
121 manager.mapFile(&file, control, pageSize, MemoryMappedObject::Read, pageSize,
122 false) &&
123 manager.faultIn(locked, false) && manager.faultIn(control, false),
124 "compactor mapping setup"))
125 return false;
126 size_t flags = 0;
127 space.getMapping(reinterpret_cast<void*>(locked), originalPhysical, flags);
128 if (!check((flags & VirtualAddressSpace::Borrowed) && file.loans() == 2 && !file.evict(0) &&
129 !file.evict(pageSize),
130 "resident shared mappings did not retain both cache loans"))
131 return false;
132 if (!check(manager.lockMemory(space, locked, pageSize, mode, false) ==
133 MemoryLockStatus::Success &&
134 manager.hasLockedMemory(space, locked, pageSize) &&
135 !manager.hasLockedMemory(space, control, pageSize) &&
136 account.charge().managedPages == 1,
137 "single-page lock policy publication"))
138 return false;
139 }
140
141 if (!check(compactUntilAbsent(manager, space, control),
142 "actual compactor did not release the unlocked control"))
143 return false;
144 {
145 MemoryMapManager::OperationGuard operation(manager);
146 if (!check(space.isMapped(reinterpret_cast<void*>(locked)) && file.loans() == 1,
147 "compactor removed the locked mapping or its backing loan"))
148 return false;
149 physical_uintptr_t physical = 0;
150 size_t flags = 0;
151 space.getMapping(reinterpret_cast<void*>(locked), physical, flags);
152 auto* bytes = reinterpret_cast<volatile uint8_t*>(locked);
153 if (!check(physical == originalPhysical && (flags & VirtualAddressSpace::Borrowed) &&
154 !(flags & VirtualAddressSpace::Write) && bytes[0] == 0x49 &&
155 bytes[pageSize - 1] == 0x49 && !file.evict(0) && file.evict(pageSize),
156 "locked identity/data or cache eviction inhibition changed"))
157 return false;
158 if (!check(manager.lockMemory(space, locked, pageSize, MemoryLockMode::None, false) ==
159 MemoryLockStatus::Success &&
160 !manager.hasLockedMemory(space, locked, pageSize) &&
161 !account.charge().managedPages,
162 "unlock retained policy or virtual-page charge"))
163 return false;
164 }
165 if (!check(compactUntilAbsent(manager, space, locked),
166 "unlock did not permit actual compaction"))
167 return false;
168 return check(!file.loans() && file.evict(0) && file.backingUnchanged(),
169 "unlock did not release the cache loan or changed backing bytes");
170 }();
171 }
172 passed = cleanupMappings(manager, account, file) && passed;
173 if (passed)
174 NOTICE("MEMORY-LOCK-TEST: PASS compactor-"
175 << (mode == MemoryLockMode::Eager ? "eager" : "onfault")
176 << " locked-physical=retained control=evicted unlock=evicted");
177 return passed;
178}
179
180bool privateEagerCopy(MemoryMapManager& manager, VirtualAddressSpace& space, TestAccount& account) {
181 LockProbeFile file;
182 const size_t pageSize = PhysicalMemoryManager::getPageSize();
183 uintptr_t address = 0;
184 physical_uintptr_t ownedPhysical = 0;
185 bool passed = check(file.initialise(), "private backing allocation");
186 if (passed) {
187 passed = [&]() {
188 MemoryMapManager::OperationGuard operation(manager);
189 if (!check(manager.mapFile(&file, address, pageSize,
190 MemoryMappedObject::Read | MemoryMappedObject::Write, 0, true) &&
191 manager.faultIn(address, false),
192 "private borrowed mapping setup"))
193 return false;
194 physical_uintptr_t borrowedPhysical = 0;
195 size_t flags = 0;
196 space.getMapping(reinterpret_cast<void*>(address), borrowedPhysical, flags);
197 if (!check((flags & VirtualAddressSpace::Borrowed) && file.loans() == 1,
198 "private read did not borrow its file page"))
199 return false;
200#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
201 const size_t borrowedReferences =
202 PhysicalMemoryManager::pageReferenceCountForTest(borrowedPhysical);
203#endif
204 if (!check(manager.lockMemory(space, address, pageSize, MemoryLockMode::OnFault, false) ==
205 MemoryLockStatus::Success,
206 "private ONFAULT lock"))
207 return false;
208 physical_uintptr_t currentPhysical = 0;
209 space.getMapping(reinterpret_cast<void*>(address), currentPhysical, flags);
210 if (!check(currentPhysical == borrowedPhysical && (flags & VirtualAddressSpace::Borrowed) &&
211 !(flags & VirtualAddressSpace::Write) && file.loans() == 1,
212 "ONFAULT eagerly copied an already-resident private page"))
213 return false;
214 if (!check(manager.lockMemory(space, address, pageSize, MemoryLockMode::Eager, false) ==
215 MemoryLockStatus::Success,
216 "private eager lock"))
217 return false;
218 space.getMapping(reinterpret_cast<void*>(address), ownedPhysical, flags);
219 if (!check(
220 ownedPhysical != borrowedPhysical && (flags & VirtualAddressSpace::Write) &&
222 !file.loans() && account.charge().managedPages == 1,
223 "eager lock did not acquire exactly one writable private page"))
224 return false;
225#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
226 if (!check(PhysicalMemoryManager::pageReferenceCountForTest(ownedPhysical) == 1 &&
227 PhysicalMemoryManager::pageReferenceCountForTest(borrowedPhysical) ==
228 borrowedReferences,
229 "eager copy changed backing ownership or leaked a private reference"))
230 return false;
231#endif
232 auto* bytes = reinterpret_cast<volatile uint8_t*>(address);
233 for (size_t n = 0; n < pageSize; ++n) {
234 if (!check(bytes[n] == 0x49, "eager private copy lost source bytes"))
235 return false;
236 }
237 bytes[0] = 0x72;
238 return check(file.backingUnchanged() && file.evict(0) && bytes[0] == 0x72 &&
239 bytes[pageSize - 1] == 0x49,
240 "private write or backing eviction changed the other owner");
241 }();
242 }
243 passed = cleanupMappings(manager, account, file) && passed;
244#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS
245 if (ownedPhysical)
246 passed &= check(!PhysicalMemoryManager::pageReferenceCountForTest(ownedPhysical),
247 "private eager page survived mapping teardown");
248#endif
249 if (passed)
250 NOTICE(
251 "MEMORY-LOCK-TEST: PASS private-eager-copy onfault=borrowed eager=owned backing=unchanged");
252 return passed;
253}
254
255bool rawState(MemoryMapManager& manager, VirtualAddressSpace& space, TestAccount& account,
256 size_t expectedPages, const char* detail) {
257 MemoryMapManager::OperationGuard operation(manager);
259 const auto status = space.rawUserMemory().prepareAllLocks(MemoryLockMode::None, plan);
260 return check(status == MemoryLockStatus::Success && plan &&
261 plan.get()->eligiblePages() == expectedPages &&
262 account.charge().rawPages == expectedPages && !account.charge().managedPages,
263 detail);
264}
265
266void quota(MemoryMapManager& manager, TestAccount& account, size_t pages) {
267 MemoryMapManager::OperationGuard operation(manager);
268 account.maximumPages = pages;
269}
270
271bool rawOwnerFlows(MemoryMapManager& manager, VirtualAddressSpace& space, TestAccount& account) {
272 const size_t pageSize = PhysicalMemoryManager::getPageSize();
273 const uintptr_t original = reinterpret_cast<uintptr_t>(space.getEndOfHeap());
274 auto* thread = Processor::information().getCurrentThread();
275 bool passed = [&]() {
276 if (!check(original && !(original & (pageSize - 1)), "isolated heap alignment") ||
277 !rawState(manager, space, account, 0, "isolated raw inventory is not empty"))
278 return false;
279 {
280 MemoryMapManager::OperationGuard operation(manager);
281 // This isolated kernel worker has no other user mappings or raw owners.
282 space.rawUserMemory().setCompleteInventory(true);
283 account.maximumPages = 0;
284 if (!check(manager.lockAllMemory(space, false, MemoryLockMode::None, MemoryLockMode::Eager,
285 false) == MemoryLockStatus::Success,
286 "raw FUTURE setup"))
287 return false;
288 }
289 if (!check(!space.expandHeap(1, VirtualAddressSpace::Write) &&
290 reinterpret_cast<uintptr_t>(space.getEndOfHeap()) == original,
291 "zero-quota heap admission changed the break") ||
292 !rawState(manager, space, account, 0, "failed heap admission published a raw owner"))
293 return false;
294 quota(manager, account, 2);
295 if (!check(reinterpret_cast<uintptr_t>(
296 space.expandHeap(pageSize + 1, VirtualAddressSpace::Write)) == original,
297 "two-page heap admission") ||
298 !rawState(manager, space, account, 2, "heap admission did not charge two pages"))
299 return false;
300 for (size_t offset = 0; offset < 2 * pageSize; offset += pageSize) {
301 void* page = reinterpret_cast<void*>(original + offset);
302 if (!check(space.isMapped(page), "eager heap page was not populated"))
303 return false;
304 physical_uintptr_t physical = 0;
305 size_t flags = 0;
306 space.getMapping(page, physical, flags);
307 if (!check(
309 "eager heap admission retained a write-faulting CoW page"))
310 return false;
311 auto* bytes = reinterpret_cast<volatile uint8_t*>(page);
312 if (!check(bytes[0] == 0 && bytes[pageSize - 1] == 0, "eager heap page was not zeroed"))
313 return false;
314 }
315 if (!check(reinterpret_cast<uintptr_t>(space.expandHeap(
316 pageSize - 1, VirtualAddressSpace::Write)) == original + pageSize + 1,
317 "same-page heap growth") ||
318 !rawState(manager, space, account, 2, "same-page growth charged an extra page"))
319 return false;
320 if (!check(reinterpret_cast<uintptr_t>(space.expandHeap(-static_cast<ssize_t>(2 * pageSize),
322 original + 2 * pageSize &&
323 reinterpret_cast<uintptr_t>(space.getEndOfHeap()) == original &&
324 !space.isMapped(reinterpret_cast<void*>(original)) &&
325 !space.isMapped(reinterpret_cast<void*>(original + pageSize)),
326 "heap shrink did not retire its pages and break") ||
327 !rawState(manager, space, account, 0, "heap shrink retained raw charge or inventory"))
328 return false;
329
330 quota(manager, account, 0);
331 if (!check(!thread->prepareInputUserStack(), "zero-quota fallback stack was admitted") ||
332 !rawState(manager, space, account, 0, "failed fallback stack published an owner"))
333 return false;
334 const size_t fallbackPages = (1024 * 1024) / pageSize;
335 quota(manager, account, fallbackPages);
336 if (!check(thread->prepareInputUserStack(), "fallback stack admission") ||
337 !rawState(manager, space, account, fallbackPages, "fallback stack charge or inventory"))
338 return false;
339 quota(manager, account, 0);
340 if (!check(thread->prepareInputUserStack(), "existing fallback stack was allocated again"))
341 return false;
342 thread->retireInputUserStack();
343 thread->retireInputUserStack();
344 if (!rawState(manager, space, account, 0, "fallback retirement retained charge or inventory"))
345 return false;
346 quota(manager, account, fallbackPages);
347 if (!check(thread->prepareInputUserStack(), "fallback stack retry") ||
348 !rawState(manager, space, account, fallbackPages, "fallback retry charge or inventory"))
349 return false;
350 thread->retireInputUserStack();
351 return rawState(manager, space, account, 0, "fallback retry teardown");
352 }();
353
354 thread->retireInputUserStack();
355 {
356 MemoryMapManager::OperationGuard operation(manager);
357 account.maximumPages = ~size_t(0);
358 account.publish(account.charge(), MemoryLockMode::None);
359 const uintptr_t current = reinterpret_cast<uintptr_t>(space.getEndOfHeap());
360 if (current > original && current - original <= 2 * pageSize)
361 space.expandHeap(-static_cast<ssize_t>(current - original), VirtualAddressSpace::Write);
362 passed &= check(reinterpret_cast<uintptr_t>(space.getEndOfHeap()) == original,
363 "raw fixture cleanup could not restore the heap");
364 }
365 passed = rawState(manager, space, account, 0, "raw fixture final cleanup") && passed;
366 if (passed)
367 NOTICE(
368 "MEMORY-LOCK-TEST: PASS raw-owners heap=quota-byte-growth-shrink "
369 "fallback=quota-retire-retry");
370 return passed;
371}
372
373int memoryLockWorker(void* parameter) {
374 bool& passed = *static_cast<bool*>(parameter);
376 VirtualAddressSpace& space = Processor::information().getVirtualAddressSpace();
377 TestAccount account;
378 {
379 MemoryMapManager::OperationGuard operation(manager);
380 if (!check(!space.memoryLockAccount(), "test address space already has a lock account"))
381 return 0;
382 space.setMemoryLockAccount(&account);
383 manager.bindMemoryLockPolicy(space);
384 }
385 passed = compactorCase(MemoryLockMode::Eager, manager, space, account) &&
386 compactorCase(MemoryLockMode::OnFault, manager, space, account) &&
387 privateEagerCopy(manager, space, account) && rawOwnerFlows(manager, space, account);
388 {
389 MemoryMapManager::OperationGuard operation(manager);
390 manager.unmapAll();
391 space.rawUserMemory().clear();
392 const auto charge = account.charge();
393 passed &= check(!charge.managedPages && !charge.rawPages,
394 "test account retained charge at final teardown");
395 space.setMemoryLockAccount(nullptr);
396 space.setUserMemoryPolicy(nullptr);
397 }
398 return 0;
399}
400} // namespace
401
402bool runMemoryLockRegressions() {
403 NOTICE("MEMORY-LOCK-TEST: BEGIN");
404 Process* process = new Process(Scheduler::instance().getKernelProcess(), true);
405 if (!check(process != nullptr, "test process allocation"))
406 return false;
407 bool passed = false;
408 Thread* worker = new Thread(process, memoryLockWorker, &passed, nullptr, false, true, true);
409 const bool started = worker && worker->start();
410 const bool joined = started && worker->joinForCompletion();
411 if (started && !joined)
412 FATAL("MEMORY-LOCK-TEST: worker could not be joined safely");
413 if (!started)
414 delete worker;
415 delete process;
416 passed = check(started && joined && passed, "isolated worker") && passed;
417 if (passed)
418 NOTICE("MEMORY-LOCK-TEST: END PASS");
419 return passed;
420}
Memory-mapped file interface.
Definition File.h:74
virtual uintptr_t readBlock(uint64_t location)
Definition File.cc:1247
virtual void unpinBlock(uint64_t location)
Definition File.cc:1298
void evict(uint64_t location)
Definition File.cc:1300
void enableFillCacheWriteback()
Definition File.cc:1627
virtual size_t getBlockSize() const
Definition File.cc:949
virtual MUST_USE_RESULT bool pinBlock(uint64_t location)
Definition File.cc:1279
static MemoryMapManager & instance()
MemoryMappedObject * mapFile(File *pFile, uintptr_t &address, size_t length, MemoryMappedObject::Permissions perms, size_t offset=0, bool bCopyOnWrite=true)
virtual bool compact()
Special memory entity in the kernel's virtual address space.
static PhysicalMemoryManager & instance()
static ProcessorInformation & information()
static Scheduler & instance()
Definition Scheduler.h:96
void yield()
Definition Scheduler.cc:226
virtual bool isMapped(void *virtualAddress)=0
virtual bool getMapping(void *virtualAddress, physical_uintptr_t &physicalAddress, size_t &flags)=0
virtual void * getEndOfHeap()=0
virtual void * expandHeap(ssize_t incr, size_t flags)