From nobody Fri Jul 24 21:27:05 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-alma10-1.taild15c8.ts.net [100.103.45.18]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 062813F1AC5; Fri, 24 Jul 2026 13:41:30 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=100.103.45.18 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784900492; cv=none; b=heRswAviKSXTMV3XvIJG8olKdTAl7Ogi5yU/wi1U1lHGMBEZ4UgmKcC1CCzQzcotViQAEt9xs53rUMC1yoU37KIShKq6F7avqJ8sIg3P9lVDwIYpB1Yp1JsvLXbz+0gUQ9UDkeMvNWdbiyaobVJtY3Q5VZPLTDC7Toa+N9A/L04= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784900492; c=relaxed/simple; bh=yIu0I1zNWOxg62L3UqwMfUk2/IO+cPuqLo+Pb+5lt2o=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=du4A79QRf0GGVZQymYhb+GcEcrCi9XESKQm1k5PZe0i1lSrC++tWW6JMINXlB19dA0Vrks7lMtXejz/N/i+Htq3WC9ZD3oJBsx6Pto0Xy9NKZx5V1ciFzOQrkQ1KHeTF8pYLN9l/+NKhS38w4Lf3XrS6wukhV0SrbEC74J3r3mM= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=LHkC+Zp3; arc=none smtp.client-ip=100.103.45.18 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="LHkC+Zp3" Received: by smtp.kernel.org (Postfix) with ESMTPSA id A1E141F00A3A; Fri, 24 Jul 2026 13:41:27 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1784900490; bh=h/FPJKjvEfLCtBp06sGmT7ICNUBOeJ9CZ91o23d9slw=; h=From:Date:Subject:References:In-Reply-To:To:Cc; b=LHkC+Zp328u2Ksc445qvxgArNONTQjXTncfXhwMABXcRULueenkr07v5cTFaGAvyX XSfGUjmWYm/NYB/n7znHbbiNK1bbg4RwjVFGgmZ9LBkYOaezdB1fEP+nfXqUpsqIhl tp/jx5adlaNJzcL77dHFDPjffy8aQeJ5sJimiYFY0ctPeEgk+bG3mNcCW+/7aeniec hzfJNPVpgabv0d4JmyBDzEP4JPWANbKkiC91+tXUKz7I0Y7qLs5WJhaeu48rqTwYBZ WzOzu4JyICzI2sAKL+o+Fni/t79wS1jxo6LWxzvWRmp5I8oMJ11vr574OHXXpjg7ZD 2p1sdOv5NmOVA== From: Christian Brauner Date: Fri, 24 Jul 2026 15:41:17 +0200 Subject: [PATCH v2 1/7] fs: add failfs Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20260724-work-failfs-v2-1-485dabbae185@kernel.org> References: <20260724-work-failfs-v2-0-485dabbae185@kernel.org> In-Reply-To: <20260724-work-failfs-v2-0-485dabbae185@kernel.org> To: linux-fsdevel@vger.kernel.org, Andy Lutomirski , Jann Horn Cc: John Ericson , linux-api@vger.kernel.org, "H. Peter Anvin" , Kees Cook , Farid Zakaria , Alexander Viro , Christian Brauner , Jan Kara , linux-kernel@vger.kernel.org, Jonathan Corbet , linux-doc@vger.kernel.org X-Mailer: b4 0.16-dev-401aa X-Developer-Signature: v=1; a=openpgp-sha256; l=9546; i=brauner@kernel.org; h=from:subject:message-id; bh=yIu0I1zNWOxg62L3UqwMfUk2/IO+cPuqLo+Pb+5lt2o=; b=owGbwMvMwCU28Zj0gdSKO4sYT6slMWQlZzc3L5dwfXDka+pBsSVnFq0qfsLv3KhiGzJlWurN6 reqnTXHO0pZGMS4GGTFFFkc2k3C5ZbzVGw2ytSAmcPKBDKEgYtTACaiNZuR4V2XUIP1mYAbgtbH V81nlHjEFSDyqVYtf+7OZ/OvHin6f4ORYeKb2cu3z/FfNn/fzg+6iVY2vGYX134XLG0pKvl7y9P wERcA X-Developer-Key: i=brauner@kernel.org; a=openpgp; fpr=4880B8C9BD0E5106FC070F4F7B3C391EFEA93624 nullfs provides a permanently empty and immutable directory. Lookups fail with ENOENT. The directory can be opened, read, stat, mounted upon. It behaves like nothing is there. Add its counterpart failfs where the semantics are not "there is nothing here" but "nothing is supported here". Every operation that reaches the filesystem fails with EOPNOTSUPP. Even statfs()/fstatfs() fail so the filesystem cannot be discovered through an fd to it. EOPNOTSUPP rather than a permission errno keeps that coherent. There is no permission model in which anything could ever be allowed and EACCES or EPERM would merely suggest that different credentials might succeed while EIO would suggest corruption. It also makes hitting the failfs boundary mostly quite dinstinguishable. A task anchoring its lookups at real directory file descriptors may be able to tell a failfs refusal from an ordinary permission failure. I wouldn't go so far as guaranteeing that but it should mostly work. No path lookup can open the root, not even with O_PATH. It is never reached by a lookup in a parent directory. The only way to a path-walk terminal at the root is a jump through a /proc//{root,cwd} magic link or by mountpoint traversal. The root also refuses ->d_weak_revalidate() which the VFS calls for jumped terminals. That covers the jump-based references too: an O_PATH open is refused, name_to_handle_at() cannot encode it into a file handle, and following a magic link into it fails. A plain readlink() of such a link still works and shows "failfs:/". There is a single instance of failfs mounted during early boot via kern_mount() making it logically distinct from every mount namespace. Since the mount is a member of no mount namespace mounting onto it fails. So nothing can ever be mounted on top of it. It cannot be cloned via OPEN_TREE_CLONE and it does not show up in statmount()/listmount() or /proc//mountinfo. The filesystem is not registered so it is not visible in /proc/filesystems and cannot be mounted from userspace. This lets tasks shed their filesystem state completely. A process with its root directory or working directory in failfs must anchor every path lookup at an explicit file descriptor or is doomed to fail any lookup. Absolute paths, absolute symlinks, and AT_FDCWD-relative lookups simply fail. Followup patches will expose it via a new FD_FAILFS_ROOT file descriptor sentinel understood by fchdir() and the new fchroot() system call. Signed-off-by: Christian Brauner (Amutable) --- fs/Makefile | 2 +- fs/d_path.c | 3 +- fs/failfs.c | 155 +++++++++++++++++++++++++++++++++++++++++= ++++ fs/internal.h | 3 + fs/namespace.c | 1 + include/uapi/linux/magic.h | 1 + 6 files changed, 163 insertions(+), 2 deletions(-) diff --git a/fs/Makefile b/fs/Makefile index 89a8a9d207d1..73b6cab7738e 100644 --- a/fs/Makefile +++ b/fs/Makefile @@ -16,7 +16,7 @@ obj-y :=3D 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 =20 obj-$(CONFIG_BUFFER_HEAD) +=3D buffer.o mpage.o obj-$(CONFIG_PROC_FS) +=3D proc_namespace.o diff --git a/fs/d_path.c b/fs/d_path.c index a48957c0971e..c25309006d5d 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 bu= flen) * 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 !=3D path->mnt->mnt_root)) + (!IS_ROOT(path->dentry) || path->dentry !=3D path->mnt->mnt_root || + failfs_mnt(path->mnt))) return path->dentry->d_op->d_dname(path->dentry, buf, buflen); =20 rcu_read_lock(); diff --git a/fs/failfs.c b/fs/failfs.c new file mode 100644 index 000000000000..d0ca1fc6c459 --- /dev/null +++ b/fs/failfs.c @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright (c) 2026 Christian Brauner */ +#include +#include +#include +#include +#include + +#include "internal.h" + +static struct path failfs_root_path =3D {}; + +void failfs_get_root(struct path *path) +{ + *path =3D failfs_root_path; + path_get(path); +} + +bool failfs_mnt(const struct vfsmount *mnt) +{ + return mnt->mnt_sb =3D=3D 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 *dent= ry, + 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 =3D { + .permission =3D failfs_permission, + .lookup =3D failfs_lookup, + .getattr =3D failfs_getattr, +}; + +static const struct file_operations failfs_dir_operations =3D {}; + +static int failfs_d_weak_revalidate(struct dentry *dentry, unsigned int fl= ags) +{ + /* + * 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//{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 =3D { + .d_dname =3D failfs_dname, + .d_weak_revalidate =3D failfs_d_weak_revalidate, +}; + +static int failfs_statfs(struct dentry *dentry, struct kstatfs *buf) +{ + return -EOPNOTSUPP; +} + +static const struct super_operations failfs_super_operations =3D { + .statfs =3D failfs_statfs, +}; + +static int failfs_fill_super(struct super_block *s, struct fs_context *fc) +{ + struct inode *inode; + + s->s_maxbytes =3D MAX_LFS_FILESIZE; + s->s_blocksize =3D PAGE_SIZE; + s->s_blocksize_bits =3D PAGE_SHIFT; + s->s_magic =3D FAIL_FS_MAGIC; + s->s_op =3D &failfs_super_operations; + s->s_export_op =3D NULL; + s->s_xattr =3D NULL; + s->s_time_gran =3D 1; + s->s_d_flags =3D 0; + + inode =3D new_inode(s); + if (!inode) + return -ENOMEM; + + /* failfs supports no operations... */ + inode->i_mode =3D S_IFDIR; + set_nlink(inode, 2); + inode->i_op =3D &failfs_dir_inode_operations; + inode->i_fop =3D &failfs_dir_operations; + simple_inode_init_ts(inode); + inode->i_ino =3D 1; + /* ... and is immutable. */ + inode->i_flags |=3D S_IMMUTABLE; + + set_default_d_op(s, &failfs_dentry_operations); + s->s_root =3D 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 =3D { + .get_tree =3D failfs_get_tree, +}; + +static int failfs_init_fs_context(struct fs_context *fc) +{ + fc->ops =3D &failfs_context_ops; + fc->global =3D true; + fc->sb_flags |=3D SB_NOUSER; + fc->s_iflags |=3D SB_I_NOEXEC | SB_I_NODEV; + return 0; +} + +static struct file_system_type failfs_fs_type =3D { + .name =3D "failfs", + .init_fs_context =3D failfs_init_fs_context, + .kill_sb =3D kill_anon_super, +}; + +void __init failfs_init(void) +{ + struct vfsmount *mnt; + + /* A single instance that is member of no mount namespace. */ + mnt =3D kern_mount(&failfs_fs_type); + if (IS_ERR(mnt)) + panic("VFS: Failed to create failfs"); + + failfs_root_path.mnt =3D mnt; + failfs_root_path.dentry =3D mnt->mnt_root; +} diff --git a/fs/internal.h b/fs/internal.h index 355d93f92208..ce7f12c5a65b 100644 --- a/fs/internal.h +++ b/fs/internal.h @@ -362,3 +362,6 @@ 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); diff --git a/fs/namespace.c b/fs/namespace.c index 3d5cd5bf3b05..87c365f2f82b 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(); } =20 void put_mnt_ns(struct mnt_namespace *ns) diff --git a/include/uapi/linux/magic.h b/include/uapi/linux/magic.h index 4f2da935a76c..fd5f0e95648e 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" */ =20 #endif /* __LINUX_MAGIC_H__ */ --=20 2.53.0 From nobody Fri Jul 24 21:27:05 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-alma10-1.taild15c8.ts.net [100.103.45.18]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 60C50433E73; Fri, 24 Jul 2026 13:41:34 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=100.103.45.18 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784900495; cv=none; b=LrcN3BpUwwjtIyG/3FrpUtrtaQXhRzZAam61T5CwIBXPMhc6UdlneIlpZX83S6SxEY7GmPUdzUO1Y/fIqohF40ZLBJYR6gvc+bcEBSv+SWLKJ5tskunFuac6Fv4CE/pcDLolf3WS/LYr1MNmj+K/FKkYa6Rw64Ag/dhUQC1rrzw= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784900495; c=relaxed/simple; bh=YmIJxhHp0eHNnAJj2/En2zWodvQxj7v0+QFVA/CVZrA=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=aM1qdIodAd7IDWuoU/lGqUQ9/xO2STuvOtkNx1lowwHuI/tFAfwoLMAAcZrdrfJjlLK7azSYdlRqj6UancPsj8JUexo2s24UnICLkAdOqjboaETpMvJ9E980ngjnFa9HvnZ4jlB9G2xON4a4c0iI5OX5T37Laflg44XcN/iQm0Q= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=bUc90F67; arc=none smtp.client-ip=100.103.45.18 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="bUc90F67" Received: by smtp.kernel.org (Postfix) with ESMTPSA id 193031F000E9; Fri, 24 Jul 2026 13:41:30 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1784900494; bh=2HSNBPyH8jhy8LlAl6XW15FyikFGFtQMPGouVz7ybkg=; h=From:Date:Subject:References:In-Reply-To:To:Cc; b=bUc90F67OF6EtwJlcBN3CrrFbmvCORcvIV6cPmls7vACl6xS3ryT1JfuVRDBEGkjw zU/IMgbNq0VfbZzWPEQfUrOvATN7MS/H7P3oZ2UI1i29V3jBvSDNNLufGqtNb0VnbL Uf2fqYPJPfzMSD7T53GbdsLUWMU9cjzjN0T3hKP2NfivXzJsK0ssBUWA05t9EuwWYB i7yXc4JhN91pQ7YhfIzE3p5p4Cy6Al7q3Xc+Izz9qgu2Y+RkWwx0DdOspliaaTcVGx kdLNFK3R77i7rEv8D6lCMLF+pEO94Rzie5CrLOZ4wwKEbp5a3ZgI1Am0dQ6BFIY4Ka ocPvNjQJ3vaeg== From: Christian Brauner Date: Fri, 24 Jul 2026 15:41:18 +0200 Subject: [PATCH v2 2/7] fs: support FD_FAILFS_ROOT in fchdir() Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20260724-work-failfs-v2-2-485dabbae185@kernel.org> References: <20260724-work-failfs-v2-0-485dabbae185@kernel.org> In-Reply-To: <20260724-work-failfs-v2-0-485dabbae185@kernel.org> To: linux-fsdevel@vger.kernel.org, Andy Lutomirski , Jann Horn Cc: John Ericson , linux-api@vger.kernel.org, "H. Peter Anvin" , Kees Cook , Farid Zakaria , Alexander Viro , Christian Brauner , Jan Kara , linux-kernel@vger.kernel.org, Jonathan Corbet , linux-doc@vger.kernel.org X-Mailer: b4 0.16-dev-401aa X-Developer-Signature: v=1; a=openpgp-sha256; l=3237; i=brauner@kernel.org; h=from:subject:message-id; bh=YmIJxhHp0eHNnAJj2/En2zWodvQxj7v0+QFVA/CVZrA=; b=owGbwMvMwCU28Zj0gdSKO4sYT6slMWQlZzfz821Lc5894UPYvF8qmQqa703F3ESWigQbu2Z2l PF2n6vtKGVhEONikBVTZHFoNwmXW85TsdkoUwNmDisTyBAGLk4BmEitGsP/iqIYsZTWJj2/onNs t6tVr+c9m9Hkzd8+zXnR5A/rxdN6Gf7HOVyIaD7UFShj8yb9QEILe+hJRSNpza2x7S66KSrnzVg A X-Developer-Key: i=brauner@kernel.org; a=openpgp; fpr=4880B8C9BD0E5106FC070F4F7B3C391EFEA93624 Add a new file descriptor sentinel FD_FAILFS_ROOT following FD_PIDFS_ROOT and FD_NSFS_ROOT and teach fchdir() to accept it. A process calling fchdir(FD_FAILFS_ROOT) moves its working directory into failfs. Every AT_FDCWD-relative lookup afterwards fails with EOPNOTSUPP including "." and ".." and getcwd() reports the working directory as unreachable from the process root by returning a path prefixed with "(unreachable)". Lookups relative to explicit directory file descriptors are unaffected. The sentinel is the only way in. No privilege or gating is required. Setting the working directory to a directory in which every operation fails grants nothing and loses nothing that closing file descriptors couldn't lose. An unlinked working directory behaves the same way today modulo errno. The working directory also plays no role in confining ".." resolution so no boundary is weakened. Signed-off-by: Christian Brauner (Amutable) --- fs/failfs.c | 11 +++++++++++ fs/internal.h | 1 + fs/open.c | 5 ++++- include/uapi/linux/fcntl.h | 1 + 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/fs/failfs.c b/fs/failfs.c index d0ca1fc6c459..66a36da3d236 100644 --- a/fs/failfs.c +++ b/fs/failfs.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include =20 @@ -135,6 +136,16 @@ static int failfs_init_fs_context(struct fs_context *f= c) return 0; } =20 +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 =3D { .name =3D "failfs", .init_fs_context =3D failfs_init_fs_context, diff --git a/fs/internal.h b/fs/internal.h index ce7f12c5a65b..67aa0444351b 100644 --- a/fs/internal.h +++ b/fs/internal.h @@ -365,3 +365,4 @@ 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/open.c b/fs/open.c index 408925d7bd0b..56b6032d4d81 100644 --- a/fs/open.c +++ b/fs/open.c @@ -570,9 +570,12 @@ SYSCALL_DEFINE1(chdir, const char __user *, filename) =20 SYSCALL_DEFINE1(fchdir, unsigned int, fd) { - CLASS(fd_raw, f)(fd); int error; =20 + if ((int)fd =3D=3D FD_FAILFS_ROOT) + return failfs_current_chdir(); + + CLASS(fd_raw, f)(fd); if (fd_empty(f)) return -EBADF; =20 diff --git a/include/uapi/linux/fcntl.h b/include/uapi/linux/fcntl.h index aadfbf6e0cb3..e43e3de3e9ee 100644 --- a/include/uapi/linux/fcntl.h +++ b/include/uapi/linux/fcntl.h @@ -124,6 +124,7 @@ struct delegation { =20 #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 =3D= -10009 */ =20 /* Generic flags for the *at(2) family of syscalls. */ --=20 2.53.0 From nobody Fri Jul 24 21:27:05 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-alma10-1.taild15c8.ts.net [100.103.45.18]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id CDC1E43304B; Fri, 24 Jul 2026 13:41:37 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=100.103.45.18 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784900498; cv=none; b=oizQvk/Y0rqQl4yGk3WVoNN/hfzNqiVNSp29V0DeJSnXuXZDREPlYHEJMC7pjNvNeSIavyKYnd639jLJW7FvUY3Ng7s+ELyZt1/9PHTSAjcJ0rjII9c+ZuhqJZoDwQuNrsFSOn52vpS4YzJROqWl0tejpgMLkPog+3Un68mZXS0= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784900498; c=relaxed/simple; bh=3kJEjg5rDhovEDPR6daxJNj3kC7dNNM37mvNMr4zgTk=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=H6dx7tLtpwDeH5jQqtqeMd7RICgv9AOu9gzvuIyVgunr+3kefXKtw0QfgMC9o56zDL8hhNBAVsUuI1ZwnBXwShP7D4+NtsoHyCUw8pnHbz5jKB+Dv0qQqe8OjokcDoNrfhUMspSYNDkDDpFJgzFXT4OH0ZfhKSNU5YI/+wBTn18= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=KNlPmki9; arc=none smtp.client-ip=100.103.45.18 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="KNlPmki9" Received: by smtp.kernel.org (Postfix) with ESMTPSA id 88D561F00A3A; Fri, 24 Jul 2026 13:41:34 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1784900497; bh=kF5z1zzpmDYjTEkMEPvP/xmtcH6zrKIbvcfMKStmosg=; h=From:Date:Subject:References:In-Reply-To:To:Cc; b=KNlPmki9BEgWPihJFqBMHxUaH6eBjJCL++woV1WhqhvwzVcWUsgJUdRLn+2PhRI1/ Z2qbw5703RjfPmJqTtf44L0HxSunRgDrfMC0JrQnN0nC4nNgG3LavDY2nEXWyidIqA 8cMGMcfjaG6fce8DkbstGeh8APW1Qj2SPyoMwNSd7W3ENzdGahNMfL0XP2PQMNBBAl DY5/qhZe2hziaUY2TNyxn1C1qhB6iYj8h1BP2Yrdi4CiU3SGl5TKuAZLATUhusOqt3 /DEfXJHoYUY4zjmscOMqD0p3S/G+4k2uVdsZge4CZwnVca55zCzPdJK2wxb4gK0Wvn TYVJ82/223PVA== From: Christian Brauner Date: Fri, 24 Jul 2026 15:41:19 +0200 Subject: [PATCH v2 3/7] fs: add fchroot() Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20260724-work-failfs-v2-3-485dabbae185@kernel.org> References: <20260724-work-failfs-v2-0-485dabbae185@kernel.org> In-Reply-To: <20260724-work-failfs-v2-0-485dabbae185@kernel.org> To: linux-fsdevel@vger.kernel.org, Andy Lutomirski , Jann Horn Cc: John Ericson , linux-api@vger.kernel.org, "H. Peter Anvin" , Kees Cook , Farid Zakaria , Alexander Viro , Christian Brauner , Jan Kara , linux-kernel@vger.kernel.org, Jonathan Corbet , linux-doc@vger.kernel.org X-Mailer: b4 0.16-dev-401aa X-Developer-Signature: v=1; a=openpgp-sha256; l=2585; i=brauner@kernel.org; h=from:subject:message-id; bh=3kJEjg5rDhovEDPR6daxJNj3kC7dNNM37mvNMr4zgTk=; b=owGbwMvMwCU28Zj0gdSKO4sYT6slMWQlZzeLxzGfa2Sc7XZmo8NuZ59fBoI/JtWuZek2d9Lxj NMRz33QUcrCIMbFICumyOLQbhIut5ynYrNRpgbMHFYmkCEMXJwCMBFeHoY/vM8T9FRyqzKtt9Vr Xv9kNEVa30iqwStYw0t3WbeM4p0tDP8D97BPNhPYeO67bEL1DddNlk/tdNPXrWZ/LXttk07yqQA mAA== X-Developer-Key: i=brauner@kernel.org; a=openpgp; fpr=4880B8C9BD0E5106FC070F4F7B3C391EFEA93624 Add a file descriptor based counterpart to chroot(2). This has been overdue for a long time. It is the natural companion to fchdir() and avoids re-resolving a path that the caller already holds a file descriptor to. No TOCTOU between resolving the target and changing the root. It composes with modern fd-based APIs meaning it works with O_PATH file descriptors and file descriptors to detached mount trees created via open_tree(OPEN_TREE_CLONE). The permission model is identical to chroot(2). The caller must have CAP_SYS_CHROOT in its user namespace, must pass MAY_EXEC | MAY_CHDIR permission checks on the target directory, and LSMs are consulted via the same security_path_chroot() hook. The system call takes a flags argument for future extensibility which must currently be zero. Signed-off-by: Christian Brauner (Amutable) --- fs/open.c | 29 +++++++++++++++++++++++++++++ include/linux/syscalls.h | 1 + 2 files changed, 30 insertions(+) diff --git a/fs/open.c b/fs/open.c index 56b6032d4d81..c57f641f2e29 100644 --- a/fs/open.c +++ b/fs/open.c @@ -618,6 +618,35 @@ SYSCALL_DEFINE1(chroot, const char __user *, filename) return error; } =20 +SYSCALL_DEFINE2(fchroot, int, fd, unsigned int, flags) +{ + int error; + + if (flags) + return -EINVAL; + + CLASS(fd_raw, f)(fd); + if (fd_empty(f)) + return -EBADF; + + if (!d_can_lookup(fd_file(f)->f_path.dentry)) + return -ENOTDIR; + + error =3D file_permission(fd_file(f), MAY_EXEC | MAY_CHDIR); + if (error) + return error; + + if (!ns_capable(current_user_ns(), CAP_SYS_CHROOT)) + return -EPERM; + + error =3D security_path_chroot(&fd_file(f)->f_path); + if (error) + return error; + + set_fs_root(current->fs, &fd_file(f)->f_path); + return 0; +} + int chmod_common(const struct path *path, umode_t mode) { struct inode *inode =3D path->dentry->d_inode; diff --git a/include/linux/syscalls.h b/include/linux/syscalls.h index 874d9067a43b..8413b624ad47 100644 --- a/include/linux/syscalls.h +++ b/include/linux/syscalls.h @@ -457,6 +457,7 @@ asmlinkage long sys_faccessat2(int dfd, const char __us= er *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); --=20 2.53.0 From nobody Fri Jul 24 21:27:05 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-alma10-1.taild15c8.ts.net [100.103.45.18]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 64ADE437440; Fri, 24 Jul 2026 13:41:41 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=100.103.45.18 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784900502; cv=none; b=LjhbStZ5QPmHtMEE8i8tEIfzI7uQukPYl1gRsROTfeu77nstPe21C8l6zfbaT76z3cD/tsdH/M+O00FtQ9f/3rkvFsIJ1kt8XILAisVI0XTB1IjHzJ2VZkX5SeDTrVUaD5BoIKG8ew3B8JTfhFROYoldSLLkzJ2/54z9qTdejB4= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784900502; c=relaxed/simple; bh=e/PMUsFjYc4cClYq8CIpXiy0k7R//ssIqvGBQ4FVIrE=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=Y45EZHkECDsdY2rm/8EOuJMOybKfdnkNhWLzHCQ9SmxlAYeC7sHPUcJd33mbqeXMTlc5cwzhv/VNEbNUjeqW4zVobsJkL96GzNIgbN5/urALV+V1Vs0J++P3fjNJ6lhV+Ttr5ylKvYq/+gs7wykX7l8YH6f0eD6P+Akf1iFC6z4= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=UlxG9tfp; arc=none smtp.client-ip=100.103.45.18 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="UlxG9tfp" Received: by smtp.kernel.org (Postfix) with ESMTPSA id F3C941F000E9; Fri, 24 Jul 2026 13:41:37 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1784900501; bh=zKWM0iXJn0uuXn3vmCcASaPMe0mI56Y0cEnjxJgGQ9c=; h=From:Date:Subject:References:In-Reply-To:To:Cc; b=UlxG9tfppoWCzLcD5HBMLjylPUkrgvQ530wvoYnuuQ9vOi2f6MG0eMeSxeaXk8R4Q V7kQfs1IPVwpM1dh8URGsXUKIyQsh+Szvojm4sT4Yh9aU4BgDa6VhybTDK+xV6KJZh xdAvKGYqid2JimFHeJF6RL7jVni1SKUODsGsUTrh3Nc2TrJEFd7VGkKTsq0yRnEcSh YzADucnCz74S+4xzkTMD/Z2gM8V9Txzb5ul/2ryDGCsPCHlY+NM+I9YXAL6S8bbNi/ Bx2lyLdYGRwjWstvDqSeOBwttPG8DSY351eb7wpdQEfpHm877gghytylIFja9YP8oA LTxG5hin5LAmw== From: Christian Brauner Date: Fri, 24 Jul 2026 15:41:20 +0200 Subject: [PATCH v2 4/7] fs: support FD_FAILFS_ROOT in fchroot() Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20260724-work-failfs-v2-4-485dabbae185@kernel.org> References: <20260724-work-failfs-v2-0-485dabbae185@kernel.org> In-Reply-To: <20260724-work-failfs-v2-0-485dabbae185@kernel.org> To: linux-fsdevel@vger.kernel.org, Andy Lutomirski , Jann Horn Cc: John Ericson , linux-api@vger.kernel.org, "H. Peter Anvin" , Kees Cook , Farid Zakaria , Alexander Viro , Christian Brauner , Jan Kara , linux-kernel@vger.kernel.org, Jonathan Corbet , linux-doc@vger.kernel.org X-Mailer: b4 0.16-dev-401aa X-Developer-Signature: v=1; a=openpgp-sha256; l=5599; i=brauner@kernel.org; h=from:subject:message-id; bh=e/PMUsFjYc4cClYq8CIpXiy0k7R//ssIqvGBQ4FVIrE=; b=owGbwMvMwCU28Zj0gdSKO4sYT6slMWQlZzfv2ZL/Y4n5Et4ardPR/iqB9x+u0NgusJ09XDdFv f2l42/djlIWBjEuBlkxRRaHdpNwueU8FZuNMjVg5rAygQxh4OIUgIk8+MvwP2eWoea/4gczzRJ2 nV3hv80oJLi50minpBbjJZZrzf3CrIwMDVVGB33kcx51CfIoLH7xePq/z+xmdk25L59cbGBeKHO HDQA= X-Developer-Key: i=brauner@kernel.org; a=openpgp; fpr=4880B8C9BD0E5106FC070F4F7B3C391EFEA93624 Allow a process to move its root directory into failfs via fchroot(FD_FAILFS_ROOT). From that point on every absolute path lookup and every absolute symlink fails with EOPNOTSUPP. Combined with fchdir(FD_FAILFS_ROOT) this leaves the process with lookups anchored at explicit directory file descriptors only. It is the fs_struct equivalent of RESOLVE_BENEATH. This allows taks to drop their filesystem state completely. Callers with CAP_SYS_CHROOT in their user namespace may always do this, mirroring chroot(2). Unprivileged callers are subject to three requirements (which may be loosened later): (1) no_new_privs must be set After entering failfs suid binaries on regular mounts remain reachable via inherited directory file descriptors or the working directory. A setuid program executing with an unusable root directory might be tricked by this. I'm not 100% convinced that this is needed but it feels more secure initially and it also forces more no_new_privs on userspace. So win-win imo. (2) The caller must not already be chrooted. The root directory is what confines .. resolution. The failfs root can never be reached by walking up a real mount tree. A task whose root is failfs has no .. barrier left below the top of its mount tree. A .. walk from any real directory fd it still holds climbs to the mount-namespace root. Which is kinda the point if you want to do fd-based lookup only. If failfs prevented you from doing that then it doesn't make a lot of sense. A task that a privileged manager chrooted into a subtree could use chroot()ing into failfs as a way to allow for an inherited fd to resolve it again. So reject already-chrooted callers closing that issue without losing anything for the intended self-sandboxing use case. (3) The caller must not share its fs_struct. Requirement (1) is checked on the calling thread, but the root lives in the fs_struct which may be shared via CLONE_FS. A sibling thread without no_new_privs could then execute a setuid binary with the failfs root and defeat (1). setns() to a mount or user namespace refuses a shared fs_struct for the same kind of reason, so do the same here and require fs->users =3D=3D 1. no_new_privs is inherited across clone() and can never be cleared, so any CLONE_FS child created afterwards carries it too and the guarantee holds. Privileged callers (CAP_SYS_CHROOT) are not subject to these requirements and may share the fs_struct. They can already chroot and exec a setuid binary today, so failfs hands them nothing new. Backing out is currently hard, but that is a property of the current implementation and not a promise. current_chrooted() treats a failfs root as chrooted so for now the task cannot create user namespaces to regain CAP_SYS_CHROOT and chroot()/fchroot() back out require CAP_SYS_CHROOT. This is not guaranteed though. current_chrooted() may change, or an unprivileged no_new_privs task could be allowed to chroot to a real directory, either of which would loosen this. So don't treat it as a permanent one-way door. The remaining way out today is setns() to a mount namespace file descriptor which requires CAP_SYS_ADMIN over the target namespace plus CAP_SYS_CHROOT and CAP_SYS_ADMIN in the caller's user namespace and resets both root and working directory. A task that closes or never had such file descriptors and restricts *chdir()/*chroot()/setns() via seccomp currently cannot get back out. Signed-off-by: Christian Brauner (Amutable) --- fs/open.c | 47 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/fs/open.c b/fs/open.c index c57f641f2e29..6b1c14e684a9 100644 --- a/fs/open.c +++ b/fs/open.c @@ -620,31 +620,48 @@ SYSCALL_DEFINE1(chroot, const char __user *, filename) =20 SYSCALL_DEFINE2(fchroot, int, fd, unsigned int, flags) { + struct path path; int error; =20 if (flags) return -EINVAL; =20 - CLASS(fd_raw, f)(fd); - if (fd_empty(f)) - return -EBADF; + if (fd =3D=3D 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 !=3D 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; =20 - if (!d_can_lookup(fd_file(f)->f_path.dentry)) - return -ENOTDIR; + if (!d_can_lookup(fd_file(f)->f_path.dentry)) + return -ENOTDIR; =20 - error =3D file_permission(fd_file(f), MAY_EXEC | MAY_CHDIR); - if (error) - return error; + error =3D file_permission(fd_file(f), MAY_EXEC | MAY_CHDIR); + if (error) + return error; =20 - if (!ns_capable(current_user_ns(), CAP_SYS_CHROOT)) - return -EPERM; + if (!ns_capable(current_user_ns(), CAP_SYS_CHROOT)) + return -EPERM; =20 - error =3D security_path_chroot(&fd_file(f)->f_path); - if (error) - return error; + path =3D fd_file(f)->f_path; + path_get(&path); + } =20 - set_fs_root(current->fs, &fd_file(f)->f_path); - return 0; + error =3D security_path_chroot(&path); + if (!error) + set_fs_root(current->fs, &path); + path_put(&path); + return error; } =20 int chmod_common(const struct path *path, umode_t mode) --=20 2.53.0 From nobody Fri Jul 24 21:27:05 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-alma10-1.taild15c8.ts.net [100.103.45.18]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id C0ABA43800A; Fri, 24 Jul 2026 13:41:44 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=100.103.45.18 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784900506; cv=none; b=M4EmbosBy64u/M1hmX6YrrirNW+UkxPALOrleK4sjUu0Ljpc/R/GC5TI4GP/N72vcqY3a60R/IfAIyU0PaDn8rjYCqymEx9PfIO++VIazWDogIlJK+7116ttc9bbw1yKJwvyzLcX8xKu53mzGDsOBkKHuHYK99zxwUNspWvkOGs= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784900506; c=relaxed/simple; bh=3HhnMCJBP4HichaGSP3Y+IbCkr0bwbfl6OQb94jw9Fs=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=n1So1unuipT4aiksROfa+z2KfsFJdzA3/n5I7rl9fIuUoBE3ZuZq3Yy/KtCh4Iz3xY5k1V2d1IAMyb27d4d+ztORepNi6JDqpq3gpz8PMPDvXzY9PvO2aMl0hZwEKulNII/Z+egZ6bLjBH6l4NnsBoZcBthYOSW9nS9I3PmZnGg= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=KEyYrWQo; arc=none smtp.client-ip=100.103.45.18 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="KEyYrWQo" Received: by smtp.kernel.org (Postfix) with ESMTPSA id 6CCC01F00A3A; Fri, 24 Jul 2026 13:41:41 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1784900504; bh=SFBKRAEnjn1ImS4A8J3VZkmnYpUAgHBRGYI2/Z6qcV4=; h=From:Date:Subject:References:In-Reply-To:To:Cc; b=KEyYrWQoXcadjq1Y2vgeweFtEAqJsOFFwIVtgSDlkRK1SyIQerKg564f6MpCLjFq+ L/kb4AHtxP5VBArltjURm6oq+gAtDdFftJC7Uf7SkysIdFmQuaZikzVwsS673pagxY BNDKvwrtrlv3A3o2iNfh9sN43LRc30CVx020Np9W2k4iXYdG/ci3ifpVNaUfqQEzwq fMuEIaKd8ubMqTAkd0qOkDuYX2W1Lt9WxgT6bTRR/Z8KNOF2yrpLIgH2x79rBbvf2C jbuix/zJQ/tUZeeTbJRNS4ubjBeQrFGaJE+VrzAj4WH1wsc4VASE0LMuRyjd9DSf6l XJ/i/OVDZ63wA== From: Christian Brauner Date: Fri, 24 Jul 2026 15:41:21 +0200 Subject: [PATCH v2 5/7] arch: hookup fchroot() system call Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20260724-work-failfs-v2-5-485dabbae185@kernel.org> References: <20260724-work-failfs-v2-0-485dabbae185@kernel.org> In-Reply-To: <20260724-work-failfs-v2-0-485dabbae185@kernel.org> To: linux-fsdevel@vger.kernel.org, Andy Lutomirski , Jann Horn Cc: John Ericson , linux-api@vger.kernel.org, "H. Peter Anvin" , Kees Cook , Farid Zakaria , Alexander Viro , Christian Brauner , Jan Kara , linux-kernel@vger.kernel.org, Jonathan Corbet , linux-doc@vger.kernel.org X-Mailer: b4 0.16-dev-401aa X-Developer-Signature: v=1; a=openpgp-sha256; l=10630; i=brauner@kernel.org; h=from:subject:message-id; bh=3HhnMCJBP4HichaGSP3Y+IbCkr0bwbfl6OQb94jw9Fs=; b=owGbwMvMwCU28Zj0gdSKO4sYT6slMWQlZzc/3Hlu+YxNy4S41rQfeM1/JdTb9ZXZ8Tie4uw5r beNjQ71dZSyMIhxMciKKbI4tJuEyy3nqdhslKkBM4eVCWQIAxenAEzE/zfDH76MwG9Ryau3ON3N 4f97cM/yOKHNsZNSY6TyP1zs0DzqvpThr3TMS9dYXtuDU3azmZubGOS2iKqc3Sqr/a3JwfyBSuR fHgA= X-Developer-Key: i=brauner@kernel.org; a=openpgp; fpr=4880B8C9BD0E5106FC070F4F7B3C391EFEA93624 Wire up the fchroot() system call as number 472 on (nearly) all architectures and sync the mirrored copies of the syscall tables and the asm-generic unistd.h under tools/. Signed-off-by: Christian Brauner (Amutable) --- arch/alpha/kernel/syscalls/syscall.tbl | 1 + arch/arm/tools/syscall.tbl | 1 + arch/arm64/tools/syscall_32.tbl | 1 + arch/m68k/kernel/syscalls/syscall.tbl | 1 + arch/microblaze/kernel/syscalls/syscall.tbl | 1 + arch/mips/kernel/syscalls/syscall_n32.tbl | 1 + arch/mips/kernel/syscalls/syscall_n64.tbl | 1 + arch/mips/kernel/syscalls/syscall_o32.tbl | 1 + arch/parisc/kernel/syscalls/syscall.tbl | 1 + arch/powerpc/kernel/syscalls/syscall.tbl | 1 + arch/s390/kernel/syscalls/syscall.tbl | 1 + arch/sh/kernel/syscalls/syscall.tbl | 1 + arch/sparc/kernel/syscalls/syscall.tbl | 1 + arch/x86/entry/syscalls/syscall_32.tbl | 1 + arch/x86/entry/syscalls/syscall_64.tbl | 1 + arch/xtensa/kernel/syscalls/syscall.tbl | 1 + include/uapi/asm-generic/unistd.h | 6 +++++- scripts/syscall.tbl | 1 + tools/include/uapi/asm-generic/unistd.h | 6 +++++- tools/perf/arch/x86/entry/syscalls/syscall_64.tbl | 1 + tools/scripts/syscall.tbl | 1 + 21 files changed, 29 insertions(+), 2 deletions(-) diff --git a/arch/alpha/kernel/syscalls/syscall.tbl b/arch/alpha/kernel/sys= calls/syscall.tbl index f31b7afffc34..52e3538cc7df 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 94351e22bfcf..55717ed32c27 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 62d93d88e0fe..df2d1d82fb3c 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/sysca= lls/syscall.tbl index 248934257101..ba7a4d8903d0 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 223d26303627..c55a5c96f49b 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/s= yscalls/syscall_n32.tbl index 7430714e2b8f..9ae88e4eac61 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/s= yscalls/syscall_n64.tbl index 630aab9e5425..83dc93a0712f 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/s= yscalls/syscall_o32.tbl index 128653112284..9c62429c9b7b 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/s= yscalls/syscall.tbl index c6331dad9461..88adc4016cce 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 4fcc7c58a105..cfbb70039ff0 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/sysca= lls/syscall.tbl index 09a7ef04d979..1b45e68a217b 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 70b315cbe710..ace068dff0de 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/sys= calls/syscall.tbl index 7e71bf7fcd14..5b9fe0e8140f 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/syscal= ls/syscall_32.tbl index f832ebd2d79b..2c172ef48dfd 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/syscal= ls/syscall_64.tbl index 524155d655da..d5b6045b0090 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 =20 # # Due to a historical design error, certain syscalls are numbered differen= tly diff --git a/arch/xtensa/kernel/syscalls/syscall.tbl b/arch/xtensa/kernel/s= yscalls/syscall.tbl index a9bca4e484de..d354bb231796 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/include/uapi/asm-generic/unistd.h b/include/uapi/asm-generic/u= nistd.h index a627acc8fb5f..5b7e77a7c736 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) =20 +/* fs/open.c */ +#define __NR_fchroot 472 +__SYSCALL(__NR_fchroot, sys_fchroot) + #undef __NR_syscalls -#define __NR_syscalls 472 +#define __NR_syscalls 473 =20 /* * 32 bit systems traditionally used different diff --git a/scripts/syscall.tbl b/scripts/syscall.tbl index 7a42b32b6577..0ab531605120 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/a= sm-generic/unistd.h index a627acc8fb5f..5b7e77a7c736 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) =20 +/* fs/open.c */ +#define __NR_fchroot 472 +__SYSCALL(__NR_fchroot, sys_fchroot) + #undef __NR_syscalls -#define __NR_syscalls 472 +#define __NR_syscalls 473 =20 /* * 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 524155d655da..d5b6045b0090 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 =20 # # Due to a historical design error, certain syscalls are numbered differen= tly diff --git a/tools/scripts/syscall.tbl b/tools/scripts/syscall.tbl index 7a42b32b6577..0ab531605120 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 --=20 2.53.0 From nobody Fri Jul 24 21:27:05 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-alma10-1.taild15c8.ts.net [100.103.45.18]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id C32324399E3; Fri, 24 Jul 2026 13:41:48 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=100.103.45.18 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784900510; cv=none; b=V0PqUu8sgrUa8nxonuuN6yTo8b1SuTwMhYkNHYVen2AEww6rwdITp6TnZxs/DDag5tJyi4t2PIAGoWQxaLcpl61W3jWM+THF9cW/n3y16fAF4biz/W730Tv1nwYiQrbCzfFH0HWc/H7oK1mEe6+Jks1Sf7/zx50cdD5LkbPMjho= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784900510; c=relaxed/simple; bh=CITidaIMWahx5lVp0xMydd68/8kaGjS2CH95QkgRuXI=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=k1FrahAvzr+ZFZmWSRtVHjgcawEtvKcGfqrmY4Td7XhgcqMF1mDHn/Zey/Y018SYcKYYH83U787BVb6SIjW0qh4MpeljAaJ1Pqb91QKq+v74x6eOCLpyp/92DVCp56lvdjq8Hxtqv2zmU3EoQ/srdtVs0ZNfQitHNJutPPysI6c= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=Uq/Jdnmx; arc=none smtp.client-ip=100.103.45.18 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="Uq/Jdnmx" Received: by smtp.kernel.org (Postfix) with ESMTPSA id EA8331F00A3D; Fri, 24 Jul 2026 13:41:44 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1784900508; bh=a1fNVH9it3FnhdyCL3L48qk+7z8DhW4kqeSE8dRCUpY=; h=From:Date:Subject:References:In-Reply-To:To:Cc; b=Uq/JdnmxE8MoRFZimPdWupA1cj7o7G/helil3LZSUbWpYRI7Phjo3eU+d6bwIpGmS aX/24f7UUAx8sXEISHRGFkX+eIrV+rat62f/3M4WJsd9s9wxjmHYKmHxDLVDlswF8r zIFT3e+31yNx75A+u51IZ9uIi8Q4rYUYqqEC35stCQ5x9lDCOHCt8pt+W39cGkxVcM hJX6yUcUKZOjkjUkSsw/fkksKK0gjzx7X6dSDO5UMx9W43aynl92VftOEbqvj49sl+ QTGX8N98u2GVC55NKAbj3Sug3PINElZSAPgyTvXEpReW71LEhSof6Y63a/STk03h7W tZMgyVbffMVWg== From: Christian Brauner Date: Fri, 24 Jul 2026 15:41:22 +0200 Subject: [PATCH v2 6/7] selftests/filesystems: add failfs selftests Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20260724-work-failfs-v2-6-485dabbae185@kernel.org> References: <20260724-work-failfs-v2-0-485dabbae185@kernel.org> In-Reply-To: <20260724-work-failfs-v2-0-485dabbae185@kernel.org> To: linux-fsdevel@vger.kernel.org, Andy Lutomirski , Jann Horn Cc: John Ericson , linux-api@vger.kernel.org, "H. Peter Anvin" , Kees Cook , Farid Zakaria , Alexander Viro , Christian Brauner , Jan Kara , linux-kernel@vger.kernel.org, Jonathan Corbet , linux-doc@vger.kernel.org X-Mailer: b4 0.16-dev-401aa X-Developer-Signature: v=1; a=openpgp-sha256; l=20035; i=brauner@kernel.org; h=from:subject:message-id; bh=CITidaIMWahx5lVp0xMydd68/8kaGjS2CH95QkgRuXI=; b=owGbwMvMwCU28Zj0gdSKO4sYT6slMWQlZze7L53zgePpxynHsrp01198kPRQyH92ZY16SEUpc /Lns0sYO0pZGMS4GGTFFFkc2k3C5ZbzVGw2ytSAmcPKBDKEgYtTACYyV5XhN+vWmWnHlma8+vJ+ 0sn1yfker0OczdQYuzZ/kWWM+vedq4mRYanYzXksh+/mh11Qd1zFZlm0acaEp3Z1xzsKf25zyXd eyAYA X-Developer-Key: i=brauner@kernel.org; a=openpgp; fpr=4880B8C9BD0E5106FC070F4F7B3C391EFEA93624 Test the failfs semantics and both new entry points: - fchdir(FD_FAILFS_ROOT): * working directory lookups and getcwd() fail * other sentinels are rejected * the state is recoverable while the root is untouched - fchroot() with regular fds: * chroot parity * CAP_SYS_CHROOT required * ENOTDIR/EBADF/EINVAL checks - fchroot(FD_FAILFS_ROOT): * absolute lookups, stat, statfs and opens of the root including O_PATH f= ail with EOPNOTSUPP * dirfd-anchored I/O keeps working * ".." walks clamp at the top of the mount tree * /proc magic links resolve but can't be stat through * absolute symlinks fail while relative symlinks keep resolving - Unprivileged entry requires no_new_privs and is rejected for chrooted callers and for a shared fs_struct - entering makes the task count as chrooted so user namespace creation fails - Nothing can be mounted on top of failfs and OPEN_TREE_CLONE is rejected; the overmount test runs in a private mount namespace so a regression cannot touch the host root - setns() to a kept mount namespace fd restores root and working directory - The failfs root is inherited across fork() and absolute exec fails - Exec by fd of a dynamically linked binary fails on opening its absolute PT_INTERP interpreter The exec tests run the exec in a child so a wrongly successful exec cannot replace the test image and masquerade as a pass. Signed-off-by: Christian Brauner (Amutable) --- tools/testing/selftests/Makefile | 1 + .../selftests/filesystems/failfs/.gitignore | 2 + .../testing/selftests/filesystems/failfs/Makefile | 5 + .../selftests/filesystems/failfs/failfs_test.c | 581 +++++++++++++++++= ++++ 4 files changed, 589 insertions(+) diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Mak= efile index 8d4db2241cc2..f87167bcf582 100644 --- a/tools/testing/selftests/Makefile +++ b/tools/testing/selftests/Makefile @@ -33,6 +33,7 @@ TARGETS +=3D fchmodat2 TARGETS +=3D filesystems TARGETS +=3D filesystems/binderfs TARGETS +=3D filesystems/epoll +TARGETS +=3D filesystems/failfs TARGETS +=3D filesystems/fat TARGETS +=3D filesystems/overlayfs TARGETS +=3D filesystems/statmount diff --git a/tools/testing/selftests/filesystems/failfs/.gitignore b/tools/= testing/selftests/filesystems/failfs/.gitignore new file mode 100644 index 000000000000..cd3b5d884d7e --- /dev/null +++ b/tools/testing/selftests/filesystems/failfs/.gitignore @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-only +failfs_test diff --git a/tools/testing/selftests/filesystems/failfs/Makefile b/tools/te= sting/selftests/filesystems/failfs/Makefile new file mode 100644 index 000000000000..3c5d98b4fe72 --- /dev/null +++ b/tools/testing/selftests/filesystems/failfs/Makefile @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: GPL-2.0 +CFLAGS +=3D -Wall -O2 -g $(KHDR_INCLUDES) +TEST_GEN_PROGS :=3D failfs_test + +include ../../lib.mk diff --git a/tools/testing/selftests/filesystems/failfs/failfs_test.c b/too= ls/testing/selftests/filesystems/failfs/failfs_test.c new file mode 100644 index 000000000000..cc461dc483a9 --- /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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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) !=3D sizeof(ehdr)) + return 0; + if (memcmp(ehdr.e_ident, ELFMAG, SELFMAG) !=3D 0) + return 0; + + for (i =3D 0; i < ehdr.e_phnum; i++) { + if (pread(fd, &phdr, sizeof(phdr), + ehdr.e_phoff + i * sizeof(phdr)) !=3D sizeof(phdr)) + return 0; + if (phdr.p_type !=3D PT_INTERP) + continue; + if (pread(fd, &interp, 1, phdr.p_offset) !=3D 1) + return 0; + return interp =3D=3D '/'; + } + + 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 =3D 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 =3D 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 =3D 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() =3D=3D 0) + ASSERT_EQ(drop_to_nobody(), 0); + + fd =3D 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[] =3D "/tmp/failfs_test.XXXXXX"; + char path[PATH_MAX]; + struct stat st; + int tmpfd, dfd, fd; + + if (geteuid() !=3D 0) + SKIP(return, "fchroot() with a regular fd requires CAP_SYS_CHROOT"); + + tmpfd =3D open("/tmp", O_PATH | O_DIRECTORY); + ASSERT_GE(tmpfd, 0); + + ASSERT_NE(mkdtemp(template), NULL); + snprintf(path, sizeof(path), "%s/canary", template); + fd =3D open(path, O_WRONLY | O_CREAT, 0600); + ASSERT_GE(fd, 0); + ASSERT_EQ(close(fd), 0); + + dfd =3D 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[] =3D "/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() !=3D 0) + SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT= "); + + ASSERT_EQ(stat("/", &realroot), 0); + procfd =3D open("/proc", O_PATH | O_DIRECTORY); + ASSERT_GE(procfd, 0); + tmpfd =3D open("/tmp", O_PATH | O_DIRECTORY); + ASSERT_GE(tmpfd, 0); + ASSERT_NE(mkdtemp(template), NULL); + dfd =3D 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 =3D 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 =3D openat(AT_FDCWD, ".", O_RDONLY | O_DIRECTORY); + ASSERT_GE(fd, 0); + ASSERT_EQ(close(fd), 0); + fd =3D 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 =3D 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 =3D 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 =3D readlinkat(procfd, "self/root", buf, sizeof(buf) - 1); + ASSERT_GT(ret, 0); + buf[ret] =3D '\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[] =3D "/tmp/failfs_test.XXXXXX"; + int tmpfd, dfd, fd; + + if (geteuid() !=3D 0) + SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT= "); + + tmpfd =3D open("/tmp", O_PATH | O_DIRECTORY); + ASSERT_GE(tmpfd, 0); + ASSERT_NE(mkdtemp(template), NULL); + dfd =3D open(template, O_RDONLY | O_DIRECTORY); + ASSERT_GE(dfd, 0); + + fd =3D 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 =3D 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() =3D=3D 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[] =3D "/tmp/failfs_test.XXXXXX"; + int tmpfd; + + if (geteuid() !=3D 0) + SKIP(return, "chroot() requires CAP_SYS_CHROOT"); + + tmpfd =3D 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() =3D=3D 0) + ASSERT_EQ(drop_to_nobody(), 0); + + /* A CLONE_FS sibling shares the fs_struct: bump fs->users to 2. */ + pid =3D 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() !=3D 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() !=3D 0) + SKIP(return, "setns() to a mount namespace requires privileges"); + + ASSERT_EQ(stat("/", &realroot), 0); + nsfd =3D 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() !=3D 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 =3D fork(); + ASSERT_GE(pid, 0); + if (pid =3D=3D 0) { + execl("/bin/true", "true", NULL); + _exit(errno =3D=3D 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[] =3D { "failfs_test", NULL }; + static const char * const envp[] =3D { NULL }; + pid_t pid; + int status, exefd; + + if (geteuid() !=3D 0) + SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT= "); + + /* Exec ourselves: the one binary guaranteed to be around. */ + exefd =3D 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 =3D fork(); + ASSERT_GE(pid, 0); + if (pid =3D=3D 0) { + syscall(__NR_execveat, exefd, "", argv, envp, AT_EMPTY_PATH); + _exit(errno =3D=3D 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() !=3D 0) + SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT= "); + + ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0); + + pid =3D fork(); + ASSERT_GE(pid, 0); + if (pid =3D=3D 0) { + if (open("/etc", O_PATH) !=3D -1 || errno !=3D EOPNOTSUPP) + _exit(1); + _exit(0); + } + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_TRUE(WIFEXITED(status)); + ASSERT_EQ(WEXITSTATUS(status), 0); +} + +TEST_HARNESS_MAIN --=20 2.53.0 From nobody Fri Jul 24 21:27:05 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-alma10-1.taild15c8.ts.net [100.103.45.18]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 8C5C2439F83; Fri, 24 Jul 2026 13:41:52 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=100.103.45.18 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784900513; cv=none; b=luDkun2Ls94jF/qtvqtqh5GNlYOyDcU7b1ZUTMBiSUtGoRwcuJf67vZy0AtCcc2RVvceLtrcx17anB1oQy86dccdN0bLJXWGyh9HMaIEbgqAtwJw42RjSxx8axDKw3rbJ4lhCDbzAedUwb1wIWQ2E6my0NiKy01z6puzsC+J4vk= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784900513; c=relaxed/simple; bh=LUhZ/dE9W3vccxYhfKIt3x4xBo6p31C7wEWZCYOVwgw=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=mHCJ1+Uur+xWxC8aVAHMAcIBm6xz6nkY5rzPVE3FmwAUcFHAUu6/gsXfnBBVOPhZuCQLSczeSXaOG+3CMECNk24mHT154KDqXP3SYwXqRIbY6/WQ9TkOJQfKU560kSsA0Ot0T0y6rKNWN6r2rqXL1Lv85ELdGeyXr9tEn9XNCqA= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=WLwPIYBP; arc=none smtp.client-ip=100.103.45.18 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="WLwPIYBP" Received: by smtp.kernel.org (Postfix) with ESMTPSA id 2CAED1F000E9; Fri, 24 Jul 2026 13:41:48 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1784900512; bh=UmJ1QzgBu1BNHHfA25a21HVe5z922jx4PkAOoCMcaSI=; h=From:Date:Subject:References:In-Reply-To:To:Cc; b=WLwPIYBPTlH4lfJZVCZ0CEJJAUM6OD+pgVtnBZYIx28BJXS/4tGPvBRlHeTT5VzYu Ah1EkyGtvpJ0z4WrP5+m6kEE2qVk6h5+QJ2vkVuDT6LUvKA9Hl1HKro7NTIBS2mIm+ 6cmRtJlRDIlOPVgq1f7zifeINuzB9wYiZ0cccVgoqkQ3mskJ6kpCnxpvNaVfJH4Chs LC7S2k4IgwXM6vVTixBvLByiRvpWaxqIU4UPbA9l42u6JeCdq/DC4G+Zp9mSZYsaxW 0zybJHroUgpGjrrCy7XU6/JWA1XyllMvOCbNm5Fp94dQsAaJh0n5+UkZIk1Td8ztG2 gC8R1ch/UT68g== From: Christian Brauner Date: Fri, 24 Jul 2026 15:41:23 +0200 Subject: [PATCH v2 7/7] Documentation: add failfs documentation Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20260724-work-failfs-v2-7-485dabbae185@kernel.org> References: <20260724-work-failfs-v2-0-485dabbae185@kernel.org> In-Reply-To: <20260724-work-failfs-v2-0-485dabbae185@kernel.org> To: linux-fsdevel@vger.kernel.org, Andy Lutomirski , Jann Horn Cc: John Ericson , linux-api@vger.kernel.org, "H. Peter Anvin" , Kees Cook , Farid Zakaria , Alexander Viro , Christian Brauner , Jan Kara , linux-kernel@vger.kernel.org, Jonathan Corbet , linux-doc@vger.kernel.org X-Mailer: b4 0.16-dev-401aa X-Developer-Signature: v=1; a=openpgp-sha256; l=4280; i=brauner@kernel.org; h=from:subject:message-id; bh=LUhZ/dE9W3vccxYhfKIt3x4xBo6p31C7wEWZCYOVwgw=; b=kA0DAAoWkcYbwGV43KIByyZiAGpja4Oiy5JAPMpHxKtL3iFsApHcrwMGRZo2wRPc9LVUR87vI Ih1BAAWCgAdFiEEQIc0Vx6nDHizMmkokcYbwGV43KIFAmpja4MACgkQkcYbwGV43KJw2wD/R3KW hVbFAlmdhvJL6vysO2aHnn/7h20Uqs+LP8Q6wHcBALgFcsfDCle89jTLMjA0ECwsCFHpDxXI2VO DTUNV214A X-Developer-Key: i=brauner@kernel.org; a=openpgp; fpr=4880B8C9BD0E5106FC070F4F7B3C391EFEA93624 Document the failfs semantics, the FD_FAILFS_ROOT sentinel, the fchroot() entry requirements, and the ways back out. Signed-off-by: Christian Brauner (Amutable) --- Documentation/filesystems/failfs.rst | 73 ++++++++++++++++++++++++++++++++= ++++ Documentation/filesystems/index.rst | 1 + 2 files changed, 74 insertions(+) diff --git a/Documentation/filesystems/failfs.rst b/Documentation/filesyste= ms/failfs.rst new file mode 100644 index 000000000000..21ff2db7941d --- /dev/null +++ b/Documentation/filesystems/failfs.rst @@ -0,0 +1,73 @@ +.. SPDX-License-Identifier: GPL-2.0 + +=3D=3D=3D=3D=3D=3D +failfs +=3D=3D=3D=3D=3D=3D + +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 wh= ich +is understood by ``fchdir(2)`` and ``fchroot(2)``. + +Semantics +=3D=3D=3D=3D=3D=3D=3D=3D=3D + +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 +=3D=3D=3D=3D=3D=3D=3D=3D + +``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 =3D= =3D + 1``, the same restriction ``setns(2)`` applies for the mount and user + namespaces. + +Leaving +=3D=3D=3D=3D=3D=3D=3D + +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/filesystem= s/index.rst index 1f71cf159547..734a45e51667 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 --=20 2.53.0