Linux userland API discussions
 help / color / mirror / Atom feed
* Re: [ANNOUNCE/CFP] Linux Plumbers 2026 Containers and Checkpoint/Restore Microconference
From: Michal Koutný @ 2026-08-05 16:50 UTC (permalink / raw)
  To: cgroups, containers, criu
  Cc: Kamalesh Babulal, bpf, linux-fsdevel, linux-api, linux-integrity,
	lxc-devel, fuse-devel, Stéphane Graber, Mike Rapoport,
	Christian Brauner, Adrian Reber, Kamalesh Babulal
In-Reply-To: <7959ba0b-c39d-4cb3-8269-482d1d593257@gmail.com>

[-- Attachment #1: Type: text/plain, Size: 670 bytes --]

Hello.

On Thu, Jun 25, 2026 at 09:25:58AM +0530, Kamalesh Babulal <kamalesh.babulal@gmail.com> wrote:
> We are pleased to announce the Call for Proposals for the Containers and
> Checkpoint/Restore Microconference[0] at Linux Plumbers Conference 2026,
> taking place in Prague, Czechia, from October 5 to 7, 2026.
...
> Please submit proposals through the LPC 2026 abstracts page by August 7:
> 
>         https://lpc.events/event/20/abstracts/

Just a quick reminder that the CfP for the MC [1] topics is just around
the corner. There is a chance to bring up topics of _your_ liking.

Regards,
Michal


[1] https://lpc.events/event/20/sessions/267/

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 265 bytes --]

^ permalink raw reply

* Re: [RFC PATCH 00/24] pidfd: add a minimal process spawn builder
From: Justin Suess @ 2026-08-04 20:25 UTC (permalink / raw)
  To: Li Chen
  Cc: Christian Brauner, Kees Cook, Gabriel Krisman Bertazi,
	Josh Triplett, Mateusz Guzik, Andy Lutomirski, John Ericson,
	Jonathan Corbet, Shuah Khan, Arnd Bergmann, Oleg Nesterov,
	Andrew Morton, Paul Moore, Eric Paris, Mickaël Salaün,
	Günther Noack, Alexander Viro, Jan Kara, linux-api,
	linux-fsdevel, linux-kernel, linux-kselftest, linux-doc, audit,
	linux-security-module, linux-arch, linux-mm
In-Reply-To: <cover.1784204592.git.me@linux.beauty>

On Thu, Jul 16, 2026 at 10:31:26PM +0800, Li Chen wrote:
> Hi,
> 
> This RFC follows feedback on my earlier spawn_template RFC [1]. That
> proposal made caching the primary interface; this one starts with general
> process construction. Christian suggested a pidfd/pidfs exec builder
> modeled after fsconfig(), with enough semantics for userspace to implement
> posix_spawn() [2], and Kees agreed [3].
> 
> This RFC is based on linux-next next-20260710 and depends on two pidfs
> fixes that I sent separately:
> 
>   * pidfs: preserve thread pidfds reopened by file handle
>     https://lore.kernel.org/all/20260716052726.1032092-1-me@linux.beauty/
>   * pidfs: handle FS_IOC32_GETVERSION in compat ioctl
>     https://lore.kernel.org/all/20260716052822.1034228-1-me@linux.beauty/
> 
> The initial implementation is source-based. The executable path can be
> provided with the final run request:
> 
>     struct pidfd_spawn_run_args run = {
>         .path = (unsigned long)"/usr/bin/rg",
This should probably be an FD for the path.

This way it prevents race conditions over multiple configuration steps.
>         .argv = (unsigned long)argv,
>         .envp = (unsigned long)envp,
>     };
> 
>     fd = pidfd_open(0, PIDFD_EMPTY);
>     pidfd_spawn_run(fd, &run, sizeof(run));
> 
> Alternatively, the path can be staged before the final run step:
> 
>     struct pidfd_spawn_run_args run = {
>         .argv = (unsigned long)argv,
>         .envp = (unsigned long)envp,
>     };
> 
>     fd = pidfd_open(0, PIDFD_EMPTY);
>     pidfd_config(fd, PIDFD_CONFIG_SET_STRING,
>                  PIDFD_CONFIG_KEY_PATH, "/usr/bin/rg", 0);
Same here. Should probably be an FD to the binary instead.

>     pidfd_spawn_run(fd, &run, sizeof(run));
I'm worried this pidfd_spawn_run just adds another varient to the existing
myriad of exec* syscalls we already have. Would it be better to just have this
work through execveat(fd, "", argv, envp, AT_EMPTY_PATH) instead?

(i.e have execveat take a pidfd directly).

Then you can get rid of pidfd_spawn_run which looks almost structurally
identical to execveat (with the argv and envp collapsed).

Justin
> 
> pidfd_open(0, PIDFD_EMPTY) creates a taskless future pidfd with a stable
> pidfs inode, but no task, PID, or process-count charge. pidfd_spawn_run()
> creates the task and PID; after publication the same fd is the child pidfd,
> and a numeric pidfd resolves to the same inode. Live-task operations return
> -ESRCH before publication. A terminal pre-task failure wakes poll/epoll
> with POLLERR | POLLHUP.
> 
> Source-based mode follows posix_spawn-style defaults. At run time it
> samples the caller's cwd, root, umask, fd table, signal dispositions and
> blocked mask, and namespaces.
> Non-FD_CLOEXEC descriptors remain unless an action changes them; file and
> filesystem state are private child copies before actions. Configuration and
> run stay bound to the creating mm_struct, exact credential object, and
> child PID namespace, so SCM_RIGHTS does not delegate launch authority.
> 
> The first authorized run claims the builder before copying its payload, so
> later failures are terminal. Pre-task failure leaves the fd taskless;
> setup or exec failure leaves it as the child pidfd and exits the child with
> status 127. PIDFD_GET_INFO with PIDFD_INFO_EXIT distinguishes them.
> Success returns the positive child PID in the caller's PID namespace.
> 
> The RFC supports ordered DUP2, CLOSE_RANGE, and FCHDIR actions in
> extensible UAPI records. This exercises child-private fd and cwd setup, but
> is not the complete posix_spawn() action or attribute surface.
> 
> Between task publication and successful exec, the child is explicitly
> embryonic and may not have a valid userspace register frame. Ptrace and
> pidfd_getfd() are denied, procfs treats the PID as absent to other tasks,
> and coredump information reports PIDFD_COREDUMP_SKIP. The child can use its
> own proc entries during executable lookup. Setup runs as initial child task
> work, and successful exec releases this state before exec events are
> published.
> 
> Seccomp sees pidfd_spawn_run(), not separate file-action or exec syscalls,
> and cannot inspect the path or action records behind the run pointer. An
> exec-only denylist that allows unknown syscalls therefore does not block
> this initial exec; policy must filter the builder syscall as a unit. Should
> later expansion provide an immutable restriction profile or action mask
> that seccomp can reason about, or is the coarse syscall boundary
> preferable?
> 
> LSM exec checks and inherited seccomp state remain active. Child setup uses
> a dedicated AUDIT_PIDFD_SPAWN transaction, not a synthetic AUDIT_SYSCALL.
> Should it be selected through the source pidfd_spawn_run() exit rule? An
> existing rule naming only execve() or execveat() does not select it.
> For source/child correlation, could an auxiliary record carry
> the source PID plus the stable pidfs inode? An already traced source is
> rejected before the claim; ptrace auto-attach is not implemented.
> 
> The implementation still uses CLONE_VM | CLONE_VFORK plus exec internally.
> Mateusz suggested that an initial implementation might start with vfork to
> get the API off the ground [4]. That is what this RFC does. It does not yet
> construct a pristine target process without first inheriting source state.
> 
> Missing posix_spawn() pieces include open and close file actions, resetids,
> signal masks/defaults, process groups, sessions, scheduler attributes,
> affinity, cgroup placement, PATH lookup/posix_spawnp(), and exec by fd. The
> RFC also does not include pristine/no-source creation or the executable
> metadata/template cache from my earlier work.
> 
> John Ericson described a real, partially initialized process that remains
> unscheduled while callers install its state, and linked an exploratory
> FreeBSD proc_new()/proc_setfd()/proc_start() refactoring [5]. This RFC
> implements the source-based mode first; a lower-authority pristine/no-source
> mode would be explicit follow-up work.
> 
> Direct process construction is not unprecedented. XNU's posix_spawn path
> does not inherit the parent's address space [6], and Windows
> CreateProcess() accepts explicit startup state [7]. Josh's io_uring_spawn
> LPC slides list "set up process from scratch" as future work and provide
> useful performance context [8]; I am not using those numbers as a claim for
> this RFC.
> 
> If the direction is acceptable, I plan to continue toward:
> 
>   * the complete file-action and attribute set needed by posix_spawn();
>   * pristine target-process construction and an explicit no-source mode;
>   * PATH/posix_spawnp support, if it belongs on the kernel side; and
>   * an optional executable/template layer for workloads such as agent tool
>     calling and compiler drivers.
> 
> The exposed UAPI surface is intentionally limited so the state model and
> kernel/userspace boundary can be reviewed first. If maintainers would
> prefer more posix_spawn semantics or backend work in this RFC, please say
> so and I will adjust the split.
> 
> Codex GPT-5.5 and GPT-5.6-sol provided substantial assistance across design
> conceptualization, implementation, patch splitting, code review, development
> of self-test cases, and test planning and execution.
> 
> Thanks to Christian, Kees, Mateusz, Gabriel, Josh, Andy, John, and others
> for the review and direction.
> 
> [1]: https://patchew.org/linux/20260528095235.2491226-1-me%40linux.beauty/
> [2]: https://lore.kernel.org/all/20260528-madig-fachrichtung-fehlinformation-61117ba640da@brauner/
> [3]: https://lore.kernel.org/all/202606011254.5FCBD65@keescook/
> [4]: https://lore.kernel.org/all/vealb52tv5suireenkke4lul2l3wbnaul2rp3ea545ly5wa5ty@yk3aksvp7skt/
> [5]: https://lore.kernel.org/all/ce71d6df-6851-4e4b-9603-1d55d8d522b8@app.fastmail.com/
> [6]: https://github.com/apple-oss-distributions/xnu/blob/f6217f891ac0bb64f3d375211650a4c1ff8ca1ea/bsd/kern/kern_exec.c#L4039
> [7]: https://learn.microsoft.com/en-us/windows/win32/procthread/creating-processes
> [8]: https://lpc.events/event/16/contributions/1213/attachments/1012/1945/io-uring-spawn.pdf
> 
> 
> Li Chen (24):
>   pidfd: add spawn builder uapi
>   libfs: allow custom validation of stashed inode data
>   pidfs: add taskless future pidfd inodes
>   pidfd: create taskless spawn builders
>   pidfd: add spawn builder path configuration
>   exec: expose execveat internals to process builders
>   fork: expose vfork completion helper
>   pidfs: attach pids to future pidfd files
>   fork: let process builders supply preallocated pids
>   pidfs: publish future pidfd files
>   pidfd: add spawn builder state tracking
>   fork: let kernel callers create embryonic tasks
>   fork: let new tasks start with task work
>   pidfd: create and execute spawn builder tasks
>   fork: keep embryonic tasks hidden until exec completes
>   audit: add pidfd spawn child contexts
>   pidfd: audit child spawn execution
>   pidfd: make spawn builder execution signal-safe
>   file: expose spawn file-action helpers
>   pidfd: add initial spawn file actions
>   pidfd: consume spawn builders on the first run attempt
>   pidfd: expose spawn builder system calls
>   selftests/pidfd: cover pidfd spawn builders
>   Documentation: describe pidfd spawn builders
> 
>  Documentation/userspace-api/index.rst         |    1 +
>  Documentation/userspace-api/pidfd_spawn.rst   |  247 ++++
>  MAINTAINERS                                   |    6 +
>  arch/alpha/kernel/syscalls/syscall.tbl        |    2 +
>  arch/arm/tools/syscall.tbl                    |    2 +
>  arch/arm64/tools/syscall_32.tbl               |    2 +
>  arch/m68k/kernel/syscalls/syscall.tbl         |    2 +
>  arch/microblaze/kernel/syscalls/syscall.tbl   |    2 +
>  arch/mips/kernel/syscalls/syscall_n32.tbl     |    2 +
>  arch/mips/kernel/syscalls/syscall_n64.tbl     |    2 +
>  arch/mips/kernel/syscalls/syscall_o32.tbl     |    2 +
>  arch/parisc/kernel/syscalls/syscall.tbl       |    2 +
>  arch/powerpc/kernel/syscalls/syscall.tbl      |    2 +
>  arch/s390/kernel/syscalls/syscall.tbl         |    2 +
>  arch/sh/kernel/syscalls/syscall.tbl           |    2 +
>  arch/sparc/kernel/syscalls/syscall.tbl        |    2 +
>  arch/x86/entry/syscalls/syscall_32.tbl        |    2 +
>  arch/x86/entry/syscalls/syscall_64.tbl        |    2 +
>  arch/xtensa/kernel/syscalls/syscall.tbl       |    2 +
>  fs/Makefile                                   |    2 +-
>  fs/coredump.c                                 |    4 +-
>  fs/exec.c                                     |   30 +-
>  fs/exec_internal.h                            |   40 +
>  fs/file.c                                     |   11 +-
>  fs/internal.h                                 |    3 +
>  fs/libfs.c                                    |    5 +-
>  fs/open.c                                     |    7 +-
>  fs/pidfd_spawn.c                              | 1095 +++++++++++++++
>  fs/pidfs.c                                    |  478 ++++++-
>  fs/proc/base.c                                |   11 +-
>  fs/proc/internal.h                            |   18 +-
>  include/linux/audit.h                         |   31 +
>  include/linux/pid.h                           |   13 +
>  include/linux/pidfd_spawn.h                   |    9 +
>  include/linux/pidfs.h                         |   28 +
>  include/linux/sched.h                         |   21 +
>  include/linux/sched/task.h                    |    6 +
>  include/linux/syscalls.h                      |    7 +
>  include/uapi/asm-generic/unistd.h             |    8 +-
>  include/uapi/linux/audit.h                    |    1 +
>  include/uapi/linux/pidfd.h                    |    1 +
>  include/uapi/linux/pidfd_spawn.h              |   49 +
>  kernel/audit.h                                |    1 +
>  kernel/auditsc.c                              |  105 +-
>  kernel/fork.c                                 |   26 +-
>  kernel/nsproxy.c                              |   11 +-
>  kernel/pid.c                                  |   41 +-
>  kernel/ptrace.c                               |    4 +
>  kernel/signal.c                               |    2 +-
>  scripts/syscall.tbl                           |    2 +
>  tools/include/uapi/asm-generic/unistd.h       |    8 +-
>  tools/include/uapi/linux/pidfd_spawn.h        |   49 +
>  .../arch/alpha/entry/syscalls/syscall.tbl     |    2 +
>  .../perf/arch/arm/entry/syscalls/syscall.tbl  |    2 +
>  .../arch/arm64/entry/syscalls/syscall_32.tbl  |   11 +
>  .../arch/mips/entry/syscalls/syscall_n64.tbl  |    2 +
>  .../arch/parisc/entry/syscalls/syscall.tbl    |    2 +
>  .../arch/powerpc/entry/syscalls/syscall.tbl   |    2 +
>  .../perf/arch/s390/entry/syscalls/syscall.tbl |    2 +
>  tools/perf/arch/sh/entry/syscalls/syscall.tbl |    2 +
>  .../arch/sparc/entry/syscalls/syscall.tbl     |    2 +
>  .../arch/x86/entry/syscalls/syscall_32.tbl    |    2 +
>  .../arch/x86/entry/syscalls/syscall_64.tbl    |    2 +
>  .../arch/xtensa/entry/syscalls/syscall.tbl    |    2 +
>  tools/scripts/syscall.tbl                     |    2 +
>  tools/testing/selftests/landlock/audit.h      |    6 +-
>  tools/testing/selftests/pidfd/.gitignore      |    9 +
>  tools/testing/selftests/pidfd/Makefile        |   25 +-
>  tools/testing/selftests/pidfd/config          |    6 +
>  .../pidfd/pidfd_spawn_accounting_test.c       |  428 ++++++
>  .../pidfd/pidfd_spawn_actions_test.c          |  474 +++++++
>  .../selftests/pidfd/pidfd_spawn_audit_test.c  |  521 +++++++
>  .../selftests/pidfd/pidfd_spawn_common.c      |  512 +++++++
>  .../selftests/pidfd/pidfd_spawn_common.h      |   59 +
>  .../selftests/pidfd/pidfd_spawn_compat.c      |  221 +++
>  .../selftests/pidfd/pidfd_spawn_exec_test.c   |  301 ++++
>  .../selftests/pidfd/pidfd_spawn_policy_test.c |  294 ++++
>  .../selftests/pidfd/pidfd_spawn_race_test.c   |  923 ++++++++++++
>  .../pidfd/pidfd_spawn_security_test.c         | 1242 +++++++++++++++++
>  .../selftests/pidfd/pidfd_spawn_test.c        |  550 ++++++++
>  80 files changed, 7938 insertions(+), 81 deletions(-)
>  create mode 100644 Documentation/userspace-api/pidfd_spawn.rst
>  create mode 100644 fs/exec_internal.h
>  create mode 100644 fs/pidfd_spawn.c
>  create mode 100644 include/linux/pidfd_spawn.h
>  create mode 100644 include/uapi/linux/pidfd_spawn.h
>  create mode 100644 tools/include/uapi/linux/pidfd_spawn.h
>  create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_accounting_test.c
>  create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_actions_test.c
>  create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_audit_test.c
>  create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_common.c
>  create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_common.h
>  create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_compat.c
>  create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_exec_test.c
>  create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_policy_test.c
>  create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_race_test.c
>  create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_security_test.c
>  create mode 100644 tools/testing/selftests/pidfd/pidfd_spawn_test.c
> 
> -- 
> 2.52.0
> 

^ permalink raw reply

* Re: [PATCH] Documentation: sysfs-class-power: Update Long_Life description
From: Randy Dunlap @ 2026-08-03 23:10 UTC (permalink / raw)
  To: Derek J. Clark, Sebastian Reichel
  Cc: Hans de Goede, Pierre-Loup A . Griffais, linux-pm, linux-kernel,
	linux-api
In-Reply-To: <20260803210720.23844-1-derekjohn.clark@gmail.com>



