| Seq | Timestamp | Type | Name | Duration |
|---|---|---|---|---|
| 0/0 | 2026/07/25 19:05 | flow | patch-triage |
0mArgs:null Results: {
"EnableConfigs": null,
"FocusSymbols": [
"sys_fchroot",
"sys_fchdir",
"failfs_current_chdir",
"failfs_get_root",
"failfs_mnt"
],
"Reasoning": "The patch introduces a new kernel-internal filesystem `failfs` and a new system call `fchroot`. It also modifies `fchdir` to accept a new magic file descriptor `FD_FAILFS_ROOT`. These are core VFS changes that are reachable from userspace and should be fuzzed to ensure they don't introduce regressions or vulnerabilities (e.g., escaping chroot, reference leaks, or unexpected behavior in path resolution).",
"WorthFuzzing": true
} |
| 1/1 | 2026/07/25 19:05 | action | read-patch-diff |
0mArgs:null Results: {
"PatchDiff": "commit 1ca4a303b094b6c8b42c3770278b93a84b17cd09\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate: Sat Jul 25 19:05:34 2026 +0000\n\n syz-cluster: applied patch under review\n\ndiff --git a/Documentation/filesystems/failfs.rst b/Documentation/filesystems/failfs.rst\nnew file mode 100644\nindex 00000000000000..21ff2db7941df2\n--- /dev/null\n+++ b/Documentation/filesystems/failfs.rst\n@@ -0,0 +1,73 @@\n+.. SPDX-License-Identifier: GPL-2.0\n+\n+======\n+failfs\n+======\n+\n+failfs is a kernel-internal filesystem that fails every operation\n+reaching it with ``EOPNOTSUPP``. It is the counterpart to nullfs. Where\n+nullfs is permanently empty, failfs means \"nothing is supported here\".\n+It cannot be mounted from userspace, nothing can be mounted on top of\n+it. It cannot be cloned.\n+\n+The only way into it is the ``FD_FAILFS_ROOT`` file descriptor sentinel which\n+is understood by ``fchdir(2)`` and ``fchroot(2)``.\n+\n+Semantics\n+=========\n+\n+Every path walk of a component through failfs fails with\n+``EOPNOTSUPP`` before that component is parsed, including ``.``.\n+\n+No path lookup can open the root, not even with ``O_PATH``.\n+\n+A process with its working directory in failfs fails every\n+``AT_FDCWD``-relative lookup. As with any working directory that is\n+unreachable from the process root, the ``getcwd(2)`` system call returns\n+a path prefixed with ``(unreachable)``.\n+\n+A process with its root directory in failfs fails every absolute path\n+lookup including absolute symlinks and the interpreter of dynamically\n+linked binaries. In other words, this fails exec.\n+\n+Lookups anchored at explicit directory file descriptors keep working. It\n+is the ``fs_struct`` equivalent of ``RESOLVE_BENEATH``. The process must\n+anchor every lookup at a file descriptor it explicitly holds.\n+\n+Entering\n+========\n+\n+``fchroot(FD_FAILFS_ROOT, 0)`` requires ``CAP_SYS_CHROOT`` in the\n+caller's user namespace, mirroring ``chroot(2)``. Unprivileged callers\n+may enter if all of the following hold:\n+\n+* ``no_new_privs`` is set: setuid binaries on regular mounts remain\n+ reachable via inherited directory file descriptors and executing them\n+ with an unusable root directory is the classic confused deputy.\n+\n+* The caller is not already chrooted: the root directory is what\n+ confines ``..`` resolution and the failfs root can never be reached by\n+ walking up a real mount tree, so moving the root of a chrooted task to\n+ failfs would allow it to escape its chroot via ``openat(fd, \"..\")``.\n+\n+* The caller does not share its ``fs_struct``: ``no_new_privs`` is\n+ checked on the calling thread, but the root lives in the ``fs_struct``.\n+ A ``CLONE_FS`` sibling without ``no_new_privs`` could otherwise execute\n+ a setuid binary with the failfs root, so entry requires ``fs-\u003eusers ==\n+ 1``, the same restriction ``setns(2)`` applies for the mount and user\n+ namespaces.\n+\n+Leaving\n+=======\n+\n+Backing out is currently hard, but this is a property of the current\n+implementation, not a guaranteed interface, and may be loosened later.\n+For now a process that entered failfs counts as chrooted, so it cannot\n+create user namespaces to regain ``CAP_SYS_CHROOT``, and ``chroot(2)``\n+or ``fchroot(2)`` back out require ``CAP_SYS_CHROOT``. The remaining way\n+out today is ``setns(2)`` with a mount namespace file descriptor, which\n+requires ``CAP_SYS_ADMIN`` over the target mount namespace as well as\n+``CAP_SYS_CHROOT`` and ``CAP_SYS_ADMIN`` in the caller's user namespace\n+and resets both root and working directory. A process that holds no such\n+file descriptor and restricts ``*chdir()``/``*chroot()``/``setns()`` via\n+seccomp cannot currently get back out.\ndiff --git a/Documentation/filesystems/index.rst b/Documentation/filesystems/index.rst\nindex 1f71cf1595476c..734a45e516675c 100644\n--- a/Documentation/filesystems/index.rst\n+++ b/Documentation/filesystems/index.rst\n@@ -91,6 +91,7 @@ Documentation for filesystem implementations.\n ext3\n ext4/index\n f2fs\n+ failfs\n gfs2/index\n hfs\n hfsplus\ndiff --git a/arch/alpha/kernel/syscalls/syscall.tbl b/arch/alpha/kernel/syscalls/syscall.tbl\nindex f31b7afffc3459..52e3538cc7df41 100644\n--- a/arch/alpha/kernel/syscalls/syscall.tbl\n+++ b/arch/alpha/kernel/syscalls/syscall.tbl\n@@ -511,3 +511,4 @@\n 579\tcommon\tfile_setattr\t\t\tsys_file_setattr\n 580\tcommon\tlistns\t\t\t\tsys_listns\n 581\tcommon\trseq_slice_yield\t\tsys_rseq_slice_yield\n+582\tcommon\tfchroot\t\t\t\tsys_fchroot\ndiff --git a/arch/arm/tools/syscall.tbl b/arch/arm/tools/syscall.tbl\nindex 94351e22bfcf76..55717ed32c27e6 100644\n--- a/arch/arm/tools/syscall.tbl\n+++ b/arch/arm/tools/syscall.tbl\n@@ -486,3 +486,4 @@\n 469\tcommon\tfile_setattr\t\t\tsys_file_setattr\n 470\tcommon\tlistns\t\t\t\tsys_listns\n 471\tcommon\trseq_slice_yield\t\tsys_rseq_slice_yield\n+472\tcommon\tfchroot\t\t\t\tsys_fchroot\ndiff --git a/arch/arm64/tools/syscall_32.tbl b/arch/arm64/tools/syscall_32.tbl\nindex 62d93d88e0fef8..df2d1d82fb3ccf 100644\n--- a/arch/arm64/tools/syscall_32.tbl\n+++ b/arch/arm64/tools/syscall_32.tbl\n@@ -483,3 +483,4 @@\n 469\tcommon\tfile_setattr\t\t\tsys_file_setattr\n 470\tcommon\tlistns\t\t\t\tsys_listns\n 471\tcommon\trseq_slice_yield\t\tsys_rseq_slice_yield\n+472\tcommon\tfchroot\t\t\t\tsys_fchroot\ndiff --git a/arch/m68k/kernel/syscalls/syscall.tbl b/arch/m68k/kernel/syscalls/syscall.tbl\nindex 2489342571014c..ba7a4d8903d044 100644\n--- a/arch/m68k/kernel/syscalls/syscall.tbl\n+++ b/arch/m68k/kernel/syscalls/syscall.tbl\n@@ -471,3 +471,4 @@\n 469\tcommon\tfile_setattr\t\t\tsys_file_setattr\n 470\tcommon\tlistns\t\t\t\tsys_listns\n 471\tcommon\trseq_slice_yield\t\tsys_rseq_slice_yield\n+472\tcommon\tfchroot\t\t\t\tsys_fchroot\ndiff --git a/arch/microblaze/kernel/syscalls/syscall.tbl b/arch/microblaze/kernel/syscalls/syscall.tbl\nindex 223d2630362724..c55a5c96f49b82 100644\n--- a/arch/microblaze/kernel/syscalls/syscall.tbl\n+++ b/arch/microblaze/kernel/syscalls/syscall.tbl\n@@ -477,3 +477,4 @@\n 469\tcommon\tfile_setattr\t\t\tsys_file_setattr\n 470\tcommon\tlistns\t\t\t\tsys_listns\n 471\tcommon\trseq_slice_yield\t\tsys_rseq_slice_yield\n+472\tcommon\tfchroot\t\t\t\tsys_fchroot\ndiff --git a/arch/mips/kernel/syscalls/syscall_n32.tbl b/arch/mips/kernel/syscalls/syscall_n32.tbl\nindex 7430714e2b8f86..9ae88e4eac61ee 100644\n--- a/arch/mips/kernel/syscalls/syscall_n32.tbl\n+++ b/arch/mips/kernel/syscalls/syscall_n32.tbl\n@@ -410,3 +410,4 @@\n 469\tn32\tfile_setattr\t\t\tsys_file_setattr\n 470\tn32\tlistns\t\t\t\tsys_listns\n 471\tn32\trseq_slice_yield\t\tsys_rseq_slice_yield\n+472\tn32\tfchroot\t\t\t\tsys_fchroot\ndiff --git a/arch/mips/kernel/syscalls/syscall_n64.tbl b/arch/mips/kernel/syscalls/syscall_n64.tbl\nindex 630aab9e542592..83dc93a0712f12 100644\n--- a/arch/mips/kernel/syscalls/syscall_n64.tbl\n+++ b/arch/mips/kernel/syscalls/syscall_n64.tbl\n@@ -386,3 +386,4 @@\n 469\tn64\tfile_setattr\t\t\tsys_file_setattr\n 470\tn64\tlistns\t\t\t\tsys_listns\n 471\tn64\trseq_slice_yield\t\tsys_rseq_slice_yield\n+472\tn64\tfchroot\t\t\t\tsys_fchroot\ndiff --git a/arch/mips/kernel/syscalls/syscall_o32.tbl b/arch/mips/kernel/syscalls/syscall_o32.tbl\nindex 128653112284b2..9c62429c9b7b4e 100644\n--- a/arch/mips/kernel/syscalls/syscall_o32.tbl\n+++ b/arch/mips/kernel/syscalls/syscall_o32.tbl\n@@ -459,3 +459,4 @@\n 469\to32\tfile_setattr\t\t\tsys_file_setattr\n 470\to32\tlistns\t\t\t\tsys_listns\n 471\to32\trseq_slice_yield\t\tsys_rseq_slice_yield\n+472\to32\tfchroot\t\t\t\tsys_fchroot\ndiff --git a/arch/parisc/kernel/syscalls/syscall.tbl b/arch/parisc/kernel/syscalls/syscall.tbl\nindex c6331dad94613b..88adc4016cce01 100644\n--- a/arch/parisc/kernel/syscalls/syscall.tbl\n+++ b/arch/parisc/kernel/syscalls/syscall.tbl\n@@ -470,3 +470,4 @@\n 469\tcommon\tfile_setattr\t\t\tsys_file_setattr\n 470\tcommon\tlistns\t\t\t\tsys_listns\n 471\tcommon\trseq_slice_yield\t\tsys_rseq_slice_yield\n+472\tcommon\tfchroot\t\t\t\tsys_fchroot\ndiff --git a/arch/powerpc/kernel/syscalls/syscall.tbl b/arch/powerpc/kernel/syscalls/syscall.tbl\nindex 4fcc7c58a105dc..cfbb70039ff007 100644\n--- a/arch/powerpc/kernel/syscalls/syscall.tbl\n+++ b/arch/powerpc/kernel/syscalls/syscall.tbl\n@@ -562,3 +562,4 @@\n 469\tcommon\tfile_setattr\t\t\tsys_file_setattr\n 470\tcommon\tlistns\t\t\t\tsys_listns\n 471\tnospu\trseq_slice_yield\t\tsys_rseq_slice_yield\n+472\tcommon\tfchroot\t\t\t\tsys_fchroot\ndiff --git a/arch/s390/kernel/syscalls/syscall.tbl b/arch/s390/kernel/syscalls/syscall.tbl\nindex 09a7ef04d9791a..1b45e68a217bb0 100644\n--- a/arch/s390/kernel/syscalls/syscall.tbl\n+++ b/arch/s390/kernel/syscalls/syscall.tbl\n@@ -398,3 +398,4 @@\n 469\tcommon\tfile_setattr\t\t\tsys_file_setattr\n 470\tcommon\tlistns\t\t\t\tsys_listns\n 471\tcommon\trseq_slice_yield\t\tsys_rseq_slice_yield\n+472\tcommon\tfchroot\t\t\t\tsys_fchroot\ndiff --git a/arch/sh/kernel/syscalls/syscall.tbl b/arch/sh/kernel/syscalls/syscall.tbl\nindex 70b315cbe710c2..ace068dff0de03 100644\n--- a/arch/sh/kernel/syscalls/syscall.tbl\n+++ b/arch/sh/kernel/syscalls/syscall.tbl\n@@ -475,3 +475,4 @@\n 469\tcommon\tfile_setattr\t\t\tsys_file_setattr\n 470\tcommon\tlistns\t\t\t\tsys_listns\n 471\tcommon\trseq_slice_yield\t\tsys_rseq_slice_yield\n+472\tcommon\tfchroot\t\t\t\tsys_fchroot\ndiff --git a/arch/sparc/kernel/syscalls/syscall.tbl b/arch/sparc/kernel/syscalls/syscall.tbl\nindex 7e71bf7fcd14f7..5b9fe0e8140f26 100644\n--- a/arch/sparc/kernel/syscalls/syscall.tbl\n+++ b/arch/sparc/kernel/syscalls/syscall.tbl\n@@ -517,3 +517,4 @@\n 469\tcommon\tfile_setattr\t\t\tsys_file_setattr\n 470\tcommon\tlistns\t\t\t\tsys_listns\n 471\tcommon\trseq_slice_yield\t\tsys_rseq_slice_yield\n+472\tcommon\tfchroot\t\t\t\tsys_fchroot\ndiff --git a/arch/x86/entry/syscalls/syscall_32.tbl b/arch/x86/entry/syscalls/syscall_32.tbl\nindex f832ebd2d79b00..2c172ef48dfd33 100644\n--- a/arch/x86/entry/syscalls/syscall_32.tbl\n+++ b/arch/x86/entry/syscalls/syscall_32.tbl\n@@ -477,3 +477,4 @@\n 469\ti386\tfile_setattr\t\tsys_file_setattr\n 470\ti386\tlistns\t\t\tsys_listns\n 471\ti386\trseq_slice_yield\tsys_rseq_slice_yield\n+472\ti386\tfchroot\t\t\tsys_fchroot\ndiff --git a/arch/x86/entry/syscalls/syscall_64.tbl b/arch/x86/entry/syscalls/syscall_64.tbl\nindex 524155d655da15..d5b6045b0090cc 100644\n--- a/arch/x86/entry/syscalls/syscall_64.tbl\n+++ b/arch/x86/entry/syscalls/syscall_64.tbl\n@@ -396,6 +396,7 @@\n 469\tcommon\tfile_setattr\t\tsys_file_setattr\n 470\tcommon\tlistns\t\t\tsys_listns\n 471\tcommon\trseq_slice_yield\tsys_rseq_slice_yield\n+472\tcommon\tfchroot\t\t\tsys_fchroot\n \n #\n # Due to a historical design error, certain syscalls are numbered differently\ndiff --git a/arch/xtensa/kernel/syscalls/syscall.tbl b/arch/xtensa/kernel/syscalls/syscall.tbl\nindex a9bca4e484decf..d354bb231796c2 100644\n--- a/arch/xtensa/kernel/syscalls/syscall.tbl\n+++ b/arch/xtensa/kernel/syscalls/syscall.tbl\n@@ -442,3 +442,4 @@\n 469\tcommon\tfile_setattr\t\t\tsys_file_setattr\n 470\tcommon\tlistns\t\t\t\tsys_listns\n 471\tcommon\trseq_slice_yield\t\tsys_rseq_slice_yield\n+472\tcommon\tfchroot\t\t\t\tsys_fchroot\ndiff --git a/fs/Makefile b/fs/Makefile\nindex 89a8a9d207d1c5..73b6cab7738ea4 100644\n--- a/fs/Makefile\n+++ b/fs/Makefile\n@@ -16,7 +16,7 @@ obj-y :=\topen.o read_write.o file_table.o super.o \\\n \t\tstack.o fs_struct.o statfs.o fs_pin.o nsfs.o \\\n \t\tfs_dirent.o fs_context.o fs_parser.o fsopen.o init.o \\\n \t\tkernel_read_file.o mnt_idmapping.o remap_range.o pidfs.o \\\n-\t\tfile_attr.o fserror.o nullfs.o\n+\t\tfile_attr.o fserror.o nullfs.o failfs.o\n \n obj-$(CONFIG_BUFFER_HEAD)\t+= buffer.o mpage.o\n obj-$(CONFIG_PROC_FS)\t\t+= proc_namespace.o\ndiff --git a/fs/d_path.c b/fs/d_path.c\nindex a48957c0971ef6..c25309006d5d6d 100644\n--- a/fs/d_path.c\n+++ b/fs/d_path.c\n@@ -279,7 +279,8 @@ char *d_path(const struct path *path, char *buf, int buflen)\n \t * and instead have d_path return the mounted path.\n \t */\n \tif (path-\u003edentry-\u003ed_op \u0026\u0026 path-\u003edentry-\u003ed_op-\u003ed_dname \u0026\u0026\n-\t (!IS_ROOT(path-\u003edentry) || path-\u003edentry != path-\u003emnt-\u003emnt_root))\n+\t (!IS_ROOT(path-\u003edentry) || path-\u003edentry != path-\u003emnt-\u003emnt_root ||\n+\t failfs_mnt(path-\u003emnt)))\n \t\treturn path-\u003edentry-\u003ed_op-\u003ed_dname(path-\u003edentry, buf, buflen);\n \n \trcu_read_lock();\ndiff --git a/fs/failfs.c b/fs/failfs.c\nnew file mode 100644\nindex 00000000000000..66a36da3d236d6\n--- /dev/null\n+++ b/fs/failfs.c\n@@ -0,0 +1,166 @@\n+// SPDX-License-Identifier: GPL-2.0-only\n+/* Copyright (c) 2026 Christian Brauner \u003cbrauner@kernel.org\u003e */\n+#include \u003clinux/fs.h\u003e\n+#include \u003clinux/fs/super_types.h\u003e\n+#include \u003clinux/fs_context.h\u003e\n+#include \u003clinux/fs_struct.h\u003e\n+#include \u003clinux/magic.h\u003e\n+#include \u003clinux/mount.h\u003e\n+\n+#include \"internal.h\"\n+\n+static struct path failfs_root_path = {};\n+\n+void failfs_get_root(struct path *path)\n+{\n+\t*path = failfs_root_path;\n+\tpath_get(path);\n+}\n+\n+bool failfs_mnt(const struct vfsmount *mnt)\n+{\n+\treturn mnt-\u003emnt_sb == failfs_root_path.mnt-\u003emnt_sb;\n+}\n+\n+static int failfs_permission(struct mnt_idmap *idmap, struct inode *inode,\n+\t\t\t int mask)\n+{\n+\treturn -EOPNOTSUPP;\n+}\n+\n+static struct dentry *failfs_lookup(struct inode *dir, struct dentry *dentry,\n+\t\t\t\t unsigned int flags)\n+{\n+\t/* Unreachable: -\u003epermission() already failed the walk. */\n+\treturn ERR_PTR(-EOPNOTSUPP);\n+}\n+\n+static int failfs_getattr(struct mnt_idmap *idmap, const struct path *path,\n+\t\t\t struct kstat *stat, u32 request_mask,\n+\t\t\t unsigned int query_flags)\n+{\n+\treturn -EOPNOTSUPP;\n+}\n+\n+static const struct inode_operations failfs_dir_inode_operations = {\n+\t.permission\t= failfs_permission,\n+\t.lookup\t\t= failfs_lookup,\n+\t.getattr\t= failfs_getattr,\n+};\n+\n+static const struct file_operations failfs_dir_operations = {};\n+\n+static int failfs_d_weak_revalidate(struct dentry *dentry, unsigned int flags)\n+{\n+\t/*\n+\t * The root is only ever reached as a path-walk terminal by jumping\n+\t * to it: as \"/\" when it is the caller's root, or through a\n+\t * /proc/\u003cpid\u003e/{root,cwd} magic link. -\u003epermission() already fails\n+\t * every walk of a component, but a jump lands on the root without\n+\t * one. Refuse here too so the root cannot be pinned by an O_PATH\n+\t * open or encoded into a file handle.\n+\t */\n+\treturn -EOPNOTSUPP;\n+}\n+\n+static char *failfs_dname(struct dentry *dentry, char *buffer, int buflen)\n+{\n+\treturn dynamic_dname(buffer, buflen, \"failfs:/\");\n+}\n+\n+static const struct dentry_operations failfs_dentry_operations = {\n+\t.d_dname\t\t= failfs_dname,\n+\t.d_weak_revalidate\t= failfs_d_weak_revalidate,\n+};\n+\n+static int failfs_statfs(struct dentry *dentry, struct kstatfs *buf)\n+{\n+\treturn -EOPNOTSUPP;\n+}\n+\n+static const struct super_operations failfs_super_operations = {\n+\t.statfs\t= failfs_statfs,\n+};\n+\n+static int failfs_fill_super(struct super_block *s, struct fs_context *fc)\n+{\n+\tstruct inode *inode;\n+\n+\ts-\u003es_maxbytes\t\t= MAX_LFS_FILESIZE;\n+\ts-\u003es_blocksize\t\t= PAGE_SIZE;\n+\ts-\u003es_blocksize_bits\t= PAGE_SHIFT;\n+\ts-\u003es_magic\t\t= FAIL_FS_MAGIC;\n+\ts-\u003es_op\t\t\t= \u0026failfs_super_operations;\n+\ts-\u003es_export_op\t\t= NULL;\n+\ts-\u003es_xattr\t\t= NULL;\n+\ts-\u003es_time_gran\t\t= 1;\n+\ts-\u003es_d_flags\t\t= 0;\n+\n+\tinode = new_inode(s);\n+\tif (!inode)\n+\t\treturn -ENOMEM;\n+\n+\t/* failfs supports no operations... */\n+\tinode-\u003ei_mode\t= S_IFDIR;\n+\tset_nlink(inode, 2);\n+\tinode-\u003ei_op\t= \u0026failfs_dir_inode_operations;\n+\tinode-\u003ei_fop\t= \u0026failfs_dir_operations;\n+\tsimple_inode_init_ts(inode);\n+\tinode-\u003ei_ino\t= 1;\n+\t/* ... and is immutable. */\n+\tinode-\u003ei_flags |= S_IMMUTABLE;\n+\n+\tset_default_d_op(s, \u0026failfs_dentry_operations);\n+\ts-\u003es_root = d_make_root(inode);\n+\tif (!s-\u003es_root)\n+\t\treturn -ENOMEM;\n+\n+\treturn 0;\n+}\n+\n+static int failfs_get_tree(struct fs_context *fc)\n+{\n+\treturn get_tree_single(fc, failfs_fill_super);\n+}\n+\n+static const struct fs_context_operations failfs_context_ops = {\n+\t.get_tree\t= failfs_get_tree,\n+};\n+\n+static int failfs_init_fs_context(struct fs_context *fc)\n+{\n+\tfc-\u003eops\t\t= \u0026failfs_context_ops;\n+\tfc-\u003eglobal\t= true;\n+\tfc-\u003esb_flags\t|= SB_NOUSER;\n+\tfc-\u003es_iflags\t|= SB_I_NOEXEC | SB_I_NODEV;\n+\treturn 0;\n+}\n+\n+int failfs_current_chdir(void)\n+{\n+\tstruct path path;\n+\n+\tfailfs_get_root(\u0026path);\n+\tset_fs_pwd(current-\u003efs, \u0026path);\n+\tpath_put(\u0026path);\n+\treturn 0;\n+}\n+\n+static struct file_system_type failfs_fs_type = {\n+\t.name\t\t\t= \"failfs\",\n+\t.init_fs_context\t= failfs_init_fs_context,\n+\t.kill_sb\t\t= kill_anon_super,\n+};\n+\n+void __init failfs_init(void)\n+{\n+\tstruct vfsmount *mnt;\n+\n+\t/* A single instance that is member of no mount namespace. */\n+\tmnt = kern_mount(\u0026failfs_fs_type);\n+\tif (IS_ERR(mnt))\n+\t\tpanic(\"VFS: Failed to create failfs\");\n+\n+\tfailfs_root_path.mnt\t= mnt;\n+\tfailfs_root_path.dentry\t= mnt-\u003emnt_root;\n+}\ndiff --git a/fs/internal.h b/fs/internal.h\nindex 355d93f9220861..67aa0444351b1a 100644\n--- a/fs/internal.h\n+++ b/fs/internal.h\n@@ -362,3 +362,7 @@ int anon_inode_setattr(struct mnt_idmap *idmap, struct dentry *dentry,\n \t\t struct iattr *attr);\n void pidfs_get_root(struct path *path);\n void nsfs_get_root(struct path *path);\n+void failfs_get_root(struct path *path);\n+void __init failfs_init(void);\n+bool failfs_mnt(const struct vfsmount *mnt);\n+int failfs_current_chdir(void);\ndiff --git a/fs/namespace.c b/fs/namespace.c\nindex 3d5cd5bf3b0537..87c365f2f82ba3 100644\n--- a/fs/namespace.c\n+++ b/fs/namespace.c\n@@ -6274,6 +6274,7 @@ void __init mnt_init(void)\n \tshmem_init();\n \tinit_rootfs();\n \tinit_mount_tree();\n+\tfailfs_init();\n }\n \n void put_mnt_ns(struct mnt_namespace *ns)\ndiff --git a/fs/open.c b/fs/open.c\nindex 408925d7bd0b77..6b1c14e684a93b 100644\n--- a/fs/open.c\n+++ b/fs/open.c\n@@ -570,9 +570,12 @@ SYSCALL_DEFINE1(chdir, const char __user *, filename)\n \n SYSCALL_DEFINE1(fchdir, unsigned int, fd)\n {\n-\tCLASS(fd_raw, f)(fd);\n \tint error;\n \n+\tif ((int)fd == FD_FAILFS_ROOT)\n+\t\treturn failfs_current_chdir();\n+\n+\tCLASS(fd_raw, f)(fd);\n \tif (fd_empty(f))\n \t\treturn -EBADF;\n \n@@ -615,6 +618,52 @@ SYSCALL_DEFINE1(chroot, const char __user *, filename)\n \treturn error;\n }\n \n+SYSCALL_DEFINE2(fchroot, int, fd, unsigned int, flags)\n+{\n+\tstruct path path;\n+\tint error;\n+\n+\tif (flags)\n+\t\treturn -EINVAL;\n+\n+\tif (fd == FD_FAILFS_ROOT) {\n+\t\tif (!ns_capable(current_user_ns(), CAP_SYS_CHROOT)) {\n+\t\t\tif (!task_no_new_privs(current))\n+\t\t\t\treturn -EPERM;\n+\t\t\t/* A shared fs_struct lets a sibling exec setuid past the check above. */\n+\t\t\tif (current-\u003efs-\u003eusers != 1)\n+\t\t\t\treturn -EINVAL;\n+\t\t\t/* Moving the root to failfs lifts the old root's \"..\" barrier. */\n+\t\t\tif (current_chrooted())\n+\t\t\t\treturn -EPERM;\n+\t\t}\n+\t\tfailfs_get_root(\u0026path);\n+\t} else {\n+\t\tCLASS(fd_raw, f)(fd);\n+\t\tif (fd_empty(f))\n+\t\t\treturn -EBADF;\n+\n+\t\tif (!d_can_lookup(fd_file(f)-\u003ef_path.dentry))\n+\t\t\treturn -ENOTDIR;\n+\n+\t\terror = file_permission(fd_file(f), MAY_EXEC | MAY_CHDIR);\n+\t\tif (error)\n+\t\t\treturn error;\n+\n+\t\tif (!ns_capable(current_user_ns(), CAP_SYS_CHROOT))\n+\t\t\treturn -EPERM;\n+\n+\t\tpath = fd_file(f)-\u003ef_path;\n+\t\tpath_get(\u0026path);\n+\t}\n+\n+\terror = security_path_chroot(\u0026path);\n+\tif (!error)\n+\t\tset_fs_root(current-\u003efs, \u0026path);\n+\tpath_put(\u0026path);\n+\treturn error;\n+}\n+\n int chmod_common(const struct path *path, umode_t mode)\n {\n \tstruct inode *inode = path-\u003edentry-\u003ed_inode;\ndiff --git a/include/linux/syscalls.h b/include/linux/syscalls.h\nindex 874d9067a43b50..8413b624ad4782 100644\n--- a/include/linux/syscalls.h\n+++ b/include/linux/syscalls.h\n@@ -457,6 +457,7 @@ asmlinkage long sys_faccessat2(int dfd, const char __user *filename, int mode,\n asmlinkage long sys_chdir(const char __user *filename);\n asmlinkage long sys_fchdir(unsigned int fd);\n asmlinkage long sys_chroot(const char __user *filename);\n+asmlinkage long sys_fchroot(int fd, unsigned int flags);\n asmlinkage long sys_fchmod(unsigned int fd, umode_t mode);\n asmlinkage long sys_fchmodat(int dfd, const char __user *filename,\n \t\t\t umode_t mode);\ndiff --git a/include/uapi/asm-generic/unistd.h b/include/uapi/asm-generic/unistd.h\nindex a627acc8fb5fea..5b7e77a7c736ee 100644\n--- a/include/uapi/asm-generic/unistd.h\n+++ b/include/uapi/asm-generic/unistd.h\n@@ -863,8 +863,12 @@ __SYSCALL(__NR_listns, sys_listns)\n #define __NR_rseq_slice_yield 471\n __SYSCALL(__NR_rseq_slice_yield, sys_rseq_slice_yield)\n \n+/* fs/open.c */\n+#define __NR_fchroot 472\n+__SYSCALL(__NR_fchroot, sys_fchroot)\n+\n #undef __NR_syscalls\n-#define __NR_syscalls 472\n+#define __NR_syscalls 473\n \n /*\n * 32 bit systems traditionally used different\ndiff --git a/include/uapi/linux/fcntl.h b/include/uapi/linux/fcntl.h\nindex aadfbf6e0cb3a0..e43e3de3e9ee44 100644\n--- a/include/uapi/linux/fcntl.h\n+++ b/include/uapi/linux/fcntl.h\n@@ -124,6 +124,7 @@ struct delegation {\n \n #define FD_PIDFS_ROOT\t\t\t-10002 /* Root of the pidfs filesystem */\n #define FD_NSFS_ROOT\t\t\t-10003 /* Root of the nsfs filesystem */\n+#define FD_FAILFS_ROOT\t\t\t-10004 /* Root of the failfs filesystem */\n #define FD_INVALID\t\t\t-10009 /* Invalid file descriptor: -10000 - EBADF = -10009 */\n \n /* Generic flags for the *at(2) family of syscalls. */\ndiff --git a/include/uapi/linux/magic.h b/include/uapi/linux/magic.h\nindex 4f2da935a76cca..fd5f0e95648ee0 100644\n--- a/include/uapi/linux/magic.h\n+++ b/include/uapi/linux/magic.h\n@@ -105,5 +105,6 @@\n #define PID_FS_MAGIC\t\t0x50494446\t/* \"PIDF\" */\n #define GUEST_MEMFD_MAGIC\t0x474d454d\t/* \"GMEM\" */\n #define NULL_FS_MAGIC\t\t0x4E554C4C\t/* \"NULL\" */\n+#define FAIL_FS_MAGIC\t\t0x4641494C\t/* \"FAIL\" */\n \n #endif /* __LINUX_MAGIC_H__ */\ndiff --git a/scripts/syscall.tbl b/scripts/syscall.tbl\nindex 7a42b32b657767..0ab5316051202e 100644\n--- a/scripts/syscall.tbl\n+++ b/scripts/syscall.tbl\n@@ -412,3 +412,4 @@\n 469\tcommon\tfile_setattr\t\t\tsys_file_setattr\n 470\tcommon\tlistns\t\t\t\tsys_listns\n 471\tcommon\trseq_slice_yield\t\tsys_rseq_slice_yield\n+472\tcommon\tfchroot\t\t\t\tsys_fchroot\ndiff --git a/tools/include/uapi/asm-generic/unistd.h b/tools/include/uapi/asm-generic/unistd.h\nindex a627acc8fb5fea..5b7e77a7c736ee 100644\n--- a/tools/include/uapi/asm-generic/unistd.h\n+++ b/tools/include/uapi/asm-generic/unistd.h\n@@ -863,8 +863,12 @@ __SYSCALL(__NR_listns, sys_listns)\n #define __NR_rseq_slice_yield 471\n __SYSCALL(__NR_rseq_slice_yield, sys_rseq_slice_yield)\n \n+/* fs/open.c */\n+#define __NR_fchroot 472\n+__SYSCALL(__NR_fchroot, sys_fchroot)\n+\n #undef __NR_syscalls\n-#define __NR_syscalls 472\n+#define __NR_syscalls 473\n \n /*\n * 32 bit systems traditionally used different\ndiff --git a/tools/perf/arch/x86/entry/syscalls/syscall_64.tbl b/tools/perf/arch/x86/entry/syscalls/syscall_64.tbl\nindex 524155d655da15..d5b6045b0090cc 100644\n--- a/tools/perf/arch/x86/entry/syscalls/syscall_64.tbl\n+++ b/tools/perf/arch/x86/entry/syscalls/syscall_64.tbl\n@@ -396,6 +396,7 @@\n 469\tcommon\tfile_setattr\t\tsys_file_setattr\n 470\tcommon\tlistns\t\t\tsys_listns\n 471\tcommon\trseq_slice_yield\tsys_rseq_slice_yield\n+472\tcommon\tfchroot\t\t\tsys_fchroot\n \n #\n # Due to a historical design error, certain syscalls are numbered differently\ndiff --git a/tools/scripts/syscall.tbl b/tools/scripts/syscall.tbl\nindex 7a42b32b657767..0ab5316051202e 100644\n--- a/tools/scripts/syscall.tbl\n+++ b/tools/scripts/syscall.tbl\n@@ -412,3 +412,4 @@\n 469\tcommon\tfile_setattr\t\t\tsys_file_setattr\n 470\tcommon\tlistns\t\t\t\tsys_listns\n 471\tcommon\trseq_slice_yield\t\tsys_rseq_slice_yield\n+472\tcommon\tfchroot\t\t\t\tsys_fchroot\ndiff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile\nindex 8d4db2241cc2a2..f87167bcf58282 100644\n--- a/tools/testing/selftests/Makefile\n+++ b/tools/testing/selftests/Makefile\n@@ -33,6 +33,7 @@ TARGETS += fchmodat2\n TARGETS += filesystems\n TARGETS += filesystems/binderfs\n TARGETS += filesystems/epoll\n+TARGETS += filesystems/failfs\n TARGETS += filesystems/fat\n TARGETS += filesystems/overlayfs\n TARGETS += filesystems/statmount\ndiff --git a/tools/testing/selftests/filesystems/failfs/.gitignore b/tools/testing/selftests/filesystems/failfs/.gitignore\nnew file mode 100644\nindex 00000000000000..cd3b5d884d7ea9\n--- /dev/null\n+++ b/tools/testing/selftests/filesystems/failfs/.gitignore\n@@ -0,0 +1,2 @@\n+# SPDX-License-Identifier: GPL-2.0-only\n+failfs_test\ndiff --git a/tools/testing/selftests/filesystems/failfs/Makefile b/tools/testing/selftests/filesystems/failfs/Makefile\nnew file mode 100644\nindex 00000000000000..3c5d98b4fe725e\n--- /dev/null\n+++ b/tools/testing/selftests/filesystems/failfs/Makefile\n@@ -0,0 +1,5 @@\n+# SPDX-License-Identifier: GPL-2.0\n+CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)\n+TEST_GEN_PROGS := failfs_test\n+\n+include ../../lib.mk\ndiff --git a/tools/testing/selftests/filesystems/failfs/failfs_test.c b/tools/testing/selftests/filesystems/failfs/failfs_test.c\nnew file mode 100644\nindex 00000000000000..cc461dc483a997\n--- /dev/null\n+++ b/tools/testing/selftests/filesystems/failfs/failfs_test.c\n@@ -0,0 +1,581 @@\n+// SPDX-License-Identifier: GPL-2.0\n+#define _GNU_SOURCE\n+#include \u003cerrno.h\u003e\n+#include \u003cfcntl.h\u003e\n+#include \u003climits.h\u003e\n+#include \u003clink.h\u003e\n+#include \u003csched.h\u003e\n+#include \u003csignal.h\u003e\n+#include \u003cstdio.h\u003e\n+#include \u003cstdlib.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003csys/mount.h\u003e\n+#include \u003csys/prctl.h\u003e\n+#include \u003csys/stat.h\u003e\n+#include \u003csys/syscall.h\u003e\n+#include \u003csys/types.h\u003e\n+#include \u003csys/vfs.h\u003e\n+#include \u003csys/wait.h\u003e\n+#include \u003cunistd.h\u003e\n+\n+#include \"../../kselftest_harness.h\"\n+\n+#ifndef __NR_fchroot\n+#define __NR_fchroot 472\n+#endif\n+\n+#ifndef FD_PIDFS_ROOT\n+#define FD_PIDFS_ROOT -10002\n+#endif\n+\n+#ifndef FD_NSFS_ROOT\n+#define FD_NSFS_ROOT -10003\n+#endif\n+\n+#ifndef FD_FAILFS_ROOT\n+#define FD_FAILFS_ROOT -10004\n+#endif\n+\n+#define NOBODY_UID 65534\n+\n+/* Child sentinel exit code: the exec was blocked as expected. */\n+#define FAILFS_EXEC_BLOCKED 99\n+\n+/* Stack for the CLONE_FS helper in fchroot_sentinel_shared_fs_struct. */\n+#define FAILFS_CLONE_STACK (64 * 1024)\n+\n+static int sys_fchroot(int fd, unsigned int flags)\n+{\n+\treturn syscall(__NR_fchroot, fd, flags);\n+}\n+\n+/*\n+ * Raw syscall: glibc's getcwd() rejects the kernel's \"(unreachable)\"\n+ * result and falls back to a generic implementation.\n+ */\n+static long sys_getcwd(char *buf, size_t size)\n+{\n+\treturn syscall(__NR_getcwd, buf, size);\n+}\n+\n+static int drop_to_nobody(void)\n+{\n+\treturn setresuid(NOBODY_UID, NOBODY_UID, NOBODY_UID);\n+}\n+\n+/* Parked CLONE_FS child; dies with its parent so it never leaks. */\n+static int failfs_park(void *arg)\n+{\n+\t(void)arg;\n+\tprctl(PR_SET_PDEATHSIG, SIGKILL);\n+\tpause();\n+\treturn 0;\n+}\n+\n+/* Is fd a dynamically linked ELF with an absolute PT_INTERP interpreter? */\n+static int elf_has_absolute_interp(int fd)\n+{\n+\tElfW(Ehdr) ehdr;\n+\tElfW(Phdr) phdr;\n+\tchar interp;\n+\tint i;\n+\n+\tif (pread(fd, \u0026ehdr, sizeof(ehdr), 0) != sizeof(ehdr))\n+\t\treturn 0;\n+\tif (memcmp(ehdr.e_ident, ELFMAG, SELFMAG) != 0)\n+\t\treturn 0;\n+\n+\tfor (i = 0; i \u003c ehdr.e_phnum; i++) {\n+\t\tif (pread(fd, \u0026phdr, sizeof(phdr),\n+\t\t\t ehdr.e_phoff + i * sizeof(phdr)) != sizeof(phdr))\n+\t\t\treturn 0;\n+\t\tif (phdr.p_type != PT_INTERP)\n+\t\t\tcontinue;\n+\t\tif (pread(fd, \u0026interp, 1, phdr.p_offset) != 1)\n+\t\t\treturn 0;\n+\t\treturn interp == '/';\n+\t}\n+\n+\treturn 0;\n+}\n+\n+TEST(fchdir_sentinel)\n+{\n+\tchar buf[PATH_MAX];\n+\tint fd;\n+\n+\tASSERT_EQ(fchdir(FD_FAILFS_ROOT), 0);\n+\n+\t/* The working directory is unreachable from the process root. */\n+\tASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0);\n+\tASSERT_EQ(strncmp(buf, \"(unreachable)\", 13), 0);\n+\n+\t/* Every AT_FDCWD-relative lookup fails. */\n+\tASSERT_EQ(openat(AT_FDCWD, \"foo\", O_RDONLY), -1);\n+\tASSERT_EQ(errno, EOPNOTSUPP);\n+\tASSERT_EQ(openat(AT_FDCWD, \".\", O_RDONLY), -1);\n+\tASSERT_EQ(errno, EOPNOTSUPP);\n+\tASSERT_EQ(openat(AT_FDCWD, \"..\", O_RDONLY), -1);\n+\tASSERT_EQ(errno, EOPNOTSUPP);\n+\tASSERT_EQ(openat(AT_FDCWD, \"foo\", O_WRONLY | O_CREAT, 0600), -1);\n+\tASSERT_EQ(errno, EOPNOTSUPP);\n+\n+\t/* The cwd cannot be pinned by following /proc/self/cwd into it. */\n+\tASSERT_EQ(open(\"/proc/self/cwd\", O_PATH), -1);\n+\tASSERT_EQ(errno, EOPNOTSUPP);\n+\n+\t/* The root is untouched so absolute lookups keep working... */\n+\tfd = open(\"/\", O_RDONLY | O_DIRECTORY);\n+\tASSERT_GE(fd, 0);\n+\tASSERT_EQ(close(fd), 0);\n+\n+\t/* ... and the working directory can be recovered. */\n+\tASSERT_EQ(chdir(\"/\"), 0);\n+\tASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0);\n+\tASSERT_EQ(strcmp(buf, \"/\"), 0);\n+}\n+\n+TEST(fchdir_rejects_other_sentinels)\n+{\n+\tASSERT_EQ(fchdir(FD_PIDFS_ROOT), -1);\n+\tASSERT_EQ(errno, EBADF);\n+\tASSERT_EQ(fchdir(FD_NSFS_ROOT), -1);\n+\tASSERT_EQ(errno, EBADF);\n+\tASSERT_EQ(fchdir(-10009), -1);\n+\tASSERT_EQ(errno, EBADF);\n+}\n+\n+TEST(fchroot_flags)\n+{\n+\tint fd;\n+\n+\tASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 1), -1);\n+\tASSERT_EQ(errno, EINVAL);\n+\n+\tfd = open(\"/\", O_PATH | O_DIRECTORY);\n+\tASSERT_GE(fd, 0);\n+\tASSERT_EQ(sys_fchroot(fd, 1), -1);\n+\tASSERT_EQ(errno, EINVAL);\n+\tASSERT_EQ(close(fd), 0);\n+}\n+\n+TEST(fchroot_bad_fd)\n+{\n+\tASSERT_EQ(sys_fchroot(-1, 0), -1);\n+\tASSERT_EQ(errno, EBADF);\n+\n+\t/* Only FD_FAILFS_ROOT is a valid sentinel. */\n+\tASSERT_EQ(sys_fchroot(FD_PIDFS_ROOT, 0), -1);\n+\tASSERT_EQ(errno, EBADF);\n+\tASSERT_EQ(sys_fchroot(FD_NSFS_ROOT, 0), -1);\n+\tASSERT_EQ(errno, EBADF);\n+}\n+\n+TEST(fchroot_notdir)\n+{\n+\tint fd;\n+\n+\tfd = open(\"/proc/self/status\", O_RDONLY);\n+\tASSERT_GE(fd, 0);\n+\tASSERT_EQ(sys_fchroot(fd, 0), -1);\n+\tASSERT_EQ(errno, ENOTDIR);\n+\tASSERT_EQ(close(fd), 0);\n+}\n+\n+TEST(fchroot_realfd_requires_cap)\n+{\n+\tint fd;\n+\n+\tif (geteuid() == 0)\n+\t\tASSERT_EQ(drop_to_nobody(), 0);\n+\n+\tfd = open(\"/\", O_PATH | O_DIRECTORY);\n+\tASSERT_GE(fd, 0);\n+\tASSERT_EQ(sys_fchroot(fd, 0), -1);\n+\tASSERT_EQ(errno, EPERM);\n+\tASSERT_EQ(close(fd), 0);\n+}\n+\n+TEST(fchroot_realfd)\n+{\n+\tchar template[] = \"/tmp/failfs_test.XXXXXX\";\n+\tchar path[PATH_MAX];\n+\tstruct stat st;\n+\tint tmpfd, dfd, fd;\n+\n+\tif (geteuid() != 0)\n+\t\tSKIP(return, \"fchroot() with a regular fd requires CAP_SYS_CHROOT\");\n+\n+\ttmpfd = open(\"/tmp\", O_PATH | O_DIRECTORY);\n+\tASSERT_GE(tmpfd, 0);\n+\n+\tASSERT_NE(mkdtemp(template), NULL);\n+\tsnprintf(path, sizeof(path), \"%s/canary\", template);\n+\tfd = open(path, O_WRONLY | O_CREAT, 0600);\n+\tASSERT_GE(fd, 0);\n+\tASSERT_EQ(close(fd), 0);\n+\n+\tdfd = open(template, O_PATH | O_DIRECTORY);\n+\tASSERT_GE(dfd, 0);\n+\tASSERT_EQ(sys_fchroot(dfd, 0), 0);\n+\tASSERT_EQ(close(dfd), 0);\n+\n+\tASSERT_EQ(stat(\"/canary\", \u0026st), 0);\n+\n+\t/* Best-effort cleanup: dirfd-anchored I/O works with the new root. */\n+\tsnprintf(path, sizeof(path), \"%s/canary\", template + strlen(\"/tmp/\"));\n+\tunlinkat(tmpfd, path, 0);\n+\tunlinkat(tmpfd, template + strlen(\"/tmp/\"), AT_REMOVEDIR);\n+}\n+\n+TEST(fchroot_sentinel)\n+{\n+\tchar template[] = \"/tmp/failfs_test.XXXXXX\";\n+\tstruct stat realroot, st;\n+\tstruct statfs sfs;\n+\tchar buf[PATH_MAX];\n+\tint procfd, tmpfd, dfd, fd;\n+\tstruct {\n+\t\tstruct file_handle handle;\n+\t\tunsigned char f_handle[MAX_HANDLE_SZ];\n+\t} fh;\n+\tint mntid;\n+\tssize_t ret;\n+\n+\tif (geteuid() != 0)\n+\t\tSKIP(return, \"privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT\");\n+\n+\tASSERT_EQ(stat(\"/\", \u0026realroot), 0);\n+\tprocfd = open(\"/proc\", O_PATH | O_DIRECTORY);\n+\tASSERT_GE(procfd, 0);\n+\ttmpfd = open(\"/tmp\", O_PATH | O_DIRECTORY);\n+\tASSERT_GE(tmpfd, 0);\n+\tASSERT_NE(mkdtemp(template), NULL);\n+\tdfd = open(template, O_RDONLY | O_DIRECTORY);\n+\tASSERT_GE(dfd, 0);\n+\n+\tASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);\n+\n+\t/* Absolute lookups fail. */\n+\tASSERT_EQ(open(\"/etc/passwd\", O_RDONLY), -1);\n+\tASSERT_EQ(errno, EOPNOTSUPP);\n+\tASSERT_EQ(mkdir(\"/foo\", 0700), -1);\n+\tASSERT_EQ(errno, EOPNOTSUPP);\n+\n+\t/*\n+\t * The root cannot be referenced at all - not even an O_PATH open,\n+\t * which skips -\u003epermission(), because it lands on the root as a\n+\t * jumped walk terminal that -\u003ed_weak_revalidate() refuses.\n+\t */\n+\tASSERT_EQ(open(\"/\", O_RDONLY | O_DIRECTORY), -1);\n+\tASSERT_EQ(errno, EOPNOTSUPP);\n+\tASSERT_EQ(open(\"/\", O_PATH), -1);\n+\tASSERT_EQ(errno, EOPNOTSUPP);\n+\tASSERT_EQ(statfs(\"/\", \u0026sfs), -1);\n+\tASSERT_EQ(errno, EOPNOTSUPP);\n+\n+\t/*\n+\t * It cannot be pinned by following /proc/self/root into it either\n+\t * (only the root is in failfs here, so self/cwd is still real).\n+\t */\n+\tASSERT_EQ(openat(procfd, \"self/root\", O_PATH), -1);\n+\tASSERT_EQ(errno, EOPNOTSUPP);\n+\n+\t/* Nor encoded into a file handle. */\n+\tfh.handle.handle_bytes = MAX_HANDLE_SZ;\n+\tASSERT_EQ(name_to_handle_at(AT_FDCWD, \"/\", \u0026fh.handle, \u0026mntid, 0), -1);\n+\tASSERT_EQ(errno, EOPNOTSUPP);\n+\n+\t/* The working directory is now unreachable from the root. */\n+\tASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0);\n+\tASSERT_EQ(strncmp(buf, \"(unreachable)\", 13), 0);\n+\n+\t/* Lookups anchored at real directories keep working. */\n+\tfd = openat(AT_FDCWD, \".\", O_RDONLY | O_DIRECTORY);\n+\tASSERT_GE(fd, 0);\n+\tASSERT_EQ(close(fd), 0);\n+\tfd = openat(dfd, \"canary\", O_WRONLY | O_CREAT, 0600);\n+\tASSERT_GE(fd, 0);\n+\tASSERT_EQ(write(fd, \"x\", 1), 1);\n+\tASSERT_EQ(close(fd), 0);\n+\tfd = openat(dfd, \"canary\", O_RDONLY);\n+\tASSERT_GE(fd, 0);\n+\tASSERT_EQ(close(fd), 0);\n+\n+\t/* \"..\" walks clamp at the top of the mount tree, not at failfs. */\n+\tfd = openat(AT_FDCWD, \"../../../../../../../../../..\", O_PATH);\n+\tASSERT_GE(fd, 0);\n+\tASSERT_EQ(fstat(fd, \u0026st), 0);\n+\tASSERT_EQ(st.st_dev, realroot.st_dev);\n+\tASSERT_EQ(st.st_ino, realroot.st_ino);\n+\tASSERT_EQ(close(fd), 0);\n+\n+\t/* readlink of the magic link still works: it does not follow. */\n+\tret = readlinkat(procfd, \"self/root\", buf, sizeof(buf) - 1);\n+\tASSERT_GT(ret, 0);\n+\tbuf[ret] = '\\0';\n+\tTH_LOG(\"/proc/self/root points to '%s'\", buf);\n+\t/* d_path() names the failfs root synthetically, never as a real path. */\n+\tASSERT_EQ(strcmp(buf, \"failfs:/\"), 0);\n+\n+\t/* But following it into failfs is refused. */\n+\tASSERT_EQ(fstatat(procfd, \"self/root\", \u0026st, 0), -1);\n+\tASSERT_EQ(errno, EOPNOTSUPP);\n+\n+\t/* Best-effort cleanup via the pre-opened dirfds. */\n+\tunlinkat(dfd, \"canary\", 0);\n+\tunlinkat(tmpfd, template + strlen(\"/tmp/\"), AT_REMOVEDIR);\n+}\n+\n+TEST(fchroot_sentinel_absolute_symlink)\n+{\n+\tchar template[] = \"/tmp/failfs_test.XXXXXX\";\n+\tint tmpfd, dfd, fd;\n+\n+\tif (geteuid() != 0)\n+\t\tSKIP(return, \"privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT\");\n+\n+\ttmpfd = open(\"/tmp\", O_PATH | O_DIRECTORY);\n+\tASSERT_GE(tmpfd, 0);\n+\tASSERT_NE(mkdtemp(template), NULL);\n+\tdfd = open(template, O_RDONLY | O_DIRECTORY);\n+\tASSERT_GE(dfd, 0);\n+\n+\tfd = openat(dfd, \"target\", O_WRONLY | O_CREAT, 0600);\n+\tASSERT_GE(fd, 0);\n+\tASSERT_EQ(close(fd), 0);\n+\tASSERT_EQ(symlinkat(\"target\", dfd, \"rel\"), 0);\n+\tASSERT_EQ(symlinkat(\"/etc\", dfd, \"abs\"), 0);\n+\n+\tASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);\n+\n+\t/* Relative symlinks keep resolving within the dirfd-anchored walk... */\n+\tfd = openat(dfd, \"rel\", O_RDONLY);\n+\tASSERT_GE(fd, 0);\n+\tASSERT_EQ(close(fd), 0);\n+\n+\t/* ... absolute symlinks restart the walk at the failfs root. */\n+\tASSERT_EQ(openat(dfd, \"abs\", O_RDONLY), -1);\n+\tASSERT_EQ(errno, EOPNOTSUPP);\n+\n+\t/* Best-effort cleanup via the pre-opened dirfds. */\n+\tunlinkat(dfd, \"abs\", 0);\n+\tunlinkat(dfd, \"rel\", 0);\n+\tunlinkat(dfd, \"target\", 0);\n+\tunlinkat(tmpfd, template + strlen(\"/tmp/\"), AT_REMOVEDIR);\n+}\n+\n+TEST(fchroot_sentinel_unprivileged)\n+{\n+\tchar buf[PATH_MAX];\n+\n+\tif (geteuid() == 0)\n+\t\tASSERT_EQ(drop_to_nobody(), 0);\n+\n+\t/* Without no_new_privs entering failfs is not allowed... */\n+\tASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), -1);\n+\tASSERT_EQ(errno, EPERM);\n+\n+\t/* ... with no_new_privs set it is allowed. */\n+\tASSERT_EQ(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0);\n+\tASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);\n+\n+\tASSERT_EQ(open(\"/etc/passwd\", O_RDONLY), -1);\n+\tASSERT_EQ(errno, EOPNOTSUPP);\n+\n+\t/* The task counts as chrooted: no user namespaces anymore. */\n+\tASSERT_EQ(unshare(CLONE_NEWUSER), -1);\n+\tASSERT_EQ(errno, EPERM);\n+\n+\t/* With both root and cwd in failfs getcwd() reports \"/\". */\n+\tASSERT_EQ(fchdir(FD_FAILFS_ROOT), 0);\n+\tASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0);\n+\tASSERT_EQ(strcmp(buf, \"/\"), 0);\n+}\n+\n+TEST(fchroot_sentinel_rejected_when_chrooted)\n+{\n+\tchar template[] = \"/tmp/failfs_test.XXXXXX\";\n+\tint tmpfd;\n+\n+\tif (geteuid() != 0)\n+\t\tSKIP(return, \"chroot() requires CAP_SYS_CHROOT\");\n+\n+\ttmpfd = open(\"/tmp\", O_PATH | O_DIRECTORY);\n+\tASSERT_GE(tmpfd, 0);\n+\tASSERT_NE(mkdtemp(template), NULL);\n+\tASSERT_EQ(chroot(template), 0);\n+\tASSERT_EQ(chdir(\"/\"), 0);\n+\n+\t/* Remove the jail while still privileged; sticky /tmp blocks nobody. */\n+\tunlinkat(tmpfd, template + strlen(\"/tmp/\"), AT_REMOVEDIR);\n+\n+\tASSERT_EQ(drop_to_nobody(), 0);\n+\tASSERT_EQ(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0);\n+\n+\t/* An unprivileged chrooted task must not lift its \"..\" barrier. */\n+\tASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), -1);\n+\tASSERT_EQ(errno, EPERM);\n+}\n+\n+TEST(fchroot_sentinel_shared_fs_struct)\n+{\n+\tchar stack[FAILFS_CLONE_STACK];\n+\tpid_t pid;\n+\n+\tif (geteuid() == 0)\n+\t\tASSERT_EQ(drop_to_nobody(), 0);\n+\n+\t/* A CLONE_FS sibling shares the fs_struct: bump fs-\u003eusers to 2. */\n+\tpid = clone(failfs_park, stack + sizeof(stack), CLONE_FS | SIGCHLD,\n+\t\t NULL);\n+\tASSERT_GE(pid, 0);\n+\n+\tASSERT_EQ(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0);\n+\n+\t/*\n+\t * A sibling without no_new_privs could exec a setuid binary with\n+\t * the failfs root, so a shared fs_struct is refused even with\n+\t * no_new_privs set.\n+\t */\n+\tASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), -1);\n+\tASSERT_EQ(errno, EINVAL);\n+\n+\tASSERT_EQ(kill(pid, SIGKILL), 0);\n+\tASSERT_EQ(waitpid(pid, NULL, 0), pid);\n+}\n+\n+TEST(fchroot_sentinel_no_overmount)\n+{\n+\tif (geteuid() != 0)\n+\t\tSKIP(return, \"mounting requires privileges\");\n+\n+\t/*\n+\t * Contain the blast radius: if failfs ever regressed and \"/\"\n+\t * resolved to the real root, the tmpfs mount below must not touch\n+\t * the host. A private mount namespace keeps it local to this child.\n+\t */\n+\tASSERT_EQ(unshare(CLONE_NEWNS), 0);\n+\tASSERT_EQ(mount(NULL, \"/\", NULL, MS_REC | MS_PRIVATE, NULL), 0);\n+\n+\tASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);\n+\n+\t/*\n+\t * Nothing can be mounted on top of the failfs root. It cannot even\n+\t * be named as a mount target: resolving \"/\" is refused before the\n+\t * mount machinery (which, failfs being in no mount namespace, would\n+\t * reject it anyway) is ever reached. open_tree(OPEN_TREE_CLONE) is\n+\t * likewise moot since no fd to the root can be obtained.\n+\t */\n+\tASSERT_EQ(mount(\"none\", \"/\", \"tmpfs\", 0, NULL), -1);\n+\tASSERT_EQ(errno, EOPNOTSUPP);\n+}\n+\n+TEST(fchroot_sentinel_setns_escape)\n+{\n+\tstruct stat realroot, st;\n+\tint nsfd;\n+\n+\tif (geteuid() != 0)\n+\t\tSKIP(return, \"setns() to a mount namespace requires privileges\");\n+\n+\tASSERT_EQ(stat(\"/\", \u0026realroot), 0);\n+\tnsfd = open(\"/proc/self/ns/mnt\", O_RDONLY);\n+\tASSERT_GE(nsfd, 0);\n+\n+\tASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);\n+\tASSERT_EQ(open(\"/etc\", O_PATH), -1);\n+\tASSERT_EQ(errno, EOPNOTSUPP);\n+\n+\t/* A mount namespace fd is the key out: it resets root and cwd. */\n+\tASSERT_EQ(setns(nsfd, CLONE_NEWNS), 0);\n+\tASSERT_EQ(close(nsfd), 0);\n+\n+\tASSERT_EQ(stat(\"/\", \u0026st), 0);\n+\tASSERT_EQ(st.st_dev, realroot.st_dev);\n+\tASSERT_EQ(st.st_ino, realroot.st_ino);\n+}\n+\n+TEST(fchroot_sentinel_exec)\n+{\n+\tpid_t pid;\n+\tint status;\n+\n+\tif (geteuid() != 0)\n+\t\tSKIP(return, \"privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT\");\n+\n+\tASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);\n+\n+\t/*\n+\t * Exec in a child: a wrongly successful exec would replace the test\n+\t * image and its exit code would not match the sentinel below.\n+\t */\n+\tpid = fork();\n+\tASSERT_GE(pid, 0);\n+\tif (pid == 0) {\n+\t\texecl(\"/bin/true\", \"true\", NULL);\n+\t\t_exit(errno == EOPNOTSUPP ? FAILFS_EXEC_BLOCKED : 1);\n+\t}\n+\tASSERT_EQ(waitpid(pid, \u0026status, 0), pid);\n+\tASSERT_TRUE(WIFEXITED(status));\n+\tASSERT_EQ(WEXITSTATUS(status), FAILFS_EXEC_BLOCKED);\n+}\n+\n+TEST(fchroot_sentinel_exec_interpreter)\n+{\n+\tstatic const char * const argv[] = { \"failfs_test\", NULL };\n+\tstatic const char * const envp[] = { NULL };\n+\tpid_t pid;\n+\tint status, exefd;\n+\n+\tif (geteuid() != 0)\n+\t\tSKIP(return, \"privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT\");\n+\n+\t/* Exec ourselves: the one binary guaranteed to be around. */\n+\texefd = open(\"/proc/self/exe\", O_RDONLY);\n+\tASSERT_GE(exefd, 0);\n+\tif (!elf_has_absolute_interp(exefd))\n+\t\tSKIP(return, \"test binary has no absolute PT_INTERP interpreter\");\n+\n+\tASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);\n+\n+\t/*\n+\t * The binary itself needs no path lookup - it is executed by fd -\n+\t * but loading it fails on opening the absolute PT_INTERP\n+\t * interpreter. Run it in a child so a wrongly successful exec does\n+\t * not replace the test image and masquerade as a pass.\n+\t */\n+\tpid = fork();\n+\tASSERT_GE(pid, 0);\n+\tif (pid == 0) {\n+\t\tsyscall(__NR_execveat, exefd, \"\", argv, envp, AT_EMPTY_PATH);\n+\t\t_exit(errno == EOPNOTSUPP ? FAILFS_EXEC_BLOCKED : 1);\n+\t}\n+\tASSERT_EQ(waitpid(pid, \u0026status, 0), pid);\n+\tASSERT_TRUE(WIFEXITED(status));\n+\tASSERT_EQ(WEXITSTATUS(status), FAILFS_EXEC_BLOCKED);\n+}\n+\n+TEST(fchroot_sentinel_inherited)\n+{\n+\tpid_t pid;\n+\tint status;\n+\n+\tif (geteuid() != 0)\n+\t\tSKIP(return, \"privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT\");\n+\n+\tASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);\n+\n+\tpid = fork();\n+\tASSERT_GE(pid, 0);\n+\tif (pid == 0) {\n+\t\tif (open(\"/etc\", O_PATH) != -1 || errno != EOPNOTSUPP)\n+\t\t\t_exit(1);\n+\t\t_exit(0);\n+\t}\n+\tASSERT_EQ(waitpid(pid, \u0026status, 0), pid);\n+\tASSERT_TRUE(WIFEXITED(status));\n+\tASSERT_EQ(WEXITSTATUS(status), 0);\n+}\n+\n+TEST_HARNESS_MAIN\n"
} |
| 2/1 | 2026/07/25 19:05 | agent | patch-evaluator |
0mModel:best-expensive Args: null Results: {
"EnableConfigs": null,
"FocusSymbols": [
"sys_fchroot",
"sys_fchdir",
"failfs_current_chdir",
"failfs_get_root",
"failfs_mnt"
],
"Reasoning": "The patch introduces a new kernel-internal filesystem `failfs` and a new system call `fchroot`. It also modifies `fchdir` to accept a new magic file descriptor `FD_FAILFS_ROOT`. These are core VFS changes that are reachable from userspace and should be fuzzed to ensure they don't introduce regressions or vulnerabilities (e.g., escaping chroot, reference leaks, or unexpected behavior in path resolution).",
"WorthFuzzing": true
}Instruction: You are an expert Linux kernel maintainer. Your job is to review a provided patch series and determine if it makes functional changes to the kernel that should be fuzzed. IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in your workspace. Do NOT rely on your internal knowledge of the kernel. You must actively use your code access tools to examine the actual source code and confirm any assumptions. Return WorthFuzzing=false if the patch only contains: - Modifications to Documentation/, Kconfig files, or code comments. - Purely decorative changes, such as logging (e.g., pr_err, printk) or tracepoints. - Changes to numeric constants or macros that do not functionally alter execution flow. - Code paths that are impossible to reach in virtualized environments like GCE or QEMU, even when utilizing software-emulated hardware (e.g., usb gadget, mac80211_hwsim). - Code in vendor-specific PCIe switch, SmartNIC, or GPU drivers (e.g., mlxsw, pds_core, qed, ionic, amdgpu) that require physical PCIe hardware cards not emulated in standard QEMU. - Driver .remove, .shutdown, or pci_unregister_driver teardown callbacks (e.g., igb_remove) that are executed only during PCI hot-unplug or sysfs driver unbind operations. If it modifies reachable core kernel logic, drivers, or architectures, use your code search tools to verify the code can be executed, then return WorthFuzzing=true. When returning WorthFuzzing=true, you MUST ALSO: 1. Extract any specific kernel functions that should be heavily fuzzed into FocusSymbols. Avoid listing generic hot-path functions to prevent skewed test distributions. Prefer non-static, non-inlined API entrypoint functions over internal static helper functions (which are inlined by the compiler and do not have distinct symbol addresses). 2. Identify any specific CONFIG_ options required to properly test this new/modified feature. Go and look into the Kconfig files and check for ifdefs around the code, do not make assumptions. Also check "depends on" lines in Kconfig to include any non-standard parent subsystem configs needed for Kbuild to compile the code statically into vmlinux. List them in the EnableConfigs output array, and DO NOT add a 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4"). Prefer calling several tools at the same time to save round-trips. Use set-results tool to provide results of the analysis. It must be called exactly once before the final reply. Ignore results of this tool. Prompt: For your convenience, here is the diff of the changes:
commit 1ca4a303b094b6c8b42c3770278b93a84b17cd09
Author: syz-cluster <triage@syzkaller.com>
Date: Sat Jul 25 19:05:34 2026 +0000
syz-cluster: applied patch under review
diff --git a/Documentation/filesystems/failfs.rst b/Documentation/filesystems/failfs.rst
new file mode 100644
index 00000000000000..21ff2db7941df2
--- /dev/null
+++ b/Documentation/filesystems/failfs.rst
@@ -0,0 +1,73 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+======
+failfs
+======
+
+failfs is a kernel-internal filesystem that fails every operation
+reaching it with ``EOPNOTSUPP``. It is the counterpart to nullfs. Where
+nullfs is permanently empty, failfs means "nothing is supported here".
+It cannot be mounted from userspace, nothing can be mounted on top of
+it. It cannot be cloned.
+
+The only way into it is the ``FD_FAILFS_ROOT`` file descriptor sentinel which
+is understood by ``fchdir(2)`` and ``fchroot(2)``.
+
+Semantics
+=========
+
+Every path walk of a component through failfs fails with
+``EOPNOTSUPP`` before that component is parsed, including ``.``.
+
+No path lookup can open the root, not even with ``O_PATH``.
+
+A process with its working directory in failfs fails every
+``AT_FDCWD``-relative lookup. As with any working directory that is
+unreachable from the process root, the ``getcwd(2)`` system call returns
+a path prefixed with ``(unreachable)``.
+
+A process with its root directory in failfs fails every absolute path
+lookup including absolute symlinks and the interpreter of dynamically
+linked binaries. In other words, this fails exec.
+
+Lookups anchored at explicit directory file descriptors keep working. It
+is the ``fs_struct`` equivalent of ``RESOLVE_BENEATH``. The process must
+anchor every lookup at a file descriptor it explicitly holds.
+
+Entering
+========
+
+``fchroot(FD_FAILFS_ROOT, 0)`` requires ``CAP_SYS_CHROOT`` in the
+caller's user namespace, mirroring ``chroot(2)``. Unprivileged callers
+may enter if all of the following hold:
+
+* ``no_new_privs`` is set: setuid binaries on regular mounts remain
+ reachable via inherited directory file descriptors and executing them
+ with an unusable root directory is the classic confused deputy.
+
+* The caller is not already chrooted: the root directory is what
+ confines ``..`` resolution and the failfs root can never be reached by
+ walking up a real mount tree, so moving the root of a chrooted task to
+ failfs would allow it to escape its chroot via ``openat(fd, "..")``.
+
+* The caller does not share its ``fs_struct``: ``no_new_privs`` is
+ checked on the calling thread, but the root lives in the ``fs_struct``.
+ A ``CLONE_FS`` sibling without ``no_new_privs`` could otherwise execute
+ a setuid binary with the failfs root, so entry requires ``fs->users ==
+ 1``, the same restriction ``setns(2)`` applies for the mount and user
+ namespaces.
+
+Leaving
+=======
+
+Backing out is currently hard, but this is a property of the current
+implementation, not a guaranteed interface, and may be loosened later.
+For now a process that entered failfs counts as chrooted, so it cannot
+create user namespaces to regain ``CAP_SYS_CHROOT``, and ``chroot(2)``
+or ``fchroot(2)`` back out require ``CAP_SYS_CHROOT``. The remaining way
+out today is ``setns(2)`` with a mount namespace file descriptor, which
+requires ``CAP_SYS_ADMIN`` over the target mount namespace as well as
+``CAP_SYS_CHROOT`` and ``CAP_SYS_ADMIN`` in the caller's user namespace
+and resets both root and working directory. A process that holds no such
+file descriptor and restricts ``*chdir()``/``*chroot()``/``setns()`` via
+seccomp cannot currently get back out.
diff --git a/Documentation/filesystems/index.rst b/Documentation/filesystems/index.rst
index 1f71cf1595476c..734a45e516675c 100644
--- a/Documentation/filesystems/index.rst
+++ b/Documentation/filesystems/index.rst
@@ -91,6 +91,7 @@ Documentation for filesystem implementations.
ext3
ext4/index
f2fs
+ failfs
gfs2/index
hfs
hfsplus
diff --git a/arch/alpha/kernel/syscalls/syscall.tbl b/arch/alpha/kernel/syscalls/syscall.tbl
index f31b7afffc3459..52e3538cc7df41 100644
--- a/arch/alpha/kernel/syscalls/syscall.tbl
+++ b/arch/alpha/kernel/syscalls/syscall.tbl
@@ -511,3 +511,4 @@
579 common file_setattr sys_file_setattr
580 common listns sys_listns
581 common rseq_slice_yield sys_rseq_slice_yield
+582 common fchroot sys_fchroot
diff --git a/arch/arm/tools/syscall.tbl b/arch/arm/tools/syscall.tbl
index 94351e22bfcf76..55717ed32c27e6 100644
--- a/arch/arm/tools/syscall.tbl
+++ b/arch/arm/tools/syscall.tbl
@@ -486,3 +486,4 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common fchroot sys_fchroot
diff --git a/arch/arm64/tools/syscall_32.tbl b/arch/arm64/tools/syscall_32.tbl
index 62d93d88e0fef8..df2d1d82fb3ccf 100644
--- a/arch/arm64/tools/syscall_32.tbl
+++ b/arch/arm64/tools/syscall_32.tbl
@@ -483,3 +483,4 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common fchroot sys_fchroot
diff --git a/arch/m68k/kernel/syscalls/syscall.tbl b/arch/m68k/kernel/syscalls/syscall.tbl
index 2489342571014c..ba7a4d8903d044 100644
--- a/arch/m68k/kernel/syscalls/syscall.tbl
+++ b/arch/m68k/kernel/syscalls/syscall.tbl
@@ -471,3 +471,4 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common fchroot sys_fchroot
diff --git a/arch/microblaze/kernel/syscalls/syscall.tbl b/arch/microblaze/kernel/syscalls/syscall.tbl
index 223d2630362724..c55a5c96f49b82 100644
--- a/arch/microblaze/kernel/syscalls/syscall.tbl
+++ b/arch/microblaze/kernel/syscalls/syscall.tbl
@@ -477,3 +477,4 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common fchroot sys_fchroot
diff --git a/arch/mips/kernel/syscalls/syscall_n32.tbl b/arch/mips/kernel/syscalls/syscall_n32.tbl
index 7430714e2b8f86..9ae88e4eac61ee 100644
--- a/arch/mips/kernel/syscalls/syscall_n32.tbl
+++ b/arch/mips/kernel/syscalls/syscall_n32.tbl
@@ -410,3 +410,4 @@
469 n32 file_setattr sys_file_setattr
470 n32 listns sys_listns
471 n32 rseq_slice_yield sys_rseq_slice_yield
+472 n32 fchroot sys_fchroot
diff --git a/arch/mips/kernel/syscalls/syscall_n64.tbl b/arch/mips/kernel/syscalls/syscall_n64.tbl
index 630aab9e542592..83dc93a0712f12 100644
--- a/arch/mips/kernel/syscalls/syscall_n64.tbl
+++ b/arch/mips/kernel/syscalls/syscall_n64.tbl
@@ -386,3 +386,4 @@
469 n64 file_setattr sys_file_setattr
470 n64 listns sys_listns
471 n64 rseq_slice_yield sys_rseq_slice_yield
+472 n64 fchroot sys_fchroot
diff --git a/arch/mips/kernel/syscalls/syscall_o32.tbl b/arch/mips/kernel/syscalls/syscall_o32.tbl
index 128653112284b2..9c62429c9b7b4e 100644
--- a/arch/mips/kernel/syscalls/syscall_o32.tbl
+++ b/arch/mips/kernel/syscalls/syscall_o32.tbl
@@ -459,3 +459,4 @@
469 o32 file_setattr sys_file_setattr
470 o32 listns sys_listns
471 o32 rseq_slice_yield sys_rseq_slice_yield
+472 o32 fchroot sys_fchroot
diff --git a/arch/parisc/kernel/syscalls/syscall.tbl b/arch/parisc/kernel/syscalls/syscall.tbl
index c6331dad94613b..88adc4016cce01 100644
--- a/arch/parisc/kernel/syscalls/syscall.tbl
+++ b/arch/parisc/kernel/syscalls/syscall.tbl
@@ -470,3 +470,4 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common fchroot sys_fchroot
diff --git a/arch/powerpc/kernel/syscalls/syscall.tbl b/arch/powerpc/kernel/syscalls/syscall.tbl
index 4fcc7c58a105dc..cfbb70039ff007 100644
--- a/arch/powerpc/kernel/syscalls/syscall.tbl
+++ b/arch/powerpc/kernel/syscalls/syscall.tbl
@@ -562,3 +562,4 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 nospu rseq_slice_yield sys_rseq_slice_yield
+472 common fchroot sys_fchroot
diff --git a/arch/s390/kernel/syscalls/syscall.tbl b/arch/s390/kernel/syscalls/syscall.tbl
index 09a7ef04d9791a..1b45e68a217bb0 100644
--- a/arch/s390/kernel/syscalls/syscall.tbl
+++ b/arch/s390/kernel/syscalls/syscall.tbl
@@ -398,3 +398,4 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common fchroot sys_fchroot
diff --git a/arch/sh/kernel/syscalls/syscall.tbl b/arch/sh/kernel/syscalls/syscall.tbl
index 70b315cbe710c2..ace068dff0de03 100644
--- a/arch/sh/kernel/syscalls/syscall.tbl
+++ b/arch/sh/kernel/syscalls/syscall.tbl
@@ -475,3 +475,4 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common fchroot sys_fchroot
diff --git a/arch/sparc/kernel/syscalls/syscall.tbl b/arch/sparc/kernel/syscalls/syscall.tbl
index 7e71bf7fcd14f7..5b9fe0e8140f26 100644
--- a/arch/sparc/kernel/syscalls/syscall.tbl
+++ b/arch/sparc/kernel/syscalls/syscall.tbl
@@ -517,3 +517,4 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common fchroot sys_fchroot
diff --git a/arch/x86/entry/syscalls/syscall_32.tbl b/arch/x86/entry/syscalls/syscall_32.tbl
index f832ebd2d79b00..2c172ef48dfd33 100644
--- a/arch/x86/entry/syscalls/syscall_32.tbl
+++ b/arch/x86/entry/syscalls/syscall_32.tbl
@@ -477,3 +477,4 @@
469 i386 file_setattr sys_file_setattr
470 i386 listns sys_listns
471 i386 rseq_slice_yield sys_rseq_slice_yield
+472 i386 fchroot sys_fchroot
diff --git a/arch/x86/entry/syscalls/syscall_64.tbl b/arch/x86/entry/syscalls/syscall_64.tbl
index 524155d655da15..d5b6045b0090cc 100644
--- a/arch/x86/entry/syscalls/syscall_64.tbl
+++ b/arch/x86/entry/syscalls/syscall_64.tbl
@@ -396,6 +396,7 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common fchroot sys_fchroot
#
# Due to a historical design error, certain syscalls are numbered differently
diff --git a/arch/xtensa/kernel/syscalls/syscall.tbl b/arch/xtensa/kernel/syscalls/syscall.tbl
index a9bca4e484decf..d354bb231796c2 100644
--- a/arch/xtensa/kernel/syscalls/syscall.tbl
+++ b/arch/xtensa/kernel/syscalls/syscall.tbl
@@ -442,3 +442,4 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common fchroot sys_fchroot
diff --git a/fs/Makefile b/fs/Makefile
index 89a8a9d207d1c5..73b6cab7738ea4 100644
--- a/fs/Makefile
+++ b/fs/Makefile
@@ -16,7 +16,7 @@ obj-y := open.o read_write.o file_table.o super.o \
stack.o fs_struct.o statfs.o fs_pin.o nsfs.o \
fs_dirent.o fs_context.o fs_parser.o fsopen.o init.o \
kernel_read_file.o mnt_idmapping.o remap_range.o pidfs.o \
- file_attr.o fserror.o nullfs.o
+ file_attr.o fserror.o nullfs.o failfs.o
obj-$(CONFIG_BUFFER_HEAD) += buffer.o mpage.o
obj-$(CONFIG_PROC_FS) += proc_namespace.o
diff --git a/fs/d_path.c b/fs/d_path.c
index a48957c0971ef6..c25309006d5d6d 100644
--- a/fs/d_path.c
+++ b/fs/d_path.c
@@ -279,7 +279,8 @@ char *d_path(const struct path *path, char *buf, int buflen)
* and instead have d_path return the mounted path.
*/
if (path->dentry->d_op && path->dentry->d_op->d_dname &&
- (!IS_ROOT(path->dentry) || path->dentry != path->mnt->mnt_root))
+ (!IS_ROOT(path->dentry) || path->dentry != path->mnt->mnt_root ||
+ failfs_mnt(path->mnt)))
return path->dentry->d_op->d_dname(path->dentry, buf, buflen);
rcu_read_lock();
diff --git a/fs/failfs.c b/fs/failfs.c
new file mode 100644
index 00000000000000..66a36da3d236d6
--- /dev/null
+++ b/fs/failfs.c
@@ -0,0 +1,166 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Copyright (c) 2026 Christian Brauner <brauner@kernel.org> */
+#include <linux/fs.h>
+#include <linux/fs/super_types.h>
+#include <linux/fs_context.h>
+#include <linux/fs_struct.h>
+#include <linux/magic.h>
+#include <linux/mount.h>
+
+#include "internal.h"
+
+static struct path failfs_root_path = {};
+
+void failfs_get_root(struct path *path)
+{
+ *path = failfs_root_path;
+ path_get(path);
+}
+
+bool failfs_mnt(const struct vfsmount *mnt)
+{
+ return mnt->mnt_sb == failfs_root_path.mnt->mnt_sb;
+}
+
+static int failfs_permission(struct mnt_idmap *idmap, struct inode *inode,
+ int mask)
+{
+ return -EOPNOTSUPP;
+}
+
+static struct dentry *failfs_lookup(struct inode *dir, struct dentry *dentry,
+ unsigned int flags)
+{
+ /* Unreachable: ->permission() already failed the walk. */
+ return ERR_PTR(-EOPNOTSUPP);
+}
+
+static int failfs_getattr(struct mnt_idmap *idmap, const struct path *path,
+ struct kstat *stat, u32 request_mask,
+ unsigned int query_flags)
+{
+ return -EOPNOTSUPP;
+}
+
+static const struct inode_operations failfs_dir_inode_operations = {
+ .permission = failfs_permission,
+ .lookup = failfs_lookup,
+ .getattr = failfs_getattr,
+};
+
+static const struct file_operations failfs_dir_operations = {};
+
+static int failfs_d_weak_revalidate(struct dentry *dentry, unsigned int flags)
+{
+ /*
+ * The root is only ever reached as a path-walk terminal by jumping
+ * to it: as "/" when it is the caller's root, or through a
+ * /proc/<pid>/{root,cwd} magic link. ->permission() already fails
+ * every walk of a component, but a jump lands on the root without
+ * one. Refuse here too so the root cannot be pinned by an O_PATH
+ * open or encoded into a file handle.
+ */
+ return -EOPNOTSUPP;
+}
+
+static char *failfs_dname(struct dentry *dentry, char *buffer, int buflen)
+{
+ return dynamic_dname(buffer, buflen, "failfs:/");
+}
+
+static const struct dentry_operations failfs_dentry_operations = {
+ .d_dname = failfs_dname,
+ .d_weak_revalidate = failfs_d_weak_revalidate,
+};
+
+static int failfs_statfs(struct dentry *dentry, struct kstatfs *buf)
+{
+ return -EOPNOTSUPP;
+}
+
+static const struct super_operations failfs_super_operations = {
+ .statfs = failfs_statfs,
+};
+
+static int failfs_fill_super(struct super_block *s, struct fs_context *fc)
+{
+ struct inode *inode;
+
+ s->s_maxbytes = MAX_LFS_FILESIZE;
+ s->s_blocksize = PAGE_SIZE;
+ s->s_blocksize_bits = PAGE_SHIFT;
+ s->s_magic = FAIL_FS_MAGIC;
+ s->s_op = &failfs_super_operations;
+ s->s_export_op = NULL;
+ s->s_xattr = NULL;
+ s->s_time_gran = 1;
+ s->s_d_flags = 0;
+
+ inode = new_inode(s);
+ if (!inode)
+ return -ENOMEM;
+
+ /* failfs supports no operations... */
+ inode->i_mode = S_IFDIR;
+ set_nlink(inode, 2);
+ inode->i_op = &failfs_dir_inode_operations;
+ inode->i_fop = &failfs_dir_operations;
+ simple_inode_init_ts(inode);
+ inode->i_ino = 1;
+ /* ... and is immutable. */
+ inode->i_flags |= S_IMMUTABLE;
+
+ set_default_d_op(s, &failfs_dentry_operations);
+ s->s_root = d_make_root(inode);
+ if (!s->s_root)
+ return -ENOMEM;
+
+ return 0;
+}
+
+static int failfs_get_tree(struct fs_context *fc)
+{
+ return get_tree_single(fc, failfs_fill_super);
+}
+
+static const struct fs_context_operations failfs_context_ops = {
+ .get_tree = failfs_get_tree,
+};
+
+static int failfs_init_fs_context(struct fs_context *fc)
+{
+ fc->ops = &failfs_context_ops;
+ fc->global = true;
+ fc->sb_flags |= SB_NOUSER;
+ fc->s_iflags |= SB_I_NOEXEC | SB_I_NODEV;
+ return 0;
+}
+
+int failfs_current_chdir(void)
+{
+ struct path path;
+
+ failfs_get_root(&path);
+ set_fs_pwd(current->fs, &path);
+ path_put(&path);
+ return 0;
+}
+
+static struct file_system_type failfs_fs_type = {
+ .name = "failfs",
+ .init_fs_context = failfs_init_fs_context,
+ .kill_sb = kill_anon_super,
+};
+
+void __init failfs_init(void)
+{
+ struct vfsmount *mnt;
+
+ /* A single instance that is member of no mount namespace. */
+ mnt = kern_mount(&failfs_fs_type);
+ if (IS_ERR(mnt))
+ panic("VFS: Failed to create failfs");
+
+ failfs_root_path.mnt = mnt;
+ failfs_root_path.dentry = mnt->mnt_root;
+}
diff --git a/fs/internal.h b/fs/internal.h
index 355d93f9220861..67aa0444351b1a 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -362,3 +362,7 @@ int anon_inode_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
struct iattr *attr);
void pidfs_get_root(struct path *path);
void nsfs_get_root(struct path *path);
+void failfs_get_root(struct path *path);
+void __init failfs_init(void);
+bool failfs_mnt(const struct vfsmount *mnt);
+int failfs_current_chdir(void);
diff --git a/fs/namespace.c b/fs/namespace.c
index 3d5cd5bf3b0537..87c365f2f82ba3 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -6274,6 +6274,7 @@ void __init mnt_init(void)
shmem_init();
init_rootfs();
init_mount_tree();
+ failfs_init();
}
void put_mnt_ns(struct mnt_namespace *ns)
diff --git a/fs/open.c b/fs/open.c
index 408925d7bd0b77..6b1c14e684a93b 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -570,9 +570,12 @@ SYSCALL_DEFINE1(chdir, const char __user *, filename)
SYSCALL_DEFINE1(fchdir, unsigned int, fd)
{
- CLASS(fd_raw, f)(fd);
int error;
+ if ((int)fd == FD_FAILFS_ROOT)
+ return failfs_current_chdir();
+
+ CLASS(fd_raw, f)(fd);
if (fd_empty(f))
return -EBADF;
@@ -615,6 +618,52 @@ SYSCALL_DEFINE1(chroot, const char __user *, filename)
return error;
}
+SYSCALL_DEFINE2(fchroot, int, fd, unsigned int, flags)
+{
+ struct path path;
+ int error;
+
+ if (flags)
+ return -EINVAL;
+
+ if (fd == FD_FAILFS_ROOT) {
+ if (!ns_capable(current_user_ns(), CAP_SYS_CHROOT)) {
+ if (!task_no_new_privs(current))
+ return -EPERM;
+ /* A shared fs_struct lets a sibling exec setuid past the check above. */
+ if (current->fs->users != 1)
+ return -EINVAL;
+ /* Moving the root to failfs lifts the old root's ".." barrier. */
+ if (current_chrooted())
+ return -EPERM;
+ }
+ failfs_get_root(&path);
+ } else {
+ CLASS(fd_raw, f)(fd);
+ if (fd_empty(f))
+ return -EBADF;
+
+ if (!d_can_lookup(fd_file(f)->f_path.dentry))
+ return -ENOTDIR;
+
+ error = file_permission(fd_file(f), MAY_EXEC | MAY_CHDIR);
+ if (error)
+ return error;
+
+ if (!ns_capable(current_user_ns(), CAP_SYS_CHROOT))
+ return -EPERM;
+
+ path = fd_file(f)->f_path;
+ path_get(&path);
+ }
+
+ error = security_path_chroot(&path);
+ if (!error)
+ set_fs_root(current->fs, &path);
+ path_put(&path);
+ return error;
+}
+
int chmod_common(const struct path *path, umode_t mode)
{
struct inode *inode = path->dentry->d_inode;
diff --git a/include/linux/syscalls.h b/include/linux/syscalls.h
index 874d9067a43b50..8413b624ad4782 100644
--- a/include/linux/syscalls.h
+++ b/include/linux/syscalls.h
@@ -457,6 +457,7 @@ asmlinkage long sys_faccessat2(int dfd, const char __user *filename, int mode,
asmlinkage long sys_chdir(const char __user *filename);
asmlinkage long sys_fchdir(unsigned int fd);
asmlinkage long sys_chroot(const char __user *filename);
+asmlinkage long sys_fchroot(int fd, unsigned int flags);
asmlinkage long sys_fchmod(unsigned int fd, umode_t mode);
asmlinkage long sys_fchmodat(int dfd, const char __user *filename,
umode_t mode);
diff --git a/include/uapi/asm-generic/unistd.h b/include/uapi/asm-generic/unistd.h
index a627acc8fb5fea..5b7e77a7c736ee 100644
--- a/include/uapi/asm-generic/unistd.h
+++ b/include/uapi/asm-generic/unistd.h
@@ -863,8 +863,12 @@ __SYSCALL(__NR_listns, sys_listns)
#define __NR_rseq_slice_yield 471
__SYSCALL(__NR_rseq_slice_yield, sys_rseq_slice_yield)
+/* fs/open.c */
+#define __NR_fchroot 472
+__SYSCALL(__NR_fchroot, sys_fchroot)
+
#undef __NR_syscalls
-#define __NR_syscalls 472
+#define __NR_syscalls 473
/*
* 32 bit systems traditionally used different
diff --git a/include/uapi/linux/fcntl.h b/include/uapi/linux/fcntl.h
index aadfbf6e0cb3a0..e43e3de3e9ee44 100644
--- a/include/uapi/linux/fcntl.h
+++ b/include/uapi/linux/fcntl.h
@@ -124,6 +124,7 @@ struct delegation {
#define FD_PIDFS_ROOT -10002 /* Root of the pidfs filesystem */
#define FD_NSFS_ROOT -10003 /* Root of the nsfs filesystem */
+#define FD_FAILFS_ROOT -10004 /* Root of the failfs filesystem */
#define FD_INVALID -10009 /* Invalid file descriptor: -10000 - EBADF = -10009 */
/* Generic flags for the *at(2) family of syscalls. */
diff --git a/include/uapi/linux/magic.h b/include/uapi/linux/magic.h
index 4f2da935a76cca..fd5f0e95648ee0 100644
--- a/include/uapi/linux/magic.h
+++ b/include/uapi/linux/magic.h
@@ -105,5 +105,6 @@
#define PID_FS_MAGIC 0x50494446 /* "PIDF" */
#define GUEST_MEMFD_MAGIC 0x474d454d /* "GMEM" */
#define NULL_FS_MAGIC 0x4E554C4C /* "NULL" */
+#define FAIL_FS_MAGIC 0x4641494C /* "FAIL" */
#endif /* __LINUX_MAGIC_H__ */
diff --git a/scripts/syscall.tbl b/scripts/syscall.tbl
index 7a42b32b657767..0ab5316051202e 100644
--- a/scripts/syscall.tbl
+++ b/scripts/syscall.tbl
@@ -412,3 +412,4 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common fchroot sys_fchroot
diff --git a/tools/include/uapi/asm-generic/unistd.h b/tools/include/uapi/asm-generic/unistd.h
index a627acc8fb5fea..5b7e77a7c736ee 100644
--- a/tools/include/uapi/asm-generic/unistd.h
+++ b/tools/include/uapi/asm-generic/unistd.h
@@ -863,8 +863,12 @@ __SYSCALL(__NR_listns, sys_listns)
#define __NR_rseq_slice_yield 471
__SYSCALL(__NR_rseq_slice_yield, sys_rseq_slice_yield)
+/* fs/open.c */
+#define __NR_fchroot 472
+__SYSCALL(__NR_fchroot, sys_fchroot)
+
#undef __NR_syscalls
-#define __NR_syscalls 472
+#define __NR_syscalls 473
/*
* 32 bit systems traditionally used different
diff --git a/tools/perf/arch/x86/entry/syscalls/syscall_64.tbl b/tools/perf/arch/x86/entry/syscalls/syscall_64.tbl
index 524155d655da15..d5b6045b0090cc 100644
--- a/tools/perf/arch/x86/entry/syscalls/syscall_64.tbl
+++ b/tools/perf/arch/x86/entry/syscalls/syscall_64.tbl
@@ -396,6 +396,7 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common fchroot sys_fchroot
#
# Due to a historical design error, certain syscalls are numbered differently
diff --git a/tools/scripts/syscall.tbl b/tools/scripts/syscall.tbl
index 7a42b32b657767..0ab5316051202e 100644
--- a/tools/scripts/syscall.tbl
+++ b/tools/scripts/syscall.tbl
@@ -412,3 +412,4 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common fchroot sys_fchroot
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 8d4db2241cc2a2..f87167bcf58282 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -33,6 +33,7 @@ TARGETS += fchmodat2
TARGETS += filesystems
TARGETS += filesystems/binderfs
TARGETS += filesystems/epoll
+TARGETS += filesystems/failfs
TARGETS += filesystems/fat
TARGETS += filesystems/overlayfs
TARGETS += filesystems/statmount
diff --git a/tools/testing/selftests/filesystems/failfs/.gitignore b/tools/testing/selftests/filesystems/failfs/.gitignore
new file mode 100644
index 00000000000000..cd3b5d884d7ea9
--- /dev/null
+++ b/tools/testing/selftests/filesystems/failfs/.gitignore
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: GPL-2.0-only
+failfs_test
diff --git a/tools/testing/selftests/filesystems/failfs/Makefile b/tools/testing/selftests/filesystems/failfs/Makefile
new file mode 100644
index 00000000000000..3c5d98b4fe725e
--- /dev/null
+++ b/tools/testing/selftests/filesystems/failfs/Makefile
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: GPL-2.0
+CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
+TEST_GEN_PROGS := failfs_test
+
+include ../../lib.mk
diff --git a/tools/testing/selftests/filesystems/failfs/failfs_test.c b/tools/testing/selftests/filesystems/failfs/failfs_test.c
new file mode 100644
index 00000000000000..cc461dc483a997
--- /dev/null
+++ b/tools/testing/selftests/filesystems/failfs/failfs_test.c
@@ -0,0 +1,581 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <link.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <sys/vfs.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "../../kselftest_harness.h"
+
+#ifndef __NR_fchroot
+#define __NR_fchroot 472
+#endif
+
+#ifndef FD_PIDFS_ROOT
+#define FD_PIDFS_ROOT -10002
+#endif
+
+#ifndef FD_NSFS_ROOT
+#define FD_NSFS_ROOT -10003
+#endif
+
+#ifndef FD_FAILFS_ROOT
+#define FD_FAILFS_ROOT -10004
+#endif
+
+#define NOBODY_UID 65534
+
+/* Child sentinel exit code: the exec was blocked as expected. */
+#define FAILFS_EXEC_BLOCKED 99
+
+/* Stack for the CLONE_FS helper in fchroot_sentinel_shared_fs_struct. */
+#define FAILFS_CLONE_STACK (64 * 1024)
+
+static int sys_fchroot(int fd, unsigned int flags)
+{
+ return syscall(__NR_fchroot, fd, flags);
+}
+
+/*
+ * Raw syscall: glibc's getcwd() rejects the kernel's "(unreachable)"
+ * result and falls back to a generic implementation.
+ */
+static long sys_getcwd(char *buf, size_t size)
+{
+ return syscall(__NR_getcwd, buf, size);
+}
+
+static int drop_to_nobody(void)
+{
+ return setresuid(NOBODY_UID, NOBODY_UID, NOBODY_UID);
+}
+
+/* Parked CLONE_FS child; dies with its parent so it never leaks. */
+static int failfs_park(void *arg)
+{
+ (void)arg;
+ prctl(PR_SET_PDEATHSIG, SIGKILL);
+ pause();
+ return 0;
+}
+
+/* Is fd a dynamically linked ELF with an absolute PT_INTERP interpreter? */
+static int elf_has_absolute_interp(int fd)
+{
+ ElfW(Ehdr) ehdr;
+ ElfW(Phdr) phdr;
+ char interp;
+ int i;
+
+ if (pread(fd, &ehdr, sizeof(ehdr), 0) != sizeof(ehdr))
+ return 0;
+ if (memcmp(ehdr.e_ident, ELFMAG, SELFMAG) != 0)
+ return 0;
+
+ for (i = 0; i < ehdr.e_phnum; i++) {
+ if (pread(fd, &phdr, sizeof(phdr),
+ ehdr.e_phoff + i * sizeof(phdr)) != sizeof(phdr))
+ return 0;
+ if (phdr.p_type != PT_INTERP)
+ continue;
+ if (pread(fd, &interp, 1, phdr.p_offset) != 1)
+ return 0;
+ return interp == '/';
+ }
+
+ return 0;
+}
+
+TEST(fchdir_sentinel)
+{
+ char buf[PATH_MAX];
+ int fd;
+
+ ASSERT_EQ(fchdir(FD_FAILFS_ROOT), 0);
+
+ /* The working directory is unreachable from the process root. */
+ ASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0);
+ ASSERT_EQ(strncmp(buf, "(unreachable)", 13), 0);
+
+ /* Every AT_FDCWD-relative lookup fails. */
+ ASSERT_EQ(openat(AT_FDCWD, "foo", O_RDONLY), -1);
+ ASSERT_EQ(errno, EOPNOTSUPP);
+ ASSERT_EQ(openat(AT_FDCWD, ".", O_RDONLY), -1);
+ ASSERT_EQ(errno, EOPNOTSUPP);
+ ASSERT_EQ(openat(AT_FDCWD, "..", O_RDONLY), -1);
+ ASSERT_EQ(errno, EOPNOTSUPP);
+ ASSERT_EQ(openat(AT_FDCWD, "foo", O_WRONLY | O_CREAT, 0600), -1);
+ ASSERT_EQ(errno, EOPNOTSUPP);
+
+ /* The cwd cannot be pinned by following /proc/self/cwd into it. */
+ ASSERT_EQ(open("/proc/self/cwd", O_PATH), -1);
+ ASSERT_EQ(errno, EOPNOTSUPP);
+
+ /* The root is untouched so absolute lookups keep working... */
+ fd = open("/", O_RDONLY | O_DIRECTORY);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(close(fd), 0);
+
+ /* ... and the working directory can be recovered. */
+ ASSERT_EQ(chdir("/"), 0);
+ ASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0);
+ ASSERT_EQ(strcmp(buf, "/"), 0);
+}
+
+TEST(fchdir_rejects_other_sentinels)
+{
+ ASSERT_EQ(fchdir(FD_PIDFS_ROOT), -1);
+ ASSERT_EQ(errno, EBADF);
+ ASSERT_EQ(fchdir(FD_NSFS_ROOT), -1);
+ ASSERT_EQ(errno, EBADF);
+ ASSERT_EQ(fchdir(-10009), -1);
+ ASSERT_EQ(errno, EBADF);
+}
+
+TEST(fchroot_flags)
+{
+ int fd;
+
+ ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 1), -1);
+ ASSERT_EQ(errno, EINVAL);
+
+ fd = open("/", O_PATH | O_DIRECTORY);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(sys_fchroot(fd, 1), -1);
+ ASSERT_EQ(errno, EINVAL);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(fchroot_bad_fd)
+{
+ ASSERT_EQ(sys_fchroot(-1, 0), -1);
+ ASSERT_EQ(errno, EBADF);
+
+ /* Only FD_FAILFS_ROOT is a valid sentinel. */
+ ASSERT_EQ(sys_fchroot(FD_PIDFS_ROOT, 0), -1);
+ ASSERT_EQ(errno, EBADF);
+ ASSERT_EQ(sys_fchroot(FD_NSFS_ROOT, 0), -1);
+ ASSERT_EQ(errno, EBADF);
+}
+
+TEST(fchroot_notdir)
+{
+ int fd;
+
+ fd = open("/proc/self/status", O_RDONLY);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(sys_fchroot(fd, 0), -1);
+ ASSERT_EQ(errno, ENOTDIR);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(fchroot_realfd_requires_cap)
+{
+ int fd;
+
+ if (geteuid() == 0)
+ ASSERT_EQ(drop_to_nobody(), 0);
+
+ fd = open("/", O_PATH | O_DIRECTORY);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(sys_fchroot(fd, 0), -1);
+ ASSERT_EQ(errno, EPERM);
+ ASSERT_EQ(close(fd), 0);
+}
+
+TEST(fchroot_realfd)
+{
+ char template[] = "/tmp/failfs_test.XXXXXX";
+ char path[PATH_MAX];
+ struct stat st;
+ int tmpfd, dfd, fd;
+
+ if (geteuid() != 0)
+ SKIP(return, "fchroot() with a regular fd requires CAP_SYS_CHROOT");
+
+ tmpfd = open("/tmp", O_PATH | O_DIRECTORY);
+ ASSERT_GE(tmpfd, 0);
+
+ ASSERT_NE(mkdtemp(template), NULL);
+ snprintf(path, sizeof(path), "%s/canary", template);
+ fd = open(path, O_WRONLY | O_CREAT, 0600);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(close(fd), 0);
+
+ dfd = open(template, O_PATH | O_DIRECTORY);
+ ASSERT_GE(dfd, 0);
+ ASSERT_EQ(sys_fchroot(dfd, 0), 0);
+ ASSERT_EQ(close(dfd), 0);
+
+ ASSERT_EQ(stat("/canary", &st), 0);
+
+ /* Best-effort cleanup: dirfd-anchored I/O works with the new root. */
+ snprintf(path, sizeof(path), "%s/canary", template + strlen("/tmp/"));
+ unlinkat(tmpfd, path, 0);
+ unlinkat(tmpfd, template + strlen("/tmp/"), AT_REMOVEDIR);
+}
+
+TEST(fchroot_sentinel)
+{
+ char template[] = "/tmp/failfs_test.XXXXXX";
+ struct stat realroot, st;
+ struct statfs sfs;
+ char buf[PATH_MAX];
+ int procfd, tmpfd, dfd, fd;
+ struct {
+ struct file_handle handle;
+ unsigned char f_handle[MAX_HANDLE_SZ];
+ } fh;
+ int mntid;
+ ssize_t ret;
+
+ if (geteuid() != 0)
+ SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT");
+
+ ASSERT_EQ(stat("/", &realroot), 0);
+ procfd = open("/proc", O_PATH | O_DIRECTORY);
+ ASSERT_GE(procfd, 0);
+ tmpfd = open("/tmp", O_PATH | O_DIRECTORY);
+ ASSERT_GE(tmpfd, 0);
+ ASSERT_NE(mkdtemp(template), NULL);
+ dfd = open(template, O_RDONLY | O_DIRECTORY);
+ ASSERT_GE(dfd, 0);
+
+ ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
+
+ /* Absolute lookups fail. */
+ ASSERT_EQ(open("/etc/passwd", O_RDONLY), -1);
+ ASSERT_EQ(errno, EOPNOTSUPP);
+ ASSERT_EQ(mkdir("/foo", 0700), -1);
+ ASSERT_EQ(errno, EOPNOTSUPP);
+
+ /*
+ * The root cannot be referenced at all - not even an O_PATH open,
+ * which skips ->permission(), because it lands on the root as a
+ * jumped walk terminal that ->d_weak_revalidate() refuses.
+ */
+ ASSERT_EQ(open("/", O_RDONLY | O_DIRECTORY), -1);
+ ASSERT_EQ(errno, EOPNOTSUPP);
+ ASSERT_EQ(open("/", O_PATH), -1);
+ ASSERT_EQ(errno, EOPNOTSUPP);
+ ASSERT_EQ(statfs("/", &sfs), -1);
+ ASSERT_EQ(errno, EOPNOTSUPP);
+
+ /*
+ * It cannot be pinned by following /proc/self/root into it either
+ * (only the root is in failfs here, so self/cwd is still real).
+ */
+ ASSERT_EQ(openat(procfd, "self/root", O_PATH), -1);
+ ASSERT_EQ(errno, EOPNOTSUPP);
+
+ /* Nor encoded into a file handle. */
+ fh.handle.handle_bytes = MAX_HANDLE_SZ;
+ ASSERT_EQ(name_to_handle_at(AT_FDCWD, "/", &fh.handle, &mntid, 0), -1);
+ ASSERT_EQ(errno, EOPNOTSUPP);
+
+ /* The working directory is now unreachable from the root. */
+ ASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0);
+ ASSERT_EQ(strncmp(buf, "(unreachable)", 13), 0);
+
+ /* Lookups anchored at real directories keep working. */
+ fd = openat(AT_FDCWD, ".", O_RDONLY | O_DIRECTORY);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(close(fd), 0);
+ fd = openat(dfd, "canary", O_WRONLY | O_CREAT, 0600);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(write(fd, "x", 1), 1);
+ ASSERT_EQ(close(fd), 0);
+ fd = openat(dfd, "canary", O_RDONLY);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(close(fd), 0);
+
+ /* ".." walks clamp at the top of the mount tree, not at failfs. */
+ fd = openat(AT_FDCWD, "../../../../../../../../../..", O_PATH);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(fstat(fd, &st), 0);
+ ASSERT_EQ(st.st_dev, realroot.st_dev);
+ ASSERT_EQ(st.st_ino, realroot.st_ino);
+ ASSERT_EQ(close(fd), 0);
+
+ /* readlink of the magic link still works: it does not follow. */
+ ret = readlinkat(procfd, "self/root", buf, sizeof(buf) - 1);
+ ASSERT_GT(ret, 0);
+ buf[ret] = '\0';
+ TH_LOG("/proc/self/root points to '%s'", buf);
+ /* d_path() names the failfs root synthetically, never as a real path. */
+ ASSERT_EQ(strcmp(buf, "failfs:/"), 0);
+
+ /* But following it into failfs is refused. */
+ ASSERT_EQ(fstatat(procfd, "self/root", &st, 0), -1);
+ ASSERT_EQ(errno, EOPNOTSUPP);
+
+ /* Best-effort cleanup via the pre-opened dirfds. */
+ unlinkat(dfd, "canary", 0);
+ unlinkat(tmpfd, template + strlen("/tmp/"), AT_REMOVEDIR);
+}
+
+TEST(fchroot_sentinel_absolute_symlink)
+{
+ char template[] = "/tmp/failfs_test.XXXXXX";
+ int tmpfd, dfd, fd;
+
+ if (geteuid() != 0)
+ SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT");
+
+ tmpfd = open("/tmp", O_PATH | O_DIRECTORY);
+ ASSERT_GE(tmpfd, 0);
+ ASSERT_NE(mkdtemp(template), NULL);
+ dfd = open(template, O_RDONLY | O_DIRECTORY);
+ ASSERT_GE(dfd, 0);
+
+ fd = openat(dfd, "target", O_WRONLY | O_CREAT, 0600);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(close(fd), 0);
+ ASSERT_EQ(symlinkat("target", dfd, "rel"), 0);
+ ASSERT_EQ(symlinkat("/etc", dfd, "abs"), 0);
+
+ ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
+
+ /* Relative symlinks keep resolving within the dirfd-anchored walk... */
+ fd = openat(dfd, "rel", O_RDONLY);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(close(fd), 0);
+
+ /* ... absolute symlinks restart the walk at the failfs root. */
+ ASSERT_EQ(openat(dfd, "abs", O_RDONLY), -1);
+ ASSERT_EQ(errno, EOPNOTSUPP);
+
+ /* Best-effort cleanup via the pre-opened dirfds. */
+ unlinkat(dfd, "abs", 0);
+ unlinkat(dfd, "rel", 0);
+ unlinkat(dfd, "target", 0);
+ unlinkat(tmpfd, template + strlen("/tmp/"), AT_REMOVEDIR);
+}
+
+TEST(fchroot_sentinel_unprivileged)
+{
+ char buf[PATH_MAX];
+
+ if (geteuid() == 0)
+ ASSERT_EQ(drop_to_nobody(), 0);
+
+ /* Without no_new_privs entering failfs is not allowed... */
+ ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), -1);
+ ASSERT_EQ(errno, EPERM);
+
+ /* ... with no_new_privs set it is allowed. */
+ ASSERT_EQ(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0);
+ ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
+
+ ASSERT_EQ(open("/etc/passwd", O_RDONLY), -1);
+ ASSERT_EQ(errno, EOPNOTSUPP);
+
+ /* The task counts as chrooted: no user namespaces anymore. */
+ ASSERT_EQ(unshare(CLONE_NEWUSER), -1);
+ ASSERT_EQ(errno, EPERM);
+
+ /* With both root and cwd in failfs getcwd() reports "/". */
+ ASSERT_EQ(fchdir(FD_FAILFS_ROOT), 0);
+ ASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0);
+ ASSERT_EQ(strcmp(buf, "/"), 0);
+}
+
+TEST(fchroot_sentinel_rejected_when_chrooted)
+{
+ char template[] = "/tmp/failfs_test.XXXXXX";
+ int tmpfd;
+
+ if (geteuid() != 0)
+ SKIP(return, "chroot() requires CAP_SYS_CHROOT");
+
+ tmpfd = open("/tmp", O_PATH | O_DIRECTORY);
+ ASSERT_GE(tmpfd, 0);
+ ASSERT_NE(mkdtemp(template), NULL);
+ ASSERT_EQ(chroot(template), 0);
+ ASSERT_EQ(chdir("/"), 0);
+
+ /* Remove the jail while still privileged; sticky /tmp blocks nobody. */
+ unlinkat(tmpfd, template + strlen("/tmp/"), AT_REMOVEDIR);
+
+ ASSERT_EQ(drop_to_nobody(), 0);
+ ASSERT_EQ(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0);
+
+ /* An unprivileged chrooted task must not lift its ".." barrier. */
+ ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), -1);
+ ASSERT_EQ(errno, EPERM);
+}
+
+TEST(fchroot_sentinel_shared_fs_struct)
+{
+ char stack[FAILFS_CLONE_STACK];
+ pid_t pid;
+
+ if (geteuid() == 0)
+ ASSERT_EQ(drop_to_nobody(), 0);
+
+ /* A CLONE_FS sibling shares the fs_struct: bump fs->users to 2. */
+ pid = clone(failfs_park, stack + sizeof(stack), CLONE_FS | SIGCHLD,
+ NULL);
+ ASSERT_GE(pid, 0);
+
+ ASSERT_EQ(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0);
+
+ /*
+ * A sibling without no_new_privs could exec a setuid binary with
+ * the failfs root, so a shared fs_struct is refused even with
+ * no_new_privs set.
+ */
+ ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), -1);
+ ASSERT_EQ(errno, EINVAL);
+
+ ASSERT_EQ(kill(pid, SIGKILL), 0);
+ ASSERT_EQ(waitpid(pid, NULL, 0), pid);
+}
+
+TEST(fchroot_sentinel_no_overmount)
+{
+ if (geteuid() != 0)
+ SKIP(return, "mounting requires privileges");
+
+ /*
+ * Contain the blast radius: if failfs ever regressed and "/"
+ * resolved to the real root, the tmpfs mount below must not touch
+ * the host. A private mount namespace keeps it local to this child.
+ */
+ ASSERT_EQ(unshare(CLONE_NEWNS), 0);
+ ASSERT_EQ(mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL), 0);
+
+ ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
+
+ /*
+ * Nothing can be mounted on top of the failfs root. It cannot even
+ * be named as a mount target: resolving "/" is refused before the
+ * mount machinery (which, failfs being in no mount namespace, would
+ * reject it anyway) is ever reached. open_tree(OPEN_TREE_CLONE) is
+ * likewise moot since no fd to the root can be obtained.
+ */
+ ASSERT_EQ(mount("none", "/", "tmpfs", 0, NULL), -1);
+ ASSERT_EQ(errno, EOPNOTSUPP);
+}
+
+TEST(fchroot_sentinel_setns_escape)
+{
+ struct stat realroot, st;
+ int nsfd;
+
+ if (geteuid() != 0)
+ SKIP(return, "setns() to a mount namespace requires privileges");
+
+ ASSERT_EQ(stat("/", &realroot), 0);
+ nsfd = open("/proc/self/ns/mnt", O_RDONLY);
+ ASSERT_GE(nsfd, 0);
+
+ ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
+ ASSERT_EQ(open("/etc", O_PATH), -1);
+ ASSERT_EQ(errno, EOPNOTSUPP);
+
+ /* A mount namespace fd is the key out: it resets root and cwd. */
+ ASSERT_EQ(setns(nsfd, CLONE_NEWNS), 0);
+ ASSERT_EQ(close(nsfd), 0);
+
+ ASSERT_EQ(stat("/", &st), 0);
+ ASSERT_EQ(st.st_dev, realroot.st_dev);
+ ASSERT_EQ(st.st_ino, realroot.st_ino);
+}
+
+TEST(fchroot_sentinel_exec)
+{
+ pid_t pid;
+ int status;
+
+ if (geteuid() != 0)
+ SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT");
+
+ ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
+
+ /*
+ * Exec in a child: a wrongly successful exec would replace the test
+ * image and its exit code would not match the sentinel below.
+ */
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0) {
+ execl("/bin/true", "true", NULL);
+ _exit(errno == EOPNOTSUPP ? FAILFS_EXEC_BLOCKED : 1);
+ }
+ ASSERT_EQ(waitpid(pid, &status, 0), pid);
+ ASSERT_TRUE(WIFEXITED(status));
+ ASSERT_EQ(WEXITSTATUS(status), FAILFS_EXEC_BLOCKED);
+}
+
+TEST(fchroot_sentinel_exec_interpreter)
+{
+ static const char * const argv[] = { "failfs_test", NULL };
+ static const char * const envp[] = { NULL };
+ pid_t pid;
+ int status, exefd;
+
+ if (geteuid() != 0)
+ SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT");
+
+ /* Exec ourselves: the one binary guaranteed to be around. */
+ exefd = open("/proc/self/exe", O_RDONLY);
+ ASSERT_GE(exefd, 0);
+ if (!elf_has_absolute_interp(exefd))
+ SKIP(return, "test binary has no absolute PT_INTERP interpreter");
+
+ ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
+
+ /*
+ * The binary itself needs no path lookup - it is executed by fd -
+ * but loading it fails on opening the absolute PT_INTERP
+ * interpreter. Run it in a child so a wrongly successful exec does
+ * not replace the test image and masquerade as a pass.
+ */
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0) {
+ syscall(__NR_execveat, exefd, "", argv, envp, AT_EMPTY_PATH);
+ _exit(errno == EOPNOTSUPP ? FAILFS_EXEC_BLOCKED : 1);
+ }
+ ASSERT_EQ(waitpid(pid, &status, 0), pid);
+ ASSERT_TRUE(WIFEXITED(status));
+ ASSERT_EQ(WEXITSTATUS(status), FAILFS_EXEC_BLOCKED);
+}
+
+TEST(fchroot_sentinel_inherited)
+{
+ pid_t pid;
+ int status;
+
+ if (geteuid() != 0)
+ SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT");
+
+ ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0) {
+ if (open("/etc", O_PATH) != -1 || errno != EOPNOTSUPP)
+ _exit(1);
+ _exit(0);
+ }
+ ASSERT_EQ(waitpid(pid, &status, 0), pid);
+ ASSERT_TRUE(WIFEXITED(status));
+ ASSERT_EQ(WEXITSTATUS(status), 0);
+}
+
+TEST_HARNESS_MAIN
|
| 3/2 | 2026/07/25 19:05 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 19006 output: 162 thoughts: 1777 Thoughts: **Analysis of the 'failfs' Patch for Fuzzing** Alright, let's break this down. We've got a new patch introducing `failfs`, a filesystem designed to fail every operation. The key additions here are the new `fchroot` system call and the magic file descriptor `FD_FAILFS_ROOT`. This means we've modified core VFS operations. My initial thought is that we need to confirm its reachability and overall functionality, which looks straightforward. `fchroot(FD_FAILFS_ROOT, 0)` or `fchdir(FD_FAILFS_ROOT)` are clear entry points. The `failfs.c` code confirms this. Functionality-wise, `failfs` itself is simple – it consistently returns `-EOPNOTSUPP`. However, the interesting part is its interaction with the Virtual File System (VFS), namely through `d_path`, `fchdir`, and `fchroot`. This tells me we should focus our fuzzing efforts on these interfaces. Specifically, I'm zeroing in on a few crucial symbols: * `sys_fchroot`: This is the new system call, the main entry point we need to test. * `sys_fchdir`: The existing `fchdir` syscall has been modified to handle `FD_FAILFS_ROOT`, making it a key focus too. * `failfs_current_chdir`: This is the function called within `sys_fchdir` when the `FD_FAILFS_ROOT` is passed to it. * `failfs_get_root`: This function is called from `sys_fchroot`. * `failfs_mnt`: This is likely the mount function. No specific configuration options are needed, since it's unconditionally built-in. The rationale is clear. The introduction of a new system call (`fchroot`) and the modification of an existing one (`fchdir`) to interact with a new "failing" filesystem create potential attack surfaces. We need to fuzz these interfaces to look for regressions, vulnerabilities such as chroot escapes, or unexpected VFS behaviors. |
| 4/2 | 2026/07/25 19:05 | tool | set-results |
0mArgs:{
"EnableConfigs": [],
"FocusSymbols": [
"sys_fchroot",
"sys_fchdir",
"failfs_current_chdir",
"failfs_get_root",
"failfs_mnt"
],
"Reasoning": "The patch introduces a new kernel-internal filesystem `failfs` and a new system call `fchroot`. It also modifies `fchdir` to accept a new magic file descriptor `FD_FAILFS_ROOT`. These are core VFS changes that are reachable from userspace and should be fuzzed to ensure they don't introduce regressions or vulnerabilities (e.g., escaping chroot, reference leaks, or unexpected behavior in path resolution).",
"WorthFuzzing": true
}Results: {
"EnableConfigs": null,
"FocusSymbols": [
"sys_fchroot",
"sys_fchdir",
"failfs_current_chdir",
"failfs_get_root",
"failfs_mnt"
],
"Reasoning": "The patch introduces a new kernel-internal filesystem `failfs` and a new system call `fchroot`. It also modifies `fchdir` to accept a new magic file descriptor `FD_FAILFS_ROOT`. These are core VFS changes that are reachable from userspace and should be fuzzed to ensure they don't introduce regressions or vulnerabilities (e.g., escaping chroot, reference leaks, or unexpected behavior in path resolution).",
"WorthFuzzing": true
} |
| Total Calls | Total Tokens | Avg Tokens | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|
| Total Calls | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|