The Pedigree Project 0.1
process-memory-regressions.cc
1/* Copyright (c) 2026, Pedigree Developers. */
2#include "pedigree/kernel/Log.h"
3#include "pedigree/kernel/errors.h"
4#include "pedigree/kernel/process/Process.h"
5#include "pedigree/kernel/process/Scheduler.h"
6#include "pedigree/kernel/process/Semaphore.h"
7#include "pedigree/kernel/process/Thread.h"
8#include "pedigree/kernel/processor/MemoryRegion.h"
9#include "pedigree/kernel/processor/PhysicalMemoryManager.h"
10#include "pedigree/kernel/processor/Processor.h"
11#include "pedigree/kernel/processor/VirtualAddressSpace.h"
12#include "pedigree/kernel/utilities/utility.h"
13
14#include "modules/system/vfs/File.h"
16
17namespace {
18using Status = MemoryMapManager::UserPageCopyStatus;
19using Resident = VirtualAddressSpace::ResidentCopyStatus;
20constexpr auto ReadWrite = MemoryMappedObject::Read | MemoryMappedObject::Write;
21bool check(bool condition, const char* detail) {
22 if (!condition)
23 ERROR("PROCESS-MEMORY-BACKEND: FAIL " << detail);
24 return condition;
25}
26bool filled(const uint8_t* bytes, size_t count, uint8_t value) {
27 for (size_t i = 0; i < count; ++i)
28 if (bytes[i] != value)
29 return false;
30 return true;
31}
32
33class ProbeFile final : public File {
34 public:
35 using File::sync;
36 ProbeFile()
37 : File(String("process-memory-probe"), 0, 0, 0, 1, nullptr,
38 2 * PhysicalMemoryManager::getPageSize() + 137, nullptr),
39 storage("Process Memory Probe") {
41 }
42 ~ProbeFile() override {
43 shutdownFillCacheWriteback();
44 }
45 bool initialise() {
46 if (!PhysicalMemoryManager::instance().allocateRegion(
48 return false;
49 ByteSet(storage.virtualAddress(), 0x49, 3 * PhysicalMemoryManager::getPageSize());
50 return true;
51 }
52 size_t getBlockSize() const override {
54 }
55 bool isDirectPhysicalMapping() const override {
56 return direct;
57 }
58 bool prepareSharedMapping(size_t, size_t) override {
59 if (noMemory) {
60 Processor::information().getCurrentThread()->setErrno(Error::OutOfMemory);
61 return false;
62 }
63 return true;
64 }
65 bool sync(size_t offset, bool async) override {
66 bool present = false;
67 return syncFillCache(offset, async, present);
68 }
69 size_t loans() {
70 return __atomic_load_n(&physicalPageLoans(), __ATOMIC_ACQUIRE);
71 }
72 bool backingEquals(size_t offset, uint8_t value) const {
73 return static_cast<const uint8_t*>(storage.virtualAddress())[offset] == value;
74 }
75 bool failRead = false, noMemory = false, direct = false;
76
77 protected:
78 uintptr_t readBlock(uint64_t offset) override {
79 if (failRead || offset >= getSize()) {
80 Processor::information().getCurrentThread()->setErrno(Error::IoError);
81 return FILE_BAD_BLOCK;
82 }
83 return reinterpret_cast<uintptr_t>(storage.virtualAddress()) + offset;
84 }
85 bool pinBlock(uint64_t offset) override {
86 return !failRead && offset < getSize();
87 }
88 void unpinBlock(uint64_t) override {}
89 void writeBlocks(uint64_t offset, uintptr_t source, size_t length) override {
90 if (offset >= getSize())
91 return;
92 const size_t remaining = getSize() - offset;
93 MemoryCopy(static_cast<uint8_t*>(storage.virtualAddress()) + offset,
94 reinterpret_cast<const void*>(source), length < remaining ? length : remaining);
95 }
96
97 private:
98 MemoryRegion storage;
99};
100
101struct Context {
102 Semaphore ready{0}, release{0};
103 VirtualAddressSpace* space = nullptr;
104 ProbeFile* file = nullptr;
105 ProbeFile* failure = nullptr;
106 uintptr_t anonymous = 0, privateFile = 0, sharedFile = 0, readonly = 0, execute = 0;
107 uintptr_t inaccessible = 0, failedFile = 0, raw = 0;
108 bool setup = false, cleaned = false;
109};
110
111int ownerWorker(void* parameter) {
112 auto& context = *static_cast<Context*>(parameter);
113 auto& manager = MemoryMapManager::instance();
114 auto& space = Processor::information().getVirtualAddressSpace();
115 const size_t page = PhysicalMemoryManager::getPageSize();
116 ProbeFile file, failure;
117 VirtualAddressSpace::Stack* raw = nullptr;
118 context.space = &space;
119 context.file = &file;
120 context.failure = &failure;
121 {
122 MemoryMapManager::OperationGuard operation(manager);
123 manager.bindMemoryLockPolicy(space);
124 context.setup = file.initialise() && failure.initialise() &&
125 manager.mapAnon(context.anonymous, page, ReadWrite) &&
126 manager.mapAnon(context.readonly, page, MemoryMappedObject::Read) &&
127 manager.mapAnon(context.execute, page, MemoryMappedObject::Exec) &&
128 manager.mapAnon(context.inaccessible, page, MemoryMappedObject::None) &&
129 manager.mapFile(&file, context.privateFile, page, ReadWrite, 0, true) &&
130 manager.mapFile(&file, context.sharedFile, 4 * page, ReadWrite, 0, false) &&
131 manager.mapFile(&failure, context.failedFile, page, ReadWrite, 0, false);
132 if (context.setup) {
133 raw = space.allocateStack(3 * page);
134 context.setup = raw && raw->regionId();
135 if (raw)
136 context.raw = reinterpret_cast<uintptr_t>(raw->getBase());
137 }
138 }
139 context.ready.release();
140 if (!context.release.acquire(1, 30))
141 FATAL("PROCESS-MEMORY-BACKEND: owner release timed out");
142 {
143 MemoryMapManager::OperationGuard operation(manager);
144 manager.unmapAll();
145 if (raw)
146 space.freeStack(raw);
147 context.cleaned = check(!file.loans() && !failure.loans(), "final file loan retirement");
148 space.rawUserMemory().clear();
149 space.setUserMemoryPolicy(nullptr);
150 }
151 return 0;
152}
153
154bool managedCopies(Context& context) {
155 auto& manager = MemoryMapManager::instance();
156 auto& space = *context.space;
157 const size_t page = PhysicalMemoryManager::getPageSize();
158 uint8_t bytes[64];
159 MemoryMapManager::OperationGuard operation(manager);
160 auto copy = [&](uintptr_t address, bool write) {
161 return manager.copyUserPage(space, address, bytes, sizeof(bytes), write);
162 };
163 if (!check(!space.isMapped(reinterpret_cast<void*>(context.anonymous)) &&
164 !space.isMapped(reinterpret_cast<void*>(context.privateFile)),
165 "initial lazy mappings"))
166 return false;
167 ByteSet(bytes, 0x71, sizeof(bytes));
168 if (!check(copy(context.anonymous + 19, false) == Status::Success && filled(bytes, 64, 0),
169 "remote anonymous zero read"))
170 return false;
171 ByteSet(bytes, 0x71, sizeof(bytes));
172 if (!check(copy(context.anonymous + 19, true) == Status::Success,
173 "remote anonymous write preparation"))
174 return false;
175 physical_uintptr_t physical = 0;
176 size_t flags = 0;
177 space.getMapping(reinterpret_cast<void*>(context.anonymous), physical, flags);
180 !(flags & VirtualAddressSpace::Shared) &&
181 copy(context.anonymous + 19, false) == Status::Success && filled(bytes, 64, 0x71),
182 "anonymous bytes and latest PTE dirty state"))
183 return false;
184
185 if (!check(copy(context.privateFile + 19, false) == Status::Success && filled(bytes, 64, 0x49),
186 "lazy private file read"))
187 return false;
188 physical_uintptr_t borrowed = 0;
189 space.getMapping(reinterpret_cast<void*>(context.privateFile), borrowed, flags);
190 if (!check((flags & VirtualAddressSpace::Borrowed) && !(flags & VirtualAddressSpace::Write) &&
191 context.file->loans() == 1,
192 "read prematurely resolved private ownership"))
193 return false;
194 ByteSet(bytes, 0x72, sizeof(bytes));
195 if (!check(copy(context.privateFile + 19, true) == Status::Success,
196 "private file write preparation"))
197 return false;
198 space.getMapping(reinterpret_cast<void*>(context.privateFile), physical, flags);
199 if (!check(physical != borrowed && !(flags & VirtualAddressSpace::Borrowed) &&
200 (flags & VirtualAddressSpace::Dirty) && !context.file->loans() &&
201 context.file->backingEquals(19, 0x49),
202 "private file write changed backing or retained its loan"))
203 return false;
204
205 ByteSet(bytes, 0x73, sizeof(bytes));
206 if (!check(copy(context.sharedFile + 19, true) == Status::Success && context.file->loans() == 1 &&
207 context.file->sync(0, false) && context.file->backingEquals(19, 0x73),
208 "shared file writeback did not use its original loan"))
209 return false;
210 ByteSet(bytes, 0x7a, sizeof(bytes));
211 if (!check(copy(context.sharedFile + 2 * page + 200, false) == Status::Success &&
212 filled(bytes, 64, 0),
213 "partial EOF page zero tail"))
214 return false;
215 ByteSet(bytes, 0x7a, sizeof(bytes));
216 return check(
217 copy(context.sharedFile + 3 * page, false) == Status::Inaccessible &&
218 copy(context.readonly, true) == Status::Inaccessible &&
219 copy(context.execute, false) == Status::Inaccessible &&
220 copy(context.inaccessible, false) == Status::Inaccessible &&
221 copy(context.anonymous + page - 32, false) == Status::Inaccessible &&
222 manager.copyUserPage(space, context.anonymous, bytes, 0, false) == Status::Inaccessible &&
223 filled(bytes, 64, 0x7a) && !space.isMapped(reinterpret_cast<void*>(context.readonly)) &&
224 !space.isMapped(reinterpret_cast<void*>(context.execute)),
225 "denied fragments changed bytes or populated a denied page");
226}
227
228bool failedPreparation(Context& context) {
229 auto& manager = MemoryMapManager::instance();
230 auto& space = *context.space;
231 auto& file = *context.failure;
232 uint8_t bytes[16];
233 ByteSet(bytes, 0x7b, sizeof(bytes));
234 MemoryMapManager::OperationGuard operation(manager);
235 auto* thread = Processor::information().getCurrentThread();
236 const size_t previous = thread->getErrno();
237 thread->setErrno(Error::BadFileDescriptor);
238 file.noMemory = true;
239 bool passed =
240 check(manager.copyUserPage(space, context.failedFile, bytes, 16, true) == Status::NoMemory &&
241 thread->getErrno() == Error::BadFileDescriptor &&
242 !space.isMapped(reinterpret_cast<void*>(context.failedFile)) && !file.loans() &&
243 filled(bytes, 16, 0x7b),
244 "backing allocation rejection changed page, bytes or caller errno");
245 file.noMemory = false;
246 file.failRead = true;
247 passed &=
248 check(manager.copyUserPage(space, context.failedFile, bytes, 16, false) == Status::IoError &&
249 thread->getErrno() == Error::BadFileDescriptor &&
250 !space.isMapped(reinterpret_cast<void*>(context.failedFile)) && !file.loans() &&
251 filled(bytes, 16, 0x7b),
252 "backing I/O failure changed page, bytes or caller errno");
253 file.failRead = false;
254 file.direct = true;
255 passed &= check(
256 manager.copyUserPage(space, context.failedFile, bytes, 16, false) == Status::Unsupported &&
257 !file.loans() && filled(bytes, 16, 0x7b),
258 "direct physical backing admitted");
259 file.direct = false;
260 passed &=
261 check(manager.copyUserPage(space, context.failedFile, bytes, 16, false) == Status::Success &&
262 filled(bytes, 16, 0x49),
263 "backing did not recover after preparation rejection");
264 thread->setErrno(previous);
265 return passed;
266}
267
268bool rawCopies(Context& context) {
269 auto& manager = MemoryMapManager::instance();
270 auto& space = *context.space;
271 auto& memory = PhysicalMemoryManager::instance();
272 const size_t page = memory.getPageSize();
273 const uintptr_t base = context.raw;
274 uint8_t bytes[16];
275 ByteSet(bytes, 0x51, sizeof(bytes));
276 MemoryMapManager::OperationGuard operation(manager);
277 auto& raw = space.rawUserMemory();
278 if (!check(raw.covers(base + 1, 3 * page - 2) && !raw.covers(base - 1, 2) &&
279 !raw.covers(base, 0) && !raw.covers(~uintptr_t(0) - 1, 4) &&
280 manager.copyUserPage(space, base, bytes, 16, true) == Status::Success,
281 "raw coverage and initial bytes"))
282 return false;
284 if (!check(raw.prepareReplacement(base + 2 * page, page, hole) == MemoryLockStatus::Success,
285 "raw hole preparation"))
286 return false;
287 hole.get()->commit();
288 if (!check(raw.covers(base, 2 * page) && !raw.covers(base, 3 * page) &&
289 !raw.covers(base + 2 * page, 1) &&
290 manager.copyUserPage(space, base + 2 * page, bytes, 16, false) ==
291 Status::Inaccessible,
292 "raw replacement hole was treated as continuous heap"))
293 return false;
294
295 physical_uintptr_t original = 0, retired = 0;
296 size_t flags = 0, retiredFlags = 0;
297 space.getMapping(reinterpret_cast<void*>(base), original, flags);
298 if (!check(space.detachMapping(reinterpret_cast<void*>(base + page), retired, retiredFlags),
299 "CoW alias destination retirement"))
300 return false;
301 memory.freePage(retired);
302 // Fresh allocations have no tracked references: enroll the original leaf
303 // before adding the alias so resolving either CoW leaf retains the other.
304 memory.pin(original);
305 memory.pin(original);
306 if (!space.map(original, reinterpret_cast<void*>(base + page),
308 memory.freePage(original);
309 return check(false, "CoW alias publication");
310 }
311 space.setFlags(reinterpret_cast<void*>(base),
313 if (!check(space.copyResidentUserPage(base, bytes, 16, true) == Resident::Inaccessible,
314 "resident primitive wrote an unresolved CoW page"))
315 return false;
316 ByteSet(bytes, 0x52, sizeof(bytes));
317 if (!check(manager.copyUserPage(space, base, bytes, 16, true) == Status::Success,
318 "raw CoW preparation"))
319 return false;
320 physical_uintptr_t replacement = 0;
321 space.getMapping(reinterpret_cast<void*>(base), replacement, flags);
322 if (!check(replacement != original && (flags & VirtualAddressSpace::Dirty) &&
324 manager.copyUserPage(space, base + page, bytes, 16, false) == Status::Success &&
325 filled(bytes, 16, 0x51),
326 "CoW replacement changed the retained alias"))
327 return false;
328 space.setFlags(reinterpret_cast<void*>(base), flags | VirtualAddressSpace::WriteProtected);
329 ByteSet(bytes, 0x53, sizeof(bytes));
330 if (!check(space.copyResidentUserPage(base, bytes, 16, true) == Resident::Inaccessible &&
331 manager.copyUserPage(space, base, bytes, 16, true) == Status::Inaccessible &&
332 manager.copyUserPage(space, base, bytes, 16, false) == Status::Success &&
333 filled(bytes, 16, 0x52),
334 "write protection changed resident bytes"))
335 return false;
336
337 const auto runtime = memory.allocatePage();
338 if (!check(runtime != 0, "runtime page allocation"))
339 return false;
340 void* runtimeAddress = reinterpret_cast<void*>(base + 2 * page);
341 if (!space.map(runtime, runtimeAddress,
343 memory.freePage(runtime);
344 return check(false, "runtime page publication");
345 }
346 bool passed =
347 check(manager.copyUserPage(space, base + 2 * page, bytes, 16, true) == Status::Success,
348 "classified runtime user RAM rejected");
349 space.setFlags(runtimeAddress, VirtualAddressSpace::Write);
350 passed &= check(
351 manager.copyUserPage(space, base + 2 * page, bytes, 16, false) == Status::Inaccessible &&
352 space.copyResidentUserPage(base + 2 * page, bytes, 16, false) == Resident::Success,
353 "unclassified resident user RAM was admitted by the manager");
354 if (!space.detachMapping(runtimeAddress, retired, retiredFlags))
355 FATAL("PROCESS-MEMORY-BACKEND: runtime page retirement failed");
356 memory.freePage(retired);
357 return passed;
358}
359} // namespace
360
361EXPORTED_PUBLIC bool processMemoryBackendRegression() {
362 NOTICE("PROCESS-MEMORY-BACKEND: BEGIN");
363 auto* callerSpace = &Processor::information().getVirtualAddressSpace();
364 Process* process = new Process(Scheduler::instance().getKernelProcess(), true);
365 if (!check(process != nullptr, "owner process allocation"))
366 return false;
367 Context context;
368 Thread* worker = new Thread(process, ownerWorker, &context, nullptr, false, true, true);
369 const bool started = worker && worker->start();
370 const bool ready = started && context.ready.acquire(1, 10);
371 bool passed = check(ready && context.setup && context.space != callerSpace, "owner setup");
372 if (passed)
373 passed = managedCopies(context) && failedPreparation(context) && rawCopies(context);
374 passed &= check(&Processor::information().getVirtualAddressSpace() == callerSpace,
375 "remote operation switched the caller's address space");
376 context.release.release();
377 if (worker && !started)
378 worker->setUnwindState(Thread::TerminateThread);
379 const bool joined = worker && worker->joinForCompletion();
380 if (worker && !joined)
381 FATAL("PROCESS-MEMORY-BACKEND: owner could not be joined safely");
382 delete process;
383 passed = check(joined && context.cleaned, "owner teardown") && passed;
384 if (passed)
385 NOTICE("PROCESS-MEMORY-BACKEND: END PASS");
386 return passed;
387}
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
virtual void writeBlocks(uint64_t location, uintptr_t addr, size_t length)
Definition File.cc:1259
virtual bool sync()
Definition File.cc:519
bool syncFillCache(size_t offset, bool async, bool &present)
Definition File.cc:1635
void enableFillCacheWriteback()
Definition File.cc:1627
virtual bool prepareSharedMapping(size_t offset, size_t length)
Definition File.cc:1015
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()
Special memory entity in the kernel's virtual address space.
static PhysicalMemoryManager & instance()
static ProcessorInformation & information()
static Scheduler & instance()
Definition Scheduler.h:96
@ TerminateThread
Exit only this thread during Process exit.
Definition Thread.h:515