From nobody Thu Sep 24 13:47:13 2026 Received: from m16.mail.163.com (m16.mail.163.com [220.197.31.2]) by smtp.subspace.kernel.org (Postfix) with SMTP id E9B2F53C3A8; Wed, 23 Sep 2026 15:35:40 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=220.197.31.2 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790177746; cv=none; b=qKGuAzkKEDTpPwlnL2bMGeYVQ+wkxjeQgvU1IHRo8+hVmvoB46uemULBk5pXzIFRrqjOuVsWM6WqV/yij1JdVqOgOBgzyWcoVSeFk5Gzph1O4JzcP0vPwdZdhs1AHqMCG/fiV/snj2yxM3XSZX3VgLZJlghUKmonyWddhgkAwYo= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790177746; c=relaxed/simple; bh=FNZti8GdGLWNufxoxx2KKqwkZZrARHf9XewQ2pCl5uY=; h=Date:From:To:Cc:Subject:Content-Type:MIME-Version:Message-ID; b=U2KaDFISvn9WIgCnw6xZLcSGVGkMqd0oJWLKvXzinQzTyqAebRz/VTGpTwt6DJKMtk4XJcSzvzG5Qq2BKw4VP1SaWUf2wvQM2V6LYZwMD6Wn5+a4W/aeHVka0JGCnQSJN4JAeA04zz/b4Air+R3/Ncg51gG8UTe9LdMDmUfFMJo= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=163.com; spf=pass smtp.mailfrom=163.com; dkim=pass (1024-bit key) header.d=163.com header.i=@163.com header.b=N/NRVRoT; arc=none smtp.client-ip=220.197.31.2 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=163.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=163.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=163.com header.i=@163.com header.b="N/NRVRoT" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=163.com; s=s110527; h=Date:From:To:Subject:Content-Type:MIME-Version: Message-ID; bh=FNZti8GdGLWNufxoxx2KKqwkZZrARHf9XewQ2pCl5uY=; b=N /NRVRoT7PMh2lyM4vSWiBndR307UafYLHzfNVI3k9KzKJ32Z4VtpzShcWqBhOwkb fubhIGf79PhF4sxLgtFkFpjlUdddqkCjsaEdknwb0kknhzAcyDlRzh6j7ywd2Bxg kI1CcFAK/11zQJpFYRxQcf98U9ayWkBbBoKJ8+M4OQ= Date: Wed, 23 Sep 2026 23:34:59 +0800 (CST) From: Shawn To: brauner@kernel.org Cc: viro@zeniv.linux.org.uk, linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org Subject: [RFC PATCH] fs: enforce chroot check for failfs entry X-Priority: 3 X-Mailer: Coremail Webmail Server Version 2023.4-cmXT build 20260511(2e539873) Copyright (c) 2002-2026 www.mailtech.cn 163com X-NTES-SC: AL_Qu2SAv2cuE4i7ymZZekdmEgXg+s9XsSysvog1YZXc+EEgQvRxh4Qc0BKOWbG+c+OIDmoixm2ahhryfh4VIZfdbJW1+uvLDx/rGO6h8FYbA== Content-Transfer-Encoding: quoted-printable Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Message-ID: <3bae94c8.a188.1a0cee7e84d.Coremail.shawnq1@163.com> X-Coremail-Locale: zh_CN X-CM-TRANSID: aigvCgD3906k8bNq98xNAA--.43382W X-CM-SenderInfo: pvkd40jtr6il2tof0z/xtbC8gQtO2qz8aSszwAA3P X-Coremail-Antispam: 1U5529EdanIXcx71UUUUU7vcSsGvfC2KfnxnUU== Content-Type: text/plain; charset="utf-8" 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 --- 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=3D0 errno=3D0 ESCAPED: read /tmp/outside.secret =3D "CANARY-OUTSIDE-CONTENT" reached dev=3D3 ino=3D1 (real root dev=3D3 ino=3D1) ATTACK_RESULT=3DCONFINEMENT_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=3D-1 errno=3D1 (Operati= on not permitted) openat(AT_FDCWD, "../../../../..", O_PATH) rc=3D3 errno=3D0 (ok) [climbed out with openat(AT_FDCWD, "..")] confined: cannot read /tmp/outs= ide.secret reached dev=3D29 ino=3D2 (real root dev=3D3 ino=3D1) 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; =20 if (fd =3D=3D 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 !=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 {