diff options
| author | Christian Brauner <brauner@kernel.org> | 2026-07-27 11:49:54 +0200 |
|---|---|---|
| committer | Christian Brauner <brauner@kernel.org> | 2026-07-27 17:18:00 +0200 |
| commit | 1d38e750a389e919c1608dbc61cd5c93cd4915dc (patch) | |
| tree | c29e5e94512a7e8ae1a148c36d2f717dbd2b6d12 /tools/testing | |
| parent | 1590cf0329716306e948a8fc29f1d3ee87d3989f (diff) | |
| parent | a45a6605cd04f2e55c1650a49bdb05106ac07ccc (diff) | |
Merge patch series "fs: add failfs"
Christian Brauner <brauner@kernel.org> says:
nullfs provides a permanently empty and immutable directory. Lookups
fail with ENOENT. The directory can be opened, read, stat, mounted upon.
It behaves like nothing is there.
Add its counterpart failfs where the semantics are not "there is
nothing here" but "nothing is supported here". Every operation that
reaches the filesystem fails with EOPNOTSUPP. Even statfs()/fstatfs()
fail so the filesystem cannot be discovered through an fd to it.
EOPNOTSUPP rather than a permission errno keeps that coherent. There
is no permission model in which anything could ever be allowed and
EACCES or EPERM would merely suggest that different credentials might
succeed while EIO would suggest corruption. It also makes hitting the
failfs boundary mostly quite dinstinguishable. A task anchoring its
lookups at real directory file descriptors may be able to tell a failfs
refusal from an ordinary permission failure. I wouldn't go so far as
guaranteeing that but it should mostly work.
The root cannot be opened at all not even with O_PATH. It is never
reached by a lookup in a parent directory. The only way to a path-walk
terminal at the root is a jump through a /proc/<pid>/{root,cwd} magic
link or by mountpoint traversal. The root also refuses
->d_weak_revalidate() which the VFS calls for jumped terminals. That
closes every remaining way to reference it. An O_PATH
open is refused and name_to_handle_at() cannot encode it into a file
handle, and following a magic link into it fails. A plain readlink() of
such a link still works and shows "failfs:/".
There is a single instance of failfs mounted during early boot via
kern_mount() making it logically distinct from every mount namespace.
Since the mount is a member of no mount namespace mounting onto it
fails. So nothing can ever be mounted on top of it. It cannot be cloned
via OPEN_TREE_CLONE and it does not show up in statmount()/listmount()
or /proc/<pid>/mountinfo. The filesystem is not registered so it is
not visible in /proc/filesystems and cannot be mounted from userspace.
This lets tasks shed their filesystem state completely. A process with
its root directory or working directory in failfs must anchor every path
lookup at an explicit file descriptor or is doomed to fail any lookup.
Absolute paths, absolute symlinks, and AT_FDCWD-relative lookups
simply fail. Followup patches will expose it via a new FD_FAILFS_ROOT
file descriptor sentinel understood by fchdir() and the new fchroot()
system call.
Fun fact, because of how dynamic binary execution work with PT_INTERP
this also currently prevents execution of dynamic binaries because
loaders have absolute paths (see selftests).
* patches from https://patch.msgid.link/20260724-work-failfs-v2-0-485dabbae185@kernel.org:
Documentation: add failfs documentation
selftests/filesystems: add failfs selftests
arch: hookup fchroot() system call
fs: support FD_FAILFS_ROOT in fchroot()
fs: add fchroot()
fs: support FD_FAILFS_ROOT in fchdir()
fs: add failfs
Link: https://patch.msgid.link/20260724-work-failfs-v2-0-485dabbae185@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
Diffstat (limited to 'tools/testing')
| -rw-r--r-- | tools/testing/selftests/Makefile | 1 | ||||
| -rw-r--r-- | tools/testing/selftests/filesystems/failfs/.gitignore | 2 | ||||
| -rw-r--r-- | tools/testing/selftests/filesystems/failfs/Makefile | 5 | ||||
| -rw-r--r-- | tools/testing/selftests/filesystems/failfs/failfs_test.c | 585 |
4 files changed, 593 insertions, 0 deletions
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile index 8d4db2241cc2..f87167bcf582 100644 --- a/tools/testing/selftests/Makefile +++ b/tools/testing/selftests/Makefile @@ -33,6 +33,7 @@ TARGETS += fchmodat2 TARGETS += filesystems TARGETS += filesystems/binderfs TARGETS += filesystems/epoll +TARGETS += filesystems/failfs TARGETS += filesystems/fat TARGETS += filesystems/overlayfs TARGETS += filesystems/statmount diff --git a/tools/testing/selftests/filesystems/failfs/.gitignore b/tools/testing/selftests/filesystems/failfs/.gitignore new file mode 100644 index 000000000000..cd3b5d884d7e --- /dev/null +++ b/tools/testing/selftests/filesystems/failfs/.gitignore @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-only +failfs_test diff --git a/tools/testing/selftests/filesystems/failfs/Makefile b/tools/testing/selftests/filesystems/failfs/Makefile new file mode 100644 index 000000000000..3c5d98b4fe72 --- /dev/null +++ b/tools/testing/selftests/filesystems/failfs/Makefile @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: GPL-2.0 +CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) +TEST_GEN_PROGS := failfs_test + +include ../../lib.mk diff --git a/tools/testing/selftests/filesystems/failfs/failfs_test.c b/tools/testing/selftests/filesystems/failfs/failfs_test.c new file mode 100644 index 000000000000..29a3c294127e --- /dev/null +++ b/tools/testing/selftests/filesystems/failfs/failfs_test.c @@ -0,0 +1,585 @@ +// SPDX-License-Identifier: GPL-2.0 +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <link.h> +#include <sched.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/mount.h> +#include <sys/prctl.h> +#include <sys/stat.h> +#include <sys/syscall.h> +#include <sys/types.h> +#include <sys/vfs.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "../../kselftest_harness.h" + +#ifndef __NR_fchroot +#define __NR_fchroot 472 +#endif + +#ifndef FD_PIDFS_ROOT +#define FD_PIDFS_ROOT -10002 +#endif + +#ifndef FD_NSFS_ROOT +#define FD_NSFS_ROOT -10003 +#endif + +#ifndef FD_FAILFS_ROOT +#define FD_FAILFS_ROOT -10004 +#endif + +#define NOBODY_UID 65534 + +/* Child sentinel exit code: the exec was blocked as expected. */ +#define FAILFS_EXEC_BLOCKED 99 + +/* Stack for the CLONE_FS helper in fchroot_sentinel_shared_fs_struct. */ +#define FAILFS_CLONE_STACK (64 * 1024) + +static int sys_fchroot(int fd, unsigned int flags) +{ + return syscall(__NR_fchroot, fd, flags); +} + +/* + * Raw syscall: glibc's getcwd() rejects the kernel's "(unreachable)" + * result and falls back to a generic implementation. + */ +static long sys_getcwd(char *buf, size_t size) +{ + return syscall(__NR_getcwd, buf, size); +} + +static int drop_to_nobody(void) +{ + return setresuid(NOBODY_UID, NOBODY_UID, NOBODY_UID); +} + +/* Parked CLONE_FS child; dies with its parent so it never leaks. */ +static int failfs_park(void *arg) +{ + pid_t parent = (pid_t)(long)arg; + + prctl(PR_SET_PDEATHSIG, SIGKILL); + /* The parent may have died before the death signal was armed. */ + if (getppid() != parent) + _exit(0); + pause(); + return 0; +} + +/* Is fd a dynamically linked ELF with an absolute PT_INTERP interpreter? */ +static int elf_has_absolute_interp(int fd) +{ + ElfW(Ehdr) ehdr; + ElfW(Phdr) phdr; + char interp; + int i; + + if (pread(fd, &ehdr, sizeof(ehdr), 0) != sizeof(ehdr)) + return 0; + if (memcmp(ehdr.e_ident, ELFMAG, SELFMAG) != 0) + return 0; + + for (i = 0; i < ehdr.e_phnum; i++) { + if (pread(fd, &phdr, sizeof(phdr), + ehdr.e_phoff + i * sizeof(phdr)) != sizeof(phdr)) + return 0; + if (phdr.p_type != PT_INTERP) + continue; + if (pread(fd, &interp, 1, phdr.p_offset) != 1) + return 0; + return interp == '/'; + } + + return 0; +} + +TEST(fchdir_sentinel) +{ + char buf[PATH_MAX]; + int fd; + + ASSERT_EQ(fchdir(FD_FAILFS_ROOT), 0); + + /* The working directory is unreachable from the process root. */ + ASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0); + ASSERT_EQ(strncmp(buf, "(unreachable)", 13), 0); + + /* Every AT_FDCWD-relative lookup fails. */ + ASSERT_EQ(openat(AT_FDCWD, "foo", O_RDONLY), -1); + ASSERT_EQ(errno, EOPNOTSUPP); + ASSERT_EQ(openat(AT_FDCWD, ".", O_RDONLY), -1); + ASSERT_EQ(errno, EOPNOTSUPP); + ASSERT_EQ(openat(AT_FDCWD, "..", O_RDONLY), -1); + ASSERT_EQ(errno, EOPNOTSUPP); + ASSERT_EQ(openat(AT_FDCWD, "foo", O_WRONLY | O_CREAT, 0600), -1); + ASSERT_EQ(errno, EOPNOTSUPP); + + /* The cwd cannot be pinned by following /proc/self/cwd into it. */ + ASSERT_EQ(open("/proc/self/cwd", O_PATH), -1); + ASSERT_EQ(errno, EOPNOTSUPP); + + /* The root is untouched so absolute lookups keep working... */ + fd = open("/", O_RDONLY | O_DIRECTORY); + ASSERT_GE(fd, 0); + ASSERT_EQ(close(fd), 0); + + /* ... and the working directory can be recovered. */ + ASSERT_EQ(chdir("/"), 0); + ASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0); + ASSERT_EQ(strcmp(buf, "/"), 0); +} + +TEST(fchdir_rejects_other_sentinels) +{ + ASSERT_EQ(fchdir(FD_PIDFS_ROOT), -1); + ASSERT_EQ(errno, EBADF); + ASSERT_EQ(fchdir(FD_NSFS_ROOT), -1); + ASSERT_EQ(errno, EBADF); + ASSERT_EQ(fchdir(-10009), -1); + ASSERT_EQ(errno, EBADF); +} + +TEST(fchroot_flags) +{ + int fd; + + ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 1), -1); + ASSERT_EQ(errno, EINVAL); + + fd = open("/", O_PATH | O_DIRECTORY); + ASSERT_GE(fd, 0); + ASSERT_EQ(sys_fchroot(fd, 1), -1); + ASSERT_EQ(errno, EINVAL); + ASSERT_EQ(close(fd), 0); +} + +TEST(fchroot_bad_fd) +{ + ASSERT_EQ(sys_fchroot(-1, 0), -1); + ASSERT_EQ(errno, EBADF); + + /* Only FD_FAILFS_ROOT is a valid sentinel. */ + ASSERT_EQ(sys_fchroot(FD_PIDFS_ROOT, 0), -1); + ASSERT_EQ(errno, EBADF); + ASSERT_EQ(sys_fchroot(FD_NSFS_ROOT, 0), -1); + ASSERT_EQ(errno, EBADF); +} + +TEST(fchroot_notdir) +{ + int fd; + + fd = open("/proc/self/status", O_RDONLY); + ASSERT_GE(fd, 0); + ASSERT_EQ(sys_fchroot(fd, 0), -1); + ASSERT_EQ(errno, ENOTDIR); + ASSERT_EQ(close(fd), 0); +} + +TEST(fchroot_realfd_requires_cap) +{ + int fd; + + if (geteuid() == 0) + ASSERT_EQ(drop_to_nobody(), 0); + + fd = open("/", O_PATH | O_DIRECTORY); + ASSERT_GE(fd, 0); + ASSERT_EQ(sys_fchroot(fd, 0), -1); + ASSERT_EQ(errno, EPERM); + ASSERT_EQ(close(fd), 0); +} + +TEST(fchroot_realfd) +{ + char template[] = "/tmp/failfs_test.XXXXXX"; + char path[PATH_MAX]; + struct stat st; + int tmpfd, dfd, fd; + + if (geteuid() != 0) + SKIP(return, "fchroot() with a regular fd requires CAP_SYS_CHROOT"); + + tmpfd = open("/tmp", O_PATH | O_DIRECTORY); + ASSERT_GE(tmpfd, 0); + + ASSERT_NE(mkdtemp(template), NULL); + snprintf(path, sizeof(path), "%s/canary", template); + fd = open(path, O_WRONLY | O_CREAT, 0600); + ASSERT_GE(fd, 0); + ASSERT_EQ(close(fd), 0); + + dfd = open(template, O_PATH | O_DIRECTORY); + ASSERT_GE(dfd, 0); + ASSERT_EQ(sys_fchroot(dfd, 0), 0); + ASSERT_EQ(close(dfd), 0); + + ASSERT_EQ(stat("/canary", &st), 0); + + /* Best-effort cleanup: dirfd-anchored I/O works with the new root. */ + snprintf(path, sizeof(path), "%s/canary", template + strlen("/tmp/")); + unlinkat(tmpfd, path, 0); + unlinkat(tmpfd, template + strlen("/tmp/"), AT_REMOVEDIR); +} + +TEST(fchroot_sentinel) +{ + char template[] = "/tmp/failfs_test.XXXXXX"; + struct stat realroot, st; + struct statfs sfs; + char buf[PATH_MAX]; + int procfd, tmpfd, dfd, fd; + struct { + struct file_handle handle; + unsigned char f_handle[MAX_HANDLE_SZ]; + } fh; + int mntid; + ssize_t ret; + + if (geteuid() != 0) + SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT"); + + ASSERT_EQ(stat("/", &realroot), 0); + procfd = open("/proc", O_PATH | O_DIRECTORY); + ASSERT_GE(procfd, 0); + tmpfd = open("/tmp", O_PATH | O_DIRECTORY); + ASSERT_GE(tmpfd, 0); + ASSERT_NE(mkdtemp(template), NULL); + dfd = open(template, O_RDONLY | O_DIRECTORY); + ASSERT_GE(dfd, 0); + + ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0); + + /* Absolute lookups fail. */ + ASSERT_EQ(open("/etc/passwd", O_RDONLY), -1); + ASSERT_EQ(errno, EOPNOTSUPP); + ASSERT_EQ(mkdir("/foo", 0700), -1); + ASSERT_EQ(errno, EOPNOTSUPP); + + /* + * The root cannot be referenced at all - not even an O_PATH open, + * which skips ->permission(), because it lands on the root as a + * jumped walk terminal that ->d_weak_revalidate() refuses. + */ + ASSERT_EQ(open("/", O_RDONLY | O_DIRECTORY), -1); + ASSERT_EQ(errno, EOPNOTSUPP); + ASSERT_EQ(open("/", O_PATH), -1); + ASSERT_EQ(errno, EOPNOTSUPP); + ASSERT_EQ(statfs("/", &sfs), -1); + ASSERT_EQ(errno, EOPNOTSUPP); + + /* + * It cannot be pinned by following /proc/self/root into it either + * (only the root is in failfs here, so self/cwd is still real). + */ + ASSERT_EQ(openat(procfd, "self/root", O_PATH), -1); + ASSERT_EQ(errno, EOPNOTSUPP); + + /* Nor encoded into a file handle. */ + fh.handle.handle_bytes = MAX_HANDLE_SZ; + ASSERT_EQ(name_to_handle_at(AT_FDCWD, "/", &fh.handle, &mntid, 0), -1); + ASSERT_EQ(errno, EOPNOTSUPP); + + /* The working directory is now unreachable from the root. */ + ASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0); + ASSERT_EQ(strncmp(buf, "(unreachable)", 13), 0); + + /* Lookups anchored at real directories keep working. */ + fd = openat(AT_FDCWD, ".", O_RDONLY | O_DIRECTORY); + ASSERT_GE(fd, 0); + ASSERT_EQ(close(fd), 0); + fd = openat(dfd, "canary", O_WRONLY | O_CREAT, 0600); + ASSERT_GE(fd, 0); + ASSERT_EQ(write(fd, "x", 1), 1); + ASSERT_EQ(close(fd), 0); + fd = openat(dfd, "canary", O_RDONLY); + ASSERT_GE(fd, 0); + ASSERT_EQ(close(fd), 0); + + /* ".." walks clamp at the top of the mount tree, not at failfs. */ + fd = openat(AT_FDCWD, "../../../../../../../../../..", O_PATH); + ASSERT_GE(fd, 0); + ASSERT_EQ(fstat(fd, &st), 0); + ASSERT_EQ(st.st_dev, realroot.st_dev); + ASSERT_EQ(st.st_ino, realroot.st_ino); + ASSERT_EQ(close(fd), 0); + + /* readlink of the magic link still works: it does not follow. */ + ret = readlinkat(procfd, "self/root", buf, sizeof(buf) - 1); + ASSERT_GT(ret, 0); + buf[ret] = '\0'; + TH_LOG("/proc/self/root points to '%s'", buf); + /* d_path() names the failfs root synthetically, never as a real path. */ + ASSERT_EQ(strcmp(buf, "failfs:/"), 0); + + /* But following it into failfs is refused. */ + ASSERT_EQ(fstatat(procfd, "self/root", &st, 0), -1); + ASSERT_EQ(errno, EOPNOTSUPP); + + /* Best-effort cleanup via the pre-opened dirfds. */ + unlinkat(dfd, "canary", 0); + unlinkat(tmpfd, template + strlen("/tmp/"), AT_REMOVEDIR); +} + +TEST(fchroot_sentinel_absolute_symlink) +{ + char template[] = "/tmp/failfs_test.XXXXXX"; + int tmpfd, dfd, fd; + + if (geteuid() != 0) + SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT"); + + tmpfd = open("/tmp", O_PATH | O_DIRECTORY); + ASSERT_GE(tmpfd, 0); + ASSERT_NE(mkdtemp(template), NULL); + dfd = open(template, O_RDONLY | O_DIRECTORY); + ASSERT_GE(dfd, 0); + + fd = openat(dfd, "target", O_WRONLY | O_CREAT, 0600); + ASSERT_GE(fd, 0); + ASSERT_EQ(close(fd), 0); + ASSERT_EQ(symlinkat("target", dfd, "rel"), 0); + ASSERT_EQ(symlinkat("/etc", dfd, "abs"), 0); + + ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0); + + /* Relative symlinks keep resolving within the dirfd-anchored walk... */ + fd = openat(dfd, "rel", O_RDONLY); + ASSERT_GE(fd, 0); + ASSERT_EQ(close(fd), 0); + + /* ... absolute symlinks restart the walk at the failfs root. */ + ASSERT_EQ(openat(dfd, "abs", O_RDONLY), -1); + ASSERT_EQ(errno, EOPNOTSUPP); + + /* Best-effort cleanup via the pre-opened dirfds. */ + unlinkat(dfd, "abs", 0); + unlinkat(dfd, "rel", 0); + unlinkat(dfd, "target", 0); + unlinkat(tmpfd, template + strlen("/tmp/"), AT_REMOVEDIR); +} + +TEST(fchroot_sentinel_unprivileged) +{ + char buf[PATH_MAX]; + + if (geteuid() == 0) + ASSERT_EQ(drop_to_nobody(), 0); + + /* Without no_new_privs entering failfs is not allowed... */ + ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), -1); + ASSERT_EQ(errno, EPERM); + + /* ... with no_new_privs set it is allowed. */ + ASSERT_EQ(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0); + ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0); + + ASSERT_EQ(open("/etc/passwd", O_RDONLY), -1); + ASSERT_EQ(errno, EOPNOTSUPP); + + /* The task counts as chrooted: no user namespaces anymore. */ + ASSERT_EQ(unshare(CLONE_NEWUSER), -1); + ASSERT_EQ(errno, EPERM); + + /* With both root and cwd in failfs getcwd() reports "/". */ + ASSERT_EQ(fchdir(FD_FAILFS_ROOT), 0); + ASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0); + ASSERT_EQ(strcmp(buf, "/"), 0); +} + +TEST(fchroot_sentinel_rejected_when_chrooted) +{ + char template[] = "/tmp/failfs_test.XXXXXX"; + int tmpfd; + + if (geteuid() != 0) + SKIP(return, "chroot() requires CAP_SYS_CHROOT"); + + tmpfd = open("/tmp", O_PATH | O_DIRECTORY); + ASSERT_GE(tmpfd, 0); + ASSERT_NE(mkdtemp(template), NULL); + ASSERT_EQ(chroot(template), 0); + ASSERT_EQ(chdir("/"), 0); + + /* Remove the jail while still privileged; sticky /tmp blocks nobody. */ + unlinkat(tmpfd, template + strlen("/tmp/"), AT_REMOVEDIR); + + ASSERT_EQ(drop_to_nobody(), 0); + ASSERT_EQ(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0); + + /* An unprivileged chrooted task must not lift its ".." barrier. */ + ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), -1); + ASSERT_EQ(errno, EPERM); +} + +TEST(fchroot_sentinel_shared_fs_struct) +{ + char stack[FAILFS_CLONE_STACK]; + pid_t pid; + + if (geteuid() == 0) + ASSERT_EQ(drop_to_nobody(), 0); + + /* A CLONE_FS sibling shares the fs_struct: bump fs->users to 2. */ + pid = clone(failfs_park, stack + sizeof(stack), CLONE_FS | SIGCHLD, + (void *)(long)getpid()); + ASSERT_GE(pid, 0); + + ASSERT_EQ(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0); + + /* + * A sibling without no_new_privs could exec a setuid binary with + * the failfs root, so a shared fs_struct is refused even with + * no_new_privs set. + */ + ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), -1); + ASSERT_EQ(errno, EINVAL); + + ASSERT_EQ(kill(pid, SIGKILL), 0); + ASSERT_EQ(waitpid(pid, NULL, 0), pid); +} + +TEST(fchroot_sentinel_no_overmount) +{ + if (geteuid() != 0) + SKIP(return, "mounting requires privileges"); + + /* + * Contain the blast radius: if failfs ever regressed and "/" + * resolved to the real root, the tmpfs mount below must not touch + * the host. A private mount namespace keeps it local to this child. + */ + ASSERT_EQ(unshare(CLONE_NEWNS), 0); + ASSERT_EQ(mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL), 0); + + ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0); + + /* + * Nothing can be mounted on top of the failfs root. It cannot even + * be named as a mount target: resolving "/" is refused before the + * mount machinery (which, failfs being in no mount namespace, would + * reject it anyway) is ever reached. open_tree(OPEN_TREE_CLONE) is + * likewise moot since no fd to the root can be obtained. + */ + ASSERT_EQ(mount("none", "/", "tmpfs", 0, NULL), -1); + ASSERT_EQ(errno, EOPNOTSUPP); +} + +TEST(fchroot_sentinel_setns_escape) +{ + struct stat realroot, st; + int nsfd; + + if (geteuid() != 0) + SKIP(return, "setns() to a mount namespace requires privileges"); + + ASSERT_EQ(stat("/", &realroot), 0); + nsfd = open("/proc/self/ns/mnt", O_RDONLY); + ASSERT_GE(nsfd, 0); + + ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0); + ASSERT_EQ(open("/etc", O_PATH), -1); + ASSERT_EQ(errno, EOPNOTSUPP); + + /* A mount namespace fd is the key out: it resets root and cwd. */ + ASSERT_EQ(setns(nsfd, CLONE_NEWNS), 0); + ASSERT_EQ(close(nsfd), 0); + + ASSERT_EQ(stat("/", &st), 0); + ASSERT_EQ(st.st_dev, realroot.st_dev); + ASSERT_EQ(st.st_ino, realroot.st_ino); +} + +TEST(fchroot_sentinel_exec) +{ + pid_t pid; + int status; + + if (geteuid() != 0) + SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT"); + + ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0); + + /* + * Exec in a child: a wrongly successful exec would replace the test + * image and its exit code would not match the sentinel below. + */ + pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + execl("/bin/true", "true", NULL); + _exit(errno == EOPNOTSUPP ? FAILFS_EXEC_BLOCKED : 1); + } + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_TRUE(WIFEXITED(status)); + ASSERT_EQ(WEXITSTATUS(status), FAILFS_EXEC_BLOCKED); +} + +TEST(fchroot_sentinel_exec_interpreter) +{ + static const char * const argv[] = { "failfs_test", NULL }; + static const char * const envp[] = { NULL }; + pid_t pid; + int status, exefd; + + if (geteuid() != 0) + SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT"); + + /* Exec ourselves: the one binary guaranteed to be around. */ + exefd = open("/proc/self/exe", O_RDONLY); + ASSERT_GE(exefd, 0); + if (!elf_has_absolute_interp(exefd)) + SKIP(return, "test binary has no absolute PT_INTERP interpreter"); + + ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0); + + /* + * The binary itself needs no path lookup - it is executed by fd - + * but loading it fails on opening the absolute PT_INTERP + * interpreter. Run it in a child so a wrongly successful exec does + * not replace the test image and masquerade as a pass. + */ + pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + syscall(__NR_execveat, exefd, "", argv, envp, AT_EMPTY_PATH); + _exit(errno == EOPNOTSUPP ? FAILFS_EXEC_BLOCKED : 1); + } + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_TRUE(WIFEXITED(status)); + ASSERT_EQ(WEXITSTATUS(status), FAILFS_EXEC_BLOCKED); +} + +TEST(fchroot_sentinel_inherited) +{ + pid_t pid; + int status; + + if (geteuid() != 0) + SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT"); + + ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0); + + pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + if (open("/etc", O_PATH) != -1 || errno != EOPNOTSUPP) + _exit(1); + _exit(0); + } + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_TRUE(WIFEXITED(status)); + ASSERT_EQ(WEXITSTATUS(status), 0); +} + +TEST_HARNESS_MAIN |
