xref: /linux/tools/testing/selftests/filesystems/failfs/failfs_test.c (revision cd051cfe1e35a471fc2cdf6d32fae6ee23305ecb)
1 // SPDX-License-Identifier: GPL-2.0
2 #define _GNU_SOURCE
3 #include <errno.h>
4 #include <fcntl.h>
5 #include <limits.h>
6 #include <link.h>
7 #include <sched.h>
8 #include <signal.h>
9 #include <stdio.h>
10 #include <stdlib.h>
11 #include <string.h>
12 #include <sys/mount.h>
13 #include <sys/prctl.h>
14 #include <sys/stat.h>
15 #include <sys/syscall.h>
16 #include <sys/types.h>
17 #include <sys/vfs.h>
18 #include <sys/wait.h>
19 #include <unistd.h>
20 
21 #include "../../kselftest_harness.h"
22 
23 #ifndef __NR_fchroot
24 #define __NR_fchroot 472
25 #endif
26 
27 #ifndef FD_PIDFS_ROOT
28 #define FD_PIDFS_ROOT -10002
29 #endif
30 
31 #ifndef FD_NSFS_ROOT
32 #define FD_NSFS_ROOT -10003
33 #endif
34 
35 #ifndef FD_FAILFS_ROOT
36 #define FD_FAILFS_ROOT -10004
37 #endif
38 
39 #define NOBODY_UID 65534
40 
41 /* Child sentinel exit code: the exec was blocked as expected. */
42 #define FAILFS_EXEC_BLOCKED 99
43 
44 /* Stack for the CLONE_FS helper in fchroot_sentinel_shared_fs_struct. */
45 #define FAILFS_CLONE_STACK (64 * 1024)
46 
47 static int sys_fchroot(int fd, unsigned int flags)
48 {
49 	return syscall(__NR_fchroot, fd, flags);
50 }
51 
52 /*
53  * Raw syscall: glibc's getcwd() rejects the kernel's "(unreachable)"
54  * result and falls back to a generic implementation.
55  */
56 static long sys_getcwd(char *buf, size_t size)
57 {
58 	return syscall(__NR_getcwd, buf, size);
59 }
60 
61 static int drop_to_nobody(void)
62 {
63 	return setresuid(NOBODY_UID, NOBODY_UID, NOBODY_UID);
64 }
65 
66 /* Parked CLONE_FS child; dies with its parent so it never leaks. */
67 static int failfs_park(void *arg)
68 {
69 	pid_t parent = (pid_t)(long)arg;
70 
71 	prctl(PR_SET_PDEATHSIG, SIGKILL);
72 	/* The parent may have died before the death signal was armed. */
73 	if (getppid() != parent)
74 		_exit(0);
75 	pause();
76 	return 0;
77 }
78 
79 /* Is fd a dynamically linked ELF with an absolute PT_INTERP interpreter? */
80 static int elf_has_absolute_interp(int fd)
81 {
82 	ElfW(Ehdr) ehdr;
83 	ElfW(Phdr) phdr;
84 	char interp;
85 	int i;
86 
87 	if (pread(fd, &ehdr, sizeof(ehdr), 0) != sizeof(ehdr))
88 		return 0;
89 	if (memcmp(ehdr.e_ident, ELFMAG, SELFMAG) != 0)
90 		return 0;
91 
92 	for (i = 0; i < ehdr.e_phnum; i++) {
93 		if (pread(fd, &phdr, sizeof(phdr),
94 			  ehdr.e_phoff + i * sizeof(phdr)) != sizeof(phdr))
95 			return 0;
96 		if (phdr.p_type != PT_INTERP)
97 			continue;
98 		if (pread(fd, &interp, 1, phdr.p_offset) != 1)
99 			return 0;
100 		return interp == '/';
101 	}
102 
103 	return 0;
104 }
105 
106 TEST(fchdir_sentinel)
107 {
108 	char buf[PATH_MAX];
109 	int fd;
110 
111 	ASSERT_EQ(fchdir(FD_FAILFS_ROOT), 0);
112 
113 	/* The working directory is unreachable from the process root. */
114 	ASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0);
115 	ASSERT_EQ(strncmp(buf, "(unreachable)", 13), 0);
116 
117 	/* Every AT_FDCWD-relative lookup fails. */
118 	ASSERT_EQ(openat(AT_FDCWD, "foo", O_RDONLY), -1);
119 	ASSERT_EQ(errno, EOPNOTSUPP);
120 	ASSERT_EQ(openat(AT_FDCWD, ".", O_RDONLY), -1);
121 	ASSERT_EQ(errno, EOPNOTSUPP);
122 	ASSERT_EQ(openat(AT_FDCWD, "..", O_RDONLY), -1);
123 	ASSERT_EQ(errno, EOPNOTSUPP);
124 	ASSERT_EQ(openat(AT_FDCWD, "foo", O_WRONLY | O_CREAT, 0600), -1);
125 	ASSERT_EQ(errno, EOPNOTSUPP);
126 
127 	/* The cwd cannot be pinned by following /proc/self/cwd into it. */
128 	ASSERT_EQ(open("/proc/self/cwd", O_PATH), -1);
129 	ASSERT_EQ(errno, EOPNOTSUPP);
130 
131 	/* The root is untouched so absolute lookups keep working... */
132 	fd = open("/", O_RDONLY | O_DIRECTORY);
133 	ASSERT_GE(fd, 0);
134 	ASSERT_EQ(close(fd), 0);
135 
136 	/* ... and the working directory can be recovered. */
137 	ASSERT_EQ(chdir("/"), 0);
138 	ASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0);
139 	ASSERT_EQ(strcmp(buf, "/"), 0);
140 }
141 
142 TEST(fchdir_rejects_other_sentinels)
143 {
144 	ASSERT_EQ(fchdir(FD_PIDFS_ROOT), -1);
145 	ASSERT_EQ(errno, EBADF);
146 	ASSERT_EQ(fchdir(FD_NSFS_ROOT), -1);
147 	ASSERT_EQ(errno, EBADF);
148 	ASSERT_EQ(fchdir(-10009), -1);
149 	ASSERT_EQ(errno, EBADF);
150 }
151 
152 TEST(fchroot_flags)
153 {
154 	int fd;
155 
156 	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 1), -1);
157 	ASSERT_EQ(errno, EINVAL);
158 
159 	fd = open("/", O_PATH | O_DIRECTORY);
160 	ASSERT_GE(fd, 0);
161 	ASSERT_EQ(sys_fchroot(fd, 1), -1);
162 	ASSERT_EQ(errno, EINVAL);
163 	ASSERT_EQ(close(fd), 0);
164 }
165 
166 TEST(fchroot_bad_fd)
167 {
168 	ASSERT_EQ(sys_fchroot(-1, 0), -1);
169 	ASSERT_EQ(errno, EBADF);
170 
171 	/* Only FD_FAILFS_ROOT is a valid sentinel. */
172 	ASSERT_EQ(sys_fchroot(FD_PIDFS_ROOT, 0), -1);
173 	ASSERT_EQ(errno, EBADF);
174 	ASSERT_EQ(sys_fchroot(FD_NSFS_ROOT, 0), -1);
175 	ASSERT_EQ(errno, EBADF);
176 }
177 
178 TEST(fchroot_notdir)
179 {
180 	int fd;
181 
182 	fd = open("/proc/self/status", O_RDONLY);
183 	ASSERT_GE(fd, 0);
184 	ASSERT_EQ(sys_fchroot(fd, 0), -1);
185 	ASSERT_EQ(errno, ENOTDIR);
186 	ASSERT_EQ(close(fd), 0);
187 }
188 
189 TEST(fchroot_realfd_requires_cap)
190 {
191 	int fd;
192 
193 	if (geteuid() == 0)
194 		ASSERT_EQ(drop_to_nobody(), 0);
195 
196 	fd = open("/", O_PATH | O_DIRECTORY);
197 	ASSERT_GE(fd, 0);
198 	ASSERT_EQ(sys_fchroot(fd, 0), -1);
199 	ASSERT_EQ(errno, EPERM);
200 	ASSERT_EQ(close(fd), 0);
201 }
202 
203 TEST(fchroot_realfd)
204 {
205 	char template[] = "/tmp/failfs_test.XXXXXX";
206 	char path[PATH_MAX];
207 	struct stat st;
208 	int tmpfd, dfd, fd;
209 
210 	if (geteuid() != 0)
211 		SKIP(return, "fchroot() with a regular fd requires CAP_SYS_CHROOT");
212 
213 	tmpfd = open("/tmp", O_PATH | O_DIRECTORY);
214 	ASSERT_GE(tmpfd, 0);
215 
216 	ASSERT_NE(mkdtemp(template), NULL);
217 	snprintf(path, sizeof(path), "%s/canary", template);
218 	fd = open(path, O_WRONLY | O_CREAT, 0600);
219 	ASSERT_GE(fd, 0);
220 	ASSERT_EQ(close(fd), 0);
221 
222 	dfd = open(template, O_PATH | O_DIRECTORY);
223 	ASSERT_GE(dfd, 0);
224 	ASSERT_EQ(sys_fchroot(dfd, 0), 0);
225 	ASSERT_EQ(close(dfd), 0);
226 
227 	ASSERT_EQ(stat("/canary", &st), 0);
228 
229 	/* Best-effort cleanup: dirfd-anchored I/O works with the new root. */
230 	snprintf(path, sizeof(path), "%s/canary", template + strlen("/tmp/"));
231 	unlinkat(tmpfd, path, 0);
232 	unlinkat(tmpfd, template + strlen("/tmp/"), AT_REMOVEDIR);
233 }
234 
235 TEST(fchroot_sentinel)
236 {
237 	char template[] = "/tmp/failfs_test.XXXXXX";
238 	struct stat realroot, st;
239 	struct statfs sfs;
240 	char buf[PATH_MAX];
241 	int procfd, tmpfd, dfd, fd;
242 	struct {
243 		struct file_handle handle;
244 		unsigned char f_handle[MAX_HANDLE_SZ];
245 	} fh;
246 	int mntid;
247 	ssize_t ret;
248 
249 	if (geteuid() != 0)
250 		SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT");
251 
252 	ASSERT_EQ(stat("/", &realroot), 0);
253 	procfd = open("/proc", O_PATH | O_DIRECTORY);
254 	ASSERT_GE(procfd, 0);
255 	tmpfd = open("/tmp", O_PATH | O_DIRECTORY);
256 	ASSERT_GE(tmpfd, 0);
257 	ASSERT_NE(mkdtemp(template), NULL);
258 	dfd = open(template, O_RDONLY | O_DIRECTORY);
259 	ASSERT_GE(dfd, 0);
260 
261 	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
262 
263 	/* Absolute lookups fail. */
264 	ASSERT_EQ(open("/etc/passwd", O_RDONLY), -1);
265 	ASSERT_EQ(errno, EOPNOTSUPP);
266 	ASSERT_EQ(mkdir("/foo", 0700), -1);
267 	ASSERT_EQ(errno, EOPNOTSUPP);
268 
269 	/*
270 	 * The root cannot be referenced at all - not even an O_PATH open,
271 	 * which skips ->permission(), because it lands on the root as a
272 	 * jumped walk terminal that ->d_weak_revalidate() refuses.
273 	 */
274 	ASSERT_EQ(open("/", O_RDONLY | O_DIRECTORY), -1);
275 	ASSERT_EQ(errno, EOPNOTSUPP);
276 	ASSERT_EQ(open("/", O_PATH), -1);
277 	ASSERT_EQ(errno, EOPNOTSUPP);
278 	ASSERT_EQ(statfs("/", &sfs), -1);
279 	ASSERT_EQ(errno, EOPNOTSUPP);
280 
281 	/*
282 	 * It cannot be pinned by following /proc/self/root into it either
283 	 * (only the root is in failfs here, so self/cwd is still real).
284 	 */
285 	ASSERT_EQ(openat(procfd, "self/root", O_PATH), -1);
286 	ASSERT_EQ(errno, EOPNOTSUPP);
287 
288 	/* Nor encoded into a file handle. */
289 	fh.handle.handle_bytes = MAX_HANDLE_SZ;
290 	ASSERT_EQ(name_to_handle_at(AT_FDCWD, "/", &fh.handle, &mntid, 0), -1);
291 	ASSERT_EQ(errno, EOPNOTSUPP);
292 
293 	/* The working directory is now unreachable from the root. */
294 	ASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0);
295 	ASSERT_EQ(strncmp(buf, "(unreachable)", 13), 0);
296 
297 	/* Lookups anchored at real directories keep working. */
298 	fd = openat(AT_FDCWD, ".", O_RDONLY | O_DIRECTORY);
299 	ASSERT_GE(fd, 0);
300 	ASSERT_EQ(close(fd), 0);
301 	fd = openat(dfd, "canary", O_WRONLY | O_CREAT, 0600);
302 	ASSERT_GE(fd, 0);
303 	ASSERT_EQ(write(fd, "x", 1), 1);
304 	ASSERT_EQ(close(fd), 0);
305 	fd = openat(dfd, "canary", O_RDONLY);
306 	ASSERT_GE(fd, 0);
307 	ASSERT_EQ(close(fd), 0);
308 
309 	/* ".." walks clamp at the top of the mount tree, not at failfs. */
310 	fd = openat(AT_FDCWD, "../../../../../../../../../..", O_PATH);
311 	ASSERT_GE(fd, 0);
312 	ASSERT_EQ(fstat(fd, &st), 0);
313 	ASSERT_EQ(st.st_dev, realroot.st_dev);
314 	ASSERT_EQ(st.st_ino, realroot.st_ino);
315 	ASSERT_EQ(close(fd), 0);
316 
317 	/* readlink of the magic link still works: it does not follow. */
318 	ret = readlinkat(procfd, "self/root", buf, sizeof(buf) - 1);
319 	ASSERT_GT(ret, 0);
320 	buf[ret] = '\0';
321 	TH_LOG("/proc/self/root points to '%s'", buf);
322 	/* d_path() names the failfs root synthetically, never as a real path. */
323 	ASSERT_EQ(strcmp(buf, "failfs:/"), 0);
324 
325 	/* But following it into failfs is refused. */
326 	ASSERT_EQ(fstatat(procfd, "self/root", &st, 0), -1);
327 	ASSERT_EQ(errno, EOPNOTSUPP);
328 
329 	/* Best-effort cleanup via the pre-opened dirfds. */
330 	unlinkat(dfd, "canary", 0);
331 	unlinkat(tmpfd, template + strlen("/tmp/"), AT_REMOVEDIR);
332 }
333 
334 TEST(fchroot_sentinel_absolute_symlink)
335 {
336 	char template[] = "/tmp/failfs_test.XXXXXX";
337 	int tmpfd, dfd, fd;
338 
339 	if (geteuid() != 0)
340 		SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT");
341 
342 	tmpfd = open("/tmp", O_PATH | O_DIRECTORY);
343 	ASSERT_GE(tmpfd, 0);
344 	ASSERT_NE(mkdtemp(template), NULL);
345 	dfd = open(template, O_RDONLY | O_DIRECTORY);
346 	ASSERT_GE(dfd, 0);
347 
348 	fd = openat(dfd, "target", O_WRONLY | O_CREAT, 0600);
349 	ASSERT_GE(fd, 0);
350 	ASSERT_EQ(close(fd), 0);
351 	ASSERT_EQ(symlinkat("target", dfd, "rel"), 0);
352 	ASSERT_EQ(symlinkat("/etc", dfd, "abs"), 0);
353 
354 	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
355 
356 	/* Relative symlinks keep resolving within the dirfd-anchored walk... */
357 	fd = openat(dfd, "rel", O_RDONLY);
358 	ASSERT_GE(fd, 0);
359 	ASSERT_EQ(close(fd), 0);
360 
361 	/* ... absolute symlinks restart the walk at the failfs root. */
362 	ASSERT_EQ(openat(dfd, "abs", O_RDONLY), -1);
363 	ASSERT_EQ(errno, EOPNOTSUPP);
364 
365 	/* Best-effort cleanup via the pre-opened dirfds. */
366 	unlinkat(dfd, "abs", 0);
367 	unlinkat(dfd, "rel", 0);
368 	unlinkat(dfd, "target", 0);
369 	unlinkat(tmpfd, template + strlen("/tmp/"), AT_REMOVEDIR);
370 }
371 
372 TEST(fchroot_sentinel_unprivileged)
373 {
374 	char buf[PATH_MAX];
375 
376 	if (geteuid() == 0)
377 		ASSERT_EQ(drop_to_nobody(), 0);
378 
379 	/* Without no_new_privs entering failfs is not allowed... */
380 	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), -1);
381 	ASSERT_EQ(errno, EPERM);
382 
383 	/* ... with no_new_privs set it is allowed. */
384 	ASSERT_EQ(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0);
385 	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
386 
387 	ASSERT_EQ(open("/etc/passwd", O_RDONLY), -1);
388 	ASSERT_EQ(errno, EOPNOTSUPP);
389 
390 	/* The task counts as chrooted: no user namespaces anymore. */
391 	ASSERT_EQ(unshare(CLONE_NEWUSER), -1);
392 	ASSERT_EQ(errno, EPERM);
393 
394 	/* With both root and cwd in failfs getcwd() reports "/". */
395 	ASSERT_EQ(fchdir(FD_FAILFS_ROOT), 0);
396 	ASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0);
397 	ASSERT_EQ(strcmp(buf, "/"), 0);
398 }
399 
400 TEST(fchroot_sentinel_rejected_when_chrooted)
401 {
402 	char template[] = "/tmp/failfs_test.XXXXXX";
403 	int tmpfd;
404 
405 	if (geteuid() != 0)
406 		SKIP(return, "chroot() requires CAP_SYS_CHROOT");
407 
408 	tmpfd = open("/tmp", O_PATH | O_DIRECTORY);
409 	ASSERT_GE(tmpfd, 0);
410 	ASSERT_NE(mkdtemp(template), NULL);
411 	ASSERT_EQ(chroot(template), 0);
412 	ASSERT_EQ(chdir("/"), 0);
413 
414 	/* Remove the jail while still privileged; sticky /tmp blocks nobody. */
415 	unlinkat(tmpfd, template + strlen("/tmp/"), AT_REMOVEDIR);
416 
417 	ASSERT_EQ(drop_to_nobody(), 0);
418 	ASSERT_EQ(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0);
419 
420 	/* An unprivileged chrooted task must not lift its ".." barrier. */
421 	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), -1);
422 	ASSERT_EQ(errno, EPERM);
423 }
424 
425 TEST(fchroot_sentinel_shared_fs_struct)
426 {
427 	char stack[FAILFS_CLONE_STACK];
428 	pid_t pid;
429 
430 	if (geteuid() == 0)
431 		ASSERT_EQ(drop_to_nobody(), 0);
432 
433 	/* A CLONE_FS sibling shares the fs_struct: bump fs->users to 2. */
434 	pid = clone(failfs_park, stack + sizeof(stack), CLONE_FS | SIGCHLD,
435 		    (void *)(long)getpid());
436 	ASSERT_GE(pid, 0);
437 
438 	ASSERT_EQ(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0);
439 
440 	/*
441 	 * A sibling without no_new_privs could exec a setuid binary with
442 	 * the failfs root, so a shared fs_struct is refused even with
443 	 * no_new_privs set.
444 	 */
445 	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), -1);
446 	ASSERT_EQ(errno, EINVAL);
447 
448 	ASSERT_EQ(kill(pid, SIGKILL), 0);
449 	ASSERT_EQ(waitpid(pid, NULL, 0), pid);
450 }
451 
452 TEST(fchroot_sentinel_no_overmount)
453 {
454 	if (geteuid() != 0)
455 		SKIP(return, "mounting requires privileges");
456 
457 	/*
458 	 * Contain the blast radius: if failfs ever regressed and "/"
459 	 * resolved to the real root, the tmpfs mount below must not touch
460 	 * the host. A private mount namespace keeps it local to this child.
461 	 */
462 	ASSERT_EQ(unshare(CLONE_NEWNS), 0);
463 	ASSERT_EQ(mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL), 0);
464 
465 	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
466 
467 	/*
468 	 * Nothing can be mounted on top of the failfs root. It cannot even
469 	 * be named as a mount target: resolving "/" is refused before the
470 	 * mount machinery (which, failfs being in no mount namespace, would
471 	 * reject it anyway) is ever reached. open_tree(OPEN_TREE_CLONE) is
472 	 * likewise moot since no fd to the root can be obtained.
473 	 */
474 	ASSERT_EQ(mount("none", "/", "tmpfs", 0, NULL), -1);
475 	ASSERT_EQ(errno, EOPNOTSUPP);
476 }
477 
478 TEST(fchroot_sentinel_setns_escape)
479 {
480 	struct stat realroot, st;
481 	int nsfd;
482 
483 	if (geteuid() != 0)
484 		SKIP(return, "setns() to a mount namespace requires privileges");
485 
486 	ASSERT_EQ(stat("/", &realroot), 0);
487 	nsfd = open("/proc/self/ns/mnt", O_RDONLY);
488 	ASSERT_GE(nsfd, 0);
489 
490 	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
491 	ASSERT_EQ(open("/etc", O_PATH), -1);
492 	ASSERT_EQ(errno, EOPNOTSUPP);
493 
494 	/* A mount namespace fd is the key out: it resets root and cwd. */
495 	ASSERT_EQ(setns(nsfd, CLONE_NEWNS), 0);
496 	ASSERT_EQ(close(nsfd), 0);
497 
498 	ASSERT_EQ(stat("/", &st), 0);
499 	ASSERT_EQ(st.st_dev, realroot.st_dev);
500 	ASSERT_EQ(st.st_ino, realroot.st_ino);
501 }
502 
503 TEST(fchroot_sentinel_exec)
504 {
505 	pid_t pid;
506 	int status;
507 
508 	if (geteuid() != 0)
509 		SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT");
510 
511 	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
512 
513 	/*
514 	 * Exec in a child: a wrongly successful exec would replace the test
515 	 * image and its exit code would not match the sentinel below.
516 	 */
517 	pid = fork();
518 	ASSERT_GE(pid, 0);
519 	if (pid == 0) {
520 		execl("/bin/true", "true", NULL);
521 		_exit(errno == EOPNOTSUPP ? FAILFS_EXEC_BLOCKED : 1);
522 	}
523 	ASSERT_EQ(waitpid(pid, &status, 0), pid);
524 	ASSERT_TRUE(WIFEXITED(status));
525 	ASSERT_EQ(WEXITSTATUS(status), FAILFS_EXEC_BLOCKED);
526 }
527 
528 TEST(fchroot_sentinel_exec_interpreter)
529 {
530 	static const char * const argv[] = { "failfs_test", NULL };
531 	static const char * const envp[] = { NULL };
532 	pid_t pid;
533 	int status, exefd;
534 
535 	if (geteuid() != 0)
536 		SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT");
537 
538 	/* Exec ourselves: the one binary guaranteed to be around. */
539 	exefd = open("/proc/self/exe", O_RDONLY);
540 	ASSERT_GE(exefd, 0);
541 	if (!elf_has_absolute_interp(exefd))
542 		SKIP(return, "test binary has no absolute PT_INTERP interpreter");
543 
544 	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
545 
546 	/*
547 	 * The binary itself needs no path lookup - it is executed by fd -
548 	 * but loading it fails on opening the absolute PT_INTERP
549 	 * interpreter. Run it in a child so a wrongly successful exec does
550 	 * not replace the test image and masquerade as a pass.
551 	 */
552 	pid = fork();
553 	ASSERT_GE(pid, 0);
554 	if (pid == 0) {
555 		syscall(__NR_execveat, exefd, "", argv, envp, AT_EMPTY_PATH);
556 		_exit(errno == EOPNOTSUPP ? FAILFS_EXEC_BLOCKED : 1);
557 	}
558 	ASSERT_EQ(waitpid(pid, &status, 0), pid);
559 	ASSERT_TRUE(WIFEXITED(status));
560 	ASSERT_EQ(WEXITSTATUS(status), FAILFS_EXEC_BLOCKED);
561 }
562 
563 TEST(fchroot_sentinel_inherited)
564 {
565 	pid_t pid;
566 	int status;
567 
568 	if (geteuid() != 0)
569 		SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT");
570 
571 	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
572 
573 	pid = fork();
574 	ASSERT_GE(pid, 0);
575 	if (pid == 0) {
576 		if (open("/etc", O_PATH) != -1 || errno != EOPNOTSUPP)
577 			_exit(1);
578 		_exit(0);
579 	}
580 	ASSERT_EQ(waitpid(pid, &status, 0), pid);
581 	ASSERT_TRUE(WIFEXITED(status));
582 	ASSERT_EQ(WEXITSTATUS(status), 0);
583 }
584 
585 TEST_HARNESS_MAIN
586