On 8/3/26 2:07 PM, Derek J. Clark wrote:
> While adding charge limiting support to the Lenovo WMI drivers, there
> was some back and forth about whether charge_types or
> charge_control_end_threshold was the appropriate attribute to expose a
> battery charge limiting toggle that is fixed in the BIOS. The confusion
> arose because the charge_control_end_threshold description closely
> matches the functional change the hardware is making, while the
> charge_types functionality better suits the actual an on/off toggle that
> occurs in the BIOS. This specific scenario is not explicitly enumerated
> in the documentation, though it is fairly common.
> 
> Given that the original intention was to use it this way[1],[2], and that
> the samsung-laptop[3], ideapad-laptop[4], and lenovo-wmi-other[5] drivers
> all use the convention of charge_types with an exposed Long_Life and
> Standard value for this, codify it in the Documentation to avoid confusion
> in the future.
> 
> [1] https://lore.kernel.org/linux-pm/49993a42-aa91-46bf-acef-4a089db4c2db@redhat.com/
> [2] https://lore.kernel.org/platform-driver-x86/20241209204051.8786-1-hdegoede@redhat.com/
> [3] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=de2884c6cdd3d133704ce37393590dd1c761500c
> [4] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=da8f2708f9b69707f4efeb432a18395e46b4666f
> [5] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=9ca8fc065b88b327acbfdc33454efea391639716
> 
> Suggested-by: Hans de Goede <hansg@kernel.org>
> Signed-off-by: Derek J. Clark  <derekjohn.clark@gmail.com>
> ---
>  Documentation/ABI/testing/sysfs-class-power | 14 +++++++++-----
>  1 file changed, 9 insertions(+), 5 deletions(-)
> 
> diff --git a/Documentation/ABI/testing/sysfs-class-power b/Documentation/ABI/testing/sysfs-class-power
> index 5641f1fd5fd6..98b389845d1e 100644
> --- a/Documentation/ABI/testing/sysfs-class-power
> +++ b/Documentation/ABI/testing/sysfs-class-power
> @@ -365,9 +365,12 @@ Contact:	linux-pm@vger.kernel.org
>  Description:
>  		Represents a battery percentage level, above which charging will
>  		stop. Not all hardware is capable of setting this to an arbitrary
> -		percentage. Drivers will round written values to the nearest
> -		supported value. Reading back the value will show the actual
> -		threshold set by the driver.
> +		value, instead providing different minimum, maximum, or step
> +		values. Drivers will round written values to the nearest supported
> +		value. Reading back the value will show the actual threshold set
> +		by the driver. For hardware that only supports a single fixed
> +		value, use charge_types with a value of "Long Life" (vs "Standard")
> +		instead'

maybe		instead.
?
>  
>  		Access: Read, Write
>  
> @@ -398,8 +401,9 @@ Description:
>  			when to start and stop charging. Advanced users
>  			can use this to drastically extend battery life.
>  		Long Life:
> -			The charger reduces its charging rate in order to
> -			prolong the battery health.
> +			The charger firmware reduces its charging rate and/or
> +			maximum charging percentage to a hardware specified
> +			fixed limit in order to prolong the battery health.
>  		Bypass:
>  			The charger bypasses the charging path around the
>  			integrated converter allowing for a "smart" wall

-- 
~Randy


^ permalink raw reply

* [PATCH] Documentation: sysfs-class-power: Update Long_Life description
From: Derek J. Clark @ 2026-08-03 21:07 UTC (permalink / raw)
  To: Sebastian Reichel
  Cc: Hans de Goede, Pierre-Loup A . Griffais, Derek J . Clark,
	linux-pm, linux-kernel, linux-api

While adding charge limiting support to the Lenovo WMI drivers, there
was some back and forth about whether charge_types or
charge_control_end_threshold was the appropriate attribute to expose a
battery charge limiting toggle that is fixed in the BIOS. The confusion
arose because the charge_control_end_threshold description closely
matches the functional change the hardware is making, while the
charge_types functionality better suits the actual an on/off toggle that
occurs in the BIOS. This specific scenario is not explicitly enumerated
in the documentation, though it is fairly common.

Given that the original intention was to use it this way[1],[2], and that
the samsung-laptop[3], ideapad-laptop[4], and lenovo-wmi-other[5] drivers
all use the convention of charge_types with an exposed Long_Life and
Standard value for this, codify it in the Documentation to avoid confusion
in the future.

[1] https://lore.kernel.org/linux-pm/49993a42-aa91-46bf-acef-4a089db4c2db@redhat.com/
[2] https://lore.kernel.org/platform-driver-x86/20241209204051.8786-1-hdegoede@redhat.com/
[3] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=de2884c6cdd3d133704ce37393590dd1c761500c
[4] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=da8f2708f9b69707f4efeb432a18395e46b4666f
[5] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=9ca8fc065b88b327acbfdc33454efea391639716

Suggested-by: Hans de Goede <hansg@kernel.org>
Signed-off-by: Derek J. Clark  <derekjohn.clark@gmail.com>
---
 Documentation/ABI/testing/sysfs-class-power | 14 +++++++++-----
 1 file changed, 9 insertions(+), 5 deletions(-)

diff --git a/Documentation/ABI/testing/sysfs-class-power b/Documentation/ABI/testing/sysfs-class-power
index 5641f1fd5fd6..98b389845d1e 100644
--- a/Documentation/ABI/testing/sysfs-class-power
+++ b/Documentation/ABI/testing/sysfs-class-power
@@ -365,9 +365,12 @@ Contact:	linux-pm@vger.kernel.org
 Description:
 		Represents a battery percentage level, above which charging will
 		stop. Not all hardware is capable of setting this to an arbitrary
-		percentage. Drivers will round written values to the nearest
-		supported value. Reading back the value will show the actual
-		threshold set by the driver.
+		value, instead providing different minimum, maximum, or step
+		values. Drivers will round written values to the nearest supported
+		value. Reading back the value will show the actual threshold set
+		by the driver. For hardware that only supports a single fixed
+		value, use charge_types with a value of "Long Life" (vs "Standard")
+		instead'
 
 		Access: Read, Write
 
@@ -398,8 +401,9 @@ Description:
 			when to start and stop charging. Advanced users
 			can use this to drastically extend battery life.
 		Long Life:
-			The charger reduces its charging rate in order to
-			prolong the battery health.
+			The charger firmware reduces its charging rate and/or
+			maximum charging percentage to a hardware specified
+			fixed limit in order to prolong the battery health.
 		Bypass:
 			The charger bypasses the charging path around the
 			integrated converter allowing for a "smart" wall
-- 
2.55.0


^ permalink raw reply related

* Re: [RFC PATCH] fs: allow opening overlayfs/erofs layers through O_ALT
From: Christian Brauner @ 2026-07-30 13:57 UTC (permalink / raw)
  To: Andy Lutomirski
  Cc: 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, Jul 22, 2026 at 09:29:22AM -0700, Andy Lutomirski wrote:
> 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.

Two operations that the proposal merges into one.

Seeing through is introspection. You want to know that the overlay has
three lower layers, that lower[0] is an erofs, which image it came
from, what its fsid is. That's information. It should come back as
data, from statmount(), the way every other mount property does. It
hands out no new reachability, so it needs no new permission model, and
it's roughly what Amir is arguing for in this thread. I think he's
right that this half covers most of the actual use case.

Pulling a mount out is acquisition. You want a usable mount for that
lower layer. That's a capability transfer and the layer is an internal
mount with no mountpoint anywhere, and afterwards you can walk it. That
needs to be an explicit, privileged operation with a single call site,
and what it hands back should be a detached mount fd, which is exactly
what open_tree(OPEN_TREE_CLONE) already gives you. Same lifetime rules,
same move_mount() to attach it, nothing new to reason about.

The reason to keep them apart is that they have entirely different
security answers, and merging them into one path walk is precisely what
forces "whose creds" to be answered per node instead of once. Split,
the introspection half never has to ask, and the acquisition half
answers it at the top, before anything is reachable.

And neither half needs O_ALT. Introspection is statmount() with more
fields. Acquisition is a flag on open_tree()/open_tree_attr(), or an
operation on a mount fd. No new resolution mode, no second namespace
hanging off every fd, nothing for a symlink to accidentally point into.
That is the whole of my objection to the RFC, and dropping the mount
options half doesn't address any of it.

Which I think is close to your capability-fd sketch minus the tree. You
wanted privilege captured by the call that hands you the fd rather than
rechecked on use. If the only thing that call returns is a mount fd,
that's already how it behaves. And we don't have to invent a per-node
cred model or a new set of rules about what a dirfd is and isn't
allowed to do.

Giuseppe, this is also your requirement. What you actually
need is something persistable, so a restarted daemon can find the erofs
mount again without anyone having to hold an fd open for it. That's the
introspection half producing a stable identifier and the acquisition
half consuming one. Much narrower to specify than a generic metadata
tree, and it doesn't require settling anything about O_ALT first.

> 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.

Yes.

^ permalink raw reply

* Re: [PATCH RFC 4/7] fs: support FD_FAILFS_ROOT in fchroot()
From: Andy Lutomirski @ 2026-07-27 10:23 UTC (permalink / raw)
  To: Christian Brauner
  Cc: John Ericson, Andy Lutomirski, linux-fsdevel, Jann Horn,
	linux-api, H. Peter Anvin, Kees Cook, Farid Zakaria, Al Viro,
	Jan Kara, LKML, Jonathan Corbet, linux-doc, Christian Brauner
