The Pedigree Project 0.1
remap-file-pages-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 {
23using Status = MemoryMapManager::FileRemapStatus;
24
25bool check(bool condition, const char* detail) {
26 if (!condition)
27 ERROR("REMAP-FILE-PAGES-TEST: FAIL " << detail);
28 return condition;
29}
30
31class TestAccount final : public MemoryLockAccount {
32 public:
33 bool permitsTotalPages(size_t, bool) const override {
34 return true;
35 }
36};
37
38class RemapProbeFile final : public File {
39 public:
40 static constexpr size_t Pages = 8;
41 RemapProbeFile()
42 : File(String("remap-file-pages-probe"), 0, 0, 0, 1, nullptr,
43 Pages * PhysicalMemoryManager::getPageSize(), nullptr),
44 m_Storage("Remap File Pages Probe") {
46 }
47 ~RemapProbeFile() override {
48 shutdownFillCacheWriteback();
49 }
50 bool initialise() {
51 if (!PhysicalMemoryManager::instance().allocateRegion(
53 return false;
54 const size_t page = PhysicalMemoryManager::getPageSize();
55 for (size_t n = 0; n < Pages; ++n)
56 ByteSet(static_cast<uint8_t*>(m_Storage.virtualAddress()) + n * page, pattern(n), page);
57 return true;
58 }
59 static uint8_t pattern(size_t page) {
60 return 0x31 + page;
61 }
62 size_t getBlockSize() const override {
64 }
65 bool allowMapping(bool, bool, bool&) override {
66 return !rejectMapping;
67 }
68 physical_uintptr_t getPhysicalPage(size_t offset) override {
69 const auto physical = File::getPhysicalPage(offset);
70 const size_t index = offset / PhysicalMemoryManager::getPageSize();
71 if (physical != ~physical_uintptr_t(0) && index < Pages)
72 __atomic_add_fetch(&m_Loans[index], 1, __ATOMIC_RELEASE);
73 return physical;
74 }
75 void returnPhysicalPage(size_t offset) override {
76 const size_t index = offset / PhysicalMemoryManager::getPageSize();
77 if (index < Pages)
78 __atomic_sub_fetch(&m_Loans[index], 1, __ATOMIC_RELEASE);
80 }
81 size_t loansAt(size_t page) const {
82 return __atomic_load_n(&m_Loans[page], __ATOMIC_ACQUIRE);
83 }
84 size_t loans() {
85 return __atomic_load_n(&physicalPageLoans(), __ATOMIC_ACQUIRE);
86 }
87 bool evict(size_t page) {
88 return cacheState().fill.evict(page * PhysicalMemoryManager::getPageSize());
89 }
90 bool balanced() {
91 if (loans())
92 return false;
93 for (size_t n = 0; n < Pages; ++n) {
94 if (loansAt(n))
95 return false;
96 }
97 return true;
98 }
99 bool rejectMapping = false;
100
101 protected:
102 uintptr_t readBlock(uint64_t offset) override {
103 if (offset >= getSize())
104 return FILE_BAD_BLOCK;
105 return reinterpret_cast<uintptr_t>(m_Storage.virtualAddress()) + offset;
106 }
107 bool pinBlock(uint64_t offset) override {
108 return offset < getSize();
109 }
110 void unpinBlock(uint64_t) override {}
111
112 private:
113 MemoryRegion m_Storage;
114 size_t m_Loans[Pages] = {};
115};
116
117bool mapProbe(MemoryMapManager& manager, RemapProbeFile& file, uintptr_t& address, size_t length,
118 size_t offset = 0, MemoryLockMode mode = MemoryLockMode::None) {
119 return manager.mapFile(&file, address, length, MemoryMappedObject::Read, offset, false,
120 MemoryMapManager::Placement::Hint, nullptr,
121 MemoryMappedObject::Read | MemoryMappedObject::Write,
123 FileMappingOrigin{0x524650, true}) != nullptr;
124}
125
126bool borrowed(VirtualAddressSpace& space, uintptr_t address, physical_uintptr_t& physical) {
127 if (!space.isMapped(reinterpret_cast<void*>(address)))
128 return false;
129 size_t flags = 0;
130 space.getMapping(reinterpret_cast<void*>(address), physical, flags);
131 return (flags & VirtualAddressSpace::Borrowed) &&
133}
134
135bool sameBorrowed(VirtualAddressSpace& space, uintptr_t address, physical_uintptr_t expected) {
136 physical_uintptr_t current = 0;
137 return borrowed(space, address, current) && current == expected;
138}
139
140bool hasPattern(uintptr_t address, size_t filePage) {
141 const auto* bytes = reinterpret_cast<volatile uint8_t*>(address);
142 const size_t page = PhysicalMemoryManager::getPageSize();
143 return bytes[0] == RemapProbeFile::pattern(filePage) &&
144 bytes[page - 1] == RemapProbeFile::pattern(filePage);
145}
146
147bool cleanup(MemoryMapManager& manager, TestAccount& account, RemapProbeFile& file) {
148 MemoryMapManager::OperationGuard operation(manager);
149 manager.unmapAll();
150 account.publish(account.charge(), MemoryLockMode::None);
151 return check(!account.charge().managedPages && !account.charge().rawPages && file.balanced(),
152 "teardown retained charge or an offset-specific loan");
153}
154
155bool slicedRetirement(MemoryMapManager& manager, VirtualAddressSpace& space, TestAccount& account) {
156 RemapProbeFile file;
157 uintptr_t base = 0;
158 const size_t page = PhysicalMemoryManager::getPageSize();
159 bool passed = check(file.initialise(), "slice backing allocation");
160 if (passed) {
161 passed = [&]() {
162 MemoryMapManager::OperationGuard operation(manager);
163 physical_uintptr_t original[3] = {};
164 if (!check(mapProbe(manager, file, base, 3 * page), "slice mapping setup"))
165 return false;
166 for (size_t n = 0; n < 3; ++n) {
167 if (!check(manager.faultIn(base + n * page, false) &&
168 borrowed(space, base + n * page, original[n]) && file.loansAt(n) == 1,
169 "slice initial PTE and loan"))
170 return false;
171 }
172 file.rejectMapping = true;
173 if (!check(manager.remapFilePages(base + page, page, 4 * page, false) == Status::PolicyDenied,
174 "backing policy rejection"))
175 return false;
176 file.rejectMapping = false;
177 for (size_t n = 0; n < 3; ++n) {
178 if (!check(sameBorrowed(space, base + n * page, original[n]) && file.loansAt(n) == 1 &&
179 hasPattern(base + n * page, n),
180 "rejected replacement changed a victim"))
181 return false;
182 }
183 if (!check(manager.remapFilePages(base + page, page, 4 * page, true) == Status::Success &&
184 !space.isMapped(reinterpret_cast<void*>(base + page)) && file.loans() == 2 &&
185 !file.loansAt(1) && !file.loansAt(4) && file.evict(1),
186 "NONBLOCK did not retire the old offset and leave a lazy replacement"))
187 return false;
188 if (!check(sameBorrowed(space, base, original[0]) &&
189 sameBorrowed(space, base + 2 * page, original[2]) && file.loansAt(0) == 1 &&
190 file.loansAt(2) == 1 && hasPattern(base, 0) && hasPattern(base + 2 * page, 2),
191 "slice replacement changed unaffected physical pages or loans"))
192 return false;
193 physical_uintptr_t replacement = 0;
194 if (!check(manager.faultIn(base + page, false) && borrowed(space, base + page, replacement) &&
195 file.loansAt(4) == 1 && hasPattern(base + page, 4),
196 "lazy replacement fault used the old offset"))
197 return false;
198 if (!check(manager.remapFilePages(base + page, page, 5 * page, false) == Status::Success &&
199 borrowed(space, base + page, replacement) && !file.loansAt(4) &&
200 file.loansAt(5) == 1 && file.loans() == 3 && hasPattern(base + page, 5) &&
201 sameBorrowed(space, base, original[0]) &&
202 sameBorrowed(space, base + 2 * page, original[2]),
203 "default remap did not populate its new offset or preserve slices"))
204 return false;
205 return true;
206 }();
207 }
208 passed = cleanup(manager, account, file) && passed;
209 if (passed)
210 NOTICE(
211 "REMAP-FILE-PAGES-TEST: PASS slices policy=preserved old-loans=retired "
212 "neighbors=retained nonblock=lazy default=populated");
213 return passed;
214}
215
216bool stagingLimit(MemoryMapManager& manager, VirtualAddressSpace& space, TestAccount& account) {
217 RemapProbeFile file;
218 uintptr_t base = 0;
219 const size_t page = PhysicalMemoryManager::getPageSize();
220 bool passed = check(file.initialise(), "staging-limit backing allocation");
221 if (passed) {
222 passed = [&]() {
223 MemoryMapManager::OperationGuard operation(manager);
224 // Only two pages are resident; the virtual extent exercises the real staging bound.
225 const size_t length = (VirtualAddressSpace::MaximumRemapPages + 1) * page;
226 physical_uintptr_t first = 0, second = 0;
227 if (!check(mapProbe(manager, file, base, length) && manager.faultIn(base, false) &&
228 manager.faultIn(base + page, false) && borrowed(space, base, first) &&
229 borrowed(space, base + page, second),
230 "staging-limit mapping setup"))
231 return false;
232 return check(manager.remapFilePages(base + page, page, 4 * page, true) == Status::NoMemory &&
233 sameBorrowed(space, base, first) &&
234 sameBorrowed(space, base + page, second) && file.loansAt(0) == 1 &&
235 file.loansAt(1) == 1 && file.loans() == 2 && !file.loansAt(4) &&
236 hasPattern(base, 0) && hasPattern(base + page, 1) &&
237 !account.charge().managedPages,
238 "failed staging changed PTEs, backing offsets, loans, or charge");
239 }();
240 }
241 passed = cleanup(manager, account, file) && passed;
242 if (passed)
243 NOTICE("REMAP-FILE-PAGES-TEST: PASS staging-limit resident-pages=2 victims=preserved");
244 return passed;
245}
246
247bool compactUntilAbsent(MemoryMapManager& manager, VirtualAddressSpace& space, uintptr_t address) {
248 for (size_t attempt = 0; attempt < 256; ++attempt) {
249 manager.compact();
250 if (!space.isMapped(reinterpret_cast<void*>(address)))
251 return true;
253 }
254 return false;
255}
256
257bool lockedReplacement(MemoryLockMode mode, MemoryMapManager& manager, VirtualAddressSpace& space,
258 TestAccount& account) {
259 RemapProbeFile file;
260 uintptr_t target = 0, control = 0;
261 const size_t page = PhysicalMemoryManager::getPageSize();
262 const bool onfault = mode == MemoryLockMode::OnFault;
263 physical_uintptr_t replacement = 0;
264 bool passed = check(file.initialise(), "locked backing allocation");
265 if (passed) {
266 passed = [&]() {
267 {
268 MemoryMapManager::OperationGuard operation(manager);
269 if (!check(mapProbe(manager, file, target, page, 0, mode) &&
270 mapProbe(manager, file, control, page, 2 * page) &&
271 manager.faultIn(target, false) && manager.faultIn(control, false),
272 "locked mapping setup"))
273 return false;
274 if (onfault)
275 account.publish(account.charge(), MemoryLockMode::OnFault);
276 if (!check(manager.remapFilePages(target, page, 4 * page, !onfault) == Status::Success &&
277 !file.loansAt(0) && file.loansAt(2) == 1 &&
278 account.charge().managedPages == 1 &&
279 manager.hasLockedMemory(space, target, page),
280 "locked replacement changed charge or retained old offset loan"))
281 return false;
282 if (onfault) {
283 if (!check(!space.isMapped(reinterpret_cast<void*>(target)) && !file.loansAt(4) &&
284 file.loans() == 1,
285 "FUTURE ONFAULT remap populated despite its lazy policy"))
286 return false;
287 if (!check(manager.faultIn(target, false), "ONFAULT replacement fault"))
288 return false;
289 }
290 if (!check(borrowed(space, target, replacement) && file.loansAt(4) == 1 &&
291 hasPattern(target, 4),
292 "locked replacement did not populate the requested file page"))
293 return false;
294 }
295 if (!check(compactUntilAbsent(manager, space, control),
296 "actual compactor did not evict the unlocked control"))
297 return false;
298 {
299 MemoryMapManager::OperationGuard operation(manager);
300 if (!check(sameBorrowed(space, target, replacement) && file.loansAt(4) == 1 &&
301 !file.loansAt(2) && file.loans() == 1 && !file.evict(4) &&
302 hasPattern(target, 4) && account.charge().managedPages == 1,
303 "actual compactor retired the locked replacement or its new-offset loan"))
304 return false;
305 if (!check(manager.lockMemory(space, target, page, MemoryLockMode::None, false) ==
306 MemoryLockStatus::Success &&
307 !account.charge().managedPages,
308 "replacement unlock retained charge"))
309 return false;
310 }
311 if (!check(compactUntilAbsent(manager, space, target),
312 "unlocked replacement retained its PTE"))
313 return false;
314 MemoryMapManager::OperationGuard operation(manager);
315 return check(!file.loansAt(4) && file.balanced() && file.evict(4),
316 "unlocked replacement retained its new-offset PTE or loan");
317 }();
318 }
319 passed = cleanup(manager, account, file) && passed;
320 if (passed)
321 NOTICE("REMAP-FILE-PAGES-TEST: PASS "
322 << (onfault ? "onfault" : "eager")
323 << " replacement=retained control=evicted unlock=evicted");
324 return passed;
325}
326
327int remapFilePagesWorker(void* parameter) {
328 bool& passed = *static_cast<bool*>(parameter);
330 VirtualAddressSpace& space = Processor::information().getVirtualAddressSpace();
331 TestAccount account;
332 {
333 MemoryMapManager::OperationGuard operation(manager);
334 if (!check(!space.memoryLockAccount(), "test address space already has a lock account"))
335 return 0;
336 space.setMemoryLockAccount(&account);
337 manager.bindMemoryLockPolicy(space);
338 }
339 passed = slicedRetirement(manager, space, account) && stagingLimit(manager, space, account) &&
340 lockedReplacement(MemoryLockMode::OnFault, manager, space, account) &&
341 lockedReplacement(MemoryLockMode::Eager, manager, space, account);
342 {
343 MemoryMapManager::OperationGuard operation(manager);
344 manager.unmapAll();
345 space.rawUserMemory().clear();
346 passed &= check(!account.charge().managedPages && !account.charge().rawPages,
347 "final account teardown");
348 space.setMemoryLockAccount(nullptr);
349 space.setUserMemoryPolicy(nullptr);
350 }
351 return 0;
352}
353} // namespace
354
355bool runRemapFilePagesRegressions() {
356 NOTICE("REMAP-FILE-PAGES-TEST: BEGIN");
357 Process* process = new Process(Scheduler::instance().getKernelProcess(), true);
358 if (!check(process != nullptr, "test process allocation"))
359 return false;
360 bool passed = false;
361 Thread* worker = new Thread(process, remapFilePagesWorker, &passed, nullptr, false, true, true);
362 const bool started = worker && worker->start();
363 const bool joined = started && worker->joinForCompletion();
364 if (started && !joined)
365 FATAL("REMAP-FILE-PAGES-TEST: worker could not be joined safely");
366 if (!started)
367 delete worker;
368 delete process;
369 passed = check(started && joined && passed, "isolated worker") && passed;
370 if (passed)
371 NOTICE("REMAP-FILE-PAGES-TEST: END PASS");
372 return passed;
373}
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
virtual physical_uintptr_t getPhysicalPage(size_t offset)
Definition File.cc:414
void enableFillCacheWriteback()
Definition File.cc:1627
virtual bool allowMapping(bool shared, bool writeRequested, bool &mayWrite)
Definition File.cc:1049
virtual size_t getBlockSize() const
Definition File.cc:949
virtual void returnPhysicalPage(size_t offset)
Definition File.cc:481
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