The Pedigree Project 0.1
usercopy-contracts.c
1/* Copyright (c) 2026, Pedigree Developers. See LICENSE for licensing details. */
2#define _GNU_SOURCE
3
4#include <errno.h>
5#include <fcntl.h>
6#include <stddef.h>
7#include <stdint.h>
8#include <stdio.h>
9#include <stdlib.h>
10#include <string.h>
11#include <unistd.h>
12#include <utime.h>
13
14#include <netinet/in.h>
15#include <sys/epoll.h>
16#include <sys/ioctl.h>
17#include <sys/mman.h>
18#include <sys/socket.h>
19#include <sys/stat.h>
20#include <sys/statfs.h>
21#include <sys/statvfs.h>
22#include <sys/syscall.h>
23#include <sys/time.h>
24#include <sys/uio.h>
25#include <sys/un.h>
26#include <sys/wait.h>
27
28extern void fail(void) __attribute__((noreturn));
29extern void test_socket_path_contracts(void);
30
31static void require(int condition, const char* operation) {
32 if (!condition) {
33 printf("USERCOPY-CONTRACT: FAIL %s errno=%d\n", operation, errno);
34 fail();
35 }
36}
37
38static void socket_buffers(void* inaccessible, size_t page) {
39 int sockets[2];
40 require(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) == 0, "stream socketpair");
41 int enabled = 1;
42 require(setsockopt(sockets[0], SOL_SOCKET, SO_REUSEADDR, &enabled, sizeof(enabled)) == 0,
43 "snapshot socket option input");
44 errno = 0;
45 require(setsockopt(sockets[0], SOL_SOCKET, SO_REUSEADDR, inaccessible, sizeof(int)) == -1 &&
46 errno == EFAULT,
47 "socket option input protection");
48 unsigned char option[sizeof(int) + 2];
49 memset(option, 0xA7, sizeof(option));
50 socklen_t option_length = 1;
51 require(getsockopt(sockets[0], SOL_SOCKET, SO_TYPE, option + 1, &option_length) == 0 &&
52 option_length == 1 && option[0] == 0xA7 && option[2] == 0xA7 &&
53 option[1] == SOCK_STREAM,
54 "truncated socket option output");
55 option_length = 0;
56 require(
57 getsockopt(sockets[0], SOL_SOCKET, SO_TYPE, NULL, &option_length) == 0 && option_length == 0,
58 "zero capacity socket option");
59 errno = 0;
60 require(
61 getsockopt(sockets[0], SOL_SOCKET, SO_TYPE, option, inaccessible) == -1 && errno == EFAULT,
62 "socket option length protection");
63
64 char first[] = "ab", second[] = "cd";
65 struct iovec send_vectors[] = {{NULL, 0}, {first, 2}, {NULL, 0}, {second, 2}};
66 struct msghdr message = {.msg_iov = send_vectors, .msg_iovlen = 4};
67 require(sendmsg(sockets[0], &message, 0) == 4, "gather stream payload");
68 char* output = mmap(NULL, page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
69 require(output != MAP_FAILED, "demand receive buffer");
70 struct iovec receive_vectors[] = {{output, 1}, {NULL, 0}, {output + 1, 3}};
71 message = (struct msghdr){.msg_iov = receive_vectors, .msg_iovlen = 3};
72 require(recvmsg(sockets[1], &message, 0) == 4 && !memcmp(output, "abcd", 4),
73 "scatter into demand pages");
74 errno = 0;
75 require(send(sockets[0], inaccessible, 4, 0) == -1 && errno == EFAULT, "stream send protection");
76 require(send(sockets[0], "x", 1, 0) == 1, "queue protected receive");
77 errno = 0;
78 require(recv(sockets[1], inaccessible, 1, 0) == -1 && errno == EFAULT,
79 "stream receive protection");
80 require(munmap(output, page) == 0, "release receive buffer");
81 close(sockets[0]);
82 close(sockets[1]);
83
84 struct sockaddr_un destination = {.sun_family = AF_UNIX};
85 snprintf(destination.sun_path, sizeof(destination.sun_path), "/tmp/usercopy-dgram-%ld.sock",
86 (long)getpid());
87 const socklen_t destination_length =
88 offsetof(struct sockaddr_un, sun_path) + strlen(destination.sun_path) + 1;
89 unlink(destination.sun_path);
90 sockets[0] = socket(AF_UNIX, SOCK_DGRAM, 0);
91 require(sockets[0] >= 0, "create datagram sender");
92 sockets[1] = socket(AF_UNIX, SOCK_DGRAM, 0);
93 require(sockets[1] >= 0, "create datagram receiver");
94 require(bind(sockets[1], (struct sockaddr*)&destination, destination_length) == 0,
95 "bind datagram pathname");
96 require(connect(sockets[0], (struct sockaddr*)&destination, destination_length) == 0,
97 "connect datagram pathname");
98 require(send(sockets[0], "packet", 6, 0) == 6, "datagram send");
99 char truncated[4] = {0, 0, 0, 0x6A};
100 require(recv(sockets[1], truncated, 3, MSG_TRUNC) == 6 && !memcmp(truncated, "pac", 3) &&
101 truncated[3] == 0x6A,
102 "datagram truncation copy extent");
103 require(unlink(destination.sun_path) == 0, "remove datagram pathname");
104 close(sockets[0]);
105 close(sockets[1]);
106 puts("USERCOPY-CONTRACT: PASS socket-buffers");
107}
108
109void test_tcp_receive_buffers(void) {
110 // Bound failures where a receive incorrectly waits after making progress.
111 alarm(15);
112 int listener = socket(AF_INET, SOCK_STREAM, 0);
113 require(listener >= 0, "TCP receive listener");
114 struct sockaddr_in address = {.sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK)};
115 require(
116 bind(listener, (struct sockaddr*)&address, sizeof(address)) == 0 && listen(listener, 1) == 0,
117 "TCP receive listen");
118 socklen_t address_length = sizeof(address);
119 require(getsockname(listener, (struct sockaddr*)&address, &address_length) == 0,
120 "TCP receive listener address");
121 int sender = socket(AF_INET, SOCK_STREAM, 0);
122 require(sender >= 0 && connect(sender, (struct sockaddr*)&address, address_length) == 0,
123 "TCP receive connect");
124 int receiver = accept(listener, (struct sockaddr*)&address, &address_length);
125 require(receiver >= 0, "TCP receive accept");
126 int alias = dup(receiver);
127 require(alias >= 0, "TCP receive duplicate");
128
129 int epoll = epoll_create1(0);
130 struct epoll_event interest = {.events = EPOLLIN | EPOLLRDHUP | EPOLLET, .data.fd = receiver};
131 require(epoll >= 0 && epoll_ctl(epoll, EPOLL_CTL_ADD, receiver, &interest) == 0,
132 "TCP receive edge watch");
133 char output[16384];
134 require(recv(receiver, output, 0, 0) == 0, "empty TCP receive capacity");
135 errno = 0;
136 require(recv(receiver, output, sizeof(output), MSG_DONTWAIT) == -1 && errno == EAGAIN &&
137 !(fcntl(receiver, F_GETFL) & O_NONBLOCK),
138 "empty TCP per-call nonblocking receive");
139 require(send(sender, "x", 1, 0) == 1, "TCP short receive payload");
140 struct epoll_event event = {};
141 require(epoll_wait(epoll, &event, 1, 5000) == 1 && (event.events & EPOLLIN),
142 "TCP receive first edge");
143 require(recv(receiver, output, sizeof(output), 0) == 1 && output[0] == 'x',
144 "TCP short receive does not wait for more data");
145 require(epoll_wait(epoll, &event, 1, 0) == 0, "TCP drain clears receive edge");
146
147 char payload[16384];
148 for (size_t i = 0; i < sizeof(payload); ++i) {
149 payload[i] = (char)(i * 37);
150 }
151 size_t sent = 0;
152 while (sent < sizeof(payload)) {
153 ssize_t result = send(sender, payload + sent, sizeof(payload) - sent, 0);
154 require(result > 0, "TCP queue receive payload");
155 sent += (size_t)result;
156 }
157 require(shutdown(sender, SHUT_WR) == 0, "TCP finish receive payload");
158 // FIN follows all payload, so RDHUP confirms that the receive buffers are
159 // queued before checking that a single recvmsg drains across packet edges.
160 int refilled = 0;
161 do {
162 require(epoll_wait(epoll, &event, 1, 5000) == 1, "TCP receive refill edge");
163 refilled |= !!(event.events & EPOLLIN);
164 } while (!(event.events & EPOLLRDHUP));
165 require(refilled, "TCP refill remains readable before EOF");
166
167 require(recv(receiver, output, 257, 0) == 257 && !memcmp(output, payload, 257),
168 "TCP retain partial receive packet");
169 struct iovec vectors[] = {
170 {NULL, 0}, {output + 257, 3}, {NULL, 0}, {output + 260, sizeof(output) - 260 - 31}};
171 struct msghdr message = {.msg_iov = vectors, .msg_iovlen = 4};
172 require(recvmsg(alias, &message, MSG_DONTWAIT) == (ssize_t)(sizeof(output) - 257 - 31) &&
173 !memcmp(output, payload, sizeof(output) - 31),
174 "TCP scatter receive drains queued packets through duplicate");
175 char tail[64];
176 require(recv(receiver, tail, sizeof(tail), 0) == 31 &&
177 !memcmp(tail, payload + sizeof(payload) - 31, 31),
178 "TCP partial progress is returned before EOF");
179 require(recv(alias, output, sizeof(output), 0) == 0 &&
180 recv(receiver, output, sizeof(output), MSG_DONTWAIT) == 0,
181 "TCP EOF follows queued payload");
182 require(close(alias) == 0 && close(receiver) == 0 && close(sender) == 0 && close(listener) == 0 &&
183 close(epoll) == 0,
184 "TCP receive fixture close");
185 alarm(0);
186 puts("USERCOPY-CONTRACT: PASS tcp-receive-buffers");
187}
188
189static void socket_addresses(void* inaccessible) {
190 struct sockaddr_un address = {.sun_family = AF_UNIX};
191 snprintf(address.sun_path, sizeof(address.sun_path), "/tmp/usercopy-%ld.sock", (long)getpid());
192 unlink(address.sun_path);
193 const socklen_t length = offsetof(struct sockaddr_un, sun_path) + strlen(address.sun_path) + 1;
194 int listener = socket(AF_UNIX, SOCK_STREAM, 0);
195 require(listener >= 0, "pathname socket");
196 errno = 0;
197 require(bind(listener, inaccessible, length) == -1 && errno == EFAULT, "bind address protection");
198 require(bind(listener, (struct sockaddr*)&address, length) == 0 && listen(listener, 1) == 0,
199 "bind and listen from address snapshot");
200 int regular = open("/dev/null", O_RDWR);
201 require(regular >= 0, "non-socket descriptor");
202 errno = 0;
203 require(connect(regular, (struct sockaddr*)&address, length) == -1 && errno == ENOTSOCK,
204 "non-socket connect rejected");
205 close(regular);
206
207 pid_t child = fork();
208 require(child >= 0, "accept peer fork");
209 if (!child) {
210 close(listener);
211 int peer = socket(AF_UNIX, SOCK_STREAM, 0);
212 if (peer < 0 || connect(peer, (struct sockaddr*)&address, length) != 0 ||
213 send(peer, "k", 1, 0) != 1)
214 _exit(11);
215 close(peer);
216 _exit(0);
217 }
218 unsigned char peer_address[3] = {0xA5, 0xA5, 0xA5};
219 socklen_t peer_length = 1;
220 int accepted = accept(listener, (struct sockaddr*)(peer_address + 1), &peer_length);
221 require(accepted >= 0 && peer_length >= sizeof(sa_family_t) && peer_address[0] == 0xA5 &&
222 peer_address[2] == 0xA5,
223 "bounded accepted address output");
224 char byte = 0;
225 require(recv(accepted, &byte, 1, 0) == 1 && byte == 'k', "accepted stream usable");
226 close(accepted);
227 require(unlink(address.sun_path) == 0, "remove socket pathname");
228 close(listener);
229 int status = 0;
230 require(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0,
231 "pathname peer completed");
232 puts("USERCOPY-CONTRACT: PASS socket-addresses");
233}
234
235static void console_buffers(void* inaccessible) {
236 int master = posix_openpt(O_RDWR | O_NOCTTY);
237 require(master >= 0, "open console pair");
238 struct winsize requested = {.ws_row = 31, .ws_col = 93}, actual = {0};
239 require(ioctl(master, TIOCSWINSZ, &requested) == 0 && ioctl(master, TIOCGWINSZ, &actual) == 0 &&
240 actual.ws_row == 31 && actual.ws_col == 93,
241 "console window snapshot roundtrip");
242 errno = 0;
243 require(ioctl(master, TIOCSWINSZ, inaccessible) == -1 && errno == EFAULT,
244 "console input protection");
245 errno = 0;
246 require(ioctl(master, TIOCGWINSZ, inaccessible) == -1 && errno == EFAULT,
247 "console output protection");
248 int enabled = 1;
249 require(ioctl(master, FIONBIO, &enabled) == 0 && (fcntl(master, F_GETFL) & O_NONBLOCK),
250 "nonblocking input snapshot");
251 errno = 0;
252 require(ioctl(master, FIONBIO, NULL) == -1 && errno == EFAULT &&
253 (fcntl(master, F_GETFL) & O_NONBLOCK),
254 "invalid nonblocking input preserves flags");
255 close(master);
256 puts("USERCOPY-CONTRACT: PASS console-buffers");
257}
258
260 uint64_t inode;
261 int64_t offset;
262 unsigned short length;
263 unsigned char type;
264 char name[];
265};
266
267static void directory_buffers(void* inaccessible) {
268 int directory = open("/tmp", O_RDONLY | O_DIRECTORY);
269 require(directory >= 0, "open directory");
270 errno = 0;
271 require(syscall(SYS_getdents64, directory, inaccessible, 256) == -1 && errno == EFAULT,
272 "directory output protection");
273 unsigned char records[512];
274 ssize_t count = syscall(SYS_getdents64, directory, records, sizeof(records));
275 require(count > 0, "directory output snapshot");
276 for (size_t offset = 0; offset < (size_t)count;) {
277 struct directory_record* record = (struct directory_record*)(records + offset);
278 require(record->length >= offsetof(struct directory_record, name) + 1 &&
279 record->length % 8 == 0 && record->length <= count - offset &&
280 memchr(record->name, 0, record->length - offsetof(struct directory_record, name)),
281 "aligned bounded directory record");
282 offset += record->length;
283 }
284 close(directory);
285 puts("USERCOPY-CONTRACT: PASS directory-buffers");
286}
287
288static void mapped_write_buffers(size_t page) {
289 char path[80];
290 snprintf(path, sizeof(path), "/tmp/usercopy-mapped-%ld", (long)getpid());
291 int file = open(path, O_CREAT | O_TRUNC | O_RDWR, 0600);
292 require(file >= 0 && ftruncate(file, page) == 0 && pwrite(file, "mapped", 6, 0) == 6,
293 "prepare file-backed write source");
294 for (int operation = 0; operation < 4; ++operation) {
295 char* source = mmap(NULL, page, PROT_READ, MAP_PRIVATE, file, 0);
296 require(source != MAP_FAILED && lseek(file, 0, SEEK_SET) == 0,
297 "cold mapping for same-file write");
298 struct iovec vectors[] = {{source, 2}, {source + 2, 4}};
299 ssize_t written;
300 switch (operation) {
301 case 0:
302 written = write(file, source, 6);
303 break;
304 case 1:
305 written = pwrite(file, source, 6, 0);
306 break;
307 case 2:
308 written = writev(file, vectors, 2);
309 break;
310 default:
311 written = pwritev(file, vectors, 2, 0);
312 break;
313 }
314 require(written == 6, "write materializes source outside backing mutation lock");
315 require(munmap(source, page) == 0, "release cold write source");
316 }
317 char actual[6];
318 require(pread(file, actual, sizeof(actual), 0) == sizeof(actual) &&
319 !memcmp(actual, "mapped", sizeof(actual)),
320 "mapped source contents preserved");
321 close(file);
322 require(unlink(path) == 0, "remove mapped write fixture");
323 puts("USERCOPY-CONTRACT: PASS mapped-write-buffers");
324}
325
326static void metadata_buffers(void* inaccessible) {
327 char path[80], link[88];
328 // The root Ext2 backend supplies the existing symlink capability.
329 snprintf(path, sizeof(path), "/usercopy-metadata-%ld", (long)getpid());
330 snprintf(link, sizeof(link), "%s.link", path);
331 int file = open(path, O_CREAT | O_EXCL | O_RDWR, 0600);
332 require(file >= 0 && write(file, "metadata", 8) == 8, "prepare metadata file");
333 struct stat by_path, by_descriptor;
334 require(stat(path, &by_path) == 0 && fstat(file, &by_descriptor) == 0 &&
335 S_ISREG(by_path.st_mode) && by_path.st_size == 8 &&
336 by_path.st_ino == by_descriptor.st_ino && by_descriptor.st_size == 8,
337 "public stat snapshots agree");
338 struct statfs filesystem;
339 struct statvfs capacity;
340 require(statfs(path, &filesystem) == 0 && filesystem.f_bsize > 0 &&
341 fstatfs(file, &filesystem) == 0 && filesystem.f_bsize > 0 &&
342 statvfs(path, &capacity) == 0 && capacity.f_bsize > 0 &&
343 fstatvfs(file, &capacity) == 0 && capacity.f_bsize > 0,
344 "public filesystem metadata snapshots");
345
346 // musl transforms or clears metadata buffers itself, so test the kernel
347 // copy boundary through the same syscall entries those wrappers use.
348 errno = 0;
349 require(syscall(SYS_stat, path, inaccessible) == -1 && errno == EFAULT, "stat output protection");
350 errno = 0;
351 require(syscall(SYS_fstat, file, inaccessible) == -1 && errno == EFAULT,
352 "fstat output protection");
353 errno = 0;
354 require(syscall(SYS_newfstatat, AT_FDCWD, path, inaccessible, 0) == -1 && errno == EFAULT,
355 "fstatat output protection");
356 errno = 0;
357 require(syscall(SYS_statfs, path, inaccessible) == -1 && errno == EFAULT,
358 "statfs output protection");
359 errno = 0;
360 require(syscall(SYS_fstatfs, file, inaccessible) == -1 && errno == EFAULT,
361 "fstatfs output protection");
362
363 require(symlink(path, link) == 0, "prepare readlink target");
364 unsigned char target[96];
365 memset(target, 0xA7, sizeof(target));
366 require(readlink(link, (char*)target + 1, 3) == 3 && !memcmp(target + 1, path, 3) &&
367 target[0] == 0xA7 && target[4] == 0xA7,
368 "readlink truncates without a terminator");
369 memset(target, 0xA7, sizeof(target));
370 size_t target_length = strlen(path);
371 require(readlinkat(AT_FDCWD, link, (char*)target + 1, target_length) == (ssize_t)target_length &&
372 !memcmp(target + 1, path, target_length) && target[0] == 0xA7 &&
373 target[target_length + 1] == 0xA7,
374 "readlinkat exact target extent");
375 errno = 0;
376 require(readlink(link, inaccessible, 3) == -1 && errno == EFAULT, "readlink output protection");
377
378 // musl now routes timestamp wrappers through utimensat. These legacy
379 // entries remain supported independently and need their own copy checks.
380 struct utimbuf seconds = {.actime = 11, .modtime = 22};
381 struct timeval fractions[2] = {{33, 100}, {44, 200}};
382 require(syscall(SYS_utime, path, &seconds) == 0 && syscall(SYS_utimes, path, fractions) == 0 &&
383 syscall(SYS_futimesat, AT_FDCWD, path, fractions) == 0 &&
384 fstat(file, &by_descriptor) == 0,
385 "legacy timestamp inputs snapshotted");
386 errno = 0;
387 require(syscall(SYS_utime, path, inaccessible) == -1 && errno == EFAULT,
388 "legacy utime input protection");
389 errno = 0;
390 require(syscall(SYS_utimes, path, inaccessible) == -1 && errno == EFAULT,
391 "legacy utimes input protection");
392 errno = 0;
393 require(syscall(SYS_futimesat, AT_FDCWD, path, inaccessible) == -1 && errno == EFAULT,
394 "legacy futimesat input protection");
395 require(fstat(file, &by_path) == 0 && by_path.st_atim.tv_sec == by_descriptor.st_atim.tv_sec &&
396 by_path.st_atim.tv_nsec == by_descriptor.st_atim.tv_nsec &&
397 by_path.st_mtim.tv_sec == by_descriptor.st_mtim.tv_sec &&
398 by_path.st_mtim.tv_nsec == by_descriptor.st_mtim.tv_nsec,
399 "rejected timestamp inputs preserve metadata");
400 close(file);
401 require(unlink(link) == 0 && unlink(path) == 0, "remove metadata fixtures");
402 puts("USERCOPY-CONTRACT: PASS metadata-buffers");
403}
404
405static void working_directory_buffer(void* inaccessible) {
406 char path[80];
407 snprintf(path, sizeof(path), "/tmp/usercopy-cwd-%ld", (long)getpid());
408 int original = open(".", O_RDONLY | O_DIRECTORY);
409 require(original >= 0 && mkdir(path, 0700) == 0 && chdir(path) == 0, "prepare working directory");
410 char expected[256];
411 require(getcwd(expected, sizeof(expected)) == expected, "working directory baseline");
412 const size_t length = strlen(expected);
413 unsigned char output[sizeof(expected) + 2];
414 memset(output, 0xA7, sizeof(output));
415 require(getcwd((char*)output + 1, length + 1) == (char*)output + 1 &&
416 !memcmp(output + 1, expected, length + 1) && output[0] == 0xA7 &&
417 output[length + 2] == 0xA7,
418 "getcwd exact capacity includes terminator");
419 require(syscall(SYS_getcwd, output + 1, length + 1) == (long)length + 1 &&
420 output[length + 1] == 0 && output[length + 2] == 0xA7,
421 "getcwd syscall reports terminated extent");
422 memset(output, 0xA7, sizeof(output));
423 errno = 0;
424 require(getcwd((char*)output + 1, length) == NULL && errno == ERANGE && output[1] == 0xA7,
425 "getcwd insufficient capacity preserves output");
426 errno = 0;
427 require(getcwd(inaccessible, length + 1) == NULL && errno == EFAULT, "getcwd output protection");
428 require(fchdir(original) == 0, "restore working directory");
429 close(original);
430 require(rmdir(path) == 0, "remove working directory fixture");
431 puts("USERCOPY-CONTRACT: PASS working-directory-buffer");
432}
433
434void test_regular_read_contracts(const char* base) {
435 const size_t page = (size_t)sysconf(_SC_PAGESIZE);
436 const size_t capacity = 64 * 1024;
437 const size_t legacy_capacity = 4097;
438 const size_t file_size = 2 * capacity + 257;
439 const size_t read_lengths[] = {16384, capacity + 37};
440 unsigned char* source = malloc(file_size);
441 unsigned char* output = malloc(capacity + 39);
442 require(page && source && output, "allocate regular read buffers");
443 for (size_t i = 0; i < file_size; ++i)
444 source[i] = (unsigned char)(i * 37 + i / 251);
445
446 char path[512];
447 const int path_length = snprintf(path, sizeof(path), "%s/regular-read-%ld", base, (long)getpid());
448 require(path_length > 0 && (size_t)path_length < sizeof(path), "regular read fixture path");
449 int file = open(path, O_CREAT | O_EXCL | O_RDWR, 0600);
450 require(file >= 0 && write(file, source, file_size) == (ssize_t)file_size,
451 "prepare regular read fixture");
452 int alias = dup(file);
453 require(alias >= 0, "duplicate regular read descriptor");
454
455 for (size_t i = 0; i < sizeof(read_lengths) / sizeof(read_lengths[0]); ++i) {
456 const size_t length = read_lengths[i];
457 memset(output, 0xA7, capacity + 39);
458 require(lseek(file, 13, SEEK_SET) == 13 && read(alias, output + 1, length) == (ssize_t)length &&
459 !memcmp(output + 1, source + 13, length) && output[0] == 0xA7 &&
460 output[length + 1] == 0xA7 && lseek(file, 0, SEEK_CUR) == (off_t)(13 + length),
461 "unaligned chunked read advances shared offset");
462 memset(output, 0xA7, capacity + 39);
463 require(pread(file, output + 1, length, 29) == (ssize_t)length &&
464 !memcmp(output + 1, source + 29, length) && output[0] == 0xA7 &&
465 output[length + 1] == 0xA7 && lseek(alias, 0, SEEK_CUR) == (off_t)(13 + length),
466 "unaligned chunked pread preserves shared offset");
467 }
468
469 memset(output, 0xA7, capacity + 39);
470 require(lseek(file, (off_t)file_size - 31, SEEK_SET) == (off_t)file_size - 31 &&
471 read(alias, output + 1, capacity + 37) == 31 &&
472 !memcmp(output + 1, source + file_size - 31, 31) && output[0] == 0xA7 &&
473 output[32] == 0xA7 && lseek(file, 0, SEEK_CUR) == (off_t)file_size,
474 "short read stops at EOF without overwriting suffix");
475 memset(output, 0xA7, capacity + 39);
476 require(pread(file, output + 1, capacity + 37, (off_t)file_size - 31) == 31 &&
477 !memcmp(output + 1, source + file_size - 31, 31) && output[0] == 0xA7 &&
478 output[32] == 0xA7 && pread(file, output + 1, capacity, file_size) == 0 &&
479 lseek(alias, 0, SEEK_CUR) == (off_t)file_size,
480 "short pread preserves suffix and shared offset");
481
482 const size_t valid_prefix = 2 * (legacy_capacity - 1);
483 const size_t writable_length = (valid_prefix + page - 1) / page * page;
484 unsigned char* guarded =
485 mmap(NULL, writable_length + page, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
486 require(guarded != MAP_FAILED && lseek(file, 23, SEEK_SET) == 23,
487 "prepare protected read destination");
488 errno = 0;
489 require(read(alias, guarded, 1) == -1 && errno == EFAULT && lseek(file, 0, SEEK_CUR) == 23,
490 "first read fault preserves shared offset");
491 errno = 0;
492 require(pread(file, guarded, 1, 47) == -1 && errno == EFAULT && lseek(alias, 0, SEEK_CUR) == 23,
493 "first pread fault preserves shared offset");
494 require(mprotect(guarded, writable_length, PROT_READ | PROT_WRITE) == 0,
495 "allow prefix of read destination");
496 unsigned char* destination = guarded + writable_length - valid_prefix;
497 memset(destination, 0xA7, valid_prefix);
498 require(read(alias, destination, valid_prefix + 1) == (ssize_t)legacy_capacity &&
499 !memcmp(destination, source + 23, legacy_capacity) &&
500 lseek(file, 0, SEEK_CUR) == (off_t)(23 + legacy_capacity),
501 "later read fault returns valid prefix and advances only copied bytes");
502 for (size_t i = legacy_capacity; i < valid_prefix; ++i)
503 require(destination[i] == 0xA7, "read fault preserves uncopied suffix");
504 memset(destination, 0xA7, valid_prefix);
505 require(pread(file, destination, valid_prefix + 1, 47) == (ssize_t)legacy_capacity &&
506 !memcmp(destination, source + 47, legacy_capacity) &&
507 lseek(alias, 0, SEEK_CUR) == (off_t)(23 + legacy_capacity),
508 "later pread fault returns valid prefix without changing shared offset");
509 for (size_t i = legacy_capacity; i < valid_prefix; ++i)
510 require(destination[i] == 0xA7, "pread fault preserves uncopied suffix");
511 require(munmap(guarded, writable_length + page) == 0, "release protected read destination");
512
513 const size_t mapped_length = (file_size + page - 1) / page * page;
514 for (int positional = 0; positional < 2; ++positional) {
515 unsigned char* mapped = mmap(NULL, mapped_length, PROT_READ | PROT_WRITE, MAP_PRIVATE, file, 0);
516 require(mapped != MAP_FAILED && lseek(file, 13, SEEK_SET) == 13,
517 "map cold file-backed read destination");
518 const ssize_t received = positional ? pread(file, mapped + 1, capacity + 37, 29)
519 : read(alias, mapped + 1, capacity + 37);
520 const size_t source_offset = positional ? 29 : 13;
521 const off_t expected_offset = positional ? 13 : (off_t)(13 + capacity + 37);
522 require(received == (ssize_t)(capacity + 37) &&
523 !memcmp(mapped + 1, source + source_offset, capacity + 37) &&
524 mapped[0] == source[0] && lseek(alias, 0, SEEK_CUR) == expected_offset,
525 "read materializes same-file destination after releasing the backing lock");
526 require(munmap(mapped, mapped_length) == 0, "release file-backed read destination");
527 }
528 require(
529 pread(file, output, capacity, 0) == (ssize_t)capacity && !memcmp(output, source, capacity),
530 "private destination leaves source file unchanged");
531
532 require(close(alias) == 0 && close(file) == 0 && unlink(path) == 0,
533 "remove regular read fixture");
534 free(output);
535 free(source);
536 puts("USERCOPY-CONTRACT: PASS regular-read-buffers");
537}
538
539void test_usercopy_contracts(void) {
540 const size_t page = (size_t)sysconf(_SC_PAGESIZE);
541 void* inaccessible = mmap(NULL, page, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
542 require(inaccessible != MAP_FAILED, "protected user range");
543 socket_buffers(inaccessible, page);
544 test_tcp_receive_buffers();
545 socket_addresses(inaccessible);
546 test_socket_path_contracts();
547 console_buffers(inaccessible);
548 directory_buffers(inaccessible);
549 mapped_write_buffers(page);
550 metadata_buffers(inaccessible);
551 working_directory_buffer(inaccessible);
552 require(munmap(inaccessible, page) == 0, "release protected user range");
553 puts("USERCOPY-CONTRACT: PASS all");
554}
#define INADDR_LOOPBACK
Definition inet.h:92