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