The Pedigree Project 0.1
mmap-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/Log.h"
10#include "pedigree/kernel/errors.h"
11#include "pedigree/kernel/process/Process.h"
12#include "pedigree/kernel/process/Semaphore.h"
13#include "pedigree/kernel/process/Thread.h"
14#include "pedigree/kernel/processor/PhysicalMemoryManager.h"
15#include "pedigree/kernel/processor/Processor.h"
16#include "pedigree/kernel/processor/VirtualAddressSpace.h"
17#include "pedigree/kernel/utilities/MemoryAllocator.h"
18
19#include <stddef.h>
20#include <stdint.h>
21
22#include "modules/subsys/posix/PosixProcess.h"
23#include "modules/subsys/posix/PosixSubsystem.h"
24#include "modules/subsys/posix/file-syscalls.h"
25#include <sys/mman.h>
26
27namespace {
28constexpr int PreservedErrno = 123;
29constexpr uint8_t OriginalSentinel = 0x5A;
30
31struct PlacementContext {
32 PlacementContext()
33 : hintFallback(false),
34 noReplaceCollision(false),
35 failedPlacementPreserved(false),
36 fixedReplacement(false),
37 fixedSpanReserved(false),
38 exactReservation(false),
39 partialReclamation(false),
40 overlappingReclamation(false),
41 inputValidation(false),
42 noReplaceAtomic(false),
43 returned(0) {}
44
45 bool hintFallback;
46 bool noReplaceCollision;
47 bool failedPlacementPreserved;
48 bool fixedReplacement;
49 bool fixedSpanReserved;
50 bool exactReservation;
51 bool partialReclamation;
52 bool overlappingReclamation;
53 bool inputValidation;
54 bool noReplaceAtomic;
55 Atomic<size_t> returned;
56};
57
58struct RaceContext {
59 RaceContext(uintptr_t address, size_t length)
60 : begin(0, false), address(address), length(length) {}
61
62 Semaphore begin;
63 uintptr_t address;
64 size_t length;
65};
66
67struct RacerContext {
68 RacerContext(RaceContext* race, uint8_t sentinel)
69 : race(race), sentinel(sentinel), result(MAP_FAILED), error(0), returned(0) {}
70
71 RaceContext* race;
72 uint8_t sentinel;
73 void* result;
74 int error;
75 Atomic<size_t> returned;
76};
77
78struct UnmapRaceContext {
79 UnmapRaceContext() : begin(0, false) {}
80
81 Semaphore begin;
82};
83
84struct UnmapRacerContext {
85 UnmapRacerContext(UnmapRaceContext* race, uintptr_t address, size_t length)
86 : race(race), address(address), length(length), result(-1), error(0), returned(0) {}
87
88 UnmapRaceContext* race;
89 uintptr_t address;
90 size_t length;
91 int result;
92 int error;
93 Atomic<size_t> returned;
94};
95
96bool findFreeDynamicRange(Process* process, size_t pageSize, size_t length, uintptr_t& address) {
98 if (!process->snapshotUserReservations(snapshot)) {
99 return false;
100 }
101 const MemoryAllocator& allocator = snapshot.dynamic;
102 const uintptr_t mask = pageSize - 1;
103 for (size_t i = 0; i < allocator.size(); ++i) {
104 MemoryAllocator::Range range(0, 0);
105 if (!allocator.getRange(i, range) || range.address > ~static_cast<uintptr_t>(0) - mask) {
106 continue;
107 }
108
109 const uintptr_t aligned = (range.address + mask) & ~mask;
110 if (aligned >= range.address && aligned - range.address <= range.length &&
111 length <= range.length - (aligned - range.address)) {
112 address = aligned;
113 return true;
114 }
115 }
116 return false;
117}
118
119struct ReservationProbe {
120 Process* process = nullptr;
121 Process::UserRegion region = Process::UserRegion::Normal;
122
123 explicit operator bool() const {
124 return process != nullptr;
125 }
126 bool allocateSpecific(uintptr_t address, size_t length) const {
127 return process->allocateSpecificUserRange(region, address, length);
128 }
129 void free(uintptr_t address, size_t length) const {
130 process->freeUserRange(region, address, length);
131 }
132};
133
134ReservationProbe allocatorFor(Process* process, uintptr_t address, size_t length) {
135 VirtualAddressSpace* addressSpace = process->getAddressSpace();
136 const uintptr_t end = address + length;
137 if (addressSpace->getDynamicStart() && address >= addressSpace->getDynamicStart() &&
138 end <= addressSpace->getDynamicEnd()) {
139 return {process, Process::UserRegion::Dynamic};
140 }
141 if (address >= addressSpace->getUserStart() && end <= addressSpace->getUserReservedStart()) {
142 return {process, Process::UserRegion::Normal};
143 }
144 return {};
145}
146
147bool reservationHeld(ReservationProbe allocator, uintptr_t address, size_t length) {
148 if (!allocator.allocateSpecific(address, length)) {
149 return true;
150 }
151 allocator.free(address, length);
152 return false;
153}
154
155bool reservationAvailableExactlyOnce(ReservationProbe allocator, uintptr_t address, size_t length) {
156 const bool first = allocator.allocateSpecific(address, length);
157 const bool second = first && allocator.allocateSpecific(address, length);
158 if (first) {
159 allocator.free(address, length);
160 }
161 return first && !second;
162}
163
164int noReplaceRacer(void* parameter) {
165 RacerContext* context = reinterpret_cast<RacerContext*>(parameter);
166 Thread* thread = Processor::information().getCurrentThread();
167 if (!context->race->begin.acquire()) {
168 context->returned += 1;
169 return 1;
170 }
171
172 thread->setErrno(0);
173 context->result =
174 posix_mmap(reinterpret_cast<void*>(context->race->address), context->race->length,
175 PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON | MAP_FIXED_NOREPLACE, -1, 0);
176 context->error = thread->getErrno();
177 if (context->result != MAP_FAILED) {
178 *reinterpret_cast<volatile uint8_t*>(context->result) = context->sentinel;
179 }
180 context->returned += 1;
181 return 0;
182}
183
184bool runNoReplaceRace(Process* process, size_t pageSize) {
185 uintptr_t address = 0;
186 if (!findFreeDynamicRange(process, pageSize, pageSize, address)) {
187 return false;
188 }
189
190 RaceContext race(address, pageSize);
191 RacerContext first(&race, 0x31);
192 RacerContext second(&race, 0x42);
193 Thread* firstThread = new Thread(process, noReplaceRacer, &first, nullptr, false, true, true);
194 Thread* secondThread = new Thread(process, noReplaceRacer, &second, nullptr, false, true, true);
195 firstThread->setName("hosted mmap no-replace racer 1");
196 secondThread->setName("hosted mmap no-replace racer 2");
197
198 const bool firstStarted = firstThread->start();
199 const bool secondStarted = secondThread->start();
200 race.begin.release(2);
201 const bool firstJoined = firstStarted && firstThread->joinForCompletion();
202 const bool secondJoined = secondStarted && secondThread->joinForCompletion();
203 if (!firstStarted) {
204 delete firstThread;
205 }
206 if (!secondStarted) {
207 delete secondThread;
208 }
209
210 const bool firstWon = first.result == reinterpret_cast<void*>(address) && !first.error;
211 const bool secondWon = second.result == reinterpret_cast<void*>(address) && !second.error;
212 const bool firstLost = first.result == MAP_FAILED && first.error == Error::FileExists;
213 const bool secondLost = second.result == MAP_FAILED && second.error == Error::FileExists;
214 const uint8_t expected = firstWon ? first.sentinel : second.sentinel;
215 const bool sentinelPreserved =
216 (firstWon || secondWon) && *reinterpret_cast<volatile uint8_t*>(address) == expected;
217 const bool passed = firstStarted && secondStarted && firstJoined && secondJoined &&
218 first.returned == 1 && second.returned == 1 &&
219 ((firstWon && secondLost) || (secondWon && firstLost)) && sentinelPreserved;
220
221 if (firstWon || secondWon) {
222 posix_munmap(reinterpret_cast<void*>(address), pageSize);
223 }
224 return passed;
225}
226
227int unmapRacer(void* parameter) {
228 UnmapRacerContext* context = reinterpret_cast<UnmapRacerContext*>(parameter);
229 Thread* thread = Processor::information().getCurrentThread();
230 if (!context->race->begin.acquire()) {
231 context->returned += 1;
232 return 1;
233 }
234
235 thread->setErrno(PreservedErrno);
236 context->result = posix_munmap(reinterpret_cast<void*>(context->address), context->length);
237 context->error = thread->getErrno();
238 context->returned += 1;
239 return context->result ? 1 : 0;
240}
241
242bool runOverlappingUnmapRace(Process* process, size_t pageSize) {
243 const size_t mappingLength = pageSize * 3;
244 void* mapping =
245 posix_mmap(nullptr, mappingLength, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
246 if (mapping == MAP_FAILED) {
247 return false;
248 }
249
250 const uintptr_t address = reinterpret_cast<uintptr_t>(mapping);
251 ReservationProbe allocator = allocatorFor(process, address, mappingLength);
252 if (!allocator) {
253 posix_munmap(mapping, mappingLength);
254 return false;
255 }
256
257 UnmapRaceContext race;
258 UnmapRacerContext first(&race, address, pageSize * 2);
259 UnmapRacerContext second(&race, address + pageSize, pageSize * 2);
260 Thread* firstThread = new Thread(process, unmapRacer, &first, nullptr, false, true, true);
261 Thread* secondThread = new Thread(process, unmapRacer, &second, nullptr, false, true, true);
262 firstThread->setName("hosted munmap overlap racer 1");
263 secondThread->setName("hosted munmap overlap racer 2");
264
265 const bool firstStarted = firstThread->start();
266 const bool secondStarted = secondThread->start();
267 race.begin.release(2);
268 const bool firstJoined = firstStarted && firstThread->joinForCompletion();
269 const bool secondJoined = secondStarted && secondThread->joinForCompletion();
270 if (!firstStarted) {
271 delete firstThread;
272 }
273 if (!secondStarted) {
274 delete secondThread;
275 }
276
277 const bool releasedExactlyOnce =
278 reservationAvailableExactlyOnce(allocator, address, mappingLength);
279 const bool passed = firstStarted && secondStarted && firstJoined && secondJoined &&
280 first.returned == 1 && second.returned == 1 && !first.result &&
281 !second.result && first.error == PreservedErrno &&
282 second.error == PreservedErrno && releasedExactlyOnce;
283 if (!releasedExactlyOnce) {
284 posix_munmap(mapping, mappingLength);
285 }
286 return passed;
287}
288
289bool anonymousReservationReclamation(Process* process, size_t pageSize) {
290 const size_t mappingLength = pageSize * 3;
291 void* mapping =
292 posix_mmap(nullptr, mappingLength, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
293 if (mapping == MAP_FAILED) {
294 return false;
295 }
296
297 const uintptr_t address = reinterpret_cast<uintptr_t>(mapping);
298 ReservationProbe allocator = allocatorFor(process, address, mappingLength + pageSize);
299 if (!allocator) {
300 posix_munmap(mapping, mappingLength);
301 return false;
302 }
303
304 const bool adjacentAvailable = allocator.allocateSpecific(address + mappingLength, pageSize);
305 if (adjacentAvailable) {
306 allocator.free(address + mappingLength, pageSize);
307 }
308
309 const uintptr_t middle = address + pageSize;
310 const bool middleUnmapped = !posix_munmap(reinterpret_cast<void*>(middle), pageSize);
311 const bool prefixHeld = reservationHeld(allocator, address, pageSize);
312 const bool suffixHeld = reservationHeld(allocator, address + (pageSize * 2), pageSize);
313 const bool middleAvailable = allocator.allocateSpecific(middle, pageSize);
314 if (middleAvailable) {
315 allocator.free(middle, pageSize);
316 }
317
318 void* middleMapping =
319 posix_mmap(reinterpret_cast<void*>(middle), pageSize, PROT_READ | PROT_WRITE,
320 MAP_PRIVATE | MAP_ANON | MAP_FIXED_NOREPLACE, -1, 0);
321 const bool middleReused = middleMapping == reinterpret_cast<void*>(middle);
322
323 const bool remainderUnmapped = !posix_munmap(mapping, mappingLength);
324 const bool releasedExactlyOnce =
325 reservationAvailableExactlyOnce(allocator, address, mappingLength);
326 if (!releasedExactlyOnce) {
327 posix_munmap(mapping, mappingLength);
328 }
329
330 return adjacentAvailable && middleUnmapped && prefixHeld && suffixHeld && middleAvailable &&
331 middleReused && remainderUnmapped && releasedExactlyOnce;
332}
333
334bool fixedReplacementReservesHoles(Process* process, size_t pageSize) {
335 const size_t replacementLength = pageSize * 3;
336 uintptr_t address = 0;
337 if (!findFreeDynamicRange(process, pageSize, replacementLength, address)) {
338 return false;
339 }
340
341 void* initial = posix_mmap(reinterpret_cast<void*>(address), pageSize, PROT_READ | PROT_WRITE,
342 MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0);
343 void* replacement =
344 initial == reinterpret_cast<void*>(address)
345 ? posix_mmap(reinterpret_cast<void*>(address), replacementLength, PROT_READ | PROT_WRITE,
346 MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0)
347 : MAP_FAILED;
348 const bool holesUnavailable = replacement == reinterpret_cast<void*>(address) &&
349 !process->allocateSpecificUserRange(
350 Process::UserRegion::Dynamic, address + pageSize, pageSize * 2);
351
352 bool releasedExactlyOnce = false;
353 if (replacement != MAP_FAILED) {
354 if (!posix_munmap(replacement, replacementLength)) {
355 releasedExactlyOnce = reservationAvailableExactlyOnce({process, Process::UserRegion::Dynamic},
356 address, replacementLength);
357 }
358 } else if (initial != MAP_FAILED) {
359 posix_munmap(initial, pageSize);
360 }
361 return holesUnavailable && releasedExactlyOnce;
362}
363
364int exerciseMmapPlacement(void* parameter) {
365 PlacementContext* context = reinterpret_cast<PlacementContext*>(parameter);
366 Thread* thread = Processor::information().getCurrentThread();
367 Process* process = thread->getParent();
368 const size_t pageSize = PhysicalMemoryManager::getPageSize();
369
370 thread->setErrno(PreservedErrno);
371 void* original =
372 posix_mmap(nullptr, pageSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
373 const bool originalMapped = original != MAP_FAILED && thread->getErrno() == PreservedErrno;
374 if (originalMapped) {
375 *reinterpret_cast<volatile uint8_t*>(original) = OriginalSentinel;
376 }
377
378 thread->setErrno(PreservedErrno);
379 void* hinted =
380 originalMapped
381 ? posix_mmap(reinterpret_cast<void*>(reinterpret_cast<uintptr_t>(original) + 17),
382 pageSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0)
383 : MAP_FAILED;
384 context->hintFallback = originalMapped && hinted != MAP_FAILED && hinted != original &&
385 !(reinterpret_cast<uintptr_t>(hinted) & (pageSize - 1)) &&
386 *reinterpret_cast<volatile uint8_t*>(original) == OriginalSentinel &&
387 thread->getErrno() == PreservedErrno;
388
389 thread->setErrno(0);
390 void* noReplace = originalMapped ? posix_mmap(original, pageSize, PROT_READ | PROT_WRITE,
391 MAP_PRIVATE | MAP_ANON | MAP_FIXED_NOREPLACE, -1, 0)
392 : nullptr;
393 context->noReplaceCollision = originalMapped && noReplace == MAP_FAILED &&
394 thread->getErrno() == Error::FileExists &&
395 *reinterpret_cast<volatile uint8_t*>(original) == OriginalSentinel;
396 ReservationProbe originalAllocator =
397 originalMapped ? allocatorFor(process, reinterpret_cast<uintptr_t>(original), pageSize)
398 : ReservationProbe{};
399 context->failedPlacementPreserved =
400 context->noReplaceCollision && originalAllocator &&
401 reservationHeld(originalAllocator, reinterpret_cast<uintptr_t>(original), pageSize);
402
403 thread->setErrno(0);
404 const void* invalidFixed = posix_mmap(nullptr, pageSize, PROT_READ | PROT_WRITE,
405 MAP_PRIVATE | MAP_ANON | MAP_FIXED_NOREPLACE, -1, 0);
406 const bool nullFixedRejected =
407 invalidFixed == MAP_FAILED && thread->getErrno() == Error::InvalidArgument;
408 thread->setErrno(0);
409 const void* overflowLength = posix_mmap(nullptr, ~static_cast<size_t>(0), PROT_READ | PROT_WRITE,
410 MAP_PRIVATE | MAP_ANON, -1, 0);
411 const bool overflowMmapRejected =
412 overflowLength == MAP_FAILED && thread->getErrno() == Error::InvalidArgument;
413 thread->setErrno(0);
414 const int overflowUnmap = originalMapped ? posix_munmap(original, ~static_cast<size_t>(0)) : 0;
415 const bool overflowUnmapRejected =
416 originalMapped && overflowUnmap == -1 && thread->getErrno() == Error::InvalidArgument &&
417 *reinterpret_cast<volatile uint8_t*>(original) == OriginalSentinel && originalAllocator &&
418 reservationHeld(originalAllocator, reinterpret_cast<uintptr_t>(original), pageSize);
419 context->inputValidation = nullFixedRejected && overflowMmapRejected && overflowUnmapRejected;
420
421 thread->setErrno(PreservedErrno);
422 void* replacement = originalMapped ? posix_mmap(original, pageSize, PROT_READ | PROT_WRITE,
423 MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0)
424 : MAP_FAILED;
425 context->fixedReplacement = originalMapped && replacement == original &&
426 thread->getErrno() == PreservedErrno &&
427 !*reinterpret_cast<volatile uint8_t*>(replacement);
428 if (replacement != MAP_FAILED) {
429 *reinterpret_cast<volatile uint8_t*>(replacement) = 0x7C;
430 }
431
432 context->fixedSpanReserved = fixedReplacementReservesHoles(process, pageSize);
433 context->partialReclamation = anonymousReservationReclamation(process, pageSize);
434 context->overlappingReclamation = runOverlappingUnmapRace(process, pageSize);
435 context->noReplaceAtomic = runNoReplaceRace(process, pageSize);
436
437 if (hinted != MAP_FAILED) {
438 posix_munmap(hinted, pageSize);
439 }
440 if (replacement != MAP_FAILED) {
441 const uintptr_t replacementAddress = reinterpret_cast<uintptr_t>(replacement);
442 ReservationProbe replacementAllocator = allocatorFor(process, replacementAddress, pageSize);
443 const bool unmapped = !posix_munmap(replacement, pageSize);
444 context->exactReservation =
445 replacementAllocator && unmapped &&
446 reservationAvailableExactlyOnce(replacementAllocator, replacementAddress, pageSize);
447 } else if (originalMapped) {
448 posix_munmap(original, pageSize);
449 }
450
451 context->returned += 1;
452 return context->hintFallback && context->noReplaceCollision &&
453 context->failedPlacementPreserved && context->fixedReplacement &&
454 context->fixedSpanReserved && context->exactReservation &&
455 context->partialReclamation && context->overlappingReclamation &&
456 context->inputValidation && context->noReplaceAtomic
457 ? 0
458 : 1;
459}
460} // namespace
461
462bool runHostedMmapPlacementRegressions(Process* kernelProcess) {
463 PosixProcess* process = new PosixProcess(kernelProcess);
464 process->setSubsystem(new PosixSubsystem);
465 PlacementContext context;
466 Thread* worker = new Thread(process, exerciseMmapPlacement, &context, nullptr, false, true, true);
467 worker->setName("hosted mmap placement worker");
468 process->publish();
469
470 const bool started = worker->start();
471 const bool joined = started && worker->joinForCompletion();
472 if (!started) {
473 delete worker;
474 }
475 const bool passed =
476 started && joined && context.returned == 1 && context.hintFallback &&
477 context.noReplaceCollision && context.failedPlacementPreserved && context.fixedReplacement &&
478 context.fixedSpanReserved && context.exactReservation && context.partialReclamation &&
479 context.overlappingReclamation && context.inputValidation && context.noReplaceAtomic;
480 delete process;
481
482 if (!passed) {
483 ERROR(
484 "HOSTED-SYSCALL-TEST: FAIL mmap-placement: "
485 "hint="
486 << context.hintFallback << " no-replace=" << context.noReplaceCollision
487 << " failed-preserved=" << context.failedPlacementPreserved << " replacement="
488 << context.fixedReplacement << " fixed-span=" << context.fixedSpanReserved
489 << " exact=" << context.exactReservation << " partial=" << context.partialReclamation
490 << " overlap=" << context.overlappingReclamation << " validation="
491 << context.inputValidation << " no-replace-race=" << context.noReplaceAtomic);
492 return false;
493 }
494 NOTICE("HOSTED-SYSCALL-TEST: PASS mmap-placement");
495 return true;
496}
VirtualAddressSpace * getAddressSpace()
Definition Process.h:478
void publish()
Definition Process.cc:832
static ProcessorInformation & information()
bool getRange(size_t index, Range &range) const
Definition RangeList.h:419
size_t size() const
Definition RangeList.h:104
void setErrno(size_t err)
Definition Thread.h:478
size_t getErrno()
Definition Thread.h:473
bool joinForCompletion()
Definition Thread.cc:2771
Process * getParent() const
Definition Thread.h:338
bool start()
Definition Thread.cc:794
virtual uintptr_t getUserStart() const =0