In-Reply-To: <20260727-nasen-gezweifelt-weitverbreitet-d6c0d9f5d28b@brauner>



> On Jul 27, 2026, at 10:59 AM, Christian Brauner <brauner@kernel.org> wrote:
> 
> On 2026-07-27 00:57 -0700, Andy Lutomirski wrote:
>>>> On Jul 27, 2026, at 9:43 AM, Andy Lutomirski <luto@amacapital.net> wrote:
>>> 
>>> 
>>>> 
>>>> goes along way to getting Capsicum's "capability mode".
>>> 
>>> One thing I dislike about this is that it’s (approximately) a task mode. Why should an fd representing a capability represent one capability (a subtree) to one task but a different capability (dotdot) in a different task?  Do we need to prevent unsharing a userns if this bit is set?   Do we want to have to worry about any task that could potentially SCM_RIGHTS and fd to a task without that bit set? I prefer beneathness being an OFD property.
>> 
>> Sigh, I keep thinking and typing OFD, and that won’t magically make it
>> be correct.  All these privilege restrictions applied to a file or
>> path reference need to follow every user-controllable reference around
>> correctly.  So if I fchdir or fchroot to a restricted OFD, my cwd or
>> root needs to preserve those restrictions, and reopening "/" or "." or
>> any other path (via root or cwd or openat etc) needs to respect the
>> restrictions as well.  Putting it in struct path may or may not be a
>> good idea.
> 
> I think making struct path carry more meaning than it does right now
> will cause us endless amounts of subtle issues. I fear that change very
> very much. That will have such a fundamental ripple effect across _all_
> corners of the codebase that I'm very unwilling to be led down that
> road. And I think Al had traditionally similar reservations as me. One
> of the really thorny issues with the O_PATH upgrade masks was precisely
> the struct path modification.
> 

The alternative (I think) is to enumerate all the places in the kernel that a path (or file?) reference exists and would want to carry various restriction bits.

I can think of:

Resolution (nameidata?)

struct file

cwd

root

optionally, vfsmount

Is that it? If I open a directory with a no-dotdot restriction, what can I possibly do with it that needs to respect that restriction?  Each such operation either needs to respect the restriction or fail outright. fchdir really ought to work. fchroot probably should, too.  We could just not allow clone/mount (at first?).  All the /proc links either start at cwd, root, or a struct file, right?

There are things like overlayfs layers, but failing an attempt to use a restrictive reference to a path as an overlayfs layer doesn’t seem to terrible to me.

I think that, logically, we could create a struct path_restrictions and add it to struct file and two copies to fs_struct.  And we wire it up into nameidata. And we carefully make every operation that doesn’t understand this fail if there are restrictions, which could be some new nameidata flag meaning “I understand path restrictions” where any attempt to resolve a path starting at root or cwd or a struct file with restrictions that doesn’t set that flag will fail immediately without even trying to resolve the path.

My intuition is that the implementation wouldn’t be so bad and that people could chip away at the unsupported bits that annoy them over time.

—Andy




^ permalink raw reply

* Re: [PATCH RFC 4/7] fs: support FD_FAILFS_ROOT in fchroot()
From: Christian Brauner @ 2026-07-27  8:59 UTC (permalink / raw)
  To: Andy Lutomirski
  Cc: John Ericson, Andy Lutomirski, Christian Brauner, linux-fsdevel,
	Jann Horn, linux-api, H. Peter Anvin, Kees Cook, Farid Zakaria,
	Al Viro, Jan Kara, LKML, Jonathan Corbet, linux-doc
In-Reply-To: <CALCETrVSKnT=wo8j+3bUBjcizEEWcg_a7WtfK_erLDGRqp3f-g@mail.gmail.com>

On 2026-07-27 00:57 -0700, Andy Lutomirski wrote:
> > On Jul 27, 2026, at 9:43 AM, Andy Lutomirski <luto@amacapital.net> wrote:
> >
> > 
> >>
> >> goes along way to getting Capsicum's "capability mode".
> >
> > One thing I dislike about this is that it’s (approximately) a task mode. Why should an fd representing a capability represent one capability (a subtree) to one task but a different capability (dotdot) in a different task?  Do we need to prevent unsharing a userns if this bit is set?   Do we want to have to worry about any task that could potentially SCM_RIGHTS and fd to a task without that bit set? I prefer beneathness being an OFD property.
> 
> Sigh, I keep thinking and typing OFD, and that won’t magically make it
> be correct.  All these privilege restrictions applied to a file or
> path reference need to follow every user-controllable reference around
> correctly.  So if I fchdir or fchroot to a restricted OFD, my cwd or
> root needs to preserve those restrictions, and reopening "/" or "." or
> any other path (via root or cwd or openat etc) needs to respect the
> restrictions as well.  Putting it in struct path may or may not be a
> good idea.

I think making struct path carry more meaning than it does right now
will cause us endless amounts of subtle issues. I fear that change very
very much. That will have such a fundamental ripple effect across _all_
corners of the codebase that I'm very unwilling to be led down that
road. And I think Al had traditionally similar reservations as me. One
of the really thorny issues with the O_PATH upgrade masks was precisely
the struct path modification.


^ permalink raw reply

* Re: [PATCH RFC 4/7] fs: support FD_FAILFS_ROOT in fchroot()
From: Andy Lutomirski @ 2026-07-27  7:57 UTC (permalink / raw)
  To: John Ericson
  Cc: Andy Lutomirski, Christian Brauner, linux-fsdevel, Jann Horn,
	linux-api, H. Peter Anvin, Kees Cook, Farid Zakaria, Al Viro,
	Jan Kara, LKML, Jonathan Corbet, linux-doc
In-Reply-To: <ABB3CF80-5F05-4279-8E0F-73A21E6B9DDA@amacapital.net>

> On Jul 27, 2026, at 9:43 AM, Andy Lutomirski <luto@amacapital.net> wrote:
>
> 
>>
>> goes along way to getting Capsicum's "capability mode".
>
> One thing I dislike about this is that it’s (approximately) a task mode. Why should an fd representing a capability represent one capability (a subtree) to one task but a different capability (dotdot) in a different task?  Do we need to prevent unsharing a userns if this bit is set?   Do we want to have to worry about any task that could potentially SCM_RIGHTS and fd to a task without that bit set? I prefer beneathness being an OFD property.

Sigh, I keep thinking and typing OFD, and that won’t magically make it
be correct.  All these privilege restrictions applied to a file or
path reference need to follow every user-controllable reference around
correctly.  So if I fchdir or fchroot to a restricted OFD, my cwd or
root needs to preserve those restrictions, and reopening "/" or "." or
any other path (via root or cwd or openat etc) needs to respect the
restrictions as well.  Putting it in struct path may or may not be a
good idea.

And, if we allow cloning a restricted reference to a subtree to mount
it, we need to make that work correctly too.

Off the top of my head, for a minimum viable product, we could
disallow cloning/mounting from a restricted reference.  And if we want
to try delegation (as in my other braindump email), we could try
*only* allowing delegation via clones/mounts and not in an OFD or cwd
or root, since there are potentially mildly thorny or at least
unintuitive interactions between symlinks and privilege delegation,
especially if mounts can delegate.  (And I think that delegation via
OFD/cwd/root but not via mount is so weak that there's almost no point
-- I want to delegate to a container, and that requires mounts.)

>
> Note that the obvious giant holes in capsicum-without-a-mode (root and cwd) and their respective dotdots are fixed, quite cleanly, by this current patch set.
>
> I also think it’s a bit of an ancient mistake that paths like /.. work at all instead of generating errors. Maybe a future beneath-only fd could also disallow trying to dotdot past the root, even for symlinks that contain dotdots.  This isn’t just an aesthetic thing - if I have ~/foo/bar being a symlink to ../bar, I think it would be a mistake for a beneath-only OFD to ~/foo to have “bar” resolve successfully to the wrong place.
>
>> Then we just
>> need to figure out things like more capabilities for directory file
>> descriptors per Andy's brainstorming thread thread (which I ought to get
>> to replying to).
>>
>> John
>>

^ permalink raw reply

* Re: [PATCH RFC 4/7] fs: support FD_FAILFS_ROOT in fchroot()
From: Andy Lutomirski @ 2026-07-27  7:42 UTC (permalink / raw)
  To: John Ericson
  Cc: Andy Lutomirski, Christian Brauner, linux-fsdevel, Jann Horn,
	linux-api, H. Peter Anvin, Kees Cook, Farid Zakaria, Al Viro,
	Jan Kara, LKML, Jonathan Corbet, linux-doc
In-Reply-To: <09b4735f-c04e-40f3-a7f5-254b7918fbfd@app.fastmail.com>



> On Jul 27, 2026, at 5:30 AM, John Ericson <mail@johnericson.me> wrote:
> 
> Very happy to see this patch series appear; thanks Christian!
> 
> Also happy with where this specific thread is going. I will just add
> that an additional flag in fs_struct to force RESOLVE_BENEATH in *all* path
> lookup would also be nice. The combination of:
> 
> root = pwd = failfs;
> chroot_locked = false;
> always_resolve_beneath = true;
> 
> goes along way to getting Capsicum's "capability mode".

One thing I dislike about this is that it’s (approximately) a task mode. Why should an fd representing a capability represent one capability (a subtree) to one task but a different capability (dotdot) in a different task?  Do we need to prevent unsharing a userns if this bit is set?   Do we want to have to worry about any task that could potentially SCM_RIGHTS and fd to a task without that bit set? I prefer beneathness being an OFD property.

