The Pedigree Project 0.1
robust-memory-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#include <errno.h>
10#include <fcntl.h>
11#include <pthread.h>
12#include <sched.h>
13#include <signal.h>
14#include <stddef.h>
15#include <stdint.h>
16#include <stdio.h>
17#include <time.h>
18#include <unistd.h>
19
20#include <sys/mman.h>
21#include <sys/syscall.h>
22#include <sys/wait.h>
23
24extern void fail(void) __attribute__((noreturn));
25static const char* executable;
26
27static int wait_word(int* word, int expected) {
28 struct timespec now;
29 clock_gettime(CLOCK_MONOTONIC, &now);
30 const time_t deadline = now.tv_sec + 3;
31 while (__atomic_load_n(word, __ATOMIC_ACQUIRE) != expected) {
32 if (now.tv_sec >= deadline)
33 return 0;
34 sched_yield();
35 clock_gettime(CLOCK_MONOTONIC, &now);
36 }
37 return 1;
38}
39
40static int reap_code(pid_t child, int expected) {
41 int status = 0;
42 pid_t result;
43 do {
44 result = waitpid(child, &status, 0);
45 } while (result < 0 && errno == EINTR);
46 return result == child && WIFEXITED(status) && WEXITSTATUS(status) == expected;
47}
48
49static int reap(pid_t child) {
50 return reap_code(child, 0);
51}
52
53struct cow_probe {
54 pthread_mutex_t* mutex;
55 int ready;
56 int release;
57 int exited;
58};
59
60static void* cow_owner(void* parameter) {
61 struct cow_probe* probe = parameter;
62 if (pthread_mutex_lock(probe->mutex))
63 _exit(11);
64 probe->exited = (int)syscall(SYS_set_tid_address, &probe->exited);
65 __atomic_store_n(&probe->ready, 1, __ATOMIC_RELEASE);
66 if (!wait_word(&probe->release, 1))
67 _exit(12);
68 syscall(SYS_exit, 0);
69 _exit(13);
70}
71
72static int private_cow_recovery(void) {
73 const size_t page_size = (size_t)sysconf(_SC_PAGESIZE);
74 pthread_mutex_t* mutex =
75 mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
76 if (mutex == MAP_FAILED)
77 return 0;
78 pthread_mutexattr_t attributes;
79 if (pthread_mutexattr_init(&attributes) ||
80 pthread_mutexattr_setpshared(&attributes, PTHREAD_PROCESS_SHARED) ||
81 pthread_mutexattr_setrobust(&attributes, PTHREAD_MUTEX_ROBUST) ||
82 pthread_mutex_init(mutex, &attributes))
83 return 0;
84 pthread_mutexattr_destroy(&attributes);
85 struct cow_probe probe = {mutex, 0, 0, -1};
86 pthread_t owner;
87 if (pthread_create(&owner, NULL, cow_owner, &probe) || !wait_word(&probe.ready, 1))
88 return 0;
89 int ready[2], release[2];
90 if (pipe(ready) || pipe(release))
91 return 0;
92 pid_t child = fork();
93 if (child < 0)
94 return 0;
95 if (!child) {
96 alarm(5);
97 close(ready[0]);
98 close(release[1]);
99 if (pthread_mutex_trylock(mutex) != EBUSY || write(ready[1], "r", 1) != 1)
100 _exit(14);
101 char token;
102 if (read(release[0], &token, 1) != 1 || pthread_mutex_trylock(mutex) != EBUSY)
103 _exit(15);
104 _exit(0);
105 }
106 close(ready[1]);
107 close(release[0]);
108 char token;
109 if (read(ready[0], &token, 1) != 1)
110 return 0;
111 close(ready[0]);
112 // This touches the coordination page after fork, leaving the separate mutex
113 // page COW until kernel owner-death processing writes it.
114 __atomic_store_n(&probe.release, 1, __ATOMIC_RELEASE);
115 int valid = wait_word(&probe.exited, 0);
116 if (valid) {
117 const int locked = pthread_mutex_trylock(mutex);
118 valid = locked == EOWNERDEAD;
119 if (locked == EOWNERDEAD)
120 valid = pthread_mutex_consistent(mutex) == 0 && valid;
121 if (locked == 0 || locked == EOWNERDEAD)
122 valid = pthread_mutex_unlock(mutex) == 0 && valid;
123 }
124 valid = write(release[1], "x", 1) == 1 && valid;
125 close(release[1]);
126 valid = reap(child) && valid;
127 valid = pthread_mutex_destroy(mutex) == 0 && valid;
128 return munmap(mutex, page_size) == 0 && valid;
129}
130
132 uintptr_t next;
133 intptr_t offset;
134 uintptr_t pending;
135};
137 uintptr_t next;
138 uint32_t owner;
139};
141 uintptr_t address;
142 size_t page_size;
143 int fd;
144 int exited;
145 int owner;
146};
147
148static void* demand_owner(void* parameter) {
149 struct demand_probe* probe = parameter;
150 const int tid = (int)syscall(SYS_gettid);
151 struct robust_head head = {probe->address + probe->page_size, offsetof(struct robust_node, owner),
152 0};
153 struct robust_node node = {probe->address, (uint32_t)tid};
154 if (pwrite(probe->fd, &head, sizeof(head), 0) != (ssize_t)sizeof(head) ||
155 pwrite(probe->fd, &node, sizeof(node), (off_t)probe->page_size) != (ssize_t)sizeof(node))
156 _exit(21);
157 probe->owner = tid;
158 probe->exited = (int)syscall(SYS_set_tid_address, &probe->exited);
159 if (syscall(SYS_set_robust_list, probe->address, sizeof(head)))
160 _exit(22);
161 // Neither list page has been touched through this private mapping.
162 syscall(SYS_exit, 0);
163 _exit(23);
164}
165
166static int demand_recovery_case(int protected_head) {
167 const size_t page_size = (size_t)sysconf(_SC_PAGESIZE);
168 char path[80];
169 snprintf(path, sizeof(path), "/tmp/robust-memory-%d", getpid());
170 int fd = open(path, O_CREAT | O_EXCL | O_RDWR, 0600);
171 if (fd < 0 || ftruncate(fd, (off_t)(2 * page_size)))
172 return 0;
173 uintptr_t address =
174 (uintptr_t)mmap(NULL, 2 * page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
175 if ((void*)address == MAP_FAILED)
176 return 0;
177 if (protected_head && mprotect((void*)address, page_size, PROT_NONE))
178 return 0;
179 struct demand_probe probe = {address, page_size, fd, -1, 0};
180 pthread_t owner;
181 if (pthread_create(&owner, NULL, demand_owner, &probe) || !wait_word(&probe.exited, 0))
182 return 0;
183 struct robust_node* node = (struct robust_node*)(address + page_size);
184 const uint32_t expected = protected_head ? (uint32_t)probe.owner : UINT32_C(0x40000000);
185 const int valid = __atomic_load_n(&node->owner, __ATOMIC_ACQUIRE) == expected;
186 return munmap((void*)address, 2 * page_size) == 0 && close(fd) == 0 && unlink(path) == 0 && valid;
187}
188
189static int demand_recovery(void) {
190 return demand_recovery_case(0);
191}
192
193static int protected_list(void) {
194 return demand_recovery_case(1);
195}
196
198 int* word;
199 int ready;
200 int release;
201};
202
203static void* ctid_owner(void* parameter) {
204 struct ctid_probe* probe = parameter;
205 *probe->word = (int)syscall(SYS_set_tid_address, probe->word);
206 __atomic_store_n(&probe->ready, 1, __ATOMIC_RELEASE);
207 if (!wait_word(&probe->release, 1))
208 _exit(31);
209 syscall(SYS_exit, 0);
210 _exit(32);
211}
212
213static int clear_tid_cow(void) {
214 const size_t page_size = (size_t)sysconf(_SC_PAGESIZE);
215 int* word = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
216 if (word == MAP_FAILED)
217 return 0;
218 struct ctid_probe probe = {word, 0, 0};
219 pthread_t owner;
220 if (pthread_create(&owner, NULL, ctid_owner, &probe) || !wait_word(&probe.ready, 1))
221 return 0;
222 int release[2];
223 if (pipe(release))
224 return 0;
225 const int tid = *word;
226 pid_t child = fork();
227 if (child < 0)
228 return 0;
229 if (!child) {
230 alarm(5);
231 close(release[1]);
232 char token;
233 if (read(release[0], &token, 1) != 1 || *word != tid)
234 _exit(33);
235 _exit(0);
236 }
237 close(release[0]);
238 __atomic_store_n(&probe.release, 1, __ATOMIC_RELEASE);
239 // No parent write touches the dedicated TID page after fork.
240 int valid = tid > 0 && wait_word(word, 0);
241 valid = write(release[1], "x", 1) == 1 && valid;
242 close(release[1]);
243 valid = reap(child) && valid;
244 return munmap(word, page_size) == 0 && valid;
245}
246
247static int exec_recovery(void) {
248 const size_t page_size = (size_t)sysconf(_SC_PAGESIZE);
249 char path[80];
250 snprintf(path, sizeof(path), "/tmp/robust-exec-%d", getpid());
251 int fd = open(path, O_CREAT | O_EXCL | O_RDWR, 0600);
252 if (fd < 0 || ftruncate(fd, (off_t)page_size))
253 return 0;
254 pthread_mutex_t* mutex = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
255 if (mutex == MAP_FAILED)
256 return 0;
257 pthread_mutexattr_t attributes;
258 if (pthread_mutexattr_init(&attributes) ||
259 pthread_mutexattr_setpshared(&attributes, PTHREAD_PROCESS_SHARED) ||
260 pthread_mutexattr_setrobust(&attributes, PTHREAD_MUTEX_ROBUST) ||
261 pthread_mutex_init(mutex, &attributes))
262 return 0;
263 pthread_mutexattr_destroy(&attributes);
264 pid_t child = fork();
265 if (child < 0)
266 return 0;
267 if (!child) {
268 if (pthread_mutex_lock(mutex))
269 _exit(41);
270 uintptr_t before = 0, after = 0;
271 size_t length = 0;
272 if (syscall(SYS_get_robust_list, 0, &before, &length) || !before)
273 _exit(42);
274 char* const arguments[] = {(char*)executable, (char*)"--exec-shebang-unexpected-child", NULL};
275 execv("/__pedigree_missing_robust_exec__", arguments);
276 if (errno != ENOENT || syscall(SYS_get_robust_list, 0, &after, &length) || before != after)
277 _exit(43);
278 execv(executable, arguments);
279 _exit(44);
280 }
281 int valid = reap_code(child, 120);
282 const int locked = pthread_mutex_trylock(mutex);
283 valid = locked == EOWNERDEAD && valid;
284 if (locked == EOWNERDEAD)
285 valid = pthread_mutex_consistent(mutex) == 0 && valid;
286 if (locked == 0 || locked == EOWNERDEAD)
287 valid = pthread_mutex_unlock(mutex) == 0 && valid;
288 valid = pthread_mutex_destroy(mutex) == 0 && valid;
289 return munmap(mutex, page_size) == 0 && close(fd) == 0 && unlink(path) == 0 && valid;
290}
291
292static int run_case(const char* name, int (*operation)(void)) {
293 pid_t child = fork();
294 if (child < 0)
295 return 0;
296 if (!child) {
297 alarm(5);
298 _exit(operation() ? 0 : 1);
299 }
300 const int passed = reap(child);
301 printf("ROBUST-MEMORY-CONTRACT: %s %s\n", passed ? "PASS" : "FAIL", name);
302 fflush(stdout);
303 return passed;
304}
305
306void test_robust_memory_contracts(const char* program) {
307 executable = program;
308 const int cow = run_case("private-cow-isolation", private_cow_recovery);
309 const int demand = run_case("demand-head-and-link", demand_recovery);
310 const int protected = run_case("protected-head-preservation", protected_list);
311 const int ctid = run_case("clear-tid-cow-isolation", clear_tid_cow);
312 const int exec = run_case("exec-owner-recovery", exec_recovery);
313 if (!cow || !demand || !protected || !ctid || !exec)
314 fail();
315 puts("ROBUST-MEMORY-CONTRACT: PASS all");
316}