The Pedigree Project 0.1
pty-contracts.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 <fcntl.h>
12#include <poll.h>
13#include <pty.h>
14#include <signal.h>
15#include <stdio.h>
16#include <stdlib.h>
17#include <string.h>
18#include <termios.h>
19#include <unistd.h>
20
21#include <sys/ioctl.h>
22#include <sys/wait.h>
23
24extern void fail(void) __attribute__((noreturn));
25
26static void require(int condition, const char* operation) {
27 if (!condition) {
28 printf("PTY-CONTRACT: FAIL %s errno=%d\n", operation, errno);
29 fail();
30 }
31}
32
33static void make_raw(int descriptor) {
34 struct termios attributes;
35 require(tcgetattr(descriptor, &attributes) == 0, "tcgetattr");
36 cfmakeraw(&attributes);
37 require(tcsetattr(descriptor, TCSANOW, &attributes) == 0, "tcsetattr");
38}
39
40static void exchange_byte(int writer, int reader, char expected, const char* operation) {
41 char received = 0;
42 int available = -1;
43 require(ioctl(reader, FIONREAD, &available) == 0 && available == 0, "empty PTY FIONREAD");
44 require(write(writer, &expected, 1) == 1, operation);
45 require(ioctl(reader, FIONREAD, &available) == 0 && available == 1, "queued PTY FIONREAD");
46 require(ioctl(reader, FIONREAD, &available) == 0 && available == 1,
47 "PTY FIONREAD preserves input");
48 errno = 0;
49 ssize_t amount = read(reader, &received, 1);
50 require(amount == 1 && received == expected, operation);
51 require(ioctl(reader, FIONREAD, &available) == 0 && available == 0, "drained PTY FIONREAD");
52}
53
54static void read_exact(int descriptor, char* buffer, size_t length, const char* operation) {
55 size_t total = 0;
56 while (total < length) {
57 ssize_t amount = read(descriptor, buffer + total, length - total);
58 require(amount > 0, operation);
59 total += (size_t)amount;
60 }
61}
62
63static void test_posix_openpt_contract(void) {
64 int master = posix_openpt(O_RDWR | O_NOCTTY | O_CLOEXEC);
65 require(master >= 0, "posix_openpt");
66 require(grantpt(master) == 0, "grantpt");
67
68 char path[64];
69 require(ptsname_r(master, path, sizeof(path)) == 0, "ptsname_r");
70
71 int locked = 1;
72 require(ioctl(master, TIOCSPTLCK, &locked) == 0, "lock PTY slave");
73 errno = 0;
74 int slave = open(path, O_RDWR | O_NOCTTY | O_CLOEXEC);
75 require(slave < 0 && errno == EIO, "locked PTY slave rejection");
76
77 locked = 0;
78 require(unlockpt(master) == 0, "unlockpt");
79 slave = open(path, O_RDWR | O_NOCTTY | O_CLOEXEC);
80 require(slave >= 0, "unlocked PTY slave");
81 make_raw(slave);
82 exchange_byte(master, slave, 'm', "master-to-slave");
83 exchange_byte(slave, master, 's', "slave-to-master");
84
85 close(slave);
86 close(master);
87}
88
89static void test_openpty_contract(void) {
90 int master = -1;
91 int slave = -1;
92 char path[64] = {0};
93 struct winsize size = {.ws_row = 25, .ws_col = 80};
94 require(openpty(&master, &slave, path, NULL, &size) == 0, "openpty");
95 require(master >= 0 && slave >= 0 && path[0] == '/', "openpty outputs");
96 make_raw(slave);
97 exchange_byte(master, slave, 'o', "openpty master-to-slave");
98 exchange_byte(slave, master, 'p', "openpty slave-to-master");
99 close(slave);
100 close(master);
101}
102
103static void test_ioctl_contracts(void) {
104 int master = -1;
105 int slave = -1;
106 require(openpty(&master, &slave, NULL, NULL, NULL) == 0, "ioctl openpty");
107 make_raw(slave);
108 int session = -1;
109 errno = 0;
110 require(ioctl(slave, TIOCGSID, &session) == -1 && errno == ENOTTY, "unattached slave TIOCGSID");
111 errno = 0;
112 require(ioctl(master, TIOCGSID, &session) == -1 && errno == ENOTTY, "unattached master TIOCGSID");
113 require(write(master, "f", 1) == 1, "invalid flush input");
114 errno = 0;
115 require(ioctl(slave, TCFLSH, 99) == -1 && errno == EINVAL, "TCFLSH invalid selector");
116 errno = 0;
117 require(tcflush(slave, -1) == -1 && errno == EINVAL, "tcflush negative selector");
118 char received = 0;
119 require(read(slave, &received, 1) == 1 && received == 'f', "invalid flush preserves input");
120
121 int ready[2];
122 int release[2];
123 require(pipe(ready) == 0 && pipe(release) == 0, "session query pipes");
124 pid_t child = fork();
125 require(child >= 0, "session query fork");
126 if (child == 0) {
127 close(master);
128 close(ready[0]);
129 close(release[1]);
130 require(setsid() == getpid(), "session query setsid");
131 require(ioctl(slave, TIOCSCTTY, 0) == 0, "session query TIOCSCTTY");
132 require(ioctl(slave, TIOCGSID, &session) == 0 && session == getsid(0),
133 "controlling slave TIOCGSID");
134 errno = 0;
135 require(ioctl(slave, TIOCGSID, (void*)1) == -1 && errno == EFAULT, "TIOCGSID bad address");
136 require(write(ready[1], "r", 1) == 1, "session query ready");
137 read_exact(release[0], &received, 1, "session query release");
138 _exit(0);
139 }
140 close(ready[1]);
141 close(release[0]);
142 read_exact(ready[0], &received, 1, "session query child ready");
143 require(ioctl(master, TIOCGSID, &session) == 0 && session == child,
144 "master queries slave session");
145 errno = 0;
146 require(ioctl(slave, TIOCGSID, &session) == -1 && errno == ENOTTY,
147 "noncontrolling slave TIOCGSID");
148 require(write(release[1], "r", 1) == 1, "session query release child");
149 int status = 0;
150 require(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0,
151 "session query child status");
152 errno = 0;
153 require(ioctl(master, TIOCGSID, &session) == -1 && errno == ENOTTY,
154 "TIOCGSID after session exit");
155 close(ready[0]);
156 close(release[1]);
157 close(slave);
158 close(master);
159}
160
161static void test_bulk_transfer(int from_master, int blocking, int signals) {
162 int master = -1;
163 int slave = -1;
164 require(openpty(&master, &slave, NULL, NULL, NULL) == 0, "bulk openpty");
165 make_raw(slave);
166 if (signals) {
167 struct termios attributes;
168 require(tcgetattr(slave, &attributes) == 0, "bulk tcgetattr");
169 attributes.c_lflag |= ISIG;
170 require(tcsetattr(slave, TCSANOW, &attributes) == 0, "bulk ISIG");
171 }
172 int writer = from_master ? master : slave;
173 int reader = from_master ? slave : master;
174 require(fcntl(reader, F_SETFL, O_NONBLOCK) == 0, "bulk nonblocking reader");
175
176 // Cross the PTY capacity and preserve both bracketed-paste delimiters.
177 const size_t length = 128 * 1024 + 13;
178 char* payload = malloc(length);
179 require(payload != NULL, "bulk allocation");
180 for (size_t i = 0; i < length; ++i) {
181 payload[i] = ' ' + (i % 95);
182 }
183 memcpy(payload, "\033[200~", 6);
184 memcpy(payload + length - 6, "\033[201~", 6);
185
186 pid_t child = -1;
187 if (blocking) {
188 child = fork();
189 require(child >= 0, "bulk fork");
190 if (child == 0) {
191 close(reader);
192 size_t sent = 0;
193 while (sent < length) {
194 ssize_t amount = write(writer, payload + sent, length - sent);
195 if (amount <= 0) {
196 _exit(111);
197 }
198 sent += (size_t)amount;
199 }
200 _exit(0);
201 }
202 close(writer);
203 } else {
204 require(fcntl(writer, F_SETFL, O_NONBLOCK) == 0, "bulk nonblocking writer");
205 }
206
207 size_t sent = 0;
208 size_t received = 0;
209 while (received < length) {
210 if (!blocking) {
211 ssize_t amount = write(writer, payload + sent, length - sent);
212 require(amount > 0, "bulk write progress");
213 sent += (size_t)amount;
214 }
215 size_t target = blocking ? length : sent;
216 while (received < target) {
217 struct pollfd descriptor = {.fd = reader, .events = POLLIN};
218 if (poll(&descriptor, 1, 2000) != 1 || !(descriptor.revents & POLLIN)) {
219 if (child > 0) {
220 kill(child, SIGKILL);
221 waitpid(child, NULL, 0);
222 }
223 printf("\nPTY bulk: master=%d blocking=%d signals=%d sent=%zu received=%zu\n", from_master,
224 blocking, signals, sent, received);
225 require(0, "bulk read readiness / acknowledged bytes missing");
226 }
227 char output[997];
228 size_t wanted = target - received;
229 if (wanted > sizeof(output)) {
230 wanted = sizeof(output);
231 }
232 ssize_t amount = read(reader, output, wanted);
233 if (amount < 0 && (errno == EAGAIN || errno == EINTR)) {
234 continue;
235 }
236 require(amount > 0, "bulk read progress");
237 require(!memcmp(output, payload + received, (size_t)amount), "bulk byte preservation");
238 received += (size_t)amount;
239 }
240 }
241 if (blocking) {
242 int status = 0;
243 require(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0,
244 "bulk child status");
245 } else {
246 close(writer);
247 }
248 close(reader);
249 free(payload);
250}
251
252static void test_fork_openpty_contract(void) {
253 int master = -1;
254 int slave = -1;
255 require(openpty(&master, &slave, NULL, NULL, NULL) == 0, "fork openpty");
256
257 pid_t child = fork();
258 require(child >= 0, "fork openpty fork");
259 if (child == 0) {
260 close(master);
261 const char message[] = "fork-openpty-ok";
262 int result =
263 write(slave, message, sizeof(message) - 1) == (ssize_t)(sizeof(message) - 1) ? 0 : 111;
264 _exit(result);
265 }
266
267 close(slave);
268 char output[sizeof("fork-openpty-ok") - 1];
269 read_exact(master, output, sizeof(output), "fork openpty output");
270 require(!memcmp(output, "fork-openpty-ok", sizeof(output)), "fork openpty output contents");
271
272 int status = 0;
273 require(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0,
274 "fork openpty child status");
275 close(master);
276}
277
278static void test_forkpty_contract(void) {
279 int master = -1;
280 char path[64] = {0};
281 pid_t child = forkpty(&master, path, NULL, NULL);
282 require(child >= 0, "forkpty");
283 if (child == 0) {
284 const char message[] = "forkpty-ok";
285 int result =
286 write(STDOUT_FILENO, message, sizeof(message) - 1) == (ssize_t)(sizeof(message) - 1) ? 0
287 : 111;
288 _exit(result);
289 }
290
291 char output[sizeof("forkpty-ok") - 1];
292 int status = 0;
293 require(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0,
294 "forkpty child status");
295 read_exact(master, output, sizeof(output), "forkpty output");
296 require(!memcmp(output, "forkpty-ok", sizeof(output)), "forkpty output contents");
297 close(master);
298}
299
300void test_pty_contracts(void) {
301 printf("Testing PTY contracts...");
302 fflush(stdout);
303 test_posix_openpt_contract();
304 test_openpty_contract();
305 test_ioctl_contracts();
306 test_bulk_transfer(1, 0, 0);
307 test_bulk_transfer(1, 0, 1);
308 test_bulk_transfer(0, 0, 0);
309 test_bulk_transfer(1, 1, 0);
310 test_bulk_transfer(0, 1, 0);
311 test_fork_openpty_contract();
312 test_forkpty_contract();
313 printf("OK\n");
314}
Definition waits.c:9