Note that the obvious giant holes in capsicum-without-a-mode (root and cwd) and their respective dotdots are fixed, quite cleanly, by this current patch set.

I also think it’s a bit of an ancient mistake that paths like /.. work at all instead of generating errors. Maybe a future beneath-only fd could also disallow trying to dotdot past the root, even for symlinks that contain dotdots.  This isn’t just an aesthetic thing - if I have ~/foo/bar being a symlink to ../bar, I think it would be a mistake for a beneath-only OFD to ~/foo to have “bar” resolve successfully to the wrong place.

> Then we just
> need to figure out things like more capabilities for directory file
> descriptors per Andy's brainstorming thread thread (which I ought to get
> to replying to).
> 
> John
> 

^ permalink raw reply

* Re: [PATCH RFC 4/7] fs: support FD_FAILFS_ROOT in fchroot()
From: John Ericson @ 2026-07-27  3:30 UTC (permalink / raw)
  To: Andy Lutomirski, Christian Brauner
  Cc: linux-fsdevel, Jann Horn, linux-api, H. Peter Anvin, Kees Cook,
	Farid Zakaria, Al Viro, Jan Kara, LKML, Jonathan Corbet,
	linux-doc
In-Reply-To: <CALCETrW-ObgCm=Vhm6AiyyS25q+SyXkCvVWB7JEaawmOMCrrpA@mail.gmail.com>

Very happy to see this patch series appear; thanks Christian!

Also happy with where this specific thread is going. I will just add
that an additional flag in fs_struct to force RESOLVE_BENEATH in *all* path
lookup would also be nice. The combination of:

root = pwd = failfs;
chroot_locked = false;
always_resolve_beneath = true;

goes along way to getting Capsicum's "capability mode". Then we just
need to figure out things like more capabilities for directory file
descriptors per Andy's brainstorming thread thread (which I ought to get
to replying to).

John

^ permalink raw reply

* Re: [RFC PATCH] fs: allow opening overlayfs/erofs layers through O_ALT
From: Giuseppe Scrivano @ 2026-07-26 17:02 UTC (permalink / raw)
  To: Amir Goldstein
  Cc: Miklos Szeredi, Christian Brauner, Miklos Szeredi, linux-fsdevel,
	linux-unionfs, linux-api, linux-erofs, Gao Xiang
In-Reply-To: <CAOQ4uxhqmh47w46x0zhOzqJ54iiL0KDpuuAqr2oPyjm4AOgDYQ@mail.gmail.com>

Amir Goldstein <amir73il@gmail.com> writes:

>> > 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.

My end goal is to retrieve the EROFS mount itself that was used as an
overlay lower layer so that it can be reused across multiple overlay
mounts.
I'd like to do it without requiring a daemon to keep the file descriptor
open.  If the daemon crashes or is terminated for any reason, it can't
retrieve this information from the kernel anymore, and the daemon must
also worry about closing the file descriptor when it's no longer used by
any mount.  It seems like an unnecessary duplication of state given the
kernel already knowns about that.

How can the uuid/fsid/fhandle alone be used to solve this?  The overlay
mounts are private mounts that cannot be accessed anymore, even if you
know their handle.

In fact, before posting these patches I thought about extending
open_by_handle_at to work with mounts, in addition to namespaces and
pidfds.  Christian didn't agree with this idea and suggested the
introspection API for overlay.  IMO, this functionality would solve the
problem in a very elegant way, because we could store the handle for the
EROFS mount in a file and try to reuse it later.  On ESTALE, the mount
is recreated.  This would need only some locking, without the daemon
duplicating the kernel state.

Regards,
Giuseppe


^ permalink raw reply

* [PATCH v2 7/7] Documentation: add failfs documentation
From: Christian Brauner @ 2026-07-24 13:41 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: <20260724-work-failfs-v2-0-485dabbae185@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 | 73 ++++++++++++++++++++++++++++++++++++
 Documentation/filesystems/index.rst  |  1 +
 2 files changed, 74 insertions(+)

diff --git a/Documentation/filesystems/failfs.rst b/Documentation/filesystems/failfs.rst
new file mode 100644
index 000000000000..21ff2db7941d
--- /dev/null
+++ b/Documentation/filesystems/failfs.rst
@@ -0,0 +1,73 @@
+.. 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 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 ``.``.
+
+No path lookup can open the root, 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 all 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, "..")``.
+
+* The caller does not share its ``fs_struct``: ``no_new_privs`` is
+  checked on the calling thread, but the root lives in the ``fs_struct``.
+  A ``CLONE_FS`` sibling without ``no_new_privs`` could otherwise execute
+  a setuid binary with the failfs root, so entry requires ``fs->users ==
+  1``, the same restriction ``setns(2)`` applies for the mount and user
+  namespaces.
+
+Leaving
+=======
+
+Backing out is currently hard, but this is a property of the current
+implementation, not a guaranteed interface, and may be loosened later.
+For now a process that entered failfs counts as chrooted, so it cannot
+create user namespaces to regain ``CAP_SYS_CHROOT``, and ``chroot(2)``
+or ``fchroot(2)`` back out require ``CAP_SYS_CHROOT``. The remaining way
+out today 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 cannot currently get back out.
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

* [PATCH v2 6/7] selftests/filesystems: add failfs selftests
From: Christian Brauner @ 2026-07-24 13:41 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: <20260724-work-failfs-v2-0-485dabbae185@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 and for a shared fs_struct

- 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; the overmount test runs in a private mount namespace so a
  regression cannot touch the host root

- 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

The exec tests run the exec in a child so a wrongly successful exec
cannot replace the test image and masquerade as a pass.

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     | 581 +++++++++++++++++++++
 4 files changed, 589 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..cc461dc483a9
--- /dev/null
+++ b/tools/testing/selftests/filesystems/failfs/failfs_test.c
@@ -0,0 +1,581 @@
+// 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 <signal.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
+
+/* Child sentinel exit code: the exec was blocked as expected. */
+#define FAILFS_EXEC_BLOCKED 99
+
+/* Stack for the CLONE_FS helper in fchroot_sentinel_shared_fs_struct. */
+#define FAILFS_CLONE_STACK (64 * 1024)
+
+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);
+}
+
+/* Parked CLONE_FS child; dies with its parent so it never leaks. */
+static int failfs_park(void *arg)
+{
+	(void)arg;
+	prctl(PR_SET_PDEATHSIG, SIGKILL);
+	pause();
+	return 0;
+}
+
+/* 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);
+	/* d_path() names the failfs root synthetically, never as a real path. */
+	ASSERT_EQ(strcmp(buf, "failfs:/"), 0);
+
+	/* 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_shared_fs_struct)
+{
+	char stack[FAILFS_CLONE_STACK];
+	pid_t pid;
+
+	if (geteuid() == 0)
+		ASSERT_EQ(drop_to_nobody(), 0);
+
+	/* A CLONE_FS sibling shares the fs_struct: bump fs->users to 2. */
+	pid = clone(failfs_park, stack + sizeof(stack), CLONE_FS | SIGCHLD,
+		    NULL);
+	ASSERT_GE(pid, 0);
+
+	ASSERT_EQ(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0);
+
+	/*
+	 * A sibling without no_new_privs could exec a setuid binary with
+	 * the failfs root, so a shared fs_struct is refused even with
+	 * no_new_privs set.
+	 */
+	ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), -1);
+	ASSERT_EQ(errno, EINVAL);
+
+	ASSERT_EQ(kill(pid, SIGKILL), 0);
+	ASSERT_EQ(waitpid(pid, NULL, 0), pid);
+}
+
+TEST(fchroot_sentinel_no_overmount)
+{
+	if (geteuid() != 0)
+		SKIP(return, "mounting requires privileges");
+
+	/*
+	 * Contain the blast radius: if failfs ever regressed and "/"
+	 * resolved to the real root, the tmpfs mount below must not touch
+	 * the host. A private mount namespace keeps it local to this child.
+	 */
+	ASSERT_EQ(unshare(CLONE_NEWNS), 0);
+	ASSERT_EQ(mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL), 0);
+
+	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)
+{
+	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);
+
+	/*
+	 * Exec in a child: a wrongly successful exec would replace the test
+	 * image and its exit code would not match the sentinel below.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		execl("/bin/true", "true", NULL);
+		_exit(errno == EOPNOTSUPP ? FAILFS_EXEC_BLOCKED : 1);
+	}
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	ASSERT_TRUE(WIFEXITED(status));
+	ASSERT_EQ(WEXITSTATUS(status), FAILFS_EXEC_BLOCKED);
+}
+
+TEST(fchroot_sentinel_exec_interpreter)
+{
+	static const char * const argv[] = { "failfs_test", NULL };
+	static const char * const envp[] = { NULL };
+	pid_t pid;
+	int status, 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. Run it in a child so a wrongly successful exec does
+	 * not replace the test image and masquerade as a pass.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		syscall(__NR_execveat, exefd, "", argv, envp, AT_EMPTY_PATH);
+		_exit(errno == EOPNOTSUPP ? FAILFS_EXEC_BLOCKED : 1);
+	}
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	ASSERT_TRUE(WIFEXITED(status));
+	ASSERT_EQ(WEXITSTATUS(status), FAILFS_EXEC_BLOCKED);
+}
+
+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 v2 5/7] arch: hookup fchroot() system call
From: Christian Brauner @ 2026-07-24 13:41 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: <20260724-work-failfs-v2-0-485dabbae185@kernel.org>

Wire up the fchroot() system call as number 472 on (nearly) all
architectures and sync the mirrored copies of the syscall tables and
the asm-generic unistd.h under tools/.

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 +
 tools/include/uapi/asm-generic/unistd.h           | 6 +++++-
 tools/perf/arch/x86/entry/syscalls/syscall_64.tbl | 1 +
 tools/scripts/syscall.tbl                         | 1 +
 21 files changed, 29 insertions(+), 2 deletions(-)

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
diff --git a/tools/include/uapi/asm-generic/unistd.h b/tools/include/uapi/asm-generic/unistd.h
index a627acc8fb5f..5b7e77a7c736 100644
--- a/tools/include/uapi/asm-generic/unistd.h
+++ b/tools/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/tools/perf/arch/x86/entry/syscalls/syscall_64.tbl b/tools/perf/arch/x86/entry/syscalls/syscall_64.tbl
index 524155d655da..d5b6045b0090 100644
--- a/tools/perf/arch/x86/entry/syscalls/syscall_64.tbl
+++ b/tools/perf/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/tools/scripts/syscall.tbl b/tools/scripts/syscall.tbl
index 7a42b32b6577..0ab531605120 100644
--- a/tools/scripts/syscall.tbl
+++ b/tools/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 v2 4/7] fs: support FD_FAILFS_ROOT in fchroot()
From: Christian Brauner @ 2026-07-24 13:41 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: <20260724-work-failfs-v2-0-485dabbae185@kernel.org>

Allow a process to move its root directory into failfs via
fchroot(FD_FAILFS_ROOT). From that point on every absolute path lookup
and every absolute symlink fails with EOPNOTSUPP. Combined with
fchdir(FD_FAILFS_ROOT) this leaves the process with lookups anchored
at explicit directory file descriptors only. It is the fs_struct
equivalent of RESOLVE_BENEATH. This allows taks to drop their filesystem
state completely.

Callers with CAP_SYS_CHROOT in their user namespace may always do
this, mirroring chroot(2). Unprivileged callers are subject to three
requirements (which may be loosened later):

(1) no_new_privs must be set

    After entering failfs suid binaries on regular mounts remain
    reachable via inherited directory file descriptors or the working
    directory. A setuid program executing with an unusable root
    directory might be tricked by this. I'm not 100% convinced that this
    is needed but it feels more secure initially and it also forces more
    no_new_privs on userspace. So win-win imo.

(2) The caller must not already be chrooted.

    The root directory is what confines .. resolution. The failfs root
    can never be reached by walking up a real mount tree. A task whose
    root is failfs has no .. barrier left below the top of its mount
    tree. A .. walk from any real directory fd it still holds climbs
    to the mount-namespace root. Which is kinda the point if you want to
    do fd-based lookup only. If failfs prevented you from doing that
    then it doesn't make a lot of sense.

    A task that a privileged manager chrooted into a subtree could use
    chroot()ing into failfs as a way to allow for an inherited fd to
    resolve it again.

    So reject already-chrooted callers closing that issue without losing
    anything for the intended self-sandboxing use case.

(3) The caller must not share its fs_struct.

    Requirement (1) is checked on the calling thread, but the root
    lives in the fs_struct which may be shared via CLONE_FS. A sibling
    thread without no_new_privs could then execute a setuid binary with
    the failfs root and defeat (1). setns() to a mount or user namespace
    refuses a shared fs_struct for the same kind of reason, so do the
    same here and require fs->users == 1. no_new_privs is inherited
    across clone() and can never be cleared, so any CLONE_FS child
    created afterwards carries it too and the guarantee holds.

Privileged callers (CAP_SYS_CHROOT) are not subject to these
requirements and may share the fs_struct. They can already chroot and
exec a setuid binary today, so failfs hands them nothing new.

Backing out is currently hard, but that is a property of the current
implementation and not a promise. current_chrooted() treats a failfs
root as chrooted so for now the task cannot create user namespaces to
regain CAP_SYS_CHROOT and chroot()/fchroot() back out require
CAP_SYS_CHROOT. This is not guaranteed though. current_chrooted() may
change, or an unprivileged no_new_privs task could be allowed to chroot
to a real directory, either of which would loosen this. So don't treat
it as a permanent one-way door.

The remaining way out today is setns() to a mount namespace file
descriptor which requires CAP_SYS_ADMIN over the target namespace plus
CAP_SYS_CHROOT and CAP_SYS_ADMIN in the caller's user namespace and
resets both root and working directory. A task that closes or never had
such file descriptors and restricts *chdir()/*chroot()/setns() via
seccomp currently cannot get back out.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 fs/open.c | 47 ++++++++++++++++++++++++++++++++---------------
 1 file changed, 32 insertions(+), 15 deletions(-)

diff --git a/fs/open.c b/fs/open.c
index c57f641f2e29..6b1c14e684a9 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -620,31 +620,48 @@ SYSCALL_DEFINE1(chroot, const char __user *, filename)
 
 SYSCALL_DEFINE2(fchroot, int, fd, unsigned int, flags)
 {
+	struct path path;
 	int error;
 
 	if (flags)
 		return -EINVAL;
 
-	CLASS(fd_raw, f)(fd);
-	if (fd_empty(f))
-		return -EBADF;
+	if (fd == FD_FAILFS_ROOT) {
+		if (!ns_capable(current_user_ns(), CAP_SYS_CHROOT)) {
+			if (!task_no_new_privs(current))
+				return -EPERM;
+			/* A shared fs_struct lets a sibling exec setuid past the check above. */
+			if (current->fs->users != 1)
+				return -EINVAL;
+			/* Moving the root to failfs lifts the old root's ".." barrier. */
+			if (current_chrooted())
+				return -EPERM;
+		}
+		failfs_get_root(&path);
+	} else {
+		CLASS(fd_raw, f)(fd);
+		if (fd_empty(f))
+			return -EBADF;
 
-	if (!d_can_lookup(fd_file(f)->f_path.dentry))
-		return -ENOTDIR;
+		if (!d_can_lookup(fd_file(f)->f_path.dentry))
+			return -ENOTDIR;
 
-	error = file_permission(fd_file(f), MAY_EXEC | MAY_CHDIR);
-	if (error)
-		return error;
+		error = file_permission(fd_file(f), MAY_EXEC | MAY_CHDIR);
+		if (error)
+			return error;
 
-	if (!ns_capable(current_user_ns(), CAP_SYS_CHROOT))
-		return -EPERM;
+		if (!ns_capable(current_user_ns(), CAP_SYS_CHROOT))
+			return -EPERM;
 
-	error = security_path_chroot(&fd_file(f)->f_path);
-	if (error)
-		return error;
+		path = fd_file(f)->f_path;
+		path_get(&path);
+	}
 
-	set_fs_root(current->fs, &fd_file(f)->f_path);
-	return 0;
+	error = security_path_chroot(&path);
+	if (!error)
+		set_fs_root(current->fs, &path);
+	path_put(&path);
+	return error;
 }
 
 int chmod_common(const struct path *path, umode_t mode)

-- 
2.53.0


^ permalink raw reply related

* [PATCH v2 3/7] fs: add fchroot()
From: Christian Brauner @ 2026-07-24 13:41 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: <20260724-work-failfs-v2-0-485dabbae185@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). The 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 v2 2/7] fs: support FD_FAILFS_ROOT in fchdir()
From: Christian Brauner @ 2026-07-24 13:41 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: <20260724-work-failfs-v2-0-485dabbae185@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 d0ca1fc6c459..66a36da3d236 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>
 
@@ -135,6 +136,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 ce7f12c5a65b..67aa0444351b 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -365,3 +365,4 @@ void nsfs_get_root(struct path *path);
 void failfs_get_root(struct path *path);
 void __init failfs_init(void);
 bool failfs_mnt(const struct vfsmount *mnt);
+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 v2 1/7] fs: add failfs
From: Christian Brauner @ 2026-07-24 13:41 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: <20260724-work-failfs-v2-0-485dabbae185@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.

No path lookup can open the root, 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
covers the jump-based references too: an O_PATH open is refused,
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 "failfs:/".

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/d_path.c                |   3 +-
 fs/failfs.c                | 155 +++++++++++++++++++++++++++++++++++++++++++++
 fs/internal.h              |   3 +
 fs/namespace.c             |   1 +
 include/uapi/linux/magic.h |   1 +
 6 files changed, 163 insertions(+), 2 deletions(-)

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/d_path.c b/fs/d_path.c
index a48957c0971e..c25309006d5d 100644
--- a/fs/d_path.c
+++ b/fs/d_path.c
@@ -279,7 +279,8 @@ char *d_path(const struct path *path, char *buf, int buflen)
 	 * and instead have d_path return the mounted path.
 	 */
 	if (path->dentry->d_op && path->dentry->d_op->d_dname &&
-	    (!IS_ROOT(path->dentry) || path->dentry != path->mnt->mnt_root))
+	    (!IS_ROOT(path->dentry) || path->dentry != path->mnt->mnt_root ||
+	     failfs_mnt(path->mnt)))
 		return path->dentry->d_op->d_dname(path->dentry, buf, buflen);
 
 	rcu_read_lock();
