The Pedigree Project 0.1
vm-contracts.c
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#define _GNU_SOURCE
9
10#include <errno.h>
11#include <fcntl.h>
12#include <signal.h>
13#include <stdint.h>
14#include <stdio.h>
15#include <stdlib.h>
16#include <string.h>
17#include <unistd.h>
18
19#include <sys/mman.h>
20#include <sys/syscall.h>
21#include <sys/wait.h>
22
23extern void fail(void) __attribute__((noreturn));
24
25static void require(int condition, const char* operation) {
26 if (!condition) {
27 printf("VM-CONTRACT: FAIL %s errno=%d\n", operation, errno);
28 fail();
29 }
30}
31
32static void anonymous_ranges(size_t page) {
33 unsigned char* memory =
34 mmap(NULL, page * 4, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
35 require(memory != MAP_FAILED, "anonymous mapping");
36 for (size_t i = 4; i; --i) {
37 memory[(i - 1) * page] = (unsigned char)(0x40 + i);
38 memory[i * page - 1] = (unsigned char)(0x60 + i);
39 }
40 require(munmap(memory, page) == 0, "remove reverse-faulted prefix");
41 unsigned char* prefix = mmap(memory, page, PROT_READ | PROT_WRITE,
42 MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0);
43 require(prefix == memory && prefix[0] == 0 && prefix[page - 1] == 0,
44 "prefix removed every resident page");
45 memory[0] = 0x41;
46 memory[page - 1] = 0x61;
47 require(mprotect(memory + page, page * 2, PROT_NONE) == 0, "protect middle none");
48 require(mprotect(memory + page, page * 2, PROT_READ | PROT_WRITE) == 0,
49 "restore middle protections");
50 for (size_t i = 1; i <= 4; ++i) {
51 require(memory[(i - 1) * page] == 0x40 + i && memory[i * page - 1] == 0x60 + i,
52 "anonymous protection preserves contents");
53 }
54 require(munmap(memory + page, page) == 0, "create range hole");
55 errno = 0;
56 require(mprotect(memory, page * 4, PROT_READ) == -1 && errno == ENOMEM,
57 "protection rejects uncovered range");
58 memory[0] = 0xA1;
59 memory[page * 2] = 0xA2;
60 errno = 0;
61 require(msync(memory, page * 4, MS_SYNC) == -1 && errno == ENOMEM,
62 "sync rejects uncovered range");
63 errno = 0;
64 require(mprotect(memory, page, 0x40000000) == -1 && errno == EINVAL,
65 "protection rejects unknown flags");
66 // musl rounds SIZE_MAX to a zero-length request before entering the kernel.
67 errno = 0;
68 require(syscall(SYS_mprotect, memory, SIZE_MAX, PROT_NONE) == -1 && errno == ENOMEM &&
69 memory[0] == 0xA1,
70 "syscall length-rounding overflow preserves mapping");
71 errno = 0;
72 require(mprotect(memory, SIZE_MAX & ~(page - 1), PROT_NONE) == -1 && errno == ENOMEM &&
73 memory[0] == 0xA1,
74 "address-range overflow preserves mapping");
75 require(munmap(memory, page * 4) == 0, "unmap range with hole");
76 puts("VM-CONTRACT: PASS anonymous-ranges");
77}
78
79static void protected_fork(size_t page) {
80 unsigned char* memory =
81 mmap(NULL, page * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
82 require(memory != MAP_FAILED, "fork mapping");
83 memory[0] = 0x31;
84 memory[page] = 0x32;
85 require(mprotect(memory, page, PROT_READ) == 0 && mprotect(memory + page, page, PROT_NONE) == 0,
86 "protect before fork");
87 int ready[2], proceed[2];
88 require(pipe(ready) == 0 && pipe(proceed) == 0, "fork barriers");
89 pid_t child = fork();
90 require(child >= 0, "fork protected mapping");
91 if (!child) {
92 close(ready[0]);
93 close(proceed[1]);
94 if (mprotect(memory, page * 2, PROT_READ | PROT_WRITE) != 0 || memory[0] != 0x31 ||
95 memory[page] != 0x32)
96 _exit(21);
97 memory[0] = 0x51;
98 memory[page] = 0x52;
99 char byte = 'x';
100 if (write(ready[1], &byte, 1) != 1 || read(proceed[0], &byte, 1) != 1 || memory[0] != 0x51 ||
101 memory[page] != 0x52)
102 _exit(22);
103 _exit(0);
104 }
105 close(ready[1]);
106 close(proceed[0]);
107 char byte = 0;
108 require(read(ready[0], &byte, 1) == 1, "child restored protected pages");
109 require(mprotect(memory, page * 2, PROT_READ | PROT_WRITE) == 0 && memory[0] == 0x31 &&
110 memory[page] == 0x32,
111 "child writes preserve parent protected contents");
112 memory[0] = 0x71;
113 memory[page] = 0x72;
114 require(write(proceed[1], &byte, 1) == 1, "release protected child");
115 int status = 0;
116 require(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0,
117 "parent writes preserve child contents");
118 close(ready[0]);
119 close(proceed[1]);
120 require(munmap(memory, page * 2) == 0, "unmap protected fork mapping");
121 puts("VM-CONTRACT: PASS protected-fork");
122}
123
124static void require_sparse(int condition, const char* operation, unsigned scenario,
125 const void* address) {
126 if (!condition) {
127 printf("VM-CONTRACT: FAIL sparse case=%u stage=%s address=%p errno=%d\n", scenario, operation,
128 address, errno);
129 fail();
130 }
131}
132
133static void sparse_file_splits(int file, size_t page, const unsigned char* pattern) {
134 const unsigned resident_masks[] = {4, 2, 5, 7};
135 for (unsigned scenario = 0; scenario < 24; ++scenario) {
136 unsigned mask = resident_masks[scenario / 6];
137 unsigned reverse_split = (scenario / 3) % 2;
138 unsigned removal = scenario % 3;
139 unsigned char* arena =
140 mmap(NULL, page * 7, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
141 require_sparse(arena != MAP_FAILED, "guarded reservation", scenario, arena);
142 arena[0] = 0x91;
143 arena[page * 4] = 0x92;
144 arena[page * 6] = 0x93;
145 unsigned char* memory = arena + page;
146 require_sparse(munmap(memory, page * 3) == 0, "file reservation hole", scenario, memory);
147 require_sparse(
148 mmap(memory, page * 3, PROT_READ, MAP_SHARED | MAP_FIXED_NOREPLACE, file, 0) == memory,
149 "file placement", scenario, memory);
150 for (size_t i = 3; i; --i) {
151 size_t offset = (i - 1) * page;
152 if (mask & (1U << (i - 1))) {
153 require_sparse(memory[offset] == pattern[offset] &&
154 memory[offset + page - 1] == pattern[offset + page - 1],
155 "resident file contents", scenario, memory);
156 }
157 }
158 require_sparse(mprotect(memory + (reverse_split ? 2 : 1) * page, (reverse_split ? 1 : 2) * page,
159 PROT_READ) == 0 &&
160 mprotect(memory + (reverse_split ? 1 : 2) * page, page, PROT_READ) == 0,
161 "protection split", scenario, memory);
162
163 if (removal) {
164 size_t first = removal == 1 ? 0 : page * 2;
165 require_sparse(munmap(memory + first, page) == 0, "first fragment removal", scenario, memory);
166 }
167 // An unrelated hole separates the file fragments in allocator storage.
168 require_sparse(munmap(arena + page * 5, page) == 0, "unrelated hole", scenario, memory);
169 if (!removal) {
170 require_sparse(munmap(memory, page * 3) == 0, "whole split removal", scenario, memory);
171 } else {
172 size_t last = removal == 1 ? page * 2 : 0;
173 require_sparse(munmap(memory + last, page) == 0 && munmap(memory + page, page) == 0,
174 "remaining fragment removal", scenario, memory);
175 }
176
177 errno = 0;
178 unsigned char* reclaimed = mmap(memory, page * 3, PROT_READ | PROT_WRITE,
179 MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0);
180 if (reclaimed != memory) {
181 printf("VM-CONTRACT: sparse case=%u requested=%p returned=%p errno=%d\n", scenario,
182 (void*)memory, (void*)reclaimed, errno);
183 require_sparse(0, "released reservation", scenario, memory);
184 }
185 for (size_t offset = 0; offset < page * 3; ++offset) {
186 unsigned char value = reclaimed[offset];
187 if (value) {
188 printf("VM-CONTRACT: sparse case=%u offset=%zu value=%u\n", scenario, offset, value);
189 require_sparse(0, "reclaimed zero contents", scenario, memory);
190 }
191 }
192 require_sparse(arena[0] == 0x91 && arena[page * 4] == 0x92 && arena[page * 6] == 0x93,
193 "neighbor contents", scenario, memory);
194 require_sparse(munmap(arena, page * 7) == 0, "guarded cleanup", scenario, arena);
195 }
196 puts("VM-CONTRACT: PASS sparse-splits cases=24");
197}
198
199static void file_mappings(size_t page) {
200 char path[80];
201 snprintf(path, sizeof(path), "/tmp/vm-contract-%d", getpid());
202 int file = open(path, O_CREAT | O_TRUNC | O_RDWR, 0600);
203 require(file >= 0, "create mapped file");
204 unsigned char* pattern = malloc(page * 3);
205 require(pattern != NULL, "file pattern allocation");
206 for (size_t i = 0; i < page * 3; ++i)
207 pattern[i] = (unsigned char)(1 + i % 251);
208 require(write(file, pattern, page * 3) == (ssize_t)(page * 3), "populate mapped file");
209
210 unsigned char* private = mmap(NULL, page + 17, PROT_READ | PROT_WRITE, MAP_PRIVATE, file, 0);
211 require(private != MAP_FAILED, "private partial-length mapping");
212 require(private[page + 25] == pattern[page + 25], "mapping covers final complete page");
213 private[0] = 0xA1;
214 private[page + 25] = 0xA2;
215 require(mprotect(private, page * 2, PROT_NONE) == 0 &&
216 mprotect(private, page * 2, PROT_READ | PROT_WRITE) == 0 && private[0] == 0xA1 &&
217 private[page + 25] == 0xA2,
218 "private file protection preserves modifications");
219 require(msync(private, page * 2, MS_SYNC | MS_INVALIDATE) == 0 && private[0] == 0xA1 &&
220 private[page + 25] == 0xA2,
221 "invalidation preserves private modifications");
222 unsigned char byte;
223 require(pread(file, &byte, 1, 0) == 1 && byte == pattern[0], "private writes preserve file");
224 require(munmap(private, page * 2) == 0, "unmap private file");
225
226 unsigned char* sparse = mmap(NULL, page * 3, PROT_READ, MAP_SHARED, file, 0);
227 require(sparse != MAP_FAILED && sparse[page * 2] == pattern[page * 2],
228 "fault last sparse file page");
229 require(mprotect(sparse + page, page * 2, PROT_READ) == 0 && munmap(sparse, page * 3) == 0,
230 "unmap split with untouched prefix page");
231 errno = 0;
232 unsigned char* reclaimed = mmap(sparse, page * 3, PROT_READ | PROT_WRITE,
233 MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0);
234 if (reclaimed != sparse)
235 printf("VM-CONTRACT: original sparse requested=%p returned=%p errno=%d\n", (void*)sparse,
236 (void*)reclaimed, errno);
237 require(reclaimed == sparse, "sparse split released address reservation");
238 unsigned char reclaimed_value = reclaimed[page * 2];
239 if (reclaimed_value)
240 printf("VM-CONTRACT: original sparse address=%p offset=%zu value=%u\n", (void*)sparse, page * 2,
241 reclaimed_value);
242 require(reclaimed_value == 0, "sparse split released every resident file page");
243 require(munmap(reclaimed, page * 3) == 0, "unmap reclaimed sparse range");
244 sparse_file_splits(file, page, pattern);
245
246 unsigned char* shared = mmap(NULL, page * 3, PROT_READ | PROT_WRITE, MAP_SHARED, file, 0);
247 require(shared != MAP_FAILED, "shared file mapping");
248 shared[page * 2] = 0xD1;
249 require(mprotect(shared + page, page, PROT_READ) == 0, "split with untouched file page");
250 require(mprotect(shared + page * 2, page, PROT_NONE) == 0 &&
251 msync(shared + page * 2, page, MS_SYNC) == 0 &&
252 mprotect(shared + page * 2, page, PROT_READ) == 0 && shared[page * 2] == 0xD1,
253 "sync after removing shared write access");
254 require(pread(file, &byte, 1, (off_t)(page * 2)) == 1 && byte == 0xD1,
255 "shared modifications reach backing view");
256 errno = 0;
257 require(msync(shared, page, MS_SYNC | MS_ASYNC) == -1 && errno == EINVAL,
258 "sync rejects conflicting flags");
259 require(munmap(shared, page * 3) == 0, "unmap sparse split file mapping");
260
261 int readonly = open(path, O_RDONLY);
262 require(readonly >= 0, "open read-only mapping descriptor");
263 shared = mmap(NULL, page, PROT_READ, MAP_SHARED, readonly, 0);
264 require(shared != MAP_FAILED && shared[0] == pattern[0], "read-only shared mapping");
265 errno = 0;
266 require(mprotect(shared, page, PROT_READ | PROT_WRITE) == -1 && errno == EACCES,
267 "shared mapping remembers descriptor permissions");
268 require(munmap(shared, page) == 0, "unmap read-only shared mapping");
269 errno = 0;
270 require(mmap(NULL, page, PROT_READ | PROT_WRITE, MAP_SHARED, readonly, 0) == MAP_FAILED &&
271 errno == EACCES,
272 "shared mapping rejects read-only descriptor writes");
273 private = mmap(NULL, page, PROT_READ | PROT_WRITE, MAP_PRIVATE, readonly, 0);
274 require(private != MAP_FAILED, "read-only descriptor allows private copy");
275 private[0] = 0xE1;
276 require(pread(readonly, &byte, 1, 0) == 1 && byte == pattern[0], "private copy remains isolated");
277 require(munmap(private, page) == 0, "unmap read-only descriptor private copy");
278 close(readonly);
279 int writeonly = open(path, O_WRONLY);
280 require(writeonly >= 0, "open write-only mapping descriptor");
281 errno = 0;
282 require(mmap(NULL, page, PROT_NONE, MAP_PRIVATE, writeonly, 0) == MAP_FAILED && errno == EACCES,
283 "file mapping requires readable descriptor");
284 close(writeonly);
285 close(file);
286 require(unlink(path) == 0, "remove mapped file");
287 free(pattern);
288 puts("VM-CONTRACT: PASS file-mappings");
289}
290
291static volatile unsigned char* bus_address;
292
293static void mapped_bus_handler(int signal, siginfo_t* info, void* context) {
294 (void)context;
295 _exit(signal == SIGBUS && info->si_code == BUS_ADRERR && info->si_addr == (void*)bus_address
296 ? 0
297 : 61);
298}
299
301 pid_t pid;
302 int proceed;
303};
304
305static struct shrink_child wait_for_shrink(unsigned char* address, int check_info) {
306 int ready[2], proceed[2];
307 require(pipe(ready) == 0 && pipe(proceed) == 0, "shrink barriers");
308 pid_t child = fork();
309 require(child >= 0, "fork mapped shrink observer");
310 if (!child) {
311 close(ready[0]);
312 close(proceed[1]);
313 bus_address = address;
314 if (check_info) {
315 struct sigaction action = {0};
316 action.sa_sigaction = mapped_bus_handler;
317 action.sa_flags = SA_SIGINFO;
318 sigemptyset(&action.sa_mask);
319 if (sigaction(SIGBUS, &action, NULL))
320 _exit(62);
321 } else {
322 signal(SIGBUS, SIG_DFL);
323 }
324 volatile unsigned char before = *bus_address;
325 (void)before;
326 char byte = 'x';
327 if (write(ready[1], &byte, 1) != 1 || read(proceed[0], &byte, 1) != 1)
328 _exit(63);
329 volatile unsigned char after = *bus_address;
330 (void)after;
331 _exit(64);
332 }
333 close(ready[1]);
334 close(proceed[0]);
335 char byte;
336 require(read(ready[0], &byte, 1) == 1, "child has resident EOF page");
337 close(ready[0]);
338 return (struct shrink_child){child, proceed[1]};
339}
340
341static void reap_shrink_child(struct shrink_child child, int check_info) {
342 char byte = 'x';
343 require(write(child.proceed, &byte, 1) == 1, "release mapped shrink observer");
344 close(child.proceed);
345 int status = 0;
346 require(waitpid(child.pid, &status, 0) == child.pid, "wait for mapped EOF signal");
347 require(check_info ? (WIFEXITED(status) && WEXITSTATUS(status) == 0)
348 : (WIFSIGNALED(status) && WTERMSIG(status) == SIGBUS),
349 check_info ? "shared EOF signal address and BUS_ADRERR" : "private EOF terminal SIGBUS");
350}
351
352static void mapped_shrink(size_t page) {
353 char path[80];
354 snprintf(path, sizeof(path), "/tmp/vm-shrink-%d", getpid());
355 int file = open(path, O_CREAT | O_EXCL | O_RDWR, 0600);
356 require(file >= 0, "create shrink file");
357 unsigned char* bytes = malloc(page * 3);
358 require(bytes != NULL, "shrink pattern");
359 memset(bytes, 0x49, page * 3);
360 require(write(file, bytes, page * 3) == (ssize_t)(page * 3), "populate shrink file");
361 free(bytes);
362 unsigned char* shared = mmap(NULL, page * 3, PROT_READ | PROT_WRITE, MAP_SHARED, file, 0);
363 unsigned char* private = mmap(NULL, page * 3, PROT_READ | PROT_WRITE, MAP_PRIVATE, file, 0);
364 unsigned char* offset = mmap(NULL, page, PROT_READ, MAP_SHARED, file, (off_t)page);
365 require(shared != MAP_FAILED && private != MAP_FAILED && offset != MAP_FAILED, "shrink mappings");
366 shared[0] = 0x51;
367 shared[page + 37] = 0x52;
368 private[page + 37] = 0xA5;
369 private[page * 2] = 0xA6;
370 require(offset[37] == 0x52, "offset alias before shrink");
371 struct shrink_child shared_child = wait_for_shrink(shared + page * 2 + 11, 1);
372 struct shrink_child private_child = wait_for_shrink(private + page * 2 + 13, 0);
373 require(mprotect(shared + page, page, PROT_NONE) == 0, "inaccessible borrowed shrink page");
374 require(ftruncate(file, (off_t)(page + 17)) == 0,
375 "shrink with shared and private resident pages");
376 reap_shrink_child(shared_child, 1);
377 reap_shrink_child(private_child, 0);
378 require(mprotect(shared + page, page, PROT_READ | PROT_WRITE) == 0, "restore shrunken tail");
379 require(shared[0] == 0x51 && shared[page + 16] == 0x49 && offset[16] == 0x49,
380 "shrink preserves preceding shared contents");
381 for (size_t i = 17; i < page; ++i)
382 require(shared[page + i] == 0 && offset[i] == 0, "shrink zeroes partial borrowed page");
383 require(private[page + 37] == 0xA5 && private[page + 16] == 0x49,
384 "shrink preserves partial private COW page");
385 errno = 0;
386 require(pread(file, shared + page * 2, 1, 0) == -1 && errno == EFAULT,
387 "usercopy into mapped EOF returns EFAULT");
388 require(ftruncate(file, (off_t)(page * 3)) == 0, "regrow mapped file");
389 require(shared[page * 2] == 0 && private[page * 2] == 0,
390 "regrowth cannot resurrect discarded private or shared whole pages");
391 require(
392 munmap(shared, page * 3) == 0 && munmap(private, page * 3) == 0 && munmap(offset, page) == 0,
393 "unmap shrunken file aliases");
394 close(file);
395 require(unlink(path) == 0, "remove shrink file");
396 puts("VM-CONTRACT: PASS mapped-shrink");
397}
398
399void test_vm_contracts(void) {
400 long configured_page_size = sysconf(_SC_PAGESIZE);
401 require(configured_page_size > 0, "page size");
402 size_t page = (size_t)configured_page_size;
403 anonymous_ranges(page);
404 protected_fork(page);
405 file_mappings(page);
406 mapped_shrink(page);
407 puts("VM-CONTRACT: PASS all");
408}