The Pedigree Project 0.1
futex-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 <limits.h>
12#include <pthread.h>
13#include <sched.h>
14#include <signal.h>
15#include <stdint.h>
16#include <stdio.h>
17#include <string.h>
18#include <time.h>
19#include <unistd.h>
20
21#include <sys/mman.h>
22#include <sys/syscall.h>
23#include <sys/wait.h>
24
25extern void fail(void) __attribute__((noreturn));
26extern void test_robust_memory_contracts(const char* program);
27
28struct shared_fixture {
29 int futex;
30 pthread_mutex_t mutex;
31};
32struct identity {
33 long tid;
34 int valid;
35};
36
37static void* identity_worker(void* parameter) {
38 struct identity* identity = parameter;
39 identity->tid = syscall(SYS_gettid);
40 identity->valid = identity->tid > 0 && syscall(SYS_tgkill, getpid(), identity->tid, 0) == 0;
41 return NULL;
42}
43
44static int reap_exit_code(pid_t child) {
45 int status = 0;
46 pid_t result;
47 do {
48 result = waitpid(child, &status, 0);
49 } while (result < 0 && errno == EINTR);
50 if (result != child || !WIFEXITED(status)) {
51 printf("FUTEX-CONTRACT: reap child=%d result=%d status=%#x errno=%d\n", child, result, status,
52 errno);
53 fflush(stdout);
54 return -1;
55 }
56 return WEXITSTATUS(status);
57}
58
59static int reap(pid_t child) {
60 int code = reap_exit_code(child);
61 if (code > 0) {
62 printf("FUTEX-CONTRACT: child=%d exit=%d\n", child, code);
63 fflush(stdout);
64 }
65 return code == 0;
66}
67
68struct exit_probe {
69 int ready;
70 int release;
71};
72
73static void* exit_race_worker(void* parameter) {
74 struct exit_probe* probe = parameter;
75 __atomic_add_fetch(&probe->ready, 1, __ATOMIC_RELEASE);
76 while (!__atomic_load_n(&probe->release, __ATOMIC_ACQUIRE))
77 sched_yield();
78 return NULL;
79}
80
81static void* raw_exit_worker(void* parameter) {
82 exit_race_worker(parameter);
83 syscall(SYS_exit, 43);
84 _exit(90);
85}
86
87static int raw_exit_contracts(void) {
88 pid_t child = fork();
89 if (child < 0)
90 return 0;
91 if (!child) {
92 syscall(SYS_exit, 37);
93 _exit(90);
94 }
95 if (reap_exit_code(child) != 37)
96 return 0;
97 puts("FUTEX-CONTRACT: raw single-thread exit status passed");
98 fflush(stdout);
99
100 for (size_t attempt = 0; attempt < 4; ++attempt) {
101 child = fork();
102 if (child < 0)
103 return 0;
104 if (!child) {
105 alarm(5);
106 struct exit_probe early = {0};
107 struct exit_probe late = {0};
108 pthread_t workers[4];
109 for (size_t i = 0; i < 2; ++i) {
110 if (pthread_create(&workers[i], NULL, exit_race_worker, &early))
111 _exit(91);
112 }
113 while (__atomic_load_n(&early.ready, __ATOMIC_ACQUIRE) != 2)
114 sched_yield();
115 __atomic_store_n(&early.release, 1, __ATOMIC_RELEASE);
116 // Normal pthread cleanup keeps musl's thread list coherent while these
117 // exits overlap new clone publication.
118 for (size_t i = 2; i < 4; ++i) {
119 if (pthread_create(&workers[i], NULL, raw_exit_worker, &late))
120 _exit(92);
121 }
122 for (size_t i = 0; i < 2; ++i) {
123 if (pthread_join(workers[i], NULL))
124 _exit(93);
125 }
126 while (__atomic_load_n(&late.ready, __ATOMIC_ACQUIRE) != 2)
127 sched_yield();
128 // No pthread operations follow these raw exits, which intentionally
129 // bypass musl's own detach-state and thread-list cleanup.
130 __atomic_store_n(&late.release, 1, __ATOMIC_RELEASE);
131 syscall(SYS_exit, 42);
132 _exit(90);
133 }
134 const int code = reap_exit_code(child);
135 if (code != 42 && code != 43) {
136 printf("FUTEX-CONTRACT: concurrent raw exit status=%d attempt=%zu\n", code, attempt);
137 fflush(stdout);
138 return 0;
139 }
140 }
141 puts("FUTEX-CONTRACT: concurrent exits and clone publication passed");
142 fflush(stdout);
143 return 1;
144}
145
146static int shared_requeue(int first_fd, struct shared_fixture* first, struct shared_fixture* second,
147 size_t page_size) {
148 int ready[2];
149 if (pipe(ready))
150 return 0;
151 pid_t child = fork();
152 if (child < 0)
153 return 0;
154 if (!child) {
155 close(ready[0]);
156 alarm(5);
157 struct shared_fixture* alias =
158 mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_SHARED, first_fd, 0);
159 if (alias == MAP_FAILED || alias == first || munmap(first, page_size) ||
160 syscall(SYS_gettid) != getpid())
161 _exit(11);
162 if (write(ready[1], "r", 1) != 1)
163 _exit(12);
164 long result;
165 do {
166 result = syscall(SYS_futex, &alias->futex, 0, 0, NULL, NULL, 0);
167 } while (result < 0 && errno == EINTR);
168 _exit(result == 0 ? 0 : 13);
169 }
170 close(ready[1]);
171 char token = 0;
172 int valid = read(ready[0], &token, 1) == 1 && token == 'r';
173 close(ready[0]);
174 struct timespec now;
175 clock_gettime(CLOCK_MONOTONIC, &now);
176 const time_t deadline = now.tv_sec + 3;
177 long moved = 0;
178 while (valid && moved == 0 && now.tv_sec < deadline) {
179 moved = syscall(SYS_futex, &first->futex, 3, 0, 1, &second->futex, 0);
180 if (!moved)
181 sched_yield();
182 clock_gettime(CLOCK_MONOTONIC, &now);
183 }
184 valid = valid && moved == 1 &&
185 syscall(SYS_futex, &first->futex, 1, INT_MAX, NULL, NULL, 0) == 0 &&
186 syscall(SYS_futex, &second->futex, 1, 1, NULL, NULL, 0) == 1;
187 printf("FUTEX-CONTRACT: shared-requeue moved=%ld valid=%d\n", moved, valid);
188 fflush(stdout);
189 if (!valid)
190 kill(child, SIGKILL);
191 return reap(child) && valid;
192}
193
194static int robust_owner_exit(int fd, struct shared_fixture* shared, size_t page_size) {
195 pthread_mutexattr_t attributes;
196 if (pthread_mutexattr_init(&attributes) ||
197 pthread_mutexattr_setpshared(&attributes, PTHREAD_PROCESS_SHARED) ||
198 pthread_mutexattr_setrobust(&attributes, PTHREAD_MUTEX_ROBUST) ||
199 pthread_mutex_init(&shared->mutex, &attributes))
200 return 0;
201 pthread_mutexattr_destroy(&attributes);
202 int ready[2];
203 int release[2];
204 if (pipe(ready) || pipe(release))
205 return 0;
206 pid_t child = fork();
207 if (child < 0)
208 return 0;
209 if (!child) {
210 close(ready[0]);
211 close(release[1]);
212 alarm(5);
213 struct shared_fixture* alias = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
214 if (alias == MAP_FAILED || alias == shared || munmap(shared, page_size) ||
215 syscall(SYS_gettid) != getpid() || pthread_mutex_lock(&alias->mutex))
216 _exit(21);
217 if (write(ready[1], "r", 1) != 1)
218 _exit(22);
219 char token;
220 if (read(release[0], &token, 1) != 1)
221 _exit(23);
222 puts("FUTEX-CONTRACT: robust owner exiting");
223 fflush(stdout);
224 // Raw thread exit exercises the kernel's registered robust-list cleanup.
225 syscall(SYS_exit, 0);
226 _exit(24);
227 }
228 close(ready[1]);
229 close(release[0]);
230 char token = 0;
231 int valid = read(ready[0], &token, 1) == 1 && token == 'r';
232 close(ready[0]);
233 valid = valid && write(release[1], "x", 1) == 1;
234 close(release[1]);
235 if (!valid) {
236 kill(child, SIGKILL);
237 reap(child);
238 return 0;
239 }
240 puts("FUTEX-CONTRACT: robust recovery begin");
241 fflush(stdout);
242 int locked = pthread_mutex_lock(&shared->mutex);
243 printf("FUTEX-CONTRACT: robust recovery result=%d expected=%d\n", locked, EOWNERDEAD);
244 fflush(stdout);
245 valid = locked == EOWNERDEAD;
246 if (locked == EOWNERDEAD)
247 valid = pthread_mutex_consistent(&shared->mutex) == 0 && valid;
248 if (locked == 0 || locked == EOWNERDEAD)
249 valid = pthread_mutex_unlock(&shared->mutex) == 0 && valid;
250 puts("FUTEX-CONTRACT: robust owner reap begin");
251 fflush(stdout);
252 valid = reap(child) && valid;
253 printf("FUTEX-CONTRACT: robust owner reap complete valid=%d\n", valid);
254 fflush(stdout);
255 return pthread_mutex_destroy(&shared->mutex) == 0 && valid;
256}
257
258static int contracts_child(void) {
259 alarm(15);
260 if (!raw_exit_contracts())
261 return 8;
262 const long main_tid = syscall(SYS_gettid);
263 if (main_tid != getpid())
264 return 1;
265 struct identity identities[2] = {{0}};
266 for (size_t i = 0; i < 2; ++i) {
267 pthread_t thread;
268 if (pthread_create(&thread, NULL, identity_worker, &identities[i]) ||
269 pthread_join(thread, NULL))
270 return 2;
271 if (!identities[i].valid || identities[i].tid == main_tid)
272 return 3;
273 }
274 if (identities[0].tid == identities[1].tid)
275 return 4;
276 puts("FUTEX-CONTRACT: task identities passed");
277 fflush(stdout);
278 const size_t page_size = (size_t)sysconf(_SC_PAGESIZE);
279 char paths[2][80];
280 int files[2];
281 struct shared_fixture* mappings[2];
282 for (size_t i = 0; i < 2; ++i) {
283 snprintf(paths[i], sizeof(paths[i]), "/tmp/futex-contract-%d-%zu", getpid(), i);
284 files[i] = open(paths[i], O_CREAT | O_TRUNC | O_RDWR, 0600);
285 if (files[i] < 0 || ftruncate(files[i], (off_t)page_size))
286 return 5;
287 mappings[i] = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_SHARED, files[i], 0);
288 if (mappings[i] == MAP_FAILED)
289 return 6;
290 memset(mappings[i], 0, sizeof(*mappings[i]));
291 }
292 int valid = shared_requeue(files[0], mappings[0], mappings[1], page_size);
293 valid = robust_owner_exit(files[0], mappings[0], page_size) && valid;
294 for (size_t i = 0; i < 2; ++i) {
295 valid = munmap(mappings[i], page_size) == 0 && valid;
296 valid = close(files[i]) == 0 && valid;
297 valid = unlink(paths[i]) == 0 && valid;
298 }
299 return valid ? 0 : 7;
300}
301
302void test_futex_contracts(const char* program) {
303 pid_t child = fork();
304 if (!child)
305 _exit(contracts_child());
306 if (child < 0 || !reap(child)) {
307 puts("FUTEX-CONTRACT: FAIL task-ids-shared-requeue-robust-exit");
308 fail();
309 }
310 puts("FUTEX-CONTRACT: PASS task-ids-shared-requeue-robust-exit");
311 test_robust_memory_contracts(program);
312}