The Pedigree Project 0.1
swap-regressions.cc
1/* Copyright (c) 2026, Pedigree Developers. */
2#include "pedigree/kernel/Log.h"
3#include "pedigree/kernel/machine/Disk.h"
4#include "pedigree/kernel/process/Process.h"
5#include "pedigree/kernel/process/Scheduler.h"
6#include "pedigree/kernel/process/Thread.h"
7#include "pedigree/kernel/processor/PhysicalMemoryManager.h"
8#include "pedigree/kernel/processor/Processor.h"
9#include "pedigree/kernel/processor/ProcessorInformation.h"
10#include "pedigree/kernel/utilities/utility.h"
11
12#include "modules/system/vfs/File.h"
14#include "modules/system/vfs/VFS.h"
15
16#if HOSTED && PEDIGREE_HOSTED_SMOKE_TESTS && THREADS
17namespace {
18constexpr size_t PageSize = 4096, DataPages = 16;
19bool check(bool value, const char* detail) {
20 if (!value)
21 ERROR("SWAP-VM-TEST: FAIL " << detail);
22 return value;
23}
24class DeferredPagingProcess final : public Process {
25 public:
26 explicit DeferredPagingProcess(Process* parent) : Process(DeferredPublication(), parent) {}
27 ~DeferredPagingProcess() override {
29 }
30 void publishForTest() {
31 publish();
32 }
33 void closeForTest() {
35 }
36};
37class RecordingSwapDisk final : public Disk, public PagingTransport {
38 public:
39 RecordingSwapDisk() : bytes(new unsigned char[(DataPages + 1) * PageSize]) {
40 if (bytes) {
41 ByteSet(bytes, 0, getSize());
42 bytes[1024] = 1;
43 bytes[1028] = DataPages;
44 MemoryCopy(bytes + PageSize - 10, "SWAPSPACE2", 10);
45 reserveEndpoint();
46 publishEndpoint();
47 }
48 }
49 ~RecordingSwapDisk() override {
51 delete[] bytes;
52 }
53 size_t getSize() const override {
54 return (DataPages + 1) * PageSize;
55 }
56 bool pin(uint64_t) override {
57 return false;
58 }
59 void unpin(uint64_t) override {}
60 PagingStatus preparePagingTransport(PagingTransport*& out) override {
61 out = bytes ? this : nullptr;
62 return bytes ? PagingStatus::Success : PagingStatus::NoMemory;
63 }
64 PagingStatus transfer(PagingOperation operation, uint64_t offset, void* data) override {
65 if (operation == PagingOperation::Flush) {
66 ++flushes;
67 return failFlush ? PagingStatus::IoError : PagingStatus::Success;
68 }
69 if (offset > getSize() - PageSize || (offset & (PageSize - 1)))
70 return PagingStatus::Invalid;
71 if (operation == PagingOperation::Read) {
72 if (readsBeforeFailure == 0)
73 return PagingStatus::IoError;
74 if (readsBeforeFailure > 0)
75 --readsBeforeFailure;
76 ++reads;
77 MemoryCopy(data, bytes + offset, PageSize);
78 } else {
79 ++writes;
80 if (failWrite)
81 return PagingStatus::IoError;
82 MemoryCopy(bytes + offset, data, PageSize);
83 }
84 return PagingStatus::Success;
85 }
86 void release() override {
87 ++releases;
88 }
89 unsigned char* bytes;
90 bool failWrite = false, failFlush = false;
91 ssize_t readsBeforeFailure = -1;
92 size_t reads = 0, writes = 0, flushes = 0, releases = 0;
93};
94int swapWorker(void* opaque) {
95 bool& passed = *static_cast<bool*>(opaque);
96 auto& manager = MemoryMapManager::instance();
97 auto& space = Processor::information().getVirtualAddressSpace();
98 auto& memory = PhysicalMemoryManager::instance();
99 RecordingSwapDisk disk;
100 File emptyFile(String("swap-empty-backing"), 0, 0, 0, 1, nullptr, 0, nullptr);
101 uintptr_t base = 0;
102 const uint32_t endpoint = disk.endpointId();
103 passed = [&]() {
104 if (!check(disk.bytes && endpoint && SwapStore::instance().prepareAtBoot(), "fixture setup"))
105 return false;
106 disk.bytes[PageSize - 10] = 0;
107 if (!check(manager.activateSwap(endpoint) == SwapStatus::Invalid, "unformatted rejected"))
108 return false;
109 disk.bytes[PageSize - 10] = 'S';
110 disk.bytes[1032] = 1;
111 if (!check(manager.activateSwap(endpoint) == SwapStatus::Unsupported, "bad-page list rejected"))
112 return false;
113 disk.bytes[1032] = 0;
114 DiskUse loan;
115 if (!check(disk.acquireUse(loan) && manager.activateSwap(endpoint) == SwapStatus::Busy,
116 "ordinary ownership excludes activation"))
117 return false;
118 loan.reset();
119 if (!check(manager.activateSwap(endpoint) == SwapStatus::Success &&
120 manager.activateSwap(endpoint) == SwapStatus::Busy && !disk.acquireUse(loan),
121 "activation exclusivity"))
122 return false;
123 if (!check(VFS::unloadAdmission(false) == Module::UnloadAdmission::Busy &&
124 VFS::unloadAdmission(true) == Module::UnloadAdmission::KeepMapped,
125 "active paging retains VFS service"))
126 return false;
127 if (!check(manager.mapAnon(base, 3 * PageSize,
128 MemoryMappedObject::Read | MemoryMappedObject::Write) != nullptr,
129 "map"))
130 return false;
131 for (size_t n = 0; n < 3; ++n) {
132 if (!check(manager.faultIn(base + n * PageSize, true), "initial population"))
133 return false;
134 ByteSet(reinterpret_cast<void*>(base + n * PageSize), 0x40 + n, PageSize);
135 }
136 AnonymousMemoryMap unenrolled(base, PageSize,
137 MemoryMappedObject::Read | MemoryMappedObject::Write);
138 if (!check(!unenrolled.supportsPageOut(space, base), "unenrolled owner stays resident"))
139 return false;
140 const auto before = memory.memorySnapshot();
141 disk.failWrite = true;
142 if (!check(manager.pageOutRange(base, PageSize) == SwapStatus::IoError &&
143 space.isMapped(reinterpret_cast<void*>(base)) &&
144 *reinterpret_cast<unsigned char*>(base) == 0x40 &&
145 !manager.swapSnapshot().usedPages,
146 "failed write retains RAM and releases slot"))
147 return false;
148 disk.failWrite = false;
149 disk.failFlush = true;
150 if (!check(manager.pageOutRange(base, PageSize) == SwapStatus::IoError &&
151 space.isMapped(reinterpret_cast<void*>(base)) &&
152 !manager.swapSnapshot().usedPages,
153 "failed flush retains RAM"))
154 return false;
155 disk.failFlush = false;
156 if (!check(manager.compact() && !space.isMapped(reinterpret_cast<void*>(base)) &&
157 manager.swapSnapshot().usedPages == 1 &&
158 memory.memorySnapshot().freePages > before.freePages,
159 "real compactor releases RAM"))
160 return false;
161 disk.readsBeforeFailure = 0;
162 if (!check(!manager.faultIn(base, false) && !space.isMapped(reinterpret_cast<void*>(base)) &&
163 manager.swapSnapshot().usedPages == 1,
164 "failed read retains slot"))
165 return false;
166 disk.readsBeforeFailure = -1;
167 if (!check(manager.faultIn(base, false) && *reinterpret_cast<unsigned char*>(base) == 0x40 &&
168 !manager.swapSnapshot().usedPages,
169 "read retry restores contents"))
170 return false;
171 NOTICE("SWAP-VM-TEST: PASS write-flush-read-retry-physical-release");
172 if (!check(manager.pageOutRange(base, 3 * PageSize) == SwapStatus::Success &&
173 manager.swapSnapshot().usedPages == 3,
174 "drain preparation"))
175 return false;
176 disk.readsBeforeFailure = 1;
177 if (!check(manager.deactivateSwap(endpoint) == SwapStatus::IoError &&
178 manager.swapSnapshot().active && manager.swapSnapshot().usedPages == 2 &&
179 space.isMapped(reinterpret_cast<void*>(base)) &&
180 *reinterpret_cast<unsigned char*>(base) == 0x40,
181 "partial I/O drain remains active"))
182 return false;
183 disk.readsBeforeFailure = -1;
184 PhysicalMemoryManager::setTryAllocationFailureForTest(0);
185 const auto exhausted = manager.deactivateSwap(endpoint);
186 PhysicalMemoryManager::setTryAllocationFailureForTest(-1);
187 if (!check(exhausted == SwapStatus::NoMemory && manager.swapSnapshot().usedPages == 2 &&
188 manager.swapSnapshot().active,
189 "OOM drain retains remaining references"))
190 return false;
191 disk.failFlush = true;
192 if (!check(manager.deactivateSwap(endpoint) == SwapStatus::IoError &&
193 manager.swapSnapshot().active && !manager.swapSnapshot().usedPages,
194 "final flush failure retains empty channel"))
195 return false;
196 disk.failFlush = false;
197 if (!check(manager.deactivateSwap(endpoint) == SwapStatus::Success &&
198 !manager.swapSnapshot().active && disk.releases == 3 &&
199 *reinterpret_cast<unsigned char*>(base + 2 * PageSize) == 0x42,
200 "drain retry releases channel"))
201 return false;
202 NOTICE("SWAP-VM-TEST: PASS partial-drain-io-oom-retry");
203 if (!check(manager.activateSwap(endpoint) == SwapStatus::Success &&
204 manager.pageOutRange(base, 3 * PageSize) == SwapStatus::Success,
205 "clone preparation"))
206 return false;
207 Process* child = nullptr;
208 {
209 MemoryMapManager::OperationGuard operation(manager);
210 child = new Process(Processor::information().getCurrentThread()->getParent());
211 if (!check(child != nullptr, "child address space"))
212 return false;
213 AnonymousMemoryMap::setCloneFailureForTest(1);
214 const bool cloned = manager.clone(child);
215 AnonymousMemoryMap::setCloneFailureForTest(-1);
216 const bool rollback = !cloned && manager.swapSnapshot().usedPages == 3;
218 manager.unmapAll();
220 delete child;
221 child = nullptr;
222 if (!check(rollback, "failed clone reference rollback"))
223 return false;
224 }
225 if (!check(manager.deactivateSwap(endpoint) == SwapStatus::Success,
226 "clone failure did not retain references"))
227 return false;
228 if (!check(manager.activateSwap(endpoint) == SwapStatus::Success &&
229 manager.pageOutRange(base, 3 * PageSize) == SwapStatus::Success,
230 "foreign owner preparation"))
231 return false;
232 {
233 MemoryMapManager::OperationGuard operation(manager);
234 auto* parent = Processor::information().getCurrentThread()->getParent();
235 auto* target = new DeferredPagingProcess(parent);
236 if (!check(target != nullptr, "unpublished target"))
237 return false;
238 bool owned = manager.clone(target);
239 owned = check(owned && manager.deactivateSwap(endpoint) == SwapStatus::Busy &&
240 manager.swapSnapshot().active,
241 "unpublished owner blocks drain") &&
242 owned;
243 target->publishForTest();
244 if (owned)
245 owned = check(manager.deactivateSwap(endpoint) == SwapStatus::Success,
246 "published foreign owner restores");
247 Processor::switchAddressSpace(*target->getAddressSpace());
248 if (owned)
249 owned = check(*reinterpret_cast<unsigned char*>(base + 2 * PageSize) == 0x42,
250 "foreign restored contents");
251 if (owned)
252 owned = check(manager.activateSwap(endpoint) == SwapStatus::Success &&
253 manager.pageOutRange(base, PageSize) == SwapStatus::Success,
254 "foreign owner pageout");
256 target->closeForTest();
257 if (owned)
258 owned = check(manager.deactivateSwap(endpoint) == SwapStatus::Busy &&
259 manager.swapSnapshot().usedPages == 1,
260 "closed owner retains slot");
261 Processor::switchAddressSpace(*target->getAddressSpace());
262 manager.unmapAll();
264 delete target;
265 if (manager.swapSnapshot().active)
266 owned = check(manager.deactivateSwap(endpoint) == SwapStatus::Success,
267 "retired owner releases drain") &&
268 owned;
269 if (!owned)
270 return false;
271 }
272 NOTICE("SWAP-VM-TEST: PASS foreign-owner-publication-retirement");
273 if (!check(VFS::unloadAdmission(false) == Module::UnloadAdmission::Busy &&
274 VFS::unloadAdmission(true) == Module::UnloadAdmission::KeepMapped,
275 "registered mapping service remains after drain"))
276 return false;
277 NOTICE("SWAP-VM-TEST: PASS clone-metadata-failure-retirement");
278 NOTICE("SWAP-VM-TEST: PASS vfs-service-retention");
279 using Resolution = MemoryMapManager::FaultResolution;
280 if (!check(manager.activateSwap(endpoint) == SwapStatus::Success &&
281 manager.pageOutRange(base, PageSize) == SwapStatus::Success &&
282 manager.setPermissions(base, 3 * PageSize, MemoryMappedObject::Read),
283 "ordinary fault resolver preparation"))
284 return false;
285 const size_t reads = disk.reads;
286 if (!check(manager.resolveUserFault(base, true, false, false) == Resolution::Unhandled &&
287 manager.resolveUserFault(base, false, false, true) == Resolution::Unhandled &&
288 manager.setPermissions(base, 3 * PageSize, MemoryMappedObject::None) &&
289 manager.resolveUserFault(base, false, false, false) == Resolution::Unhandled &&
290 disk.reads == reads && manager.swapSnapshot().usedPages == 1 &&
291 !space.isMapped(reinterpret_cast<void*>(base)),
292 "readonly NX and inaccessible faults retain slot without I/O"))
293 return false;
294 if (!check(manager.setPermissions(base, 3 * PageSize, MemoryMappedObject::Read),
295 "resolver read permission restored"))
296 return false;
297 disk.readsBeforeFailure = 0;
298 if (!check(manager.resolveUserFault(base, false, false, false) == Resolution::Unhandled &&
299 manager.swapSnapshot().usedPages == 1 &&
300 !space.isMapped(reinterpret_cast<void*>(base)),
301 "resolver I/O failure retains backing"))
302 return false;
303 disk.readsBeforeFailure = -1;
304 if (!check(manager.resolveUserFault(base, false, false, false) == Resolution::Resolved &&
305 *reinterpret_cast<unsigned char*>(base) == 0x40 &&
306 !manager.swapSnapshot().usedPages &&
307 manager.resolveUserFault(base, true, true, false) == Resolution::Unhandled,
308 "resolver retry restores contents and preserves write protection"))
309 return false;
310 if (!check(manager.setPermissions(base, 3 * PageSize,
311 MemoryMappedObject::Read | MemoryMappedObject::Write) &&
312 manager.pageOutRange(base, PageSize) == SwapStatus::Success &&
313 manager.setPermissions(base, 3 * PageSize, MemoryMappedObject::Exec) &&
314 manager.resolveUserFault(base, false, false, true) == Resolution::Resolved &&
315 !manager.swapSnapshot().usedPages,
316 "permitted fetch restores execute-only swapped page"))
317 return false;
318 const bool splitMappingCreated =
319 manager.setPermissions(base + PageSize, PageSize,
320 MemoryMappedObject::Read | MemoryMappedObject::Write) == 1;
321 if (!check(splitMappingCreated &&
322 manager.pageOutRange(base + PageSize, PageSize) == SwapStatus::Success &&
323 manager.resolveUserFault(base + PageSize, false, false, false) ==
324 Resolution::Resolved &&
325 *reinterpret_cast<unsigned char*>(base + PageSize) == 0x41 &&
326 !manager.swapSnapshot().usedPages,
327 "split mapping fault resolver reuses selected object"))
328 return false;
329 uintptr_t empty = 0;
330 if (!check(
331 manager.mapFile(&emptyFile, empty, PageSize, MemoryMappedObject::Read) &&
332 manager.resolveUserFault(empty, false, false, false) == Resolution::BackingFault &&
333 manager.resolveUserFault(empty, true, false, false) == Resolution::Unhandled &&
334 !space.isMapped(reinterpret_cast<void*>(empty)) &&
335 manager.deactivateSwap(endpoint) == SwapStatus::Success,
336 "EOF classified for SIGBUS after logical permission checks"))
337 return false;
338 NOTICE("SWAP-VM-TEST: PASS ordinary-fault-retry-permissions-EOF");
339 return true;
340 }();
341 PhysicalMemoryManager::setTryAllocationFailureForTest(-1);
342 AnonymousMemoryMap::setCloneFailureForTest(-1);
343 disk.failWrite = disk.failFlush = false;
344 disk.readsBeforeFailure = -1;
345 manager.unmapAll();
346 if (manager.swapSnapshot().active)
347 passed =
348 check(manager.deactivateSwap(endpoint) == SwapStatus::Success, "fixture drain") && passed;
349 return 0;
350}
351} // namespace
352bool runSwapRegressions() {
353 NOTICE("SWAP-VM-TEST: BEGIN");
354 auto* process = new Process(Scheduler::instance().getKernelProcess(), true);
355 if (!check(process != nullptr, "isolated process"))
356 return false;
357 bool passed = false;
358 auto* worker = new Thread(process, swapWorker, &passed, nullptr, false, true, true);
359 const bool started = worker && worker->start();
360 const bool joined = started && worker->joinForCompletion();
361 if (!started)
362 delete worker;
363 if (!joined && started)
364 return check(false, "worker completion");
365 delete process;
366 passed = check(started && joined && passed, "isolated worker result");
367 if (passed)
368 NOTICE("SWAP-VM-TEST: END PASS");
369 return passed;
370}
371#endif
Memory-mapped file interface.
Definition Disk.h:35
virtual size_t getSize() const
Gets the size of the disk.
Definition Disk.cc:344
void retireEndpoint()
Definition Disk.cc:133
virtual void unpin(uint64_t location)=0
virtual PagingStatus preparePagingTransport(PagingTransport *&transport)
Definition Disk.cc:142
virtual MUST_USE_RESULT bool pin(uint64_t location)=0
Pins a cache page.
Definition File.h:74
static MemoryMapManager & instance()
static PhysicalMemoryManager & instance()
VirtualAddressSpace * getAddressSpace()
Definition Process.h:478
void publish()
Definition Process.cc:832
void prepareForDestruction()
Definition Process.cc:914
static ProcessorInformation & information()
static void switchAddressSpace(VirtualAddressSpace &AddressSpace)
static Scheduler & instance()
Definition Scheduler.h:96