The Pedigree Project 0.1
socket-path-contracts.c
1/* Copyright (c) 2026, Pedigree Developers. See LICENSE for licensing details. */
2#define _GNU_SOURCE
3#include <errno.h>
4#include <fcntl.h>
5#include <stddef.h>
6#include <stdio.h>
7#include <string.h>
8#include <unistd.h>
9
10#include <sys/socket.h>
11#include <sys/stat.h>
12#include <sys/un.h>
13
14extern void fail(void) __attribute__((noreturn));
15
16static void require(int condition, const char* operation) {
17 if (!condition) {
18 printf("SOCKET-PATH-CONTRACT: FAIL %s errno=%d\n", operation, errno);
19 fail();
20 }
21}
22
23static socklen_t abstract_socket_address(struct sockaddr_un* address, char discriminator) {
24 memset(address, 0, sizeof(*address));
25 address->sun_family = AF_UNIX;
26
27 const int prefix_length =
28 snprintf(address->sun_path + 1, sizeof(address->sun_path) - 1, "pedigree-%c", discriminator);
29 require(prefix_length > 0, "format abstract socket prefix");
30 const size_t suffix_offset = 1 + (size_t)prefix_length + 1;
31 const int suffix_length =
32 snprintf(address->sun_path + suffix_offset, sizeof(address->sun_path) - suffix_offset, "%ld",
33 (long)getpid());
34 require(suffix_length > 0, "format abstract socket suffix");
35
36 return offsetof(struct sockaddr_un, sun_path) + suffix_offset + (size_t)suffix_length;
37}
38
39static void abstract_socket_contracts(void) {
40 struct sockaddr_un stream_address;
41 const socklen_t stream_length = abstract_socket_address(&stream_address, 's');
42 int listener = socket(AF_UNIX, SOCK_STREAM, 0);
43 int replacement = socket(AF_UNIX, SOCK_STREAM, 0);
44 int client = socket(AF_UNIX, SOCK_STREAM, 0);
45 require(listener >= 0 && replacement >= 0 && client >= 0 &&
46 bind(listener, (struct sockaddr*)&stream_address, stream_length) == 0,
47 "bind abstract stream socket");
48
49 struct sockaddr_un reported = {};
50 socklen_t reported_length = sizeof(reported);
51 require(getsockname(listener, (struct sockaddr*)&reported, &reported_length) == 0 &&
52 reported_length == stream_length &&
53 memcmp(&reported, &stream_address, stream_length) == 0,
54 "report length-sensitive abstract address");
55 errno = 0;
56 require(bind(replacement, (struct sockaddr*)&stream_address, stream_length) == -1 &&
57 errno == EADDRINUSE,
58 "reject duplicate abstract stream bind");
59 require(listen(listener, 1) == 0 &&
60 connect(client, (struct sockaddr*)&stream_address, stream_length) == 0,
61 "connect abstract stream socket");
62 int accepted = accept(listener, NULL, NULL);
63 require(accepted >= 0, "accept abstract stream socket");
64
65 int duplicate = dup(listener);
66 require(duplicate >= 0 && close(listener) == 0, "duplicate abstract listener");
67 errno = 0;
68 require(bind(replacement, (struct sockaddr*)&stream_address, stream_length) == -1 &&
69 errno == EADDRINUSE,
70 "duplicate retains abstract address");
71 require(close(duplicate) == 0 &&
72 bind(replacement, (struct sockaddr*)&stream_address, stream_length) == 0,
73 "final listener close releases abstract address");
74
75 char byte = 0;
76 require(send(client, "c", 1, 0) == 1 && recv(accepted, &byte, 1, 0) == 1 && byte == 'c' &&
77 send(accepted, "a", 1, 0) == 1 && recv(client, &byte, 1, 0) == 1 && byte == 'a',
78 "accepted connection survives abstract rebind");
79 require(close(client) == 0 && close(accepted) == 0 && close(replacement) == 0,
80 "close abstract stream sockets");
81
82 struct sockaddr_un receiver_address;
83 struct sockaddr_un sender_address;
84 const socklen_t receiver_length = abstract_socket_address(&receiver_address, 'r');
85 const socklen_t sender_length = abstract_socket_address(&sender_address, 'd');
86 int receiver = socket(AF_UNIX, SOCK_DGRAM, 0);
87 int sender = socket(AF_UNIX, SOCK_DGRAM, 0);
88 require(receiver >= 0 && sender >= 0 &&
89 bind(receiver, (struct sockaddr*)&receiver_address, receiver_length) == 0 &&
90 bind(sender, (struct sockaddr*)&sender_address, sender_length) == 0 &&
91 sendto(sender, "d", 1, 0, (struct sockaddr*)&receiver_address, receiver_length) == 1,
92 "send abstract datagram");
93
94 memset(&reported, 0, sizeof(reported));
95 reported_length = sizeof(reported);
96 require(recvfrom(receiver, &byte, 1, 0, (struct sockaddr*)&reported, &reported_length) == 1 &&
97 byte == 'd' && reported_length == sender_length &&
98 memcmp(&reported, &sender_address, sender_length) == 0,
99 "report abstract datagram sender");
100 require(close(receiver) == 0 && close(sender) == 0, "close abstract datagram sockets");
101}
102
103static void rename_socket_paths(const char* directory) {
104 struct sockaddr_un source_path = {.sun_family = AF_UNIX};
105 struct sockaddr_un destination = {.sun_family = AF_UNIX};
106 char regular_path[108];
107 snprintf(source_path.sun_path, sizeof(source_path.sun_path), "%s/socket-source-%ld", directory,
108 (long)getpid());
109 snprintf(destination.sun_path, sizeof(destination.sun_path), "%s/socket-destination-%ld",
110 directory, (long)getpid());
111 snprintf(regular_path, sizeof(regular_path), "%s/socket-regular-%ld", directory, (long)getpid());
112 const socklen_t source_length =
113 offsetof(struct sockaddr_un, sun_path) + strlen(source_path.sun_path) + 1;
114 const socklen_t destination_length =
115 offsetof(struct sockaddr_un, sun_path) + strlen(destination.sun_path) + 1;
116 int source = socket(AF_UNIX, SOCK_DGRAM, 0);
117 int victim = socket(AF_UNIX, SOCK_DGRAM, 0);
118 int source_peer = socket(AF_UNIX, SOCK_DGRAM, 0);
119 int victim_peer = socket(AF_UNIX, SOCK_DGRAM, 0);
120 require(source >= 0 && victim >= 0 && source_peer >= 0 && victim_peer >= 0 &&
121 bind(source, (struct sockaddr*)&source_path, source_length) == 0 &&
122 bind(victim, (struct sockaddr*)&destination, destination_length) == 0 &&
123 connect(source_peer, (struct sockaddr*)&source_path, source_length) == 0 &&
124 connect(victim_peer, (struct sockaddr*)&destination, destination_length) == 0,
125 "prepare socket rename endpoints");
126 require(rename(source_path.sun_path, destination.sun_path) == 0,
127 "rename replaces an open socket pathname");
128 struct stat metadata;
129 errno = 0;
130 require(lstat(source_path.sun_path, &metadata) == -1 && errno == ENOENT,
131 "rename removes old socket name");
132 char byte = 0;
133 require(send(source_peer, "s", 1, 0) == 1 && recv(source, &byte, 1, 0) == 1 && byte == 's' &&
134 send(victim_peer, "v", 1, 0) == 1 && recv(victim, &byte, 1, 0) == 1 && byte == 'v',
135 "rename preserves both existing endpoint references");
136 require(connect(victim_peer, (struct sockaddr*)&destination, destination_length) == 0 &&
137 send(victim_peer, "n", 1, 0) == 1 && recv(source, &byte, 1, 0) == 1 && byte == 'n' &&
138 close(victim) == 0,
139 "new connection follows renamed endpoint");
140
141 int regular = open(regular_path, O_CREAT | O_EXCL | O_RDWR, 0600);
142 require(regular >= 0 && write(regular, "r", 1) == 1 &&
143 rename(destination.sun_path, regular_path) == 0 && pread(regular, &byte, 1, 0) == 1 &&
144 byte == 'r' && lstat(regular_path, &metadata) == 0 && S_ISSOCK(metadata.st_mode),
145 "socket replaces backing file while old descriptor survives");
146 int backing = open(source_path.sun_path, O_CREAT | O_EXCL | O_RDWR, 0600);
147 require(backing >= 0 && write(backing, "f", 1) == 1 &&
148 rename(source_path.sun_path, regular_path) == 0 &&
149 stat(regular_path, &metadata) == 0 && S_ISREG(metadata.st_mode) &&
150 send(source_peer, "u", 1, 0) == 1 && recv(source, &byte, 1, 0) == 1 && byte == 'u',
151 "backing file replaces pathname without closing socket endpoint");
152 require(close(source) == 0 && stat(regular_path, &metadata) == 0 && S_ISREG(metadata.st_mode) &&
153 close(source_peer) == 0 && close(victim_peer) == 0 && close(regular) == 0 &&
154 close(backing) == 0 && unlink(regular_path) == 0,
155 "close preserves the renamed backing replacement");
156}
157
158void test_socket_path_contracts(void) {
159 const int types[] = {SOCK_STREAM, SOCK_DGRAM};
160 for (size_t i = 0; i < sizeof(types) / sizeof(types[0]); ++i) {
161 struct sockaddr_un address = {.sun_family = AF_UNIX};
162 snprintf(address.sun_path, sizeof(address.sun_path), "/tmp/socket-life-%ld-%d", (long)getpid(),
163 types[i]);
164 const socklen_t length = offsetof(struct sockaddr_un, sun_path) + strlen(address.sun_path) + 1;
165 int bound = socket(AF_UNIX, types[i], 0);
166 require(bound >= 0 && bind(bound, (struct sockaddr*)&address, length) == 0,
167 "bind socket lifetime fixture");
168 if (types[i] == SOCK_STREAM) {
169 require(listen(bound, 1) == 0, "listen lifetime fixture");
170 }
171 int duplicate = dup(bound);
172 require(duplicate >= 0 && close(bound) == 0, "close duplicated bound descriptor");
173 struct stat metadata;
174 require(lstat(address.sun_path, &metadata) == 0 && S_ISSOCK(metadata.st_mode),
175 "duplicated socket retains pathname");
176 int replacement = socket(AF_UNIX, types[i], 0);
177 require(replacement >= 0, "replacement socket");
178 errno = 0;
179 require(bind(replacement, (struct sockaddr*)&address, length) == -1 && errno == EADDRINUSE,
180 "bound name cannot be replaced by bind");
181 require(close(duplicate) == 0 && lstat(address.sun_path, &metadata) == 0 &&
182 S_ISSOCK(metadata.st_mode),
183 "final close preserves socket pathname");
184 errno = 0;
185 require(connect(replacement, (struct sockaddr*)&address, length) == -1 && errno == ECONNREFUSED,
186 "closed pathname refuses connections");
187 require(unlink(address.sun_path) == 0 &&
188 bind(replacement, (struct sockaddr*)&address, length) == 0 &&
189 close(replacement) == 0 && unlink(address.sun_path) == 0,
190 "explicit unlink permits rebinding");
191 }
192
193 struct sockaddr_un address = {.sun_family = AF_UNIX};
194 snprintf(address.sun_path, sizeof(address.sun_path), "/tmp/socket-rebind-%ld", (long)getpid());
195 const socklen_t length = offsetof(struct sockaddr_un, sun_path) + strlen(address.sun_path) + 1;
196 int original = socket(AF_UNIX, SOCK_DGRAM, 0);
197 int sender = socket(AF_UNIX, SOCK_DGRAM, 0);
198 require(original >= 0 && sender >= 0 && bind(original, (struct sockaddr*)&address, length) == 0 &&
199 connect(sender, (struct sockaddr*)&address, length) == 0 &&
200 unlink(address.sun_path) == 0,
201 "unlink a connected datagram endpoint");
202 int replacement = socket(AF_UNIX, SOCK_DGRAM, 0);
203 require(replacement >= 0 && bind(replacement, (struct sockaddr*)&address, length) == 0,
204 "reuse unlinked pathname for a new endpoint");
205 char byte = 0;
206 require(sendto(sender, "n", 1, 0, (struct sockaddr*)&address, length) == 1 &&
207 recv(replacement, &byte, 1, 0) == 1 && byte == 'n' && send(sender, "o", 1, 0) == 1,
208 "explicit datagram destination preserves the default peer");
209 require(recv(original, &byte, 1, 0) == 1 && byte == 'o' && close(original) == 0,
210 "unlinked endpoint remains usable until close");
211 errno = 0;
212 require(send(sender, "x", 1, 0) == -1 && errno == ECONNREFUSED,
213 "old connection cannot reach replacement endpoint");
214 require(connect(sender, (struct sockaddr*)&address, length) == 0 &&
215 send(sender, "n", 1, 0) == 1 && recv(replacement, &byte, 1, 0) == 1 && byte == 'n',
216 "closing old endpoint preserves replacement pathname");
217 require(close(sender) == 0 && close(replacement) == 0 && unlink(address.sun_path) == 0,
218 "remove replacement endpoint");
219 puts("SOCKET-PATH-CONTRACT: PASS close-unlink-rebind");
220 rename_socket_paths("/tmp");
221 puts("SOCKET-PATH-CONTRACT: PASS ramfs-rename-replacement");
222 rename_socket_paths("");
223 puts("SOCKET-PATH-CONTRACT: PASS ext2-rename-replacement");
224 abstract_socket_contracts();
225 puts("SOCKET-PATH-CONTRACT: PASS abstract-namespace");
226}