The Pedigree Project 0.1
pivot-root-contract-test/main.c
1#define _GNU_SOURCE
2#include <errno.h>
3#include <fcntl.h>
4#include <limits.h>
5#include <poll.h>
6#include <signal.h>
7#include <stdio.h>
8#include <string.h>
9#include <time.h>
10#include <unistd.h>
11
12#include <sys/mman.h>
13#include <sys/mount.h>
14#include <sys/stat.h>
15#include <sys/syscall.h>
16#include <sys/wait.h>
17
18#define CHECK(expression) \
19 do { \
20 if (!(expression)) { \
21 fprintf(stderr, "PIVOT-ROOT-CONTRACT: line=%d errno=%d\n", __LINE__, errno); \
22 return 0; \
23 } \
24 } while (0)
25#define ERROR(expression, expected) \
26 do { \
27 errno = 0; \
28 CHECK((expression) == -1 && errno == (expected)); \
29 } while (0)
30
31_Static_assert(SYS_pivot_root == 155 && SYS_mount == 165 && SYS_umount2 == 166,
32 "Linux amd64 mount routes");
33
34struct Fixture {
35 char base[128], newRoot[160], putOld[192], oldCwd[160], park[160], restoreTarget[192];
36 int originalCwd, oldRoot, baseFd, newFd, childFd, oldFile;
37 int command[2], response[2];
38 pid_t observer;
39 int created, mounted, pivoted, restored;
40};
41
42static int write_marker(int directory, const char* name, char value) {
43 int fd = openat(directory, name, O_CREAT | O_EXCL | O_WRONLY, 0600);
44 CHECK(fd >= 0);
45 int good = write(fd, &value, 1) == 1;
46 CHECK(close(fd) == 0 && good);
47 return 1;
48}
49
50static int marker(int directory, const char* name, char expected) {
51 int fd = openat(directory, name, O_RDONLY);
52 CHECK(fd >= 0);
53 char value = 0;
54 int good = read(fd, &value, 1) == 1 && value == expected;
55 CHECK(close(fd) == 0 && good);
56 return 1;
57}
58
59static int receive(int fd, char expected) {
60 struct pollfd ready = {fd, POLLIN, 0};
61 CHECK(poll(&ready, 1, 10000) == 1 && (ready.revents & POLLIN));
62 char value = 0;
63 CHECK(read(fd, &value, 1) == 1 && value == expected);
64 return 1;
65}
66
67static int wait_child(pid_t child) {
68 int status = 0;
69 for (unsigned attempt = 0; attempt < 100; ++attempt) {
70 pid_t result = waitpid(child, &status, WNOHANG);
71 CHECK(result >= 0);
72 if (result == child) {
73 CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0);
74 return 1;
75 }
76 const struct timespec delay = {0, 100000000};
77 CHECK(nanosleep(&delay, NULL) == 0);
78 }
79 kill(child, SIGKILL);
80 waitpid(child, &status, 0);
81 CHECK(0);
82}
83
84static int setup(struct Fixture* fixture) {
85 CHECK(geteuid() == 0);
86 fixture->originalCwd = open(".", O_PATH | O_DIRECTORY);
87 CHECK(fixture->originalCwd >= 0 && chdir("/") == 0);
88 fixture->oldRoot = open("/", O_PATH | O_DIRECTORY);
89 CHECK(fixture->oldRoot >= 0);
90 snprintf(fixture->base, sizeof(fixture->base), "/tmp/pivot-contract-%ld", (long)getpid());
91 snprintf(fixture->newRoot, sizeof(fixture->newRoot), "%s/new", fixture->base);
92 snprintf(fixture->putOld, sizeof(fixture->putOld), "%s/old", fixture->newRoot);
93 snprintf(fixture->oldCwd, sizeof(fixture->oldCwd), "%s/cwd", fixture->base);
94 snprintf(fixture->park, sizeof(fixture->park), "%s/park", fixture->base);
95 snprintf(fixture->restoreTarget, sizeof(fixture->restoreTarget), "/old%s", fixture->park);
96 CHECK(mkdir(fixture->base, 0700) == 0);
97 fixture->created = 1;
98 fixture->baseFd = open(fixture->base, O_PATH | O_DIRECTORY);
99 CHECK(fixture->baseFd >= 0 && write_marker(fixture->baseFd, "original", 'O'));
100 CHECK(mkdirat(fixture->baseFd, "cwd", 0700) == 0 && mkdirat(fixture->baseFd, "new", 0700) == 0 &&
101 mkdirat(fixture->baseFd, "park", 0700) == 0);
102 int cwd = openat(fixture->baseFd, "cwd", O_PATH | O_DIRECTORY);
103 CHECK(cwd >= 0 && write_marker(cwd, "old-marker", 'S') && close(cwd) == 0);
104 fixture->oldFile = openat(fixture->baseFd, "original", O_RDONLY);
105 CHECK(fixture->oldFile >= 0);
106 CHECK(mount("none", fixture->newRoot, "tmpfs", 0, NULL) == 0);
107 fixture->mounted = 1;
108 fixture->newFd = open(fixture->newRoot, O_PATH | O_DIRECTORY);
109 CHECK(fixture->newFd >= 0);
110 const char* directories[] = {"old", "child", "proc", "dev", "mapped"};
111 for (size_t i = 0; i < sizeof(directories) / sizeof(directories[0]); ++i)
112 CHECK(mkdirat(fixture->newFd, directories[i], 0755) == 0);
113 CHECK(write_marker(fixture->newFd, "new-marker", 'N'));
114 CHECK(symlinkat("/new-marker", fixture->newFd, "absolute-link") == 0 &&
115 symlinkat("../new-marker", fixture->newFd, "relative-link") == 0);
116 for (unsigned i = 0; i <= 40; ++i) {
117 char name[32], target[32];
118 snprintf(name, sizeof(name), "chain-%u", i);
119 if (i == 40)
120 snprintf(target, sizeof(target), "new-marker");
121 else
122 snprintf(target, sizeof(target), "chain-%u", i + 1);
123 CHECK(symlinkat(target, fixture->newFd, name) == 0);
124 }
125 CHECK(symlinkat("child/", fixture->newFd, "directory-target") == 0 &&
126 symlinkat("directory-target", fixture->newFd, "directory-chain") == 0);
127 char path[192];
128 snprintf(path, sizeof(path), "%s/proc", fixture->newRoot);
129 CHECK(mount("none", path, "proc", 0, NULL) == 0);
130 snprintf(path, sizeof(path), "%s/child", fixture->newRoot);
131 CHECK(mount("none", path, "tmpfs", 0, NULL) == 0);
132 fixture->childFd = open(path, O_PATH | O_DIRECTORY);
133 CHECK(fixture->childFd >= 0 && write_marker(fixture->childFd, "child-marker", 'C'));
134 return 1;
135}
136
137static int unprivileged(const struct Fixture* fixture) {
138 CHECK(setgid(42424) == 0 && setuid(42424) == 0);
139 ERROR(syscall(SYS_pivot_root, fixture->newRoot, fixture->putOld), EPERM);
140 return 1;
141}
142
143static int confined(const struct Fixture* fixture) {
144 CHECK(chroot(fixture->newRoot) == 0 && chdir("/") == 0);
145 CHECK(marker(AT_FDCWD, "/new-marker", 'N') && marker(AT_FDCWD, "/../../new-marker", 'N'));
146 ERROR(open("/dev/tty", O_RDWR | O_NOCTTY), ENOENT);
147 ERROR(open(fixture->base, O_PATH | O_DIRECTORY), ENOENT);
148 return 1;
149}
150
151static int before_pivot(struct Fixture* fixture) {
152 char file[192], byte = 0;
153 snprintf(file, sizeof(file), "%s/new-marker", fixture->newRoot);
154 ERROR(syscall(SYS_pivot_root, (const char*)1, fixture->putOld), EFAULT);
155 ERROR(syscall(SYS_pivot_root, fixture->newRoot, (const char*)1), EFAULT);
156 ERROR(syscall(SYS_pivot_root, file, fixture->putOld), ENOTDIR);
157 ERROR(syscall(SYS_pivot_root, fixture->oldCwd, fixture->putOld), EINVAL);
158 ERROR(syscall(SYS_pivot_root, fixture->newRoot, fixture->base), EINVAL);
159 ERROR(syscall(SYS_pivot_root, fixture->newRoot, fixture->newRoot), EOPNOTSUPP);
160 ERROR(mount("none", fixture->newRoot, "tmpfs", 0, NULL), EOPNOTSUPP);
161 ERROR(mount("none", fixture->park, "tmpfs", MS_RDONLY, NULL), EOPNOTSUPP);
162 ERROR(mount("none", fixture->park, "tmpfs", 0, "size=4096"), EOPNOTSUPP);
163 ERROR(syscall(SYS_umount2, fixture->newRoot, 0x40000000), EOPNOTSUPP);
164 ERROR(read(fixture->newFd, &byte, 1), EBADF);
165 ERROR(linkat(fixture->newFd, "old", fixture->newFd, "directory-link", 0), EPERM);
166 pid_t child = fork();
167 CHECK(child >= 0);
168 if (!child)
169 _exit(unprivileged(fixture) ? 0 : 1);
170 CHECK(wait_child(child));
171 child = fork();
172 CHECK(child >= 0);
173 if (!child)
174 _exit(confined(fixture) ? 0 : 1);
175 CHECK(wait_child(child));
176 char cwd[PATH_MAX];
177 CHECK(getcwd(cwd, sizeof(cwd)) && !strcmp(cwd, "/") && marker(fixture->baseFd, "original", 'O'));
178 return 1;
179}
180
181static int observe_pivot(const struct Fixture* fixture) {
182 close(fixture->command[1]);
183 close(fixture->response[0]);
184 CHECK(chdir(fixture->oldCwd) == 0 && write(fixture->response[1], "R", 1) == 1);
185 CHECK(receive(fixture->command[0], 'P'));
186 char cwd[PATH_MAX], expected[192];
187 snprintf(expected, sizeof(expected), "/old%s", fixture->oldCwd);
188 CHECK(getcwd(cwd, sizeof(cwd)) && !strcmp(cwd, expected));
189 CHECK(marker(AT_FDCWD, "old-marker", 'S') && marker(AT_FDCWD, "/new-marker", 'N'));
190 CHECK(write(fixture->response[1], "G", 1) == 1);
191 return 1;
192}
193
194static int symlink_paths(const struct Fixture* fixture) {
195 CHECK(marker(AT_FDCWD, "/chain-10", 'N') && marker(AT_FDCWD, "/chain-1", 'N'));
196 ERROR(open("/chain-0", O_RDONLY), ELOOP);
197 int fd = open("/chain-0", O_PATH | O_NOFOLLOW);
198 struct stat attributes;
199 CHECK(fd >= 0 && fstat(fd, &attributes) == 0 && S_ISLNK(attributes.st_mode));
200 CHECK(close(fd) == 0);
201 CHECK(marker(AT_FDCWD, "/directory-chain/child-marker", 'C'));
202 fd = open("/directory-chain/", O_PATH | O_DIRECTORY);
203 CHECK(fd >= 0 && close(fd) == 0);
204 ERROR(open("/chain-10/", O_PATH), ENOTDIR);
205 char magic[96];
206 snprintf(magic, sizeof(magic), "/proc/self/fd/%d/chain-10", fixture->newFd);
207 CHECK(marker(AT_FDCWD, magic, 'N'));
208 return 1;
209}
210
211static int mount_report(void) {
212 char contents[32768];
213 int fd = open("/proc/mounts", O_RDONLY);
214 CHECK(fd >= 0);
215 size_t used = 0;
216 while (used < sizeof(contents) - 1) {
217 ssize_t count = read(fd, contents + used, sizeof(contents) - 1 - used);
218 CHECK(count >= 0);
219 if (!count)
220 break;
221 used += count;
222 }
223 CHECK(close(fd) == 0 && used < sizeof(contents) - 1);
224 contents[used] = 0;
225 unsigned found = 0;
226 char* state = NULL;
227 for (char* line = strtok_r(contents, "\n", &state); line; line = strtok_r(NULL, "\n", &state)) {
228 char source[256], path[512], type[64];
229 CHECK(sscanf(line, "%255s %511s %63s", source, path, type) == 3);
230 if (!strcmp(path, "/"))
231 found |= 1;
232 if (!strcmp(path, "/old"))
233 found |= 2;
234 if (!strcmp(path, "/child"))
235 found |= 4;
236 if (!strcmp(path, "/proc"))
237 found |= 8;
238 }
239 CHECK(found == 15);
240 return 1;
241}
242
243static int mapped_attachment(void) {
244 CHECK(mount("none", "/mapped", "tmpfs", 0, NULL) == 0);
245 const long page = sysconf(_SC_PAGESIZE);
246 CHECK(page > 0);
247 int fd = open("/mapped/data", O_CREAT | O_EXCL | O_RDWR, 0600);
248 CHECK(fd >= 0 && ftruncate(fd, page) == 0);
249 char* mapping = mmap(NULL, page, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
250 CHECK(mapping != MAP_FAILED && close(fd) == 0);
251 mapping[0] = 'M';
252 ERROR(umount2("/mapped", 0), EBUSY);
253 int command[2], response[2];
254 CHECK(pipe(command) == 0 && pipe(response) == 0);
255 pid_t child = fork();
256 CHECK(child >= 0);
257 if (!child) {
258 close(command[1]);
259 close(response[0]);
260 const int good = mapping[0] == 'M' && write(response[1], "R", 1) == 1 &&
261 receive(command[0], 'X') && mapping[0] == 'M';
262 // Exit must release the inherited VMA's last attachment reference.
263 _exit(good ? 0 : 1);
264 }
265 close(command[0]);
266 close(response[1]);
267 CHECK(receive(response[0], 'R'));
268 CHECK(munmap(mapping, page) == 0);
269 ERROR(umount2("/mapped", 0), EBUSY);
270 CHECK(write(command[1], "X", 1) == 1 && wait_child(child));
271 CHECK(close(command[1]) == 0 && close(response[0]) == 0);
272 CHECK(umount2("/mapped", 0) == 0);
273 ERROR(open("/mapped/data", O_RDONLY), ENOENT);
274 return 1;
275}
276
277static int after_pivot(struct Fixture* fixture) {
278 CHECK(write(fixture->command[1], "P", 1) == 1 && receive(fixture->response[0], 'G'));
279 CHECK(wait_child(fixture->observer));
280 fixture->observer = -1;
281 char cwd[PATH_MAX], value = 0;
282 CHECK(getcwd(cwd, sizeof(cwd)) && !strcmp(cwd, "/"));
283 CHECK(marker(AT_FDCWD, "/new-marker", 'N') && marker(AT_FDCWD, "/../../new-marker", 'N') &&
284 marker(AT_FDCWD, "/absolute-link", 'N') && marker(AT_FDCWD, "/relative-link", 'N'));
285 CHECK(pread(fixture->oldFile, &value, 1, 0) == 1 && value == 'O');
286 CHECK(marker(fixture->baseFd, "original", 'O') && marker(fixture->newFd, "../new-marker", 'N'));
287 CHECK(fchdir(fixture->oldRoot) == 0 && getcwd(cwd, sizeof(cwd)) && !strcmp(cwd, "/old"));
288 CHECK(chdir("/") == 0);
289 ERROR(open(fixture->base, O_PATH | O_DIRECTORY), ENOENT);
290 int ns = open("/proc/self/ns/uts", O_RDONLY);
291 CHECK(ns >= 0 && close(ns) == 0);
292 ssize_t length = readlink("/proc/self/ns/uts", cwd, sizeof(cwd));
293 CHECK(length > 5 && !memcmp(cwd, "uts:[", 5));
294 CHECK(mount_report());
295 CHECK(symlink_paths(fixture));
296 CHECK(mapped_attachment());
297 ERROR(umount2("/old", 0), EBUSY);
298 ERROR(umount2("/child", 0), EBUSY);
299 CHECK(syscall(SYS_umount2, "/child", MNT_DETACH) == 0);
300 ERROR(open("/child/child-marker", O_RDONLY), ENOENT);
301 CHECK(marker(fixture->childFd, "child-marker", 'C'));
302 CHECK(fchdir(fixture->childFd) == 0 && marker(AT_FDCWD, "child-marker", 'C'));
303 ERROR(getcwd(cwd, sizeof(cwd)) ? 0 : -1, ENOENT);
304 CHECK(chdir("/") == 0);
305 CHECK(syscall(SYS_mount, "none", "/child", "ramfs", 0, NULL) == 0);
306 int replacement = open("/child", O_PATH | O_DIRECTORY);
307 CHECK(replacement >= 0 && write_marker(replacement, "child-marker", 'R'));
308 CHECK(marker(replacement, "child-marker", 'R') && marker(fixture->childFd, "child-marker", 'C'));
309 CHECK(close(replacement) == 0);
310 return 1;
311}
312
313static int restore(struct Fixture* fixture) {
314 if (!fixture->pivoted)
315 return 1;
316 if (syscall(SYS_pivot_root, "/old", fixture->restoreTarget) < 0) {
317 fprintf(stderr, "PIVOT-ROOT-CONTRACT: restoration errno=%d\n", errno);
318 return 0;
319 }
320 fixture->pivoted = 0;
321 fixture->restored = 1;
322 CHECK(chdir("/") == 0 && marker(fixture->baseFd, "original", 'O'));
323 char path[160];
324 snprintf(path, sizeof(path), "%s/original", fixture->base);
325 CHECK(marker(AT_FDCWD, path, 'O'));
326 ERROR(open("/new-marker", O_RDONLY), ENOENT);
327 return 1;
328}
329
330int main(void) {
331 struct Fixture fixture = {.originalCwd = -1,
332 .oldRoot = -1,
333 .baseFd = -1,
334 .newFd = -1,
335 .childFd = -1,
336 .oldFile = -1,
337 .command = {-1, -1},
338 .response = {-1, -1},
339 .observer = -1};
340 int success = 0;
341 const mode_t previousMask = umask(0);
342 setvbuf(stdout, NULL, _IOLBF, 0);
343 puts("PIVOT-ROOT-CONTRACT: BEGIN");
344 if (!setup(&fixture) || !before_pivot(&fixture))
345 goto cleanup;
346 if (pipe(fixture.command) || pipe(fixture.response))
347 goto cleanup;
348 fixture.observer = fork();
349 if (!fixture.observer)
350 _exit(observe_pivot(&fixture) ? 0 : 1);
351 if (fixture.observer < 0 || !receive(fixture.response[0], 'R'))
352 goto cleanup;
353 if (syscall(SYS_pivot_root, fixture.newRoot, fixture.putOld) < 0)
354 goto cleanup;
355 fixture.pivoted = 1;
356 puts("PIVOT-ROOT-CONTRACT: pivoted");
357 if (!after_pivot(&fixture) || !restore(&fixture))
358 goto cleanup;
359 success = 1;
360cleanup:
361 if (fixture.observer > 0) {
362 kill(fixture.observer, SIGKILL);
363 waitpid(fixture.observer, NULL, 0);
364 }
365 if (!restore(&fixture))
366 success = 0;
367 if (!fixture.pivoted && fixture.mounted &&
368 umount2(fixture.restored ? fixture.park : fixture.newRoot, MNT_DETACH) < 0)
369 success = 0;
370 if (!fixture.pivoted && fixture.originalCwd >= 0 && fchdir(fixture.originalCwd) < 0)
371 success = 0;
372 if (fixture.baseFd >= 0) {
373 unlinkat(fixture.baseFd, "original", 0);
374 unlinkat(fixture.baseFd, "cwd/old-marker", 0);
375 unlinkat(fixture.baseFd, "cwd", AT_REMOVEDIR);
376 unlinkat(fixture.baseFd, "new", AT_REMOVEDIR);
377 unlinkat(fixture.baseFd, "park", AT_REMOVEDIR);
378 }
379 int descriptors[] = {fixture.originalCwd, fixture.oldRoot, fixture.baseFd,
380 fixture.newFd, fixture.childFd, fixture.oldFile,
381 fixture.command[0], fixture.command[1], fixture.response[0],
382 fixture.response[1]};
383 for (size_t i = 0; i < sizeof(descriptors) / sizeof(descriptors[0]); ++i)
384 if (descriptors[i] >= 0)
385 close(descriptors[i]);
386 if (!fixture.pivoted && fixture.created)
387 rmdir(fixture.base);
388 umask(previousMask);
389 printf("PIVOT-ROOT-CONTRACT: %s restored=%d\n", success ? "PASS" : "FAIL", fixture.restored);
390 return success ? 0 : 1;
391}