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======
4failfs
5======
6
7failfs is a kernel-internal filesystem that fails every operation
8reaching it with ``EOPNOTSUPP``. It is the counterpart to nullfs. Where
9nullfs is permanently empty, failfs means "nothing is supported here".
10It cannot be mounted from userspace, nothing can be mounted on top of
11it. It cannot be cloned.
12
13The only way into it is the ``FD_FAILFS_ROOT`` file descriptor sentinel which
14is understood by ``fchdir(2)`` and ``fchroot(2)``.
15
16Semantics
17=========
18
19Every path walk of a component through failfs fails with
20``EOPNOTSUPP`` before that component is parsed, including ``.``.
21
22No path lookup can open the root, not even with ``O_PATH``.
23
24A process with its working directory in failfs fails every
25``AT_FDCWD``-relative lookup. As with any working directory that is
26unreachable from the process root, the ``getcwd(2)`` system call returns
27a path prefixed with ``(unreachable)``.
28
29A process with its root directory in failfs fails every absolute path
30lookup including absolute symlinks and the interpreter of dynamically
31linked binaries. In other words, this fails exec.
32
33Lookups anchored at explicit directory file descriptors keep working. It
34is the ``fs_struct`` equivalent of ``RESOLVE_BENEATH``. The process must
35anchor every lookup at a file descriptor it explicitly holds.
36
37Entering
38========
39
40``fchroot(FD_FAILFS_ROOT, 0)`` requires ``CAP_SYS_CHROOT`` in the
41caller's user namespace, mirroring ``chroot(2)``. Unprivileged callers
42may 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
60Leaving
61=======
62
63Backing out is currently hard, but this is a property of the current
64implementation, not a guaranteed interface, and may be loosened later.
65For now a process that entered failfs counts as chrooted, so it cannot
66create user namespaces to regain ``CAP_SYS_CHROOT``, and ``chroot(2)``
67or ``fchroot(2)`` back out require ``CAP_SYS_CHROOT``. The remaining way
68out today is ``setns(2)`` with a mount namespace file descriptor, which
69requires ``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
71and resets both root and working directory. A process that holds no such
72file descriptor and restricts ``*chdir()``/``*chroot()``/``setns()`` via
73seccomp cannot currently get back out.
Documentation/filesystems/index.rst+1−0
@@ -91,6 +91,7 @@ Documentation for filesystem implementations. · 90 unmodified lines
91 ext391 ext3
92 ext4/index92 ext4/index
93 f2fs93 f2fs
94 failfs
94 gfs2/index95 gfs2/index
95 hfs96 hfs
96 hfsplus97 hfsplus
arch/alpha/kernel/syscalls/syscall.tbl+1−0
@@ -511,3 +511,4 @@ · 510 unmodified lines
511579 common file_setattr sys_file_setattr511579 common file_setattr sys_file_setattr
512580 common listns sys_listns512580 common listns sys_listns
513581 common rseq_slice_yield sys_rseq_slice_yield513581 common rseq_slice_yield sys_rseq_slice_yield
514582 common fchroot sys_fchroot
arch/arm/tools/syscall.tbl+1−0
@@ -486,3 +486,4 @@ · 485 unmodified lines
486469 common file_setattr sys_file_setattr486469 common file_setattr sys_file_setattr
487470 common listns sys_listns487470 common listns sys_listns
488471 common rseq_slice_yield sys_rseq_slice_yield488471 common rseq_slice_yield sys_rseq_slice_yield
489472 common fchroot sys_fchroot
arch/arm64/tools/syscall_32.tbl+1−0
@@ -483,3 +483,4 @@ · 482 unmodified lines
483469 common file_setattr sys_file_setattr483469 common file_setattr sys_file_setattr
484470 common listns sys_listns484470 common listns sys_listns
485471 common rseq_slice_yield sys_rseq_slice_yield485471 common rseq_slice_yield sys_rseq_slice_yield
486472 common fchroot sys_fchroot
arch/m68k/kernel/syscalls/syscall.tbl+1−0
@@ -471,3 +471,4 @@ · 470 unmodified lines
471469 common file_setattr sys_file_setattr471469 common file_setattr sys_file_setattr
472470 common listns sys_listns472470 common listns sys_listns
473471 common rseq_slice_yield sys_rseq_slice_yield473471 common rseq_slice_yield sys_rseq_slice_yield
474472 common fchroot sys_fchroot
arch/microblaze/kernel/syscalls/syscall.tbl+1−0
@@ -477,3 +477,4 @@ · 476 unmodified lines
477469 common file_setattr sys_file_setattr477469 common file_setattr sys_file_setattr
478470 common listns sys_listns478470 common listns sys_listns
479471 common rseq_slice_yield sys_rseq_slice_yield479471 common rseq_slice_yield sys_rseq_slice_yield
480472 common fchroot sys_fchroot
arch/mips/kernel/syscalls/syscall_n32.tbl+1−0
@@ -410,3 +410,4 @@ · 409 unmodified lines
410469 n32 file_setattr sys_file_setattr410469 n32 file_setattr sys_file_setattr
411470 n32 listns sys_listns411470 n32 listns sys_listns
412471 n32 rseq_slice_yield sys_rseq_slice_yield412471 n32 rseq_slice_yield sys_rseq_slice_yield
413472 n32 fchroot sys_fchroot
arch/mips/kernel/syscalls/syscall_n64.tbl+1−0
@@ -386,3 +386,4 @@ · 385 unmodified lines
386469 n64 file_setattr sys_file_setattr386469 n64 file_setattr sys_file_setattr
387470 n64 listns sys_listns387470 n64 listns sys_listns
388471 n64 rseq_slice_yield sys_rseq_slice_yield388471 n64 rseq_slice_yield sys_rseq_slice_yield
389472 n64 fchroot sys_fchroot
arch/mips/kernel/syscalls/syscall_o32.tbl+1−0
@@ -459,3 +459,4 @@ · 458 unmodified lines
459469 o32 file_setattr sys_file_setattr459469 o32 file_setattr sys_file_setattr
460470 o32 listns sys_listns460470 o32 listns sys_listns
461471 o32 rseq_slice_yield sys_rseq_slice_yield461471 o32 rseq_slice_yield sys_rseq_slice_yield
462472 o32 fchroot sys_fchroot
arch/parisc/kernel/syscalls/syscall.tbl+1−0
@@ -470,3 +470,4 @@ · 469 unmodified lines
470469 common file_setattr sys_file_setattr470469 common file_setattr sys_file_setattr
471470 common listns sys_listns471470 common listns sys_listns
472471 common rseq_slice_yield sys_rseq_slice_yield472471 common rseq_slice_yield sys_rseq_slice_yield
473472 common fchroot sys_fchroot
arch/powerpc/kernel/syscalls/syscall.tbl+1−0
@@ -562,3 +562,4 @@ · 561 unmodified lines
562469 common file_setattr sys_file_setattr562469 common file_setattr sys_file_setattr
563470 common listns sys_listns563470 common listns sys_listns
564471 nospu rseq_slice_yield sys_rseq_slice_yield564471 nospu rseq_slice_yield sys_rseq_slice_yield
565472 common fchroot sys_fchroot
arch/s390/kernel/syscalls/syscall.tbl+1−0
@@ -398,3 +398,4 @@ · 397 unmodified lines
398469 common file_setattr sys_file_setattr398469 common file_setattr sys_file_setattr
399470 common listns sys_listns399470 common listns sys_listns
400471 common rseq_slice_yield sys_rseq_slice_yield400471 common rseq_slice_yield sys_rseq_slice_yield
401472 common fchroot sys_fchroot
arch/sh/kernel/syscalls/syscall.tbl+1−0
@@ -475,3 +475,4 @@ · 474 unmodified lines
475469 common file_setattr sys_file_setattr475469 common file_setattr sys_file_setattr
476470 common listns sys_listns476470 common listns sys_listns
477471 common rseq_slice_yield sys_rseq_slice_yield477471 common rseq_slice_yield sys_rseq_slice_yield
478472 common fchroot sys_fchroot
arch/sparc/kernel/syscalls/syscall.tbl+1−0
@@ -517,3 +517,4 @@ · 516 unmodified lines
517469 common file_setattr sys_file_setattr517469 common file_setattr sys_file_setattr
518470 common listns sys_listns518470 common listns sys_listns
519471 common rseq_slice_yield sys_rseq_slice_yield519471 common rseq_slice_yield sys_rseq_slice_yield
520472 common fchroot sys_fchroot
arch/x86/entry/syscalls/syscall_32.tbl+1−0
@@ -477,3 +477,4 @@ · 476 unmodified lines
477469 i386 file_setattr sys_file_setattr477469 i386 file_setattr sys_file_setattr
478470 i386 listns sys_listns478470 i386 listns sys_listns
479471 i386 rseq_slice_yield sys_rseq_slice_yield479471 i386 rseq_slice_yield sys_rseq_slice_yield
480472 i386 fchroot sys_fchroot
arch/x86/entry/syscalls/syscall_64.tbl+1−0
@@ -396,6 +396,7 @@ · 395 unmodified lines
396469 common file_setattr sys_file_setattr396469 common file_setattr sys_file_setattr
397470 common listns sys_listns397470 common listns sys_listns
398471 common rseq_slice_yield sys_rseq_slice_yield398471 common rseq_slice_yield sys_rseq_slice_yield
399472 common fchroot sys_fchroot
399 400
400#401#
401# Due to a historical design error, certain syscalls are numbered differently402# 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
442469 common file_setattr sys_file_setattr442469 common file_setattr sys_file_setattr
443470 common listns sys_listns443470 common listns sys_listns
444471 common rseq_slice_yield sys_rseq_slice_yield444471 common rseq_slice_yield sys_rseq_slice_yield
445472 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
16 stack.o fs_struct.o statfs.o fs_pin.o nsfs.o \16 stack.o fs_struct.o statfs.o fs_pin.o nsfs.o \
17 fs_dirent.o fs_context.o fs_parser.o fsopen.o init.o \17 fs_dirent.o fs_context.o fs_parser.o fsopen.o init.o \
18 kernel_read_file.o mnt_idmapping.o remap_range.o pidfs.o \18 kernel_read_file.o mnt_idmapping.o remap_range.o pidfs.o \
19 file_attr.o fserror.o nullfs.o19 file_attr.o fserror.o nullfs.o failfs.o
20 20
21obj-$(CONFIG_BUFFER_HEAD) += buffer.o mpage.o21obj-$(CONFIG_BUFFER_HEAD) += buffer.o mpage.o
22obj-$(CONFIG_PROC_FS) += proc_namespace.o22obj-$(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
279 * and instead have d_path return the mounted path.279 * and instead have d_path return the mounted path.
280 */280 */
281 if (path->dentry->d_op && path->dentry->d_op->d_dname &&281 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)))
283 return path->dentry->d_op->d_dname(path->dentry, buf, buflen);284 return path->dentry->d_op->d_dname(path->dentry, buf, buflen);
284 285
285 rcu_read_lock();286 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
12static struct path failfs_root_path = {};
13
14void failfs_get_root(struct path *path)
15{
16 *path = failfs_root_path;
17 path_get(path);
18}
19
20bool failfs_mnt(const struct vfsmount *mnt)
21{
22 return mnt->mnt_sb == failfs_root_path.mnt->mnt_sb;
23}
24
25static int failfs_permission(struct mnt_idmap *idmap, struct inode *inode,
26 int mask)
27{
28 return -EOPNOTSUPP;
29}
30
31static 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
38static 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
45static const struct inode_operations failfs_dir_inode_operations = {
46 .permission = failfs_permission,
47 .lookup = failfs_lookup,
48 .getattr = failfs_getattr,
49};
50
51static const struct file_operations failfs_dir_operations = {};
52
53static 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
66static char *failfs_dname(struct dentry *dentry, char *buffer, int buflen)
67{
68 return dynamic_dname(buffer, buflen, "failfs:/");
69}
70
71static const struct dentry_operations failfs_dentry_operations = {
72 .d_dname = failfs_dname,
73 .d_weak_revalidate = failfs_d_weak_revalidate,
74};
75
76static int failfs_statfs(struct dentry *dentry, struct kstatfs *buf)
77{
78 return -EOPNOTSUPP;
79}
80
81static const struct super_operations failfs_super_operations = {
82 .statfs = failfs_statfs,
83};
84
85static 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
121static int failfs_get_tree(struct fs_context *fc)
122{
123 return get_tree_single(fc, failfs_fill_super);
124}
125
126static const struct fs_context_operations failfs_context_ops = {
127 .get_tree = failfs_get_tree,
128};
129
130static 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
139int 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
149static 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
155void __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
362 struct iattr *attr);362 struct iattr *attr);
363void pidfs_get_root(struct path *path);363void pidfs_get_root(struct path *path);
364void nsfs_get_root(struct path *path);364void nsfs_get_root(struct path *path);
365void failfs_get_root(struct path *path);
366void __init failfs_init(void);
367bool failfs_mnt(const struct vfsmount *mnt);
368int failfs_current_chdir(void);
fs/namespace.c+1−0
@@ -6274,6 +6274,7 @@ void __init mnt_init(void) · 6273 unmodified lines
6274 shmem_init();6274 shmem_init();
6275 init_rootfs();6275 init_rootfs();
6276 init_mount_tree();6276 init_mount_tree();
6277 failfs_init();
6277}6278}
6278 6279
6279void put_mnt_ns(struct mnt_namespace *ns)6280void put_mnt_ns(struct mnt_namespace *ns)
fs/open.c+50−1
@@ -570,9 +570,12 @@ retry: · 569 unmodified lines
570 570
571SYSCALL_DEFINE1(fchdir, unsigned int, fd)571SYSCALL_DEFINE1(fchdir, unsigned int, fd)
572{572{
573 CLASS(fd_raw, f)(fd);
574 int error;573 int error;
575 574
575 if ((int)fd == FD_FAILFS_ROOT)
576 return failfs_current_chdir();
577
578 CLASS(fd_raw, f)(fd);
576 if (fd_empty(f))579 if (fd_empty(f))
577 return -EBADF;580 return -EBADF;
578 581
@@ -615,6 +618,52 @@ dput_and_out: · 36 unmodified lines
615 return error;618 return error;
616}619}
617 620
621SYSCALL_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
618int chmod_common(const struct path *path, umode_t mode)667int chmod_common(const struct path *path, umode_t mode)
619{668{
620 struct inode *inode = path->dentry->d_inode;669 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
457asmlinkage long sys_chdir(const char __user *filename);457asmlinkage long sys_chdir(const char __user *filename);
458asmlinkage long sys_fchdir(unsigned int fd);458asmlinkage long sys_fchdir(unsigned int fd);
459asmlinkage long sys_chroot(const char __user *filename);459asmlinkage long sys_chroot(const char __user *filename);
460asmlinkage long sys_fchroot(int fd, unsigned int flags);
460asmlinkage long sys_fchmod(unsigned int fd, umode_t mode);461asmlinkage long sys_fchmod(unsigned int fd, umode_t mode);
461asmlinkage long sys_fchmodat(int dfd, const char __user *filename,462asmlinkage long sys_fchmodat(int dfd, const char __user *filename,
462 umode_t mode);463 umode_t mode);
include/uapi/asm-generic/unistd.h+5−1
@@ -863,8 +863,12 @@ __SYSCALL(__NR_listns, sys_listns) · 862 unmodified lines
863#define __NR_rseq_slice_yield 471863#define __NR_rseq_slice_yield 471
864__SYSCALL(__NR_rseq_slice_yield, sys_rseq_slice_yield)864__SYSCALL(__NR_rseq_slice_yield, sys_rseq_slice_yield)
865 865
866/* fs/open.c */
867#define __NR_fchroot 472
868__SYSCALL(__NR_fchroot, sys_fchroot)
869
866#undef __NR_syscalls870#undef __NR_syscalls
867#define __NR_syscalls 472871#define __NR_syscalls 473
868 872
869/*873/*
870 * 32 bit systems traditionally used different874 * 32 bit systems traditionally used different
include/uapi/linux/fcntl.h+1−0
@@ -124,6 +124,7 @@ struct delegation { · 123 unmodified lines
124 124
125#define FD_PIDFS_ROOT -10002 /* Root of the pidfs filesystem */125#define FD_PIDFS_ROOT -10002 /* Root of the pidfs filesystem */
126#define FD_NSFS_ROOT -10003 /* Root of the nsfs filesystem */126#define FD_NSFS_ROOT -10003 /* Root of the nsfs filesystem */
127#define FD_FAILFS_ROOT -10004 /* Root of the failfs filesystem */
127#define FD_INVALID -10009 /* Invalid file descriptor: -10000 - EBADF = -10009 */128#define FD_INVALID -10009 /* Invalid file descriptor: -10000 - EBADF = -10009 */
128 129
129/* Generic flags for the *at(2) family of syscalls. */130/* Generic flags for the *at(2) family of syscalls. */
include/uapi/linux/magic.h+1−0
@@ -105,5 +105,6 @@ · 104 unmodified lines
105#define PID_FS_MAGIC 0x50494446 /* "PIDF" */105#define PID_FS_MAGIC 0x50494446 /* "PIDF" */
106#define GUEST_MEMFD_MAGIC 0x474d454d /* "GMEM" */106#define GUEST_MEMFD_MAGIC 0x474d454d /* "GMEM" */
107#define NULL_FS_MAGIC 0x4E554C4C /* "NULL" */107#define NULL_FS_MAGIC 0x4E554C4C /* "NULL" */
108#define FAIL_FS_MAGIC 0x4641494C /* "FAIL" */
108 109
109#endif /* __LINUX_MAGIC_H__ */110#endif /* __LINUX_MAGIC_H__ */
scripts/syscall.tbl+1−0
@@ -412,3 +412,4 @@ · 411 unmodified lines
412469 common file_setattr sys_file_setattr412469 common file_setattr sys_file_setattr
413470 common listns sys_listns413470 common listns sys_listns
414471 common rseq_slice_yield sys_rseq_slice_yield414471 common rseq_slice_yield sys_rseq_slice_yield
415472 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
863#define __NR_rseq_slice_yield 471863#define __NR_rseq_slice_yield 471
864__SYSCALL(__NR_rseq_slice_yield, sys_rseq_slice_yield)864__SYSCALL(__NR_rseq_slice_yield, sys_rseq_slice_yield)
865 865
866/* fs/open.c */
867#define __NR_fchroot 472
868__SYSCALL(__NR_fchroot, sys_fchroot)
869
866#undef __NR_syscalls870#undef __NR_syscalls
867#define __NR_syscalls 472871#define __NR_syscalls 473
868 872
869/*873/*
870 * 32 bit systems traditionally used different874 * 32 bit systems traditionally used different
tools/perf/arch/arm/entry/syscalls/syscall.tbl+1−0
@@ -486,3 +486,4 @@ · 485 unmodified lines
486469 common file_setattr sys_file_setattr486469 common file_setattr sys_file_setattr
487470 common listns sys_listns487470 common listns sys_listns
488471 common rseq_slice_yield sys_rseq_slice_yield488471 common rseq_slice_yield sys_rseq_slice_yield
489472 common fchroot sys_fchroot
tools/perf/arch/mips/entry/syscalls/syscall_n64.tbl+1−0
@@ -386,3 +386,4 @@ · 385 unmodified lines
386469 n64 file_setattr sys_file_setattr386469 n64 file_setattr sys_file_setattr
387470 n64 listns sys_listns387470 n64 listns sys_listns
388471 n64 rseq_slice_yield sys_rseq_slice_yield388471 n64 rseq_slice_yield sys_rseq_slice_yield
389472 n64 fchroot sys_fchroot
tools/perf/arch/powerpc/entry/syscalls/syscall.tbl+1−0
@@ -562,3 +562,4 @@ · 561 unmodified lines
562469 common file_setattr sys_file_setattr562469 common file_setattr sys_file_setattr
563470 common listns sys_listns563470 common listns sys_listns
564471 nospu rseq_slice_yield sys_rseq_slice_yield564471 nospu rseq_slice_yield sys_rseq_slice_yield
565472 common fchroot sys_fchroot
tools/perf/arch/s390/entry/syscalls/syscall.tbl+1−0
@@ -398,3 +398,4 @@ · 397 unmodified lines
398469 common file_setattr sys_file_setattr398469 common file_setattr sys_file_setattr
399470 common listns sys_listns399470 common listns sys_listns
400471 common rseq_slice_yield sys_rseq_slice_yield400471 common rseq_slice_yield sys_rseq_slice_yield
401472 common fchroot sys_fchroot
tools/perf/arch/sh/entry/syscalls/syscall.tbl+1−0
@@ -475,3 +475,4 @@ · 474 unmodified lines
475469 common file_setattr sys_file_setattr475469 common file_setattr sys_file_setattr
476470 common listns sys_listns476470 common listns sys_listns
477471 common rseq_slice_yield sys_rseq_slice_yield477471 common rseq_slice_yield sys_rseq_slice_yield
478472 common fchroot sys_fchroot
tools/perf/arch/sparc/entry/syscalls/syscall.tbl+1−0
@@ -517,3 +517,4 @@ · 516 unmodified lines
517469 common file_setattr sys_file_setattr517469 common file_setattr sys_file_setattr
518470 common listns sys_listns518470 common listns sys_listns
519471 common rseq_slice_yield sys_rseq_slice_yield519471 common rseq_slice_yield sys_rseq_slice_yield
520472 common fchroot sys_fchroot
tools/perf/arch/x86/entry/syscalls/syscall_32.tbl+1−0
@@ -477,3 +477,4 @@ · 476 unmodified lines
477469 i386 file_setattr sys_file_setattr477469 i386 file_setattr sys_file_setattr
478470 i386 listns sys_listns478470 i386 listns sys_listns
479471 i386 rseq_slice_yield sys_rseq_slice_yield479471 i386 rseq_slice_yield sys_rseq_slice_yield
480472 i386 fchroot sys_fchroot
tools/perf/arch/x86/entry/syscalls/syscall_64.tbl+1−0
@@ -396,6 +396,7 @@ · 395 unmodified lines
396469 common file_setattr sys_file_setattr396469 common file_setattr sys_file_setattr
397470 common listns sys_listns397470 common listns sys_listns
398471 common rseq_slice_yield sys_rseq_slice_yield398471 common rseq_slice_yield sys_rseq_slice_yield
399472 common fchroot sys_fchroot
399 400
400#401#
401# Due to a historical design error, certain syscalls are numbered differently402# 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
442469 common file_setattr sys_file_setattr442469 common file_setattr sys_file_setattr
443470 common listns sys_listns443470 common listns sys_listns
444471 common rseq_slice_yield sys_rseq_slice_yield444471 common rseq_slice_yield sys_rseq_slice_yield
445472 common fchroot sys_fchroot
tools/scripts/syscall.tbl+1−0
@@ -412,3 +412,4 @@ · 411 unmodified lines
412469 common file_setattr sys_file_setattr412469 common file_setattr sys_file_setattr
413470 common listns sys_listns413470 common listns sys_listns
414471 common rseq_slice_yield sys_rseq_slice_yield414471 common rseq_slice_yield sys_rseq_slice_yield
415472 common fchroot sys_fchroot
tools/testing/selftests/Makefile+1−0
@@ -33,6 +33,7 @@ TARGETS += fchmodat2 · 32 unmodified lines
33TARGETS += filesystems33TARGETS += filesystems
34TARGETS += filesystems/binderfs34TARGETS += filesystems/binderfs
35TARGETS += filesystems/epoll35TARGETS += filesystems/epoll
36TARGETS += filesystems/failfs
36TARGETS += filesystems/fat37TARGETS += filesystems/fat
37TARGETS += filesystems/overlayfs38TARGETS += filesystems/overlayfs
38TARGETS += filesystems/statmount39TARGETS += filesystems/statmount
tools/testing/selftests/filesystems/failfs/.gitignore+2−0
@@ -0,0 +1,2 @@
1# SPDX-License-Identifier: GPL-2.0-only
2failfs_test
tools/testing/selftests/filesystems/failfs/Makefile+5−0
@@ -0,0 +1,5 @@
1# SPDX-License-Identifier: GPL-2.0
2CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
3TEST_GEN_PROGS := failfs_test
4
5include ../../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
47static 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 */
56static long sys_getcwd(char *buf, size_t size)
57{
58 return syscall(__NR_getcwd, buf, size);
59}
60
61static 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. */
67static 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? */
80static 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
106TEST(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
142TEST(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
152TEST(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
166TEST(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
178TEST(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
189TEST(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
203TEST(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
235TEST(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
334TEST(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
372TEST(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
400TEST(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
425TEST(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
452TEST(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
478TEST(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
503TEST(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
528TEST(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
563TEST(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
585TEST_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