* [RFC PATCH 07/24] fork: expose vfork completion helper
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: 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, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>
Allow process builders that call copy_process() directly to attach and
wait for the existing vfork completion without duplicating its signal
and task-reference handling.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
include/linux/sched/task.h | 3 +++
kernel/fork.c | 3 +--
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/include/linux/sched/task.h b/include/linux/sched/task.h
index e0c1ca8c6a188..92b4e3bc31e66 100644
--- a/include/linux/sched/task.h
+++ b/include/linux/sched/task.h
@@ -14,6 +14,7 @@
struct task_struct;
struct rusage;
+struct completion;
union thread_union;
struct css_set;
@@ -99,6 +100,8 @@ extern void exit_itimers(struct task_struct *);
extern pid_t kernel_clone(struct kernel_clone_args *kargs);
struct task_struct *copy_process(struct pid *pid, int trace, int node,
struct kernel_clone_args *args);
+int wait_for_vfork_done(struct task_struct *child,
+ struct completion *vfork);
struct task_struct *create_io_thread(int (*fn)(void *), void *arg, int node);
struct task_struct *fork_idle(int);
extern pid_t kernel_thread(int (*fn)(void *), void *arg, const char *name,
diff --git a/kernel/fork.c b/kernel/fork.c
index f3f378a85be4c..970810a01bbf6 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -1435,8 +1435,7 @@ static void complete_vfork_done(struct task_struct *tsk)
task_unlock(tsk);
}
-static int wait_for_vfork_done(struct task_struct *child,
- struct completion *vfork)
+int wait_for_vfork_done(struct task_struct *child, struct completion *vfork)
{
unsigned int state = TASK_KILLABLE|TASK_FREEZABLE;
int killed;
--
2.52.0
^ permalink raw reply related
* [RFC PATCH 06/24] exec: expose execveat internals to process builders
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: 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, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>
Move struct user_arg_ptr and its native and compat constructors to an
fs-private header. Expose do_execveat_common() within fs so a process
builder can enter the normal exec path without synthesizing a userspace
execveat() syscall.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
MAINTAINERS | 1 +
fs/exec.c | 28 ++++------------------------
fs/exec_internal.h | 40 ++++++++++++++++++++++++++++++++++++++++
3 files changed, 45 insertions(+), 24 deletions(-)
create mode 100644 fs/exec_internal.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 8d7f94f09f414..85b1306cb2ff3 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -9757,6 +9757,7 @@ F: Documentation/userspace-api/ELF.rst
F: fs/*binfmt_*.c
F: fs/Kconfig.binfmt
F: fs/exec.c
+F: fs/exec_internal.h
F: fs/tests/binfmt_*_kunit.c
F: fs/tests/exec_kunit.c
F: include/linux/binfmts.h
diff --git a/fs/exec.c b/fs/exec.c
index d5993cedc829a..1a256ba26425e 100644
--- a/fs/exec.c
+++ b/fs/exec.c
@@ -76,6 +76,7 @@
#include <trace/events/task.h>
#include "internal.h"
+#include "exec_internal.h"
#include <trace/events/sched.h>
@@ -291,17 +292,6 @@ static int bprm_mm_init(struct linux_binprm *bprm)
return err;
}
-struct user_arg_ptr {
-#ifdef CONFIG_COMPAT
- bool is_compat;
-#endif
- union {
- const char __user *const __user *native;
-#ifdef CONFIG_COMPAT
- const compat_uptr_t __user *compat;
-#endif
- } ptr;
-};
static const char __user *get_user_arg_ptr(struct user_arg_ptr argv, int nr)
{
@@ -1805,10 +1795,9 @@ static int bprm_execve(struct linux_binprm *bprm)
return retval;
}
-static int do_execveat_common(int fd, struct filename *filename,
- struct user_arg_ptr argv,
- struct user_arg_ptr envp,
- int flags)
+int do_execveat_common(int fd, struct filename *filename,
+ struct user_arg_ptr argv,
+ struct user_arg_ptr envp, int flags)
{
int retval;
@@ -1935,10 +1924,6 @@ void set_binfmt(struct linux_binfmt *new)
}
EXPORT_SYMBOL(set_binfmt);
-static inline struct user_arg_ptr native_arg(const char __user *const __user *p)
-{
- return (struct user_arg_ptr){.ptr.native = p};
-}
SYSCALL_DEFINE3(execve,
const char __user *, filename,
@@ -1963,11 +1948,6 @@ SYSCALL_DEFINE5(execveat,
#ifdef CONFIG_COMPAT
-static inline struct user_arg_ptr compat_arg(const compat_uptr_t __user *p)
-{
- return (struct user_arg_ptr){.is_compat = true, .ptr.compat = p};
-}
-
COMPAT_SYSCALL_DEFINE3(execve, const char __user *, filename,
const compat_uptr_t __user *, argv,
const compat_uptr_t __user *, envp)
diff --git a/fs/exec_internal.h b/fs/exec_internal.h
new file mode 100644
index 0000000000000..bf6a873e98426
--- /dev/null
+++ b/fs/exec_internal.h
@@ -0,0 +1,40 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _FS_EXEC_INTERNAL_H
+#define _FS_EXEC_INTERNAL_H
+
+#include <linux/compat.h>
+#include <linux/compiler_types.h>
+#include <linux/types.h>
+
+struct filename;
+
+struct user_arg_ptr {
+#ifdef CONFIG_COMPAT
+ bool is_compat;
+#endif
+ union {
+ const char __user *const __user *native;
+#ifdef CONFIG_COMPAT
+ const compat_uptr_t __user *compat;
+#endif
+ } ptr;
+};
+
+static inline struct user_arg_ptr
+native_arg(const char __user *const __user *p)
+{
+ return (struct user_arg_ptr){.ptr.native = p};
+}
+
+#ifdef CONFIG_COMPAT
+static inline struct user_arg_ptr compat_arg(const compat_uptr_t __user *p)
+{
+ return (struct user_arg_ptr){.is_compat = true, .ptr.compat = p};
+}
+#endif
+
+int do_execveat_common(int fd, struct filename *filename,
+ struct user_arg_ptr argv,
+ struct user_arg_ptr envp, int flags);
+
+#endif /* _FS_EXEC_INTERNAL_H */
--
2.52.0
^ permalink raw reply related
* [RFC PATCH 05/24] pidfd: add spawn builder path configuration
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: 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, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>
Add an fsconfig-style pidfd_config() implementation and the initial path
string key. The builder inode retains configuration, which userspace may
replace before the child is created.
Bind configuration authority to the creator mm, credential object, and
child PID namespace. Passing the fd alone therefore does not delegate
launch authority to a different process context.
Accept absolute and relative executable paths. A later run operation
resolves relative paths from the child working directory without PATH
search. Keep the syscall unreachable until the complete spawn state machine
is wired later in the series.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
fs/pidfd_spawn.c | 155 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 155 insertions(+)
diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
index ff2b0cb6365a5..46207c8e8139f 100644
--- a/fs/pidfd_spawn.c
+++ b/fs/pidfd_spawn.c
@@ -3,24 +3,47 @@
* pidfd-backed process spawn builders
*/
+#include <linux/cred.h>
#include <linux/file.h>
#include <linux/fs.h>
+#include <linux/mm.h>
+#include <linux/mutex.h>
+#include <linux/namei.h>
#include <linux/pid.h>
+#include <linux/pid_namespace.h>
#include <linux/pidfd_spawn.h>
#include <linux/pidfs.h>
#include <linux/refcount.h>
+#include <linux/sched/signal.h>
#include <linux/slab.h>
+#include <linux/syscalls.h>
+#include <linux/uaccess.h>
#include <uapi/linux/pidfd.h>
+#define PIDFD_SPAWN_MAX_CONFIG_KEY_SIZE 256
+
struct pidfd_spawn_state {
+ /* Serializes configuration and payload teardown. */
+ struct mutex lock;
refcount_t count;
struct pid *pid;
+ struct mm_struct *creator_mm;
+ const struct cred *creator_cred;
+ struct pid_namespace *creator_pid_ns;
+ char *staged_path;
};
static void pidfd_spawn_state_put(struct pidfd_spawn_state *state);
static void pidfd_spawn_free_state(struct pidfd_spawn_state *state)
{
+ kfree(state->staged_path);
+ if (state->creator_mm)
+ mmdrop(state->creator_mm);
+ if (state->creator_cred)
+ put_cred(state->creator_cred);
+ if (state->creator_pid_ns)
+ put_pid_ns(state->creator_pid_ns);
put_pid(state->pid);
kfree(state);
}
@@ -31,6 +54,15 @@ static void pidfd_spawn_state_put(struct pidfd_spawn_state *state)
pidfd_spawn_free_state(state);
}
+static void pidfd_spawn_state_put_cleanup(struct pidfd_spawn_state *state)
+{
+ if (!IS_ERR_OR_NULL(state))
+ pidfd_spawn_state_put(state);
+}
+
+DEFINE_FREE(pidfd_spawn_state, struct pidfd_spawn_state *,
+ pidfd_spawn_state_put_cleanup(_T))
+
static void pidfd_spawn_state_release(void *data)
{
pidfd_spawn_state_put(data);
@@ -50,17 +82,140 @@ static const struct pidfs_future_file_ops pidfd_spawn_future_ops = {
.release = pidfd_spawn_state_release,
};
+static struct pidfd_spawn_state *
+pidfd_spawn_state_get_file(struct file *file)
+{
+ struct pidfd_spawn_state *state;
+
+ state = pidfs_future_file_data(file, &pidfd_spawn_future_ops);
+ if (!state || !refcount_inc_not_zero(&state->count))
+ return ERR_PTR(state ? -ESRCH : -EINVAL);
+ return state;
+}
+
+static bool pidfd_spawn_same_creator(struct pidfd_spawn_state *state)
+{
+ return current->mm == state->creator_mm &&
+ current_cred() == state->creator_cred &&
+ current->nsproxy->pid_ns_for_children == state->creator_pid_ns;
+}
+
+static struct filename *pidfd_spawn_get_path(const char __user *value)
+{
+ CLASS(filename, name)(value);
+
+ if (IS_ERR(name))
+ return ERR_CAST(name);
+
+ return no_free_ptr(name);
+}
+
+static char *pidfd_spawn_copy_path(const char __user *value)
+{
+ struct filename *name __free(putname) = NULL;
+ char *path;
+
+ name = pidfd_spawn_get_path(value);
+ if (IS_ERR(name))
+ return ERR_CAST(name);
+ path = kstrdup(name->name, GFP_KERNEL_ACCOUNT);
+ if (!path)
+ return ERR_PTR(-ENOMEM);
+
+ return path;
+}
+
+static int pidfd_spawn_state_set_path(struct pidfd_spawn_state *state,
+ const char __user *value)
+{
+ char *path __free(kfree) = NULL;
+ char *old __free(kfree) = NULL;
+
+ path = pidfd_spawn_copy_path(value);
+ if (IS_ERR(path))
+ return PTR_ERR(path);
+
+ scoped_cond_guard(mutex_intr, return -EINTR, &state->lock) {
+ if (!pidfd_spawn_same_creator(state))
+ return -EPERM;
+
+ old = state->staged_path;
+ state->staged_path = no_free_ptr(path);
+ }
+ return 0;
+}
+
+static int pidfd_spawn_state_set_string(struct pidfd_spawn_state *state,
+ const char *key,
+ const void __user *value)
+{
+ if (!strcmp(key, PIDFD_CONFIG_KEY_PATH))
+ return pidfd_spawn_state_set_path(state, value);
+
+ return -EOPNOTSUPP;
+}
+
+SYSCALL_DEFINE5(pidfd_config, int, fd, unsigned int, cmd,
+ const char __user *, ukey, const void __user *, uvalue,
+ int, aux)
+{
+ char *key __free(kfree) = NULL;
+ struct pidfd_spawn_state *state __free(pidfd_spawn_state) = NULL;
+ int ret;
+
+ switch (cmd) {
+ case PIDFD_CONFIG_SET_STRING:
+ if (!ukey || !uvalue || aux)
+ return -EINVAL;
+ break;
+ default:
+ return -EOPNOTSUPP;
+ }
+
+ CLASS(fd, f)(fd);
+ if (fd_empty(f))
+ return -EBADF;
+
+ state = pidfd_spawn_state_get_file(fd_file(f));
+ if (IS_ERR(state))
+ return PTR_ERR(state);
+
+ key = strndup_user(ukey, PIDFD_SPAWN_MAX_CONFIG_KEY_SIZE);
+ if (IS_ERR(key))
+ return PTR_ERR(key);
+
+ switch (cmd) {
+ case PIDFD_CONFIG_SET_STRING:
+ ret = pidfd_spawn_state_set_string(state, key, uvalue);
+ break;
+ default:
+ ret = -EOPNOTSUPP;
+ break;
+ }
+
+ return ret;
+}
+
int pidfd_empty_open(unsigned int flags)
{
struct pidfd_spawn_state *state;
struct file *pidfile;
int pidfd;
+ if (!current->mm)
+ return -EINVAL;
+
state = kzalloc_obj(*state, GFP_KERNEL_ACCOUNT);
if (!state)
return -ENOMEM;
+ mutex_init(&state->lock);
refcount_set(&state->count, 1);
+ mmgrab(current->mm);
+ state->creator_mm = current->mm;
+ state->creator_cred = get_current_cred();
+ state->creator_pid_ns =
+ get_pid_ns(current->nsproxy->pid_ns_for_children);
pidfile = pidfs_alloc_future_file("[pidfd_spawn]", state,
&pidfd_spawn_future_ops,
--
2.52.0
^ permalink raw reply related
* [RFC PATCH 04/24] pidfd: create taskless spawn builders
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: 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, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>
Add a pidfs-backed constructor for taskless spawn-builder files. The inode
owns the builder state, so procfd reopens and bind mounts retain it without
creating per-file lifetime rules.
The constructor allocates no task, struct pid, numeric PID, or
process-count charge. Until a later run operation publishes a pid, ordinary
pidfd operations resolve the file as -ESRCH. Preserve that error through
setns() and pidfd_send_signal() instead of translating a valid future pidfd
into an unrelated descriptor error. Assert that PIDFD_EMPTY does not
overlap a valid open flag, so future collisions fail the build instead of
silently aliasing builder creation.
Keep the public pidfd_open() entry point disabled until the complete spawn
state machine is wired later in the series.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
MAINTAINERS | 2 +
fs/Makefile | 2 +-
fs/pidfd_spawn.c | 81 +++++++++++++++++++++++++++++++++++++
fs/pidfs.c | 5 ++-
include/linux/pidfd_spawn.h | 9 +++++
kernel/nsproxy.c | 11 +++--
kernel/signal.c | 2 +-
7 files changed, 106 insertions(+), 6 deletions(-)
create mode 100644 fs/pidfd_spawn.c
create mode 100644 include/linux/pidfd_spawn.h
diff --git a/MAINTAINERS b/MAINTAINERS
index b63bc1ee0171f..8d7f94f09f414 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -21287,6 +21287,8 @@ M: Christian Brauner <christian@brauner.io>
L: linux-kernel@vger.kernel.org
S: Maintained
T: git git://git.kernel.org/pub/scm/linux/kernel/git/brauner/linux.git
+F: fs/pidfd_spawn.c
+F: include/linux/pidfd_spawn.h
F: include/uapi/linux/pidfd_spawn.h
F: samples/pidfd/
F: tools/include/uapi/linux/pidfd_spawn.h
diff --git a/fs/Makefile b/fs/Makefile
index aa847be93bc6f..f24beb7c9b7dc 100644
--- a/fs/Makefile
+++ b/fs/Makefile
@@ -8,7 +8,7 @@
obj-y := open.o read_write.o file_table.o super.o \
- char_dev.o stat.o exec.o pipe.o namei.o fcntl.o \
+ char_dev.o stat.o exec.o pidfd_spawn.o pipe.o namei.o fcntl.o \
ioctl.o readdir.o select.o dcache.o inode.o \
attr.o bad_inode.o file.o filesystems.o namespace.o \
seq_file.o xattr.o libfs.o fs-writeback.o \
diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
new file mode 100644
index 0000000000000..ff2b0cb6365a5
--- /dev/null
+++ b/fs/pidfd_spawn.c
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * pidfd-backed process spawn builders
+ */
+
+#include <linux/file.h>
+#include <linux/fs.h>
+#include <linux/pid.h>
+#include <linux/pidfd_spawn.h>
+#include <linux/pidfs.h>
+#include <linux/refcount.h>
+#include <linux/slab.h>
+#include <uapi/linux/pidfd.h>
+
+struct pidfd_spawn_state {
+ refcount_t count;
+ struct pid *pid;
+};
+
+static void pidfd_spawn_state_put(struct pidfd_spawn_state *state);
+
+static void pidfd_spawn_free_state(struct pidfd_spawn_state *state)
+{
+ put_pid(state->pid);
+ kfree(state);
+}
+
+static void pidfd_spawn_state_put(struct pidfd_spawn_state *state)
+{
+ if (refcount_dec_and_test(&state->count))
+ pidfd_spawn_free_state(state);
+}
+
+static void pidfd_spawn_state_release(void *data)
+{
+ pidfd_spawn_state_put(data);
+}
+
+static struct pid *pidfd_spawn_file_pid(void *data)
+{
+ struct pidfd_spawn_state *state = data;
+ struct pid *pid;
+
+ pid = READ_ONCE(state->pid);
+ return pid ? pid : ERR_PTR(-ESRCH);
+}
+
+static const struct pidfs_future_file_ops pidfd_spawn_future_ops = {
+ .get_pid = pidfd_spawn_file_pid,
+ .release = pidfd_spawn_state_release,
+};
+
+int pidfd_empty_open(unsigned int flags)
+{
+ struct pidfd_spawn_state *state;
+ struct file *pidfile;
+ int pidfd;
+
+ state = kzalloc_obj(*state, GFP_KERNEL_ACCOUNT);
+ if (!state)
+ return -ENOMEM;
+
+ refcount_set(&state->count, 1);
+
+ pidfile = pidfs_alloc_future_file("[pidfd_spawn]", state,
+ &pidfd_spawn_future_ops,
+ O_RDWR | flags);
+ if (IS_ERR(pidfile)) {
+ pidfd_spawn_state_put(state);
+ return PTR_ERR(pidfile);
+ }
+
+ pidfd = get_unused_fd_flags(O_CLOEXEC);
+ if (pidfd < 0) {
+ fput(pidfile);
+ return pidfd;
+ }
+
+ fd_install(pidfd, pidfile);
+ return pidfd;
+}
diff --git a/fs/pidfs.c b/fs/pidfs.c
index ebd8cc463811b..28464fe274c9e 100644
--- a/fs/pidfs.c
+++ b/fs/pidfs.c
@@ -2,6 +2,7 @@
#include <linux/anon_inodes.h>
#include <linux/compat.h>
#include <linux/exportfs.h>
+#include <linux/fcntl.h>
#include <linux/file.h>
#include <linux/fs.h>
#include <linux/cgroup.h>
@@ -1261,7 +1262,9 @@ struct file *pidfs_alloc_file(struct pid *pid, unsigned int flags)
* other or with uapi pidfd flags.
*/
BUILD_BUG_ON(hweight32(PIDFD_THREAD | PIDFD_NONBLOCK |
- PIDFD_STALE | PIDFD_AUTOKILL) != 4);
+ PIDFD_EMPTY | PIDFD_STALE |
+ PIDFD_AUTOKILL) != 5);
+ BUILD_BUG_ON(PIDFD_EMPTY & VALID_OPEN_FLAGS);
ret = path_from_stashed(&pid->stashed, pidfs_mnt, get_pid(pid), &path);
if (ret < 0)
diff --git a/include/linux/pidfd_spawn.h b/include/linux/pidfd_spawn.h
new file mode 100644
index 0000000000000..8df168cf0c2f9
--- /dev/null
+++ b/include/linux/pidfd_spawn.h
@@ -0,0 +1,9 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _LINUX_PIDFD_SPAWN_H
+#define _LINUX_PIDFD_SPAWN_H
+
+#include <uapi/linux/pidfd_spawn.h>
+
+int pidfd_empty_open(unsigned int flags);
+
+#endif /* _LINUX_PIDFD_SPAWN_H */
diff --git a/kernel/nsproxy.c b/kernel/nsproxy.c
index d9d3d5973bf52..20befb6185e2d 100644
--- a/kernel/nsproxy.c
+++ b/kernel/nsproxy.c
@@ -581,10 +581,15 @@ SYSCALL_DEFINE2(setns, int, fd, int, flags)
if (flags && (ns->ns_type != flags))
err = -EINVAL;
flags = ns->ns_type;
- } else if (!IS_ERR(pidfd_pid(fd_file(f)))) {
- err = check_setns_flags(flags);
} else {
- err = -EINVAL;
+ struct pid *pid = pidfd_pid(fd_file(f));
+
+ if (!IS_ERR(pid))
+ err = check_setns_flags(flags);
+ else if (PTR_ERR(pid) == -ESRCH)
+ err = -ESRCH;
+ else
+ err = -EINVAL;
}
if (err)
goto out;
diff --git a/kernel/signal.c b/kernel/signal.c
index fdee0b012a117..4c7d95981263e 100644
--- a/kernel/signal.c
+++ b/kernel/signal.c
@@ -3996,7 +3996,7 @@ static struct pid *pidfd_to_pid(const struct file *file)
struct pid *pid;
pid = pidfd_pid(file);
- if (!IS_ERR(pid))
+ if (!IS_ERR(pid) || PTR_ERR(pid) != -EBADF)
return pid;
return tgid_pidfd_to_pid(file);
--
2.52.0
^ permalink raw reply related
* [RFC PATCH 03/24] pidfs: add taskless future pidfd inodes
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: 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, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>
Teach pidfs inodes to represent either a struct pid or a typed future
object. Use the ordinary pidfs file operations for both kinds and
resolve a future object through its callback.
Allocate a stable inode identity without allocating a task or struct
pid. Ordinary pidfd operations return -ESRCH until the future producer
publishes a process, while inode-only GETVERSION remains available.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
fs/pidfs.c | 254 ++++++++++++++++++++++++++++++++++++++----
include/linux/pid.h | 10 ++
include/linux/pidfs.h | 21 ++++
3 files changed, 262 insertions(+), 23 deletions(-)
diff --git a/fs/pidfs.c b/fs/pidfs.c
index c55f46c32801d..ebd8cc463811b 100644
--- a/fs/pidfs.c
+++ b/fs/pidfs.c
@@ -25,6 +25,7 @@
#include <net/net_namespace.h>
#include <linux/coredump.h>
#include <linux/rhashtable.h>
+#include <linux/security.h>
#include <linux/llist.h>
#include <linux/xattr.h>
#include <linux/cookie.h>
@@ -35,6 +36,8 @@
#define PIDFS_PID_DEAD ERR_PTR(-ESRCH)
static struct kmem_cache *pidfs_attr_cachep __ro_after_init;
+static const struct file_operations pidfs_file_operations;
+static struct vfsmount *pidfs_mnt __ro_after_init;
static struct path pidfs_root_path = {};
@@ -106,6 +109,64 @@ struct pidfs_attr {
};
};
+struct pidfs_future_file {
+ struct pidfs_node node;
+ void *data;
+ const struct pidfs_future_file_ops *ops;
+ struct pidfs_attr *attr;
+ u64 ino;
+};
+
+static struct pidfs_node *pidfs_inode_node(const struct inode *inode)
+{
+ if (!pidfs_mnt || inode->i_sb != pidfs_mnt->mnt_sb ||
+ inode->i_fop != &pidfs_file_operations)
+ return NULL;
+ return inode->i_private;
+}
+
+static struct pidfs_future_file *
+pidfs_future_file(const struct inode *inode)
+{
+ struct pidfs_node *node = pidfs_inode_node(inode);
+
+ if (!node || node->type != PIDFS_NODE_FUTURE)
+ return NULL;
+ return container_of(node, struct pidfs_future_file, node);
+}
+
+void *pidfs_future_file_data(const struct file *file,
+ const struct pidfs_future_file_ops *ops)
+{
+ struct pidfs_future_file *future;
+
+ if (file->f_op != &pidfs_file_operations)
+ return NULL;
+ future = pidfs_future_file(file_inode(file));
+ return future && future->ops == ops ? future->data : NULL;
+}
+
+static struct pid *pidfs_inode_pid(const struct inode *inode)
+{
+ struct pidfs_future_file *future;
+ struct pidfs_node *node;
+ struct pid *pid;
+
+ node = pidfs_inode_node(inode);
+ if (!node)
+ return ERR_PTR(-EBADF);
+ if (node->type == PIDFS_NODE_PID)
+ return container_of(node, struct pid, pidfs_node);
+
+ future = pidfs_future_file(inode);
+ if (!future || !future->ops || !future->ops->get_pid)
+ return ERR_PTR(-EBADF);
+ pid = future->ops->get_pid(future->data);
+ if (WARN_ON_ONCE(!pid))
+ return ERR_PTR(-EBADF);
+ return pid;
+}
+
#if BITS_PER_LONG == 32
DEFINE_SPINLOCK(pidfs_ino_lock);
@@ -166,6 +227,7 @@ static u64 pidfs_alloc_ino(void)
void pidfs_prepare_pid(struct pid *pid)
{
+ pid->pidfs_node.type = PIDFS_NODE_PID;
pid->stashed = NULL;
pid->attr = NULL;
pid->ino = 0;
@@ -275,7 +337,7 @@ static void pidfd_show_fdinfo(struct seq_file *m, struct file *f)
struct pid_namespace *ns;
pid_t nr = -1;
- if (likely(pid_has_task(pid, PIDTYPE_PID))) {
+ if (!IS_ERR(pid) && likely(pid_has_task(pid, PIDTYPE_PID))) {
ns = proc_pid_ns(file_inode(m->file)->i_sb);
nr = pid_nr_ns(pid, ns);
}
@@ -305,11 +367,15 @@ static void pidfd_show_fdinfo(struct seq_file *m, struct file *f)
*/
static __poll_t pidfd_poll(struct file *file, struct poll_table_struct *pts)
{
- struct pid *pid = pidfd_pid(file);
struct task_struct *task;
__poll_t poll_flags = 0;
+ struct pid *pid = pidfd_pid(file);
+
+ if (IS_ERR(pid))
+ return PTR_ERR(pid) == -ESRCH ? 0 : EPOLLHUP;
poll_wait(file, &pid->wait_pidfd, pts);
+
/*
* Don't wake waiters if the thread-group leader exited
* prematurely. They either get notified when the last subthread
@@ -376,6 +442,8 @@ static long pidfd_info(struct file *file, unsigned int cmd, unsigned long arg)
BUILD_BUG_ON(sizeof(struct pidfd_info) != PIDFD_INFO_SIZE_VER3);
+ if (IS_ERR(pid))
+ return PTR_ERR(pid);
if (!uinfo)
return -EINVAL;
if (usize < PIDFD_INFO_SIZE_VER0)
@@ -532,6 +600,7 @@ static long pidfd_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
struct task_struct *task __free(put_task) = NULL;
struct nsproxy *nsp __free(put_nsproxy) = NULL;
struct ns_common *ns_common = NULL;
+ struct pid *pid;
if (!pidfs_ioctl_valid(cmd))
return -ENOIOCTLCMD;
@@ -544,11 +613,15 @@ static long pidfd_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
return put_user(file_inode(file)->i_generation, argp);
}
+ pid = pidfd_pid(file);
+ if (IS_ERR(pid))
+ return PTR_ERR(pid);
+
/* Extensible IOCTL that does not open namespace FDs, take a shortcut */
if (_IOC_NR(cmd) == _IOC_NR(PIDFD_GET_INFO))
return pidfd_info(file, cmd, arg);
- task = get_pid_task(pidfd_pid(file), PIDTYPE_PID);
+ task = get_pid_task(pid, PIDTYPE_PID);
if (!task)
return -ESRCH;
@@ -673,11 +746,14 @@ static long pidfd_compat_ioctl(struct file *file, unsigned int cmd,
static int pidfs_file_release(struct inode *inode, struct file *file)
{
- struct pid *pid = inode->i_private;
+ struct pid *pid;
struct task_struct *task;
if (!(file->f_flags & PIDFD_AUTOKILL))
return 0;
+ pid = pidfd_pid(file);
+ if (IS_ERR(pid))
+ return 0;
guard(rcu)();
task = pid_task(pid, PIDTYPE_TGID);
@@ -705,9 +781,9 @@ static const struct file_operations pidfs_file_operations = {
struct pid *pidfd_pid(const struct file *file)
{
- if (file->f_op != &pidfs_file_operations)
+ if (unlikely(file->f_op != &pidfs_file_operations))
return ERR_PTR(-EBADF);
- return file_inode(file)->i_private;
+ return pidfs_inode_pid(file_inode(file));
}
/*
@@ -797,8 +873,6 @@ void pidfs_coredump(const struct coredump_params *cprm)
}
#endif
-static struct vfsmount *pidfs_mnt __ro_after_init;
-
/*
* The vfs falls back to simple_setattr() if i_op->setattr() isn't
* implemented. Let's reject it completely until we have a clean
@@ -820,7 +894,10 @@ static int pidfs_getattr(struct mnt_idmap *idmap, const struct path *path,
static ssize_t pidfs_listxattr(struct dentry *dentry, char *buf, size_t size)
{
struct inode *inode = d_inode(dentry);
- struct pid *pid = inode->i_private;
+ struct pid *pid = pidfs_inode_pid(inode);
+
+ if (IS_ERR(pid))
+ return PTR_ERR(pid);
return simple_xattr_list(inode, &pid->attr->xattrs, buf, size);
}
@@ -833,10 +910,25 @@ static const struct inode_operations pidfs_inode_operations = {
static void pidfs_evict_inode(struct inode *inode)
{
- struct pid *pid = inode->i_private;
+ struct pidfs_future_file *future;
+ struct pidfs_node *node = pidfs_inode_node(inode);
clear_inode(inode);
- put_pid(pid);
+ if (!node)
+ return;
+ if (node->type == PIDFS_NODE_PID) {
+ put_pid(container_of(node, struct pid, pidfs_node));
+ return;
+ }
+ if (WARN_ON_ONCE(node->type != PIDFS_NODE_FUTURE))
+ return;
+
+ future = container_of(node, struct pidfs_future_file, node);
+ if (future->ops->release)
+ future->ops->release(future->data);
+ if (future->attr)
+ kmem_cache_free(pidfs_attr_cachep, future->attr);
+ kfree(future);
}
static const struct super_operations pidfs_sops = {
@@ -854,15 +946,24 @@ static char *pidfs_dname(struct dentry *dentry, char *buffer, int buflen)
return dynamic_dname(buffer, buflen, "anon_inode:[pidfd]");
}
+static void pidfs_dentry_prune(struct dentry *dentry)
+{
+ if (dentry->d_fsdata)
+ stashed_dentry_prune(dentry);
+}
+
const struct dentry_operations pidfs_dentry_operations = {
.d_dname = pidfs_dname,
- .d_prune = stashed_dentry_prune,
+ .d_prune = pidfs_dentry_prune,
};
static int pidfs_encode_fh(struct inode *inode, u32 *fh, int *max_len,
struct inode *parent)
{
- const struct pid *pid = inode->i_private;
+ const struct pid *pid = pidfs_inode_pid(inode);
+
+ if (IS_ERR(pid))
+ return FILEID_INVALID;
if (*max_len < 2) {
*max_len = 2;
@@ -974,22 +1075,36 @@ static const struct export_operations pidfs_export_operations = {
.permission = pidfs_export_permission,
};
-static int pidfs_init_inode(struct inode *inode, void *data)
+static void pidfs_init_common_inode(struct inode *inode,
+ struct pidfs_node *node, u64 ino)
{
- const struct pid *pid = data;
-
- inode->i_private = data;
+ inode->i_private = node;
inode->i_flags |= S_PRIVATE | S_ANON_INODE;
/* We allow to set xattrs. */
inode->i_flags &= ~S_IMMUTABLE;
- inode->i_mode |= S_IRWXU;
+ inode->i_mode = S_IFREG | 0700;
+ inode->i_uid = GLOBAL_ROOT_UID;
+ inode->i_gid = GLOBAL_ROOT_GID;
inode->i_op = &pidfs_inode_operations;
inode->i_fop = &pidfs_file_operations;
- inode->i_ino = pidfs_ino(pid->ino);
- inode->i_generation = pidfs_gen(pid->ino);
+ inode->i_ino = pidfs_ino(ino);
+ inode->i_generation = pidfs_gen(ino);
+}
+
+static int pidfs_init_inode(struct inode *inode, void *data)
+{
+ struct pid *pid = data;
+
+ pidfs_init_common_inode(inode, &pid->pidfs_node, pid->ino);
return 0;
}
+static bool pidfs_inode_data_matches(const struct inode *inode,
+ const void *data)
+{
+ return pidfs_inode_pid(inode) == data;
+}
+
static void pidfs_put_data(void *data)
{
struct pid *pid = data;
@@ -1044,9 +1159,11 @@ int pidfs_register_pid_gfp(struct pid *pid, gfp_t gfp)
static struct dentry *pidfs_stash_dentry(struct dentry **stashed,
struct dentry *dentry)
{
+ struct pid *pid = pidfs_inode_pid(d_inode(dentry));
int ret;
- struct pid *pid = d_inode(dentry)->i_private;
+ if (WARN_ON_ONCE(IS_ERR(pid)))
+ return ERR_CAST(pid);
VFS_WARN_ON_ONCE(stashed != &pid->stashed);
ret = pidfs_register_pid(pid);
@@ -1060,15 +1177,19 @@ static const struct stashed_operations pidfs_stashed_ops = {
.stash_dentry = pidfs_stash_dentry,
.init_inode = pidfs_init_inode,
.put_data = pidfs_put_data,
+ .data_matches = pidfs_inode_data_matches,
};
static int pidfs_xattr_get(const struct xattr_handler *handler,
struct dentry *unused, struct inode *inode,
const char *suffix, void *value, size_t size)
{
- struct pid *pid = inode->i_private;
+ struct pid *pid = pidfs_inode_pid(inode);
const char *name = xattr_full_name(handler, suffix);
+ if (IS_ERR(pid))
+ return PTR_ERR(pid);
+
return simple_xattr_get(&pidfs_xa_cache, &pid->attr->xattrs, name, value, size);
}
@@ -1077,10 +1198,13 @@ static int pidfs_xattr_set(const struct xattr_handler *handler,
struct inode *inode, const char *suffix,
const void *value, size_t size, int flags)
{
- struct pid *pid = inode->i_private;
+ struct pid *pid = pidfs_inode_pid(inode);
const char *name = xattr_full_name(handler, suffix);
struct simple_xattr *old_xattr;
+ if (IS_ERR(pid))
+ return PTR_ERR(pid);
+
/* Ensure we're the only one to set @attr->xattrs. */
WARN_ON_ONCE(!inode_is_locked(inode));
@@ -1158,6 +1282,90 @@ struct file *pidfs_alloc_file(struct pid *pid, unsigned int flags)
return pidfd_file;
}
+/**
+ * pidfs_alloc_future_file - allocate a taskless pidfs file
+ * @name: anonymous file name used by LSM initialization
+ * @data: producer data retained for the inode lifetime
+ * @ops: callbacks that resolve and release @data
+ * @flags: file status flags
+ *
+ * Ownership of @data transfers to the returned file on success. One serialized
+ * producer owns all later association and publication transitions. It may
+ * associate one preallocated pid with
+ * pidfs_future_file_set_pid(), publish its pidfs metadata with
+ * pidfs_future_file_publish_pid(), make @ops->get_pid() resolve that pid, and
+ * finally wake waiters with pidfs_future_file_notify().
+ *
+ * A task associated with the future pid must not become runnable before
+ * publication metadata and exit-wakeup forwarding are installed.
+ *
+ * Return: A new pidfs file on success or an error pointer on failure.
+ */
+struct file *pidfs_alloc_future_file(const char *name, void *data,
+ const struct pidfs_future_file_ops *ops,
+ unsigned int flags)
+{
+ struct pidfs_future_file *future;
+ struct inode *inode;
+ struct file *file;
+ u64 ino;
+ int ret;
+
+ if (!ops || !ops->get_pid)
+ return ERR_PTR(-EINVAL);
+
+ future = kzalloc_obj(*future, GFP_KERNEL_ACCOUNT);
+ if (!future)
+ return ERR_PTR(-ENOMEM);
+ future->attr = kmem_cache_zalloc(pidfs_attr_cachep, GFP_KERNEL_ACCOUNT);
+ if (!future->attr) {
+ kfree(future);
+ return ERR_PTR(-ENOMEM);
+ }
+ INIT_LIST_HEAD_RCU(&future->attr->xattrs);
+
+ inode = new_inode_pseudo(pidfs_mnt->mnt_sb);
+ if (!inode) {
+ kmem_cache_free(pidfs_attr_cachep, future->attr);
+ kfree(future);
+ return ERR_PTR(-ENOMEM);
+ }
+ /* Preserve the anonymous-inode creation check for future pidfds. */
+ ret = security_inode_init_security_anon(inode, &QSTR(name), NULL);
+ if (ret) {
+ iput(inode);
+ kmem_cache_free(pidfs_attr_cachep, future->attr);
+ kfree(future);
+ return ERR_PTR(ret);
+ }
+ ino = pidfs_alloc_ino();
+ simple_inode_init_ts(inode);
+ pidfs_init_common_inode(inode, NULL, ino);
+
+ file = alloc_file_pseudo(inode, pidfs_mnt, name, flags,
+ &pidfs_file_operations);
+ if (IS_ERR(file)) {
+ iput(inode);
+ kmem_cache_free(pidfs_attr_cachep, future->attr);
+ kfree(future);
+ return file;
+ }
+
+ future->node.type = PIDFS_NODE_FUTURE;
+ future->data = data;
+ future->ops = ops;
+ future->ino = ino;
+ inode->i_private = &future->node;
+ return file;
+}
+
+u64 pidfs_future_file_ino(const struct file *file)
+{
+ struct pidfs_future_file *future = pidfs_future_file(file_inode(file));
+
+ return future ? future->ino : 0;
+}
+
void __init pidfs_init(void)
{
if (rhashtable_init(&pidfs_ino_ht, &pidfs_ino_ht_params))
diff --git a/include/linux/pid.h b/include/linux/pid.h
index ddaef0bbc8ba3..a29ffe2a5fa8e 100644
--- a/include/linux/pid.h
+++ b/include/linux/pid.h
@@ -50,6 +50,15 @@
struct pidfs_attr;
+enum pidfs_node_type {
+ PIDFS_NODE_PID,
+ PIDFS_NODE_FUTURE,
+};
+
+struct pidfs_node {
+ enum pidfs_node_type type;
+};
+
struct upid {
int nr;
struct pid_namespace *ns;
@@ -59,6 +68,7 @@ struct pid {
refcount_t count;
unsigned int level;
spinlock_t lock;
+ struct pidfs_node pidfs_node;
struct {
u64 ino;
struct rhash_head pidfs_hash;
diff --git a/include/linux/pidfs.h b/include/linux/pidfs.h
index 0abf7da9ab236..6b7fbc54ab388 100644
--- a/include/linux/pidfs.h
+++ b/include/linux/pidfs.h
@@ -5,8 +5,29 @@
#include <linux/gfp_types.h>
struct coredump_params;
+struct file;
+struct pid;
+
+/**
+ * struct pidfs_future_file_ops - callbacks for a taskless pidfs file
+ * @get_pid: Return the published, borrowed, non-NULL process identity, or an
+ * error pointer while the producer is still taskless. The producer must
+ * keep the returned pid alive for the future inode lifetime.
+ * @release: Optionally release producer-owned data when the pidfs inode is
+ * evicted. If provided, this is called at most once.
+ */
+struct pidfs_future_file_ops {
+ struct pid *(*get_pid)(void *data);
+ void (*release)(void *data);
+};
struct file *pidfs_alloc_file(struct pid *pid, unsigned int flags);
+struct file *pidfs_alloc_future_file(const char *name, void *data,
+ const struct pidfs_future_file_ops *ops,
+ unsigned int flags);
+void *pidfs_future_file_data(const struct file *file,
+ const struct pidfs_future_file_ops *ops);
+u64 pidfs_future_file_ino(const struct file *file);
void __init pidfs_init(void);
void pidfs_prepare_pid(struct pid *pid);
int pidfs_add_pid(struct pid *pid);
--
2.52.0
^ permalink raw reply related
* [RFC PATCH 02/24] libfs: allow custom validation of stashed inode data
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: 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, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>
path_from_stashed() assumes that inode->i_private is identical to the
caller data used to find or create a dentry. That is too strict for an
inode whose stable identity can later resolve through another object.
Add an optional data_matches() callback to stashed_operations. Existing
users retain the pointer-identity check when no callback is provided.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
fs/internal.h | 1 +
fs/libfs.c | 5 ++++-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/fs/internal.h b/fs/internal.h
index 174f063575558..71cc43e72b33e 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -334,6 +334,7 @@ struct stashed_operations {
struct dentry *dentry);
void (*put_data)(void *data);
int (*init_inode)(struct inode *inode, void *data);
+ bool (*data_matches)(const struct inode *inode, const void *data);
};
int path_from_stashed(struct dentry **stashed, struct vfsmount *mnt, void *data,
struct path *path);
diff --git a/fs/libfs.c b/fs/libfs.c
index 5a0d276379d13..68cc1671b4699 100644
--- a/fs/libfs.c
+++ b/fs/libfs.c
@@ -2260,7 +2260,10 @@ int path_from_stashed(struct dentry **stashed, struct vfsmount *mnt, void *data,
path->dentry = res;
path->mnt = mntget(mnt);
VFS_WARN_ON_ONCE(path->dentry->d_fsdata != stashed);
- VFS_WARN_ON_ONCE(d_inode(path->dentry)->i_private != data);
+ if (sops->data_matches)
+ VFS_WARN_ON_ONCE(!sops->data_matches(d_inode(path->dentry), data));
+ else
+ VFS_WARN_ON_ONCE(d_inode(path->dentry)->i_private != data);
return 0;
}
--
2.52.0
^ permalink raw reply related
* [RFC PATCH 01/24] pidfd: add spawn builder uapi
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: 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, Li Chen
In-Reply-To: <cover.1784204592.git.me@linux.beauty>
Define PIDFD_EMPTY, the fsconfig-style configuration command and path
key, and versioned run arguments for pidfd spawn builders. Add the
initial ordered file-action records.
Store userspace pointers in full-width aligned containers on every ABI.
Keep flags and reserved fields zero so later kernels can extend the
structures without changing their initial layout.
Suggested-by: Christian Brauner <brauner@kernel.org>
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
MAINTAINERS | 2 ++
include/uapi/linux/pidfd.h | 1 +
include/uapi/linux/pidfd_spawn.h | 49 ++++++++++++++++++++++++++
tools/include/uapi/linux/pidfd_spawn.h | 49 ++++++++++++++++++++++++++
4 files changed, 101 insertions(+)
create mode 100644 include/uapi/linux/pidfd_spawn.h
create mode 100644 tools/include/uapi/linux/pidfd_spawn.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 8729cea57c3dd..b63bc1ee0171f 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -21287,7 +21287,9 @@ M: Christian Brauner <christian@brauner.io>
L: linux-kernel@vger.kernel.org
S: Maintained
T: git git://git.kernel.org/pub/scm/linux/kernel/git/brauner/linux.git
+F: include/uapi/linux/pidfd_spawn.h
F: samples/pidfd/
+F: tools/include/uapi/linux/pidfd_spawn.h
F: tools/testing/selftests/clone3/
F: tools/testing/selftests/pidfd/
K: (?i)pidfd
diff --git a/include/uapi/linux/pidfd.h b/include/uapi/linux/pidfd.h
index 0919246a1611c..cad5b722e9f0e 100644
--- a/include/uapi/linux/pidfd.h
+++ b/include/uapi/linux/pidfd.h
@@ -10,6 +10,7 @@
/* Flags for pidfd_open(). */
#define PIDFD_NONBLOCK O_NONBLOCK
#define PIDFD_THREAD O_EXCL
+#define PIDFD_EMPTY (1U << 27)
#ifdef __KERNEL__
#include <linux/sched.h>
#define PIDFD_STALE CLONE_PIDFD
diff --git a/include/uapi/linux/pidfd_spawn.h b/include/uapi/linux/pidfd_spawn.h
new file mode 100644
index 0000000000000..46d61e72435cc
--- /dev/null
+++ b/include/uapi/linux/pidfd_spawn.h
@@ -0,0 +1,49 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+
+#ifndef _UAPI_LINUX_PIDFD_SPAWN_H
+#define _UAPI_LINUX_PIDFD_SPAWN_H
+
+#include <linux/types.h>
+
+#define PIDFD_SPAWN_RUN_SIZE_VER0 64
+#define PIDFD_SPAWN_ACTION_SIZE_VER0 32
+
+/* Commands for pidfd_config(). */
+#define PIDFD_CONFIG_SET_STRING 0
+
+/* String keys for pidfd_config(). */
+#define PIDFD_CONFIG_KEY_PATH "path"
+
+struct pidfd_spawn_run_args {
+ __u32 flags;
+ __u32 nr_actions;
+ /* The next four fields are full-width userspace virtual addresses. */
+ __aligned_u64 path;
+ __aligned_u64 argv;
+ __aligned_u64 envp;
+ __aligned_u64 actions;
+ __u32 action_size;
+ __u32 reserved0;
+ __u64 reserved[2];
+};
+
+enum pidfd_spawn_action_type {
+ PIDFD_SPAWN_ACTION_DUP2 = 0,
+ PIDFD_SPAWN_ACTION_CLOSE_RANGE = 1,
+ PIDFD_SPAWN_ACTION_FCHDIR = 2,
+};
+
+struct pidfd_spawn_action {
+ __u32 type;
+ __u32 flags;
+ /*
+ * DUP2 uses fd as the source and newfd as the destination.
+ * CLOSE_RANGE uses fd as the first and newfd as the last descriptor.
+ * FCHDIR uses fd as the directory descriptor and requires newfd to be 0.
+ */
+ __u32 fd;
+ __u32 newfd;
+ __u64 reserved[2];
+};
+
+#endif /* _UAPI_LINUX_PIDFD_SPAWN_H */
diff --git a/tools/include/uapi/linux/pidfd_spawn.h b/tools/include/uapi/linux/pidfd_spawn.h
new file mode 100644
index 0000000000000..46d61e72435cc
--- /dev/null
+++ b/tools/include/uapi/linux/pidfd_spawn.h
@@ -0,0 +1,49 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+
+#ifndef _UAPI_LINUX_PIDFD_SPAWN_H
+#define _UAPI_LINUX_PIDFD_SPAWN_H
+
+#include <linux/types.h>
+
+#define PIDFD_SPAWN_RUN_SIZE_VER0 64
+#define PIDFD_SPAWN_ACTION_SIZE_VER0 32
+
+/* Commands for pidfd_config(). */
+#define PIDFD_CONFIG_SET_STRING 0
+
+/* String keys for pidfd_config(). */
+#define PIDFD_CONFIG_KEY_PATH "path"
+
+struct pidfd_spawn_run_args {
+ __u32 flags;
+ __u32 nr_actions;
+ /* The next four fields are full-width userspace virtual addresses. */
+ __aligned_u64 path;
+ __aligned_u64 argv;
+ __aligned_u64 envp;
+ __aligned_u64 actions;
+ __u32 action_size;
+ __u32 reserved0;
+ __u64 reserved[2];
+};
+
+enum pidfd_spawn_action_type {
+ PIDFD_SPAWN_ACTION_DUP2 = 0,
+ PIDFD_SPAWN_ACTION_CLOSE_RANGE = 1,
+ PIDFD_SPAWN_ACTION_FCHDIR = 2,
+};
+
+struct pidfd_spawn_action {
+ __u32 type;
+ __u32 flags;
+ /*
+ * DUP2 uses fd as the source and newfd as the destination.
+ * CLOSE_RANGE uses fd as the first and newfd as the last descriptor.
+ * FCHDIR uses fd as the directory descriptor and requires newfd to be 0.
+ */
+ __u32 fd;
+ __u32 newfd;
+ __u64 reserved[2];
+};
+
+#endif /* _UAPI_LINUX_PIDFD_SPAWN_H */
--
2.52.0
^ permalink raw reply related
* [RFC PATCH 00/24] pidfd: add a minimal process spawn builder
From: Li Chen @ 2026-07-16 14:31 UTC (permalink / raw)
To: Christian Brauner
Cc: 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
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",
.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);
pidfd_spawn_run(fd, &run, sizeof(run));
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: [BUG] Landlock denies LANDLOCK_ACCESS_FS_EXECUTE despite a correctly-anchored PathBeneath rule (contradicts selftest layout1.execute)
From: Mickaël Salaün @ 2026-07-16 9:37 UTC (permalink / raw)
To: Günther Noack
Cc: Ken Grimes, linux-security-module@vger.kernel.org, landlock
In-Reply-To: <aldGXyfBQz8GsAMn@google.com>
Hi!
You should also get a look at the audit logs:
https://docs.kernel.org/admin-guide/LSM/landlock.html#audit
Mickaël
On Wed, Jul 15, 2026 at 10:39:29AM +0200, Günther Noack wrote:
> Hello Ken!
>
> (Also adding landlock@lists.linux.dev to CC)
>
> On Tue, Jul 14, 2026 at 09:07:56PM +0000, Ken Grimes wrote:
> > Hey all, this is my first bug report for linux. The issue was discovered
> > alongside llm-assisted coding on a downstream project. The
> > investigation/testing of the bug was a mostly manual process so I could
> > be sure this was something real. Please let me know if I can provide any
> > further details or assistance. Hope this is helpful, thank you for all of your
> > hard work!
>
> Welcome and thanks for reporting your first issue!
>
> I believe the issue you are observing is that the /bin/true program you are
> starting is a dynamically linked executable. As such, executing it requires
> both the LANDLOCK_ACCESS_FS_EXECUTE right on the binary itself and on
> the system's dynamic loader binary, which usually lives in /lib/ld-linux.so.*
> (but there are symlinks and 32/64-bit differences at play as well, which
> influence the actual final location).
>
> You can try this out with the following experiments:
>
> (1) Compile a "true" program statically and try using that:
>
> $ echo 'int main() { return 0; }' > true.c
> $ CFLAGS=-static make true
>
> This can be started with the test you have,
> unlike the dynamically linked version.
>
> (2) Alternatively, add execute permissions for the dynamic loader:
>
> Add an additional "path beneath" rule that allow-lists the execution
> access right on /lib/ld-linux.so.2, /lib64/ld-linux-x86-64.so.2 or
> wherever else your dynamic loader is. (You can discover the actual
> location using "ldd /bin/true".)
>
> With either one of these two changes, your standalone reproducer program
> starts working again. Or at least it does on my machine. If it still doesn't
> work on your end that way, please let us know. :)
>
> I admit that we should probably point this out in the Landlock documentation
> for the LANDLOCK_ACCESS_FS_EXECUTE right, as dynamic linking is common and it
> is a potential issue that many people might run into.
>
> For background on the dynamic loading mechanism, see the man page ld.so(8) [1]
> and the LWN article "How programs get run: ELF binaries" [2] (specifically the
> section "Dynamically linked programs").
>
> —Günther
>
>
> [1] https://man7.org/linux/man-pages/man8/ld.so.8.html
> [2] https://lwn.net/Articles/631631/
>
>
^ permalink raw reply
* Re: [PATCH -next,v2] ima: add cond_resched() in ima_calc_file_hash_tfm loop
From: Roberto Sassu @ 2026-07-16 7:29 UTC (permalink / raw)
To: Gaosheng Cui, lujialin4, zohar, roberto.sassu, dmitry.kasatkin,
eric.snowberg, paul, jmorris, serge
Cc: linux-integrity, linux-security-module
In-Reply-To: <e6270a062ebfdbc6d9e50ffe80ee5e77b4f0678c.camel@huaweicloud.com>
On 7/14/2026 11:26 AM, Roberto Sassu wrote:
> On Tue, 2026-07-14 at 17:17 +0800, Gaosheng Cui wrote:
>> When hashing large files, the while loop in ima_calc_file_hash_tfm
>> processes PAGE_SIZE chunks without any scheduling point, which can
>> cause soft lockup warnings:
>> watchdog: BUG: soft lockup - CPU#0 stuck for 50s!
>> Call Trace:
>> _sha256_update+0x12d/0x1a0
>> ima_calc_file_hash_tfm+0xfb/0x150
>> ima_calc_file_hash+0x6e/0x160
>> ima_collect_measurement+0x202/0x340
>> process_measurement+0x3a9/0xb30
>> ima_file_check+0x56/0xa0
>> do_open+0x11b/0x250
>> path_openat+0x10b/0x1d0
>> do_filp_open+0xa9/0x150
>> do_sys_openat2+0x223/0x2a0
>> __x64_sys_openat+0x54/0xa0
>> do_syscall_64+0x59/0x110
>> entry_SYSCALL_64_after_hwframe+0x78/0xe2
>>
>> Call cond_resched() every 4MB to yield the CPU when needed, rather
>> than at every loop iteration, to reduce overhead.
>>
>> Fixes: 3323eec921ef ("integrity: IMA as an integrity service provider")
>> Signed-off-by: Gaosheng Cui <cuigaosheng1@huawei.com>
>
> Reviewed-by: Roberto Sassu <roberto.sassu@huawei.com>
Actually, as Sashiko pointed out, alignment does not work well, since
__kernel_read() can return fewer than PAGE_SIZE bytes, causing the
offset to be always misaligned. Instead, we can keep a variable with the
number of bytes read since last cond_resched(), which we reset when we
hit 4 MB.
Sashiko also noted that the same mechanism could apply for
calc_buffer_shash_tfm(), but we can add it later if we have a warning
(note that in this case we already have the buffer, we are not doing
I/O).
Thanks
Roberto
> Thanks
>
> Roberto
>
>> ---
>> v2: call cond_resched() every 4MB to yield the CPU when needed
>> security/integrity/ima/ima_crypto.c | 3 +++
>> 1 file changed, 3 insertions(+)
>>
>> diff --git a/security/integrity/ima/ima_crypto.c b/security/integrity/ima/ima_crypto.c
>> index 0d72b48249ee..aab2349c0c33 100644
>> --- a/security/integrity/ima/ima_crypto.c
>> +++ b/security/integrity/ima/ima_crypto.c
>> @@ -233,6 +233,9 @@ static int ima_calc_file_hash_tfm(struct file *file,
>> rc = crypto_shash_update(shash, rbuf, rbuf_len);
>> if (rc)
>> break;
>> +
>> + if (IS_ALIGNED(offset, SZ_4M))
>> + cond_resched();
>> }
>> kfree(rbuf);
>> out:
>
^ permalink raw reply
* 障害者の就労を支援する事業 新規開業パートナー募集
From: 就労移行支援事業説明会 @ 2026-07-16 0:43 UTC (permalink / raw)
To: linux-security-module
下記エリアにおいて、大人の発達障害の方が働くことを支援する
「厚労省許認可」の就労移行支援事業の
新規開業パートナーを募集しています。
◇1エリア1企業様限定のエリアパートナー制となります◇
〜募集エリア〜
北海道/青森県/岩手県/宮城県/秋田県/山形県/福島県
茨城県/栃木県/群馬県/埼玉県/千葉県/東京都/神奈川
新潟県/富山県/石川県/福井県/山梨県/長野県/静岡県/愛知県
三重県/滋賀県/京都府/大阪府/兵庫県/和歌山
鳥取県/島根県/岡山県/広島県/山口県
徳島県/香川県/愛媛県/高知県
福岡県/佐賀県/長崎県/熊本県/大分県/宮崎県/鹿児島/沖縄県
7月30日(木)16:00-17:00 オンライン開催
----------------------------------------------------------
− 1エリア1企業 限定 −
大人の発達障害の方が働くことを支援する
就労移行支援事業 「ディーキャリア ITエキスパート」
新規開業パートナー募集説明会
>>詳細・申込<<
https://decoboco-fc.jp/25m/
◆ 提供 ◆
デコボコベース株式会社
(ディーキャリア ITエキスパート FC運営本部)
----------------------------------------------------------
いつもお世話になります。
厚労省認可の「就労移行支援事業」の新規開業パートナーを
募集するオンライン説明会のご案内につきご連絡差し上げました。
本事業は業界未経験からスタートができ、地域社会への貢献と
ストック型の安定した収益を実現することが可能です。
新たなビジネスの展開をご検討でしたら、
この機会にまずは本説明会へご参加くださいませ。
よろしくお願いします。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
就労移行支援事業 FC説明会事務局
電話:0120-889-859
住所:東京都中央区銀座7-13-6
―――――――――――――――――――――――――――――――
本メールのご不要な方には大変ご迷惑をおかけいたしました。
下記アドレスより、お手続きをお願いいたします。
┗ https://decoboco-fc.jp/mail/
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
^ permalink raw reply
* Re: [PATCH] apparmor: replace decompress_zstd() prototype with its entity
From: John Johansen @ 2026-07-15 23:18 UTC (permalink / raw)
To: Tetsuo Handa, Georgia Garcia, Maxime Bélair
Cc: linux-security-module, Christoph Hellwig, Mark Brown
In-Reply-To: <66e44083-d021-4207-9f88-aa9ef737619b@I-love.SAKURA.ne.jp>
On 7/15/26 16:05, Tetsuo Handa wrote:
> Thank you. Please send to linux-next tree via apparmor tree, for
> syzbot won't be able to test linux-next tree due to this build failure.
>
done, thanks Tetsuo
> On 2026/07/15 21:14, Georgia Garcia wrote:
>> Hello,
>>
>> On Sun, 2026-07-12 at 19:17 +0900, Tetsuo Handa wrote:
>>> Fix "undefined symbol: decompress_zstd" error caused by decompress_zstd()
>>> being guarded by CONFIG_SECURITY_APPARMOR_EXPORT_BINARY=y.
>>>
>>
>> Acked-by: Georgia Garcia <georgia.garcia@canonical.com>
>>
>>> Reported-by: syzbot+1f14a35d0c73d31555e4@syzkaller.appspotmail.com
>>> Closes: https://syzkaller.appspot.com/bug?extid=1f14a35d0c73d31555e4
>>> Fixes: 17b5758bf35c ("apparmor: Initial support for compressed policies")
>>> Signed-off-by: Tetsuo Handa <penguin-kernel@I-love.SAKURA.ne.jp>
>>> ---
>>> security/apparmor/apparmorfs.c | 72 +++++++++++++++++-----------------
>>> 1 file changed, 36 insertions(+), 36 deletions(-)
>>>
>>> diff --git a/security/apparmor/apparmorfs.c b/security/apparmor/apparmorfs.c
>>> index 2ae9ab94a5a91..152c7967ff1ba 100644
>>> --- a/security/apparmor/apparmorfs.c
>>> +++ b/security/apparmor/apparmorfs.c
>>> @@ -483,7 +483,42 @@ static struct aa_loaddata *aa_simple_write_to_buffer(const char __user *userbuf,
>>>
>>> return data;
>>> }
>>> -static int decompress_zstd(char *src, size_t slen, char *dst, size_t dlen);
>>> +
>>> +static int decompress_zstd(char *src, size_t slen, char *dst, size_t dlen)
>>> +{
>>> + if (slen < dlen) {
>>> + const size_t wksp_len = zstd_dctx_workspace_bound();
>>> + zstd_dctx *ctx;
>>> + void *wksp;
>>> + size_t out_len;
>>> + int ret = 0;
>>> +
>>> + wksp = kvzalloc(wksp_len, GFP_KERNEL);
>>> + if (!wksp) {
>>> + ret = -ENOMEM;
>>> + goto cleanup;
>>> + }
>>> + ctx = zstd_init_dctx(wksp, wksp_len);
>>> + if (ctx == NULL) {
>>> + ret = -ENOMEM;
>>> + goto cleanup;
>>> + }
>>> + out_len = zstd_decompress_dctx(ctx, dst, dlen, src, slen);
>>> + if (zstd_is_error(out_len)) {
>>> + ret = -EINVAL;
>>> + goto cleanup;
>>> + }
>>> +cleanup:
>>> + kvfree(wksp);
>>> + return ret;
>>> + }
>>> +
>>> + if (dlen < slen)
>>> + return -EINVAL;
>>> + memcpy(dst, src, slen);
>>> + return 0;
>>> +}
>>> +
>>> /**
>>> * aa_get_data_from_compressed - common routine for getting compressed policy
>>> * from user and get both compressed and uncompressed version.
>>> @@ -1517,41 +1552,6 @@ SEQ_RAWDATA_FOPS(revision);
>>> SEQ_RAWDATA_FOPS(hash);
>>> SEQ_RAWDATA_FOPS(compressed_size);
>>>
>>> -static int decompress_zstd(char *src, size_t slen, char *dst, size_t dlen)
>>> -{
>>> - if (slen < dlen) {
>>> - const size_t wksp_len = zstd_dctx_workspace_bound();
>>> - zstd_dctx *ctx;
>>> - void *wksp;
>>> - size_t out_len;
>>> - int ret = 0;
>>> -
>>> - wksp = kvzalloc(wksp_len, GFP_KERNEL);
>>> - if (!wksp) {
>>> - ret = -ENOMEM;
>>> - goto cleanup;
>>> - }
>>> - ctx = zstd_init_dctx(wksp, wksp_len);
>>> - if (ctx == NULL) {
>>> - ret = -ENOMEM;
>>> - goto cleanup;
>>> - }
>>> - out_len = zstd_decompress_dctx(ctx, dst, dlen, src, slen);
>>> - if (zstd_is_error(out_len)) {
>>> - ret = -EINVAL;
>>> - goto cleanup;
>>> - }
>>> -cleanup:
>>> - kvfree(wksp);
>>> - return ret;
>>> - }
>>> -
>>> - if (dlen < slen)
>>> - return -EINVAL;
>>> - memcpy(dst, src, slen);
>>> - return 0;
>>> -}
>>> -
>>> static ssize_t rawdata_read(struct file *file, char __user *buf, size_t size,
>>> loff_t *ppos)
>>> {
>>
>
^ permalink raw reply
* Re: [PATCH] apparmor: replace decompress_zstd() prototype with its entity
From: Tetsuo Handa @ 2026-07-15 23:05 UTC (permalink / raw)
To: Georgia Garcia, Maxime Bélair, John Johansen
Cc: linux-security-module, Christoph Hellwig, Mark Brown
In-Reply-To: <dd652e1fbc0620d7d45e61f81336428860441f2d.camel@canonical.com>
Thank you. Please send to linux-next tree via apparmor tree, for
syzbot won't be able to test linux-next tree due to this build failure.
On 2026/07/15 21:14, Georgia Garcia wrote:
> Hello,
>
> On Sun, 2026-07-12 at 19:17 +0900, Tetsuo Handa wrote:
>> Fix "undefined symbol: decompress_zstd" error caused by decompress_zstd()
>> being guarded by CONFIG_SECURITY_APPARMOR_EXPORT_BINARY=y.
>>
>
> Acked-by: Georgia Garcia <georgia.garcia@canonical.com>
>
>> Reported-by: syzbot+1f14a35d0c73d31555e4@syzkaller.appspotmail.com
>> Closes: https://syzkaller.appspot.com/bug?extid=1f14a35d0c73d31555e4
>> Fixes: 17b5758bf35c ("apparmor: Initial support for compressed policies")
>> Signed-off-by: Tetsuo Handa <penguin-kernel@I-love.SAKURA.ne.jp>
>> ---
>> security/apparmor/apparmorfs.c | 72 +++++++++++++++++-----------------
>> 1 file changed, 36 insertions(+), 36 deletions(-)
>>
>> diff --git a/security/apparmor/apparmorfs.c b/security/apparmor/apparmorfs.c
>> index 2ae9ab94a5a91..152c7967ff1ba 100644
>> --- a/security/apparmor/apparmorfs.c
>> +++ b/security/apparmor/apparmorfs.c
>> @@ -483,7 +483,42 @@ static struct aa_loaddata *aa_simple_write_to_buffer(const char __user *userbuf,
>>
>> return data;
>> }
>> -static int decompress_zstd(char *src, size_t slen, char *dst, size_t dlen);
>> +
>> +static int decompress_zstd(char *src, size_t slen, char *dst, size_t dlen)
>> +{
>> + if (slen < dlen) {
>> + const size_t wksp_len = zstd_dctx_workspace_bound();
>> + zstd_dctx *ctx;
>> + void *wksp;
>> + size_t out_len;
>> + int ret = 0;
>> +
>> + wksp = kvzalloc(wksp_len, GFP_KERNEL);
>> + if (!wksp) {
>> + ret = -ENOMEM;
>> + goto cleanup;
>> + }
>> + ctx = zstd_init_dctx(wksp, wksp_len);
>> + if (ctx == NULL) {
>> + ret = -ENOMEM;
>> + goto cleanup;
>> + }
>> + out_len = zstd_decompress_dctx(ctx, dst, dlen, src, slen);
>> + if (zstd_is_error(out_len)) {
>> + ret = -EINVAL;
>> + goto cleanup;
>> + }
>> +cleanup:
>> + kvfree(wksp);
>> + return ret;
>> + }
>> +
>> + if (dlen < slen)
>> + return -EINVAL;
>> + memcpy(dst, src, slen);
>> + return 0;
>> +}
>> +
>> /**
>> * aa_get_data_from_compressed - common routine for getting compressed policy
>> * from user and get both compressed and uncompressed version.
>> @@ -1517,41 +1552,6 @@ SEQ_RAWDATA_FOPS(revision);
>> SEQ_RAWDATA_FOPS(hash);
>> SEQ_RAWDATA_FOPS(compressed_size);
>>
>> -static int decompress_zstd(char *src, size_t slen, char *dst, size_t dlen)
>> -{
>> - if (slen < dlen) {
>> - const size_t wksp_len = zstd_dctx_workspace_bound();
>> - zstd_dctx *ctx;
>> - void *wksp;
>> - size_t out_len;
>> - int ret = 0;
>> -
>> - wksp = kvzalloc(wksp_len, GFP_KERNEL);
>> - if (!wksp) {
>> - ret = -ENOMEM;
>> - goto cleanup;
>> - }
>> - ctx = zstd_init_dctx(wksp, wksp_len);
>> - if (ctx == NULL) {
>> - ret = -ENOMEM;
>> - goto cleanup;
>> - }
>> - out_len = zstd_decompress_dctx(ctx, dst, dlen, src, slen);
>> - if (zstd_is_error(out_len)) {
>> - ret = -EINVAL;
>> - goto cleanup;
>> - }
>> -cleanup:
>> - kvfree(wksp);
>> - return ret;
>> - }
>> -
>> - if (dlen < slen)
>> - return -EINVAL;
>> - memcpy(dst, src, slen);
>> - return 0;
>> -}
>> -
>> static ssize_t rawdata_read(struct file *file, char __user *buf, size_t size,
>> loff_t *ppos)
>> {
>
^ permalink raw reply
* Re: [PATCH v2] NFSv4.2: fix nfs4_listxattr size accounting
From: Paul Moore @ 2026-07-15 20:41 UTC (permalink / raw)
To: Scott Mayhew
Cc: linux-nfs, Anna Schumaker, jlayton, chuck.lever, Stephen Smalley,
Achilles Gaikwad, Trond Myklebust, linux-security-module, selinux
In-Reply-To: <alaxhbWbFMZyC8VQ@smayhew-thinkpadp1gen4i.remote.csb>
On Tue, Jul 14, 2026 at 6:00 PM Scott Mayhew <smayhew@redhat.com> wrote:
> On Tue, 14 Jul 2026, Paul Moore wrote:
> > On Fri, Jul 10, 2026 at 6:20 PM Paul Moore <paul@paul-moore.com> wrote:
> > >
> > > ... and scratch that, the offending commit was that one.
> > >
> > > commit 01c2305795a3b6b164df48e72b12022a68fd60c1
> > > Author: Jeff Layton <jlayton@kernel.org>
> > > Date: Wed Mar 25 10:40:32 2026 -0400
> > >
> > > nfsd: add netlink upcall for the nfsd.fh cache
> > >
> > > Add netlink-based cache upcall support for the expkey (nfsd.fh) cache,
> > > following the same pattern as the existing svc_export netlink support.
> > >
> > > Add expkey to the cache-type enum, a new expkey attribute-set with
> > > client, fsidtype, fsid, negative, expiry, and path fields, and the
> > > expkey-get-reqs / expkey-set-reqs operations to the nfsd YAML spec
> > > and generated headers.
> > >
> > > Implement nfsd_nl_expkey_get_reqs_dumpit() which snapshots pending
> > > expkey cache requests and sends each entry's seqno, client name,
> > > fsidtype, and fsid over netlink.
> > >
> > > Implement nfsd_nl_expkey_set_reqs_doit() which parses expkey cache
> > > responses from userspace (client, fsidtype, fsid, expiry, and path
> > > or negative flag) and updates the cache via svc_expkey_lookup() /
> > > svc_expkey_update().
> > >
> > > Wire up the expkey_notify() callback in svc_expkey_cache_template
> > > so cache misses trigger NFSD_CMD_CACHE_NOTIFY multicast events with
> > > NFSD_CACHE_TYPE_EXPKEY.
> > >
> > > Signed-off-by: Jeff Layton <jlayton@kernel.org>
> > > Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
> >
> > Playing around with it some this morning on a current Fedora Rawhide
> > system, I can reproduce the problem with a simple command line:
> >
> > % mount -t nfs localhost:/mnt/test /mnt/nfs_test
> > mount.nfs: Connection refused for localhost:/mnt/test on /mnt/nfs_test
> >
> > ... adding an explicit "vers={4,4.1,4.2}" has no effect; versions 2
> > and 3 are no supported on my kernel builds. There is nothing obvious
> > in dmesg. I've run with SELinux both in permissive mode and disabled
> > and encountered the same problem. This doesn't appear to be related
> > to SELinux, it may simply be that we are the first ones to hit this.
> >
> > As there was some earlier discussion about this being a wonky
> > interaction with userspace, here are some of the relevant packages on
> > my system:
> >
> > nfs-common-utils-2.9.1-4.rc4.fc45.x86_64
> > nfs-client-utils-2.9.1-4.rc4.fc45.x86_64
> > nfsv4-client-utils-2.9.1-4.rc4.fc45.x86_64
> > nfs-utils-2.9.1-4.rc4.fc45.x86_64
> >
> > My next step is to try disabling portions of the NFS file handle cache
> > upcall to see if that is the issue, but it would be nice if the NFS
> > devs could take a look at this too. I'm happy to test things out or
> > answer any questions about my test system.
>
> Pardon the dumb question, but are you positive your NFS server is
> running?
I'm slightly embarrassed to admit that your question wasn't as dumb as
one might have thought :)
While my bisection testing used the selinux-testsuite which ensures
the NFS server starts, when I tried to create a simple reproducer to
share with the non-SELinux crowd I neglected to verify that the NFS
server was running, the directory, was exported, etc. My apologies
for the confusion. However, the problem was real, even if my
reproducer and error message were junk.
> FWIW the netlink upcall requires CAP_NET_ADMIN, which is blocked by the
> current selinux-policy on rawhide (and I think it's a dontaudit rule)...
As a quick test I added a quick hack to my policy (allow rule below)
and everything appears to be working again. Thanks for the pointer!
allow nfsd_t nfsd_t:capability { net_admin };
--
paul-moore.com
^ permalink raw reply
* Re: [PATCH] apparmor: replace decompress_zstd() prototype with its entity
From: Georgia Garcia @ 2026-07-15 12:14 UTC (permalink / raw)
To: Tetsuo Handa, Maxime Bélair, John Johansen; +Cc: linux-security-module
In-Reply-To: <df2830d0-5ae8-4bb9-b11c-6051fc3784ea@I-love.SAKURA.ne.jp>
Hello,
On Sun, 2026-07-12 at 19:17 +0900, Tetsuo Handa wrote:
> Fix "undefined symbol: decompress_zstd" error caused by decompress_zstd()
> being guarded by CONFIG_SECURITY_APPARMOR_EXPORT_BINARY=y.
>
Acked-by: Georgia Garcia <georgia.garcia@canonical.com>
> Reported-by: syzbot+1f14a35d0c73d31555e4@syzkaller.appspotmail.com
> Closes: https://syzkaller.appspot.com/bug?extid=1f14a35d0c73d31555e4
> Fixes: 17b5758bf35c ("apparmor: Initial support for compressed policies")
> Signed-off-by: Tetsuo Handa <penguin-kernel@I-love.SAKURA.ne.jp>
> ---
> security/apparmor/apparmorfs.c | 72 +++++++++++++++++-----------------
> 1 file changed, 36 insertions(+), 36 deletions(-)
>
> diff --git a/security/apparmor/apparmorfs.c b/security/apparmor/apparmorfs.c
> index 2ae9ab94a5a91..152c7967ff1ba 100644
> --- a/security/apparmor/apparmorfs.c
> +++ b/security/apparmor/apparmorfs.c
> @@ -483,7 +483,42 @@ static struct aa_loaddata *aa_simple_write_to_buffer(const char __user *userbuf,
>
> return data;
> }
> -static int decompress_zstd(char *src, size_t slen, char *dst, size_t dlen);
> +
> +static int decompress_zstd(char *src, size_t slen, char *dst, size_t dlen)
> +{
> + if (slen < dlen) {
> + const size_t wksp_len = zstd_dctx_workspace_bound();
> + zstd_dctx *ctx;
> + void *wksp;
> + size_t out_len;
> + int ret = 0;
> +
> + wksp = kvzalloc(wksp_len, GFP_KERNEL);
> + if (!wksp) {
> + ret = -ENOMEM;
> + goto cleanup;
> + }
> + ctx = zstd_init_dctx(wksp, wksp_len);
> + if (ctx == NULL) {
> + ret = -ENOMEM;
> + goto cleanup;
> + }
> + out_len = zstd_decompress_dctx(ctx, dst, dlen, src, slen);
> + if (zstd_is_error(out_len)) {
> + ret = -EINVAL;
> + goto cleanup;
> + }
> +cleanup:
> + kvfree(wksp);
> + return ret;
> + }
> +
> + if (dlen < slen)
> + return -EINVAL;
> + memcpy(dst, src, slen);
> + return 0;
> +}
> +
> /**
> * aa_get_data_from_compressed - common routine for getting compressed policy
> * from user and get both compressed and uncompressed version.
> @@ -1517,41 +1552,6 @@ SEQ_RAWDATA_FOPS(revision);
> SEQ_RAWDATA_FOPS(hash);
> SEQ_RAWDATA_FOPS(compressed_size);
>
> -static int decompress_zstd(char *src, size_t slen, char *dst, size_t dlen)
> -{
> - if (slen < dlen) {
> - const size_t wksp_len = zstd_dctx_workspace_bound();
> - zstd_dctx *ctx;
> - void *wksp;
> - size_t out_len;
> - int ret = 0;
> -
> - wksp = kvzalloc(wksp_len, GFP_KERNEL);
> - if (!wksp) {
> - ret = -ENOMEM;
> - goto cleanup;
> - }
> - ctx = zstd_init_dctx(wksp, wksp_len);
> - if (ctx == NULL) {
> - ret = -ENOMEM;
> - goto cleanup;
> - }
> - out_len = zstd_decompress_dctx(ctx, dst, dlen, src, slen);
> - if (zstd_is_error(out_len)) {
> - ret = -EINVAL;
> - goto cleanup;
> - }
> -cleanup:
> - kvfree(wksp);
> - return ret;
> - }
> -
> - if (dlen < slen)
> - return -EINVAL;
> - memcpy(dst, src, slen);
> - return 0;
> -}
> -
> static ssize_t rawdata_read(struct file *file, char __user *buf, size_t size,
> loff_t *ppos)
> {
^ permalink raw reply
* Re: [BUG] Landlock denies LANDLOCK_ACCESS_FS_EXECUTE despite a correctly-anchored PathBeneath rule (contradicts selftest layout1.execute)
From: Günther Noack @ 2026-07-15 8:39 UTC (permalink / raw)
To: Ken Grimes
Cc: linux-security-module@vger.kernel.org, Mickaël Salaün,
landlock
In-Reply-To: <P8RYlSi7C7oum2lk1F3PumYSvaUyd0Sn_VwUYAcWwjbQxSt_Rau30Aa1oLIEnLZ2iafcsFmEG5C4LTeBWCyd9vCx94FYeTX_OJdHoERcWSI=@kengrimes.com>
Hello Ken!
(Also adding landlock@lists.linux.dev to CC)
On Tue, Jul 14, 2026 at 09:07:56PM +0000, Ken Grimes wrote:
> Hey all, this is my first bug report for linux. The issue was discovered
> alongside llm-assisted coding on a downstream project. The
> investigation/testing of the bug was a mostly manual process so I could
> be sure this was something real. Please let me know if I can provide any
> further details or assistance. Hope this is helpful, thank you for all of your
> hard work!
Welcome and thanks for reporting your first issue!
I believe the issue you are observing is that the /bin/true program you are
starting is a dynamically linked executable. As such, executing it requires
both the LANDLOCK_ACCESS_FS_EXECUTE right on the binary itself and on
the system's dynamic loader binary, which usually lives in /lib/ld-linux.so.*
(but there are symlinks and 32/64-bit differences at play as well, which
influence the actual final location).
You can try this out with the following experiments:
(1) Compile a "true" program statically and try using that:
$ echo 'int main() { return 0; }' > true.c
$ CFLAGS=-static make true
This can be started with the test you have,
unlike the dynamically linked version.
(2) Alternatively, add execute permissions for the dynamic loader:
Add an additional "path beneath" rule that allow-lists the execution
access right on /lib/ld-linux.so.2, /lib64/ld-linux-x86-64.so.2 or
wherever else your dynamic loader is. (You can discover the actual
location using "ldd /bin/true".)
With either one of these two changes, your standalone reproducer program
starts working again. Or at least it does on my machine. If it still doesn't
work on your end that way, please let us know. :)
I admit that we should probably point this out in the Landlock documentation
for the LANDLOCK_ACCESS_FS_EXECUTE right, as dynamic linking is common and it
is a potential issue that many people might run into.
For background on the dynamic loading mechanism, see the man page ld.so(8) [1]
and the LWN article "How programs get run: ELF binaries" [2] (specifically the
section "Dynamically linked programs").
—Günther
[1] https://man7.org/linux/man-pages/man8/ld.so.8.html
[2] https://lwn.net/Articles/631631/
^ permalink raw reply
* Re: [PATCH v2] NFSv4.2: fix nfs4_listxattr size accounting
From: Scott Mayhew @ 2026-07-14 22:00 UTC (permalink / raw)
To: Paul Moore
Cc: linux-nfs, Anna Schumaker, jlayton, chuck.lever, Stephen Smalley,
Achilles Gaikwad, Trond Myklebust, linux-security-module, selinux
In-Reply-To: <CAHC9VhR+pWFnt=1pBJTky78br7NsfVcZE=V4UkfkfTbMvBo+mQ@mail.gmail.com>
On Tue, 14 Jul 2026, Paul Moore wrote:
> On Fri, Jul 10, 2026 at 6:20 PM Paul Moore <paul@paul-moore.com> wrote:
> >
> > ... and scratch that, the offending commit was that one.
> >
> > commit 01c2305795a3b6b164df48e72b12022a68fd60c1
> > Author: Jeff Layton <jlayton@kernel.org>
> > Date: Wed Mar 25 10:40:32 2026 -0400
> >
> > nfsd: add netlink upcall for the nfsd.fh cache
> >
> > Add netlink-based cache upcall support for the expkey (nfsd.fh) cache,
> > following the same pattern as the existing svc_export netlink support.
> >
> > Add expkey to the cache-type enum, a new expkey attribute-set with
> > client, fsidtype, fsid, negative, expiry, and path fields, and the
> > expkey-get-reqs / expkey-set-reqs operations to the nfsd YAML spec
> > and generated headers.
> >
> > Implement nfsd_nl_expkey_get_reqs_dumpit() which snapshots pending
> > expkey cache requests and sends each entry's seqno, client name,
> > fsidtype, and fsid over netlink.
> >
> > Implement nfsd_nl_expkey_set_reqs_doit() which parses expkey cache
> > responses from userspace (client, fsidtype, fsid, expiry, and path
> > or negative flag) and updates the cache via svc_expkey_lookup() /
> > svc_expkey_update().
> >
> > Wire up the expkey_notify() callback in svc_expkey_cache_template
> > so cache misses trigger NFSD_CMD_CACHE_NOTIFY multicast events with
> > NFSD_CACHE_TYPE_EXPKEY.
> >
> > Signed-off-by: Jeff Layton <jlayton@kernel.org>
> > Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
>
> Playing around with it some this morning on a current Fedora Rawhide
> system, I can reproduce the problem with a simple command line:
>
> % mount -t nfs localhost:/mnt/test /mnt/nfs_test
> mount.nfs: Connection refused for localhost:/mnt/test on /mnt/nfs_test
>
> ... adding an explicit "vers={4,4.1,4.2}" has no effect; versions 2
> and 3 are no supported on my kernel builds. There is nothing obvious
> in dmesg. I've run with SELinux both in permissive mode and disabled
> and encountered the same problem. This doesn't appear to be related
> to SELinux, it may simply be that we are the first ones to hit this.
>
> As there was some earlier discussion about this being a wonky
> interaction with userspace, here are some of the relevant packages on
> my system:
>
> nfs-common-utils-2.9.1-4.rc4.fc45.x86_64
> nfs-client-utils-2.9.1-4.rc4.fc45.x86_64
> nfsv4-client-utils-2.9.1-4.rc4.fc45.x86_64
> nfs-utils-2.9.1-4.rc4.fc45.x86_64
>
> My next step is to try disabling portions of the NFS file handle cache
> upcall to see if that is the issue, but it would be nice if the NFS
> devs could take a look at this too. I'm happy to test things out or
> answer any questions about my test system.
Pardon the dumb question, but are you positive your NFS server is
running? No errors from 'systemctl status nfs-server.service' or
'systemctl status nfs-mountd.service'? A connection refused error
typically indicates the server isn't running.
What if you add 'no-netlink=1' to the mountd stanza in /etc/nfs.conf and
restart nfs-server.service?
FWIW the netlink upcall requires CAP_NET_ADMIN, which is blocked by the
current selinux-policy on rawhide (and I think it's a dontaudit rule)...
but you said you've disabled SELinux and reproduced your problem, so I'm
scratching my head. Plus the symptom I ran into was the mount command
hanging, not a 'connection refused' error. At any rate I filed
https://github.com/fedora-selinux/selinux-policy/pull/3278 to update the
selinux-policy (no movement on that yet) and the most recent mountd
patch improves the fallback to the old proc interface, but I don't think
it's been added to Fedora yet either.
-Scott
>
> --
> paul-moore.com
>
^ permalink raw reply
* [BUG] Landlock denies LANDLOCK_ACCESS_FS_EXECUTE despite a correctly-anchored PathBeneath rule (contradicts selftest layout1.execute)
From: Ken Grimes @ 2026-07-14 21:07 UTC (permalink / raw)
To: linux-security-module@vger.kernel.org
Cc: Mickaël Salaün, Günther Noack
[-- Attachment #1.1.1: Type: text/plain, Size: 16855 bytes --]
Hey all, this is my first bug report for linux. The issue was discovered
alongside llm-assisted coding on a downstream project. The
investigation/testing of the bug was a mostly manual process so I could
be sure this was something real. Please let me know if I can provide any
further details or assistance. Hope this is helpful, thank you for all of your
hard work!
Summary
-------
On Linux 7.1.3 (Fedora 44, Landlock ABI version 9), a minimal, correctly
constructed Landlock ruleset denies execve()/execveat() of that file with
EACCES. This directly contradicts the expected behavior of the official
selftest assertion: tools/testing/selftests/landlock/fs_test.c's
TEST_F_FORK(layout1, execute), specifically the file1_s1d2 case,
expects exactly this shape of rule to permit execution
(test_execute(_metadata, 0, file1_s1d2)).
This was confirmed two ways: a minimal standalone reproduction (below),
and by building and running your own unmodified
tools/testing/selftests/landlock/fs_test.c from the v7.1.3 tag (exact
match to the running kernel and its kernel-headers package) against this
kernel. layout1.execute fails on this kernel:
# RUN layout1.execute ...
# fs_test.c:1949:execute:Expected err ? -1 : 0 (0) == execve(path, argv, NULL) (-1)
# fs_test.c:1951:execute:Failed to execute "tmp/s1d1/s1d2/f1": Permission denied
# execute: Test terminated by assertion
# FAIL layout1.execute
not ok 29 layout1.execute
tmp/s1d1/s1d2/f1 is file1_s1d2 is the file living directly inside
dir_s1d2, which is the directory the test's own PathBeneath rule
anchors on (rules[0] = { .path = dir_s1d2, .access =
LANDLOCK_ACCESS_FS_EXECUTE }). The failing assertion is
test_execute(_metadata, 0, file1_s1d2) (fs_test.c:1949 in the v7.1.3
tree, quoted below). The test's author expects this call to succeed
(err == 0), and on this kernel it doesn't. Full raw test output is
attached (official-selftest-output.log).
Relevant excerpt of the actual test body (identical across the v7.1.3
tag and current master):
TEST_F_FORK(layout1, execute)
{
const struct rule rules[] = {
{
.path = dir_s1d2,
.access = LANDLOCK_ACCESS_FS_EXECUTE,
},
{},
};
copy_file(_metadata, bin_true, file1_s1d1);
copy_file(_metadata, bin_true, file1_s1d2);
copy_file(_metadata, bin_true, file1_s1d3);
/* Checks before file1_s1d1 being denied. */
test_execute(_metadata, 0, file1_s1d1);
test_check_exec(_metadata, 0, file1_s1d1);
enforce_fs(_metadata, rules[0].access, rules);
ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY));
ASSERT_EQ(0, test_open(file1_s1d1, O_RDONLY));
test_execute(_metadata, EACCES, file1_s1d1);
test_check_exec(_metadata, EACCES, file1_s1d1);
ASSERT_EQ(0, test_open(dir_s1d2, O_RDONLY));
ASSERT_EQ(0, test_open(file1_s1d2, O_RDONLY));
test_execute(_metadata, 0, file1_s1d2); /* <-- fails here */
test_check_exec(_metadata, 0, file1_s1d2);
...
(Reproducing the official fixture required running from a
world-writable directory: prepare_layout_opt() calls disable_caps()
which drops CAP_DAC_OVERRIDE from the otherwise-root test process,
before creating its working directory, so it must be run from
somewhere an unprivileged-DAC root can still write, e.g. under /tmp
directly. This is unrelated to the actual bug, just a note for anyone
else reproducing it.)
Environment
-----------
- Kernel: 7.1.3-200.fc44.x86_64 (Fedora 44 Workstation)
- Landlock ABI version: 9 (via landlock_create_ruleset(NULL, 0,
LANDLOCK_CREATE_RULESET_VERSION))
- kernel-headers package: kernel-headers-7.1.3-200.fc44.x86_64
(exact version match to the running kernel; the reproduction below was
built against these headers)
- No root/CAP_SYS_ADMIN involved anywhere in the reproduction -- Landlock
is unprivileged by design, and this reproduces identically as an
ordinary user.
Reproduction
------------
Official selftest (strongest evidence):
$ git clone --branch v7.1.3 --depth 1 https://github.com/gregkh/linux
$ cd linux/tools/testing/selftests/landlock
$ gcc -O0 -g -Wall -o fs_test fs_test.c -lcap -lpthread
$ gcc -O0 -o true true.c
$ mkdir -p /tmp/lltest && chmod 1777 /tmp/lltest
$ cp fs_test true /tmp/lltest/ && cd /tmp/lltest
$ sudo ./fs_test -f layout1 -t execute
(Run from a world-writable directory. See the note above about
disable_caps()) Full output attached as official-selftest-output.log.
Minimal standalone reproduction (self-contained, no external
dependencies beyond libc and the system's own <linux/landlock.h>,
unprivileged, attached as repro.c):
/*
* Minimal standalone reproduction of a Landlock EXECUTE-right denial that
* contradicts the upstream kernel's own selftest expectation.
*
* This mirrors tools/testing/selftests/landlock/fs_test.c's
* TEST_F_FORK(layout1, execute) -- specifically the file1_s1d2 case: a
* PathBeneath rule granting only LANDLOCK_ACCESS_FS_EXECUTE, anchored on a
* directory, is expected to permit execve() of a file directly inside that
* directory (upstream asserts test_execute(_metadata, 0, file1_s1d2), i.e.
* "no error"). On this kernel it is denied with EACCES instead.
*
* No root/CAP_SYS_ADMIN required -- Landlock is designed for unprivileged
* use, and this reproduces the same way as an ordinary user.
*
* Build: gcc -O0 -g -Wall -o repro repro.c
* Run: ./repro
*
* Observed on: Linux 7.1.3-200.fc44.x86_64 (Fedora 44 Workstation),
* Landlock ABI version 9 (per landlock_create_ruleset(NULL, 0,
* LANDLOCK_CREATE_RULESET_VERSION)).
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/landlock.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <unistd.h>
static int landlock_create_ruleset(const struct landlock_ruleset_attr *attr,
size_t size, __u32 flags)
{
return syscall(SYS_landlock_create_ruleset, attr, size, flags);
}
static int landlock_add_rule(int ruleset_fd, enum landlock_rule_type type,
const void *attr, __u32 flags)
{
return syscall(SYS_landlock_add_rule, ruleset_fd, type, attr, flags);
}
static int landlock_restrict_self(int ruleset_fd, __u32 flags)
{
return syscall(SYS_landlock_restrict_self, ruleset_fd, flags);
}
static void die(const char *what)
{
fprintf(stderr, "%s: %s\n", what, strerror(errno));
exit(1);
}
/* Runs `path` via a fresh execve() in a child, prints and returns its result. */
static int try_execve(const char *path)
{
pid_t pid = fork();
if (pid < 0)
die("fork");
if (pid == 0) {
char *const argv[] = { (char *)path, NULL };
execve(path, argv, NULL);
/* Only reached on failure. */
fprintf(stderr, "execve(%s) failed: %s (errno %d)\n", path,
strerror(errno), errno);
_exit(1);
}
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status) && WEXITSTATUS(status) == 0)
return 0;
return 1;
}
int main(void)
{
int abi = landlock_create_ruleset(NULL, 0,
LANDLOCK_CREATE_RULESET_VERSION);
printf("Landlock ABI version: %d\n", abi);
if (abi < 0)
die("landlock_create_ruleset(VERSION)");
/* Build tmp/dir/exe, matching layout1's dir_s1d2/file1_s1d2 shape:
* a rule anchored on the parent directory, target file directly
* inside it (zero levels of extra nesting). */
if (mkdir("repro-dir", 0700) < 0 && errno != EEXIST)
die("mkdir repro-dir");
/* Copy /bin/true in as our target executable. */
{
char cmd[256];
snprintf(cmd, sizeof(cmd),
"cp /bin/true repro-dir/exe && chmod 755 repro-dir/exe");
if (system(cmd) != 0)
die("cp /bin/true");
}
/* Baseline: confirm it executes fine before Landlock is involved. */
if (try_execve("repro-dir/exe") != 0) {
fprintf(stderr, "baseline execve failed even without Landlock -- "
"environment problem, not a Landlock issue\n");
return 1;
}
printf("Baseline (no Landlock): execve succeeded, as expected.\n");
/* Minimal ruleset: EXECUTE only, matching the official selftest's
* rules[0].access = LANDLOCK_ACCESS_FS_EXECUTE for layout1.execute. */
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_EXECUTE,
};
int ruleset_fd = landlock_create_ruleset(&ruleset_attr,
sizeof(ruleset_attr), 0);
if (ruleset_fd < 0)
die("landlock_create_ruleset");
int parent_fd = open("repro-dir", O_PATH);
if (parent_fd < 0)
die("open repro-dir O_PATH");
struct landlock_path_beneath_attr path_beneath = {
.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE,
.parent_fd = parent_fd,
};
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
&path_beneath, 0) != 0)
die("landlock_add_rule");
close(parent_fd);
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0)
die("prctl(PR_SET_NO_NEW_PRIVS)");
if (landlock_restrict_self(ruleset_fd, 0) != 0)
die("landlock_restrict_self");
close(ruleset_fd);
printf("Landlock ruleset installed: EXECUTE-only, one PathBeneath "
"rule anchored on repro-dir (the target file's own parent).\n");
int result = try_execve("repro-dir/exe");
if (result == 0) {
printf("PASS: execve succeeded, as the upstream selftest "
"(layout1.execute, file1_s1d2 case) expects.\n");
} else {
printf("FAIL: execve was denied (EACCES) despite an "
"explicit, correctly-anchored PathBeneath rule "
"granting LANDLOCK_ACCESS_FS_EXECUTE on the file's "
"own parent directory -- this contradicts "
"tools/testing/selftests/landlock/fs_test.c's "
"TEST_F_FORK(layout1, execute) expectation for the "
"file1_s1d2 case.\n");
}
system("rm -rf repro-dir");
return result;
}
Build and run:
$ gcc -O0 -g -Wall -o repro repro.c
$ ./repro
Landlock ABI version: 9
Baseline (no Landlock): execve succeeded, as expected.
Landlock ruleset installed: EXECUTE-only, one PathBeneath rule anchored on repro-dir (the target file's own parent).
execve(repro-dir/exe) failed: Permission denied (errno 13)
FAIL: execve was denied (EACCES) despite an explicit, correctly-anchored PathBeneath rule granting LANDLOCK_ACCESS_FS_EXECUTE on the file's own parent directory -- this contradicts tools/testing/selftests/landlock/fs_test.c's TEST_F_FORK(layout1, execute) expectation for the file1_s1d2 case.
What was ruled out before concluding this is a kernel-level issue
--------------------------------------------------------------------
This was investigated extensively in the course of debugging a real
downstream project (a Rust codebase using the landlock crate 0.4.5) that
hit this exact denial. Before concluding it's a kernel bug rather than a
userspace mistake, the following were each tested and ruled out as the
cause:
1. SELinux: this host runs SELinux Enforcing, but the reproduction
above runs entirely under the interactive shell's unconfined_t
domain, and the identical target file executes fine via a plain
(non-Landlock) execve() beforehand in the same process. Only adding
the Landlock ruleset flips the result to EACCES.
2. IPE (Integrity Policy Enforcement): enforce=1 on this host, but
/sys/kernel/security/ipe/policies/ is empty (no policy loaded), and
no IPE-related denial appears in the audit log around the failure. A
plain execve() of the same file with no Landlock domain active
succeeds, which would not be the case if IPE were unconditionally
denying this file.
3. Kernel lockdown mode: active at "integrity" level on this host
(Fedora enables this automatically under Secure Boot), but lockdown's
documented restrictions (module signing, /dev/mem, hibernation,
kexec, debugfs, etc.) govern kernel-image-integrity operations, not
ordinary unprivileged LSM policy decisions like Landlock's own
filesystem checks, and no documented interaction between the two was
found.
4. tmpfs vs. a real on-disk filesystem: the denial reproduces
identically whether the target directory is on tmpfs (/tmp) or a
real on-disk btrfs filesystem (the root filesystem).
5. fd-based (execveat+AT_EMPTY_PATH) vs. path-based execve(): both
fail identically; this is not specific to executing via an
already-open file descriptor.
6. Direct child vs. nested subdirectory: a file nested one level deeper
than the anchored rule fails identically to a file directly in the
anchored directory.
7. O_PATH vs. plain open() for the rule's anchor fd: no difference
(the landlock Rust crate's own documentation warns plain File opens
"may lead to unexpected errors," but explicitly testing O_PATH
produced the identical denial).
8. Ruleset rights breadth: tested with handled_access_fs as EXECUTE-only
as the full V3-era 15-right set, and with an additional explicit
file-anchored rule (correctly scoped to file-valid rights) layered
alongside the directory rule. All identical.
9. landlock_ruleset_attr struct size: tested with the struct passed at
24, 32, 48, and 64 bytes (padded with zeroed trailing fields, in case
of an extensible-struct zero-extension gap for fields introduced at
ABI 8/9, e.g. the quiet_access_fs/quiet_access_net/quiet_scoped
fields added around ABI 7-9). No difference; the reproduction
attached uses sizeof(ruleset_attr) against the real system header,
matching upstream selftest convention exactly.
10. Published errata: checked all three currently-documented entries
in security/landlock/errata/ (disconnected-directory rename
widening, TCP socket protocol misclassification, thread signal
scoping). None match this symptom.
11. Direct read of security/landlock/fs.c on torvalds/linux master:
the is_access_to_paths_allowed()/current_check_access_path()/
hook_file_open() hierarchy-walk logic appears correct for this
simple case (walk starts at the target file's own dentry, checks
find_rule() at each level while walking up via dget_parent(), and
our anchor is exactly one level up). No obvious logic defect was
found by inspection, which is part of why this is being reported
rather than fixed downstream.
Given (11), if the master-branch logic is correct, this may already be
fixed in a tree newer than 7.1.3, or the regression window may be
narrow but as of this exact kernel, the discrepancy against the
selftest's own documented expectation is real and reproducible.
Impact
------
This affects any unprivileged (or privileged, self-restricting) process
that installs a minimal Landlock domain and then tries to execute a file
covered by a directory-anchored rule which is a common Landlock usage
pattern (matching the kernel's own selftest).
Downstream, it silently breaks a security-hardening code path
(Landlock-based filesystem confinement for a privileged process tree)
with no diagnostic beyond a generic EACCES, since the process attempting
the exec has already dropped privileges and cannot meaningfully report
the failure back to a parent through advanced means.
Request
-------
Could you confirm whether this is a known/already-fixed issue, and if
not, whether the attached reproduction is sufficient to track down the
regression? Let me know if you'd like me test a patch or provide more
instrumentation from this exact kernel build if useful.
Attachments
-----------
- repro.c - minimal standalone reproduction (also inlined above)
- official-selftest-output.log - full raw TAP output from running the
unmodified v7.1.3 fs_test -f layout1 -t execute against this kernel
[-- Attachment #1.1.2.1: Type: text/html, Size: 27985 bytes --]
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #1.2: repro.c --]
[-- Type: text/x-csrc; filename="repro.c"; name="repro.c", Size: 5066 bytes --]
/*
* Minimal standalone reproduction of a Landlock EXECUTE-right denial that
* contradicts the upstream kernel's own selftest expectation.
*
* This mirrors tools/testing/selftests/landlock/fs_test.c's
* TEST_F_FORK(layout1, execute) -- specifically the file1_s1d2 case: a
* PathBeneath rule granting only LANDLOCK_ACCESS_FS_EXECUTE, anchored on a
* directory, is expected to permit execve() of a file directly inside that
* directory (upstream asserts test_execute(_metadata, 0, file1_s1d2), i.e.
* "no error"). On this kernel it is denied with EACCES instead.
*
* No root/CAP_SYS_ADMIN required -- Landlock is designed for unprivileged
* use, and this reproduces the same way as an ordinary user.
*
* Build: gcc -O0 -g -Wall -o repro repro.c
* Run: ./repro
*
* Observed on: Linux 7.1.3-200.fc44.x86_64 (Fedora 44 Workstation),
* Landlock ABI version 9 (per landlock_create_ruleset(NULL, 0,
* LANDLOCK_CREATE_RULESET_VERSION)).
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/landlock.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <unistd.h>
static int landlock_create_ruleset(const struct landlock_ruleset_attr *attr,
size_t size, __u32 flags)
{
return syscall(SYS_landlock_create_ruleset, attr, size, flags);
}
static int landlock_add_rule(int ruleset_fd, enum landlock_rule_type type,
const void *attr, __u32 flags)
{
return syscall(SYS_landlock_add_rule, ruleset_fd, type, attr, flags);
}
static int landlock_restrict_self(int ruleset_fd, __u32 flags)
{
return syscall(SYS_landlock_restrict_self, ruleset_fd, flags);
}
static void die(const char *what)
{
fprintf(stderr, "%s: %s\n", what, strerror(errno));
exit(1);
}
/* Runs `path` via a fresh execve() in a child, prints and returns its result. */
static int try_execve(const char *path)
{
pid_t pid = fork();
if (pid < 0)
die("fork");
if (pid == 0) {
char *const argv[] = { (char *)path, NULL };
execve(path, argv, NULL);
/* Only reached on failure. */
fprintf(stderr, "execve(%s) failed: %s (errno %d)\n", path,
strerror(errno), errno);
_exit(1);
}
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status) && WEXITSTATUS(status) == 0)
return 0;
return 1;
}
int main(void)
{
int abi = landlock_create_ruleset(NULL, 0,
LANDLOCK_CREATE_RULESET_VERSION);
printf("Landlock ABI version: %d\n", abi);
if (abi < 0)
die("landlock_create_ruleset(VERSION)");
/* Build tmp/dir/exe, matching layout1's dir_s1d2/file1_s1d2 shape:
* a rule anchored on the parent directory, target file directly
* inside it (zero levels of extra nesting). */
if (mkdir("repro-dir", 0700) < 0 && errno != EEXIST)
die("mkdir repro-dir");
/* Copy /bin/true in as our target executable. */
{
char cmd[256];
snprintf(cmd, sizeof(cmd),
"cp /bin/true repro-dir/exe && chmod 755 repro-dir/exe");
if (system(cmd) != 0)
die("cp /bin/true");
}
/* Baseline: confirm it executes fine before Landlock is involved. */
if (try_execve("repro-dir/exe") != 0) {
fprintf(stderr, "baseline execve failed even without Landlock -- "
"environment problem, not a Landlock issue\n");
return 1;
}
printf("Baseline (no Landlock): execve succeeded, as expected.\n");
/* Minimal ruleset: EXECUTE only, matching the official selftest's
* rules[0].access = LANDLOCK_ACCESS_FS_EXECUTE for layout1.execute. */
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_EXECUTE,
};
int ruleset_fd = landlock_create_ruleset(&ruleset_attr,
sizeof(ruleset_attr), 0);
if (ruleset_fd < 0)
die("landlock_create_ruleset");
int parent_fd = open("repro-dir", O_PATH);
if (parent_fd < 0)
die("open repro-dir O_PATH");
struct landlock_path_beneath_attr path_beneath = {
.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE,
.parent_fd = parent_fd,
};
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
&path_beneath, 0) != 0)
die("landlock_add_rule");
close(parent_fd);
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0)
die("prctl(PR_SET_NO_NEW_PRIVS)");
if (landlock_restrict_self(ruleset_fd, 0) != 0)
die("landlock_restrict_self");
close(ruleset_fd);
printf("Landlock ruleset installed: EXECUTE-only, one PathBeneath "
"rule anchored on repro-dir (the target file's own parent).\n");
int result = try_execve("repro-dir/exe");
if (result == 0) {
printf("PASS: execve succeeded, as the upstream selftest "
"(layout1.execute, file1_s1d2 case) expects.\n");
} else {
printf("FAIL: execve was denied (EACCES) despite an "
"explicit, correctly-anchored PathBeneath rule "
"granting LANDLOCK_ACCESS_FS_EXECUTE on the file's "
"own parent directory -- this contradicts "
"tools/testing/selftests/landlock/fs_test.c's "
"TEST_F_FORK(layout1, execute) expectation for the "
"file1_s1d2 case.\n");
}
system("rm -rf repro-dir");
return result;
}
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #1.3: official-selftest-output.log --]
[-- Type: text/x-log; filename="official-selftest-output.log"; name="official-selftest-output.log", Size: 9001 bytes --]
TAP version 13
1..68
# Starting 68 tests from 1 test cases.
# RUN layout1.no_restriction ...
# OK layout1.no_restriction
ok 1 layout1.no_restriction
# RUN layout1.inval ...
# OK layout1.inval
ok 2 layout1.inval
# RUN layout1.file_and_dir_access_rights ...
# OK layout1.file_and_dir_access_rights
ok 3 layout1.file_and_dir_access_rights
# RUN layout1.rule_with_unhandled_access ...
# OK layout1.rule_with_unhandled_access
ok 4 layout1.rule_with_unhandled_access
# RUN layout1.effective_access ...
# OK layout1.effective_access
ok 5 layout1.effective_access
# RUN layout1.unhandled_access ...
# OK layout1.unhandled_access
ok 6 layout1.unhandled_access
# RUN layout1.ruleset_overlap ...
# OK layout1.ruleset_overlap
ok 7 layout1.ruleset_overlap
# RUN layout1.layer_rule_unions ...
# OK layout1.layer_rule_unions
ok 8 layout1.layer_rule_unions
# RUN layout1.non_overlapping_accesses ...
# OK layout1.non_overlapping_accesses
ok 9 layout1.non_overlapping_accesses
# RUN layout1.interleaved_masked_accesses ...
# OK layout1.interleaved_masked_accesses
ok 10 layout1.interleaved_masked_accesses
# RUN layout1.inherit_subset ...
# OK layout1.inherit_subset
ok 11 layout1.inherit_subset
# RUN layout1.inherit_superset ...
# OK layout1.inherit_superset
ok 12 layout1.inherit_superset
# RUN layout1.empty_or_same_ruleset ...
# OK layout1.empty_or_same_ruleset
ok 13 layout1.empty_or_same_ruleset
# RUN layout1.rule_on_mountpoint ...
# OK layout1.rule_on_mountpoint
ok 14 layout1.rule_on_mountpoint
# RUN layout1.rule_over_mountpoint ...
# OK layout1.rule_over_mountpoint
ok 15 layout1.rule_over_mountpoint
# RUN layout1.rule_over_root_allow_then_deny ...
# OK layout1.rule_over_root_allow_then_deny
ok 16 layout1.rule_over_root_allow_then_deny
# RUN layout1.rule_over_root_deny ...
# OK layout1.rule_over_root_deny
ok 17 layout1.rule_over_root_deny
# RUN layout1.rule_inside_mount_ns ...
# OK layout1.rule_inside_mount_ns
ok 18 layout1.rule_inside_mount_ns
# RUN layout1.mount_and_pivot ...
# OK layout1.mount_and_pivot
ok 19 layout1.mount_and_pivot
# RUN layout1.move_mount ...
# OK layout1.move_mount
ok 20 layout1.move_mount
# RUN layout1.topology_changes_with_net_only ...
# OK layout1.topology_changes_with_net_only
ok 21 layout1.topology_changes_with_net_only
# RUN layout1.topology_changes_with_net_and_fs ...
# OK layout1.topology_changes_with_net_and_fs
ok 22 layout1.topology_changes_with_net_and_fs
# RUN layout1.release_inodes ...
# OK layout1.release_inodes
ok 23 layout1.release_inodes
# RUN layout1.covered_rule ...
# OK layout1.covered_rule
ok 24 layout1.covered_rule
# RUN layout1.relative_open ...
# OK layout1.relative_open
ok 25 layout1.relative_open
# RUN layout1.relative_chdir ...
# OK layout1.relative_chdir
ok 26 layout1.relative_chdir
# RUN layout1.relative_chroot_only ...
# OK layout1.relative_chroot_only
ok 27 layout1.relative_chroot_only
# RUN layout1.relative_chroot_chdir ...
# OK layout1.relative_chroot_chdir
ok 28 layout1.relative_chroot_chdir
# RUN layout1.execute ...
# fs_test.c:1949:execute:Expected err ? -1 : 0 (0) == execve(path, argv, NULL) (-1)
# fs_test.c:1951:execute:Failed to execute "tmp/s1d1/s1d2/f1": Permission denied
# execute: Test terminated by assertion
# FAIL layout1.execute
not ok 29 layout1.execute
# RUN layout1.umount_sandboxer ...
# fs_test.c:1929:umount_sandboxer:Expected 0 (0) <= src_fd (-1)
# fs_test.c:1931:umount_sandboxer:Failed to open "./sandbox-and-launch": No such file or directory
# umount_sandboxer: Test terminated by assertion
# FAIL layout1.umount_sandboxer
not ok 30 layout1.umount_sandboxer
# RUN layout1.link ...
# OK layout1.link
ok 31 layout1.link
# RUN layout1.rename_file ...
# OK layout1.rename_file
ok 32 layout1.rename_file
# RUN layout1.rename_dir ...
# OK layout1.rename_dir
ok 33 layout1.rename_dir
# RUN layout1.reparent_refer ...
# OK layout1.reparent_refer
ok 34 layout1.reparent_refer
# RUN layout1.refer_denied_by_default1 ...
# OK layout1.refer_denied_by_default1
ok 35 layout1.refer_denied_by_default1
# RUN layout1.refer_denied_by_default2 ...
# OK layout1.refer_denied_by_default2
ok 36 layout1.refer_denied_by_default2
# RUN layout1.refer_denied_by_default3 ...
# OK layout1.refer_denied_by_default3
ok 37 layout1.refer_denied_by_default3
# RUN layout1.refer_denied_by_default4 ...
# OK layout1.refer_denied_by_default4
ok 38 layout1.refer_denied_by_default4
# RUN layout1.refer_mount_root_deny ...
# OK layout1.refer_mount_root_deny
ok 39 layout1.refer_mount_root_deny
# RUN layout1.refer_part_mount_tree_is_allowed ...
# OK layout1.refer_part_mount_tree_is_allowed
ok 40 layout1.refer_part_mount_tree_is_allowed
# RUN layout1.reparent_link ...
# OK layout1.reparent_link
ok 41 layout1.reparent_link
# RUN layout1.reparent_rename ...
# OK layout1.reparent_rename
ok 42 layout1.reparent_rename
# RUN layout1.reparent_exdev_layers_rename1 ...
# OK layout1.reparent_exdev_layers_rename1
ok 43 layout1.reparent_exdev_layers_rename1
# RUN layout1.reparent_exdev_layers_rename2 ...
# OK layout1.reparent_exdev_layers_rename2
ok 44 layout1.reparent_exdev_layers_rename2
# RUN layout1.reparent_exdev_layers_exchange1 ...
# OK layout1.reparent_exdev_layers_exchange1
ok 45 layout1.reparent_exdev_layers_exchange1
# RUN layout1.reparent_exdev_layers_exchange2 ...
# OK layout1.reparent_exdev_layers_exchange2
ok 46 layout1.reparent_exdev_layers_exchange2
# RUN layout1.reparent_exdev_layers_exchange3 ...
# OK layout1.reparent_exdev_layers_exchange3
ok 47 layout1.reparent_exdev_layers_exchange3
# RUN layout1.reparent_remove ...
# OK layout1.reparent_remove
ok 48 layout1.reparent_remove
# RUN layout1.reparent_dom_superset ...
# OK layout1.reparent_dom_superset
ok 49 layout1.reparent_dom_superset
# RUN layout1.remove_dir ...
# OK layout1.remove_dir
ok 50 layout1.remove_dir
# RUN layout1.remove_file ...
# OK layout1.remove_file
ok 51 layout1.remove_file
# RUN layout1.make_char ...
# OK layout1.make_char
ok 52 layout1.make_char
# RUN layout1.make_block ...
# OK layout1.make_block
ok 53 layout1.make_block
# RUN layout1.make_reg_1 ...
# OK layout1.make_reg_1
ok 54 layout1.make_reg_1
# RUN layout1.make_reg_2 ...
# OK layout1.make_reg_2
ok 55 layout1.make_reg_2
# RUN layout1.make_sock ...
# OK layout1.make_sock
ok 56 layout1.make_sock
# RUN layout1.make_fifo ...
# OK layout1.make_fifo
ok 57 layout1.make_fifo
# RUN layout1.make_sym ...
# OK layout1.make_sym
ok 58 layout1.make_sym
# RUN layout1.make_dir ...
# OK layout1.make_dir
ok 59 layout1.make_dir
# RUN layout1.proc_unlinked_file ...
# OK layout1.proc_unlinked_file
ok 60 layout1.proc_unlinked_file
# RUN layout1.proc_pipe ...
# OK layout1.proc_pipe
ok 61 layout1.proc_pipe
# RUN layout1.truncate_unhandled ...
# OK layout1.truncate_unhandled
ok 62 layout1.truncate_unhandled
# RUN layout1.truncate ...
# OK layout1.truncate
ok 63 layout1.truncate
# RUN layout1.ftruncate ...
# OK layout1.ftruncate
ok 64 layout1.ftruncate
# RUN layout1.o_path_ftruncate_and_ioctl ...
# OK layout1.o_path_ftruncate_and_ioctl
ok 65 layout1.o_path_ftruncate_and_ioctl
# RUN layout1.blanket_permitted_ioctls ...
# OK layout1.blanket_permitted_ioctls
ok 66 layout1.blanket_permitted_ioctls
# RUN layout1.named_pipe_ioctl ...
# OK layout1.named_pipe_ioctl
ok 67 layout1.named_pipe_ioctl
# RUN layout1.named_unix_domain_socket_ioctl ...
# OK layout1.named_unix_domain_socket_ioctl
ok 68 layout1.named_unix_domain_socket_ioctl
# FAILED: 66 / 68 tests passed.
# Totals: pass:66 fail:2 xfail:0 xpass:0 skip:0 error:0
[-- Attachment #1.4: publickey - ken@kengrimes.com - 0xEDFCC5BC.asc --]
[-- Type: application/pgp-keys, Size: 645 bytes --]
[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 343 bytes --]
^ permalink raw reply
* Re: [PATCH v2] NFSv4.2: fix nfs4_listxattr size accounting
From: Paul Moore @ 2026-07-14 18:20 UTC (permalink / raw)
To: linux-nfs, Anna Schumaker, jlayton, chuck.lever
Cc: Stephen Smalley, Achilles Gaikwad, Trond Myklebust,
linux-security-module, selinux
In-Reply-To: <CAHC9VhQy4LOgQg0mk+s5sjHOzMe1sxPUgJ2W7vRT8ms4znZp+Q@mail.gmail.com>
On Fri, Jul 10, 2026 at 6:20 PM Paul Moore <paul@paul-moore.com> wrote:
>
> ... and scratch that, the offending commit was that one.
>
> commit 01c2305795a3b6b164df48e72b12022a68fd60c1
> Author: Jeff Layton <jlayton@kernel.org>
> Date: Wed Mar 25 10:40:32 2026 -0400
>
> nfsd: add netlink upcall for the nfsd.fh cache
>
> Add netlink-based cache upcall support for the expkey (nfsd.fh) cache,
> following the same pattern as the existing svc_export netlink support.
>
> Add expkey to the cache-type enum, a new expkey attribute-set with
> client, fsidtype, fsid, negative, expiry, and path fields, and the
> expkey-get-reqs / expkey-set-reqs operations to the nfsd YAML spec
> and generated headers.
>
> Implement nfsd_nl_expkey_get_reqs_dumpit() which snapshots pending
> expkey cache requests and sends each entry's seqno, client name,
> fsidtype, and fsid over netlink.
>
> Implement nfsd_nl_expkey_set_reqs_doit() which parses expkey cache
> responses from userspace (client, fsidtype, fsid, expiry, and path
> or negative flag) and updates the cache via svc_expkey_lookup() /
> svc_expkey_update().
>
> Wire up the expkey_notify() callback in svc_expkey_cache_template
> so cache misses trigger NFSD_CMD_CACHE_NOTIFY multicast events with
> NFSD_CACHE_TYPE_EXPKEY.
>
> Signed-off-by: Jeff Layton <jlayton@kernel.org>
> Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
Playing around with it some this morning on a current Fedora Rawhide
system, I can reproduce the problem with a simple command line:
% mount -t nfs localhost:/mnt/test /mnt/nfs_test
mount.nfs: Connection refused for localhost:/mnt/test on /mnt/nfs_test
... adding an explicit "vers={4,4.1,4.2}" has no effect; versions 2
and 3 are no supported on my kernel builds. There is nothing obvious
in dmesg. I've run with SELinux both in permissive mode and disabled
and encountered the same problem. This doesn't appear to be related
to SELinux, it may simply be that we are the first ones to hit this.
As there was some earlier discussion about this being a wonky
interaction with userspace, here are some of the relevant packages on
my system:
nfs-common-utils-2.9.1-4.rc4.fc45.x86_64
nfs-client-utils-2.9.1-4.rc4.fc45.x86_64
nfsv4-client-utils-2.9.1-4.rc4.fc45.x86_64
nfs-utils-2.9.1-4.rc4.fc45.x86_64
My next step is to try disabling portions of the NFS file handle cache
upcall to see if that is the issue, but it would be nice if the NFS
devs could take a look at this too. I'm happy to test things out or
answer any questions about my test system.
--
paul-moore.com
^ permalink raw reply
* Re: [PATCH] apparmor: fix cred UAF caused by begin_current_label_crit_section()
From: Jann Horn @ 2026-07-14 15:49 UTC (permalink / raw)
To: John Johansen, John Johansen, Georgia Garcia, apparmor,
Paul Moore, Serge E. Hallyn
Cc: James Morris, Christian Brauner, Al Viro, Peter Zijlstra (Intel),
linux-security-module, kernel list, stable
In-Reply-To: <20260714-fix-apparmor-cred-uaf-v1-1-be40e8c83b90@google.com>
On Tue, Jul 14, 2026 at 5:39 PM Jann Horn <jannh@google.com> wrote:
> I have a test case where I run aa-disable on a profile while a process
> using that profile is blocked on splice() from a FUSE passthrough file into
> a full pipe; after the profile update, the pipe becomes empty, splice()
> resumes, the credentials go out of sync, and a subsequent getuid() syscall
> results in a KASAN UAF splat.
To test this, you should run a kernel with KASAN.
CONFIG_RCU_STRICT_GRACE_PERIOD=y might also be necessary to trigger a
KASAN warning.
Open two terminals A and B. In terminal A, write the following policy
into /etc/apparmor.d/credbug-test:
```
abi <abi/4.0>,
include <tunables/global>
profile credbug /tmp/credbug {
/** rwm,
mount,
umount,
capability sys_admin setuid,
}
```
and enable it:
```
user@vm:~$ sudo aa-enforce /tmp/credbug
Setting /tmp/credbug to enforce mode.
Warning: profile credbug represents multiple programs
user@vm:~$
```
In terminal B, build the reproducer and launch it with root privileges:
```
user@vm:~/apparmor-replace-label$ cat > credbug.c
#define _GNU_SOURCE
#include <pthread.h>
#include <assert.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/fsuid.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <linux/fuse.h>
#define SYSCHK(x) ({ \
typeof(x) __res = (x); \
if (__res == (typeof(x))-1) \
err(1, "SYSCHK(" #x ")"); \
__res; \
})
static int p[2];
static int fuse_fd;
static volatile int fuse_ready = 0;
static volatile int backing_id = -1;
static int last_credref_fd = -1;
static int drop_last_credref = 0;
static void *splice_thread_fn(void *dummy) {
int backing_fd = SYSCHK(open("/tmp/credbug", O_RDONLY));
// unshare creds
setfsuid(1);
setfsuid(0);
last_credref_fd = SYSCHK(open("/", O_PATH));
while (!fuse_ready) /*spin*/;
struct fuse_backing_map backing_arg = { .fd = backing_fd };
backing_id = SYSCHK(ioctl(fuse_fd, FUSE_DEV_IOC_BACKING_OPEN, &backing_arg));
int passthrough_fd = SYSCHK(open("/tmp/mntfile", O_RDONLY));
off_t off0 = 0;
// creds are changed in the middle of this
splice(passthrough_fd, &off0, p[1], NULL, 1, 0);
close(passthrough_fd);
SYSCHK(ioctl(fuse_fd, FUSE_DEV_IOC_BACKING_CLOSE, &backing_id));
drop_last_credref = 1;
while (drop_last_credref) /*spin*/;
sleep(1);
getuid();
return NULL;
}
#define READ(_obj) if (read(fuse_fd, &(_obj), sizeof(_obj)) !=
sizeof(_obj)) err(1, "failed to read " #_obj)
#define WRITE(_obj) if (write(fuse_fd, &(_obj), (_obj).h.len) !=
(_obj).h.len) err(1, "failed to write " #_obj)
static void *fuse_thread_fn(void *dummy) {
while (1) {
struct {
struct fuse_in_header inh;
union {
struct fuse_init_in init_in;
struct fuse_open_in open_in;
struct fuse_read_in read_in;
char pad[10000];
};
} buf;
ssize_t read_res = read(fuse_fd, &buf, sizeof(buf));
if (read_res == -1) {
if (errno == ENODEV)
return NULL;
}
assert(read_res >= sizeof(buf.inh));
if (buf.inh.opcode == FUSE_INIT) {
printf("fuse: init\n");
struct {
struct fuse_out_header h;
struct fuse_init_out b;
} reply = {
.h = { .len = sizeof(reply), .error = 0, .unique = buf.inh.unique },
.b = {
.major = buf.init_in.major, .minor = buf.init_in.minor,
.max_stack_depth=1, .flags=FUSE_INIT_EXT, .flags2=FUSE_PASSTHROUGH>>32
}
};
WRITE(reply);
fuse_ready = 1;
} else if (buf.inh.opcode == FUSE_GETATTR) {
printf("fuse: getattr\n");
struct {
struct fuse_out_header h;
struct fuse_attr_out b;
} reply = {
.h = { .len = sizeof(reply), .error = 0, .unique = buf.inh.unique },
.b = {
.attr_valid = FATTR_SIZE | FATTR_MODE,
.attr = {
.size = 0x1,
.mode = 0100777
}
}
};
WRITE(reply);
} else if (buf.inh.opcode == FUSE_OPEN) {
printf("fuse: open node 0x%lu\n", (unsigned long)buf.inh.nodeid);
while (backing_id == -1) /*spin*/;
struct {
struct fuse_out_header h;
struct fuse_open_out b;
} reply = {
.h = { .len = sizeof(reply), .error = 0, .unique = buf.inh.unique },
.b = { .open_flags = FOPEN_PASSTHROUGH, .backing_id = backing_id }
};
WRITE(reply);
} else {
printf("FUSE_<%d> unhandled\n", buf.inh.opcode);
struct {
struct fuse_out_header h;
} reply = {
.h = { .len = sizeof(reply), .error = -ENOSYS, .unique =
buf.inh.unique },
};
WRITE(reply);
}
}
}
int main(void) {
// create a FUSE mount, and handle requests in a thread
SYSCHK(close(SYSCHK(open("/tmp/mntfile", O_RDONLY|O_CREAT, 0666))));
fuse_fd = SYSCHK(open("/dev/fuse", O_RDWR));
char mount_data[4096];
sprintf(mount_data, "fd=%d,rootmode=0100777,user_id=%d,group_id=%d",
fuse_fd, getuid(), getgid());
SYSCHK(mount("blah", "/tmp/mntfile", "fuse", MS_NODEV|MS_NOSUID, mount_data));
pthread_t fuse_thread;
pthread_create(&fuse_thread, NULL, fuse_thread_fn, NULL);
SYSCHK(pipe(p));
SYSCHK(fcntl(p[1], F_SETPIPE_SZ, 0x1000));
char buf[0x1000] = {};
SYSCHK(write(p[1], buf, 0x1000));
pthread_t splice_thread;
pthread_create(&splice_thread, NULL, splice_thread_fn, NULL);
getchar();
read(p[0], buf, 0x1000);
while (!drop_last_credref) /*spin*/;
close(last_credref_fd);
drop_last_credref = 0;
pthread_join(splice_thread, NULL);
SYSCHK(umount2("/tmp/mntfile", MNT_DETACH));
pthread_join(fuse_thread, NULL);
}
user@vm:~/apparmor-replace-label$ gcc -o /tmp/credbug credbug.c
user@vm:~/apparmor-replace-label$ sudo /tmp/credbug
fuse: init
fuse: open node 0x1
```
While the reproducer is blocked, use terminal A to disable its profile:
```
user@vm:~$ sudo aa-disable /tmp/credbug
Disabling /tmp/credbug.
user@vm:~$
```
Then hit enter in terminal B to let the reproducer continue:
```
FUSE_<25> unhandled
FUSE_<18> unhandled
user@vm:~/apparmor-replace-label$
```
Now you should see a KASAN UAF splat in dmesg, when getuid() tries
accessing the creds:
```
==================================================================
BUG: KASAN: slab-use-after-free in __ia32_sys_getuid+0x3d/0x80
Read of size 8 at addr ffff8881269a8950 by task credbug/696
CPU: 2 UID: 0 PID: 696 Comm: credbug Not tainted
7.2.0-rc3-00038-g3b029c035b34 #55 PREEMPT
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS
1.17.0-debian-1.17.0-1 04/01/2014
Call Trace:
<TASK>
__dump_stack+0x21/0x30
dump_stack_lvl+0x76/0xa0
print_address_description+0x7b/0x1f0
print_report+0x5b/0x70
kasan_report+0x16d/0x1a0
[...]
__asan_load8+0x98/0xa0
__ia32_sys_getuid+0x3d/0x80
x64_sys_call+0x1cc1/0x3030
do_syscall_64+0xd8/0x380
[...]
entry_SYSCALL_64_after_hwframe+0x76/0x7e
[...]
</TASK>
Allocated by task 696:
kasan_save_track+0x3a/0x70
kasan_save_alloc_info+0x3c/0x50
__kasan_slab_alloc+0x4e/0x60
kmem_cache_alloc_noprof+0x25f/0x570
prepare_creds+0x2b/0x4d0
__sys_setfsuid+0x8b/0x190
__x64_sys_setfsuid+0x1e/0x30
x64_sys_call+0x955/0x3030
do_syscall_64+0xd8/0x380
entry_SYSCALL_64_after_hwframe+0x76/0x7e
Freed by task 70:
kasan_save_track+0x3a/0x70
kasan_save_free_info+0x46/0x60
__kasan_slab_free+0x43/0x70
kmem_cache_free+0x182/0x500
put_cred_rcu+0x19e/0x210
rcu_core+0x877/0xfd0
rcu_core_si+0x9/0x10
handle_softirqs+0x19f/0x550
__irq_exit_rcu+0xab/0x180
irq_exit_rcu+0x9/0x20
sysvec_call_function+0x73/0x80
asm_sysvec_call_function+0x1b/0x20
Last potentially related work creation:
kasan_save_stack+0x3a/0x60
kasan_record_aux_stack+0x99/0xb0
call_rcu+0x51/0x5c0
__put_cred+0x9a/0xc0
__fput+0x425/0x540
fput_close_sync+0x8a/0x140
__x64_sys_close+0x55/0xe0
x64_sys_call+0x26ce/0x3030
do_syscall_64+0xd8/0x380
entry_SYSCALL_64_after_hwframe+0x76/0x7e
The buggy address belongs to the object at ffff8881269a88c0
which belongs to the cache cred of size 184
The buggy address is located 144 bytes inside of
freed 184-byte region [ffff8881269a88c0, ffff8881269a8978)
[...]
==================================================================
```
^ permalink raw reply
* [PATCH] apparmor: fix cred UAF caused by begin_current_label_crit_section()
From: Jann Horn @ 2026-07-14 15:38 UTC (permalink / raw)
To: John Johansen, John Johansen, Georgia Garcia, apparmor,
Paul Moore, Serge E. Hallyn
Cc: James Morris, Christian Brauner, Al Viro, Peter Zijlstra (Intel),
linux-security-module, kernel list, stable, Jann Horn
AppArmor's begin_current_label_crit_section() is a scary function called
from lots of LSM hooks (in particular VFS/socket-related ones) that checks
if the label referenced by the current creds is marked FLAG_STALE, and if
so, attempts to use aa_replace_current_label() to replace the creds with an
updated version that uses a new label.
The first problem with this is that it would directly lead to UAF of
`struct cred` if anything in the kernel takes a pointer to the current
creds and accesses these past a security hook invocation that replaces
creds, like so:
```
const struct cred *cred = current_cred();
alloc_file_pseudo(...);
uid_t uid = cred->euid;
```
I don't know if anything in the kernel actually does this, but I think it
is very surprising that this pattern could lead to UAF.
The second problem is that things go wrong when aa_replace_current_label()
runs with overridden credentials. aa_replace_current_label() bails out if
`current_cred() != current_real_cred()` (mirroring the check in
proc_pid_attr_write()), but this check can't actually reliably detect
overridden credentials because the overridden creds can be the same as the
objective creds.
So in approximately the following scenario, things go wrong:
1. task begins with <creds A> (as both objective and subjective creds),
with refcount=2
2. task grabs an extra reference on <creds A> for overriding
3. task calls override_creds(<creds A>), which returns a pointer to the old
subjective creds (<creds A>)
4. task enters AppArmor LSM hook
5. AppArmor checks that objective/subjective creds are equal
6. AppArmor replaces both cred pointers with <creds B> and drops 2 refs on
<creds A>
7. task leaves AppArmor LSM hook
8. task calls revert_creds(<creds A>)
9. now task->cred is <creds A> while task->real_cred is <creds B>, but the
task_struct logically holds two references to <creds B>
10. another task drops the extra reference on <creds A> that was used for
overriding, refcount drops to 0
11. now task->real_cred points to freed creds
At this point, any access to current_cred() will be UAF.
I have a test case where I run aa-disable on a profile while a process
using that profile is blocked on splice() from a FUSE passthrough file into
a full pipe; after the profile update, the pipe becomes empty, splice()
resumes, the credentials go out of sync, and a subsequent getuid() syscall
results in a KASAN UAF splat.
To fix this, instead of directly replacing creds, do it via task_work that
will run at the end of the current syscall. (The point in time at which the
cred replacement happens should have no correctness impact; it is just a
performance optimization to avoid unnecessarily touching the refcount of
the new label.)
Note that AppArmor still performs direct cred replacements in the
sb_pivotroot LSM hook after this change, and that direct cred replacements
can still happen in VFS ->write() callbacks via proc_pid_attr_write().
Cc: stable@vger.kernel.org
Fixes: c75afcd153f6 ("AppArmor: contexts used in attaching policy to system objects")
Signed-off-by: Jann Horn <jannh@google.com>
---
include/linux/task_work.h | 1 +
kernel/task_work.c | 14 ++++++++++++++
security/apparmor/include/cred.h | 6 +-----
security/apparmor/include/task.h | 1 +
security/apparmor/task.c | 29 +++++++++++++++++++++++++++++
5 files changed, 46 insertions(+), 5 deletions(-)
diff --git a/include/linux/task_work.h b/include/linux/task_work.h
index 0646804860ff..ce19fc14060c 100644
--- a/include/linux/task_work.h
+++ b/include/linux/task_work.h
@@ -33,6 +33,7 @@ struct callback_head *task_work_cancel_match(struct task_struct *task,
bool (*match)(struct callback_head *, void *data), void *data);
struct callback_head *task_work_cancel_func(struct task_struct *, task_work_func_t);
bool task_work_cancel(struct task_struct *task, struct callback_head *cb);
+bool task_work_has_func(struct task_struct *task, task_work_func_t func);
void task_work_run(void);
static inline void exit_task_work(struct task_struct *task)
diff --git a/kernel/task_work.c b/kernel/task_work.c
index 0f7519f8e7c9..f83d1528e0bc 100644
--- a/kernel/task_work.c
+++ b/kernel/task_work.c
@@ -189,6 +189,20 @@ bool task_work_cancel(struct task_struct *task, struct callback_head *cb)
return ret == cb;
}
+bool task_work_has_func(struct task_struct *task, task_work_func_t func)
+{
+ struct callback_head *work;
+
+ if (!task_work_pending(task))
+ return false;
+ guard(raw_spinlock_irqsave)(&task->pi_lock);
+ for (work = READ_ONCE(task->task_works); work; work = READ_ONCE(work->next)) {
+ if (work->func == func)
+ return true;
+ }
+ return false;
+}
+
/**
* task_work_run - execute the works added by task_work_add()
*
diff --git a/security/apparmor/include/cred.h b/security/apparmor/include/cred.h
index 2b6098149b15..0e8b67159f56 100644
--- a/security/apparmor/include/cred.h
+++ b/security/apparmor/include/cred.h
@@ -222,13 +222,9 @@ static inline struct aa_label *begin_current_label_crit_section(void)
{
struct aa_label *label = aa_current_raw_label();
- might_sleep();
-
if (label_is_stale(label)) {
label = aa_get_newest_label(label);
- if (aa_replace_current_label(label) == 0)
- /* task cred will keep the reference */
- aa_put_label(label);
+ aa_schedule_stale_label_replacement();
}
return label;
diff --git a/security/apparmor/include/task.h b/security/apparmor/include/task.h
index b1aaaf60fa8b..4e49a4142777 100644
--- a/security/apparmor/include/task.h
+++ b/security/apparmor/include/task.h
@@ -30,6 +30,7 @@ struct aa_task_ctx {
};
int aa_replace_current_label(struct aa_label *label);
+void aa_schedule_stale_label_replacement(void);
void aa_set_current_onexec(struct aa_label *label, bool stack);
int aa_set_current_hat(struct aa_label *label, u64 token);
int aa_restore_previous_label(u64 cookie);
diff --git a/security/apparmor/task.c b/security/apparmor/task.c
index b9fb3738124e..8e368f6278f5 100644
--- a/security/apparmor/task.c
+++ b/security/apparmor/task.c
@@ -14,6 +14,7 @@
#include <linux/gfp.h>
#include <linux/ptrace.h>
+#include <linux/task_work.h>
#include "include/path.h"
#include "include/audit.h"
@@ -89,6 +90,34 @@ int aa_replace_current_label(struct aa_label *label)
return 0;
}
+static void aa_replace_stale_label_tw_func(struct callback_head *tw)
+{
+ struct aa_label *label;
+
+ kfree(tw);
+ label = aa_current_raw_label();
+ if (!label_is_stale(label))
+ return;
+ label = aa_get_newest_label(label);
+ aa_replace_current_label(label);
+ aa_put_label(label);
+}
+
+/* replace the current task's stale label on syscall return */
+void aa_schedule_stale_label_replacement(void)
+{
+ struct callback_head *tw;
+
+ if (task_work_has_func(current, aa_replace_stale_label_tw_func))
+ return;
+ tw = kmalloc_obj(struct callback_head);
+ if (!tw)
+ return;
+ init_task_work(tw, aa_replace_stale_label_tw_func);
+ if (task_work_add(current, tw, TWA_RESUME))
+ kfree(tw);
+}
+
/**
* aa_set_current_onexec - set the tasks change_profile to happen onexec
---
base-commit: 3b029c035b34bbc693405ddf759f0e9b920c27f1
change-id: 20260714-fix-apparmor-cred-uaf-cc38ec2b38b7
Best regards,
--
Jann Horn <jannh@google.com>
^ permalink raw reply related
* [PATCH v2 3/3] assoc_array: trim the final shortcut word using the current chunk end
From: Michael Bommarito @ 2026-07-14 11:54 UTC (permalink / raw)
To: David Howells, Jarkko Sakkinen
Cc: Andrew Morton, Paul Moore, James Morris, Serge E . Hallyn,
keyrings, linux-security-module, linux-kernel
In-Reply-To: <20260714115451.3773164-1-michael.bommarito@gmail.com>
assoc_array_walk() masks off the bits past shortcut->skip_to_level in the
word that contains skip_to_level, gated on
round_up(sc_level, ASSOC_ARRAY_KEY_CHUNK_SIZE) > skip_to_level.
That guard is wrong in two opposite ways:
- When sc_level is word-aligned (every word after the first) round_up()
is a no-op, so the guard is sc_level > skip_to_level and never fires for
the word that holds skip_to_level. A shortcut that spans more than one
word and ends in the middle of its last word leaves that word untrimmed,
and its stale high bits leak into the dissimilarity word and can steer
the walk down the wrong descendant.
- When sc_level is unaligned (the first word) and skip_to_level sits on
the next chunk boundary, sc_level + CHUNK would exceed skip_to_level and
fire the trim with shift = skip_to_level & CHUNK_MASK == 0, which clears
the whole dissimilarity word and makes a differing shortcut compare
equal.
Use the end of the chunk that contains sc_level instead:
skip_to_level < round_down(sc_level, CHUNK) + CHUNK
For an aligned sc_level whose word holds skip_to_level this now fires (the
first bug); for an unaligned sc_level with skip_to_level on the following
boundary it does not, so shift is never 0 when the branch runs and the trim
never clears the whole word.
Fixes: 3cb989501c26 ("Add a generic associative array implementation.")
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com>
---
lib/assoc_array.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/lib/assoc_array.c b/lib/assoc_array.c
index bcc6e0a013eb8..b6c9723e12ced 100644
--- a/lib/assoc_array.c
+++ b/lib/assoc_array.c
@@ -255,7 +255,8 @@ assoc_array_walk(const struct assoc_array *array,
sc_segments = shortcut->index_key[sc_level >> ASSOC_ARRAY_KEY_CHUNK_SHIFT];
dissimilarity = segments ^ sc_segments;
- if (round_up(sc_level, ASSOC_ARRAY_KEY_CHUNK_SIZE) > shortcut->skip_to_level) {
+ if (shortcut->skip_to_level < round_down(sc_level,
+ ASSOC_ARRAY_KEY_CHUNK_SIZE) + ASSOC_ARRAY_KEY_CHUNK_SIZE) {
/* Trim segments that are beyond the shortcut */
int shift = shortcut->skip_to_level & ASSOC_ARRAY_KEY_CHUNK_MASK;
dissimilarity &= ~(ULONG_MAX << shift);
--
2.53.0
^ permalink raw reply related
* [PATCH v2 2/3] keys: make keyring key-chunk byte order agree with keyring_diff_objects()
From: Michael Bommarito @ 2026-07-14 11:54 UTC (permalink / raw)
To: David Howells, Jarkko Sakkinen
Cc: Andrew Morton, Paul Moore, James Morris, Serge E . Hallyn,
keyrings, linux-security-module, linux-kernel
In-Reply-To: <20260714115451.3773164-1-michael.bommarito@gmail.com>
keyring_get_key_chunk() loads description bytes into the index chunk low
address first, while keyring_diff_objects() numbers the first differing
bit from the low end and folds the absolute byte index into the level
without removing the inline-prefix offset the level already carries.
The two disagree on byte order and bit position, so the array can be
told two keys first differ at a bit that does not differ in the chunk
the walker uses, letting crafted descriptions collide into one node.
Load the chunk in the order keyring_diff_objects() assumes and drop the
inline-prefix length when folding the byte index into the level. This
only changes the in-memory ordering used to place keys within a keyring;
add, search and read of non-colliding keys are unaffected.
Fixes: f771fde82051 ("keys: Simplify key description management")
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com>
---
security/keys/keyring.c | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/security/keys/keyring.c b/security/keys/keyring.c
index 1739373172ad5..e7066893e6ffc 100644
--- a/security/keys/keyring.c
+++ b/security/keys/keyring.c
@@ -292,9 +292,10 @@ static unsigned long keyring_get_key_chunk(const void *data, int level)
desc_len -= offset;
if (desc_len > n)
desc_len = n;
+ d += desc_len;
do {
chunk <<= 8;
- chunk |= *d++;
+ chunk |= *--d;
} while (--desc_len > 0);
return chunk;
}
@@ -375,7 +376,7 @@ static int keyring_diff_objects(const void *object, const void *data)
return -1;
differ_plus_i:
- level += i;
+ level += i - (int)sizeof(a->desc);
differ:
i = level * 8 + __ffs(seg_a ^ seg_b);
return i;
--
2.53.0
^ permalink raw reply related
* [PATCH v2 1/3] keys: fix out-of-bounds read in keyring_get_key_chunk()
From: Michael Bommarito @ 2026-07-14 11:54 UTC (permalink / raw)
To: David Howells, Jarkko Sakkinen
Cc: Andrew Morton, Paul Moore, James Morris, Serge E . Hallyn,
keyrings, linux-security-module, linux-kernel
In-Reply-To: <20260714115451.3773164-1-michael.bommarito@gmail.com>
For description-level chunks keyring_get_key_chunk() advances the read
pointer by level * sizeof(long) past the inline prefix but only
bounds-checks the prefix, so a long enough key description is read past
its kmemdup(desc, desc_len + 1) allocation. Compute the full byte
offset and bounds-check the description against it before reading.
The walk only reaches a description-level chunk when two keys collide
through the hash, x, type and domain_tag chunks, so this is reached from
an unprivileged add_key(2) with a crafted pair of same-type keys whose
index hashes collide; KASAN reports a slab-out-of-bounds read.
Fixes: f771fde82051 ("keys: Simplify key description management")
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com>
---
security/keys/keyring.c | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
KASAN, x86_64: add_key(2) of a crafted hash-colliding "user"-key pair
(~63-byte descriptions) reports
BUG: KASAN: slab-out-of-bounds in keyring_get_key_chunk
keyring_get_key_chunk <- assoc_array_insert <- __key_link_begin
<- __do_sys_add_key
reading one byte past the description allocation; the same trigger is
KASAN-clean with this patch. On a kernel built without init-on-alloc,
reading the colliding keyring back with KEYCTL_READ returns
uninitialized slab until patches 2 and 3 are applied too. Trigger
available off-list.
diff --git a/security/keys/keyring.c b/security/keys/keyring.c
index 7a2ee0ded7c93..1739373172ad5 100644
--- a/security/keys/keyring.c
+++ b/security/keys/keyring.c
@@ -270,7 +270,7 @@ static unsigned long keyring_get_key_chunk(const void *data, int level)
const struct keyring_index_key *index_key = data;
unsigned long chunk = 0;
const u8 *d;
- int desc_len = index_key->desc_len, n = sizeof(chunk);
+ int desc_len = index_key->desc_len, n = sizeof(chunk), offset;
level /= ASSOC_ARRAY_KEY_CHUNK_SIZE;
switch (level) {
@@ -284,12 +284,12 @@ static unsigned long keyring_get_key_chunk(const void *data, int level)
return (unsigned long)index_key->domain_tag;
default:
level -= 4;
- if (desc_len <= sizeof(index_key->desc))
+ offset = sizeof(index_key->desc) + level * sizeof(long);
+ if (desc_len <= offset)
return 0;
- d = index_key->description + sizeof(index_key->desc);
- d += level * sizeof(long);
- desc_len -= sizeof(index_key->desc);
+ d = index_key->description + offset;
+ desc_len -= offset;
if (desc_len > n)
desc_len = n;
do {
--
2.53.0
^ permalink raw reply related
* [PATCH v2 0/3] keys: fix keyring assoc-array out-of-bounds read and index inconsistency
From: Michael Bommarito @ 2026-07-14 11:54 UTC (permalink / raw)
To: David Howells, Jarkko Sakkinen
Cc: Andrew Morton, Paul Moore, James Morris, Serge E . Hallyn,
keyrings, linux-security-module, linux-kernel
An unprivileged keyring whose keys collide through the description-chunk
path can drive assoc_array node splitting into an out-of-bounds slot write.
Patch 1 stops the out-of-bounds read in keyring_get_key_chunk(); patch 2
makes the chunk byte order agree with keyring_diff_objects(); patch 3 fixes
the shortcut-walk trim so the walk cannot be steered down the wrong
descendant.
v2 changes (patch 3 only; patches 1 and 2 are unchanged):
As sashiko pointed out, the v1 patch-3 guard (sc_level + CHUNK >
skip_to_level) fixed the word-aligned leak but wrongly fired for an
unaligned first word whose skip_to_level sits on the next chunk
boundary: shift = skip_to_level & CHUNK_MASK is then 0 and the trim clears
the whole dissimilarity word, making a differing shortcut compare equal. v2
keys the trim on the end of the chunk that contains sc_level,
round_down(sc_level, CHUNK) + CHUNK, which matches a brute-force oracle over
every sc_level/skip_to_level pair; the original round_up guard and the v1
guard each disagree with the oracle in one regime.
v1: https://lore.kernel.org/keyrings/20260712014500.480410-1-michael.bommarito@gmail.com/
Michael Bommarito (3):
keys: fix out-of-bounds read in keyring_get_key_chunk()
keys: make keyring key-chunk byte order agree with keyring_diff_objects()
assoc_array: trim the final shortcut word using the current chunk end
lib/assoc_array.c | 3 ++-
security/keys/keyring.c | 15 ++++++++-------
2 files changed, 10 insertions(+), 8 deletions(-)
--
2.53.0
^ permalink raw reply
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox