History

Merge tag 'vfs-7.3-rc1.failfs' of git://git.kernel.org/pub/scm/linux/kernel/git/vfs/vfs

Pull failfs filesystem from Christian Brauner: "Add failfs and expose a FD_FAILFS_ROOT sentinel. This allows userspace to shed their filesystem state completely. A process with its root or working directory in failfs must anchor every path lookup at an explicit file descriptor. Absolute paths, absolute symlinks and AT_FDCWD-relative lookups simply fail. Failfs is the counterpart to nullfs. nullfs says adds a permanently empty, immutable directory whose lookups fail with ENOENT but which can be opened, read, stat'd and mounted upon. Failfs on the other hand fails every operation. The root cannot be opened at all. A single instance is mounted during early boot via kern_mount(), which makes it logically distinct from every mount namespace. This is accompanied by a new fchroot() system call which makes chrooting via a file descriptor a first class concept. It's possible to chroot into failfs as an unprivileged user provided the task has no new privileges set" * tag 'vfs-7.3-rc1.failfs' of git://git.kernel.org/pub/scm/linux/kernel/git/vfs/vfs: 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

Linus Torvalds committed 2 months ago

Commit cd051cf2 parents d31a688 1d38e7544 files · +931 −5Browse files

Unified · Split

44 files
  1. Documentation/filesystems/failfs.rst +73 −0
  2. Documentation/filesystems/index.rst +1 −0
  3. arch/alpha/kernel/syscalls/syscall.tbl +1 −0
  4. arch/arm/tools/syscall.tbl +1 −0
  5. arch/arm64/tools/syscall_32.tbl +1 −0
  6. arch/m68k/kernel/syscalls/syscall.tbl +1 −0
  7. arch/microblaze/kernel/syscalls/syscall.tbl +1 −0
  8. arch/mips/kernel/syscalls/syscall_n32.tbl +1 −0
  9. arch/mips/kernel/syscalls/syscall_n64.tbl +1 −0
  10. arch/mips/kernel/syscalls/syscall_o32.tbl +1 −0
  11. arch/parisc/kernel/syscalls/syscall.tbl +1 −0
  12. arch/powerpc/kernel/syscalls/syscall.tbl +1 −0
  13. arch/s390/kernel/syscalls/syscall.tbl +1 −0
  14. arch/sh/kernel/syscalls/syscall.tbl +1 −0
  15. arch/sparc/kernel/syscalls/syscall.tbl +1 −0
  16. arch/x86/entry/syscalls/syscall_32.tbl +1 −0
  17. arch/x86/entry/syscalls/syscall_64.tbl +1 −0
  18. arch/xtensa/kernel/syscalls/syscall.tbl +1 −0
  19. fs/Makefile +1 −1
  20. fs/d_path.c +2 −1
  21. fs/failfs.c +166 −0
  22. fs/internal.h +4 −0
  23. fs/namespace.c +1 −0
  24. fs/open.c +50 −1
  25. include/linux/syscalls.h +1 −0
  26. include/uapi/asm-generic/unistd.h +5 −1
  27. include/uapi/linux/fcntl.h +1 −0
  28. include/uapi/linux/magic.h +1 −0
  29. scripts/syscall.tbl +1 −0
  30. tools/include/uapi/asm-generic/unistd.h +5 −1
  31. tools/perf/arch/arm/entry/syscalls/syscall.tbl +1 −0
  32. tools/perf/arch/mips/entry/syscalls/syscall_n64.tbl +1 −0
  33. tools/perf/arch/powerpc/entry/syscalls/syscall.tbl +1 −0
  34. tools/perf/arch/s390/entry/syscalls/syscall.tbl +1 −0
  35. tools/perf/arch/sh/entry/syscalls/syscall.tbl +1 −0
  36. tools/perf/arch/sparc/entry/syscalls/syscall.tbl +1 −0
  37. tools/perf/arch/x86/entry/syscalls/syscall_32.tbl +1 −0
  38. tools/perf/arch/x86/entry/syscalls/syscall_64.tbl +1 −0
  39. tools/perf/arch/xtensa/entry/syscalls/syscall.tbl +1 −0
  40. tools/scripts/syscall.tbl +1 −0
  41. tools/testing/selftests/Makefile +1 −0
  42. tools/testing/selftests/filesystems/failfs/.gitignore +2 −0
  43. tools/testing/selftests/filesystems/failfs/Makefile +5 −0
  44. tools/testing/selftests/filesystems/failfs/failfs_test.c +585 −0
