The Pedigree Project 0.1
command.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 <pthread.h>
12#include <sched.h>
13#include <stdint.h>
14#include <string.h>
15#include <time.h>
16#include <unistd.h>
17
18#include <arpa/inet.h>
19#include <pedigree/log.h>
20#include <sys/epoll.h>
21#include <sys/reboot.h>
22#include <sys/socket.h>
23#include <sys/syscall.h>
24#include <sys/uio.h>
25
27 int listener;
28 int phase;
29 int failure;
30 uint8_t received;
31 int saw_eof;
32};
33
34enum loopback_phase {
35 loopback_accept_ready = 1,
36 loopback_receive_ready = 2,
37};
38
39enum {
40 loopback_phase_yields = 4096,
41 loopback_blocking_window_yields = 32,
42 compute_probe_sleep_us = 500000,
43 compute_probe_fallback_ns = 2000000000ULL,
44 compute_probe_latest_wake_ns = 1500000000ULL,
45};
46
48 int stop;
49 int fallback;
50 uintptr_t parent_self;
51 uintptr_t child_self[2];
52 int child_syscall_ok[2];
53 uint64_t counters[2];
54};
55
57 struct compute_preemption_probe* probe;
58 int index;
59};
60
62 int tid_word;
63 int ready;
64 int go;
65 int failure;
66};
67
68static uint64_t monotonic_nanoseconds(void) {
69 struct timespec now;
70 if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) {
71 return 0;
72 }
73 return ((uint64_t)now.tv_sec * 1000000000ULL) + now.tv_nsec;
74}
75
76static void* run_compute_preemption_worker(void* parameter) {
77 struct compute_preemption_worker* worker = parameter;
78 struct compute_preemption_probe* probe = worker->probe;
79 __atomic_store_n(&probe->child_self[worker->index], (uintptr_t)pthread_self(), __ATOMIC_RELEASE);
80 const uint64_t started = monotonic_nanoseconds();
81 __atomic_store_n(&probe->child_syscall_ok[worker->index], started != 0, __ATOMIC_RELEASE);
82 const uint64_t deadline = started + compute_probe_fallback_ns;
83
84 while (!__atomic_load_n(&probe->stop, __ATOMIC_ACQUIRE)) {
85 const uint64_t count = __atomic_add_fetch(&probe->counters[worker->index], 1, __ATOMIC_RELAXED);
86 if ((count & 0x3fff) == 0) {
87 const uint64_t now = monotonic_nanoseconds();
88 if (!now || now >= deadline) {
89 __atomic_store_n(&probe->fallback, 1, __ATOMIC_RELEASE);
90 sched_yield();
91 }
92 }
93 }
94
95 return 0;
96}
97
98static int run_compute_preemption_test(void) {
99 struct compute_preemption_probe probe;
100 memset(&probe, 0, sizeof(probe));
101 probe.parent_self = (uintptr_t)pthread_self();
102 struct compute_preemption_worker workers[2] = {
103 {&probe, 0},
104 {&probe, 1},
105 };
106 pthread_t threads[2];
107
108 if (pthread_create(&threads[0], 0, run_compute_preemption_worker, &workers[0])) {
109 return 1;
110 }
111 if (pthread_create(&threads[1], 0, run_compute_preemption_worker, &workers[1])) {
112 __atomic_store_n(&probe.stop, 1, __ATOMIC_RELEASE);
113 pthread_join(threads[0], 0);
114 return 2;
115 }
116 pedigree_log(LOG_INFO, "HOSTED-SMOKE: PASS pthread-clone-state-switch");
117
118 const uint64_t started = monotonic_nanoseconds();
119 const int sleep_result = usleep(compute_probe_sleep_us);
120 const uint64_t elapsed = monotonic_nanoseconds() - started;
121 __atomic_store_n(&probe.stop, 1, __ATOMIC_RELEASE);
122
123 const uintptr_t first_self = __atomic_load_n(&probe.child_self[0], __ATOMIC_ACQUIRE);
124 const uintptr_t second_self = __atomic_load_n(&probe.child_self[1], __ATOMIC_ACQUIRE);
125 const int child_contract = first_self && second_self && first_self != second_self &&
126 first_self != probe.parent_self && second_self != probe.parent_self &&
127 __atomic_load_n(&probe.child_syscall_ok[0], __ATOMIC_ACQUIRE) &&
128 __atomic_load_n(&probe.child_syscall_ok[1], __ATOMIC_ACQUIRE) &&
129 __atomic_load_n(&probe.counters[0], __ATOMIC_ACQUIRE) &&
130 __atomic_load_n(&probe.counters[1], __ATOMIC_ACQUIRE);
131 if (child_contract) {
132 pedigree_log(LOG_INFO, "HOSTED-SMOKE: PASS pthread-child-tls-args-syscall");
133 }
134
135 const int first_join = pthread_join(threads[0], 0);
136 const int second_join = pthread_join(threads[1], 0);
137 if (!first_join && !second_join) {
138 pedigree_log(LOG_INFO, "HOSTED-SMOKE: PASS pthread-clear-tid-join");
139 }
140 if (sleep_result || first_join || second_join || !child_contract ||
141 __atomic_load_n(&probe.fallback, __ATOMIC_ACQUIRE) ||
142 elapsed > compute_probe_latest_wake_ns) {
143 return 3;
144 }
145
146 return 0;
147}
148
149static void* run_detached_exit_worker(void* parameter) {
150 struct detached_exit_probe* probe = parameter;
151 const long tid = syscall(SYS_set_tid_address, &probe->tid_word);
152 if (tid <= 0) {
153 __atomic_store_n(&probe->failure, 1, __ATOMIC_RELEASE);
154 __atomic_store_n(&probe->ready, 1, __ATOMIC_RELEASE);
155 return 0;
156 }
157
158 __atomic_store_n(&probe->tid_word, (int)tid, __ATOMIC_RELEASE);
159 __atomic_store_n(&probe->ready, 1, __ATOMIC_RELEASE);
160 while (!__atomic_load_n(&probe->go, __ATOMIC_ACQUIRE)) {
161 sched_yield();
162 }
163
164 syscall(SYS_exit, 0);
165 __atomic_store_n(&probe->failure, 2, __ATOMIC_RELEASE);
166 return 0;
167}
168
169static int run_detached_clear_tid_test(void) {
170 struct detached_exit_probe probe;
171 memset(&probe, 0, sizeof(probe));
172
173 pthread_attr_t attributes;
174 if (pthread_attr_init(&attributes)) {
175 return 1;
176 }
177 if (pthread_attr_setdetachstate(&attributes, PTHREAD_CREATE_DETACHED)) {
178 pthread_attr_destroy(&attributes);
179 return 2;
180 }
181
182 pthread_t thread;
183 const int create_result = pthread_create(&thread, &attributes, run_detached_exit_worker, &probe);
184 pthread_attr_destroy(&attributes);
185 if (create_result) {
186 return 3;
187 }
188
189 for (int i = 0; i < 100000; ++i) {
190 if (__atomic_load_n(&probe.ready, __ATOMIC_ACQUIRE)) {
191 break;
192 }
193 sched_yield();
194 }
195 if (!__atomic_load_n(&probe.ready, __ATOMIC_ACQUIRE) ||
196 __atomic_load_n(&probe.failure, __ATOMIC_ACQUIRE)) {
197 return 4;
198 }
199
200 __atomic_store_n(&probe.go, 1, __ATOMIC_RELEASE);
201 for (int attempt = 0; attempt < 3; ++attempt) {
202 const int observed = __atomic_load_n(&probe.tid_word, __ATOMIC_ACQUIRE);
203 if (!observed) {
204 break;
205 }
206
207 const struct timespec timeout = {1, 0};
208 syscall(SYS_futex, &probe.tid_word, 0, observed, &timeout);
209 }
210
211 if (__atomic_load_n(&probe.tid_word, __ATOMIC_ACQUIRE) ||
212 __atomic_load_n(&probe.failure, __ATOMIC_ACQUIRE)) {
213 return 5;
214 }
215
216 return 0;
217}
218
219static int wait_for_phase(struct loopback_server* server, int phase) {
220 for (int i = 0; i < loopback_phase_yields; ++i) {
221 if (__atomic_load_n(&server->phase, __ATOMIC_ACQUIRE) >= phase) {
222 return 1;
223 }
224 sched_yield();
225 }
226 return 0;
227}
228
229static void yield_blocking_window(void) {
230 for (int i = 0; i < loopback_blocking_window_yields; ++i) {
231 sched_yield();
232 }
233}
234
235static void shutdown_and_close(int descriptor) {
236 if (descriptor >= 0) {
237 shutdown(descriptor, SHUT_RDWR);
238 close(descriptor);
239 }
240}
241
242static void* run_loopback_server(void* parameter) {
243 struct loopback_server* server = parameter;
244 __atomic_store_n(&server->phase, loopback_accept_ready, __ATOMIC_RELEASE);
245 int connection = accept(server->listener, 0, 0);
246 if (connection < 0) {
247 server->failure = 1;
248 return 0;
249 }
250
251 __atomic_store_n(&server->phase, loopback_receive_ready, __ATOMIC_RELEASE);
252 ssize_t received = recv(connection, &server->received, 1, 0);
253 if (received != 1) {
254 server->failure = 2;
255 close(connection);
256 return 0;
257 }
258
259 received = recv(connection, &server->received, 1, 0);
260 server->saw_eof = received == 0;
261 if (!server->saw_eof) {
262 server->failure = 3;
263 }
264
265 close(connection);
266 return 0;
267}
268
269static int run_loopback_test(void) {
270 int listener = socket(AF_INET, SOCK_STREAM, 0);
271 if (listener < 0) {
272 return 10;
273 }
274
275 struct sockaddr_in address;
276 memset(&address, 0, sizeof(address));
277 address.sin_family = AF_INET;
278 address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
279 if (bind(listener, (struct sockaddr*)&address, sizeof(address)) != 0) {
280 close(listener);
281 return 11;
282 }
283
284 socklen_t address_length = sizeof(address);
285 if (getsockname(listener, (struct sockaddr*)&address, &address_length) != 0 ||
286 !address.sin_port) {
287 close(listener);
288 return 12;
289 }
290 if (listen(listener, 1) != 0) {
291 close(listener);
292 return 13;
293 }
294
295 int client = socket(AF_INET, SOCK_STREAM, 0);
296 if (client < 0) {
297 close(listener);
298 return 14;
299 }
300
301 struct loopback_server server;
302 memset(&server, 0, sizeof(server));
303 server.listener = listener;
304 pthread_t server_thread;
305 int thread_error = pthread_create(&server_thread, 0, run_loopback_server, &server);
306 if (thread_error) {
307 close(client);
308 close(listener);
309 return 15;
310 }
311
312 if (!wait_for_phase(&server, loopback_accept_ready)) {
313 shutdown_and_close(client);
314 shutdown_and_close(listener);
315 pthread_join(server_thread, 0);
316 return 16;
317 }
318
319 // Give the server a bounded opportunity to enter accept before connecting.
320 yield_blocking_window();
321 if (connect(client, (struct sockaddr*)&address, address_length) != 0) {
322 shutdown_and_close(client);
323 shutdown_and_close(listener);
324 pthread_join(server_thread, 0);
325 return 17;
326 }
327
328 if (!wait_for_phase(&server, loopback_receive_ready)) {
329 shutdown_and_close(client);
330 shutdown_and_close(listener);
331 pthread_join(server_thread, 0);
332 return 18;
333 }
334
335 // Likewise, let the server reach recv while the client has sent no data.
336 yield_blocking_window();
337 const uint8_t expected = 0xA5;
338 if (send(client, &expected, sizeof(expected), 0) != (ssize_t)sizeof(expected) ||
339 shutdown(client, SHUT_WR) != 0) {
340 shutdown_and_close(client);
341 shutdown_and_close(listener);
342 pthread_join(server_thread, 0);
343 return 19;
344 }
345
346 close(client);
347 if (pthread_join(server_thread, 0) != 0) {
348 close(listener);
349 return 20;
350 }
351 close(listener);
352
353 if (server.failure || server.received != expected || !server.saw_eof) {
354 return 30 + server.failure;
355 }
356 return 0;
357}
358
359static int run_udp_message_test(void) {
360 int result = 0;
361 int server = -1;
362 int client = -1;
363 int epoll_descriptor = -1;
364 struct sockaddr_in server_address;
365 struct sockaddr_in client_address;
366 socklen_t server_address_length = sizeof(server_address);
367 socklen_t client_address_length = sizeof(client_address);
368
369 memset(&server_address, 0, sizeof(server_address));
370 server_address.sin_family = AF_INET;
371 server_address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
372 server = socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, 0);
373 if (server < 0 || bind(server, (struct sockaddr*)&server_address, sizeof(server_address)) ||
374 getsockname(server, (struct sockaddr*)&server_address, &server_address_length) ||
375 server_address_length != sizeof(server_address) || !server_address.sin_port) {
376 result = 40;
377 goto out;
378 }
379
380 memset(&client_address, 0, sizeof(client_address));
381 client_address.sin_family = AF_INET;
382 client_address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
383 client = socket(AF_INET, SOCK_DGRAM, 0);
384 if (client < 0 || bind(client, (struct sockaddr*)&client_address, sizeof(client_address)) ||
385 getsockname(client, (struct sockaddr*)&client_address, &client_address_length) ||
386 client_address_length != sizeof(client_address) || !client_address.sin_port) {
387 result = 41;
388 goto out;
389 }
390
391 epoll_descriptor = epoll_create1(EPOLL_CLOEXEC);
392 struct epoll_event watch;
393 memset(&watch, 0, sizeof(watch));
394 watch.events = EPOLLIN | EPOLLET;
395 watch.data.fd = server;
396 if (epoll_descriptor < 0 || epoll_ctl(epoll_descriptor, EPOLL_CTL_ADD, server, &watch)) {
397 result = 42;
398 goto out;
399 }
400
401 const uint8_t first_prefix[] = {0x10, 0x11};
402 const uint8_t first_suffix[] = {0x12, 0x13, 0x14, 0x15};
403 struct iovec outgoing_vectors[2] = {
404 {(void*)first_prefix, sizeof(first_prefix)},
405 {(void*)first_suffix, sizeof(first_suffix)},
406 };
407 struct msghdr outgoing;
408 memset(&outgoing, 0, sizeof(outgoing));
409 outgoing.msg_name = &server_address;
410 outgoing.msg_namelen = server_address_length;
411 outgoing.msg_iov = outgoing_vectors;
412 outgoing.msg_iovlen = 2;
413 if (sendmsg(client, &outgoing, 0) != (ssize_t)(sizeof(first_prefix) + sizeof(first_suffix))) {
414 result = 43;
415 goto out;
416 }
417
418 struct epoll_event ready;
419 memset(&ready, 0, sizeof(ready));
420 if (epoll_wait(epoll_descriptor, &ready, 1, 1000) != 1 || !(ready.events & EPOLLIN) ||
421 ready.data.fd != server) {
422 result = 44;
423 goto out;
424 }
425
426 uint8_t first_received[3] = {0};
427 struct iovec incoming_vectors[2] = {
428 {&first_received[0], 2},
429 {&first_received[2], 1},
430 };
431 struct sockaddr_in first_source;
432 memset(&first_source, 0, sizeof(first_source));
433 struct msghdr incoming;
434 memset(&incoming, 0, sizeof(incoming));
435 incoming.msg_name = &first_source;
436 incoming.msg_namelen = sizeof(first_source);
437 incoming.msg_iov = incoming_vectors;
438 incoming.msg_iovlen = 2;
439 if (recvmsg(server, &incoming, 0) != (ssize_t)sizeof(first_received) ||
440 memcmp(first_received, first_prefix, sizeof(first_prefix)) || first_received[2] != 0x12 ||
441 !(incoming.msg_flags & MSG_TRUNC) || incoming.msg_namelen != sizeof(first_source) ||
442 first_source.sin_family != AF_INET || first_source.sin_port != client_address.sin_port ||
443 first_source.sin_addr.s_addr != client_address.sin_addr.s_addr) {
444 result = 45;
445 goto out;
446 }
447
448 uint8_t extra = 0;
449 errno = 0;
450 if (recvfrom(server, &extra, sizeof(extra), 0, 0, 0) != -1 || errno != EAGAIN ||
451 epoll_wait(epoll_descriptor, &ready, 1, 0) != 0) {
452 result = 46;
453 goto out;
454 }
455
456 const uint8_t second_payload[] = {0x20, 0x21, 0x22, 0x23, 0x24};
457 if (sendto(client, second_payload, sizeof(second_payload), 0, (struct sockaddr*)&server_address,
458 server_address_length) != (ssize_t)sizeof(second_payload)) {
459 result = 47;
460 goto out;
461 }
462 memset(&ready, 0, sizeof(ready));
463 if (epoll_wait(epoll_descriptor, &ready, 1, 1000) != 1 || !(ready.events & EPOLLIN) ||
464 ready.data.fd != server) {
465 result = 48;
466 goto out;
467 }
468
469 uint8_t second_received[2] = {0};
470 struct sockaddr_in second_source;
471 memset(&second_source, 0, sizeof(second_source));
472 socklen_t second_source_length = sizeof(second_source);
473 if (recvfrom(server, second_received, sizeof(second_received), MSG_TRUNC,
474 (struct sockaddr*)&second_source,
475 &second_source_length) != (ssize_t)sizeof(second_payload) ||
476 memcmp(second_received, second_payload, sizeof(second_received)) ||
477 second_source_length != sizeof(second_source) || second_source.sin_family != AF_INET ||
478 second_source.sin_port != client_address.sin_port ||
479 second_source.sin_addr.s_addr != client_address.sin_addr.s_addr) {
480 result = 49;
481 goto out;
482 }
483
484 errno = 0;
485 if (recvfrom(server, &extra, sizeof(extra), 0, 0, 0) != -1 || errno != EAGAIN ||
486 epoll_wait(epoll_descriptor, &ready, 1, 0) != 0) {
487 result = 50;
488 }
489
490out:
491 if (epoll_descriptor >= 0) {
492 close(epoll_descriptor);
493 }
494 if (client >= 0) {
495 close(client);
496 }
497 if (server >= 0) {
498 close(server);
499 }
500 return result;
501}
502
503int main(int argc, char** argv) {
504 const char* stage = argc > 1 ? argv[1] : "shutdown";
505
506 pedigree_log(LOG_INFO, "HOSTED-SMOKE: simple userspace command ran");
507 if (!strcmp(stage, "command")) {
508 const int preemption_result = run_compute_preemption_test();
509 if (preemption_result) {
510 pedigree_log(LOG_ERR, "HOSTED-SMOKE: FAIL userspace-compute-preemption: %d",
511 preemption_result);
512 } else {
513 pedigree_log(LOG_INFO, "HOSTED-SMOKE: PASS userspace-compute-preemption");
514 }
515
516 const int detached_result = run_detached_clear_tid_test();
517 if (detached_result) {
518 pedigree_log(LOG_ERR, "HOSTED-SMOKE: FAIL pthread-clear-tid-detached: %d", detached_result);
519 } else {
520 pedigree_log(LOG_INFO, "HOSTED-SMOKE: PASS pthread-clear-tid-detached");
521 }
522
523 const int loopback_result = run_loopback_test();
524 if (loopback_result) {
525 pedigree_log(LOG_ERR,
526 "HOSTED-SMOKE: FAIL posix-lwip-loopback-roundtrip: %d "
527 "(errno %d)",
528 loopback_result, errno);
529 } else {
530 pedigree_log(LOG_INFO, "HOSTED-SMOKE: PASS posix-lwip-loopback-roundtrip");
531 }
532
533 const int udp_result = run_udp_message_test();
534 if (udp_result) {
535 pedigree_log(LOG_ERR,
536 "HOSTED-SMOKE: FAIL posix-lwip-udp-message-semantics: %d "
537 "(errno %d)",
538 udp_result, errno);
539 } else {
540 pedigree_log(LOG_INFO, "HOSTED-SMOKE: PASS posix-lwip-udp-message-semantics");
541 }
542 }
543 if (!strcmp(stage, "shutdown")) {
544 pedigree_log(LOG_INFO, "HOSTED-SMOKE: requesting clean shutdown");
545 }
546
547 if (reboot(0) != 0) {
548 pedigree_log(LOG_ERR, "HOSTED-SMOKE: shutdown request failed: %d", errno);
549 return 1;
550 }
551
552 return 0;
553}
#define INADDR_LOOPBACK
Definition inet.h:92