The Pedigree Project 0.1
vhangup-contract-test/main.c
1/* Copyright (c) 2026, Pedigree Developers. */
2#define _GNU_SOURCE
3#include <errno.h>
4#include <fcntl.h>
5#include <poll.h>
6#include <pthread.h>
7#include <signal.h>
8#include <stdio.h>
9#include <string.h>
10#include <termios.h>
11#include <unistd.h>
12
13#include <sys/epoll.h>
14#include <sys/ioctl.h>
15#include <sys/socket.h>
16#include <sys/syscall.h>
17#include <sys/wait.h>
18
19#define CHECK(expression) \
20 do { \
21 if (!(expression)) { \
22 fprintf(stderr, "VHANGUP-CONTRACT: FAIL line=%d errno=%d\n", __LINE__, errno); \
23 return 1; \
24 } \
25 } while (0)
26
27static volatile sig_atomic_t hangups, continuations;
28static volatile sig_atomic_t control_interrupts, control_error;
29static int control_terminal = -1;
30
31static void control_handler(int signal) {
32 (void)signal;
33 int saved_errno = errno;
34 struct winsize size;
35 if (ioctl(control_terminal, TIOCGWINSZ, &size) || tcgetpgrp(control_terminal) != getpgrp() ||
36 tcsetpgrp(control_terminal, getpgrp()) || syscall(SYS_vhangup))
37 control_error = 1;
38 ++control_interrupts;
39 errno = saved_errno;
40}
41static void signal_handler(int signal) {
42 if (signal == SIGHUP)
43 ++hangups;
44 if (signal == SIGCONT)
45 ++continuations;
46}
47
48struct blocked_io {
49 int fd;
50 int writing;
51 ssize_t result;
52 int error;
53};
54
55static void* blocked_io(void* argument) {
56 struct blocked_io* io = argument;
57 char data[16] = "blocked";
58 io->result = io->writing ? write(io->fd, data, sizeof(data)) : read(io->fd, data, sizeof(data));
59 io->error = errno;
60 return NULL;
61}
62
63static int transferred_fd(int fd) {
64 int sockets[2];
65 if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockets))
66 return -1;
67 union {
68 struct cmsghdr alignment;
69 char data[CMSG_SPACE(sizeof(int))];
70 } control = {0};
71 char byte = 'f';
72 struct iovec vector = {&byte, 1};
73 struct msghdr message = {.msg_iov = &vector,
74 .msg_iovlen = 1,
75 .msg_control = control.data,
76 .msg_controllen = sizeof(control)};
77 struct cmsghdr* header = CMSG_FIRSTHDR(&message);
78 header->cmsg_level = SOL_SOCKET;
79 header->cmsg_type = SCM_RIGHTS;
80 header->cmsg_len = CMSG_LEN(sizeof(int));
81 memcpy(CMSG_DATA(header), &fd, sizeof(fd));
82 int received = -1;
83 if (sendmsg(sockets[0], &message, 0) == 1 && recvmsg(sockets[1], &message, 0) == 1) {
84 header = CMSG_FIRSTHDR(&message);
85 if (header && header->cmsg_level == SOL_SOCKET && header->cmsg_type == SCM_RIGHTS)
86 memcpy(&received, CMSG_DATA(header), sizeof(received));
87 }
88 close(sockets[0]);
89 close(sockets[1]);
90 return received;
91}
92
93static int revoked_descriptor(int fd) {
94 char byte;
95 struct winsize size;
96 CHECK(read(fd, &byte, 1) == 0);
97 CHECK(write(fd, "x", 1) == -1 && errno == EIO);
98 CHECK(ioctl(fd, TIOCGWINSZ, &size) == -1 && errno == EIO);
99 CHECK(tcsetpgrp(fd, getpgrp()) == -1 && errno == ENOTTY);
100 struct pollfd pollfd = {.fd = fd, .events = POLLIN | POLLOUT};
101 CHECK(poll(&pollfd, 1, 0) == 1);
102 CHECK((pollfd.revents & (POLLIN | POLLOUT | POLLERR | POLLHUP)) ==
103 (POLLIN | POLLOUT | POLLERR | POLLHUP));
104 return 0;
105}
106
107static int control_character_test(void) {
108 puts("VHANGUP-CONTRACT: control-character");
109 int master = open("/dev/ptmx", O_RDWR | O_NOCTTY);
110 CHECK(master >= 0);
111 unsigned int number;
112 CHECK(ioctl(master, TIOCGPTN, &number) == 0);
113 char path[64];
114 snprintf(path, sizeof(path), "/dev/pts/%u", number);
115 int slave = open(path, O_RDWR | O_NOCTTY);
116 CHECK(slave >= 0 && ioctl(slave, TIOCSCTTY, 0) == 0);
117 struct termios attributes;
118 CHECK(tcgetattr(slave, &attributes) == 0);
119 cfmakeraw(&attributes);
120 attributes.c_lflag |= ISIG;
121 CHECK(tcsetattr(slave, TCSANOW, &attributes) == 0);
122 struct sigaction action = {.sa_handler = control_handler};
123 sigemptyset(&action.sa_mask);
124 CHECK(sigaction(SIGINT, &action, NULL) == 0);
125 control_terminal = slave;
126 char interrupt = attributes.c_cc[VINTR];
127 CHECK(interrupt && write(master, &interrupt, 1) == 1);
128 for (int i = 0; i < 100 && !control_interrupts; ++i)
129 usleep(10000);
130 CHECK(control_interrupts == 1 && !control_error);
131 CHECK(hangups == 1 && continuations == 1);
132 CHECK(revoked_descriptor(slave) == 0);
133 CHECK(open("/dev/tty", O_RDWR) == -1 && errno == ENXIO);
134 CHECK(close(slave) == 0 && close(master) == 0);
135 control_terminal = -1;
136 hangups = continuations = 0;
137 puts("VHANGUP-CONTRACT: control-character-reentry-pass");
138 return 0;
139}
140
141static int session_test(void) {
142 alarm(25);
143 CHECK(setsid() == getpid());
144 CHECK(syscall(SYS_vhangup) == 0);
145 pid_t unprivileged = fork();
146 CHECK(unprivileged >= 0);
147 if (!unprivileged) {
148 if (seteuid(1000))
149 _exit(2);
150 _exit(syscall(SYS_vhangup) == -1 && errno == EPERM ? 0 : 3);
151 }
152 int status;
153 CHECK(waitpid(unprivileged, &status, 0) == unprivileged && WIFEXITED(status) &&
154 WEXITSTATUS(status) == 0);
155 struct sigaction action = {.sa_handler = signal_handler};
156 sigemptyset(&action.sa_mask);
157 CHECK(sigaction(SIGHUP, &action, NULL) == 0 && sigaction(SIGCONT, &action, NULL) == 0);
158 CHECK(control_character_test() == 0);
159
160 puts("VHANGUP-CONTRACT: private-pty");
161 int master = open("/dev/ptmx", O_RDWR | O_NOCTTY);
162 CHECK(master >= 0);
163 unsigned int number;
164 CHECK(ioctl(master, TIOCGPTN, &number) == 0);
165 char path[64];
166 snprintf(path, sizeof(path), "/dev/pts/%u", number);
167 int slave = open(path, O_RDWR | O_NOCTTY);
168 CHECK(slave >= 0);
169 CHECK(open("/dev/tty", O_RDWR) == -1 && errno == ENXIO);
170 CHECK(ioctl(slave, TIOCSCTTY, 0) == 0);
171 CHECK(tcgetpgrp(slave) == getpgrp());
172 struct termios attributes;
173 CHECK(tcgetattr(slave, &attributes) == 0);
174 cfmakeraw(&attributes);
175 CHECK(tcsetattr(slave, TCSANOW, &attributes) == 0);
176 int duplicate = dup(slave), passed = transferred_fd(slave);
177 CHECK(duplicate >= 0 && passed >= 0);
178
179 pid_t foreign = fork();
180 CHECK(foreign >= 0);
181 if (!foreign) {
182 if (setsid() != getpid())
183 _exit(4);
184 int other = open(path, O_RDWR | O_NOCTTY);
185 if (other < 0)
186 _exit(5);
187 _exit(ioctl(other, TIOCSCTTY, 0) == -1 && errno == EPERM ? 0 : 6);
188 }
189 CHECK(waitpid(foreign, &status, 0) == foreign && WIFEXITED(status) && WEXITSTATUS(status) == 0);
190 puts("VHANGUP-CONTRACT: foreign-session-refused");
191 CHECK(tcsetpgrp(slave, 0) == -1 && errno == EPERM);
192
193 int commands[2], ready[2];
194 CHECK(pipe(commands) == 0 && pipe(ready) == 0);
195 puts("VHANGUP-CONTRACT: foreground-fork");
196 pid_t child = fork();
197 CHECK(child >= 0);
198 if (!child) {
199 close(commands[1]);
200 close(ready[0]);
201 if (setpgid(0, 0) || write(ready[1], "r", 1) != 1)
202 _exit(7);
203 char byte;
204 if (read(commands[0], &byte, 1) != 1)
205 _exit(8);
206 if (hangups || continuations || open("/dev/tty", O_RDWR) != -1 || errno != ENXIO ||
207 revoked_descriptor(slave))
208 _exit(9);
209 _exit(0);
210 }
211 close(commands[0]);
212 close(ready[1]);
213 char byte;
214 CHECK(read(ready[0], &byte, 1) == 1);
215 puts("VHANGUP-CONTRACT: foreground-assign");
216 CHECK(tcsetpgrp(slave, child) == 0 && tcgetpgrp(slave) == child);
217
218 int epoll = epoll_create1(EPOLL_CLOEXEC);
219 CHECK(epoll >= 0);
220 struct epoll_event event = {.events = EPOLLIN | EPOLLOUT | EPOLLET, .data.fd = slave};
221 puts("VHANGUP-CONTRACT: epoll-add");
222 CHECK(epoll_ctl(epoll, EPOLL_CTL_ADD, slave, &event) == 0);
223 puts("VHANGUP-CONTRACT: epoll-initial");
224 CHECK(epoll_wait(epoll, &event, 1, 0) == 1 && (event.events & EPOLLOUT));
225 CHECK(fcntl(slave, F_SETFL, O_NONBLOCK) == 0);
226 char fill[512];
227 memset(fill, 'q', sizeof(fill));
228 puts("VHANGUP-CONTRACT: fill-output");
229 size_t total = 0;
230 for (;;) {
231 ssize_t amount = write(slave, fill, sizeof(fill));
232 if (amount < 0) {
233 CHECK(errno == EAGAIN);
234 break;
235 }
236 CHECK(amount > 0);
237 total += amount;
238 CHECK(total < 16 * 1024 * 1024);
239 }
240 puts("VHANGUP-CONTRACT: output-full");
241 CHECK(fcntl(slave, F_SETFL, 0) == 0);
242 struct blocked_io reader = {.fd = duplicate, .result = -2};
243 struct blocked_io writer = {.fd = passed, .writing = 1, .result = -2};
244 pthread_t read_thread, write_thread;
245 CHECK(pthread_create(&read_thread, NULL, blocked_io, &reader) == 0);
246 CHECK(pthread_create(&write_thread, NULL, blocked_io, &writer) == 0);
247 usleep(50000);
248 puts("VHANGUP-CONTRACT: revoke");
249 CHECK(syscall(SYS_vhangup) == 0);
250 CHECK(pthread_join(read_thread, NULL) == 0 && pthread_join(write_thread, NULL) == 0);
251 CHECK(reader.result == 0 && writer.result == -1 && writer.error == EIO);
252 CHECK(hangups == 1 && continuations == 1);
253 CHECK(revoked_descriptor(slave) == 0 && revoked_descriptor(duplicate) == 0 &&
254 revoked_descriptor(passed) == 0);
255 CHECK(epoll_wait(epoll, &event, 1, 0) == 1 &&
256 (event.events & (EPOLLIN | EPOLLERR | EPOLLHUP)) == (EPOLLIN | EPOLLERR | EPOLLHUP));
257 CHECK(epoll_wait(epoll, &event, 1, 0) == 0);
258 CHECK(open("/dev/tty", O_RDWR) == -1 && errno == ENXIO);
259 CHECK(write(commands[1], "g", 1) == 1);
260 CHECK(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0);
261
262 puts("VHANGUP-CONTRACT: reopen");
263 int fresh = open(path, O_RDWR | O_NOCTTY);
264 CHECK(fresh >= 0);
265 struct pollfd pollfd = {.fd = fresh, .events = POLLIN | POLLOUT};
266 CHECK(poll(&pollfd, 1, 0) == 1 && !(pollfd.revents & (POLLERR | POLLHUP)));
267 CHECK(tcgetattr(fresh, &attributes) == 0 && (attributes.c_lflag & ICANON));
268 cfmakeraw(&attributes);
269 CHECK(tcsetattr(fresh, TCSANOW, &attributes) == 0);
270 CHECK(write(master, "new", 3) == 3);
271 char data[4];
272 CHECK(read(fresh, data, 3) == 3 && !memcmp(data, "new", 3));
273 CHECK(write(fresh, "out", 3) == 3);
274 CHECK(read(master, data, 3) == 3 && !memcmp(data, "out", 3));
275 CHECK(ioctl(fresh, TIOCSCTTY, 0) == 0 && tcgetpgrp(fresh) == getpgrp());
276 CHECK(revoked_descriptor(slave) == 0);
277 CHECK(syscall(SYS_vhangup) == 0);
278 CHECK(close(fresh) == 0 && close(master) == 0 && close(slave) == 0 && close(duplicate) == 0 &&
279 close(passed) == 0 && close(epoll) == 0);
280 close(commands[1]);
281 close(ready[0]);
282 return 0;
283}
284
285static int session_exit_test(void) {
286 puts("VHANGUP-CONTRACT: session-exit");
287 int master = open("/dev/ptmx", O_RDWR | O_NOCTTY);
288 CHECK(master >= 0);
289 unsigned int number;
290 CHECK(ioctl(master, TIOCGPTN, &number) == 0);
291 char path[64];
292 snprintf(path, sizeof(path), "/dev/pts/%u", number);
293 int slave = open(path, O_RDWR | O_NOCTTY);
294 CHECK(slave >= 0);
295 pid_t leader = fork();
296 CHECK(leader >= 0);
297 if (!leader) {
298 if (setsid() != getpid() || ioctl(slave, TIOCSCTTY, 0))
299 _exit(2);
300 _exit(0);
301 }
302 int status;
303 CHECK(waitpid(leader, &status, 0) == leader && WIFEXITED(status) && WEXITSTATUS(status) == 0);
304 CHECK(revoked_descriptor(slave) == 0);
305 int fresh = open(path, O_RDWR | O_NOCTTY);
306 CHECK(fresh >= 0);
307 struct termios attributes;
308 CHECK(tcgetattr(fresh, &attributes) == 0);
309 cfmakeraw(&attributes);
310 CHECK(tcsetattr(fresh, TCSANOW, &attributes) == 0);
311 CHECK(write(master, "e", 1) == 1);
312 char byte;
313 CHECK(read(fresh, &byte, 1) == 1 && byte == 'e');
314 CHECK(close(fresh) == 0 && close(slave) == 0 && close(master) == 0);
315 return 0;
316}
317
318int main(void) {
319 setvbuf(stdout, NULL, _IONBF, 0);
320 alarm(30);
321 CHECK(geteuid() == 0);
322 pid_t child = fork();
323 CHECK(child >= 0);
324 if (!child)
325 _exit(session_test());
326 int status;
327 CHECK(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0);
328 CHECK(session_exit_test() == 0);
329 puts("VHANGUP-CONTRACT: PASS");
330 return 0;
331}
Definition waits.c:9