The Pedigree Project 0.1
unix-stream-interruption.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#include <errno.h>
9#include <fcntl.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/socket.h>
21#include <sys/syscall.h>
22#include <sys/wait.h>
23
24extern void fail(void) __attribute__((noreturn));
25
26enum { wait_timeout_seconds = 2 };
27
28enum io_operation {
29 receive_operation,
30 send_operation,
31};
32
33struct io_context {
34 int descriptor;
35 char* payload;
36 size_t length;
37 enum io_operation operation;
38 int use_message;
39 int rights_descriptor;
40 volatile int entered;
41 volatile int returned;
42 long tid;
43 int tid_error;
44 ssize_t result;
45 int error;
46};
47
49 struct cmsghdr alignment;
50 unsigned char bytes[CMSG_SPACE(sizeof(int))];
51};
52
53static int socket_type = SOCK_STREAM;
54static volatile sig_atomic_t signal_calls;
55static volatile sig_atomic_t close_from_signal = -1;
56static volatile sig_atomic_t signal_close_result = -2;
57
58static void signal_handler(int signal_number) {
59 if (signal_number == SIGUSR1) {
60 ++signal_calls;
61 const int descriptor = __atomic_exchange_n(&close_from_signal, -1, __ATOMIC_RELAXED);
62 if (descriptor >= 0) {
63 __atomic_store_n(&signal_close_result, close(descriptor), __ATOMIC_RELEASE);
64 }
65 }
66}
67
68static void* run_io(void* parameter) {
69 struct io_context* context = parameter;
70 errno = 0;
71 context->tid = syscall(SYS_gettid);
72 context->tid_error = context->tid < 0 ? errno : 0;
73 errno = 0;
74 __atomic_store_n(&context->entered, 1, __ATOMIC_RELEASE);
75 if (context->use_message) {
76 struct iovec vector = {
77 .iov_base = context->payload,
78 .iov_len = context->length,
79 };
80 union control_buffer control = {0};
81 struct msghdr message = {
82 .msg_iov = &vector,
83 .msg_iovlen = 1,
84 .msg_control = control.bytes,
85 .msg_controllen = sizeof(control.bytes),
86 };
87 if (context->operation == send_operation) {
88 struct cmsghdr* header = CMSG_FIRSTHDR(&message);
89 header->cmsg_len = CMSG_LEN(sizeof(int));
90 header->cmsg_level = SOL_SOCKET;
91 header->cmsg_type = SCM_RIGHTS;
92 memcpy(CMSG_DATA(header), &context->rights_descriptor, sizeof(context->rights_descriptor));
93 context->result = sendmsg(context->descriptor, &message, MSG_NOSIGNAL);
94 } else {
95 context->result = recvmsg(context->descriptor, &message, 0);
96 }
97 } else if (context->operation == receive_operation) {
98 context->result = recv(context->descriptor, context->payload, context->length, 0);
99 } else {
100 context->result = send(context->descriptor, context->payload, context->length, MSG_NOSIGNAL);
101 }
102 context->error = errno;
103 __atomic_store_n(&context->returned, 1, __ATOMIC_RELEASE);
104 return 0;
105}
106
107static int monotonic_now(struct timespec* now) {
108 if (clock_gettime(CLOCK_MONOTONIC, now)) {
109 fprintf(stderr, "AF_UNIX interruption clock_gettime failed: errno=%d\n", errno);
110 return -1;
111 }
112 return 0;
113}
114
115static int deadline_passed(const struct timespec* deadline) {
116 struct timespec now;
117 if (monotonic_now(&now)) {
118 return -1;
119 }
120 return now.tv_sec > deadline->tv_sec ||
121 (now.tv_sec == deadline->tv_sec && now.tv_nsec >= deadline->tv_nsec);
122}
123
124static int wait_for_value(volatile int* value) {
125 struct timespec deadline;
126 if (monotonic_now(&deadline)) {
127 return -1;
128 }
129 deadline.tv_sec += wait_timeout_seconds;
130 while (!__atomic_load_n(value, __ATOMIC_ACQUIRE)) {
131 const int expired = deadline_passed(&deadline);
132 if (expired) {
133 if (expired > 0) {
134 errno = ETIMEDOUT;
135 fprintf(stderr, "AF_UNIX interruption state wait timed out\n");
136 }
137 return -1;
138 }
139 sched_yield();
140 }
141 return 0;
142}
143
144static int install_signal_handler(void) {
145 struct sigaction action = {0};
146 signal_calls = 0;
147 __atomic_store_n(&close_from_signal, -1, __ATOMIC_RELAXED);
148 __atomic_store_n(&signal_close_result, -2, __ATOMIC_RELAXED);
149 action.sa_handler = signal_handler;
150 return sigemptyset(&action.sa_mask) || sigaction(SIGUSR1, &action, 0) ||
151 signal(SIGPIPE, SIG_IGN) == SIG_ERR;
152}
153
154static int interrupt_worker(struct io_context* context) {
155 struct timespec deadline;
156 if (monotonic_now(&deadline)) {
157 return -1;
158 }
159 deadline.tv_sec += wait_timeout_seconds;
160 for (size_t attempt = 0;; ++attempt) {
161 if (__atomic_load_n(&context->returned, __ATOMIC_ACQUIRE)) {
162 return 0;
163 }
164 if (deadline_passed(&deadline)) {
165 fprintf(stderr, "AF_UNIX interruption worker timed out: tid=%ld attempts=%zu\n", context->tid,
166 attempt);
167 return -1;
168 }
169 if (syscall(SYS_tkill, context->tid, SIGUSR1)) {
170 const int error = errno;
171 // The worker can publish completion and exit between the initial
172 // returned check and tkill. Its completed I/O result is checked below.
173 if (__atomic_load_n(&context->returned, __ATOMIC_ACQUIRE)) {
174 return 0;
175 }
176 fprintf(stderr, "AF_UNIX interruption tkill failed: tid=%ld errno=%d\n", context->tid, error);
177 errno = error;
178 return -1;
179 }
180 if (attempt < 32) {
181 for (size_t pause = 0; pause < 32; ++pause) {
182 if (__atomic_load_n(&context->returned, __ATOMIC_ACQUIRE)) {
183 return 0;
184 }
185 sched_yield();
186 }
187 if (attempt == 31) {
188 fprintf(stderr, "AF_UNIX interruption initial yield budget exhausted: tid=%ld\n",
189 context->tid);
190 }
191 } else {
192 // Publishing entered does not prove the worker is already blocked in
193 // its socket syscall. Keep retries bounded by elapsed time on SMP.
194 const struct timespec pause = {.tv_nsec = 10000000};
195 if (nanosleep(&pause, NULL) && errno != EINTR) {
196 fprintf(stderr, "AF_UNIX interruption retry sleep failed: errno=%d\n", errno);
197 return -1;
198 }
199 }
200 }
201}
202
203static int receive_stage_failed(const char* stage, int error, const struct io_context* context) {
204 const int entered = __atomic_load_n(&context->entered, __ATOMIC_ACQUIRE);
205 const int returned = __atomic_load_n(&context->returned, __ATOMIC_ACQUIRE);
206 fprintf(stderr,
207 "AF_UNIX interrupted receive failed: stage=%s error=%d tid=%ld tid_errno=%d "
208 "entered=%d returned=%d result=%ld io_errno=%d\n",
209 stage, error, entered ? context->tid : 0, entered ? context->tid_error : 0, entered,
210 returned, (long)(returned ? context->result : -2), returned ? context->error : 0);
211 return 12;
212}
213
214static int fill_send_queue(int descriptor, const char* payload, size_t length) {
215 const int flags = fcntl(descriptor, F_GETFL);
216 if (flags < 0 || fcntl(descriptor, F_SETFL, flags | O_NONBLOCK))
217 return -1;
218
219 size_t written = 0;
220 while (1) {
221 errno = 0;
222 const ssize_t result = send(descriptor, payload, length, MSG_NOSIGNAL);
223 if (result > 0) {
224 written += (size_t)result;
225 continue;
226 }
227 if (result != -1 || (errno != EAGAIN && errno != EWOULDBLOCK))
228 return -1;
229 break;
230 }
231
232 return written && !fcntl(descriptor, F_SETFL, flags) ? 0 : -1;
233}
234
235static int interrupted_receive_child(void) {
236 alarm(10);
237 if (install_signal_handler())
238 return 10;
239
240 int sockets[2];
241 char payload = 0;
242 if (socketpair(AF_UNIX, socket_type, 0, sockets)) {
243 fprintf(stderr, "interrupted receive socketpair failed: errno=%d\n", errno);
244 return 11;
245 }
246
247 struct io_context context = {
248 .descriptor = sockets[1],
249 .payload = &payload,
250 .length = 1,
251 .operation = receive_operation,
252 .result = -2,
253 };
254 pthread_t worker;
255 int error = pthread_create(&worker, 0, run_io, &context);
256 if (error) {
257 return receive_stage_failed("pthread_create", error, &context);
258 }
259 if (wait_for_value(&context.entered)) {
260 return receive_stage_failed("worker_entered", errno, &context);
261 }
262 if (context.tid <= 0) {
263 return receive_stage_failed("worker_tid", context.tid_error, &context);
264 }
265 if (interrupt_worker(&context)) {
266 return receive_stage_failed("interrupt_worker", errno, &context);
267 }
268 error = pthread_join(worker, 0);
269 if (error) {
270 return receive_stage_failed("pthread_join", error, &context);
271 }
272
273 if (context.result != -1 || context.error != EINTR || signal_calls < 1)
274 return 13;
275 return close(sockets[0]) || close(sockets[1]) ? 14 : 0;
276}
277
278static int partial_send_child(void) {
279 alarm(10);
280 if (install_signal_handler())
281 return 20;
282
283 int sockets[2];
284 char payload[1024];
285 memset(payload, 'q', sizeof(payload));
286 if (socketpair(AF_UNIX, socket_type, 0, sockets) ||
287 fill_send_queue(sockets[0], payload, sizeof(payload)) || recv(sockets[1], payload, 1, 0) != 1)
288 return 21;
289
290 payload[0] = 'x';
291 payload[1] = 'y';
292 struct io_context context = {
293 .descriptor = sockets[0],
294 .payload = payload,
295 .length = 2,
296 .operation = send_operation,
297 .result = -2,
298 };
299 pthread_t worker;
300 if (pthread_create(&worker, 0, run_io, &context) || wait_for_value(&context.entered) ||
301 context.tid <= 0 || interrupt_worker(&context) || pthread_join(worker, 0))
302 return 22;
303
304 if (context.result != 1 || context.error || signal_calls < 1)
305 return 23;
306 return close(sockets[0]) || close(sockets[1]) ? 24 : 0;
307}
308
309static int interrupted_send_child(void) {
310 alarm(10);
311 if (install_signal_handler())
312 return 25;
313
314 int sockets[2];
315 char payload[1024];
316 memset(payload, 'i', sizeof(payload));
317 if (socketpair(AF_UNIX, socket_type, 0, sockets) ||
318 fill_send_queue(sockets[0], payload, sizeof(payload)))
319 return 26;
320
321 struct io_context context = {
322 .descriptor = sockets[0],
323 .payload = payload,
324 .length = 1,
325 .operation = send_operation,
326 .result = -2,
327 };
328 pthread_t worker;
329 if (pthread_create(&worker, 0, run_io, &context) || wait_for_value(&context.entered) ||
330 context.tid <= 0 || interrupt_worker(&context) || pthread_join(worker, 0))
331 return 27;
332
333 if (context.result != -1 || context.error != EINTR || signal_calls < 1)
334 return 28;
335 return close(sockets[0]) || close(sockets[1]) ? 29 : 0;
336}
337
338static int signal_handler_close_child(enum io_operation operation) {
339 alarm(10);
340 if (install_signal_handler())
341 return 50;
342
343 int sockets[2];
344 char payload[1024];
345 memset(payload, 'h', sizeof(payload));
346 if (socketpair(AF_UNIX, socket_type, 0, sockets))
347 return 51;
348 if (operation == send_operation && fill_send_queue(sockets[0], payload, sizeof(payload)))
349 return 52;
350
351 int rights_pipe[2] = {-1, -1};
352 if (operation == send_operation && pipe(rights_pipe))
353 return 53;
354
355 const int operation_side = operation == receive_operation ? 1 : 0;
356 const int close_side = operation == receive_operation ? operation_side : 1 - operation_side;
357 struct io_context context = {
358 .descriptor = sockets[operation_side],
359 .payload = payload,
360 .length = 1,
361 .operation = operation,
362 .use_message = 1,
363 .rights_descriptor = rights_pipe[0],
364 .result = -2,
365 };
366 pthread_t worker;
367 if (pthread_create(&worker, 0, run_io, &context) || wait_for_value(&context.entered))
368 return 54;
369 for (size_t pause = 0; pause < 512; ++pause)
370 sched_yield();
371
372 __atomic_store_n(&close_from_signal, sockets[close_side], __ATOMIC_RELEASE);
373 if (__atomic_load_n(&context.returned, __ATOMIC_ACQUIRE) || context.tid <= 0 ||
374 interrupt_worker(&context) || pthread_join(worker, 0))
375 return 55;
376 if (__atomic_load_n(&signal_close_result, __ATOMIC_ACQUIRE) || context.result != -1 ||
377 context.error != EINTR || signal_calls < 1)
378 return 56;
379
380 sockets[close_side] = -1;
381 if (sockets[0] >= 0 && close(sockets[0]))
382 return 57;
383 if (sockets[1] >= 0 && close(sockets[1]))
384 return 58;
385 if (rights_pipe[0] >= 0 && close(rights_pipe[0]))
386 return 59;
387 if (rights_pipe[1] >= 0 && close(rights_pipe[1]))
388 return 60;
389 return 0;
390}
391
392static int receive_signal_handler_close_child(void) {
393 return signal_handler_close_child(receive_operation);
394}
395
396static int send_signal_handler_close_child(void) {
397 return signal_handler_close_child(send_operation);
398}
399
400static int serialized_wait_child(enum io_operation operation) {
401 alarm(10);
402 if (install_signal_handler())
403 return 40;
404
405 int sockets[2];
406 char payload[1024];
407 memset(payload, 's', sizeof(payload));
408 if (socketpair(AF_UNIX, socket_type, 0, sockets))
409 return 41;
410 if (operation == send_operation && fill_send_queue(sockets[0], payload, sizeof(payload)))
411 return 42;
412
413 const int operation_side = operation == receive_operation ? 1 : 0;
414 struct io_context holder = {
415 .descriptor = sockets[operation_side],
416 .payload = payload,
417 .length = 1,
418 .operation = operation,
419 .result = -2,
420 };
421 struct io_context waiter = {
422 .descriptor = sockets[operation_side],
423 .payload = payload,
424 .length = 1,
425 .operation = operation,
426 .result = -2,
427 };
428 pthread_t holder_thread;
429 pthread_t waiter_thread;
430 if (pthread_create(&holder_thread, 0, run_io, &holder) || wait_for_value(&holder.entered))
431 return 43;
432 for (size_t pause = 0; pause < 512; ++pause)
433 sched_yield();
434 if (__atomic_load_n(&holder.returned, __ATOMIC_ACQUIRE) ||
435 pthread_create(&waiter_thread, 0, run_io, &waiter) || wait_for_value(&waiter.entered) ||
436 waiter.tid <= 0 || interrupt_worker(&waiter) || pthread_join(waiter_thread, 0))
437 return 44;
438
439 if (waiter.result != -1 || waiter.error != EINTR || signal_calls < 1 ||
440 __atomic_load_n(&holder.returned, __ATOMIC_ACQUIRE))
441 return 45;
442 if (close(sockets[0]) || close(sockets[1]) || pthread_join(holder_thread, 0))
443 return 46;
444
445 const int holder_result_ok = operation == receive_operation
446 ? holder.result == 0
447 : holder.result == -1 && holder.error == EPIPE;
448 return holder_result_ok ? 0 : 47;
449}
450
451static int serialized_receive_wait_child(void) {
452 return serialized_wait_child(receive_operation);
453}
454
455static int serialized_send_wait_child(void) {
456 return serialized_wait_child(send_operation);
457}
458
459static int close_wake_child(enum io_operation operation, int close_local) {
460 alarm(10);
461 if (install_signal_handler())
462 return 30;
463
464 int sockets[2];
465 char payload[1024];
466 memset(payload, 'c', sizeof(payload));
467 if (socketpair(AF_UNIX, socket_type, 0, sockets))
468 return 31;
469 if (operation == send_operation && fill_send_queue(sockets[0], payload, sizeof(payload)))
470 return 32;
471
472 const int operation_side = operation == receive_operation ? 1 : 0;
473 const int close_side = close_local ? operation_side : 1 - operation_side;
474 struct io_context context = {
475 .descriptor = sockets[operation_side],
476 .payload = payload,
477 .length = 1,
478 .operation = operation,
479 .result = -2,
480 };
481 pthread_t worker;
482 if (pthread_create(&worker, 0, run_io, &context) || wait_for_value(&context.entered))
483 return 33;
484 for (size_t pause = 0; pause < 256; ++pause)
485 sched_yield();
486
487 if (close(sockets[close_side]))
488 return 34;
489 sockets[close_side] = -1;
490 const int returned_without_rescue = !wait_for_value(&context.returned);
491 if (!returned_without_rescue) {
492 if (sockets[0] >= 0)
493 close(sockets[0]);
494 if (sockets[1] >= 0)
495 close(sockets[1]);
496 (void)syscall(SYS_tkill, context.tid, SIGUSR1);
497 }
498 if (pthread_join(worker, 0))
499 return 35;
500
501 const int result_ok = operation == receive_operation
502 ? context.result == 0
503 : context.result == -1 && context.error == EPIPE;
504 if (!returned_without_rescue || !result_ok)
505 return 36;
506 if (sockets[0] >= 0 && close(sockets[0]))
507 return 37;
508 if (sockets[1] >= 0 && close(sockets[1]))
509 return 38;
510 return 0;
511}
512
513static void run_bounded(int (*child_test)(void)) {
514 const pid_t child = fork();
515 if (child < 0)
516 fail();
517 if (!child)
518 _exit(child_test());
519
520 int status_code = 0;
521 pid_t waited;
522 do {
523 waited = waitpid(child, &status_code, 0);
524 } while (waited < 0 && errno == EINTR);
525 if (waited != child || !WIFEXITED(status_code) || WEXITSTATUS(status_code)) {
526 if (waited == child && WIFEXITED(status_code))
527 fprintf(stderr, "AF_UNIX interruption child failed: code=%d\n", WEXITSTATUS(status_code));
528 fail();
529 }
530}
531
532static int peer_receive_close_child(void) {
533 return close_wake_child(receive_operation, 0);
534}
535
536static int local_receive_close_child(void) {
537 return close_wake_child(receive_operation, 1);
538}
539
540static int peer_send_close_child(void) {
541 return close_wake_child(send_operation, 0);
542}
543
544static int local_send_close_child(void) {
545 return close_wake_child(send_operation, 1);
546}
547
548static void test_unix_interruption(int type) {
549 socket_type = type;
550 printf("Testing AF_UNIX %s interruption and close wakeups... \n",
551 type == SOCK_STREAM ? "stream" : "sequenced-packet");
552 fflush(stdout);
553 run_bounded(interrupted_receive_child);
554 run_bounded(interrupted_send_child);
555 run_bounded(receive_signal_handler_close_child);
556 run_bounded(send_signal_handler_close_child);
557 run_bounded(serialized_receive_wait_child);
558 run_bounded(serialized_send_wait_child);
559 if (type == SOCK_STREAM) {
560 run_bounded(partial_send_child);
561 }
562 run_bounded(peer_receive_close_child);
563 run_bounded(local_receive_close_child);
564 run_bounded(peer_send_close_child);
565 run_bounded(local_send_close_child);
566 puts(type == SOCK_STREAM ? "OK\n" : "SEQPACKET-INTERRUPTION-PASS");
567 fflush(stdout);
568}
569
570void test_unix_stream_interruption(void) {
571 test_unix_interruption(SOCK_STREAM);
572}
573
574void test_unix_seqpacket_interruption(void) {
575 test_unix_interruption(SOCK_SEQPACKET);
576}