diff --git a/fs/failfs.c b/fs/failfs.c
new file mode 100644
index 000000000000..d0ca1fc6c459
--- /dev/null
+++ b/fs/failfs.c
@@ -0,0 +1,155 @@
+// 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);
+}
+
+bool failfs_mnt(const struct vfsmount *mnt)
+{
+	return mnt->mnt_sb == failfs_root_path.mnt->mnt_sb;
+}
+
+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 char *failfs_dname(struct dentry *dentry, char *buffer, int buflen)
+{
+	return dynamic_dname(buffer, buflen, "failfs:/");
+}
+
+static const struct dentry_operations failfs_dentry_operations = {
+	.d_dname		= failfs_dname,
+	.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..ce7f12c5a65b 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -362,3 +362,6 @@ 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);
+bool failfs_mnt(const struct vfsmount *mnt);
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 v2 0/7] fs: add failfs
From: Christian Brauner @ 2026-07-24 13:41 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 "failfs:/".

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>
---
Changes in v2:
- Don't promise guarantees we might not want to give.
- Link to v1: https://patch.msgid.link/20260723-work-failfs-v1-0-3f69b9a9e958@kernel.org

---
Christian Brauner (7):
      fs: add failfs
      fs: support FD_FAILFS_ROOT 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               |  73 +++
 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/d_path.c                                        |   3 +-
 fs/failfs.c                                        | 166 ++++++
 fs/internal.h                                      |   4 +
 fs/namespace.c                                     |   1 +
 fs/open.c                                          |  51 +-
 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/include/uapi/asm-generic/unistd.h            |   6 +-
 tools/perf/arch/x86/entry/syscalls/syscall_64.tbl  |   1 +
 tools/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     | 581 +++++++++++++++++++++
 36 files changed, 919 insertions(+), 5 deletions(-)
---
base-commit: 1590cf0329716306e948a8fc29f1d3ee87d3989f
change-id: 20260723-work-failfs-d86a0c1db48d


^ permalink raw reply

* Re: [PATCH 1/2] riscv: hwprobe: export the availability of vector to user
From: Florian Weimer @ 2026-07-24 12:53 UTC (permalink / raw)
  To: Andy Chiu
  Cc: Jonathan Corbet, Shuah Khan, Paul Walmsley, Palmer Dabbelt,
	Albert Ou, Alexandre Ghiti, linux-doc, linux-riscv, bergner,
	kito.cheng, dfustini, greentime.hu, Andrew Jones, Nutty Liu,
	Pincheng Wang, Yao Zihong, Xu Lu, Aleksa Paunovic, Jingwei Wang,
	Zong Li, Deepak Gupta, Clément Léger, linux-api
In-Reply-To: <20260723222109.2229089-2-tchiu@tenstorrent.com>

* Andy Chiu:

> Userland IFUNC resolvers use hwprobe to decide whether to dispatch to
> vectorized routines. But RISCV_HWPROBE_KEY_IMA_EXT_0 only reports what
> is present in hardware, not what the calling process may actually use:
> when Vector is disabled for a process via
> prctl(PR_RISCV_V_SET_CONTROL, PR_RISCV_V_VSTATE_CTRL_OFF), it is still
> reported as present. A resolver that trusts this and runs a vector
> instruction crashes with SIGILL.
>
> Add RISCV_HWPROBE_KEY_EXT_ENABLED, a positional modifier key that carries
> no value of its own. Within a single request, keys placed after it report
> extensions that are both present and enabled for the calling process,
> while keys before it keep reporting hardware presence. This masks out V
> and its V-dependent sub-extensions when V is disabled for the process, and
> lets userland obtain both views in one query:
>
>       [ {IMA_EXT_0}, {EXT_ENABLED}, {IMA_EXT_0} ]
>           present       modifier       enabled
>
> The enabled view depends on per-process state, so it cannot be served from
> the vDSO's process-independent cache; requests carrying the modifier are
> deferred to the syscall. Unknown keys are still reported as -1, so the
> feature is detectable and existing users are unaffected.

What is the expected behavior if RISCV_HWPROBE_KEY_EXT_ENABLED is not
supported?

We only get a true userspace simplification over hwprobe + prctl if we
can disable vector extension usage if the kernel does not support
RISCV_HWPROBE_KEY_EXT_ENABLED (so a V usage regression for older
kernels).  Otherwise we'd have to use the new approach and, as a
fallback, the old combination of hwprobe and prctl.

Thanks,
Florian


^ permalink raw reply

* Re: [RFC] Null Namespaces
From: Christian Brauner @ 2026-07-24  8:04 UTC (permalink / raw)
  To: Andy Lutomirski
  Cc: Jann Horn, John Ericson, Li Chen, Cong Wang, linux-arch,
	linux-kernel, linux-fsdevel, linux-api, Arnd Bergmann,
	Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen,
	H. Peter Anvin, Jan Kara, Jonathan Corbet, Shuah Khan,
	Alexander Viro, Kees Cook, Sergei Zimmerman, Farid Zakaria
In-Reply-To: <CALCETrVjDtBmB+boCUgd069UpZGUB1TCbEn3k1HZQex-_dPgbQ@mail.gmail.com>

> > > For my FD_FAILFS_ROOT proposal it would be enough if we make failfs
> > > SB_KERNMOUNT which means it's logically distinct from every mount
> > > namespace. I think that might be the right thing to do. I need to spend
> > > one or more brain cycles on this though.
> >
> > I had to take a long drive on Sunday and I kept thinking about both
> > FD_NULLFS_ROOT and FD_FAILFS_ROOT and ofc there are some things to
> > consider/discuss.
> >
> > I think the straightforward solution to FD_NULLFS_ROOT would be to just:
> >
> > - make it always available
> > - refer to the caller's mount namespace nullfs
> > - work with fchroot()/fchdir()
> >
> > So I considered two chroot() use-cases for the sake of simplicity:
> >
> > (1) You want to isolate yourself for the sake of lookup
> >
> > (2) You want to isolate yourself to assemble a "private mount tree" but
> >     not really be in a separate namespace (very odd use-case... but it
> >     helps to make a point).
> >
> > The problem with this approach is that everyone who chroots into the
> > nullfs root would suffer from the problem that any mount on top of it is
> > still visible. So that kinda makes it pointless for both (1) and (2).
> >
> > Also all mounts that someone else would do would also be visible
> > allowing multiple chroot()ers to affect each others state. That also
> > would somewhat defeat the purpose of the chroot(). So I'm not convinced
> > this is what we should do.
> >
> 
> After some contemplation and a long place flight: are we talking about
> nullfs or failfs?  Because I would expect that it's entirely

nullfs

> impossible to mount anything on top of failfs.  So failfs would be
> useless for #2 but would still solve #1.

Yes, failfs can't be mounted on at all.

^ permalink raw reply

* [PATCH v2 3/5] ntsync: reject wait ioctls with zero owner
From: Elizabeth Figura @ 2026-07-23 20:12 UTC (permalink / raw)
  To: Arnd Bergmann, Greg Kroah-Hartman
  Cc: linux-kernel, linux-api, wine-devel, linux-kselftest, linux-doc,
	Iván Ezequiel Rodriguez, Elizabeth Figura
In-Reply-To: <20260723201301.11826-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

* [PATCH v2 0/5] ntsync miscellaneous patches
From: Elizabeth Figura @ 2026-07-23 20:12 UTC (permalink / raw)
  To: Arnd Bergmann, Greg Kroah-Hartman
  Cc: linux-kernel, linux-api, wine-devel, linux-kselftest, linux-doc,
	Iván Ezequiel Rodriguez, Elizabeth Figura

This is a resend of a series originally submitted as [1], combined with two
patches originally sent as [2].

This supersedes a previous set of patches sent as [3].

The following is the original cover letter sent with the series at [1].

[1] https://lore.kernel.org/all/20260628024239.152852-1-ivanrwcm25@gmail.com/
[2] https://lore.kernel.org/all/20260720175402.44338-1-ivanrwcm25@gmail.com/
[3] https://lore.kernel.org/all/20260720171740.447035-1-zfigura@codeweavers.com/

===

This series improves ntsync without changing wait/wake semantics:

 1/4 — Align Documentation/userspace-api/ntsync.rst with
       include/uapi/linux/ntsync.h (ioctl macro names and struct layout).

 2/4 — Fix wake_all selftest: CREATE_EVENT returns an fd, not zero.

 3/4 — Add selftests for documented EINVAL cases (zero owner, non-zero
       pad, cross-instance object use).

 4/4 — Reject wait ioctls when owner is zero, matching the documented
       uAPI (3/4 depends on 4/4 for the owner tests).

Patch 4/4 closes a spec gap: Documentation/userspace-api/ntsync.rst
requires EINVAL when wait owner is zero, but setup_wait() only validated
pad and flags.  Unlock/kill mutex ioctls already reject owner == 0.

Testing:
- scripts/checkpatch.pl --strict --no-tree: clean (4/4 patches)
- make headers && make -C tools/testing/selftests TARGETS=drivers/ntsync
- Kernel 7.1.0-ntsync-test+ (CONFIG_NTSYNC=y), QEMU x86_64 initramfs:
  tools/testing/selftests/drivers/ntsync/ntsync — 12/12 PASS,
  including wake_all and wait_args_validation
- On 6.17.0-35-generic with the distro ntsync.ko (without patch 4/4):
  wait_args_validation fails on owner==0 (wait proceeds instead of
  EINVAL), confirming the gap this series fixes

Iván Ezequiel Rodriguez (5):
  selftests: ntsync: fix wake_all CREATE_EVENT fd expectation
  selftests: ntsync: add wait argument validation tests
  ntsync: reject wait ioctls with zero owner
  docs: ntsync: align ioctl names and struct layouts with uapi
  selftests: ntsync: test absolute MONOTONIC waits under time namespaces

 Documentation/userspace-api/ntsync.rst        |  33 ++---
 drivers/misc/ntsync.c                         |   3 +
 .../testing/selftests/drivers/ntsync/ntsync.c | 133 +++++++++++++++++-
 3 files changed, 152 insertions(+), 17 deletions(-)


base-commit: 2cedf2272f1bb42471e646868ac572cc5752bd91
-- 
2.53.0


^ permalink raw reply

* [PATCH v2 5/5] selftests: ntsync: test absolute MONOTONIC waits under time namespaces
From: Elizabeth Figura @ 2026-07-23 20:13 UTC (permalink / raw)
  To: Arnd Bergmann, Greg Kroah-Hartman
  Cc: linux-kernel, linux-api, wine-devel, linux-kselftest, linux-doc,
	Iván Ezequiel Rodriguez, Maoyi Xie, Elizabeth Figura
In-Reply-To: <20260723201301.11826-1-zfigura@codeweavers.com>

From: Iván Ezequiel Rodriguez <ivanrwcm25@gmail.com>

Cover the timens conversion in ntsync_schedule(): with a negative
CLOCK_MONOTONIC offset, a 100 ms absolute wait must not return
immediately against the host clock.

Suggested-by: Maoyi Xie <maoyixie.tju@gmail.com>
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>
---
 .../testing/selftests/drivers/ntsync/ntsync.c | 87 +++++++++++++++++++
 1 file changed, 87 insertions(+)

diff --git a/tools/testing/selftests/drivers/ntsync/ntsync.c b/tools/testing/selftests/drivers/ntsync/ntsync.c
index c9fe4d5987ec..1f0dc43bb4c0 100644
--- a/tools/testing/selftests/drivers/ntsync/ntsync.c
+++ b/tools/testing/selftests/drivers/ntsync/ntsync.c
@@ -8,12 +8,18 @@
 #define _GNU_SOURCE
 #include <sys/ioctl.h>
 #include <sys/stat.h>
+#include <sys/wait.h>
 #include <fcntl.h>
+#include <sched.h>
 #include <time.h>
 #include <pthread.h>
 #include <linux/ntsync.h>
 #include "kselftest_harness.h"
 
+#ifndef CLONE_NEWTIME
+#define CLONE_NEWTIME 0x00000080
+#endif
+
 static int read_sem_state(int sem, __u32 *count, __u32 *max)
 {
 	struct ntsync_sem_args args;
@@ -1384,4 +1390,85 @@ TEST(wait_args_validation)
 	close(fd);
 }
 
+/*
+ * Absolute MONOTONIC timeouts must honour the caller's time namespace.
+ * With a negative monotonic offset, a 100 ms wait must still take ~100 ms
+ * of namespace time (not return immediately against the host clock).
+ */
+TEST(wait_any_monotonic_timens)
+{
+	struct ntsync_sem_args sem_args = {0};
+	struct ntsync_wait_args wait_args = {0};
+	struct timespec start, end;
+	char buf[64];
+	__u64 elapsed_ns;
+	int fd, offset_fd, sem, ret, status, len;
+	pid_t pid;
+
+	if (access("/proc/self/ns/time", F_OK))
+		SKIP(return, "Time namespaces are not supported");
+
+	fd = open("/dev/ntsync", O_CLOEXEC | O_RDONLY);
+	if (fd < 0)
+		SKIP(return, "/dev/ntsync is not available");
+
+	ret = unshare(CLONE_NEWTIME);
+	if (ret) {
+		close(fd);
+		if (errno == EPERM)
+			SKIP(return, "need CAP_SYS_ADMIN for CLONE_NEWTIME");
+		ASSERT_EQ(0, ret);
+	}
+
+	len = snprintf(buf, sizeof(buf), "%d %d 0", CLOCK_MONOTONIC, -10);
+	offset_fd = open("/proc/self/timens_offsets", O_WRONLY);
+	ASSERT_LE(0, offset_fd);
+	ASSERT_EQ(len, write(offset_fd, buf, len));
+	close(offset_fd);
+
+	pid = fork();
+	ASSERT_LE(0, pid);
+	if (!pid) {
+		int obj;
+
+		sem_args.count = 0;
+		sem_args.max = 1;
+		sem = ioctl(fd, NTSYNC_IOC_CREATE_SEM, &sem_args);
+		if (sem < 0)
+			_exit(1);
+
+		obj = sem;
+		wait_args.timeout = get_abs_timeout(100);
+		wait_args.objs = (uintptr_t)&obj;
+		wait_args.count = 1;
+		wait_args.owner = 123;
+		wait_args.index = 0xdeadbeef;
+
+		if (clock_gettime(CLOCK_MONOTONIC, &start))
+			_exit(2);
+		ret = ioctl(fd, NTSYNC_IOC_WAIT_ANY, &wait_args);
+		if (clock_gettime(CLOCK_MONOTONIC, &end))
+			_exit(2);
+
+		if (ret != -1 || errno != ETIMEDOUT)
+			_exit(3);
+
+		elapsed_ns = (end.tv_sec - start.tv_sec) * 1000000000ULL +
+			     (end.tv_nsec - start.tv_nsec);
+		/* Without timens conversion this returns in ~0 ms. */
+		if (elapsed_ns < 50 * 1000000ULL)
+			_exit(4);
+		if (elapsed_ns > 1000 * 1000000ULL)
+			_exit(5);
+
+		_exit(0);
+	}
+
+	ASSERT_EQ(pid, waitpid(pid, &status, 0));
+	EXPECT_TRUE(WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status));
+
+	close(fd);
+}
+
 TEST_HARNESS_MAIN
-- 
2.53.0


^ permalink raw reply related

* [PATCH v2 2/5] selftests: ntsync: add wait argument validation tests
From: Elizabeth Figura @ 2026-07-23 20:12 UTC (permalink / raw)
  To: Arnd Bergmann, Greg Kroah-Hartman
  Cc: linux-kernel, linux-api, wine-devel, linux-kselftest, linux-doc,
	Iván Ezequiel Rodriguez, Elizabeth Figura
In-Reply-To: <20260723201301.11826-1-zfigura@codeweavers.com>

From: Iván Ezequiel Rodriguez <ivanrwcm25@gmail.com>

Add coverage for documented EINVAL cases: zero owner on wait any/all,
non-zero pad, and objects from a different /dev/ntsync instance.

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>
---
 .../testing/selftests/drivers/ntsync/ntsync.c | 44 +++++++++++++++++++
 1 file changed, 44 insertions(+)

diff --git a/tools/testing/selftests/drivers/ntsync/ntsync.c b/tools/testing/selftests/drivers/ntsync/ntsync.c
index 12b4b81edf7f..c9fe4d5987ec 100644
--- a/tools/testing/selftests/drivers/ntsync/ntsync.c
+++ b/tools/testing/selftests/drivers/ntsync/ntsync.c
@@ -1340,4 +1340,48 @@ TEST(stress_wait)
 	close(stress_device);
 }
 
+TEST(wait_args_validation)
+{
+	struct ntsync_sem_args sem_args = { .count = 1, .max = 1 };
+	struct ntsync_wait_args wait_args = {0};
+	struct timespec timeout;
+	int fd, fd2, sem, ret;
+	__u32 index;
+
+	fd = open("/dev/ntsync", O_CLOEXEC | O_RDONLY);
+	ASSERT_GE(fd, 0);
+
+	fd2 = open("/dev/ntsync", O_CLOEXEC | O_RDONLY);
+	ASSERT_GE(fd2, 0);
+
+	sem = ioctl(fd, NTSYNC_IOC_CREATE_SEM, &sem_args);
+	EXPECT_GE(sem, 0);
+
+	ret = wait_any(fd, 1, &sem, 0, &index);
+	EXPECT_EQ(-1, ret);
+	EXPECT_EQ(EINVAL, errno);
+
+	ret = wait_all(fd, 1, &sem, 0, &index);
+	EXPECT_EQ(-1, ret);
+	EXPECT_EQ(EINVAL, errno);
+
+	clock_gettime(CLOCK_MONOTONIC, &timeout);
+	wait_args.timeout = timeout.tv_sec * 1000000000ULL + timeout.tv_nsec;
+	wait_args.count = 0;
+	wait_args.objs = 0;
+	wait_args.owner = 123;
+	wait_args.pad = 1;
+	ret = ioctl(fd, NTSYNC_IOC_WAIT_ANY, &wait_args);
+	EXPECT_EQ(-1, ret);
+	EXPECT_EQ(EINVAL, errno);
+
+	ret = wait_any(fd2, 1, &sem, 123, &index);
+	EXPECT_EQ(-1, ret);
+	EXPECT_EQ(EINVAL, errno);
+
+	close(sem);
+	close(fd2);
+	close(fd);
+}
+
 TEST_HARNESS_MAIN
-- 
2.53.0


^ permalink raw reply related


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox