* [PATCH 4/4] ntsync: reject wait ioctls with zero owner
From: Elizabeth Figura @ 2026-07-20 17:17 UTC (permalink / raw)
To: Arnd Bergmann, Greg Kroah-Hartman
Cc: linux-kernel, linux-api, wine-devel, linux-kselftest,
Iván Ezequiel Rodriguez, Elizabeth Figura
In-Reply-To: <20260720171740.447035-1-zfigura@codeweavers.com>
From: Iván Ezequiel Rodriguez <ivanrwcm25@gmail.com>
setup_wait() already validates pad and flags but not owner, while
Documentation/userspace-api/ntsync.rst requires EINVAL when owner is
zero. Reject early before queueing waiters.
Signed-off-by: Iván Ezequiel Rodriguez <ivanrwcm25@gmail.com>
Reviewed-by: Elizabeth Figura <zfigura@codeweavers.com>
Signed-off-by: Elizabeth Figura <zfigura@codeweavers.com>
---
drivers/misc/ntsync.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/drivers/misc/ntsync.c b/drivers/misc/ntsync.c
index 02c9d1192812..4a805919bb0c 100644
--- a/drivers/misc/ntsync.c
+++ b/drivers/misc/ntsync.c
@@ -875,6 +875,9 @@ static int setup_wait(struct ntsync_device *dev,
if (args->pad || (args->flags & ~NTSYNC_WAIT_REALTIME))
return -EINVAL;
+ if (!args->owner)
+ return -EINVAL;
+
if (size >= sizeof(fds))
return -EINVAL;
--
2.53.0
^ permalink raw reply related
* Re: PATCH v2 0/2] Power: supply: Add PbAc, NiZn, RAM, and ZnAr support
From: Sebastian Reichel @ 2026-07-20 21:43 UTC (permalink / raw)
To: Sebastian Reichel, Shuah Khan, Boris Shtrasman
Cc: linux-pm, linux-kernel, linux-kselftest, linux-api
In-Reply-To: <20260624135718.286771-1-borissh1983@gmail.com>
On Wed, 24 Jun 2026 16:57:16 +0300, Boris Shtrasman wrote:
> These series adds support for PbAc, NiZn, RAM, and ZnAr chemistries as
> defined in the Smart Battery Data Specification v1.1 (Section 5.1.30
> DeviceChemistry).
>
> Currently, the sbs-battery driver only handles LION, LiP, NiCd and NiMH.
> The Smart Battery specification defines 8 possible values:
> - Lead Acid (PbAc)
> - Lithium Ion (LION)
> - Nickel Cadmium (NiCd)
> - Nickel Metal Hydride (NiMH)
> - Nickel Zinc (NiZn)
> - Rechargeable Alkaline-Manganese (RAM)
> - Zinc Air (ZnAr)
> - Lithium Polymer (LiP)
>
> [...]
Applied, thanks!
[1/2] power: supply: Add PbAc, NiZn, RAM, and ZnAr support
commit: defb072f411c630337946f3babf1bf9b38b23a84
[2/2] power: supply: sbs-battery: Add PbAc, NiZn, RAM, and ZnAr support
commit: 751253c3c3a40c43de8014a9d81e9e9a9f7b1039
Best regards,
--
Sebastian Reichel <sebastian.reichel@collabora.com>
^ permalink raw reply
* Re: [RFC] signal: per-thread control over alternate signal stack delivery for selected signals
From: Tim Parth @ 2026-07-21 9:08 UTC (permalink / raw)
To: linux-api@vger.kernel.org
Cc: linux-kernel@vger.kernel.org, linux-arch@vger.kernel.org,
oleg@redhat.com
In-Reply-To: <AM6PR05MB49032404F2001561036ADA1BE1EE2@AM6PR05MB4903.eurprd05.prod.outlook.com>
Hi,
gentle ping on this RFC after four weeks. Adding Oleg Nesterov to Cc because this concerns signal and sigaltstack semantics.
The concrete question I am trying to resolve is whether the interaction between process-wide SA_ONSTACK and per-thread sigaltstack in processes hosting multiple language runtimes is expected to be handled entirely in userspace.
In the reproducer, Go enables SA_ONSTACK on an existing CoreCLR signal handler, causing CoreCLR's SIGRTMIN handler to run on a 16 KiB alternate stack and overflow it. Increasing the stack is a practical mitigation for this particular case, but does not provide a way for independent runtimes to compose their stack requirements.
Before attempting a proof-of-concept patch, I would appreciate guidance on one point:
Would an opt-in per-thread mechanism for excluding selected signals from SA_ONSTACK delivery be considered a potentially acceptable UAPI direction, or is such a mechanism fundamentally undesirable?
If this discussion belongs on another list or should include particular maintainers, please let me know.
Thanks,
Tim
> -----Ursprüngliche Nachricht-----
> Von: Tim Parth
> Gesendet: Dienstag, 23. Juni 2026 08:30
> An: 'linux-api@vger.kernel.org' <linux-api@vger.kernel.org>
> Cc: linux-kernel@vger.kernel.org; linux-arch@vger.kernel.org
> Betreff: [RFC] signal: per-thread control over alternate signal stack delivery for
> selected signals
>
> Hi,
>
> I am looking for guidance on a Linux signal ABI limitation that shows up in
> multi-runtime processes, specifically a .NET host loading a Go c-shared library.
>
> Disclaimer: I am reporting this from the application/runtime integration side,
> not as a kernel developer. I arrived here after tracing crashes in a .NET
> application hosting a Go shared library through several runtime-specific issues,
> reproductions, and analyses. My understanding of the Linux signal subsystem
> and ABI details is therefore limited, and I may be missing important details.
>
> The technical summary below reflects my best understanding of the issue
> based on the referenced investigations. I used AI-assisted editing to help
> structure and clarify this report, but the observations, reproducer, and
> referenced analyses come from the linked investigations.
>
> This is not a claim that the current kernel behavior violates the existing ABI.
> Rather, I believe the current ABI lacks a way for multiple language runtimes in
> the same process to compose their signal and sigaltstack requirements safely.
>
> Observed failure
> ================
>
> A .NET process loads a Go shared library built with -buildmode=c-shared and
> calls it via P/Invoke. Under stress, the process crashes with SIGSEGV while
> CoreCLR is handling SIGRTMIN for runtime activation / GC suspension.
>
> The reproducer is here:
>
> https://github.com/egonelbre/csharp-go-interop-issue/tree/main/dotnet-
> go-reproducer
>
> Related runtime issues:
>
> https://github.com/golang/go/issues/78883
> https://github.com/dotnet/runtime/issues/127320
>
> The .NET-side analysis shows that the crash happens inside CoreCLR's
> inject_activation_handler path. The kernel delivered SIGRTMIN on the thread's
> alternate signal stack, and CoreCLR then ran a call chain deep enough to
> overflow that stack. In the reported case the per-thread alternate stack
> installed by CoreCLR was 16 KiB. Increasing it to around 49 KiB avoids the
> crash in the provided stress test, but that is a runtime-specific mitigation and
> does not address the general ABI composition problem.
>
> Current ABI interaction
> =======================
>
> The problematic interaction is:
>
> 1. Signal disposition, including SA_ONSTACK, is per-process.
> 2. sigaltstack is per-thread.
> 3. On signal delivery, Linux uses the alternate signal stack if the handler has
> SA_ONSTACK and the current thread has an alternate stack.
> 4. The Go runtime documents that non-Go signal handlers must use
> SA_ONSTACK, because Go may be running on limited stacks. For -
> buildmode=c-shared, when Go sees an existing signal handler it may turn on
> SA_ONSTACK and otherwise keep the existing handler.
> 5. CoreCLR has internal signals such as SIGRTMIN whose handlers may need a
> different stack policy or a larger stack budget than the alternate stack currently
> registered on that thread.
>
> The result is that one runtime can make a process-wide SA_ONSTACK decision
> that affects handlers and threads owned by another runtime. The other
> runtime can install a larger per-thread sigaltstack, but that becomes an arms
> race and does not give a runtime any way to express which signals should use
> which stack policy on a particular thread.
>
> Why existing mechanisms do not fully solve this
> ===============================================
>
> - Raising SIGSTKSZ or MINSIGSTKSZ does not solve the general issue. The
> kernel can only know the signal frame requirements, not the maximum user-
> space stack consumption of an arbitrary signal handler and everything it calls.
>
> - The kernel cannot automatically extend an alternate signal stack.
>
> - Clearing SA_ONSTACK with sigaction is process-wide and can violate the
> requirements of another runtime, for example Go's requirement that signal
> handlers run on an alternate stack when Go code may be interrupted.
>
> - SS_AUTODISARM helps with a different class of problems, such as avoiding
> corruption when switching away from a signal handler, but it does not let a
> thread express "use an alternate stack for SIGSEGV but not for this runtime-
> internal suspension signal", nor does it provide separate stack policies for
> different signals.
>
> Possible ABI direction
> ======================
>
> One possible direction would be an opt-in, per-thread signal-altstack policy,
> for example a prctl() or similar interface that lets a thread provide a signal mask
> for which SA_ONSTACK should be ignored on that thread:
> PR_SET_SIGALTSTACK_EXCLUDE_MASK(sigset_t *mask, size_t sigsetsize)
>
> The default mask would be empty, preserving current behavior. Signal delivery
> would then become, conceptually:
>
> if (handler_has_SA_ONSTACK &&
> thread_has_altstack &&
> !signal_is_in_current_thread_altstack_exclude_mask)
> deliver_on_altstack;
> else
> deliver_on_normal_stack;
>
> This is only a sketch. I am not attached to this exact interface. Another shape
> might be preferable, such as a more general per-thread/per-signal alternate
> stack policy or a way to associate alternate stack requirements with particular
> signals.
>
> Questions
> =========
>
> 1. Is the signal maintainers' view that multi-runtime processes should solve
> this entirely in userspace by agreeing on one sufficiently large per-thread
> sigaltstack?
>
> 2. Would a per-thread/per-signal opt-in policy for alternate signal stack
> delivery be considered acceptable as a Linux UAPI extension?
>
> 3. If such a UAPI is plausible, is prctl() the right place, or would maintainers
> prefer a different interface?
>
> 4. Which subsystem/list should own this discussion? I am sending this first to
> linux-api and LKML because this appears to be a userspace ABI issue around
> signal delivery.
>
> Environment from the reproducer report
> ======================================
>
> - Architecture: x86_64
> - OS: Linux
> - Example distro: Ubuntu 24.04
> - Go: go1.26.2 linux/amd64
> - .NET: 10.0.6 and runtime main were tested in the linked report
> - Signal involved in the reproducer: SIGRTMIN
> - Failure mode: SIGSEGV while running CoreCLR activation handling on the
> alternate signal stack
>
> Thanks,
>
> Tim Parth
^ permalink raw reply
* Re: [PATCH] drm/fourcc: Add P212, P410 and P412 formats
From: Jonah Walker @ 2026-07-22 4:15 UTC (permalink / raw)
To: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, linux-api
In-Reply-To: <20260702212355.1072804-1-jonahw@nvidia.com>
Hello,
Friendly ping -- any comments for this patch?
https://lore.kernel.org/all/20260702212355.1072804-1-jonahw@nvidia.com/
Thanks,
Jonah
^ permalink raw reply
* Re: [RFC PATCH] fs: allow opening overlayfs/erofs layers through O_ALT
From: Christian Brauner @ 2026-07-22 14:25 UTC (permalink / raw)
To: Miklos Szeredi
Cc: Andy Lutomirski, Miklos Szeredi, Giuseppe Scrivano, linux-fsdevel,
linux-unionfs, linux-api, linux-erofs, Amir Goldstein, Gao Xiang
In-Reply-To: <CAJfpeguxCOw4cURjPVOMj5-A7JnLrmy8zx4qE9uwxg7x_Wr+zg@mail.gmail.com>
On 2026-07-20 11:22 +0200, Miklos Szeredi wrote:
> On Sun, 19 Jul 2026 at 16:13, Andy Lutomirski <luto@amacapital.net> wrote:
>
> > This gives me the willies a bit. It's very cool, but it has some
> > potential security issues that I think we need to watch out for. As
> > sort-of prior art, NTFS supports alternate streams and Reiser4, ahem,
> > supported files-as-directories.
>
> I realized that we can keep the creepiness factor minimal by
> introducing this first in procfs only. That way zero changes are
> needed in the path lookup code:
>
> sprintf(buf, "/proc/self/fd-alt/%d/%s", base_fd, alt_path);
> alt_fd = open(buf, O_PATH);
I'm still vehemently opposed to stuffing more creepy apis into procfs.
So this gets clear opposition from me.
^ permalink raw reply
* Re: [RFC PATCH] fs: allow opening overlayfs/erofs layers through O_ALT
From: Miklos Szeredi @ 2026-07-22 14:51 UTC (permalink / raw)
To: Christian Brauner
Cc: Andy Lutomirski, Miklos Szeredi, Giuseppe Scrivano, linux-fsdevel,
linux-unionfs, linux-api, linux-erofs, Amir Goldstein, Gao Xiang
In-Reply-To: <20260722-erraten-hinblick-erlesen-2520fd1a3726@brauner>
On Wed, 22 Jul 2026 at 16:25, Christian Brauner <brauner@kernel.org> wrote:
> I'm still vehemently opposed to stuffing more creepy apis into procfs.
> So this gets clear opposition from me.
Let's try to leave out loaded terms from a technical discussion. I
never said this was creepy, Andy was qualifying the O_ALT thing.
Other than that, do you have a specific issue with the proposed interface?
Thanks,
Miklos
^ permalink raw reply
* Re: [RFC PATCH] fs: allow opening overlayfs/erofs layers through O_ALT
From: Christian Brauner @ 2026-07-22 15:44 UTC (permalink / raw)
To: Miklos Szeredi
Cc: Giuseppe Scrivano, linux-fsdevel, linux-unionfs, linux-api,
linux-erofs, Amir Goldstein, Gao Xiang
In-Reply-To: <20260715101107.973997-1-mszeredi@redhat.com>
> This is a prototype patch. Needs to be split up!
>
> 1) O_ALT / OPEN_TREE_ALT
>
> These open an alternative namespace rooted at dfd - instead of resolving
> the path in the real filesystem, it resolves it in a virtual tree that
> exposes metadata about that file.
>
> This is intended to provide an alternative to adding new ioctls:
>
> - provides structured namespace
> - allows accessing data through fs ops
>
> Could call this O_META, but O_ALT seems more generic and there could be
> uses beyond metadata (e.g. seekable, in-place decompression).
>
> This has been discussed previously:
> https://lore.kernel.org/all/CAHk-=wjzLmMRf=QG-n+1HnxWCx4KTQn9+OhVvUSJ=ZCQd6Y1WA@mail.gmail.com/
>
> 2/a) s_op.get_options(), sb_add_option()
>
> .get_options() is a generic version of .show_options().
>
> After converting everyting to seq_show_option() the rest is trivial:
>
> - change first arg to "struct sb_opt_ctx *ctx"
> - replace "seq_show_option(m" with "sb_add_option(ctx"
>
> 2/b) sb_add_option_path()
>
> The last arg is "struct path *". This is for options that designate a
> path (e.g. stacked fs layers).
>
> Will allow opening the path through O_ALT. Ignored when retrieving the
> string value of the option.
>
> 3) MNT_CLONABLE
>
> Allow an internal mount to be cloned. Nsfs and pidfs are already special
> cased, move these over to this flag.
>
> Also allow overlayfs layers to be cloned.
>
> 4) mnt.mnt_devname -> sb.s_devname
>
> This is historical thing, mnt_devname was just copied when cloning the
> mount, sharing the same value for all mounts belonging to a super block.
>
> 5) mount options in metafs
>
> Support retrieving mount options through O_ALT opens:
>
> mount/options/OPT/N: Nth instance of OPT
>
> This will be a symlink pointint to the option value. If the option refers
> to a path (as per 2/b), following the symlink jumps to the given path.
>
> At this point only overlayfs and erofs are converted.
>
> Signed-off-by: Miklos Szeredi <mszeredi@redhat.com>
We've added a primary set of apis to retrieve mount options. I really
dislike the idea of introducing a completely orthogonal layer to do the
same thing (or more) and also make it filesystem based while at it.
The other proposal of moving it into procfs doesn't lower the creepiness
factor. It just paints it a different color. My objection is to the
shape of this not where the entry point lives.
metafs is the same thing in a different color. I really don't see any
way that we would ever merge something like this.
Like, I'm fine having an api where we can use open_tree() to see through
into underlying layers and given the right permission pull a mount out
of it. That seems something we can potentially do.
There's a few issues on top of that. O_ALT is silently ignored unless
you spell the open as openat(dirfd, relative, ...). path_init() only
looks at LOOKUP_ALT in the dirfd branch. open("/abs", O_ALT) and
openat(AT_FDCWD, "rel", O_ALT) fall straight through and open the real
file.
The layer symlinks are an unchecked jump into internal, out-of-namespace
mounts. Following mount/options/upperdir/0 ends in nd_jump_link(). That
doesn't do permission checks and no mnt_ns check.
The overlay upper/lower layers are internal mounts with no mountpoint in
any namespace. So any unprivileged process that can open the overlay
root gets an O_PATH handle to a layer root it has no search-permission
path to and on a mount outside its namespace and can then use it as a
dirfd.
Pulling the mount out is gated by CAP_SYS_ADMIN, but the handle and the
traversal are not. That's exactly Andy's "whose creds" question, it's
inherent to the design, and the procfs variant has it too.
get_options() forks ->show_options() permanently. It's added
alongside the existing method and mnt_show_options() now tries one then
the other.
That leaves the tree split forever or commits us to a flag-day
conversion of everything. The new thig is also heavie and loses the
seq_file overflow-retry. All so the 99% of filesystems that will never
expose a path option can carry a more complicated interface.
mnt_devname to s_devname move is a behavioral change. alloc_super()
records fc->source only on the sb-create path in sget_fc(). Sharing an
existing sb path never updates it. For filesystems that share a
superblock across mounts every mount now reports the first mounter's
source in /proc/self/mountinfo and statmount().
And the smaller details really show how inelegant this model is and why
Al and I have opposed it multiple times before. Scalar options are
modeled as symlinks and non-path options are dangling links whose
readlink still returns the value while for path options readlink returns
the option string but following the link jumps somewhere else entirely.
erofs silently emits a valueless "fsoffset" if the kasprintf fails.
There's no Kconfig at all. metafs is obj-y and kern_mount()ed in every
kernel. I don't want to see this leak into every container going forward
at all with the same hackishness to make it somehow safe in containers.
Like I said before: I'm fine with an API where we can use open_tree() to
see through into underlying layers, and given the right permission pull
a mount out of it. That is a narrow, checkable primitive and it needs
none of this. No new open flag, no second ->show_options, no metafs, no
mnt_devname surgery. That part we can potentially do.
But a generic metadata interface layered on top of yet another magic
filesystem whether it's mounted or hidden behind procfs I feel very
strongly about. I also have great difficulty seeing Al supporting it.
--
^ permalink raw reply
* Re: [RFC PATCH] fs: allow opening overlayfs/erofs layers through O_ALT
From: Andy Lutomirski @ 2026-07-22 16:29 UTC (permalink / raw)
To: Christian Brauner
Cc: Miklos Szeredi, Giuseppe Scrivano, linux-fsdevel, linux-unionfs,
linux-api, linux-erofs, Amir Goldstein, Gao Xiang
In-Reply-To: <20260722-fundort-zogen-kormoran-09fb867f1b59@brauner>
On Wed, Jul 22, 2026 at 9:06 AM Christian Brauner <brauner@kernel.org> wrote:
> Like I said before: I'm fine with an API where we can use open_tree() to
> see through into underlying layers, and given the right permission pull
> a mount out of it. That is a narrow, checkable primitive and it needs
> none of this. No new open flag, no second ->show_options, no metafs, no
> mnt_devname surgery. That part we can potentially do.
What, exactly, do you mean as the distinction between "[seeing]
through into underlying layers" and "[pulling] a mount out of it". I
have a guess, but I'm not convinced that my guess is right.
Other than that question, I think I generally agree with you. The
interesting operations here (getting an fd to something that was
previously inaccessible) are different enough from normal path lookups
that I think they deserve to be explicit syscalls or syscall modes,
not magic links.
--Andy
^ permalink raw reply
* Re: [RFC PATCH] fs: allow opening overlayfs/erofs layers through O_ALT
From: Miklos Szeredi @ 2026-07-23 8:00 UTC (permalink / raw)
To: Christian Brauner
Cc: Miklos Szeredi, Giuseppe Scrivano, linux-fsdevel, linux-unionfs,
linux-api, linux-erofs, Amir Goldstein, Gao Xiang
In-Reply-To: <20260722-fundort-zogen-kormoran-09fb867f1b59@brauner>
On Wed, 22 Jul 2026 at 18:06, Christian Brauner <brauner@kernel.org> wrote:
> We've added a primary set of apis to retrieve mount options. I really
> dislike the idea of introducing a completely orthogonal layer to do the
> same thing (or more) and also make it filesystem based while at it.
Fair enough.
> The other proposal of moving it into procfs doesn't lower the creepiness
> factor. It just paints it a different color. My objection is to the
> shape of this not where the entry point lives.
Also fair point. The reason I like the proc entry point is that it
makes this accessible in the global namespace.
> Like, I'm fine having an api where we can use open_tree() to see through
> into underlying layers and given the right permission pull a mount out
> of it. That seems something we can potentially do.
That's exactly what I was proposing. The "retrieve all mount options"
was a bad idea. Will drop it.
> There's a few issues on top of that. O_ALT is silently ignored unless
> you spell the open as openat(dirfd, relative, ...). path_init() only
> looks at LOOKUP_ALT in the dirfd branch. open("/abs", O_ALT) and
> openat(AT_FDCWD, "rel", O_ALT) fall straight through and open the real
> file.
Good point. It should either reject the non fd case (including plain
open(2) or use cwd as the base. I'm leaning towards the former.
> The layer symlinks are an unchecked jump into internal, out-of-namespace
> mounts. Following mount/options/upperdir/0 ends in nd_jump_link(). That
> doesn't do permission checks and no mnt_ns check.
It should do the permission check, will fix.
It's jumping into an anonymous mount, why should it check mnt_ns?
> The overlay upper/lower layers are internal mounts with no mountpoint in
> any namespace. So any unprivileged process that can open the overlay
> root gets an O_PATH handle to a layer root it has no search-permission
> path to and on a mount outside its namespace and can then use it as a
> dirfd.
It's just missing the permission check in metafs_path_get_link().
> Pulling the mount out is gated by CAP_SYS_ADMIN, but the handle and the
> traversal are not. That's exactly Andy's "whose creds" question, it's
> inherent to the design, and the procfs variant has it too.
Maybe I'm not getting something. Why would the metafs based design
be more limited in how it checks permission and with which creds than
any other interface?
>
> get_options() forks ->show_options() permanently. It's added
> alongside the existing method and mnt_show_options() now tries one then
> the other.
>
> That leaves the tree split forever or commits us to a flag-day
> conversion of everything. The new thig is also heavie and loses the
> seq_file overflow-retry. All so the 99% of filesystems that will never
> expose a path option can carry a more complicated interface.
This is something that would be useful anyway. Would prevent the back
and forth escaping for statmount. I can do a full conversion patch
either with coccinelle or claude in a couple hours. And nothing is
lost, the layer above seq_file is just an abstract class, could make
it call exactly the same seq_file ops as before.
> mnt_devname to s_devname move is a behavioral change. alloc_super()
> records fc->source only on the sb-create path in sget_fc(). Sharing an
> existing sb path never updates it. For filesystems that share a
> superblock across mounts every mount now reports the first mounter's
> source in /proc/self/mountinfo and statmount().
Well, drat. Doesn't really matter, though.
> And the smaller details really show how inelegant this model is and why
> Al and I have opposed it multiple times before. Scalar options are
> modeled as symlinks and non-path options are dangling links whose
> readlink still returns the value while for path options readlink returns
> the option string but following the link jumps somewhere else entirely.
Hey, this is was a prototype to show that this can do what Giuseppe
needs. Not with nicely polished interfaces.
Completely agree with you, that's a shitty interface that I've done in
there. Not proud of it.
But that's completely besides the point. The point is that it would
allow a lot of things to be done more elegantly, without adding
special interfaces all over the place.
> But a generic metadata interface layered on top of yet another magic
> filesystem whether it's mounted or hidden behind procfs I feel very
> strongly about. I also have great difficulty seeing Al supporting it.
Me too, as you have obviously noted.
I'm going offline for two weeks starting this weekend, so no lengthy
and heated discussions for a while.
But I'll be back! HA HA HA ha
Thanks,
Miklos
^ permalink raw reply
* Re: [RFC PATCH] fs: allow opening overlayfs/erofs layers through O_ALT
From: Miklos Szeredi @ 2026-07-23 8:22 UTC (permalink / raw)
To: Andy Lutomirski
Cc: Christian Brauner, Miklos Szeredi, Giuseppe Scrivano,
linux-fsdevel, linux-unionfs, linux-api, linux-erofs,
Amir Goldstein, Gao Xiang
In-Reply-To: <CALCETrU9KdLsvZzUE-5sh7KkP5-cPh=hwErGmN=THsDatFJLfA@mail.gmail.com>
On Wed, 22 Jul 2026 at 18:36, Andy Lutomirski <luto@amacapital.net> wrote:
> Other than that question, I think I generally agree with you. The
> interesting operations here (getting an fd to something that was
> previously inaccessible) are different enough from normal path lookups
> that I think they deserve to be explicit syscalls or syscall modes,
> not magic links.
This is a super specialized use case.
I sort of agree that doing statmount() as a syscall was not a bad
idea. But new syscall for getting a backing layer, that makes sense
on just a couple of filesystems?
And that leaves us with ioctl. And ioclt() returning an open fd has
it's own problems, besides ioctl being a generally bad interface.
And we have all these powerful concepts and interfaces for
filesystems, why the big resistance to actually using them?.
Sure, it's easy to misuse, but I don't yet see why this particular
case would be a misuse. Enlighten me please.
Thanks,
Miklos
^ permalink raw reply
* Re: [RFC PATCH] fs: allow opening overlayfs/erofs layers through O_ALT
From: Andy Lutomirski @ 2026-07-23 9:00 UTC (permalink / raw)
To: Miklos Szeredi
Cc: Christian Brauner, Miklos Szeredi, Giuseppe Scrivano,
linux-fsdevel, linux-unionfs, linux-api, linux-erofs,
Amir Goldstein, Xiang Gao
In-Reply-To: <CAJfpegupwNTaVNZtdQTmvtz9x=ehH59hzsRT7D0=VbhOZvaqrQ@mail.gmail.com>
> On Jul 23, 2026, at 10:22 AM, Miklos Szeredi <miklos@szeredi.hu> wrote:
>
> On Wed, 22 Jul 2026 at 18:36, Andy Lutomirski <luto@amacapital.net> wrote:
>
>> Other than that question, I think I generally agree with you. The
>> interesting operations here (getting an fd to something that was
>> previously inaccessible) are different enough from normal path lookups
>> that I think they deserve to be explicit syscalls or syscall modes,
>> not magic links.
>
> This is a super specialized use case.
>
> I sort of agree that doing statmount() as a syscall was not a bad
> idea. But new syscall for getting a backing layer, that makes sense
> on just a couple of filesystems?
>
> And that leaves us with ioctl. And ioclt() returning an open fd has
> it's own problems, besides ioctl being a generally bad interface.
>
> And we have all these powerful concepts and interfaces for
> filesystems, why the big resistance to actually using them?.
>
> Sure, it's easy to misuse, but I don't yet see why this particular
> case would be a misuse. Enlighten me please.
>
I think there are are a couple of complications involved with open, openat, etc that aren’t present with explicit for-the-purpose syscalls:
- O_XYZ flags to open() are all kinds of awful, for historical reasons that are not fundamental to the concept.
- symlinks. If we have a new API where opening /proc/something/magic/blah can access something that ought to be inaccessible when accessed intentionally and with privilege, a symlink pointing at /proc/something/… can cause the API to be used inadvertently. Admittedly we have this problem with basically all symlinks, so this isn’t exactly unique.
- our nasty fs permission model. We gave a sort of gnarly mix of a bit of fd-based permission and mostly mode/ACL-based permissions for path traversal and opening, and mapping this nicely only new APIs (as opposed to actual files and directories) can have unpleasant results.
Of course, I’m busy arguing (slowly and without a concrete proposal) that we should have proper capability-like fds, and maybe that’s kind of an answer to this:
What if we had an API to get an fd to the “control filesystem” for a superblock, like your O_ALT but as a real syscall or maybe only accessible via one of the newer and less janky open variants? And what if the resulting fd and the filesystem tree it represented had a few properties that made it very different from normal directory fds:
- You cannot mount anything on it or its subdirectories, nor can you open_tree or otherwise mount it anywhere. But you can open_tree the very specific things in it that point outside of the special API (e.g. the overlayfs layers).
- Privilege is fully captured by the original call that gets you the fd. current->cred is not checked when *using* it except to the extent that you might need privileges over your own namespaces to do operations that might affect them.
- You can’t fchdir or (hypothetically) fchroot into it. (Not sure how important this is.)
- Maybe you can’t follow /proc/pid/fd/N links into it either? Or maybe that would break CRIU too badly.
The basic idea here is to try to treat it like an API that happens to use the open machinery but not as part of the filesystem hierarchy.
One could go even farther and try to remove a bunch of the parts that make implementing it tedious. For example, if this hierarchy had “/layers/1/options”, there is really no reason to over open an fd to layers or layers/1, and if we had a readfile syscall there wouldn’t be a reason to open an fd to options either. But maybe this is a silly direction to move in.
> Thanks,
> Miklos
^ permalink raw reply
* Re: [RFC PATCH] fs: allow opening overlayfs/erofs layers through O_ALT
From: Miklos Szeredi @ 2026-07-23 9:19 UTC (permalink / raw)
To: Andy Lutomirski
Cc: Christian Brauner, Miklos Szeredi, Giuseppe Scrivano,
linux-fsdevel, linux-unionfs, linux-api, linux-erofs,
Amir Goldstein, Xiang Gao
In-Reply-To: <6C96C8A7-345E-4388-8EDC-D94E9463D214@amacapital.net>
On Thu, 23 Jul 2026 at 11:01, Andy Lutomirski <luto@amacapital.net> wrote:
> - O_XYZ flags to open() are all kinds of awful, for historical reasons that are not fundamental to the concept.
I'm fine with limiting this to openat2().
> - symlinks. If we have a new API where opening /proc/something/magic/blah can access something that ought to be inaccessible when accessed intentionally and with privilege, a symlink pointing at /proc/something/… can cause the API to be used inadvertently. Admittedly we have this problem with basically all symlinks, so this isn’t exactly unique.
>
> - our nasty fs permission model. We gave a sort of gnarly mix of a bit of fd-based permission and mostly mode/ACL-based permissions for path traversal and opening, and mapping this nicely only new APIs (as opposed to actual files and directories) can have unpleasant results.
Pseudo filesystems don't usually do mode/acl based permission checks.
I don't see any issue with that.
> Of course, I’m busy arguing (slowly and without a concrete proposal) that we should have proper capability-like fds, and maybe that’s kind of an answer to this:
>
> What if we had an API to get an fd to the “control filesystem” for a superblock, like your O_ALT but as a real syscall or maybe only accessible via one of the newer and less janky open variants? And what if the resulting fd and the filesystem tree it represented had a few properties that made it very different from normal directory fds:
>
> - You cannot mount anything on it or its subdirectories, nor can you open_tree or otherwise mount it anywhere. But you can open_tree the very specific things in it that point outside of the special API (e.g. the overlayfs layers).
This patch provides exeactly those properties.
> - Privilege is fully captured by the original call that gets you the fd. current->cred is not checked when *using* it except to the extent that you might need privileges over your own namespaces to do operations that might affect them.
This is trivial to add to metafs: just store a ref to current->cred
besides the path.
Not sure how this security model would work, though.
>
> - You can’t fchdir or (hypothetically) fchroot into it. (Not sure how important this is.)
Not sure if there are internal mounts that have directories? If not,
we can just add a check to fchdir/chroot against mnt_ns being NULL.
> - Maybe you can’t follow /proc/pid/fd/N links into it either? Or maybe that would break CRIU too badly.
>
> The basic idea here is to try to treat it like an API that happens to use the open machinery but not as part of the filesystem hierarchy.
I'm not opposed to limiting this in various ways, though I don't
really see the advantage.
One thing I hate vehemently is directory lseek to a non-zero offset.
I'd happily error out on that one.
Thanks,
Miklos
^ permalink raw reply
* Re: [RFC PATCH] fs: allow opening overlayfs/erofs layers through O_ALT
From: Amir Goldstein @ 2026-07-23 9:20 UTC (permalink / raw)
To: Miklos Szeredi
Cc: Christian Brauner, Miklos Szeredi, Giuseppe Scrivano,
linux-fsdevel, linux-unionfs, linux-api, linux-erofs, Gao Xiang
In-Reply-To: <CAJfpegs9Mzx2bPTmWWzGWv7D2jF5Nspns-U6jf=KeAufiFMXyA@mail.gmail.com>
> > And the smaller details really show how inelegant this model is and why
> > Al and I have opposed it multiple times before. Scalar options are
> > modeled as symlinks and non-path options are dangling links whose
> > readlink still returns the value while for path options readlink returns
> > the option string but following the link jumps somewhere else entirely.
>
> Hey, this is was a prototype to show that this can do what Giuseppe
> needs. Not with nicely polished interfaces.
>
I want to do a sidebar regarding "what Giuseppe needs".
As I wrote I am all for introspection of overlayfs layers, but I was never
fully convinced that getting an open fd was the correct API.
The starting point of the use case is a userspace daemon that wants
to reuse mounted erofs images, which are used as lower layers.
When considering a single daemon (e.g. composefs) this reuse
would be better done completely in userspace without kernel involvement.
The reason for a need for kernel UAPI for introspection of layers was
to allow cross daemon optimizations, so that containerd could reuse
erofs images mounted by composefs.
This optimization works under the assumption that the image file
or the image file hash is a unique identifier of the desired lower layer.
But what if composefs mounted the image with idmapping or some other
property?
And how many container runtimes are there out there?
Is it really worth the elaborate effort to provide introspection
to this level?
My intuition is that introspection should provide static information
like uuid/fsid/fhandle rather than an open fd.
I am willing to be convinced otherwise with the proper arguments.
Thanks,
Amir.
^ permalink raw reply
* [PATCH RFC 0/7] fs: add failfs
From: Christian Brauner @ 2026-07-23 11:30 UTC (permalink / raw)
To: linux-fsdevel, Andy Lutomirski, Jann Horn
Cc: John Ericson, linux-api, H. Peter Anvin, Kees Cook, Farid Zakaria,
Alexander Viro, Christian Brauner, Jan Kara, linux-kernel,
Jonathan Corbet, linux-doc
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.
The root cannot be opened at all 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/<pid>/{root,cwd} magic
link or by mountpoint traversal. The root also refuses
->d_weak_revalidate() which the VFS calls for jumped terminals. That
closes every remaining way to reference it. An O_PATH
open is refused and 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 "/".
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/<pid>/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.
Fun fact, because of how dynamic binary execution work with PT_INTERP
this also currently prevents execution of dynamic binaries because
loaders have absolute paths (see selftests).
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
Christian Brauner (7):
fs: add failfs
fs: add FD_FAILFS_ROOT and support it in fchdir()
fs: add fchroot()
fs: support FD_FAILFS_ROOT in fchroot()
arch: hookup fchroot() system call
selftests/filesystems: add failfs selftests
Documentation: add failfs documentation
Documentation/filesystems/failfs.rst | 64 +++
Documentation/filesystems/index.rst | 1 +
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 +
fs/Makefile | 2 +-
fs/failfs.c | 155 +++++++
fs/internal.h | 3 +
fs/namespace.c | 1 +
fs/open.c | 58 ++-
include/linux/syscalls.h | 1 +
include/uapi/asm-generic/unistd.h | 6 +-
include/uapi/linux/fcntl.h | 1 +
include/uapi/linux/magic.h | 1 +
scripts/syscall.tbl | 1 +
tools/testing/selftests/Makefile | 1 +
.../selftests/filesystems/failfs/.gitignore | 2 +
.../testing/selftests/filesystems/failfs/Makefile | 5 +
.../selftests/filesystems/failfs/failfs_test.c | 506 +++++++++++++++++++++
32 files changed, 821 insertions(+), 3 deletions(-)
---
base-commit: 1590cf0329716306e948a8fc29f1d3ee87d3989f
change-id: 20260723-work-failfs-d86a0c1db48d
^ permalink raw reply
* [PATCH RFC 1/7] fs: add failfs
From: Christian Brauner @ 2026-07-23 11:30 UTC (permalink / raw)
To: linux-fsdevel, Andy Lutomirski, Jann Horn
Cc: John Ericson, linux-api, H. Peter Anvin, Kees Cook, Farid Zakaria,
Alexander Viro, Christian Brauner, Jan Kara, linux-kernel,
Jonathan Corbet, linux-doc
In-Reply-To: <20260723-work-failfs-v1-0-3f69b9a9e958@kernel.org>
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.
The root cannot be opened at all 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/<pid>/{root,cwd} magic
link or by mountpoint traversal. The root also refuses
->d_weak_revalidate() which the VFS calls for jumped terminals. That
closes every remaining way to reference it. An O_PATH
open is refused and 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 "/".
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/<pid>/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) <brauner@kernel.org>
---
fs/Makefile | 2 +-
fs/failfs.c | 144 +++++++++++++++++++++++++++++++++++++++++++++
fs/internal.h | 2 +
fs/namespace.c | 1 +
include/uapi/linux/magic.h | 1 +
5 files changed, 149 insertions(+), 1 deletion(-)
diff --git a/fs/Makefile b/fs/Makefile
index 89a8a9d207d1..73b6cab7738e 100644
--- a/fs/Makefile
+++ b/fs/Makefile
@@ -16,7 +16,7 @@ obj-y := 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
obj-$(CONFIG_BUFFER_HEAD) += buffer.o mpage.o
obj-$(CONFIG_PROC_FS) += proc_namespace.o
diff --git a/fs/failfs.c b/fs/failfs.c
new file mode 100644
index 000000000000..f9d4ba791928
--- /dev/null
+++ b/fs/failfs.c
@@ -0,0 +1,144 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Copyright (c) 2026 Christian Brauner <brauner@kernel.org> */
+#include <linux/fs.h>
+#include <linux/fs/super_types.h>
+#include <linux/fs_context.h>
+#include <linux/magic.h>
+#include <linux/mount.h>
+
+#include "internal.h"
+
+static struct path failfs_root_path = {};
+
+void failfs_get_root(struct path *path)
+{
+ *path = failfs_root_path;
+ path_get(path);
+}
+
+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 *dentry,
+ 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 = {
+ .permission = failfs_permission,
+ .lookup = failfs_lookup,
+ .getattr = failfs_getattr,
+};
+
+static const struct file_operations failfs_dir_operations = {};
+
+static int failfs_d_weak_revalidate(struct dentry *dentry, unsigned int flags)
+{
+ /*
+ * 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/<pid>/{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 const struct dentry_operations failfs_dentry_operations = {
+ .d_weak_revalidate = failfs_d_weak_revalidate,
+};
+
+static int failfs_statfs(struct dentry *dentry, struct kstatfs *buf)
+{
+ return -EOPNOTSUPP;
+}
+
+static const struct super_operations failfs_super_operations = {
+ .statfs = failfs_statfs,
+};
+
+static int failfs_fill_super(struct super_block *s, struct fs_context *fc)
+{
+ struct inode *inode;
+
+ s->s_maxbytes = MAX_LFS_FILESIZE;
+ s->s_blocksize = PAGE_SIZE;
+ s->s_blocksize_bits = PAGE_SHIFT;
+ s->s_magic = FAIL_FS_MAGIC;
+ s->s_op = &failfs_super_operations;
+ s->s_export_op = NULL;
+ s->s_xattr = NULL;
+ s->s_time_gran = 1;
+ s->s_d_flags = 0;
+
+ inode = new_inode(s);
+ if (!inode)
+ return -ENOMEM;
+
+ /* failfs supports no operations... */
+ inode->i_mode = S_IFDIR;
+ set_nlink(inode, 2);
+ inode->i_op = &failfs_dir_inode_operations;
+ inode->i_fop = &failfs_dir_operations;
+ simple_inode_init_ts(inode);
+ inode->i_ino = 1;
+ /* ... and is immutable. */
+ inode->i_flags |= S_IMMUTABLE;
+
+ set_default_d_op(s, &failfs_dentry_operations);
+ s->s_root = 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 = {
+ .get_tree = failfs_get_tree,
+};
+
+static int failfs_init_fs_context(struct fs_context *fc)
+{
+ fc->ops = &failfs_context_ops;
+ fc->global = true;
+ fc->sb_flags |= SB_NOUSER;
+ fc->s_iflags |= SB_I_NOEXEC | SB_I_NODEV;
+ return 0;
+}
+
+static struct file_system_type failfs_fs_type = {
+ .name = "failfs",
+ .init_fs_context = failfs_init_fs_context,
+ .kill_sb = kill_anon_super,
+};
+
+void __init failfs_init(void)
+{
+ struct vfsmount *mnt;
+
+ /* A single instance that is member of no mount namespace. */
+ mnt = kern_mount(&failfs_fs_type);
+ if (IS_ERR(mnt))
+ panic("VFS: Failed to create failfs");
+
+ failfs_root_path.mnt = mnt;
+ failfs_root_path.dentry = mnt->mnt_root;
+}
diff --git a/fs/internal.h b/fs/internal.h
index 355d93f92208..b6d38a5794eb 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -362,3 +362,5 @@ 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);
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();
}
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" */
#endif /* __LINUX_MAGIC_H__ */
--
2.53.0
^ permalink raw reply related
* [PATCH RFC 2/7] fs: add FD_FAILFS_ROOT and support it in fchdir()
From: Christian Brauner @ 2026-07-23 11:30 UTC (permalink / raw)
To: linux-fsdevel, Andy Lutomirski, Jann Horn
Cc: John Ericson, linux-api, H. Peter Anvin, Kees Cook, Farid Zakaria,
Alexander Viro, Christian Brauner, Jan Kara, linux-kernel,
Jonathan Corbet, linux-doc
In-Reply-To: <20260723-work-failfs-v1-0-3f69b9a9e958@kernel.org>
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) <brauner@kernel.org>
---
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 f9d4ba791928..f37e6af9c03c 100644
--- a/fs/failfs.c
+++ b/fs/failfs.c
@@ -3,6 +3,7 @@
#include <linux/fs.h>
#include <linux/fs/super_types.h>
#include <linux/fs_context.h>
+#include <linux/fs_struct.h>
#include <linux/magic.h>
#include <linux/mount.h>
@@ -124,6 +125,16 @@ static int failfs_init_fs_context(struct fs_context *fc)
return 0;
}
+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 = {
.name = "failfs",
.init_fs_context = failfs_init_fs_context,
diff --git a/fs/internal.h b/fs/internal.h
index b6d38a5794eb..b3c88d999dec 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -364,3 +364,4 @@ 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);
+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)
SYSCALL_DEFINE1(fchdir, unsigned int, fd)
{
- CLASS(fd_raw, f)(fd);
int error;
+ if ((int)fd == FD_FAILFS_ROOT)
+ return failfs_current_chdir();
+
+ CLASS(fd_raw, f)(fd);
if (fd_empty(f))
return -EBADF;
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 {
#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 = -10009 */
/* Generic flags for the *at(2) family of syscalls. */
--
2.53.0
^ permalink raw reply related
* [PATCH RFC 3/7] fs: add fchroot()
From: Christian Brauner @ 2026-07-23 11:30 UTC (permalink / raw)
To: linux-fsdevel, Andy Lutomirski, Jann Horn
Cc: John Ericson, linux-api, H. Peter Anvin, Kees Cook, Farid Zakaria,
Alexander Viro, Christian Brauner, Jan Kara, linux-kernel,
Jonathan Corbet, linux-doc
In-Reply-To: <20260723-work-failfs-v1-0-3f69b9a9e958@kernel.org>
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). Rhe 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) <brauner@kernel.org>
---
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;
}
+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 = 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 = 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 = 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 __user *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);
--
2.53.0
^ permalink raw reply related
* [PATCH RFC 4/7] fs: support FD_FAILFS_ROOT in fchroot()
From: Christian Brauner @ 2026-07-23 11:30 UTC (permalink / raw)
To: linux-fsdevel, Andy Lutomirski, Jann Horn
Cc: John Ericson, linux-api, H. Peter Anvin, Kees Cook, Farid Zakaria,
Alexander Viro, Christian Brauner, Jan Kara, linux-kernel,
Jonathan Corbet, linux-doc
In-Reply-To: <20260723-work-failfs-v1-0-3f69b9a9e958@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 two
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.
There's also some thought needed around shared fs_struct state. It's
obviously possible to chroot into failfs with a shared fs_struct if the
caller has CAP_SYS_CHROOT and shares the fs_struct or if the caller is
no_new_privs and shares the fs_struct. The non-chrooted-currently
requirement still applies.
Once entered, failfs is a throw-away-the-key moment. The task is
considered chrooted so it cannot create user namespaces to regain
CAP_SYS_CHROOT. chroot()/fchroot() back out require CAP_SYS_CHROOT.
The only other exit 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 process that closes or never
had such file descriptors and restricts *chdir()/*chroot()/setns() via
seccomp has thrown away the key.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
fs/open.c | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/fs/open.c b/fs/open.c
index c57f641f2e29..8adc9f00889a 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -618,6 +618,27 @@ SYSCALL_DEFINE1(chroot, const char __user *, filename)
return error;
}
+static int fchroot_failfs(void)
+{
+ struct path path;
+ int error;
+
+ if (!ns_capable(current_user_ns(), CAP_SYS_CHROOT)) {
+ if (!task_no_new_privs(current))
+ return -EPERM;
+ /* Moving the root to failfs lifts the old root's ".." barrier. */
+ if (current_chrooted())
+ return -EPERM;
+ }
+
+ failfs_get_root(&path);
+ error = security_path_chroot(&path);
+ if (!error)
+ set_fs_root(current->fs, &path);
+ path_put(&path);
+ return error;
+}
+
SYSCALL_DEFINE2(fchroot, int, fd, unsigned int, flags)
{
int error;
@@ -625,6 +646,9 @@ SYSCALL_DEFINE2(fchroot, int, fd, unsigned int, flags)
if (flags)
return -EINVAL;
+ if (fd == FD_FAILFS_ROOT)
+ return fchroot_failfs();
+
CLASS(fd_raw, f)(fd);
if (fd_empty(f))
return -EBADF;
--
2.53.0
^ permalink raw reply related
* [PATCH RFC 5/7] arch: hookup fchroot() system call
From: Christian Brauner @ 2026-07-23 11:30 UTC (permalink / raw)
To: linux-fsdevel, Andy Lutomirski, Jann Horn
Cc: John Ericson, linux-api, H. Peter Anvin, Kees Cook, Farid Zakaria,
Alexander Viro, Christian Brauner, Jan Kara, linux-kernel,
Jonathan Corbet, linux-doc
In-Reply-To: <20260723-work-failfs-v1-0-3f69b9a9e958@kernel.org>
Wire up the fchroot() system call as number 472 on (nearly) all
architectures.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
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 +
18 files changed, 22 insertions(+), 1 deletion(-)
diff --git a/arch/alpha/kernel/syscalls/syscall.tbl b/arch/alpha/kernel/syscalls/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/syscalls/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/syscalls/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/syscalls/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/syscalls/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/syscalls/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/syscalls/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/syscalls/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/syscalls/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/syscalls/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
#
# Due to a historical design error, certain syscalls are numbered differently
diff --git a/arch/xtensa/kernel/syscalls/syscall.tbl b/arch/xtensa/kernel/syscalls/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/unistd.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)
+/* fs/open.c */
+#define __NR_fchroot 472
+__SYSCALL(__NR_fchroot, sys_fchroot)
+
#undef __NR_syscalls
-#define __NR_syscalls 472
+#define __NR_syscalls 473
/*
* 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
--
2.53.0
^ permalink raw reply related
* [PATCH RFC 6/7] selftests/filesystems: add failfs selftests
From: Christian Brauner @ 2026-07-23 11:30 UTC (permalink / raw)
To: linux-fsdevel, Andy Lutomirski, Jann Horn
Cc: John Ericson, linux-api, H. Peter Anvin, Kees Cook, Farid Zakaria,
Alexander Viro, Christian Brauner, Jan Kara, linux-kernel,
Jonathan Corbet, linux-doc
In-Reply-To: <20260723-work-failfs-v1-0-3f69b9a9e958@kernel.org>
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 fail 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
- 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
- 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
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
tools/testing/selftests/Makefile | 1 +
.../selftests/filesystems/failfs/.gitignore | 2 +
.../testing/selftests/filesystems/failfs/Makefile | 5 +
.../selftests/filesystems/failfs/failfs_test.c | 506 +++++++++++++++++++++
4 files changed, 514 insertions(+)
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 8d4db2241cc2..f87167bcf582 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -33,6 +33,7 @@ TARGETS += fchmodat2
TARGETS += filesystems
TARGETS += filesystems/binderfs
TARGETS += filesystems/epoll
+TARGETS += filesystems/failfs
TARGETS += filesystems/fat
TARGETS += filesystems/overlayfs
TARGETS += 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/testing/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 += -Wall -O2 -g $(KHDR_INCLUDES)
+TEST_GEN_PROGS := failfs_test
+
+include ../../lib.mk
diff --git a/tools/testing/selftests/filesystems/failfs/failfs_test.c b/tools/testing/selftests/filesystems/failfs/failfs_test.c
new file mode 100644
index 000000000000..bb72a3d2102b
--- /dev/null
+++ b/tools/testing/selftests/filesystems/failfs/failfs_test.c
@@ -0,0 +1,506 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <link.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <sys/vfs.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#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
+
+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);
+}
+
+/* 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) != sizeof(ehdr))
+ return 0;
+ if (memcmp(ehdr.e_ident, ELFMAG, SELFMAG) != 0)
+ return 0;
+
+ for (i = 0; i < ehdr.e_phnum; i++) {
+ if (pread(fd, &phdr, sizeof(phdr),
+ ehdr.e_phoff + i * sizeof(phdr)) != sizeof(phdr))
+ return 0;
+ if (phdr.p_type != PT_INTERP)
+ continue;
+ if (pread(fd, &interp, 1, phdr.p_offset) != 1)
+ return 0;
+ return interp == '/';
+ }
+
+ 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 = 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 = 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 = 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() == 0)
+ ASSERT_EQ(drop_to_nobody(), 0);
+
+ fd = 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[] = "/tmp/failfs_test.XXXXXX";
+ char path[PATH_MAX];
+ struct stat st;
+ int tmpfd, dfd, fd;
+
+ if (geteuid() != 0)
+ SKIP(return, "fchroot() with a regular fd requires CAP_SYS_CHROOT");
+
+ tmpfd = open("/tmp", O_PATH | O_DIRECTORY);
+ ASSERT_GE(tmpfd, 0);
+
+ ASSERT_NE(mkdtemp(template), NULL);
+ snprintf(path, sizeof(path), "%s/canary", template);
+ fd = open(path, O_WRONLY | O_CREAT, 0600);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(close(fd), 0);
+
+ dfd = 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[] = "/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() != 0)
+ SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT");
+
+ ASSERT_EQ(stat("/", &realroot), 0);
+ procfd = open("/proc", O_PATH | O_DIRECTORY);
+ ASSERT_GE(procfd, 0);
+ tmpfd = open("/tmp", O_PATH | O_DIRECTORY);
+ ASSERT_GE(tmpfd, 0);
+ ASSERT_NE(mkdtemp(template), NULL);
+ dfd = 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 = 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 = openat(AT_FDCWD, ".", O_RDONLY | O_DIRECTORY);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(close(fd), 0);
+ fd = 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 = 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 = 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 = readlinkat(procfd, "self/root", buf, sizeof(buf) - 1);
+ ASSERT_GT(ret, 0);
+ buf[ret] = '\0';
+ TH_LOG("/proc/self/root points to '%s'", buf);
+
+ /* 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[] = "/tmp/failfs_test.XXXXXX";
+ int tmpfd, dfd, fd;
+
+ if (geteuid() != 0)
+ SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT");
+
+ tmpfd = open("/tmp", O_PATH | O_DIRECTORY);
+ ASSERT_GE(tmpfd, 0);
+ ASSERT_NE(mkdtemp(template), NULL);
+ dfd = open(template, O_RDONLY | O_DIRECTORY);
+ ASSERT_GE(dfd, 0);
+
+ fd = 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 = 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() == 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[] = "/tmp/failfs_test.XXXXXX";
+ int tmpfd;
+
+ if (geteuid() != 0)
+ SKIP(return, "chroot() requires CAP_SYS_CHROOT");
+
+ tmpfd = 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_no_overmount)
+{
+ if (geteuid() != 0)
+ SKIP(return, "mounting requires privileges");
+
+ 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() != 0)
+ SKIP(return, "setns() to a mount namespace requires privileges");
+
+ ASSERT_EQ(stat("/", &realroot), 0);
+ nsfd = 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)
+{
+ if (geteuid() != 0)
+ SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT");
+
+ ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
+
+ ASSERT_EQ(execl("/bin/true", "true", NULL), -1);
+ ASSERT_EQ(errno, EOPNOTSUPP);
+}
+
+TEST(fchroot_sentinel_exec_interpreter)
+{
+ static const char * const argv[] = { "failfs_test", NULL };
+ static const char * const envp[] = { NULL };
+ int exefd;
+
+ if (geteuid() != 0)
+ SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT");
+
+ /* Exec ourselves: the one binary guaranteed to be around. */
+ exefd = 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.
+ */
+ ASSERT_EQ(syscall(__NR_execveat, exefd, "", argv, envp,
+ AT_EMPTY_PATH), -1);
+ ASSERT_EQ(errno, EOPNOTSUPP);
+}
+
+TEST(fchroot_sentinel_inherited)
+{
+ pid_t pid;
+ int status;
+
+ if (geteuid() != 0)
+ SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT");
+
+ ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0);
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0) {
+ if (open("/etc", O_PATH) != -1 || errno != EOPNOTSUPP)
+ _exit(1);
+ _exit(0);
+ }
+ ASSERT_EQ(waitpid(pid, &status, 0), pid);
+ ASSERT_TRUE(WIFEXITED(status));
+ ASSERT_EQ(WEXITSTATUS(status), 0);
+}
+
+TEST_HARNESS_MAIN
--
2.53.0
^ permalink raw reply related
* [PATCH RFC 7/7] Documentation: add failfs documentation
From: Christian Brauner @ 2026-07-23 11:30 UTC (permalink / raw)
To: linux-fsdevel, Andy Lutomirski, Jann Horn
Cc: John Ericson, linux-api, H. Peter Anvin, Kees Cook, Farid Zakaria,
Alexander Viro, Christian Brauner, Jan Kara, linux-kernel,
Jonathan Corbet, linux-doc
In-Reply-To: <20260723-work-failfs-v1-0-3f69b9a9e958@kernel.org>
Document the failfs semantics, the FD_FAILFS_ROOT sentinel, the
fchroot() entry requirements, and the ways back out.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
Documentation/filesystems/failfs.rst | 64 ++++++++++++++++++++++++++++++++++++
Documentation/filesystems/index.rst | 1 +
2 files changed, 65 insertions(+)
diff --git a/Documentation/filesystems/failfs.rst b/Documentation/filesystems/failfs.rst
new file mode 100644
index 000000000000..46d91525916b
--- /dev/null
+++ b/Documentation/filesystems/failfs.rst
@@ -0,0 +1,64 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+======
+failfs
+======
+
+failfs is a kernel-internal filesystem that fails every operation
+reaching it with ``EOPNOTSUPP``. It is the counterpart to nullfs. Where
+nullfs is a 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 which
+is understood by ``fchdir(2)`` and ``fchroot(2)``.
+
+Semantics
+=========
+
+Every path walk of a component through failfs fails with
+``EOPNOTSUPP`` before that component is parsed, including ``.``.
+
+The root itself cannot be opened at all 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
+========
+
+``fchroot(FD_FAILFS_ROOT, 0)`` requires ``CAP_SYS_CHROOT`` in the
+caller's user namespace, mirroring ``chroot(2)``. Unprivileged callers
+may enter if both 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, "..")``.
+
+Leaving
+=======
+
+A process that entered failfs counts as chrooted. It cannot create user
+namespaces to regain ``CAP_SYS_CHROOT``, and ``chroot(2)`` or
+``fchroot(2)`` back out require ``CAP_SYS_CHROOT``. The only other exit
+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 has thrown away the key.
diff --git a/Documentation/filesystems/index.rst b/Documentation/filesystems/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
--
2.53.0
^ permalink raw reply related
* Re: [PATCH RFC 4/7] fs: support FD_FAILFS_ROOT in fchroot()
From: Andy Lutomirski @ 2026-07-23 12:49 UTC (permalink / raw)
To: Christian Brauner
Cc: linux-fsdevel, Andy Lutomirski, Jann Horn, John Ericson,
linux-api, H. Peter Anvin, Kees Cook, Farid Zakaria,
Alexander Viro, Jan Kara, linux-kernel, Jonathan Corbet,
linux-doc
In-Reply-To: <20260723-work-failfs-v1-4-3f69b9a9e958@kernel.org>
On Thu, Jul 23, 2026 at 4:37 AM Christian Brauner <brauner@kernel.org> wrote:
>
> Once entered, failfs is a throw-away-the-key moment. The task is
> considered chrooted so it cannot create user namespaces to regain
> CAP_SYS_CHROOT. chroot()/fchroot() back out require CAP_SYS_CHROOT.
I kind of alluded to this earlier, but I don't think we should promise
this. In particular, I still think we should eventually allow a
non-chrooted process with no_new_privs to chroot to any valid
directory, and I think we should consider changing the definition of
current_chrooted() such that failfs-as-root would cause it to return
false. I'm also not sure why it's useful -- if I want to throw away
the key to accessing the normal contents of my mountns without
changing my mountns, I need to chroot somewhere (like failfs) *and* I
need to somehow prevent myself from getting an fd or a magic link back
to the ordinary mountns contents. If I somehow get such an fd,
preventing my from fchrooting to it doesn't seem to accomplish
anything, since I could traverse the fd with openat, etc just as
easily, or even fchdir to it and use relative paths.
My understanding of the exact workings of the mount tree is possibly
not good enough to specify the semantics I think are correct quite
exactly, but I think a decent approximation is that a task should be
considered to be not chrooted if its root is at the top of its mount
tree or is not in a mount tree at all.
--Andy
^ permalink raw reply
* Re: [PATCH RFC 0/7] fs: add failfs
From: Andy Lutomirski @ 2026-07-23 12:53 UTC (permalink / raw)
To: Christian Brauner
Cc: linux-fsdevel, Andy Lutomirski, Jann Horn, John Ericson,
linux-api, H. Peter Anvin, Kees Cook, Farid Zakaria,
Alexander Viro, Jan Kara, linux-kernel, Jonathan Corbet,
linux-doc
In-Reply-To: <20260723-work-failfs-v1-0-3f69b9a9e958@kernel.org>
On Thu, Jul 23, 2026 at 4:39 AM Christian Brauner <brauner@kernel.org> wrote:
>
[snipped excellent list of things one cannot do with failfs]
> A plain readlink() of
> such a link still works and shows "/".
This seems awkward to me. I feel like ls -ld /proc/PID/{cwd,exe}
should probably show something distinctive instead of "/". Maybe
readlink() should show "[failfs] /" or similar? Both admins or
developers trying to see what's going on and CRIU might want this.
IMO it's too bad that we don't have a nicely parseable readlink output
for oddities like links to deleted files, but oh well.
--Andy
^ permalink raw reply
* Re: [PATCH RFC 4/7] fs: support FD_FAILFS_ROOT in fchroot()
From: Christian Brauner @ 2026-07-23 13:01 UTC (permalink / raw)
To: Andy Lutomirski
Cc: linux-fsdevel, Jann Horn, John Ericson, linux-api, H. Peter Anvin,
Kees Cook, Farid Zakaria, Alexander Viro, Jan Kara, linux-kernel,
Jonathan Corbet, linux-doc
In-Reply-To: <CALCETrVDX-PcSJoHFwymh7d=oUKotJQQuh+AzmsA7YQySBzD3w@mail.gmail.com>
On Thu, Jul 23, 2026 at 05:49:06AM -0700, Andy Lutomirski wrote:
> On Thu, Jul 23, 2026 at 4:37 AM Christian Brauner <brauner@kernel.org> wrote:
> >
> > Once entered, failfs is a throw-away-the-key moment. The task is
> > considered chrooted so it cannot create user namespaces to regain
> > CAP_SYS_CHROOT. chroot()/fchroot() back out require CAP_SYS_CHROOT.
>
> I kind of alluded to this earlier, but I don't think we should promise
> this. In particular, I still think we should eventually allow a
> non-chrooted process with no_new_privs to chroot to any valid
> directory, and I think we should consider changing the definition of
> current_chrooted() such that failfs-as-root would cause it to return
> false. I'm also not sure why it's useful -- if I want to throw away
I had considered that but introducing this change alongside this series
felt too spicy for me.
> the key to accessing the normal contents of my mountns without
> changing my mountns, I need to chroot somewhere (like failfs) *and* I
> need to somehow prevent myself from getting an fd or a magic link back
> to the ordinary mountns contents. If I somehow get such an fd,
> preventing my from fchrooting to it doesn't seem to accomplish
> anything, since I could traverse the fd with openat, etc just as
> easily, or even fchdir to it and use relative paths.
I think I generally agree with you. My main concern here is mostly about
changing very long-standing behavior here and making failfs special here
feels like it could easily misused.
>
> My understanding of the exact workings of the mount tree is possibly
> not good enough to specify the semantics I think are correct quite
> exactly, but I think a decent approximation is that a task should be
> considered to be not chrooted if its root is at the top of its mount
> tree or is not in a mount tree at all.
If we could altering this definition from the patchset I would be in
favor of it. I'm curious what your take is though.
^ permalink raw reply
* Re: [PATCH RFC 4/7] fs: support FD_FAILFS_ROOT in fchroot()
From: Andy Lutomirski @ 2026-07-23 13:50 UTC (permalink / raw)
To: Christian Brauner
Cc: Andy Lutomirski, linux-fsdevel, Jann Horn, John Ericson,
linux-api, H. Peter Anvin, Kees Cook, Farid Zakaria,
Alexander Viro, Jan Kara, linux-kernel, Jonathan Corbet,
linux-doc
In-Reply-To: <20260723-mahlt-abhob-semantik-21254c8d36cb@brauner>
On Thu, Jul 23, 2026 at 6:09 AM Christian Brauner <brauner@kernel.org> wrote:
>
> On Thu, Jul 23, 2026 at 05:49:06AM -0700, Andy Lutomirski wrote:
> > On Thu, Jul 23, 2026 at 4:37 AM Christian Brauner <brauner@kernel.org> wrote:
> > >
> > > Once entered, failfs is a throw-away-the-key moment. The task is
> > > considered chrooted so it cannot create user namespaces to regain
> > > CAP_SYS_CHROOT. chroot()/fchroot() back out require CAP_SYS_CHROOT.
> >
> > I kind of alluded to this earlier, but I don't think we should promise
> > this. In particular, I still think we should eventually allow a
> > non-chrooted process with no_new_privs to chroot to any valid
> > directory, and I think we should consider changing the definition of
> > current_chrooted() such that failfs-as-root would cause it to return
> > false. I'm also not sure why it's useful -- if I want to throw away
>
> I had considered that but introducing this change alongside this series
> felt too spicy for me.
I think I agree. My only actual objection to your series as is is
that your wording seems to make a promise that I think shouldn't be
made.
>
> > the key to accessing the normal contents of my mountns without
> > changing my mountns, I need to chroot somewhere (like failfs) *and* I
> > need to somehow prevent myself from getting an fd or a magic link back
> > to the ordinary mountns contents. If I somehow get such an fd,
> > preventing my from fchrooting to it doesn't seem to accomplish
> > anything, since I could traverse the fd with openat, etc just as
> > easily, or even fchdir to it and use relative paths.
>
> I think I generally agree with you. My main concern here is mostly about
> changing very long-standing behavior here and making failfs special here
> feels like it could easily misused.
I don't think either of us are really suggesting making failfs
special. I agree about the long-standing behavior.
>
> >
> > My understanding of the exact workings of the mount tree is possibly
> > not good enough to specify the semantics I think are correct quite
> > exactly, but I think a decent approximation is that a task should be
> > considered to be not chrooted if its root is at the top of its mount
> > tree or is not in a mount tree at all.
>
> If we could altering this definition from the patchset I would be in
> favor of it. I'm curious what your take is though.
>
I can't quite parse your question.
I have three sort-of-concrete proposals:
(a) current_chrooted returns false if follow_dotdot starting at
current root but with nd->root equal to the namespace root would
succeed and return the current root. I think this is fairly solid.
The main caveat I can think of is that someone could create a detached
mount tree, chroot to its root, then fork and have the child:
- drop privileges
- set no_new_privs
- chroot somewhere else
then the parent or another privileged task mounts that tree somewhere,
and the child could now access the parent of the mountpoint. Any
actual exploit based on this seems farfetched.
(b) separate the concepts of fs-root-for-absolute-paths and
fs-root-for-blocking-dotdot. IMO this actually makes sense but would
would be a much more intrusive change. We would have
fs->root_of_abspaths and fs->dotdot_blocking_root, and fchroot would
only change root_of_abspaths unless a flag is set asking to change
both. (And document that setting that flag is not advised and that
one should create a detached mount tree instead for 90% of use cases.)
And no_new_privs tasks would be allowed to freely change
root_of_abspaths.
I like (b) conceptually in the sense that I wish no one had ever
invented chroot-for-security in the first place. I'm not sure I like
it as an actual practical idea because it has a much larger scope.
Also (a) is almost equivalent as long as no one ever sets the root to
something that isn't the root of a mount tree, and it really would
confuse some people if "/.." was not equivalent to "./".
(c) Add a bit to fs_struct called something like chroot_locked.
chroot() sets the bit if it chroots anywhere other than "/" or its
equivalent. fchroot does not set it unless a flag asking for it is
set. Switching mount namespace clears the bit. If you have
no_new_privs and don't have chroot_locked then you can chroot.
--Andy
^ permalink raw reply
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox