All of lore.kernel.org
 help / color / mirror / Atom feed
From: Christian Brauner <brauner@kernel.org>
To: linux-fsdevel@vger.kernel.org, Andy Lutomirski <luto@kernel.org>,
	 Jann Horn <jannh@google.com>
Cc: John Ericson <mail@johnericson.me>,
	linux-api@vger.kernel.org,  "H. Peter Anvin" <hpa@zytor.com>,
	Kees Cook <kees@kernel.org>,
	 Farid Zakaria <farid.m.zakaria@gmail.com>,
	 Alexander Viro <viro@zeniv.linux.org.uk>,
	 Christian Brauner <brauner@kernel.org>, Jan Kara <jack@suse.cz>,
	 linux-kernel@vger.kernel.org, Jonathan Corbet <corbet@lwn.net>,
	 linux-doc@vger.kernel.org
Subject: [PATCH v2 4/7] fs: support FD_FAILFS_ROOT in fchroot()
Date: Fri, 24 Jul 2026 15:41:20 +0200	[thread overview]
Message-ID: <20260724-work-failfs-v2-4-485dabbae185@kernel.org> (raw)
In-Reply-To: <20260724-work-failfs-v2-0-485dabbae185@kernel.org>

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 == 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) <brauner@kernel.org>
---
 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)
 
 SYSCALL_DEFINE2(fchroot, int, fd, unsigned int, flags)
 {
+	struct path path;
 	int error;
 
 	if (flags)
 		return -EINVAL;
 
-	CLASS(fd_raw, f)(fd);
-	if (fd_empty(f))
-		return -EBADF;
+	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;
+		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;
+		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;
+		if (!ns_capable(current_user_ns(), CAP_SYS_CHROOT))
+			return -EPERM;
 
-	error = security_path_chroot(&fd_file(f)->f_path);
-	if (error)
-		return error;
+		path = fd_file(f)->f_path;
+		path_get(&path);
+	}
 
-	set_fs_root(current->fs, &fd_file(f)->f_path);
-	return 0;
+	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)

-- 
2.53.0


  parent reply	other threads:[~2026-07-24 13:41 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-24 13:41 [PATCH v2 0/7] fs: add failfs Christian Brauner
2026-07-24 13:41 ` [PATCH v2 1/7] " Christian Brauner
2026-07-24 13:41 ` [PATCH v2 2/7] fs: support FD_FAILFS_ROOT in fchdir() Christian Brauner
2026-07-24 13:41 ` [PATCH v2 3/7] fs: add fchroot() Christian Brauner
2026-07-24 13:41 ` Christian Brauner [this message]
2026-07-24 13:41 ` [PATCH v2 5/7] arch: hookup fchroot() system call Christian Brauner
2026-07-24 13:41 ` [PATCH v2 6/7] selftests/filesystems: add failfs selftests Christian Brauner
2026-07-24 13:41 ` [PATCH v2 7/7] Documentation: add failfs documentation Christian Brauner

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260724-work-failfs-v2-4-485dabbae185@kernel.org \
    --to=brauner@kernel.org \
    --cc=corbet@lwn.net \
    --cc=farid.m.zakaria@gmail.com \
    --cc=hpa@zytor.com \
    --cc=jack@suse.cz \
    --cc=jannh@google.com \
    --cc=kees@kernel.org \
    --cc=linux-api@vger.kernel.org \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-fsdevel@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=luto@kernel.org \
    --cc=mail@johnericson.me \
    --cc=viro@zeniv.linux.org.uk \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.