fs/open.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-)
The failfs v2 design permits callers with CAP_SYS_CHROOT in their current
user namespace to enter failfs even when already chrooted. The
current_chrooted() check is only reached when the caller lacks that
capability.
A task can create a user namespace before entering a chroot and retain
CAP_SYS_CHROOT in that namespace. The reproducer demonstrates that such a
task can call fchroot(FD_FAILFS_ROOT), then walk through ".." from its
working directory to the mount-namespace root and read a canary outside the
chroot. The operation does not grant host uid or capabilities.
This RFC applies current_chrooted() to all FD_FAILFS_ROOT callers, including
those with CAP_SYS_CHROOT. This narrows the existing capability exception;
the other restrictions for callers without that capability remain unchanged.
Fixes: b1221afa31cc2 ("fs: support FD_FAILFS_ROOT in fchroot()")
Signed-off-by: shaos <shawnq1@163.com>
---
Tested on: 5dd1818b15d98d4a20806cd00b1b40320b06004f
Linux 7.3.0-rc3
Compiler: gcc (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0,
GNU ld (GNU Binutils for Ubuntu) 2.38
Reproducer (poc.c):
https://gist.githubusercontent.com/shaos26/38ed8d3da6cfb1db3ff18837d1f69ef7/raw/d02cd506e403674ce34be0f7c3c723c64f242d73/poc.c
Kernel config:
https://gist.githubusercontent.com/shaos26/38ed8d3da6cfb1db3ff18837d1f69ef7/raw/d02cd506e403674ce34be0f7c3c723c64f242d73/kernel-config.txt
Complete vulnerable and patched console log:
https://gist.githubusercontent.com/shaos26/38ed8d3da6cfb1db3ff18837d1f69ef7/raw/d02cd506e403674ce34be0f7c3c723c64f242d73/console-output.txt
To reproduce, build the kernel at the tested commit with the config above,
boot it in QEMU with a static BusyBox initramfs that runs poc.c as root, and
let the reproducer create the jail and canary. The attack child drops to uid
65534 before creating its user namespace and attempting the escape.
The vulnerable run reached:
[no fchroot] confined: cannot read /tmp/outside.secret
fchroot(FD_FAILFS_ROOT, 0) rc=0 errno=0
ESCAPED: read /tmp/outside.secret = "CANARY-OUTSIDE-CONTENT"
reached dev=3 ino=1 (real root dev=3 ino=1)
ATTACK_RESULT=CONFINEMENT_ESCAPED
The PoC closes inherited file descriptors before chrooting. The escape uses
the task's working directory and a relative ".." walk, not a pre-opened
directory descriptor outside the jail.
With the patch, the same PoC produced:
fchroot(FD_FAILFS_ROOT, 0) rc=-1 errno=1 (Operation not permitted)
openat(AT_FDCWD, "../../../../..", O_PATH) rc=3 errno=0 (ok)
[climbed out with openat(AT_FDCWD, "..")] confined: cannot read /tmp/outside.secret
reached dev=29 ino=2 (real root dev=3 ino=1)
For the patched A/B run, I applied this diff to the same source tree and
rebuilt with the same config, initramfs, and QEMU command. The relative walk
stayed at the chroot root and the canary remained inaccessible. The
unprivileged chrooted control still returned EPERM, while an unchrooted
caller with no_new_privs could still enter failfs.
git apply --check, checkpatch.pl --no-tree --strict, and
cc -O2 -Wall -Wextra -fsyntax-only poc.c passed.
fs/open.c | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/fs/open.c b/fs/open.c
--- a/fs/open.c
+++ b/fs/open.c
@@ -627,15 +627,15 @@ SYSCALL_DEFINE2(fchroot, int, fd, unsigned int, flags)
return -EINVAL;
if (fd == FD_FAILFS_ROOT) {
+ /* Moving the root to failfs lifts the old root's ".." barrier. */
+ if (current_chrooted())
+ return -EPERM;
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 {
© 2016 - 2026 Red Hat, Inc.