Unprivileged chroot

Started by Mickaël Salaün on Mar 10, 2021 · 2 messages from 1 author · Last reply Mar 10, 2021

Series

1 patch, 1 file

+20 −3

Subsystems (2)

Maintainers (3)

DORMANTno replies SUPERSEDED

Status: Superseded by v5 (posted 2021-03-16). [go to v5]

Revision v2 (5 revisions archived).

Revisions (5)
  1. v1
  2. v2 current [diff against previous]
  3. v3 [diff against previous]
  4. v4 [diff against previous]
  5. v5 [diff against previous]

open the first message on its own page

  1. [PATCH v2 1/1] fs: Allow no_new_privs tasks to call chroot(2)
    Revisions and related patches
    Revisions (5)
    1. v1
    2. v2 current [diff against previous]
    3. v3 [diff against previous]
    4. v4 [diff against previous]
    5. v5 [diff against previous]
Message details and related content
Hi,

This second patch replaces the custom is_path_beneath() with the
existing path_is_under().

The chroot system call is currently limited to be used by processes with
the CAP_SYS_CHROOT capability.  This protects against malicious
procesess willing to trick SUID-like binaries.  The following patch
allows unprivileged users to safely use chroot(2).

This patch is a follow-up of a previous one sent by Andy Lutomirski some
time ago:
https://lore.kernel.org/lkml/0e2f0f54e19bff53a3739ecfddb4ffa9a6dbde4d.1327858005.git.luto@amacapital.net/

This patch can be applied on top of v5.12-rc2 .  I would really
appreciate constructive reviews.

Previous version:
https://lore.kernel.org/r/20210310161000.382796-1-mic@digikod.net

Regards,

Mickaël Salaün (1):
  fs: Allow no_new_privs tasks to call chroot(2)

 fs/open.c | 23 ++++++++++++++++++++---
 1 file changed, 20 insertions(+), 3 deletions(-)


base-commit: a38fd8748464831584a19438cbb3082b5a2dab15
-- 
2.30.2

[PATCH v2 1/1] fs: Allow no_new_privs tasks to call chroot(2)

Message details and related content
From: Mickaël Salaün <redacted>

Being able to easily change root directories enable to ease some
development workflow and can be used as a tool to strengthen
unprivileged security sandboxes.  chroot(2) is not an access-control
mechanism per se, but it can be used to limit the absolute view of the
filesystem, and then limit ways to access data and kernel interfaces
(e.g. /proc, /sys, /dev, etc.).

Users may not wish to expose namespace complexity to potentially
malicious processes, or limit their use because of limited resources.
The chroot feature is much more simple (and limited) than the mount
namespace, but can still be useful.  As for containers, users of
chroot(2) should take care of file descriptors or data accessible by
other means (e.g. current working directory, leaked FDs, passed FDs,
devices, mount points, etc.).  There is a lot of literature that discuss
the limitations of chroot, and users of this feature should be aware of
the multiple ways to bypass it.  Using chroot(2) for security purposes
can make sense if it is combined with other features (e.g. dedicated
user, seccomp, LSM access-controls, etc.).

One could argue that chroot(2) is useless without a properly populated
root hierarchy (i.e. without /dev and /proc).  However, there are
multiple use cases that don't require the chrooting process to create
file hierarchies with special files nor mount points, e.g.:
* A process sandboxing itself, once all its libraries are loaded, may
  not need files other than regular files, or even no file at all.
* Some pre-populated root hierarchies could be used to chroot into,
  provided for instance by development environments or tailored
  distributions.
* Processes executed in a chroot may not require access to these special
  files (e.g. with minimal runtimes, or by emulating some special files
  with a LD_PRELOADed library or seccomp).

Allowing a task to change its own root directory is not a threat to the
system if we can prevent confused deputy attacks, which could be
performed through execution of SUID-like binaries.  This can be
prevented if the calling task sets PR_SET_NO_NEW_PRIVS on itself with
prctl(2).  To only affect this task, its filesystem information must not
be shared with other tasks, which can be achieved by not passing

46 more lines

diff --git a/fs/open.c b/fs/open.c
index e53af13b5835..280dbff25b25 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -22,6 +22,7 @@
 #include <linux/slab.h>
 #include <linux/uaccess.h>
 #include <linux/fs.h>
+#include <linux/path.h>
 #include <linux/personality.h>
 #include <linux/pagemap.h>
 #include <linux/syscalls.h>
@@ -546,15 +547,31 @@ SYSCALL_DEFINE1(chroot, const char __user *, filename)
 	if (error)
 		goto dput_and_out;
 
+	/*
+	 * Changing the root directory for the calling task (and its future
+	 * children) requires that this task has CAP_SYS_CHROOT in its
+	 * namespace, or be running with no_new_privs and not sharing its
+	 * fs_struct and not escaping its current root directory.  As for
+	 * seccomp, checking no_new_privs avoids scenarios where unprivileged
+	 * tasks can affect the behavior of privileged children.  Lock the path
+	 * to protect against TOCTOU race between path_is_under() and
+	 * set_fs_root().  No need to lock the root because it is not possible
+	 * to rename it beneath itself.
+	 */
 	error = -EPERM;
-	if (!ns_capable(current_user_ns(), CAP_SYS_CHROOT))
-		goto dput_and_out;
+	inode_lock(d_inode(path.dentry));
+	if (!ns_capable(current_user_ns(), CAP_SYS_CHROOT) &&
+			!(task_no_new_privs(current) && current->fs->users == 1
+				&& path_is_under(&path, &current->fs->root)))
+		goto unlock_and_out;
 	error = security_path_chroot(&path);
 	if (error)
-		goto dput_and_out;
+		goto unlock_and_out;
 
 	set_fs_root(current->fs, &path);
 	error = 0;
+unlock_and_out:
+	inode_unlock(d_inode(path.dentry));
 dput_and_out:
 	path_put(&path);
 	if (retry_estale(error, lookup_flags)) {
-- 
2.30.2
Keyboard shortcuts
hback out one level
jnext message in thread / next series-diff position
kprevious message in thread / previous series-diff position
ldrill in
Escclose help / fold thread tree
?toggle this help