The Pedigree Project 0.1
namespaces.c
1/* Copyright (c) 2026, Pedigree Developers. */
2#define _GNU_SOURCE
3#include <errno.h>
4#include <fcntl.h>
5#include <mqueue.h>
6#include <sched.h>
7#include <signal.h>
8#include <stddef.h>
9#include <stdint.h>
10#include <stdio.h>
11#include <stdlib.h>
12#include <string.h>
13#include <unistd.h>
14
15#include <arpa/inet.h>
16#include <linux/rtnetlink.h>
17#include <net/if.h>
18#include <sys/mount.h>
19#include <sys/msg.h>
20#include <sys/sem.h>
21#include <sys/shm.h>
22#include <sys/socket.h>
23#include <sys/stat.h>
24#include <sys/syscall.h>
25#include <sys/un.h>
26#include <sys/wait.h>
27
28#define CHECK(expression) \
29 do { \
30 if (!(expression)) { \
31 fprintf(stderr, "NAMESPACE-CONTRACT: FAIL line=%d %s errno=%d\n", __LINE__, #expression, \
32 errno); \
33 return 1; \
34 } \
35 } while (0)
36
37static pid_t clone_process(unsigned long flags) {
38 return syscall(SYS_clone, flags | SIGCHLD, NULL, NULL, NULL, 0);
39}
40
41static int reap(pid_t child) {
42 int status = 0;
43 CHECK(child > 0 && waitpid(child, &status, 0) == child);
44 CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0);
45 return 0;
46}
47
48static int pid_child(int report, int ready_read, int ready_write, pid_t outside) {
49 CHECK(getpid() == 1 && syscall(SYS_gettid) == 1 && getppid() == 0);
50 errno = 0;
51 CHECK(kill(outside, 0) == -1 && errno == ESRCH);
52 pid_t worker = fork();
53 CHECK(worker >= 0);
54 if (!worker) {
55 CHECK(getpid() == 2 && syscall(SYS_gettid) == 2 && getppid() == 1);
56 /* The inherited proc mount still reports IDs in the parent's namespace. */
57 char identity[32] = {0};
58 ssize_t length = readlink("/proc/self", identity, sizeof(identity) - 1);
59 CHECK(length > 0);
60 pid_t outer = strtol(identity, NULL, 10);
61 CHECK(outer > 2 && write(report, &outer, sizeof(outer)) == sizeof(outer));
62 CHECK(write(ready_write, "x", 1) == 1);
63 for (;;) {
64 pause();
65 }
66 }
67 CHECK(worker == 2);
68 char ready;
69 CHECK(read(ready_read, &ready, 1) == 1);
70 /* Exiting PID 1 must kill its still-running descendant before wait returns. */
71 return 0;
72}
73
74static int pending_pid_namespace(void) {
75 pid_t before = getpid();
76 CHECK(unshare(CLONE_NEWPID) == 0 && getpid() == before);
77 pid_t child = fork();
78 CHECK(child >= 0);
79 if (!child) {
80 _exit(getpid() == 1 && syscall(SYS_gettid) == 1 && getppid() == 0 ? 0 : 1);
81 }
82 CHECK(!reap(child));
83 errno = 0;
84 CHECK(fork() == -1 && errno == ENOMEM);
85 return 0;
86}
87
88static int pid_namespace(void) {
89 const pid_t before = getpid();
90 int report[2], ready[2];
91 CHECK(pipe(report) == 0 && pipe(ready) == 0);
92 pid_t child = clone_process(CLONE_NEWPID);
93 CHECK(child >= 0);
94 if (!child) {
95 _exit(pid_child(report[1], ready[0], ready[1], before));
96 }
97 close(report[1]);
98 close(ready[0]);
99 close(ready[1]);
100 pid_t descendant = 0;
101 CHECK(read(report[0], &descendant, sizeof(descendant)) == sizeof(descendant));
102 close(report[0]);
103 CHECK(!reap(child));
104 CHECK(getpid() == before && syscall(SYS_gettid) == before);
105 errno = 0;
106 CHECK(kill(descendant, 0) == -1 && errno == ESRCH);
107 child = fork();
108 CHECK(child >= 0);
109 if (!child) {
110 _exit(pending_pid_namespace());
111 }
112 CHECK(!reap(child));
113 puts("NAMESPACE-CONTRACT: pid PASS");
114 return 0;
115}
116
117struct message {
118 long type;
119 char value;
120};
121
122static int ipc_child(key_t key, int parent_sem, int parent_shm, int parent_msg,
123 int* inherited_mapping, const char* name) {
124 errno = 0;
125 CHECK(semget(key, 1, 0) == -1 && errno == ENOENT);
126 errno = 0;
127 CHECK(semctl(parent_sem, 0, GETVAL) == -1 && errno == EINVAL);
128 errno = 0;
129 CHECK(shmget(key, 4096, 0) == -1 && errno == ENOENT);
130 errno = 0;
131 CHECK(shmat(parent_shm, NULL, 0) == (void*)-1 && errno == EINVAL);
132 errno = 0;
133 CHECK(msgget(key, 0) == -1 && errno == ENOENT);
134 struct msqid_ds status;
135 errno = 0;
136 CHECK(msgctl(parent_msg, IPC_STAT, &status) == -1 && errno == EINVAL);
137 errno = 0;
138 CHECK(mq_open(name, O_RDONLY) == (mqd_t)-1 && errno == ENOENT);
139
140 int sem = semget(key, 1, IPC_CREAT | IPC_EXCL | 0600);
141 int shm = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0600);
142 int msg = msgget(key, IPC_CREAT | IPC_EXCL | 0600);
143 CHECK(sem >= 0 && shm >= 0 && msg >= 0);
144 CHECK(semctl(sem, 0, SETVAL, 7) == 0 && semctl(sem, 0, GETVAL) == 7);
145 int* mapping = shmat(shm, NULL, 0);
146 CHECK(mapping != (void*)-1);
147 *mapping = 8;
148 CHECK(*inherited_mapping == 17 && shmdt(mapping) == 0);
149 struct mq_attr attr = {.mq_maxmsg = 2, .mq_msgsize = 32};
150 mqd_t mq = mq_open(name, O_CREAT | O_EXCL | O_RDWR, 0600, &attr);
151 CHECK(mq != (mqd_t)-1);
152 CHECK(mq_send(mq, "private", 8, 0) == 0);
153 char received[32];
154 CHECK(mq_receive(mq, received, sizeof(received), NULL) == 8);
155 CHECK(!strcmp(received, "private") && mq_close(mq) == 0);
156 /* Namespace teardown owns the named objects deliberately left registered. */
157 return 0;
158}
159
160static int ipc_namespace(void) {
161 key_t key = 0x41000000 | getpid();
162 char name[64];
163 snprintf(name, sizeof(name), "/namespace-%ld", (long)getpid());
164 int sem = semget(key, 1, IPC_CREAT | IPC_EXCL | 0600);
165 int shm = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0600);
166 int msg = msgget(key, IPC_CREAT | IPC_EXCL | 0600);
167 CHECK(sem >= 0 && shm >= 0 && msg >= 0);
168 CHECK(semctl(sem, 0, SETVAL, 17) == 0);
169 int* mapping = shmat(shm, NULL, 0);
170 CHECK(mapping != (void*)-1);
171 *mapping = 17;
172 struct mq_attr attr = {.mq_maxmsg = 2, .mq_msgsize = 32};
173 mqd_t mq = mq_open(name, O_CREAT | O_EXCL | O_RDWR, 0600, &attr);
174 CHECK(mq != (mqd_t)-1 && mq_send(mq, "parent", 7, 0) == 0);
175 pid_t child = clone_process(CLONE_NEWIPC);
176 CHECK(child >= 0);
177 if (!child) {
178 _exit(ipc_child(key, sem, shm, msg, mapping, name));
179 }
180 CHECK(!reap(child));
181 CHECK(semctl(sem, 0, GETVAL) == 17 && *mapping == 17);
182 char received[32];
183 CHECK(mq_receive(mq, received, sizeof(received), NULL) == 7);
184 CHECK(!strcmp(received, "parent"));
185 CHECK(mq_close(mq) == 0 && mq_unlink(name) == 0);
186 CHECK(semctl(sem, 0, IPC_RMID) == 0 && msgctl(msg, IPC_RMID, NULL) == 0);
187 CHECK(shmdt(mapping) == 0 && shmctl(shm, IPC_RMID, NULL) == 0);
188 puts("NAMESPACE-CONTRACT: ipc PASS");
189 return 0;
190}
191
192static int write_file(const char* path, const char* value) {
193 int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
194 CHECK(fd >= 0);
195 CHECK(write(fd, value, strlen(value)) == (ssize_t)strlen(value));
196 CHECK(close(fd) == 0);
197 return 0;
198}
199
200static int write_control(const char* path, const char* value) {
201 int fd = open(path, O_WRONLY);
202 CHECK(fd >= 0);
203 CHECK(write(fd, value, strlen(value)) == (ssize_t)strlen(value));
204 CHECK(close(fd) == 0);
205 return 0;
206}
207
208static int map_user(uid_t uid, gid_t gid) {
209 char map[64];
210 snprintf(map, sizeof(map), "0 %u 1\n", uid);
211 CHECK(!write_control("/proc/self/uid_map", map));
212 CHECK(!write_control("/proc/self/setgroups", "deny\n"));
213 snprintf(map, sizeof(map), "0 %u 1\n", gid);
214 CHECK(!write_control("/proc/self/gid_map", map));
215 CHECK(getuid() == 0 && getgid() == 0);
216 return 0;
217}
218
219static int mount_child(const char* source, const char* target, const char* writable, uid_t uid,
220 gid_t gid, int inherited_proc) {
221 int self = openat(inherited_proc, "self", O_PATH | O_DIRECTORY);
222 CHECK(self >= 0);
223 int uid_map = openat(self, "uid_map", O_RDONLY);
224 CHECK(uid_map >= 0);
225 CHECK(close(uid_map) == 0 && close(self) == 0 && close(inherited_proc) == 0);
226 CHECK(!map_user(uid, gid));
227 CHECK(mount(source, target, NULL, MS_BIND, NULL) == 0);
228 char file[512], workspace[512], output[1024];
229 snprintf(file, sizeof(file), "%s/sentinel", target);
230 snprintf(workspace, sizeof(workspace), "%s/work", target);
231 snprintf(output, sizeof(output), "%s/output", workspace);
232 int held = open(file, O_WRONLY);
233 CHECK(held >= 0);
234 errno = 0;
235 CHECK(mount(NULL, target, NULL, MS_BIND | MS_REMOUNT | MS_RDONLY, NULL) == -1 && errno == EBUSY);
236 CHECK(close(held) == 0);
237 CHECK(mount(NULL, target, NULL, MS_BIND | MS_REMOUNT | MS_RDONLY, NULL) == 0);
238 CHECK(mount(writable, workspace, NULL, MS_BIND, NULL) == 0);
239 errno = 0;
240 CHECK(open(file, O_WRONLY | O_TRUNC) == -1 && errno == EROFS);
241 CHECK(!write_file(output, "workspace"));
242 pid_t child = clone_process(CLONE_NEWUSER | CLONE_NEWNS);
243 CHECK(child >= 0);
244 if (!child) {
245 errno = 0;
246 int result = mount(NULL, target, NULL, MS_BIND | MS_REMOUNT, NULL);
247 _exit(result == -1 && errno == EPERM ? 0 : 1);
248 }
249 CHECK(!reap(child));
250 return 0;
251}
252
253static int mount_namespace(void) {
254 char base[256], source[320], target[320], writable[320], work[384], file[384], output[384];
255 snprintf(base, sizeof(base), "/tmp/namespace-contract-%ld", (long)getpid());
256 snprintf(source, sizeof(source), "%s/source", base);
257 snprintf(target, sizeof(target), "%s/target", base);
258 snprintf(writable, sizeof(writable), "%s/writable", base);
259 snprintf(work, sizeof(work), "%s/work", source);
260 snprintf(file, sizeof(file), "%s/sentinel", source);
261 snprintf(output, sizeof(output), "%s/output", writable);
262 CHECK(mkdir(base, 0700) == 0 && mkdir(source, 0700) == 0 && mkdir(target, 0700) == 0);
263 CHECK(mkdir(writable, 0700) == 0 && mkdir(work, 0700) == 0);
264 CHECK(!write_file(file, "parent"));
265 uid_t uid = getuid();
266 gid_t gid = getgid();
267 int inherited_proc = open("/proc", O_PATH | O_DIRECTORY);
268 CHECK(inherited_proc >= 0);
269 pid_t child = clone_process(CLONE_NEWUSER | CLONE_NEWNS);
270 CHECK(child >= 0);
271 if (!child) {
272 _exit(mount_child(source, target, writable, uid, gid, inherited_proc));
273 }
274 CHECK(close(inherited_proc) == 0);
275 CHECK(!reap(child));
276 CHECK(!write_file(file, "parent-still-writable"));
277 char hidden[384];
278 snprintf(hidden, sizeof(hidden), "%s/sentinel", target);
279 errno = 0;
280 CHECK(access(hidden, F_OK) == -1 && errno == ENOENT);
281 CHECK(access(output, F_OK) == 0);
282 CHECK(unlink(output) == 0 && unlink(file) == 0 && rmdir(work) == 0);
283 CHECK(rmdir(writable) == 0 && rmdir(target) == 0 && rmdir(source) == 0 && rmdir(base) == 0);
284 puts("NAMESPACE-CONTRACT: mounts PASS");
285 return 0;
286}
287
288static int route_request(int route, void* request, size_t length) {
289 struct sockaddr_nl kernel = {.nl_family = AF_NETLINK};
290 CHECK(sendto(route, request, length, 0, (struct sockaddr*)&kernel, sizeof(kernel)) ==
291 (ssize_t)length);
292 char response[512];
293 ssize_t received = recv(route, response, sizeof(response), 0);
294 CHECK(received >= (ssize_t)NLMSG_LENGTH(sizeof(struct nlmsgerr)));
295 struct nlmsghdr* header = (struct nlmsghdr*)response;
296 CHECK(header->nlmsg_type == NLMSG_ERROR && header->nlmsg_len <= (unsigned)received);
297 struct nlmsgerr* error = NLMSG_DATA(header);
298 CHECK(error->error == 0);
299 return 0;
300}
301
302static int configure_loopback(void) {
303 int index = if_nametoindex("lo");
304 CHECK(index > 0);
305 int route = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
306 CHECK(route >= 0);
307 struct sockaddr_nl local = {.nl_family = AF_NETLINK, .nl_pid = getpid()};
308 CHECK(bind(route, (struct sockaddr*)&local, sizeof(local)) == 0);
309 char buffer[128] = {0};
310 struct nlmsghdr* header = (struct nlmsghdr*)buffer;
311 header->nlmsg_len = NLMSG_LENGTH(sizeof(struct ifaddrmsg));
312 header->nlmsg_type = RTM_NEWADDR;
313 header->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL;
314 header->nlmsg_seq = 1;
315 struct ifaddrmsg* address = NLMSG_DATA(header);
316 address->ifa_family = AF_INET;
317 address->ifa_prefixlen = 8;
318 address->ifa_scope = RT_SCOPE_HOST;
319 address->ifa_index = index;
320 for (int field = IFA_ADDRESS; field <= IFA_LOCAL; ++field) {
321 struct rtattr* attribute = (struct rtattr*)(buffer + NLMSG_ALIGN(header->nlmsg_len));
322 attribute->rta_type = field;
323 attribute->rta_len = RTA_LENGTH(sizeof(uint32_t));
324 uint32_t loopback = htonl(INADDR_LOOPBACK);
325 memcpy(RTA_DATA(attribute), &loopback, sizeof(loopback));
326 header->nlmsg_len = NLMSG_ALIGN(header->nlmsg_len) + RTA_ALIGN(attribute->rta_len);
327 }
328 CHECK(!route_request(route, buffer, header->nlmsg_len));
329 memset(buffer, 0, sizeof(buffer));
330 header->nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
331 header->nlmsg_type = RTM_NEWLINK;
332 header->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
333 header->nlmsg_seq = 2;
334 struct ifinfomsg* link = NLMSG_DATA(header);
335 link->ifi_index = index;
336 link->ifi_flags = link->ifi_change = IFF_UP;
337 CHECK(!route_request(route, buffer, header->nlmsg_len));
338 CHECK(close(route) == 0);
339 return 0;
340}
341
342static int inet_socket(int type, unsigned port) {
343 int fd = socket(AF_INET, type | SOCK_CLOEXEC, 0);
344 if (fd < 0) {
345 return -1;
346 }
347 struct sockaddr_in address = {
348 .sin_family = AF_INET, .sin_port = htons(port), .sin_addr.s_addr = htonl(INADDR_LOOPBACK)};
349 if (bind(fd, (struct sockaddr*)&address, sizeof(address)) < 0 ||
350 (type == SOCK_STREAM && listen(fd, 4) < 0)) {
351 close(fd);
352 return -1;
353 }
354 return fd;
355}
356
357static int network_child(unsigned port, const struct sockaddr_un* abstract, socklen_t extent) {
358 CHECK(!configure_loopback());
359 struct sockaddr_in address = {
360 .sin_family = AF_INET, .sin_port = htons(port), .sin_addr.s_addr = htonl(INADDR_LOOPBACK)};
361 int isolated = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
362 CHECK(isolated >= 0);
363 errno = 0;
364 CHECK(connect(isolated, (struct sockaddr*)&address, sizeof(address)) == -1 &&
365 (errno == ECONNREFUSED || errno == ENETUNREACH || errno == EHOSTUNREACH));
366 CHECK(close(isolated) == 0);
367 isolated = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0);
368 CHECK(isolated >= 0);
369 errno = 0;
370 CHECK(connect(isolated, (const struct sockaddr*)abstract, extent) == -1 &&
371 (errno == ECONNREFUSED || errno == ENOENT));
372 CHECK(bind(isolated, (const struct sockaddr*)abstract, extent) == 0);
373 CHECK(close(isolated) == 0);
374
375 int listener = inet_socket(SOCK_STREAM, port);
376 int udp = inet_socket(SOCK_DGRAM, port);
377 CHECK(listener >= 0 && udp >= 0);
378 pid_t client = fork();
379 CHECK(client >= 0);
380 if (!client) {
381 int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
382 if (socket_fd < 0 || connect(socket_fd, (struct sockaddr*)&address, sizeof(address)) ||
383 write(socket_fd, "private", 8) != 8) {
384 _exit(1);
385 }
386 char reply;
387 _exit(read(socket_fd, &reply, 1) == 1 && reply == 'x' ? 0 : 1);
388 }
389 int accepted = accept(listener, NULL, NULL);
390 CHECK(accepted >= 0);
391 char received[32];
392 CHECK(read(accepted, received, sizeof(received)) == 8 && !strcmp(received, "private"));
393 CHECK(write(accepted, "x", 1) == 1 && close(accepted) == 0);
394 CHECK(!reap(client));
395 int sender = socket(AF_INET, SOCK_DGRAM, 0);
396 CHECK(sender >= 0);
397 CHECK(sendto(sender, "datagram", 9, 0, (struct sockaddr*)&address, sizeof(address)) == 9);
398 CHECK(recv(udp, received, sizeof(received), 0) == 9 && !strcmp(received, "datagram"));
399 CHECK(close(sender) == 0 && close(udp) == 0 && close(listener) == 0);
400 return 0;
401}
402
403int namespace_network_test(void) {
404 unsigned port = 34000 + (unsigned)getpid() % 20000;
405 int listener = inet_socket(SOCK_STREAM, port);
406 int udp = inet_socket(SOCK_DGRAM, port);
407 CHECK(listener >= 0 && udp >= 0);
408 struct sockaddr_un abstract = {.sun_family = AF_UNIX};
409 snprintf(abstract.sun_path + 1, sizeof(abstract.sun_path) - 1, "namespace-%ld", (long)getpid());
410 socklen_t extent = offsetof(struct sockaddr_un, sun_path) + 1 + strlen(abstract.sun_path + 1);
411 int named = socket(AF_UNIX, SOCK_STREAM, 0);
412 CHECK(named >= 0 && bind(named, (struct sockaddr*)&abstract, extent) == 0);
413 CHECK(listen(named, 4) == 0);
414 pid_t child = clone_process(CLONE_NEWNET);
415 CHECK(child >= 0);
416 if (!child) {
417 close(listener);
418 close(udp);
419 close(named);
420 _exit(network_child(port, &abstract, extent));
421 }
422 CHECK(!reap(child));
423 char received[32];
424 CHECK(fcntl(udp, F_SETFL, O_NONBLOCK) == 0);
425 errno = 0;
426 CHECK(recv(udp, received, sizeof(received), 0) == -1 && errno == EAGAIN);
427 CHECK(close(listener) == 0 && close(udp) == 0 && close(named) == 0);
428 puts("NAMESPACE-CONTRACT: network PASS");
429 return 0;
430}
431
432int namespace_tests(void) {
433 if (pid_namespace() || ipc_namespace() || mount_namespace() || namespace_network_test()) {
434 puts("NAMESPACE-CONTRACT: FAIL");
435 return 1;
436 }
437 puts("NAMESPACE-CONTRACT: PASS");
438 return 0;
439}
#define INADDR_LOOPBACK
Definition inet.h:92