The Pedigree Project 0.1
messages.c
1/* Copyright (c) 2026, Pedigree Developers. See LICENSE for licensing details. */
2#define _GNU_SOURCE
3#include <errno.h>
4#include <grp.h>
5#include <limits.h>
6#include <signal.h>
7#include <stdio.h>
8#include <string.h>
9#include <time.h>
10#include <unistd.h>
11
12#include <sys/msg.h>
13#include <sys/wait.h>
14
15#ifndef MSG_COPY
16#define MSG_COPY 040000
17#endif
18
19struct message {
20 long type;
21 char bytes[8];
22};
23
24#define CHECK(condition, label) \
25 do { \
26 if (!(condition)) { \
27 printf("IPC-MESSAGES: FAIL %s errno=%d\n", label, errno); \
28 return 1; \
29 } \
30 } while (0)
31
32static int send_byte(int id, long type, char value) {
33 struct message message = {.type = type, .bytes = {value}};
34 return msgsnd(id, &message, 1, IPC_NOWAIT);
35}
36
37static int receive_byte(int id, long type, int flags, long expected_type, char value) {
38 struct message message = {0};
39 return msgrcv(id, &message, sizeof(message.bytes), type, flags | IPC_NOWAIT) == 1 &&
40 message.type == expected_type && message.bytes[0] == value;
41}
42
43static int selection(int id) {
44 struct msqid_ds status;
45 CHECK(!msgctl(id, IPC_STAT, &status), "initial status");
46 CHECK(status.msg_qnum == 0 && status.msg_cbytes == 0 && status.msg_qbytes > 0 &&
47 status.msg_perm.uid == geteuid() && status.msg_perm.cuid == geteuid() &&
48 !status.msg_lspid && !status.msg_lrpid && !status.msg_stime && !status.msg_rtime,
49 "initial queue metadata");
50 CHECK(msgget(status.msg_perm.__ipc_perm_key, 0600) == id, "named queue lookup");
51 errno = 0;
52 CHECK(
53 msgget(status.msg_perm.__ipc_perm_key, IPC_CREAT | IPC_EXCL | 0600) == -1 && errno == EEXIST,
54 "exclusive queue creation");
55 int private_id = msgget(IPC_PRIVATE, 0600);
56 CHECK(private_id >= 0 && private_id != id, "private queue identity");
57 CHECK(!msgctl(private_id, IPC_RMID, NULL), "private queue removal");
58 struct msginfo info;
59 int highest = msgctl(0, MSG_INFO, (struct msqid_ds*)&info);
60 CHECK(highest >= 0 && info.msgpool >= 1 && info.msgmax >= 8 && info.msgmni >= 1,
61 "queue usage query");
62 int found = 0;
63 for (int slot = 0; slot <= highest; ++slot) {
64 if (msgctl(slot, MSG_STAT, &status) == id) {
65 CHECK(msgctl(slot, MSG_STAT_ANY, &status) == id, "unrestricted index query");
66 found = 1;
67 break;
68 }
69 }
70 CHECK(found, "queue index enumeration");
71 CHECK(msgctl(0, IPC_INFO, (struct msqid_ds*)&info) >= 0 && info.msgmnb > 0, "queue limits query");
72 CHECK(!send_byte(id, 7, 'a') && !send_byte(id, 2, 'b') && !send_byte(id, 2, 'c') &&
73 !send_byte(id, 5, 'd'),
74 "queue typed records");
75 CHECK(!msgctl(id, IPC_STAT, &status) && status.msg_qnum == 4 && status.msg_cbytes == 4 &&
76 status.msg_lspid == getpid(),
77 "send metadata");
78 CHECK(receive_byte(id, 1, MSG_COPY, 2, 'b'), "ordinal nondestructive copy");
79 CHECK(!msgctl(id, IPC_STAT, &status) && status.msg_qnum == 4 && status.msg_lrpid == 0,
80 "copy preserves metadata");
81 CHECK(receive_byte(id, -6, 0, 2, 'b'), "negative selector lowest type");
82 CHECK(receive_byte(id, 2, 0, 2, 'c'), "equal type FIFO order");
83 CHECK(receive_byte(id, 7, MSG_EXCEPT, 5, 'd'), "excluded type");
84 CHECK(receive_byte(id, 0, 0, 7, 'a'), "zero selector FIFO");
85 CHECK(!send_byte(id, LONG_MAX, 'z') && receive_byte(id, LONG_MIN, 0, LONG_MAX, 'z'),
86 "minimum signed selector");
87 CHECK(!msgctl(id, IPC_STAT, &status) && !status.msg_qnum && !status.msg_cbytes &&
88 status.msg_lrpid == getpid(),
89 "receive metadata");
90 struct message message = {.type = 1};
91 errno = 0;
92 CHECK(msgrcv(id, &message, sizeof(message.bytes), 0, IPC_NOWAIT) == -1 && errno == ENOMSG,
93 "empty queue nonblocking receive");
94 CHECK(!msgsnd(id, &message, 0, IPC_NOWAIT) && msgrcv(id, &message, 0, 0, IPC_NOWAIT) == 0,
95 "zero length message");
96 message.type = 0;
97 errno = 0;
98 CHECK(msgsnd(id, &message, 0, IPC_NOWAIT) == -1 && errno == EINVAL, "invalid message type");
99 return 0;
100}
101
102static int copies(int id) {
103 struct message message = {.type = 3, .bytes = "text"};
104 CHECK(!msgsnd(id, &message, 4, IPC_NOWAIT), "queue complete payload");
105 errno = 0;
106 CHECK(msgrcv(id, &message, 2, 0, IPC_NOWAIT) == -1 && errno == E2BIG,
107 "short receive preserves message");
108 errno = 0;
109 CHECK(msgrcv(id, NULL, 4, 0, IPC_NOWAIT) == -1 && errno == EFAULT,
110 "invalid destination rejected");
111 struct msqid_ds status;
112 CHECK(!msgctl(id, IPC_STAT, &status) && status.msg_qnum == 1 && status.msg_cbytes == 4,
113 "failed copy preserves message");
114 memset(&message, 0, sizeof(message));
115 CHECK(msgrcv(id, &message, 2, 0, MSG_NOERROR | IPC_NOWAIT) == 2 && message.type == 3 &&
116 !memcmp(message.bytes, "te", 2) && message.bytes[2] == 0,
117 "explicit truncation");
118 CHECK(!msgctl(id, IPC_STAT, &status) && status.msg_qnum == 0,
119 "truncation consumes complete message");
120 errno = 0;
121 CHECK(msgsnd(id, NULL, 1, IPC_NOWAIT) == -1 && errno == EFAULT, "invalid sender buffer");
122 errno = 0;
123 CHECK(msgctl(id, IPC_STAT, NULL) == -1 && errno == EFAULT, "invalid status buffer");
124 errno = 0;
125 CHECK(msgrcv(id, &message, 0, 0, MSG_COPY) == -1 && errno == EINVAL, "copy requires nonblocking");
126 status.msg_qbytes = 2;
127 CHECK(!msgctl(id, IPC_SET, &status), "limit zero length records");
128 message.type = 1;
129 CHECK(!msgsnd(id, &message, 0, IPC_NOWAIT) && !msgsnd(id, &message, 0, IPC_NOWAIT),
130 "fill queue with zero length records");
131 errno = 0;
132 CHECK(msgsnd(id, &message, 0, IPC_NOWAIT) == -1 && errno == EAGAIN,
133 "zero length records consume queue capacity");
134 CHECK(msgrcv(id, &message, 0, 0, IPC_NOWAIT) == 0 && msgrcv(id, &message, 0, 0, IPC_NOWAIT) == 0,
135 "drain zero length records");
136 return 0;
137}
138
139static int wait_child(pid_t child) {
140 int status = 0;
141 pid_t result;
142 do {
143 result = waitpid(child, &status, 0);
144 } while (result < 0 && errno == EINTR);
145 return result == child && WIFEXITED(status) && WEXITSTATUS(status) == 0;
146}
147
148static int wait_ready(int descriptor, pid_t child) {
149 char ready = 0;
150 int status = 0;
151 const struct timespec delay = {.tv_nsec = 100000000};
152 if (read(descriptor, &ready, 1) != 1 || ready != 'r' || nanosleep(&delay, NULL)) {
153 return 0;
154 }
155 return waitpid(child, &status, WNOHANG) == 0;
156}
157
158/* Each peer announces entry and has its own terminal timeout. */
159static int blocking(int id, int sending, int removing, int terminating) {
160 struct msqid_ds status;
161 CHECK(!msgctl(id, IPC_STAT, &status), "read queue capacity");
162 status.msg_qbytes = 1;
163 CHECK(!msgctl(id, IPC_SET, &status), "set queue capacity");
164 if (sending) {
165 CHECK(!send_byte(id, 1, 'a'), "fill queue");
166 errno = 0;
167 CHECK(send_byte(id, 1, 'b') == -1 && errno == EAGAIN, "full queue nonblocking send");
168 }
169 int ready[2];
170 CHECK(!pipe(ready), "peer readiness pipe");
171 pid_t child = fork();
172 CHECK(child >= 0, "fork blocking peer");
173 if (!child) {
174 close(ready[0]);
175 alarm(4);
176 struct message message = {.type = 1, .bytes = {'b'}};
177 if (write(ready[1], "r", 1) != 1) {
178 _exit(2);
179 }
180 close(ready[1]);
181 errno = 0;
182 ssize_t result = sending ? msgsnd(id, &message, 1, 0) : msgrcv(id, &message, 1, 0, 0);
183 int error = errno;
184 if (removing) {
185 _exit(result == -1 && error == EIDRM ? 0 : 3);
186 }
187 _exit((sending ? result == 0 : result == 1 && message.bytes[0] == 'a') ? 0 : 4);
188 }
189 close(ready[1]);
190 CHECK(wait_ready(ready[0], child), "peer remains blocked");
191 close(ready[0]);
192 if (terminating) {
193 CHECK(!kill(child, SIGKILL), "terminate blocked peer");
194 int child_status;
195 CHECK(waitpid(child, &child_status, 0) == child && WIFSIGNALED(child_status) &&
196 WTERMSIG(child_status) == SIGKILL,
197 "reap terminated peer");
198 CHECK(!send_byte(id, 1, 'k') && receive_byte(id, 0, 0, 1, 'k'),
199 "queue survives peer termination");
200 return 0;
201 }
202 if (removing) {
203 CHECK(!msgctl(id, IPC_RMID, NULL), "remove queue with blocked peer");
204 } else if (sending) {
205 CHECK(receive_byte(id, 0, 0, 1, 'a'), "receive releases sender capacity");
206 } else {
207 CHECK(!send_byte(id, 1, 'a'), "send wakes receiver");
208 }
209 CHECK(wait_child(child), "blocking peer result");
210 if (sending && !removing) {
211 CHECK(receive_byte(id, 0, 0, 1, 'b'), "blocked sender delivered payload");
212 }
213 if (removing) {
214 errno = 0;
215 CHECK(msgctl(id, IPC_STAT, &status) == -1 && errno == EINVAL, "removed identifier retired");
216 }
217 return 0;
218}
219
220static volatile sig_atomic_t interrupted;
221static void interrupt_handler(int signal_number) {
222 if (signal_number == SIGALRM) {
223 if (++interrupted >= 3) {
224 _exit(124);
225 }
226 alarm(1);
227 }
228}
229
230static int interruption(int id, int sending) {
231 struct sigaction action = {.sa_handler = interrupt_handler, .sa_flags = SA_RESTART};
232 CHECK(!sigemptyset(&action.sa_mask) && !sigaction(SIGALRM, &action, NULL),
233 "install interrupt handler");
234 if (sending) {
235 struct msqid_ds status;
236 CHECK(!msgctl(id, IPC_STAT, &status), "interrupted sender status");
237 status.msg_qbytes = 1;
238 CHECK(!msgctl(id, IPC_SET, &status) && !send_byte(id, 1, 'a'), "fill interrupted sender");
239 }
240 struct message message = {.type = 1, .bytes = {'b'}};
241 interrupted = 0;
242 alarm(1);
243 errno = 0;
244 ssize_t result = sending ? msgsnd(id, &message, 1, 0) : msgrcv(id, &message, 1, 0, 0);
245 int error = errno;
246 alarm(0);
247 CHECK(result == -1 && error == EINTR && interrupted, "IPC is interrupted with SA_RESTART");
248 if (sending) {
249 CHECK(receive_byte(id, 0, 0, 1, 'a'), "interrupted sender preserved queued payload");
250 }
251 return 0;
252}
253
254static int permissions(int id) {
255 if (geteuid() != 0) {
256 puts("IPC-MESSAGES: SKIP alternate credentials requires root");
257 return 0;
258 }
259 struct msqid_ds status;
260 CHECK(!msgctl(id, IPC_STAT, &status), "permission status");
261 pid_t child = fork();
262 CHECK(child >= 0, "fork credential peer");
263 if (!child) {
264 alarm(4);
265 if (setgroups(0, NULL) || setgid(65534) || setuid(65534)) {
266 _exit(2);
267 }
268 struct message message = {.type = 1};
269 errno = 0;
270 if (msgget(status.msg_perm.__ipc_perm_key, 0600) != -1 || errno != EACCES) {
271 _exit(3);
272 }
273 errno = 0;
274 if (msgsnd(id, &message, 0, IPC_NOWAIT) != -1 || errno != EACCES) {
275 _exit(4);
276 }
277 errno = 0;
278 if (msgrcv(id, &message, 0, 0, IPC_NOWAIT) != -1 || errno != EACCES) {
279 _exit(5);
280 }
281 errno = 0;
282 if (msgctl(id, IPC_STAT, &status) != -1 || errno != EACCES) {
283 _exit(6);
284 }
285 errno = 0;
286 if (msgctl(id, IPC_SET, &status) != -1 || errno != EPERM) {
287 _exit(7);
288 }
289 errno = 0;
290 _exit(msgctl(id, IPC_RMID, NULL) == -1 && errno == EPERM ? 0 : 8);
291 }
292 CHECK(wait_child(child), "enforce queue permissions");
293 return 0;
294}
295
296int ipc_test_messages(void) {
297 static const char* names[] = {
298 "selection", "copies", "send-wait", "receive-wait", "send-remove",
299 "receive-remove", "send-interrupt", "receive-interrupt", "permissions", "peer-termination"};
300 for (unsigned int test = 0; test < sizeof(names) / sizeof(names[0]); ++test) {
301 key_t key = (key_t)(0x4d000000U | ((unsigned int)getpid() & 0xffffU) << 8 | test);
302 int id = msgget(key, IPC_CREAT | IPC_EXCL | 0600);
303 CHECK(id >= 0, "create case queue");
304 pid_t child = fork();
305 if (!child) {
306 alarm(8);
307 int result;
308 switch (test) {
309 case 0:
310 result = selection(id);
311 break;
312 case 1:
313 result = copies(id);
314 break;
315 case 2:
316 result = blocking(id, 1, 0, 0);
317 break;
318 case 3:
319 result = blocking(id, 0, 0, 0);
320 break;
321 case 4:
322 result = blocking(id, 1, 1, 0);
323 break;
324 case 5:
325 result = blocking(id, 0, 1, 0);
326 break;
327 case 6:
328 result = interruption(id, 1);
329 break;
330 case 7:
331 result = interruption(id, 0);
332 break;
333 case 8:
334 result = permissions(id);
335 break;
336 default:
337 result = blocking(id, 0, 0, 1);
338 break;
339 }
340 _exit(result);
341 }
342 int passed = child > 0 && wait_child(child);
343 int removed = msgctl(id, IPC_RMID, NULL);
344 if (removed && errno != EINVAL) {
345 passed = 0;
346 }
347 printf("IPC-MESSAGES: %s %s\n", passed ? "PASS" : "FAIL", names[test]);
348 if (!passed) {
349 return 1;
350 }
351 }
352 return 0;
353}