The Pedigree Project 0.1
semaphores.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 <pthread.h>
11#include <sched.h>
12#include <signal.h>
13#include <stddef.h>
14#include <stdint.h>
15#include <stdio.h>
16#include <string.h>
17#include <time.h>
18#include <unistd.h>
19
20#include <sys/mman.h>
21#include <sys/sem.h>
22#include <sys/syscall.h>
23#include <sys/wait.h>
24
26 int val;
27 struct semid_ds* buf;
28 unsigned short* array;
29 struct seminfo* info;
30};
31
32#define CHECK(expression) \
33 do { \
34 if (!(expression)) { \
35 printf("IPC-SEM: line %d failed: %s (errno=%d)\n", __LINE__, #expression, errno); \
36 goto failed; \
37 } \
38 } while (0)
39
40static int reap(pid_t child) {
41 int status;
42 pid_t result;
43 do {
44 result = waitpid(child, &status, 0);
45 } while (result < 0 && errno == EINTR);
46 if (result == child && WIFEXITED(status) && !WEXITSTATUS(status))
47 return 1;
48 printf("IPC-SEM: child %d returned %d status=%#x errno=%d\n", child, result,
49 result == child ? status : 0, errno);
50 return 0;
51}
52
53static int wait_value(int id, int command, int value) {
54 struct timespec start, now, pause = {0, 1000000};
55 if (clock_gettime(CLOCK_MONOTONIC, &start))
56 return 0;
57 do {
58 int result = semctl(id, 0, command);
59 if (result == value)
60 return 1;
61 if (result < 0 || clock_gettime(CLOCK_MONOTONIC, &now))
62 return 0;
63 nanosleep(&pause, NULL);
64 } while (now.tv_sec - start.tv_sec < 3);
65 return 0;
66}
67
68static void interrupted(int signal_number) {
69 (void)signal_number;
70}
71
72static int waiter_contract(int id, int zero, int expected_error, int remove_set) {
73 pid_t child = fork();
74 if (child < 0)
75 return 0;
76 if (!child) {
77 alarm(5);
78 struct sigaction action = {.sa_handler = interrupted};
79 sigemptyset(&action.sa_mask);
80 if (sigaction(SIGUSR1, &action, NULL))
81 _exit(10);
82 struct sembuf operation = {0, zero ? 0 : -1, 0};
83 const int result = semop(id, &operation, 1);
84 _exit(expected_error ? !(result == -1 && errno == expected_error) : result != 0);
85 }
86 const int command = zero ? GETZCNT : GETNCNT;
87 int ok = wait_value(id, command, 1);
88 if (ok) {
89 if (remove_set)
90 ok = semctl(id, 0, IPC_RMID) == 0;
91 else if (expected_error == EINTR)
92 ok = kill(child, SIGUSR1) == 0;
93 else
94 ok = semctl(id, 0, SETVAL, (union test_semun){.val = zero ? 0 : 1}) == 0;
95 }
96 if (!ok)
97 kill(child, SIGKILL);
98 ok = reap(child) && ok;
99 if (!remove_set && semctl(id, 0, command) != 0)
100 ok = 0;
101 return ok;
102}
103
104static void* undo_worker(void* argument) {
105 struct sembuf operation = {0, -1, SEM_UNDO};
106 return (void*)(intptr_t)semop(*(int*)argument, &operation, 1);
107}
108
109struct raw_undo {
110 int id;
111 int tid;
112 int result;
113 struct sembuf operation;
114};
115
116_Static_assert(offsetof(struct raw_undo, tid) == 4, "clone child TID offset");
117_Static_assert(offsetof(struct raw_undo, result) == 8, "semop result offset");
118_Static_assert(offsetof(struct raw_undo, operation) == 12, "semop operation offset");
119
120// musl's public clone rejects CLONE_THREAD. This child has no libc thread state,
121// so it executes only semop and exit directly, without entering C or touching TLS.
122long ipc_sem_raw_clone(void* stack, struct raw_undo* state);
123#define ASM_VALUE_INNER(value) #value
124#define ASM_VALUE(value) ASM_VALUE_INNER(value)
125__asm__(".text\n"
126 ".global ipc_sem_raw_clone\n"
127 ".hidden ipc_sem_raw_clone\n"
128 ".type ipc_sem_raw_clone,@function\n"
129 "ipc_sem_raw_clone:\n"
130 "mov %rsi,%r9\n"
131 "mov %rdi,%rsi\n"
132 "and $-16,%rsi\n"
133 "mov $(" ASM_VALUE(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD |
134 CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID) "),%edi\n"
135 "lea 4(%r9),%r10\n"
136 "xor %edx,%edx\n"
137 "xor %r8d,%r8d\n"
138 "mov $" ASM_VALUE(SYS_clone) ",%eax\n"
139 "syscall\n"
140 "test %rax,%rax\n"
141 "jnz 1f\n"
142 "mov (%r9),%edi\n"
143 "lea 12(%r9),%rsi\n"
144 "mov $1,%edx\n"
145 "mov $" ASM_VALUE(SYS_semop) ",%eax\n"
146 "syscall\n"
147 "mov %eax,8(%r9)\n"
148 "xor %edi,%edi\n"
149 "mov $" ASM_VALUE(SYS_exit) ",%eax\n"
150 "syscall\n"
151 "ud2\n"
152 "1: ret\n"
153 ".size ipc_sem_raw_clone,.-ipc_sem_raw_clone\n");
154#undef ASM_VALUE
155#undef ASM_VALUE_INNER
156
157static int undo_contracts(int id) {
158 // A pthread shares undo state: joining it must leave its adjustment pending.
159 pid_t child = fork();
160 if (child < 0)
161 return 0;
162 if (!child) {
163 alarm(5);
164 pthread_t thread;
165 void* result;
166 if (pthread_create(&thread, NULL, undo_worker, &id) || pthread_join(thread, &result) ||
167 result || semctl(id, 0, GETVAL) != 0)
168 _exit(11);
169 _exit(0);
170 }
171 if (!reap(child) || semctl(id, 0, GETVAL) != 1)
172 return 0;
173
174 struct sembuf operation = {0, -1, SEM_UNDO};
175 if (semop(id, &operation, 1))
176 return 0;
177 child = fork();
178 if (child < 0)
179 return 0;
180 if (!child) {
181 alarm(5);
182 _exit(0);
183 }
184 if (!reap(child) || semctl(id, 0, GETVAL) != 0 ||
185 semctl(id, 0, SETVAL, (union test_semun){.val = 1}))
186 return 0;
187
188 // Omitting CLONE_SYSVSEM gives a thread an independent undo lifetime.
189 child = fork();
190 if (child < 0)
191 return 0;
192 if (!child) {
193 alarm(5);
194 const size_t size = 65536;
195 void* stack = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
196 if (stack == MAP_FAILED)
197 _exit(12);
198 struct raw_undo state = {id, -1, -1, {0, -1, SEM_UNDO}};
199 const long tid = ipc_sem_raw_clone((char*)stack + size, &state);
200 if (tid < 0) {
201 printf("IPC-SEM: independent undo clone failed: %ld\n", tid);
202 _exit(13);
203 }
204 while (__atomic_load_n(&state.tid, __ATOMIC_ACQUIRE))
205 sched_yield();
206 const int value = semctl(id, 0, GETVAL);
207 if (state.result || value != 1) {
208 printf("IPC-SEM: independent undo semop=%d value=%d\n", state.result, value);
209 _exit(14);
210 }
211 munmap(stack, size);
212 _exit(0);
213 }
214 if (!reap(child) || semctl(id, 0, GETVAL) != 1)
215 return 0;
216
217 // SETVAL clears every process's outstanding undo adjustment for that entry.
218 int ready[2], release[2];
219 if (pipe(ready))
220 return 0;
221 if (pipe(release)) {
222 close(ready[0]);
223 close(ready[1]);
224 return 0;
225 }
226 child = fork();
227 if (child < 0) {
228 close(ready[0]);
229 close(ready[1]);
230 close(release[0]);
231 close(release[1]);
232 return 0;
233 }
234 if (!child) {
235 alarm(5);
236 close(ready[0]);
237 close(release[1]);
238 char token = 0;
239 if (semop(id, &operation, 1) || write(ready[1], &token, 1) != 1 ||
240 read(release[0], &token, 1) != 1)
241 _exit(15);
242 _exit(0);
243 }
244 close(ready[1]);
245 close(release[0]);
246 char token;
247 int ok =
248 read(ready[0], &token, 1) == 1 && semctl(id, 0, SETVAL, (union test_semun){.val = 4}) == 0;
249 close(ready[0]);
250 if (ok)
251 ok = write(release[1], &token, 1) == 1;
252 close(release[1]);
253 if (!ok)
254 kill(child, SIGKILL);
255 return reap(child) && ok && semctl(id, 0, GETVAL) == 4;
256}
257
258int ipc_test_semaphores(void) {
259 int id = -1, keyed = -1;
260 id = semget(IPC_PRIVATE, 3, 0600);
261 CHECK(id >= 0);
262 struct semid_ds metadata;
263 CHECK(semctl(id, 0, IPC_STAT, (union test_semun){.buf = &metadata}) == 0);
264 CHECK(metadata.sem_nsems == 3 && !metadata.sem_otime && metadata.sem_ctime > 0);
265 CHECK(metadata.sem_perm.uid == geteuid() && (metadata.sem_perm.mode & 0777) == 0600);
266 struct seminfo information;
267 const int highest = semctl(0, 0, IPC_INFO, (union test_semun){.info = &information});
268 CHECK(highest >= 0 && information.semmsl >= 3 && information.semvmx == 32767 &&
269 information.semopm >= 3);
270 int found = 0;
271 for (int index = 0; index <= highest; ++index) {
272 if (semctl(index, 0, SEM_STAT, (union test_semun){.buf = &metadata}) == id) {
273 CHECK(metadata.sem_nsems == 3);
274 CHECK(semctl(index, 0, SEM_STAT_ANY, (union test_semun){.buf = &metadata}) == id);
275 found = 1;
276 break;
277 }
278 }
279 CHECK(found);
280 CHECK(semctl(0, 0, SEM_INFO, (union test_semun){.info = &information}) >= 0 &&
281 information.semusz >= 1 && information.semaem >= 3);
282
283 key_t key = (key_t)(0x53450000u | (getpid() & 0xffff));
284 keyed = semget(key, 2, IPC_CREAT | IPC_EXCL | 0600);
285 CHECK(keyed >= 0);
286 CHECK(semget(key, 0, 0600) == keyed);
287 errno = 0;
288 CHECK(semget(key, 2, IPC_CREAT | IPC_EXCL | 0600) == -1 && errno == EEXIST);
289 errno = 0;
290 CHECK(semget(key, 3, 0600) == -1 && errno == EINVAL);
291 CHECK(semctl(keyed, 0, IPC_RMID) == 0);
292 keyed = -1;
293 errno = 0;
294 CHECK(semget(key, 0, 0600) == -1 && errno == ENOENT);
295
296 unsigned short values[3] = {99, 99, 99};
297 CHECK(semctl(id, 0, GETALL, (union test_semun){.array = values}) == 0);
298 CHECK(!values[0] && !values[1] && !values[2]);
299 values[0] = 2;
300 values[2] = 1;
301 CHECK(semctl(id, 0, SETALL, (union test_semun){.array = values}) == 0);
302 CHECK(semctl(id, 0, GETPID) == getpid());
303 values[0] = 9;
304 values[1] = 65535;
305 errno = 0;
306 CHECK(semctl(id, 0, SETALL, (union test_semun){.array = values}) == -1 && errno == ERANGE);
307 CHECK(semctl(id, 0, GETVAL) == 2 && semctl(id, 1, GETVAL) == 0);
308
309 struct sembuf failed_pair[2] = {{0, -1, 0}, {1, -1, IPC_NOWAIT}};
310 errno = 0;
311 CHECK(semop(id, failed_pair, 2) == -1 && errno == EAGAIN);
312 CHECK(semctl(id, 0, GETVAL) == 2);
313 struct sembuf duplicate[3] = {{0, -1, 0}, {0, -1, 0}, {1, 2, 0}};
314 CHECK(semop(id, duplicate, 3) == 0);
315 CHECK(semctl(id, 0, GETVAL) == 0 && semctl(id, 1, GETVAL) == 2);
316 struct sembuf out_of_range = {3, 1, IPC_NOWAIT};
317 errno = 0;
318 CHECK(semop(id, &out_of_range, 1) == -1 && errno == EFBIG);
319 errno = 0;
320 CHECK(semop(id, duplicate, 0) == -1 && errno == EINVAL);
321 struct sembuf zero = {0, 0, 0};
322 CHECK(semop(id, &zero, 1) == 0);
323 CHECK(semctl(id, 0, IPC_STAT, (union test_semun){.buf = &metadata}) == 0 &&
324 metadata.sem_otime > 0);
325 metadata.sem_perm.mode = 0640;
326 CHECK(semctl(id, 0, IPC_SET, (union test_semun){.buf = &metadata}) == 0);
327 CHECK(semctl(id, 0, IPC_STAT, (union test_semun){.buf = &metadata}) == 0 &&
328 (metadata.sem_perm.mode & 0777) == 0640);
329
330 struct timespec duration = {0, 20000000};
331 const struct timespec original = duration;
332 struct sembuf timed = {2, -2, 0};
333 errno = 0;
334 CHECK(semtimedop(id, &timed, 1, &duration) == -1 && errno == EAGAIN);
335 CHECK(!memcmp(&duration, &original, sizeof(duration)) && semctl(id, 2, GETVAL) == 1);
336 duration.tv_nsec = 1000000000;
337 errno = 0;
338 CHECK(semtimedop(id, &timed, 1, &duration) == -1 && errno == EINVAL);
339 duration = (struct timespec){0, 0};
340 errno = 0;
341 CHECK(semtimedop(id, &timed, 1, &duration) == -1 && errno == EAGAIN);
342
343 CHECK(waiter_contract(id, 0, 0, 0));
344 CHECK(semctl(id, 0, SETVAL, (union test_semun){.val = 1}) == 0);
345 CHECK(waiter_contract(id, 1, 0, 0));
346 CHECK(waiter_contract(id, 0, EINTR, 0));
347 CHECK(semctl(id, 0, SETVAL, (union test_semun){.val = 1}) == 0);
348 CHECK(undo_contracts(id));
349
350 if (!geteuid()) {
351 pid_t child = fork();
352 CHECK(child >= 0);
353 if (!child) {
354 alarm(5);
355 if (setgid(65534) || setuid(65534))
356 _exit(16);
357 errno = 0;
358 if (semctl(id, 0, GETVAL) != -1 || errno != EACCES)
359 _exit(17);
360 errno = 0;
361 _exit(!(semctl(id, 0, IPC_RMID) == -1 && errno == EPERM));
362 }
363 CHECK(reap(child));
364 }
365 CHECK(semctl(id, 0, SETVAL, (union test_semun){.val = 0}) == 0);
366 CHECK(waiter_contract(id, 0, EIDRM, 1));
367 errno = 0;
368 CHECK(semctl(id, 0, GETVAL) == -1 && (errno == EINVAL || errno == EIDRM));
369 id = -1;
370 puts("IPC-SEM: values, atomic vectors, waits, metadata, removal and undo passed");
371 return 0;
372
373failed:
374 if (keyed >= 0)
375 semctl(keyed, 0, IPC_RMID);
376 if (id >= 0)
377 semctl(id, 0, IPC_RMID);
378 return -1;
379}