The Pedigree Project 0.1
syscall-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 <signal.h>
13#include <stdio.h>
14#include <stdlib.h>
15#include <string.h>
16#include <termios.h>
17#include <unistd.h>
18
19#include <sys/ioctl.h>
20#include <sys/resource.h>
21#include <sys/socket.h>
22#include <sys/stat.h>
23#include <sys/syscall.h>
24#include <sys/uio.h>
25#include <sys/wait.h>
26
27extern void fail(void) __attribute__((noreturn));
28
29static void require(int condition, const char* operation) {
30 if (!condition) {
31 printf("SYSCALL-CONTRACT: FAIL %s errno=%d\n", operation, errno);
32 fail();
33 }
34}
35
36static void file_contracts(void) {
37 char path[80];
38 snprintf(path, sizeof(path), "/tmp/syscall-contract-%d", getpid());
39 int file = open(path, O_CREAT | O_TRUNC | O_RDWR, 0600);
40 require(file >= 0, "create");
41 require(write(file, "data", 4) == 4, "write");
42 struct stat status;
43 require(fstat(file, &status) == 0 && status.st_size == 4 && S_ISREG(status.st_mode) &&
44 status.st_blocks == sysconf(_SC_PAGESIZE) / 512,
45 "file metadata");
46 close(file);
47
48 char data[4] = {0};
49 struct iovec vector = {.iov_base = data, .iov_len = sizeof(data)};
50 file = open(path, O_RDONLY | O_CLOEXEC);
51 require(file >= 0, "read-only open");
52 require(read(file, data, sizeof(data)) == 4 && !memcmp(data, "data", 4), "read");
53 errno = 0;
54 require(write(file, data, 1) == -1 && errno == EBADF, "read-only write");
55 errno = 0;
56 require(writev(file, &vector, 1) == -1 && errno == EBADF, "read-only writev");
57 errno = 0;
58 require(write(file, data, 0) == -1 && errno == EBADF, "read-only empty write");
59 require(lseek(file, 0, SEEK_SET) == 0 && readv(file, &vector, 1) == 4, "readv");
60 close(file);
61
62 file = open(path, O_WRONLY | O_CLOEXEC);
63 require(file >= 0, "write-only open");
64 errno = 0;
65 require(read(file, data, 1) == -1 && errno == EBADF, "write-only read");
66 errno = 0;
67 require(readv(file, &vector, 1) == -1 && errno == EBADF, "write-only readv");
68 errno = 0;
69 require(read(file, data, 0) == -1 && errno == EBADF, "write-only empty read");
70 require(writev(file, &vector, 1) == 4, "writev");
71 close(file);
72 require(unlink(path) == 0, "unlink");
73
74 int descriptors[2];
75 require(pipe(descriptors) == 0, "pipe");
76 require(fstat(descriptors[0], &status) == 0 && S_ISFIFO(status.st_mode), "pipe metadata");
77 close(descriptors[0]);
78 close(descriptors[1]);
79 puts("SYSCALL-CONTRACT: PASS file-modes-and-stat");
80}
81
82static void pipe_ioctl_contracts(void) {
83 int descriptors[2];
84 int available = -1;
85 require(pipe(descriptors) == 0, "FIONREAD pipe");
86 require(ioctl(descriptors[0], FIONREAD, &available) == 0 && available == 0,
87 "empty pipe FIONREAD");
88 require(write(descriptors[1], "abcde", 5) == 5, "FIONREAD pipe write");
89 for (int endpoint = 0; endpoint < 2; ++endpoint) {
90 require(ioctl(descriptors[endpoint], FIONREAD, &available) == 0 && available == 5,
91 "pipe endpoints FIONREAD");
92 errno = 0;
93 require(ioctl(descriptors[endpoint], FIONREAD, (void*)1) == -1 && errno == EFAULT,
94 "pipe FIONREAD bad address");
95 }
96 char data[5];
97 require(read(descriptors[0], data, 2) == 2 && !memcmp(data, "ab", 2), "FIONREAD partial read");
98 require(ioctl(descriptors[0], FIONREAD, &available) == 0 && available == 3,
99 "FIONREAD remaining bytes");
100 close(descriptors[1]);
101 require(ioctl(descriptors[0], FIONREAD, &available) == 0 && available == 3,
102 "FIONREAD bytes after writer close");
103 require(read(descriptors[0], data, sizeof(data)) == 3 && !memcmp(data, "cde", 3),
104 "FIONREAD does not consume bytes");
105 require(ioctl(descriptors[0], FIONREAD, &available) == 0 && available == 0 &&
106 read(descriptors[0], data, 1) == 0,
107 "drained pipe FIONREAD and EOF");
108 close(descriptors[0]);
109
110 char path[80];
111 snprintf(path, sizeof(path), "/tmp/fionread-contract-%d", getpid());
112 require(mkfifo(path, 0600) == 0, "FIONREAD FIFO create");
113 int fifo = open(path, O_RDWR | O_NONBLOCK);
114 require(fifo >= 0, "FIONREAD FIFO open");
115 require(write(fifo, "fifo", 4) == 4 && ioctl(fifo, FIONREAD, &available) == 0 && available == 4,
116 "FIFO FIONREAD");
117 require(read(fifo, data, sizeof(data)) == 4 && !memcmp(data, "fifo", 4),
118 "FIFO FIONREAD preserves bytes");
119 close(fifo);
120 require(unlink(path) == 0, "FIONREAD FIFO unlink");
121 puts("SYSCALL-CONTRACT: PASS pipe-and-fifo-FIONREAD");
122}
123
124static void socket_name_contracts(void) {
125 int sockets[2];
126 require(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) == 0, "socketpair");
127 for (int peer = 0; peer < 2; ++peer) {
128 struct sockaddr_storage complete;
129 memset(&complete, 0, sizeof(complete));
130 socklen_t full_length = sizeof(complete);
131 int result = peer ? getpeername(sockets[0], (struct sockaddr*)&complete, &full_length)
132 : getsockname(sockets[0], (struct sockaddr*)&complete, &full_length);
133 require(result == 0 && full_length == sizeof(sa_family_t) && complete.ss_family == AF_UNIX,
134 "complete socket name");
135
136 struct sockaddr_storage short_name;
137 memset(&short_name, 0xA5, sizeof(short_name));
138 socklen_t capacity = 1;
139 result = peer ? getpeername(sockets[0], (struct sockaddr*)&short_name, &capacity)
140 : getsockname(sockets[0], (struct sockaddr*)&short_name, &capacity);
141 require(result == 0 && capacity == full_length &&
142 ((unsigned char*)&short_name)[0] == ((unsigned char*)&complete)[0],
143 "short socket name");
144 for (size_t i = 1; i < sizeof(short_name); ++i)
145 require(((unsigned char*)&short_name)[i] == 0xA5, "socket name capacity");
146
147 capacity = 0;
148 result =
149 peer ? getpeername(sockets[0], NULL, &capacity) : getsockname(sockets[0], NULL, &capacity);
150 require(result == 0 && capacity == full_length, "zero-capacity socket name");
151 }
152 close(sockets[0]);
153 close(sockets[1]);
154 puts("SYSCALL-CONTRACT: PASS socket-name-capacity");
155}
156
157static void termios_contracts(void) {
158 int master = posix_openpt(O_RDWR | O_NOCTTY | O_NONBLOCK);
159 require(master >= 0, "open PTY");
160 unsigned int terminal = 0;
161 unsigned int raw_terminal = 0;
162 require(ioctl(master, TIOCGPTN, &terminal) == 0, "ioctl signed request encoding");
163 require(syscall(SYS_ioctl, master, (unsigned long)TIOCGPTN, &raw_terminal) == 0 &&
164 terminal == raw_terminal,
165 "ioctl raw request encoding");
166 char path[80];
167 require(ptsname_r(master, path, sizeof(path)) == 0, "PTY name");
168 int slave = open(path, O_RDWR | O_NOCTTY | O_NONBLOCK);
169 require(slave >= 0, "open PTY slave");
170
171 struct termios attributes;
172 memset(&attributes, 0xA5, sizeof(attributes));
173 require(tcgetattr(slave, &attributes) == 0, "tcgetattr");
174 require(attributes.c_cc[VINTR] == 3 && attributes.c_cc[VERASE] == 8 &&
175 attributes.c_cc[VEOF] == 4 && attributes.c_cc[VSUSP] == 26,
176 "termios default character indices");
177 attributes.c_lflag |= ICANON;
178 attributes.c_lflag &= ~(ECHO | ISIG);
179 attributes.c_cc[VEOF] = '~';
180 require(tcsetattr(slave, TCSANOW, &attributes) == 0, "tcsetattr");
181 require(write(master, "~", 1) == 1, "PTY EOF input");
182 char character;
183 require(read(slave, &character, 1) == 0, "termios EOF character behavior");
184 close(slave);
185 close(master);
186 puts("SYSCALL-CONTRACT: PASS termios-musl-layout");
187}
188
189static volatile sig_atomic_t caught_signal;
190
191static void caught(int signal) {
192 (void)signal;
193 caught_signal = 1;
194}
195
196static void process_contracts(void) {
197 errno = 0;
198 require(getpriority(PRIO_PROCESS, 0) == 0 && errno == 0, "getpriority libc encoding");
199 require(getpriority(PRIO_PROCESS, getpid()) == 0, "getpriority process target");
200 require(syscall(SYS_getpriority, PRIO_PROCESS, 0) == 20, "getpriority raw encoding");
201 errno = 0;
202 require(getpriority(-1, 0) == -1 && errno == EINVAL, "getpriority selector");
203
204 struct sigaction action = {0};
205 struct sigaction previous;
206 action.sa_handler = caught;
207 sigemptyset(&action.sa_mask);
208 require(sigaction(SIGUSR1, &action, &previous) == 0, "wait signal disposition");
209 caught_signal = 0;
210 pid_t child = fork();
211 require(child >= 0, "wait child fork");
212 if (!child) {
213 usleep(250000);
214 kill(getppid(), SIGUSR1);
215 usleep(250000);
216 _exit(0);
217 }
218
219 int status = 0;
220 errno = 0;
221 pid_t result = waitpid(child, &status, 0);
222 int interrupted = result == -1 && errno == EINTR && caught_signal;
223 if (result != child) {
224 do {
225 result = waitpid(child, &status, 0);
226 } while (result == -1 && errno == EINTR);
227 }
228 require(result == child && WIFEXITED(status) && WEXITSTATUS(status) == 0, "reap wait child");
229 require(sigaction(SIGUSR1, &previous, NULL) == 0, "restore signal disposition");
230 require(interrupted, "caught signal interrupts waitpid");
231 puts("SYSCALL-CONTRACT: PASS priority-and-wait-interruption");
232}
233
234void test_syscall_contracts(void) {
235 file_contracts();
236 pipe_ioctl_contracts();
237 socket_name_contracts();
238 termios_contracts();
239 process_contracts();
240 puts("SYSCALL-CONTRACT: PASS all");
241}