Documentation/filesystems/failfs.rst+73−0
@@ -0,0 +1,73 @@
1+.. SPDX-License-Identifier: GPL-2.0
2+
3+======
4+failfs
5+======
6+
7+failfs is a kernel-internal filesystem that fails every operation
8+reaching it with ``EOPNOTSUPP``. It is the counterpart to nullfs. Where
9+nullfs is permanently empty, failfs means "nothing is supported here".
10+It cannot be mounted from userspace, nothing can be mounted on top of
11+it. It cannot be cloned.
12+
13+The only way into it is the ``FD_FAILFS_ROOT`` file descriptor sentinel which
14+is understood by ``fchdir(2)`` and ``fchroot(2)``.
15+
16+Semantics
17+=========
18+
19+Every path walk of a component through failfs fails with
20+``EOPNOTSUPP`` before that component is parsed, including ``.``.
21+
22+No path lookup can open the root, not even with ``O_PATH``.
23+
24+A process with its working directory in failfs fails every
25+``AT_FDCWD``-relative lookup. As with any working directory that is
26+unreachable from the process root, the ``getcwd(2)`` system call returns
27+a path prefixed with ``(unreachable)``.
28+
29+A process with its root directory in failfs fails every absolute path
30+lookup including absolute symlinks and the interpreter of dynamically
31+linked binaries. In other words, this fails exec.
32+
33+Lookups anchored at explicit directory file descriptors keep working. It
34+is the ``fs_struct`` equivalent of ``RESOLVE_BENEATH``. The process must
35+anchor every lookup at a file descriptor it explicitly holds.
36+
37+Entering
38+========
39+
40+``fchroot(FD_FAILFS_ROOT, 0)`` requires ``CAP_SYS_CHROOT`` in the
41+caller's user namespace, mirroring ``chroot(2)``. Unprivileged callers
42+may enter if all of the following hold:
43+
44+* ``no_new_privs`` is set: setuid binaries on regular mounts remain
45+ reachable via inherited directory file descriptors and executing them
46+ with an unusable root directory is the classic confused deputy.
47+
48+* The caller is not already chrooted: the root directory is what
49+ confines ``..`` resolution and the failfs root can never be reached by
50+ walking up a real mount tree, so moving the root of a chrooted task to
51+ failfs would allow it to escape its chroot via ``openat(fd, "..")``.
52+
53+* The caller does not share its ``fs_struct``: ``no_new_privs`` is
54+ checked on the calling thread, but the root lives in the ``fs_struct``.
55+ A ``CLONE_FS`` sibling without ``no_new_privs`` could otherwise execute
56+ a setuid binary with the failfs root, so entry requires ``fs->users ==
57+ 1``, the same restriction ``setns(2)`` applies for the mount and user
58+ namespaces.
59+
60+Leaving
61+=======
62+
63+Backing out is currently hard, but this is a property of the current
64+implementation, not a guaranteed interface, and may be loosened later.
65+For now a process that entered failfs counts as chrooted, so it cannot
66+create user namespaces to regain ``CAP_SYS_CHROOT``, and ``chroot(2)``
67+or ``fchroot(2)`` back out require ``CAP_SYS_CHROOT``. The remaining way
68+out today is ``setns(2)`` with a mount namespace file descriptor, which
69+requires ``CAP_SYS_ADMIN`` over the target mount namespace as well as
70+``CAP_SYS_CHROOT`` and ``CAP_SYS_ADMIN`` in the caller's user namespace
71+and resets both root and working directory. A process that holds no such
72+file descriptor and restricts ``*chdir()``/``*chroot()``/``setns()`` via
73+seccomp cannot currently get back out.
Documentation/filesystems/index.rst+1−0
@@ -91,6 +91,7 @@ Documentation for filesystem implementations. · 90 unmodified lines
9191 ext3
9292 ext4/index
9393 f2fs
94+ failfs
9495 gfs2/index
9596 hfs
9697 hfsplus
arch/alpha/kernel/syscalls/syscall.tbl+1−0
@@ -511,3 +511,4 @@ · 510 unmodified lines
511511 579 common file_setattr sys_file_setattr
512512 580 common listns sys_listns
513513 581 common rseq_slice_yield sys_rseq_slice_yield
514+582 common fchroot sys_fchroot
arch/arm/tools/syscall.tbl+1−0
@@ -486,3 +486,4 @@ · 485 unmodified lines
486486 469 common file_setattr sys_file_setattr
487487 470 common listns sys_listns
488488 471 common rseq_slice_yield sys_rseq_slice_yield
489+472 common fchroot sys_fchroot
arch/arm64/tools/syscall_32.tbl+1−0
@@ -483,3 +483,4 @@ · 482 unmodified lines
483483 469 common file_setattr sys_file_setattr
484484 470 common listns sys_listns
485485 471 common rseq_slice_yield sys_rseq_slice_yield
486+472 common fchroot sys_fchroot
arch/m68k/kernel/syscalls/syscall.tbl+1−0
@@ -471,3 +471,4 @@ · 470 unmodified lines
471471 469 common file_setattr sys_file_setattr
472472 470 common listns sys_listns
473473 471 common rseq_slice_yield sys_rseq_slice_yield
474+472 common fchroot sys_fchroot
arch/microblaze/kernel/syscalls/syscall.tbl+1−0
@@ -477,3 +477,4 @@ · 476 unmodified lines
477477 469 common file_setattr sys_file_setattr
478478 470 common listns sys_listns
479479 471 common rseq_slice_yield sys_rseq_slice_yield
480+472 common fchroot sys_fchroot
arch/mips/kernel/syscalls/syscall_n32.tbl+1−0
@@ -410,3 +410,4 @@ · 409 unmodified lines
410410 469 n32 file_setattr sys_file_setattr
411411 470 n32 listns sys_listns
412412 471 n32 rseq_slice_yield sys_rseq_slice_yield
413+472 n32 fchroot sys_fchroot
arch/mips/kernel/syscalls/syscall_n64.tbl+1−0
@@ -386,3 +386,4 @@ · 385 unmodified lines
386386 469 n64 file_setattr sys_file_setattr
387387 470 n64 listns sys_listns
388388 471 n64 rseq_slice_yield sys_rseq_slice_yield
389+472 n64 fchroot sys_fchroot
arch/mips/kernel/syscalls/syscall_o32.tbl+1−0
@@ -459,3 +459,4 @@ · 458 unmodified lines
459459 469 o32 file_setattr sys_file_setattr
460460 470 o32 listns sys_listns
461461 471 o32 rseq_slice_yield sys_rseq_slice_yield
462+472 o32 fchroot sys_fchroot
arch/parisc/kernel/syscalls/syscall.tbl+1−0
@@ -470,3 +470,4 @@ · 469 unmodified lines
470470 469 common file_setattr sys_file_setattr
471471 470 common listns sys_listns
472472 471 common rseq_slice_yield sys_rseq_slice_yield
473+472 common fchroot sys_fchroot
arch/powerpc/kernel/syscalls/syscall.tbl+1−0
@@ -562,3 +562,4 @@ · 561 unmodified lines
562562 469 common file_setattr sys_file_setattr
563563 470 common listns sys_listns
564564 471 nospu rseq_slice_yield sys_rseq_slice_yield
565+472 common fchroot sys_fchroot
arch/s390/kernel/syscalls/syscall.tbl+1−0
@@ -398,3 +398,4 @@ · 397 unmodified lines
398398 469 common file_setattr sys_file_setattr
399399 470 common listns sys_listns
400400 471 common rseq_slice_yield sys_rseq_slice_yield
401+472 common fchroot sys_fchroot
arch/sh/kernel/syscalls/syscall.tbl+1−0
@@ -475,3 +475,4 @@ · 474 unmodified lines
475475 469 common file_setattr sys_file_setattr
476476 470 common listns sys_listns
477477 471 common rseq_slice_yield sys_rseq_slice_yield
478+472 common fchroot sys_fchroot
arch/sparc/kernel/syscalls/syscall.tbl+1−0
@@ -517,3 +517,4 @@ · 516 unmodified lines
517517 469 common file_setattr sys_file_setattr
518518 470 common listns sys_listns
519519 471 common rseq_slice_yield sys_rseq_slice_yield
520+472 common fchroot sys_fchroot
arch/x86/entry/syscalls/syscall_32.tbl+1−0
@@ -477,3 +477,4 @@ · 476 unmodified lines
477477 469 i386 file_setattr sys_file_setattr
478478 470 i386 listns sys_listns
479479 471 i386 rseq_slice_yield sys_rseq_slice_yield
480+472 i386 fchroot sys_fchroot
arch/x86/entry/syscalls/syscall_64.tbl+1−0
@@ -396,6 +396,7 @@ · 395 unmodified lines
396396 469 common file_setattr sys_file_setattr
397397 470 common listns sys_listns
398398 471 common rseq_slice_yield sys_rseq_slice_yield
399+472 common fchroot sys_fchroot
399400
400401 #
401402 # Due to a historical design error, certain syscalls are numbered differently
arch/xtensa/kernel/syscalls/syscall.tbl+1−0
@@ -442,3 +442,4 @@ · 441 unmodified lines
442442 469 common file_setattr sys_file_setattr
443443 470 common listns sys_listns
444444 471 common rseq_slice_yield sys_rseq_slice_yield
445+472 common fchroot sys_fchroot
fs/Makefile+1−1
@@ -16,7 +16,7 @@ obj-y := open.o read_write.o file_table.o super.o \ · 15 unmodified lines
1616 stack.o fs_struct.o statfs.o fs_pin.o nsfs.o \
1717 fs_dirent.o fs_context.o fs_parser.o fsopen.o init.o \
1818 kernel_read_file.o mnt_idmapping.o remap_range.o pidfs.o \
19− file_attr.o fserror.o nullfs.o
19+ file_attr.o fserror.o nullfs.o failfs.o
2020
2121 obj-$(CONFIG_BUFFER_HEAD) += buffer.o mpage.o
2222 obj-$(CONFIG_PROC_FS) += proc_namespace.o
fs/d_path.c+2−1
@@ -279,7 +279,8 @@ char *d_path(const struct path *path, char *buf, int buflen) · 278 unmodified lines
279279 * and instead have d_path return the mounted path.
280280 */
281281 if (path->dentry->d_op && path->dentry->d_op->d_dname &&
282− (!IS_ROOT(path->dentry) || path->dentry != path->mnt->mnt_root))
282+ (!IS_ROOT(path->dentry) || path->dentry != path->mnt->mnt_root ||
283+ failfs_mnt(path->mnt)))
283284 return path->dentry->d_op->d_dname(path->dentry, buf, buflen);
284285
285286 rcu_read_lock();
fs/failfs.c+166−0
@@ -0,0 +1,166 @@
1+// SPDX-License-Identifier: GPL-2.0-only
2+/* Copyright (c) 2026 Christian Brauner <brauner@kernel.org> */
3+#include <linux/fs.h>
4+#include <linux/fs/super_types.h>
5+#include <linux/fs_context.h>
6+#include <linux/fs_struct.h>
7+#include <linux/magic.h>
8+#include <linux/mount.h>
9+
10+#include "internal.h"
11+
12+static struct path failfs_root_path = {};
13+
14+void failfs_get_root(struct path *path)
15+{
16+ *path = failfs_root_path;
17+ path_get(path);
18+}
19+
20+bool failfs_mnt(const struct vfsmount *mnt)
21+{
22+ return mnt->mnt_sb == failfs_root_path.mnt->mnt_sb;
23+}
24+
25+static int failfs_permission(struct mnt_idmap *idmap, struct inode *inode,
26+ int mask)
27+{
28+ return -EOPNOTSUPP;
29+}
30+
31+static struct dentry *failfs_lookup(struct inode *dir, struct dentry *dentry,
32+ unsigned int flags)
33+{
34+ /* Unreachable: ->permission() already failed the walk. */
35+ return ERR_PTR(-EOPNOTSUPP);
36+}
37+
38+static int failfs_getattr(struct mnt_idmap *idmap, const struct path *path,
39+ struct kstat *stat, u32 request_mask,
40+ unsigned int query_flags)
41+{
42+ return -EOPNOTSUPP;
43+}
44+
45+static const struct inode_operations failfs_dir_inode_operations = {
46+ .permission = failfs_permission,
47+ .lookup = failfs_lookup,
48+ .getattr = failfs_getattr,
49+};
50+
51+static const struct file_operations failfs_dir_operations = {};
52+
53+static int failfs_d_weak_revalidate(struct dentry *dentry, unsigned int flags)
54+{
55+ /*
56+ * The root is only ever reached as a path-walk terminal by jumping
57+ * to it: as "/" when it is the caller's root, or through a
58+ * /proc/<pid>/{root,cwd} magic link. ->permission() already fails
59+ * every walk of a component, but a jump lands on the root without
60+ * one. Refuse here too so the root cannot be pinned by an O_PATH
61+ * open or encoded into a file handle.
62+ */
63+ return -EOPNOTSUPP;
64+}
65+
66+static char *failfs_dname(struct dentry *dentry, char *buffer, int buflen)
67+{
68+ return dynamic_dname(buffer, buflen, "failfs:/");
69+}
70+
71+static const struct dentry_operations failfs_dentry_operations = {
72+ .d_dname = failfs_dname,
73+ .d_weak_revalidate = failfs_d_weak_revalidate,
74+};
75+
76+static int failfs_statfs(struct dentry *dentry, struct kstatfs *buf)
77+{
78+ return -EOPNOTSUPP;
79+}
80+
81+static const struct super_operations failfs_super_operations = {
82+ .statfs = failfs_statfs,
83+};
84+
85+static int failfs_fill_super(struct super_block *s, struct fs_context *fc)
86+{
87+ struct inode *inode;
88+
89+ s->s_maxbytes = MAX_LFS_FILESIZE;
90+ s->s_blocksize = PAGE_SIZE;
91+ s->s_blocksize_bits = PAGE_SHIFT;
92+ s->s_magic = FAIL_FS_MAGIC;
93+ s->s_op = &failfs_super_operations;
94+ s->s_export_op = NULL;
95+ s->s_xattr = NULL;
96+ s->s_time_gran = 1;
97+ s->s_d_flags = 0;
98+
99+ inode = new_inode(s);
100+ if (!inode)
101+ return -ENOMEM;
102+
103+ /* failfs supports no operations... */
104+ inode->i_mode = S_IFDIR;
105+ set_nlink(inode, 2);
106+ inode->i_op = &failfs_dir_inode_operations;
107+ inode->i_fop = &failfs_dir_operations;
108+ simple_inode_init_ts(inode);
109+ inode->i_ino = 1;
110+ /* ... and is immutable. */
111+ inode->i_flags |= S_IMMUTABLE;
112+
113+ set_default_d_op(s, &failfs_dentry_operations);
114+ s->s_root = d_make_root(inode);
115+ if (!s->s_root)
116+ return -ENOMEM;
117+
118+ return 0;
119+}
120+
121+static int failfs_get_tree(struct fs_context *fc)
122+{
123+ return get_tree_single(fc, failfs_fill_super);
124+}
125+
126+static const struct fs_context_operations failfs_context_ops = {
127+ .get_tree = failfs_get_tree,
128+};
129+
130+static int failfs_init_fs_context(struct fs_context *fc)
131+{
132+ fc->ops = &failfs_context_ops;
133+ fc->global = true;
134+ fc->sb_flags |= SB_NOUSER;
135+ fc->s_iflags |= SB_I_NOEXEC | SB_I_NODEV;
136+ return 0;
137+}
138+
139+int failfs_current_chdir(void)
140+{
141+ struct path path;
142+
143+ failfs_get_root(&path);
144+ set_fs_pwd(current->fs, &path);
145+ path_put(&path);
146+ return 0;
147+}
148+
149+static struct file_system_type failfs_fs_type = {
150+ .name = "failfs",
151+ .init_fs_context = failfs_init_fs_context,
152+ .kill_sb = kill_anon_super,
153+};
154+
155+void __init failfs_init(void)
156+{
157+ struct vfsmount *mnt;
158+
159+ /* A single instance that is member of no mount namespace. */
160+ mnt = kern_mount(&failfs_fs_type);
161+ if (IS_ERR(mnt))
162+ panic("VFS: Failed to create failfs");
163+
164+ failfs_root_path.mnt = mnt;
165+ failfs_root_path.dentry = mnt->mnt_root;
166+}
fs/internal.h+4−0
@@ -362,3 +362,7 @@ int anon_inode_setattr(struct mnt_idmap *idmap, struct dentry *dentry, · 361 unmodified lines
362362 struct iattr *attr);
363363 void pidfs_get_root(struct path *path);
364364 void nsfs_get_root(struct path *path);
365+void failfs_get_root(struct path *path);
366+void __init failfs_init(void);
367+bool failfs_mnt(const struct vfsmount *mnt);
368+int failfs_current_chdir(void);
fs/namespace.c+1−0
@@ -6274,6 +6274,7 @@ void __init mnt_init(void) · 6273 unmodified lines
62746274 shmem_init();
62756275 init_rootfs();
62766276 init_mount_tree();
6277+ failfs_init();
62776278 }
62786279
62796280 void put_mnt_ns(struct mnt_namespace *ns)
fs/open.c+50−1
@@ -570,9 +570,12 @@ retry: · 569 unmodified lines
570570
571571 SYSCALL_DEFINE1(fchdir, unsigned int, fd)
572572 {
573− CLASS(fd_raw, f)(fd);
574573 int error;
575574
575+ if ((int)fd == FD_FAILFS_ROOT)
576+ return failfs_current_chdir();
577+
578+ CLASS(fd_raw, f)(fd);
576579 if (fd_empty(f))
577580 return -EBADF;
578581
@@ -615,6 +618,52 @@ dput_and_out: · 36 unmodified lines
615618 return error;
616619 }
617620
621+SYSCALL_DEFINE2(fchroot, int, fd, unsigned int, flags)
622+{
623+ struct path path;
624+ int error;
625+
626+ if (flags)
627+ return -EINVAL;
628+
629+ if (fd == FD_FAILFS_ROOT) {
630+ if (!ns_capable(current_user_ns(), CAP_SYS_CHROOT)) {
631+ if (!task_no_new_privs(current))
632+ return -EPERM;
633+ /* A shared fs_struct lets a sibling exec setuid past the check above. */
634+ if (current->fs->users != 1)
635+ return -EINVAL;
636+ /* Moving the root to failfs lifts the old root's ".." barrier. */
637+ if (current_chrooted())
638+ return -EPERM;
639+ }
640+ failfs_get_root(&path);
641+ } else {
642+ CLASS(fd_raw, f)(fd);
643+ if (fd_empty(f))
644+ return -EBADF;
645+
646+ if (!d_can_lookup(fd_file(f)->f_path.dentry))
647+ return -ENOTDIR;
648+
649+ error = file_permission(fd_file(f), MAY_EXEC | MAY_CHDIR);
650+ if (error)
651+ return error;
652+
653+ if (!ns_capable(current_user_ns(), CAP_SYS_CHROOT))
654+ return -EPERM;
655+
656+ path = fd_file(f)->f_path;
657+ path_get(&path);
658+ }
659+
660+ error = security_path_chroot(&path);
661+ if (!error)
662+ set_fs_root(current->fs, &path);
663+ path_put(&path);
664+ return error;
665+}
666+
618667 int chmod_common(const struct path *path, umode_t mode)
619668 {
620669 struct inode *inode = path->dentry->d_inode;
include/linux/syscalls.h+1−0
@@ -457,6 +457,7 @@ asmlinkage long sys_faccessat2(int dfd, const char __user *filename, int mode, · 456 unmodified lines
457457 asmlinkage long sys_chdir(const char __user *filename);
458458 asmlinkage long sys_fchdir(unsigned int fd);
459459 asmlinkage long sys_chroot(const char __user *filename);
460+asmlinkage long sys_fchroot(int fd, unsigned int flags);
460461 asmlinkage long sys_fchmod(unsigned int fd, umode_t mode);
461462 asmlinkage long sys_fchmodat(int dfd, const char __user *filename,
462463 umode_t mode);
include/uapi/asm-generic/unistd.h+5−1
@@ -863,8 +863,12 @@ __SYSCALL(__NR_listns, sys_listns) · 862 unmodified lines
863863 #define __NR_rseq_slice_yield 471
864864 __SYSCALL(__NR_rseq_slice_yield, sys_rseq_slice_yield)
865865
866+/* fs/open.c */
867+#define __NR_fchroot 472
868+__SYSCALL(__NR_fchroot, sys_fchroot)
869+
866870 #undef __NR_syscalls
867−#define __NR_syscalls 472
871+#define __NR_syscalls 473
868872
869873 /*
870874 * 32 bit systems traditionally used different
include/uapi/linux/fcntl.h+1−0
@@ -124,6 +124,7 @@ struct delegation { · 123 unmodified lines
124124
125125 #define FD_PIDFS_ROOT -10002 /* Root of the pidfs filesystem */
126126 #define FD_NSFS_ROOT -10003 /* Root of the nsfs filesystem */
127+#define FD_FAILFS_ROOT -10004 /* Root of the failfs filesystem */
127128 #define FD_INVALID -10009 /* Invalid file descriptor: -10000 - EBADF = -10009 */
128129
129130 /* Generic flags for the *at(2) family of syscalls. */
include/uapi/linux/magic.h+1−0
@@ -105,5 +105,6 @@ · 104 unmodified lines
105105 #define PID_FS_MAGIC 0x50494446 /* "PIDF" */
106106 #define GUEST_MEMFD_MAGIC 0x474d454d /* "GMEM" */
107107 #define NULL_FS_MAGIC 0x4E554C4C /* "NULL" */
108+#define FAIL_FS_MAGIC 0x4641494C /* "FAIL" */
108109
109110 #endif /* __LINUX_MAGIC_H__ */
scripts/syscall.tbl+1−0
@@ -412,3 +412,4 @@ · 411 unmodified lines
412412 469 common file_setattr sys_file_setattr
413413 470 common listns sys_listns
414414 471 common rseq_slice_yield sys_rseq_slice_yield
415+472 common fchroot sys_fchroot
tools/include/uapi/asm-generic/unistd.h+5−1
@@ -863,8 +863,12 @@ __SYSCALL(__NR_listns, sys_listns) · 862 unmodified lines
863863 #define __NR_rseq_slice_yield 471
864864 __SYSCALL(__NR_rseq_slice_yield, sys_rseq_slice_yield)
865865
866+/* fs/open.c */
867+#define __NR_fchroot 472
868+__SYSCALL(__NR_fchroot, sys_fchroot)
869+
866870 #undef __NR_syscalls
867−#define __NR_syscalls 472
871+#define __NR_syscalls 473
868872
869873 /*
870874 * 32 bit systems traditionally used different
tools/perf/arch/arm/entry/syscalls/syscall.tbl+1−0
@@ -486,3 +486,4 @@ · 485 unmodified lines
486486 469 common file_setattr sys_file_setattr
487487 470 common listns sys_listns
488488 471 common rseq_slice_yield sys_rseq_slice_yield
489+472 common fchroot sys_fchroot
tools/perf/arch/mips/entry/syscalls/syscall_n64.tbl+1−0
@@ -386,3 +386,4 @@ · 385 unmodified lines
386386 469 n64 file_setattr sys_file_setattr
387387 470 n64 listns sys_listns
388388 471 n64 rseq_slice_yield sys_rseq_slice_yield
389+472 n64 fchroot sys_fchroot
tools/perf/arch/powerpc/entry/syscalls/syscall.tbl+1−0
@@ -562,3 +562,4 @@ · 561 unmodified lines
562562 469 common file_setattr sys_file_setattr
563563 470 common listns sys_listns
564564 471 nospu rseq_slice_yield sys_rseq_slice_yield
565+472 common fchroot sys_fchroot
tools/perf/arch/s390/entry/syscalls/syscall.tbl+1−0
@@ -398,3 +398,4 @@ · 397 unmodified lines
398398 469 common file_setattr sys_file_setattr
399399 470 common listns sys_listns
400400 471 common rseq_slice_yield sys_rseq_slice_yield
401+472 common fchroot sys_fchroot
tools/perf/arch/sh/entry/syscalls/syscall.tbl+1−0
@@ -475,3 +475,4 @@ · 474 unmodified lines
475475 469 common file_setattr sys_file_setattr
476476 470 common listns sys_listns
477477 471 common rseq_slice_yield sys_rseq_slice_yield
478+472 common fchroot sys_fchroot
tools/perf/arch/sparc/entry/syscalls/syscall.tbl+1−0
@@ -517,3 +517,4 @@ · 516 unmodified lines
517517 469 common file_setattr sys_file_setattr
518518 470 common listns sys_listns
519519 471 common rseq_slice_yield sys_rseq_slice_yield
520+472 common fchroot sys_fchroot
tools/perf/arch/x86/entry/syscalls/syscall_32.tbl+1−0
@@ -477,3 +477,4 @@ · 476 unmodified lines
477477 469 i386 file_setattr sys_file_setattr
478478 470 i386 listns sys_listns
479479 471 i386 rseq_slice_yield sys_rseq_slice_yield
480+472 i386 fchroot sys_fchroot
tools/perf/arch/x86/entry/syscalls/syscall_64.tbl+1−0
@@ -396,6 +396,7 @@ · 395 unmodified lines
396396 469 common file_setattr sys_file_setattr
397397 470 common listns sys_listns
398398 471 common rseq_slice_yield sys_rseq_slice_yield
399+472 common fchroot sys_fchroot
399400
400401 #
401402 # Due to a historical design error, certain syscalls are numbered differently
tools/perf/arch/xtensa/entry/syscalls/syscall.tbl+1−0
@@ -442,3 +442,4 @@ · 441 unmodified lines
442442 469 common file_setattr sys_file_setattr
443443 470 common listns sys_listns
444444 471 common rseq_slice_yield sys_rseq_slice_yield
445+472 common fchroot sys_fchroot
tools/scripts/syscall.tbl+1−0
@@ -412,3 +412,4 @@ · 411 unmodified lines
412412 469 common file_setattr sys_file_setattr
413413 470 common listns sys_listns
414414 471 common rseq_slice_yield sys_rseq_slice_yield
415+472 common fchroot sys_fchroot
tools/testing/selftests/Makefile+1−0
@@ -33,6 +33,7 @@ TARGETS += fchmodat2 · 32 unmodified lines
3333 TARGETS += filesystems
3434 TARGETS += filesystems/binderfs
3535 TARGETS += filesystems/epoll
36+TARGETS += filesystems/failfs
3637 TARGETS += filesystems/fat
3738 TARGETS += filesystems/overlayfs
3839 TARGETS += filesystems/statmount
tools/testing/selftests/filesystems/failfs/.gitignore+2−0
@@ -0,0 +1,2 @@
1+# SPDX-License-Identifier: GPL-2.0-only
2+failfs_test
tools/testing/selftests/filesystems/failfs/Makefile+5−0
@@ -0,0 +1,5 @@
1+# SPDX-License-Identifier: GPL-2.0
2+CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
3+TEST_GEN_PROGS := failfs_test
4+
5+include ../../lib.mk
tools/testing/selftests/filesystems/failfs/failfs_test.c+585−0
@@ -0,0 +1,585 @@
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

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t