Linux Security Modules development
 help / color / mirror / Atom feed
* [RFC PATCH 18/24] pidfd: make spawn builder execution signal-safe
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>

Wait for child setup in a killable and freezable state. If the caller
is interrupted after task creation, cancel a child that is still
setting up and normalize restart errors because the spawn operation is
not idempotent.

Preserve externally delivered fatal signals over setup errors. Complete
the builder and audit transaction before entering the signal core
directly, so the callback never returns through an invalid userspace
frame. The task work can run from an outer get_signal(), so fatal delivery
deliberately nests get_signal(); the inner path exits and never returns to
the outer call. The child remains embryonic and nondumpable until exec
installs a valid frame and mm.

Reject an already traced caller while holding cred_guard_mutex across
the spawn operation. This prevents CLONE_UNTRACED from becoming an
escape from an existing ptrace supervisor.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 fs/pidfd_spawn.c | 189 +++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 183 insertions(+), 6 deletions(-)

diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
index 5586926988406..0e52503976c0c 100644
--- a/fs/pidfd_spawn.c
+++ b/fs/pidfd_spawn.c
@@ -5,6 +5,7 @@
 
 #include <asm/syscall.h>
 #include <linux/audit.h>
+#include <linux/cgroup.h>
 #include <linux/completion.h>
 #include <linux/cred.h>
 #include <linux/file.h>
@@ -212,8 +213,10 @@ static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
 	state->envp = native_arg(NULL);
 	memset(state->audit_args, 0, sizeof(state->audit_args));
 	state->audit_syscall = 0;
-	state->result = result;
-	pidfd_spawn_set_status(state, PIDFD_SPAWN_SETUP_DONE);
+	if (pidfd_spawn_get_status(state) != PIDFD_SPAWN_CANCELLED) {
+		state->result = result;
+		pidfd_spawn_set_status(state, PIDFD_SPAWN_SETUP_DONE);
+	}
 	mutex_unlock(&state->lock);
 
 	putname(filename);
@@ -226,6 +229,32 @@ static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
 	kfree(staged_path);
 }
 
+static bool pidfd_spawn_cancel(struct pidfd_spawn_state *state,
+			       struct task_struct *task)
+{
+	bool cancelled = false;
+
+	mutex_lock(&state->lock);
+	if (pidfd_spawn_get_status(state) == PIDFD_SPAWN_STARTING) {
+		/*
+		 * Establish SIGKILL group-exit state before publishing
+		 * cancellation. The child also takes state->lock before committing
+		 * its setup result, so it cannot return from task work before the
+		 * signal is pending.
+		 */
+		if (WARN_ON_ONCE(do_send_sig_info(SIGKILL, SEND_SIG_PRIV, task,
+						  PIDTYPE_PID)))
+			goto out_unlock;
+		state->result = -ECANCELED;
+		pidfd_spawn_set_status(state, PIDFD_SPAWN_CANCELLED);
+		cancelled = true;
+	}
+out_unlock:
+	mutex_unlock(&state->lock);
+
+	return cancelled;
+}
+
 static bool pidfd_spawn_same_pid_ns(struct pidfd_spawn_state *state)
 {
 	return current->nsproxy->pid_ns_for_children == state->creator_pid_ns;
@@ -383,6 +412,35 @@ SYSCALL_DEFINE5(pidfd_config, int, fd, unsigned int, cmd,
 	return ret;
 }
 
+static bool pidfd_spawn_child_cancelled(struct pidfd_spawn_state *state)
+{
+	bool cancelled;
+
+	mutex_lock(&state->lock);
+	cancelled = pidfd_spawn_get_status(state) == PIDFD_SPAWN_CANCELLED;
+	mutex_unlock(&state->lock);
+
+	return cancelled;
+}
+
+static bool pidfd_spawn_setup_done(struct pidfd_spawn_state *state)
+{
+	bool done = false;
+
+	mutex_lock(&state->lock);
+	switch (pidfd_spawn_get_status(state)) {
+	case PIDFD_SPAWN_SETUP_DONE:
+	case PIDFD_SPAWN_STARTED:
+		done = true;
+		break;
+	default:
+		break;
+	}
+	mutex_unlock(&state->lock);
+
+	return done;
+}
+
 static void pidfd_spawn_save_audit_context(struct pidfd_spawn_state *state)
 {
 	struct pt_regs *regs = current_pt_regs();
@@ -399,6 +457,52 @@ static void pidfd_spawn_audit_entry(struct pidfd_spawn_state *state)
 				state->audit_args[1], state->audit_args[2],
 				state->audit_args[3]);
 }
+
+static int pidfd_spawn_normalize_result(int result)
+{
+	if (result == -ERESTARTSYS || result == -ERESTARTNOINTR ||
+	    result == -ERESTARTNOHAND || result == -ERESTART_RESTARTBLOCK)
+		return -EINTR;
+	return result;
+}
+
+static int pidfd_spawn_pending_fatal_signal(void)
+{
+	struct sighand_struct *sighand;
+	sigset_t pending;
+	unsigned long flags;
+	int fatal = 0;
+	int sig;
+
+	/*
+	 * fatal_signal_pending() only recognizes pending SIGKILL. A default-fatal
+	 * coredump signal remains pending as itself, but must still win over a
+	 * setup error. Inspect the same pending sets and default dispositions as
+	 * get_signal() without dequeuing the signal.
+	 */
+	sighand = lock_task_sighand(current, &flags);
+	if (WARN_ON_ONCE(!sighand))
+		return 0;
+
+	sigorsets(&pending, &current->pending.signal,
+		  &current->signal->shared_pending.signal);
+	sigandnsets(&pending, &pending, &current->blocked);
+
+	for (sig = 1; sig < _NSIG; sig++) {
+		if (!sigismember(&pending, sig) || sig_kernel_ignore(sig) ||
+		    sig_kernel_stop(sig) ||
+		    sighand->action[sig - 1].sa.sa_handler != SIG_DFL)
+			continue;
+		if ((current->signal->flags & SIGNAL_UNKILLABLE) &&
+		    !sig_kernel_only(sig))
+			continue;
+		fatal = sig;
+		break;
+	}
+	spin_unlock_irqrestore(&sighand->siglock, flags);
+	return fatal;
+}
+
 static void pidfd_spawn_finish_child(struct pidfd_spawn_state *state,
 				     struct filename *filename, int result)
 {
@@ -406,11 +510,40 @@ static void pidfd_spawn_finish_child(struct pidfd_spawn_state *state,
 	complete_all(&state->done);
 }
 
+static __noreturn void
+pidfd_spawn_deliver_fatal_signal(struct pidfd_spawn_state *state,
+				 struct filename *filename, int sig)
+{
+	struct ksignal ksig;
+	sigset_t blocked;
+
+	/*
+	 * The callback frame is not a valid userspace signal frame on every
+	 * architecture. Complete the spawn transaction, then enter the signal
+	 * core directly instead of returning through the callback trampoline.
+	 *
+	 * The embryonic task state prevents ptrace access and coredumps until exec
+	 * installs a valid userspace frame and private mm. Block other catchable
+	 * signals so the selected default-fatal signal remains terminal. If the
+	 * signal core unexpectedly returns, exit without exposing the frame.
+	 */
+	pidfd_spawn_finish_child(state, filename, 0);
+	audit_pidfd_spawn_exit(0, -EINTR);
+	pidfd_spawn_state_put(state);
+
+	sigfillset(&blocked);
+	sigdelset(&blocked, sig);
+	set_current_blocked(&blocked);
+	get_signal(&ksig);
+	do_group_exit(PIDFD_SPAWN_EXIT_FAILURE);
+}
+
 static void pidfd_spawn_child(struct callback_head *work)
 {
 	struct pidfd_spawn_state *state =
 		container_of(work, struct pidfd_spawn_state, task_work);
 	struct filename *filename;
+	int fatal_sig;
 	int ret;
 
 	pidfd_spawn_audit_entry(state);
@@ -420,9 +553,27 @@ static void pidfd_spawn_child(struct callback_head *work)
 	else
 		ret = 0;
 
+	if (pidfd_spawn_child_cancelled(state)) {
+		ret = -ECANCELED;
+		pidfd_spawn_finish_child(state, filename, ret);
+		audit_pidfd_spawn_exit(0, ret);
+		pidfd_spawn_state_put(state);
+		do_group_exit(SIGKILL);
+	}
+	fatal_sig = pidfd_spawn_pending_fatal_signal();
+	if (fatal_sig)
+		pidfd_spawn_deliver_fatal_signal(state, filename, fatal_sig);
+
 	if (!ret)
 		ret = do_execveat_common(AT_FDCWD, filename,
 					 state->argv, state->envp, 0);
+	ret = pidfd_spawn_normalize_result(ret);
+	if (ret) {
+		fatal_sig = pidfd_spawn_pending_fatal_signal();
+		if (fatal_sig)
+			pidfd_spawn_deliver_fatal_signal(state, filename,
+							 fatal_sig);
+	}
 
 	pidfd_spawn_finish_child(state, filename, ret);
 	if (ret) {
@@ -531,9 +682,16 @@ static void pidfd_spawn_attach_vfork_done(struct task_struct *task,
 	task_unlock(task);
 }
 
-static void pidfd_spawn_wait_for_child(struct pidfd_spawn_state *state)
+static int pidfd_spawn_wait_for_child(struct pidfd_spawn_state *state)
 {
-	wait_for_completion(&state->done);
+	unsigned int wait_state = TASK_KILLABLE | TASK_FREEZABLE;
+	int ret;
+
+	cgroup_enter_frozen();
+	ret = wait_for_completion_state(&state->done, wait_state);
+	cgroup_leave_frozen(false);
+
+	return pidfd_spawn_normalize_result(ret);
 }
 
 static int pidfd_spawn_finish_vfork(struct pidfd_spawn_state *state,
@@ -544,7 +702,7 @@ static int pidfd_spawn_finish_vfork(struct pidfd_spawn_state *state,
 
 	ret = wait_for_vfork_done(task, vfork);
 	if (ret)
-		return ret;
+		return pidfd_spawn_normalize_result(ret);
 
 	mutex_lock(&state->lock);
 	ret = state->result;
@@ -624,7 +782,26 @@ static int pidfd_spawn_start(struct file *file,
 	}
 
 	wake_up_new_task(task);
-	pidfd_spawn_wait_for_child(state);
+	ret = pidfd_spawn_wait_for_child(state);
+	if (ret) {
+		int interrupt = ret;
+
+		if (pidfd_spawn_setup_done(state)) {
+			if (pidfd_spawn_finish_vfork(state, task, &vfork))
+				return interrupt;
+			return 0;
+		}
+		if (!pidfd_spawn_cancel(state, task) &&
+		    pidfd_spawn_setup_done(state)) {
+			if (pidfd_spawn_finish_vfork(state, task, &vfork))
+				return interrupt;
+			return 0;
+		}
+
+		/* Drop the stack-based completion without waiting for child setup. */
+		wait_for_vfork_done(task, &vfork);
+		return interrupt;
+	}
 	return pidfd_spawn_finish_vfork(state, task, &vfork);
 }
 
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 17/24] pidfd: audit child spawn execution
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>

The child exec path runs outside the caller's audit context. A delayed
filename can leave a name-only record in the caller transaction, but inode
metadata and exec arguments are otherwise lost because the new task starts
with an unused audit context.

Start a dedicated AUDIT_PIDFD_SPAWN transaction before executable lookup so
audit rules can observe child identity, EXECVE arguments, and inode-backed
PATH records. Close it explicitly with the child setup result.
Task work does not enter through a userspace syscall frame, so do not
synthesize one.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 fs/pidfd_spawn.c | 29 +++++++++++++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
index 41d5cbb49a0f7..5586926988406 100644
--- a/fs/pidfd_spawn.c
+++ b/fs/pidfd_spawn.c
@@ -3,6 +3,8 @@
  * pidfd-backed process spawn builders
  */
 
+#include <asm/syscall.h>
+#include <linux/audit.h>
 #include <linux/completion.h>
 #include <linux/cred.h>
 #include <linux/file.h>
@@ -81,6 +83,8 @@ struct pidfd_spawn_state {
 	char *staged_path;
 	struct user_arg_ptr argv;
 	struct user_arg_ptr envp;
+	unsigned long audit_args[4];
+	int audit_syscall;
 	int result;
 	enum pidfd_spawn_status status;
 };
@@ -206,6 +210,8 @@ static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
 	state->staged_path = NULL;
 	state->argv = native_arg(NULL);
 	state->envp = native_arg(NULL);
+	memset(state->audit_args, 0, sizeof(state->audit_args));
+	state->audit_syscall = 0;
 	state->result = result;
 	pidfd_spawn_set_status(state, PIDFD_SPAWN_SETUP_DONE);
 	mutex_unlock(&state->lock);
@@ -377,6 +383,22 @@ SYSCALL_DEFINE5(pidfd_config, int, fd, unsigned int, cmd,
 	return ret;
 }
 
+static void pidfd_spawn_save_audit_context(struct pidfd_spawn_state *state)
+{
+	struct pt_regs *regs = current_pt_regs();
+	unsigned long args[6];
+
+	syscall_get_arguments(current, regs, args);
+	state->audit_syscall = syscall_get_nr(current, regs);
+	memcpy(state->audit_args, args, sizeof(state->audit_args));
+}
+
+static void pidfd_spawn_audit_entry(struct pidfd_spawn_state *state)
+{
+	audit_pidfd_spawn_entry(state->audit_syscall, state->audit_args[0],
+				state->audit_args[1], state->audit_args[2],
+				state->audit_args[3]);
+}
 static void pidfd_spawn_finish_child(struct pidfd_spawn_state *state,
 				     struct filename *filename, int result)
 {
@@ -391,6 +413,7 @@ static void pidfd_spawn_child(struct callback_head *work)
 	struct filename *filename;
 	int ret;
 
+	pidfd_spawn_audit_entry(state);
 	filename = complete_getname(&state->filename);
 	if (IS_ERR(filename))
 		ret = PTR_ERR(filename);
@@ -403,9 +426,11 @@ static void pidfd_spawn_child(struct callback_head *work)
 
 	pidfd_spawn_finish_child(state, filename, ret);
 	if (ret) {
+		audit_pidfd_spawn_exit(0, ret);
 		pidfd_spawn_state_put(state);
 		do_group_exit(PIDFD_SPAWN_EXIT_FAILURE);
 	}
+	audit_pidfd_spawn_exit(1, 0);
 	pidfd_spawn_state_put(state);
 }
 
@@ -575,6 +600,7 @@ static int pidfd_spawn_start(struct file *file,
 		state->argv = pidfd_spawn_user_arg(kargs->argv);
 		state->envp = pidfd_spawn_user_arg(kargs->envp);
 		pidfd_spawn_set_status(state, PIDFD_SPAWN_STARTING);
+		pidfd_spawn_save_audit_context(state);
 		reinit_completion(&state->done);
 
 		task = pidfd_spawn_create_task(file, state, &clone_args);
@@ -584,6 +610,9 @@ static int pidfd_spawn_start(struct file *file,
 			INIT_DELAYED_FILENAME(&state->filename);
 			state->argv = native_arg(NULL);
 			state->envp = native_arg(NULL);
+			memset(state->audit_args, 0,
+			       sizeof(state->audit_args));
+			state->audit_syscall = 0;
 			state->result = 0;
 			pidfd_spawn_set_status(state,
 					       PIDFD_SPAWN_CONFIGURING);
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 16/24] audit: add pidfd spawn child contexts
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>

Kernel-mediated spawn executes file setup and exec from initial child task
work, not from a second syscall entry. Representing that work as another
AUDIT_SYSCALL record duplicates the source pidfd_spawn_run transaction and
invents a syscall that the child never entered.

Add a dedicated audit context and AUDIT_PIDFD_SPAWN record. Carry the
originating syscall number and arguments. Existing syscall exit filters can
then select the child transaction. CWD, PATH, and EXECVE records remain
correlated with it. Let the child close the transaction with an explicit
result instead of rewriting an architecture syscall-return frame.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 include/linux/audit.h      |  31 +++++++++++
 include/uapi/linux/audit.h |   1 +
 kernel/audit.h             |   1 +
 kernel/auditsc.c           | 105 ++++++++++++++++++++++++++++++++++++-
 4 files changed, 136 insertions(+), 2 deletions(-)

diff --git a/include/linux/audit.h b/include/linux/audit.h
index 45abb3722d304..872624a9fba08 100644
--- a/include/linux/audit.h
+++ b/include/linux/audit.h
@@ -323,6 +323,9 @@ extern int  audit_alloc(struct task_struct *task);
 extern void __audit_free(struct task_struct *task);
 extern void __audit_uring_entry(u8 op);
 extern void __audit_uring_exit(int success, long code);
+void __audit_pidfd_spawn_entry(int major, unsigned long a0, unsigned long a1,
+			       unsigned long a2, unsigned long a3);
+void __audit_pidfd_spawn_exit(int success, long code);
 extern void __audit_syscall_entry(int major, unsigned long a0, unsigned long a1,
 				  unsigned long a2, unsigned long a3);
 extern void __audit_syscall_exit(int ret_success, long ret_value);
@@ -373,6 +376,22 @@ static inline void audit_uring_exit(int success, long code)
 	if (unlikely(audit_context()))
 		__audit_uring_exit(success, code);
 }
+
+static inline void audit_pidfd_spawn_entry(int major, unsigned long a0,
+					   unsigned long a1,
+					   unsigned long a2,
+					   unsigned long a3)
+{
+	if (unlikely(audit_context()))
+		__audit_pidfd_spawn_entry(major, a0, a1, a2, a3);
+}
+
+static inline void audit_pidfd_spawn_exit(int success, long code)
+{
+	if (unlikely(audit_context()))
+		__audit_pidfd_spawn_exit(success, code);
+}
+
 static inline void audit_syscall_entry(int major, unsigned long a0,
 				       unsigned long a1, unsigned long a2,
 				       unsigned long a3)
@@ -380,6 +399,7 @@ static inline void audit_syscall_entry(int major, unsigned long a0,
 	if (unlikely(audit_context()))
 		__audit_syscall_entry(major, a0, a1, a2, a3);
 }
+
 static inline void audit_syscall_exit(void *pt_regs)
 {
 	if (unlikely(audit_context())) {
@@ -611,10 +631,21 @@ static inline void audit_uring_entry(u8 op)
 { }
 static inline void audit_uring_exit(int success, long code)
 { }
+
+static inline void audit_pidfd_spawn_entry(int major, unsigned long a0,
+					   unsigned long a1,
+					   unsigned long a2,
+					   unsigned long a3)
+{ }
+
+static inline void audit_pidfd_spawn_exit(int success, long code)
+{ }
+
 static inline void audit_syscall_entry(int major, unsigned long a0,
 				       unsigned long a1, unsigned long a2,
 				       unsigned long a3)
 { }
+
 static inline void audit_syscall_exit(void *pt_regs)
 { }
 static inline bool audit_dummy_context(void)
diff --git a/include/uapi/linux/audit.h b/include/uapi/linux/audit.h
index e8f5ce677df73..e09fb5222293a 100644
--- a/include/uapi/linux/audit.h
+++ b/include/uapi/linux/audit.h
@@ -122,6 +122,7 @@
 #define AUDIT_OPENAT2		1337	/* Record showing openat2 how args */
 #define AUDIT_DM_CTRL		1338	/* Device Mapper target control */
 #define AUDIT_DM_EVENT		1339	/* Device Mapper events */
+#define AUDIT_PIDFD_SPAWN	1340	/* pidfd spawn child operation */
 
 #define AUDIT_AVC		1400	/* SE Linux avc denial or grant */
 #define AUDIT_SELINUX_ERR	1401	/* Internal SE Linux Errors */
diff --git a/kernel/audit.h b/kernel/audit.h
index 92d5e723d570b..2f740df7beecf 100644
--- a/kernel/audit.h
+++ b/kernel/audit.h
@@ -112,6 +112,7 @@ struct audit_context {
 		AUDIT_CTX_UNUSED,	/* audit_context is currently unused */
 		AUDIT_CTX_SYSCALL,	/* in use by syscall */
 		AUDIT_CTX_URING,	/* in use by io_uring */
+		AUDIT_CTX_PIDFD_SPAWN,	/* pidfd spawn child operation */
 	} context;
 	enum audit_state    state, current_state;
 	struct audit_stamp  stamp;	/* event identifier */
diff --git a/kernel/auditsc.c b/kernel/auditsc.c
index 6610e667c728a..8f6d84a902239 100644
--- a/kernel/auditsc.c
+++ b/kernel/auditsc.c
@@ -1649,6 +1649,30 @@ static void audit_log_uring(struct audit_context *ctx)
 	audit_log_end(ab);
 }
 
+/**
+ * audit_log_pidfd_spawn - generate an AUDIT_PIDFD_SPAWN record
+ * @ctx: the audit context
+ */
+static void audit_log_pidfd_spawn(struct audit_context *ctx)
+{
+	struct audit_buffer *ab;
+
+	ab = audit_log_start(ctx, GFP_KERNEL, AUDIT_PIDFD_SPAWN);
+	if (!ab)
+		return;
+	audit_log_format(ab, "arch=%x syscall=%d", ctx->arch, ctx->major);
+	if (ctx->return_valid != AUDITSC_INVALID)
+		audit_log_format(ab, " success=%s exit=%ld",
+				 str_yes_no(ctx->return_valid == AUDITSC_SUCCESS),
+				 ctx->return_code);
+	audit_log_format(ab, " a0=%lx a1=%lx a2=%lx a3=%lx items=%d",
+			 ctx->argv[0], ctx->argv[1], ctx->argv[2],
+			 ctx->argv[3], ctx->name_count);
+	audit_log_task_info(ab);
+	audit_log_key(ab, ctx->filterkey);
+	audit_log_end(ab);
+}
+
 static void audit_log_exit(void)
 {
 	int i, call_panic = 0;
@@ -1687,6 +1711,9 @@ static void audit_log_exit(void)
 	case AUDIT_CTX_URING:
 		audit_log_uring(context);
 		break;
+	case AUDIT_CTX_PIDFD_SPAWN:
+		audit_log_pidfd_spawn(context);
+		break;
 	default:
 		BUG();
 		break;
@@ -1782,7 +1809,8 @@ static void audit_log_exit(void)
 		audit_log_name(context, n, NULL, i++, &call_panic);
 	}
 
-	if (context->context == AUDIT_CTX_SYSCALL)
+	if (context->context == AUDIT_CTX_SYSCALL ||
+	    context->context == AUDIT_CTX_PIDFD_SPAWN)
 		audit_log_proctitle();
 
 	/* Send end of event record to help user space know we are finished */
@@ -1818,7 +1846,8 @@ void __audit_free(struct task_struct *tsk)
 	if (tsk == current && !context->dummy) {
 		context->return_valid = AUDITSC_INVALID;
 		context->return_code = 0;
-		if (context->context == AUDIT_CTX_SYSCALL) {
+		if (context->context == AUDIT_CTX_SYSCALL ||
+		    context->context == AUDIT_CTX_PIDFD_SPAWN) {
 			audit_filter_syscall(tsk, context);
 			audit_filter_inodes(tsk, context);
 			if (context->current_state == AUDIT_STATE_RECORD)
@@ -1967,6 +1996,78 @@ void __audit_uring_exit(int success, long code)
 	audit_reset_context(ctx);
 }
 
+/**
+ * __audit_pidfd_spawn_entry - start a pidfd spawn child audit context
+ * @major: originating pidfd_spawn_run syscall number
+ * @a0: originating syscall argument 0
+ * @a1: originating syscall argument 1
+ * @a2: originating syscall argument 2
+ * @a3: originating syscall argument 3
+ */
+void __audit_pidfd_spawn_entry(int major, unsigned long a0, unsigned long a1,
+			       unsigned long a2, unsigned long a3)
+{
+	struct audit_context *context = audit_context();
+	enum audit_state state;
+
+	if (!audit_enabled || !context)
+		return;
+
+	WARN_ON(context->context != AUDIT_CTX_UNUSED);
+	WARN_ON(context->name_count);
+	if (context->context != AUDIT_CTX_UNUSED || context->name_count) {
+		audit_panic("unrecoverable error in audit_pidfd_spawn_entry()");
+		return;
+	}
+
+	state = context->state;
+	if (state == AUDIT_STATE_DISABLED)
+		return;
+
+	context->dummy = !audit_n_rules;
+	if (!context->dummy && state == AUDIT_STATE_BUILD) {
+		context->prio = 0;
+		if (auditd_test_task(current))
+			return;
+	}
+
+	context->arch = syscall_get_arch(current);
+	context->major = major;
+	context->argv[0] = a0;
+	context->argv[1] = a1;
+	context->argv[2] = a2;
+	context->argv[3] = a3;
+	context->context = AUDIT_CTX_PIDFD_SPAWN;
+	context->current_state = state;
+	ktime_get_coarse_real_ts64(&context->stamp.ctime);
+}
+
+/**
+ * __audit_pidfd_spawn_exit - finish a pidfd spawn child audit context
+ * @success: whether child-side setup succeeded
+ * @return_code: child-side setup result
+ */
+void __audit_pidfd_spawn_exit(int success, long return_code)
+{
+	struct audit_context *context = audit_context();
+
+	if (!context || context->dummy ||
+	    context->context != AUDIT_CTX_PIDFD_SPAWN)
+		goto out;
+
+	if (!list_empty(&context->killed_trees))
+		audit_kill_trees(context);
+
+	audit_return_fixup(context, success, return_code);
+	audit_filter_syscall(current, context);
+	audit_filter_inodes(current, context);
+	if (context->current_state == AUDIT_STATE_RECORD)
+		audit_log_exit();
+
+out:
+	audit_reset_context(context);
+}
+
 /**
  * __audit_syscall_entry - fill in an audit record at syscall entry
  * @major: major syscall type (function)
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 15/24] fork: keep embryonic tasks hidden until exec completes
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>

A spawn task does not gain a valid userspace register frame when
exec_mmap() installs its private mm. A binary loader can still sleep or
fail before START_THREAD(). This can expose setup registers to ptrace or a
late failed-exec core dump. During earlier setup the task also shares the
source mm, which procfs could expose through the child's PID.

Hide procfs PID lookup, iteration, and cached dentry revalidation from
other tasks, deny coredumps, and report coredump skip state through pidfs.
Allow the embryonic task to use its own proc entries because executable and
interpreter lookup can legitimately use /proc/self. This does not expose
source state to another task. Sample the flag before reading published
credentials; acquire ordering covers the state installed by exec.

Release-publish the transition in the exec core after the final binary
handler succeeds and before audit, tracepoint, ptrace, and connector exec
events. Every successful exec observer then sees a normal task, while a
failed exec leaves the task hidden.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 fs/coredump.c      |  4 +++-
 fs/exec.c          |  2 ++
 fs/pidfd_spawn.c   |  2 --
 fs/pidfs.c         |  7 ++++++-
 fs/proc/base.c     | 11 +++++++++--
 fs/proc/internal.h | 18 +++++++++++++++++-
 kernel/ptrace.c    |  2 +-
 7 files changed, 38 insertions(+), 8 deletions(-)

diff --git a/fs/coredump.c b/fs/coredump.c
index ac3cd74808c64..4b2f0b5c7d606 100644
--- a/fs/coredump.c
+++ b/fs/coredump.c
@@ -1166,7 +1166,9 @@ void vfs_coredump(const kernel_siginfo_t *siginfo)
 		.limit = rlimit(RLIMIT_CORE),
 		/* Snapshot MMF_DUMP_FILTER_* (unlocked) and dumpable for the dump. */
 		.mm_flags = __mm_flags_get_word(mm),
-		.dumpable = task_exec_state_get_dumpable(current),
+		.dumpable = task_is_embryonic_exec(current) ?
+			TASK_DUMPABLE_OFF :
+			task_exec_state_get_dumpable(current),
 		.vma_meta = NULL,
 		.cpu = raw_smp_processor_id(),
 	};
diff --git a/fs/exec.c b/fs/exec.c
index 1a256ba26425e..292c30e80aecb 100644
--- a/fs/exec.c
+++ b/fs/exec.c
@@ -1734,6 +1734,8 @@ static int exec_binprm(struct linux_binprm *bprm)
 			fput(exec);
 	}
 
+	if (task_is_embryonic_exec(current))
+		task_clear_embryonic_exec(current);
 	audit_bprm(bprm);
 	trace_sched_process_exec(current, old_pid, bprm);
 	ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
index c0e95c2ddbe6e..41d5cbb49a0f7 100644
--- a/fs/pidfd_spawn.c
+++ b/fs/pidfd_spawn.c
@@ -400,8 +400,6 @@ static void pidfd_spawn_child(struct callback_head *work)
 	if (!ret)
 		ret = do_execveat_common(AT_FDCWD, filename,
 					 state->argv, state->envp, 0);
-	if (!ret)
-		task_clear_embryonic_exec(current);
 
 	pidfd_spawn_finish_child(state, filename, ret);
 	if (ret) {
diff --git a/fs/pidfs.c b/fs/pidfs.c
index d62235e01b351..7a473c049dcd4 100644
--- a/fs/pidfs.c
+++ b/fs/pidfs.c
@@ -477,6 +477,7 @@ static long pidfd_info(struct file *file, unsigned int cmd, unsigned long arg)
 	struct user_namespace *user_ns;
 	struct pidfs_attr *attr;
 	const struct cred *c;
+	bool embryonic;
 	__u64 mask;
 
 	BUILD_BUG_ON(sizeof(struct pidfd_info) != PIDFD_INFO_SIZE_VER3);
@@ -533,12 +534,16 @@ static long pidfd_info(struct file *file, unsigned int cmd, unsigned long arg)
 		goto copy_out;
 	}
 
+	embryonic = task_is_embryonic_exec(task);
 	c = get_task_cred(task);
 	if (!c)
 		return -ESRCH;
 
 	if ((mask & PIDFD_INFO_COREDUMP) && !kinfo.coredump_mask) {
-		kinfo.coredump_mask = pidfs_coredump_mask(task_exec_state_get_dumpable(task));
+		enum task_dumpable dumpable = embryonic ?
+			TASK_DUMPABLE_OFF : task_exec_state_get_dumpable(task);
+
+		kinfo.coredump_mask = pidfs_coredump_mask(dumpable);
 		kinfo.mask |= PIDFD_INFO_COREDUMP;
 		/* No coredump actually took place, so no coredump signal. */
 	}
diff --git a/fs/proc/base.c b/fs/proc/base.c
index 6a39de424f62a..bd059ddcce863 100644
--- a/fs/proc/base.c
+++ b/fs/proc/base.c
@@ -2053,7 +2053,7 @@ static int pid_revalidate(struct inode *dir, const struct qstr *name,
 		goto out;
 	task = pid_task(proc_pid(inode), PIDTYPE_PID);
 
-	if (task) {
+	if (task && proc_task_visible(task)) {
 		pid_update_inode(task, inode);
 		ret = 1;
 	}
@@ -3492,6 +3492,8 @@ struct dentry *proc_pid_lookup(struct dentry *dentry, unsigned int flags)
 	rcu_read_unlock();
 	if (!task)
 		goto out;
+	if (!proc_task_visible(task))
+		goto out_put_task;
 
 	/* Limit procfs to only ptraceable tasks */
 	if (fs_info->hide_pid == HIDEPID_NOT_PTRACEABLE) {
@@ -3539,11 +3541,12 @@ static bool next_tgid(struct tgid_iter *it)
 		if (pid) {
 			it->tgid = pid_nr_ns(pid, it->pid_ns);
 			it->task = pid_task(pid, PIDTYPE_TGID);
-			if (it->task) {
+			if (it->task && proc_task_visible(it->task)) {
 				get_task_struct(it->task);
 				rcu_read_unlock();
 				return true;
 			}
+			it->task = NULL;
 		} else {
 			rcu_read_unlock();
 			return false;
@@ -3807,6 +3810,8 @@ static struct dentry *proc_task_lookup(struct inode *dir, struct dentry * dentry
 	rcu_read_unlock();
 	if (!task)
 		goto out;
+	if (!proc_task_visible(task))
+		goto out_drop_task;
 	if (!same_thread_group(leader, task))
 		goto out_drop_task;
 
@@ -3844,6 +3849,8 @@ static struct task_struct *first_tid(struct pid *pid, int tid, loff_t f_pos,
 	task = pid_task(pid, PIDTYPE_PID);
 	if (!task)
 		goto fail;
+	if (!proc_task_visible(task))
+		goto fail;
 
 	/* Attempt to start with the tid of a thread */
 	if (tid && nr) {
diff --git a/fs/proc/internal.h b/fs/proc/internal.h
index b232e1098117b..81ad44502272e 100644
--- a/fs/proc/internal.h
+++ b/fs/proc/internal.h
@@ -147,9 +147,25 @@ static inline struct pid *proc_pid(const struct inode *inode)
 	return PROC_I(inode)->pid;
 }
 
+static inline bool proc_task_visible(struct task_struct *task)
+{
+	/*
+	 * An embryonic task may need its own proc entries during exec setup,
+	 * but it is not a valid userspace process image for other tasks yet.
+	 */
+	return task == current || !task_is_embryonic_exec(task);
+}
+
 static inline struct task_struct *get_proc_task(const struct inode *inode)
 {
-	return get_pid_task(proc_pid(inode), PIDTYPE_PID);
+	struct task_struct *task;
+
+	task = get_pid_task(proc_pid(inode), PIDTYPE_PID);
+	if (task && !proc_task_visible(task)) {
+		put_task_struct(task);
+		return NULL;
+	}
+	return task;
 }
 
 void task_dump_owner(struct task_struct *task, umode_t mode,
diff --git a/kernel/ptrace.c b/kernel/ptrace.c
index 36eb9b154a397..59e11dae803c8 100644
--- a/kernel/ptrace.c
+++ b/kernel/ptrace.c
@@ -314,7 +314,7 @@ static int __ptrace_may_access(struct task_struct *task, unsigned int mode)
 		WARN(1, "denying ptrace access check without PTRACE_MODE_*CREDS\n");
 		return -EPERM;
 	}
-	if (task_is_embryonic_exec(task))
+	if (task_is_embryonic_exec(task) && task != current)
 		return -EPERM;
 
 	/* May we inspect the given task?
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 14/24] pidfd: create and execute spawn builder tasks
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 pidfd_spawn_run() for a taskless builder. Validate the versioned run
payload, including native and compat pointer-container widths. Allocate a
numeric pid with the reserved pidfs identity. Create the child through the
existing vfork backend.

Publish the same future pidfd as the child pidfd before waking the child.
Queue setup as initial task work so no kernel callback pointers are carried
through architecture register state. Keep the task embryonic until exec has
installed a valid userspace image and register frame.

A failure before task creation initially leaves the builder configurable.
Once a task exists, return setup errors after the child exits. A later
state-machine change tightens the pre-task path to a one-shot claim.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 fs/pidfd_spawn.c | 406 ++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 402 insertions(+), 4 deletions(-)

diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
index 99e3ef56ecfa3..c0e95c2ddbe6e 100644
--- a/fs/pidfd_spawn.c
+++ b/fs/pidfd_spawn.c
@@ -3,6 +3,7 @@
  * pidfd-backed process spawn builders
  */
 
+#include <linux/completion.h>
 #include <linux/cred.h>
 #include <linux/file.h>
 #include <linux/fs.h>
@@ -13,14 +14,50 @@
 #include <linux/pid_namespace.h>
 #include <linux/pidfd_spawn.h>
 #include <linux/pidfs.h>
+#include <linux/random.h>
 #include <linux/refcount.h>
+#include <linux/sched/mm.h>
 #include <linux/sched/signal.h>
+#include <linux/sched/task.h>
+#include <linux/security.h>
 #include <linux/slab.h>
 #include <linux/syscalls.h>
+#include <linux/task_work.h>
 #include <linux/uaccess.h>
 #include <uapi/linux/pidfd.h>
+#include <uapi/linux/wait.h>
+#include <trace/events/sched.h>
 
+#include "exec_internal.h"
+
+static inline void __user *pidfd_spawn_user_ptr(__u64 p)
+{
+	return u64_to_user_ptr(p);
+}
+
+static inline bool pidfd_spawn_user_ptr_fits(__u64 p)
+{
+#ifdef CONFIG_COMPAT
+	if (in_compat_syscall())
+		return p == (__u64)(compat_uptr_t)p;
+#endif
+	return p == (__u64)(unsigned long)p;
+}
+
+static inline struct user_arg_ptr pidfd_spawn_user_arg(__u64 p)
+{
+#ifdef CONFIG_COMPAT
+	if (in_compat_syscall())
+		return compat_arg((compat_uptr_t __user *)
+				  u64_to_user_ptr(p));
+#endif
+	return native_arg(u64_to_user_ptr(p));
+}
+
+#define PIDFD_SPAWN_EXIT_FAILURE	(127 << 8)
 #define PIDFD_SPAWN_MAX_CONFIG_KEY_SIZE	256
+DEFINE_FREE(pidfd_spawn_dismiss_filename, struct delayed_filename,
+	    dismiss_delayed_filename(&_T))
 
 enum pidfd_spawn_status {
 	PIDFD_SPAWN_CONFIGURING,
@@ -31,14 +68,20 @@ enum pidfd_spawn_status {
 };
 
 struct pidfd_spawn_state {
-	/* Serializes configuration and payload teardown. */
+	/* Serializes configuration, launch, cancellation, and payload teardown. */
 	struct mutex lock;
+	struct completion done;
+	struct callback_head task_work;
 	refcount_t count;
 	struct pid *pid;
 	struct mm_struct *creator_mm;
 	const struct cred *creator_cred;
 	struct pid_namespace *creator_pid_ns;
+	struct delayed_filename filename;
 	char *staged_path;
+	struct user_arg_ptr argv;
+	struct user_arg_ptr envp;
+	int result;
 	enum pidfd_spawn_status status;
 };
 
@@ -80,6 +123,10 @@ pidfd_spawn_configuring_error(const struct pidfd_spawn_state *state)
 
 static void pidfd_spawn_free_state(struct pidfd_spawn_state *state)
 {
+	if (!state)
+		return;
+
+	dismiss_delayed_filename(&state->filename);
 	kfree(state->staged_path);
 	if (state->creator_mm)
 		mmdrop(state->creator_mm);
@@ -136,11 +183,73 @@ pidfd_spawn_state_get_file(struct file *file)
 	return state;
 }
 
+static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
+				      struct filename *filename, int result)
+{
+	const struct cred *creator_cred;
+	struct mm_struct *creator_mm;
+	struct pid_namespace *creator_pid_ns;
+	char *staged_path;
+
+	/*
+	 * Run data is one-shot. Detach it under the lock, then drop refs
+	 * after the child no longer needs the shared setup payload.
+	 */
+	mutex_lock(&state->lock);
+	creator_mm = state->creator_mm;
+	creator_cred = state->creator_cred;
+	creator_pid_ns = state->creator_pid_ns;
+	staged_path = state->staged_path;
+	state->creator_mm = NULL;
+	state->creator_cred = NULL;
+	state->creator_pid_ns = NULL;
+	state->staged_path = NULL;
+	state->argv = native_arg(NULL);
+	state->envp = native_arg(NULL);
+	state->result = result;
+	pidfd_spawn_set_status(state, PIDFD_SPAWN_SETUP_DONE);
+	mutex_unlock(&state->lock);
+
+	putname(filename);
+	if (creator_mm)
+		mmdrop(creator_mm);
+	if (creator_cred)
+		put_cred(creator_cred);
+	if (creator_pid_ns)
+		put_pid_ns(creator_pid_ns);
+	kfree(staged_path);
+}
+
+static bool pidfd_spawn_same_pid_ns(struct pidfd_spawn_state *state)
+{
+	return current->nsproxy->pid_ns_for_children == state->creator_pid_ns;
+}
+
 static bool pidfd_spawn_same_creator(struct pidfd_spawn_state *state)
 {
+	/*
+	 * Holding the pidfd alone is not enough authority to configure or start
+	 * this builder. A caller must also share the creator's mm, credential
+	 * object, and child PID namespace.
+	 *
+	 * The child PID namespace is part of the builder authority even though no
+	 * numeric PID is allocated until run.
+	 */
 	return current->mm == state->creator_mm &&
 	       current_cred() == state->creator_cred &&
-	       current->nsproxy->pid_ns_for_children == state->creator_pid_ns;
+	       pidfd_spawn_same_pid_ns(state);
+}
+
+static int pidfd_spawn_check_startable(struct pidfd_spawn_state *state)
+{
+	int ret;
+
+	scoped_cond_guard(mutex_intr, return -EINTR, &state->lock) {
+		ret = pidfd_spawn_configuring_error(state);
+		if (!ret && !pidfd_spawn_same_creator(state))
+			ret = -EPERM;
+	}
+	return ret;
 }
 
 static struct filename *pidfd_spawn_get_path(const char __user *value)
@@ -153,6 +262,31 @@ static struct filename *pidfd_spawn_get_path(const char __user *value)
 	return no_free_ptr(name);
 }
 
+static int
+pidfd_spawn_get_delayed_path(struct delayed_filename *delayed,
+			     const char __user *value)
+{
+	struct filename *name __free(putname) = NULL;
+
+	name = pidfd_spawn_get_path(value);
+	if (IS_ERR(name))
+		return PTR_ERR(name);
+
+	return putname_to_delayed(delayed, no_free_ptr(name));
+}
+
+static int
+pidfd_spawn_get_delayed_kernel_path(struct delayed_filename *delayed,
+				    const char *value)
+{
+	struct filename *name __free(putname) = getname_kernel(value);
+
+	if (IS_ERR(name))
+		return PTR_ERR(name);
+
+	return putname_to_delayed(delayed, no_free_ptr(name));
+}
+
 static char *pidfd_spawn_copy_path(const char __user *value)
 {
 	struct filename *name __free(putname) = NULL;
@@ -173,14 +307,14 @@ static int pidfd_spawn_state_set_path(struct pidfd_spawn_state *state,
 {
 	char *path __free(kfree) = NULL;
 	char *old __free(kfree) = NULL;
+	int ret;
 
 	path = pidfd_spawn_copy_path(value);
 	if (IS_ERR(path))
 		return PTR_ERR(path);
 
 	scoped_cond_guard(mutex_intr, return -EINTR, &state->lock) {
-		int ret = pidfd_spawn_configuring_error(state);
-
+		ret = pidfd_spawn_configuring_error(state);
 		if (ret)
 			return ret;
 		if (!pidfd_spawn_same_creator(state))
@@ -243,6 +377,230 @@ SYSCALL_DEFINE5(pidfd_config, int, fd, unsigned int, cmd,
 	return ret;
 }
 
+static void pidfd_spawn_finish_child(struct pidfd_spawn_state *state,
+				     struct filename *filename, int result)
+{
+	pidfd_spawn_drop_run_data(state, filename, result);
+	complete_all(&state->done);
+}
+
+static void pidfd_spawn_child(struct callback_head *work)
+{
+	struct pidfd_spawn_state *state =
+		container_of(work, struct pidfd_spawn_state, task_work);
+	struct filename *filename;
+	int ret;
+
+	filename = complete_getname(&state->filename);
+	if (IS_ERR(filename))
+		ret = PTR_ERR(filename);
+	else
+		ret = 0;
+
+	if (!ret)
+		ret = do_execveat_common(AT_FDCWD, filename,
+					 state->argv, state->envp, 0);
+	if (!ret)
+		task_clear_embryonic_exec(current);
+
+	pidfd_spawn_finish_child(state, filename, ret);
+	if (ret) {
+		pidfd_spawn_state_put(state);
+		do_group_exit(PIDFD_SPAWN_EXIT_FAILURE);
+	}
+	pidfd_spawn_state_put(state);
+}
+
+static int pidfd_spawn_copy_run_args(struct pidfd_spawn_run_args *kargs,
+				     struct pidfd_spawn_run_args __user *uargs,
+				     size_t usize)
+{
+	int ret;
+
+	BUILD_BUG_ON(sizeof(struct pidfd_spawn_run_args) !=
+		     PIDFD_SPAWN_RUN_SIZE_VER0);
+
+	if (usize < PIDFD_SPAWN_RUN_SIZE_VER0)
+		return -EINVAL;
+	if (usize > PAGE_SIZE)
+		return -E2BIG;
+	ret = copy_struct_from_user(kargs, sizeof(*kargs), uargs, usize);
+	if (ret)
+		return ret;
+	if (kargs->flags || kargs->reserved0 || kargs->reserved[0] ||
+	    kargs->reserved[1])
+		return -EINVAL;
+	if (!kargs->argv)
+		return -EINVAL;
+	if (kargs->nr_actions || kargs->actions || kargs->action_size)
+		return -EINVAL;
+	if (!pidfd_spawn_user_ptr_fits(kargs->path) ||
+	    !pidfd_spawn_user_ptr_fits(kargs->argv) ||
+	    !pidfd_spawn_user_ptr_fits(kargs->envp) ||
+	    !pidfd_spawn_user_ptr_fits(kargs->actions))
+		return -EFAULT;
+
+	return 0;
+}
+
+static struct task_struct *
+pidfd_spawn_create_task(struct file *file, struct pidfd_spawn_state *state,
+			struct kernel_clone_args *clone_args)
+{
+	struct pid *pid;
+	struct task_struct *task;
+	u64 ino;
+	int ret;
+
+	ino = pidfs_future_file_ino(file);
+	if (!ino)
+		return ERR_PTR(-EBADF);
+
+	pid = alloc_pid_with_pidfs_ino(current->nsproxy->pid_ns_for_children,
+				       NULL, 0, ino);
+	if (IS_ERR(pid))
+		return ERR_CAST(pid);
+
+	ret = pidfs_future_file_set_pid(file, pid);
+	if (ret) {
+		free_pid(pid);
+		return ERR_PTR(ret);
+	}
+
+	refcount_inc(&state->count);
+	task = copy_process(pid, 0, NUMA_NO_NODE, clone_args);
+	add_latent_entropy();
+	if (IS_ERR(task)) {
+		pidfd_spawn_state_put(state);
+		pidfs_future_file_clear_pid(file, pid);
+		free_pid(pid);
+		return task;
+	}
+
+	trace_sched_process_fork(current, task);
+	get_task_struct(task);
+	return task;
+}
+
+static void pidfd_spawn_publish_pid(struct file *file,
+				    struct pidfd_spawn_state *state,
+				    struct task_struct *task)
+{
+	struct pid *pid = get_task_pid(task, PIDTYPE_PID);
+
+	pidfs_future_file_publish_pid(file, pid);
+	/*
+	 * Publish only after the inode identity, pidfs attributes, and exit-wakeup
+	 * forwarding are ready. Pair with the acquire loads in pidfd resolution
+	 * and poll so readers that observe @pid can use ordinary pidfd operations.
+	 */
+	smp_store_release(&state->pid, pid);
+	pidfs_future_file_notify(file);
+}
+
+static void pidfd_spawn_attach_vfork_done(struct task_struct *task,
+					  struct completion *vfork)
+{
+	init_completion(vfork);
+
+	task_lock(task);
+	task->vfork_done = vfork;
+	task_unlock(task);
+}
+
+static void pidfd_spawn_wait_for_child(struct pidfd_spawn_state *state)
+{
+	wait_for_completion(&state->done);
+}
+
+static int pidfd_spawn_finish_vfork(struct pidfd_spawn_state *state,
+				    struct task_struct *task,
+				    struct completion *vfork)
+{
+	int ret;
+
+	ret = wait_for_vfork_done(task, vfork);
+	if (ret)
+		return ret;
+
+	mutex_lock(&state->lock);
+	ret = state->result;
+	pidfd_spawn_set_status(state, PIDFD_SPAWN_STARTED);
+	mutex_unlock(&state->lock);
+	return ret;
+}
+
+static int pidfd_spawn_start(struct file *file,
+			     struct pidfd_spawn_state *state,
+			     const struct pidfd_spawn_run_args *kargs)
+{
+	struct delayed_filename filename
+		__free(pidfd_spawn_dismiss_filename) = {};
+	struct delayed_filename drop_filename
+		__free(pidfd_spawn_dismiss_filename) = {};
+	struct kernel_clone_args clone_args = {
+		.flags		= CLONE_VM | CLONE_VFORK | CLONE_UNTRACED,
+		.exit_signal	= SIGCHLD,
+		.task_work	= &state->task_work,
+		.embryonic_exec = true,
+	};
+	struct completion vfork;
+	struct task_struct *task;
+	int ret;
+
+	if (kargs->path) {
+		ret = pidfd_spawn_get_delayed_path(&filename,
+						   pidfd_spawn_user_ptr(kargs->path));
+		if (ret)
+			return ret;
+	}
+
+	scoped_cond_guard(mutex_intr, return -EINTR, &state->lock) {
+		ret = pidfd_spawn_configuring_error(state);
+		if (ret)
+			return ret;
+		if (!pidfd_spawn_same_creator(state))
+			return -EPERM;
+		if (state->staged_path && kargs->path)
+			return -EINVAL;
+		if (!state->staged_path && !kargs->path)
+			return -EINVAL;
+		if (state->staged_path) {
+			ret = pidfd_spawn_get_delayed_kernel_path(&filename,
+								  state->staged_path);
+			if (ret)
+				return ret;
+		}
+		state->filename = filename;
+		INIT_DELAYED_FILENAME(&filename);
+
+		state->argv = pidfd_spawn_user_arg(kargs->argv);
+		state->envp = pidfd_spawn_user_arg(kargs->envp);
+		pidfd_spawn_set_status(state, PIDFD_SPAWN_STARTING);
+		reinit_completion(&state->done);
+
+		task = pidfd_spawn_create_task(file, state, &clone_args);
+		if (IS_ERR(task)) {
+			ret = PTR_ERR(task);
+			drop_filename = state->filename;
+			INIT_DELAYED_FILENAME(&state->filename);
+			state->argv = native_arg(NULL);
+			state->envp = native_arg(NULL);
+			state->result = 0;
+			pidfd_spawn_set_status(state,
+					       PIDFD_SPAWN_CONFIGURING);
+			return ret;
+		}
+
+		pidfd_spawn_publish_pid(file, state, task);
+		pidfd_spawn_attach_vfork_done(task, &vfork);
+	}
+
+	wake_up_new_task(task);
+	pidfd_spawn_wait_for_child(state);
+	return pidfd_spawn_finish_vfork(state, task, &vfork);
+}
+
 int pidfd_empty_open(unsigned int flags)
 {
 	struct pidfd_spawn_state *state;
@@ -257,7 +615,13 @@ int pidfd_empty_open(unsigned int flags)
 		return -ENOMEM;
 
 	mutex_init(&state->lock);
+	init_completion(&state->done);
+	init_task_work(&state->task_work, pidfd_spawn_child);
 	refcount_set(&state->count, 1);
+	/*
+	 * Remember the creator so later builder operations cannot be delegated
+	 * just by passing the pidfd through SCM_RIGHTS or similar fd passing.
+	 */
 	mmgrab(current->mm);
 	state->creator_mm = current->mm;
 	state->creator_cred = get_current_cred();
@@ -282,3 +646,37 @@ int pidfd_empty_open(unsigned int flags)
 	fd_install(pidfd, pidfile);
 	return pidfd;
 }
+
+SYSCALL_DEFINE3(pidfd_spawn_run, int, fd,
+		struct pidfd_spawn_run_args __user *, uargs, size_t, usize)
+{
+	struct pidfd_spawn_run_args kargs;
+	struct pidfd_spawn_state *state __free(pidfd_spawn_state) = NULL;
+	int ret;
+
+	ret = pidfd_spawn_copy_run_args(&kargs, uargs, usize);
+	if (ret)
+		return ret;
+
+	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);
+
+	scoped_cond_guard(mutex_intr, return -ERESTARTNOINTR,
+			  &current->signal->cred_guard_mutex) {
+		if (READ_ONCE(current->ptrace))
+			return -EPERM;
+		ret = pidfd_spawn_check_startable(state);
+		if (ret)
+			return ret;
+		ret = pidfd_spawn_start(fd_file(f), state, &kargs);
+	}
+	if (!ret)
+		ret = pid_vnr(state->pid);
+
+	return ret;
+}
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 13/24] fork: let new tasks start with task work
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>

Process builders need to run setup in the child before its first return
to userspace without storing a kernel callback in architecture-specific
saved registers.

Allow an internal clone caller to supply task work. Queue it after the
last ordinary fork failure check, while the child is still TASK_NEW. A
NULL work pointer leaves existing clone callers unchanged.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 include/linux/sched/task.h | 2 ++
 kernel/fork.c              | 6 ++++++
 2 files changed, 8 insertions(+)

diff --git a/include/linux/sched/task.h b/include/linux/sched/task.h
index 5b7facf12b33b..830de99aabaed 100644
--- a/include/linux/sched/task.h
+++ b/include/linux/sched/task.h
@@ -43,6 +43,8 @@ struct kernel_clone_args {
 	int idle;
 	int (*fn)(void *);
 	void *fn_arg;
+	/* Run before the new task first returns to userspace. */
+	struct callback_head *task_work;
 	struct cgroup *cgrp;
 	struct css_set *cset;
 	bool embryonic_exec;
diff --git a/kernel/fork.c b/kernel/fork.c
index e3ade83b2d4e2..c0584eaa74711 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -109,6 +109,7 @@
 #include <uapi/linux/pidfd.h>
 #include <linux/pidfs.h>
 #include <linux/tick.h>
+#include <linux/task_work.h>
 #include <linux/unwind_deferred.h>
 #include <linux/pgalloc.h>
 #include <linux/uaccess.h>
@@ -2504,6 +2505,11 @@ __latent_entropy struct task_struct *copy_process(
 		retval = -EINTR;
 		goto bad_fork_core_free;
 	}
+	if (args->task_work) {
+		retval = task_work_add(p, args->task_work, TWA_RESUME);
+		if (WARN_ON_ONCE(retval))
+			goto bad_fork_core_free;
+	}
 
 	/* No more failure paths after this point. */
 
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 12/24] fork: let kernel callers create embryonic tasks
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>

A kernel-created task can become visible before it has installed a new
executable image or a valid userspace register frame. Exposing such a task
through ptrace can disclose kernel setup state.

Add a task-local embryonic flag and an internal clone argument for callers
that need this lifecycle. Reject ptrace access until the creator clears the
flag. Clear it with release ordering and observe it with acquire ordering.
This orders visibility of the completed exec state with the transition.

Existing fork, vfork, clone, and kernel-thread callers leave the argument
unset and retain their current behavior.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 include/linux/sched.h      | 21 +++++++++++++++++++++
 include/linux/sched/task.h |  1 +
 kernel/fork.c              |  1 +
 kernel/ptrace.c            |  4 ++++
 4 files changed, 27 insertions(+)

diff --git a/include/linux/sched.h b/include/linux/sched.h
index 908aff695ef86..031ae92884c3c 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -1863,6 +1863,7 @@ static __always_inline bool is_user_task(struct task_struct *task)
 #define PFA_SPEC_IB_DISABLE		5	/* Indirect branch speculation restricted */
 #define PFA_SPEC_IB_FORCE_DISABLE	6	/* Indirect branch speculation permanently restricted */
 #define PFA_SPEC_SSB_NOEXEC		7	/* Speculative Store Bypass clear on execve() */
+#define PFA_EMBRYONIC_EXEC		8	/* No valid user register frame */
 
 #define TASK_PFA_TEST(name, func)					\
 	static inline bool task_##func(struct task_struct *p)		\
@@ -1905,6 +1906,26 @@ TASK_PFA_CLEAR(SPEC_IB_DISABLE, spec_ib_disable)
 TASK_PFA_TEST(SPEC_IB_FORCE_DISABLE, spec_ib_force_disable)
 TASK_PFA_SET(SPEC_IB_FORCE_DISABLE, spec_ib_force_disable)
 
+/* Clearing this bit publishes the register and security state from exec. */
+static inline bool task_is_embryonic_exec(struct task_struct *p)
+{
+	return test_bit_acquire(PFA_EMBRYONIC_EXEC, &p->atomic_flags);
+}
+
+/* The task is private during copy_process(), so no publication barrier. */
+static inline void task_init_embryonic_exec(struct task_struct *p, bool set)
+{
+	if (set)
+		__set_bit(PFA_EMBRYONIC_EXEC, &p->atomic_flags);
+	else
+		__clear_bit(PFA_EMBRYONIC_EXEC, &p->atomic_flags);
+}
+
+static inline void task_clear_embryonic_exec(struct task_struct *p)
+{
+	clear_bit_unlock(PFA_EMBRYONIC_EXEC, &p->atomic_flags);
+}
+
 static inline void
 current_restore_flags(unsigned long orig_flags, unsigned long flags)
 {
diff --git a/include/linux/sched/task.h b/include/linux/sched/task.h
index 92b4e3bc31e66..5b7facf12b33b 100644
--- a/include/linux/sched/task.h
+++ b/include/linux/sched/task.h
@@ -45,6 +45,7 @@ struct kernel_clone_args {
 	void *fn_arg;
 	struct cgroup *cgrp;
 	struct css_set *cset;
+	bool embryonic_exec;
 	unsigned int kill_seq;
 };
 
diff --git a/kernel/fork.c b/kernel/fork.c
index d16405c037c2f..e3ade83b2d4e2 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -2138,6 +2138,7 @@ __latent_entropy struct task_struct *copy_process(
 	retval = copy_exec_state(clone_flags, p);
 	if (retval)
 		goto bad_fork_free;
+	task_init_embryonic_exec(p, args->embryonic_exec);
 	p->flags &= ~PF_KTHREAD;
 	if (args->kthread)
 		p->flags |= PF_KTHREAD;
diff --git a/kernel/ptrace.c b/kernel/ptrace.c
index d041645d9d17d..36eb9b154a397 100644
--- a/kernel/ptrace.c
+++ b/kernel/ptrace.c
@@ -56,6 +56,8 @@ bool ptracer_access_allowed(struct task_struct *tsk)
 	guard(rcu)();
 	if (ptrace_parent(tsk) != current)
 		return false;
+	if (task_is_embryonic_exec(tsk))
+		return false;
 	es = task_exec_state_rcu(tsk);
 	return READ_ONCE(es->dumpable) == TASK_DUMPABLE_OWNER ||
 	       ptracer_capable(tsk, es->user_ns);
@@ -312,6 +314,8 @@ static int __ptrace_may_access(struct task_struct *task, unsigned int mode)
 		WARN(1, "denying ptrace access check without PTRACE_MODE_*CREDS\n");
 		return -EPERM;
 	}
+	if (task_is_embryonic_exec(task))
+		return -EPERM;
 
 	/* May we inspect the given task?
 	 * This check is used both for attaching with ptrace
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 11/24] pidfd: add spawn builder state tracking
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 explicit configuring, starting, setup, started, and cancelled
states. Serialize configuration and launch transitions with the builder
mutex so configuration cannot change after task creation starts.

Publish the eventual pid with release and acquire ordering. This state
machine also provides the boundary for later one-shot claim and
cancellation semantics.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 fs/pidfd_spawn.c | 50 +++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 49 insertions(+), 1 deletion(-)

diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
index 46207c8e8139f..99e3ef56ecfa3 100644
--- a/fs/pidfd_spawn.c
+++ b/fs/pidfd_spawn.c
@@ -22,6 +22,14 @@
 
 #define PIDFD_SPAWN_MAX_CONFIG_KEY_SIZE	256
 
+enum pidfd_spawn_status {
+	PIDFD_SPAWN_CONFIGURING,
+	PIDFD_SPAWN_STARTING,
+	PIDFD_SPAWN_SETUP_DONE,
+	PIDFD_SPAWN_STARTED,
+	PIDFD_SPAWN_CANCELLED,
+};
+
 struct pidfd_spawn_state {
 	/* Serializes configuration and payload teardown. */
 	struct mutex lock;
@@ -31,10 +39,45 @@ struct pidfd_spawn_state {
 	const struct cred *creator_cred;
 	struct pid_namespace *creator_pid_ns;
 	char *staged_path;
+	enum pidfd_spawn_status status;
 };
 
+static struct pid *
+pidfd_spawn_load_pid(const struct pidfd_spawn_state *state)
+{
+	/* Pair with the release publication in pidfd_spawn_publish_pid(). */
+	return smp_load_acquire(&state->pid);
+}
+
+static enum pidfd_spawn_status
+pidfd_spawn_get_status(const struct pidfd_spawn_state *state)
+{
+	return READ_ONCE(state->status);
+}
+
+static void pidfd_spawn_set_status(struct pidfd_spawn_state *state,
+				   enum pidfd_spawn_status status)
+{
+	WRITE_ONCE(state->status, status);
+}
+
 static void pidfd_spawn_state_put(struct pidfd_spawn_state *state);
 
+static int
+pidfd_spawn_configuring_error(const struct pidfd_spawn_state *state)
+{
+	switch (pidfd_spawn_get_status(state)) {
+	case PIDFD_SPAWN_CONFIGURING:
+		return 0;
+	case PIDFD_SPAWN_CANCELLED:
+		if (!pidfd_spawn_load_pid(state))
+			return -ECANCELED;
+		fallthrough;
+	default:
+		return -EBUSY;
+	}
+}
+
 static void pidfd_spawn_free_state(struct pidfd_spawn_state *state)
 {
 	kfree(state->staged_path);
@@ -73,7 +116,7 @@ static struct pid *pidfd_spawn_file_pid(void *data)
 	struct pidfd_spawn_state *state = data;
 	struct pid *pid;
 
-	pid = READ_ONCE(state->pid);
+	pid = pidfd_spawn_load_pid(state);
 	return pid ? pid : ERR_PTR(-ESRCH);
 }
 
@@ -136,6 +179,10 @@ static int pidfd_spawn_state_set_path(struct pidfd_spawn_state *state,
 		return PTR_ERR(path);
 
 	scoped_cond_guard(mutex_intr, return -EINTR, &state->lock) {
+		int ret = pidfd_spawn_configuring_error(state);
+
+		if (ret)
+			return ret;
 		if (!pidfd_spawn_same_creator(state))
 			return -EPERM;
 
@@ -216,6 +263,7 @@ int pidfd_empty_open(unsigned int flags)
 	state->creator_cred = get_current_cred();
 	state->creator_pid_ns =
 		get_pid_ns(current->nsproxy->pid_ns_for_children);
+	pidfd_spawn_set_status(state, PIDFD_SPAWN_CONFIGURING);
 
 	pidfile = pidfs_alloc_future_file("[pidfd_spawn]", state,
 					  &pidfd_spawn_future_ops,
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 10/24] pidfs: publish future pidfd files
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>

Publish an attached future pidfd by transferring pidfs attributes and
forwarding process-exit wakeups to poll registrations made while the
file was taskless.

Make numeric pidfd opens and file-handle opens wait across the interval
between task visibility and producer publication. Internal PIDFD_STALE
callers remain nonblocking so task creation cannot wait on itself.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 fs/pidfs.c            | 127 +++++++++++++++++++++++++++++++++++++++++-
 include/linux/pidfs.h |   5 ++
 2 files changed, 130 insertions(+), 2 deletions(-)

diff --git a/fs/pidfs.c b/fs/pidfs.c
index 36ee4f210f73f..d62235e01b351 100644
--- a/fs/pidfs.c
+++ b/fs/pidfs.c
@@ -116,7 +116,10 @@ struct pidfs_future_file {
 	const struct pidfs_future_file_ops *ops;
 	struct pidfs_attr *attr;
 	struct pid *pid;
+	wait_queue_head_t wait_pidfd;
+	wait_queue_entry_t pid_wait;
 	u64 ino;
+	bool pid_wait_attached;
 };
 
 static struct pidfs_node *pidfs_inode_node(const struct inode *inode)
@@ -137,6 +140,12 @@ pidfs_future_file(const struct inode *inode)
 	return container_of(node, struct pidfs_future_file, node);
 }
 
+static bool pidfs_future_file_terminal(struct pidfs_future_file *future)
+{
+	return future && future->ops->is_terminal &&
+	       future->ops->is_terminal(future->data);
+}
+
 void *pidfs_future_file_data(const struct file *file,
 			     const struct pidfs_future_file_ops *ops)
 {
@@ -169,6 +178,21 @@ static struct pid *pidfs_inode_pid(const struct inode *inode)
 	return pid;
 }
 
+static int pidfs_wait_for_published_pid(struct inode *inode)
+{
+	struct pidfs_future_file *future = pidfs_future_file(inode);
+	int status = -ESRCH;
+	int ret;
+
+	if (!future)
+		return 0;
+
+	ret = wait_event_killable(future->wait_pidfd,
+				  (status = PTR_ERR_OR_ZERO(pidfs_inode_pid(inode))) !=
+				  -ESRCH || pidfs_future_file_terminal(future));
+	return ret ? ret : status;
+}
+
 #if BITS_PER_LONG == 32
 
 DEFINE_SPINLOCK(pidfs_ino_lock);
@@ -370,10 +394,22 @@ 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 pidfs_future_file *future;
 	struct task_struct *task;
 	__poll_t poll_flags = 0;
-	struct pid *pid = pidfd_pid(file);
+	struct pid *pid;
 
+	future = pidfs_future_file(file_inode(file));
+	pid = pidfd_pid(file);
+	if (IS_ERR(pid) && PTR_ERR(pid) == -ESRCH && future) {
+		/* Close the publication race after registering the future wait. */
+		poll_wait(file, &future->wait_pidfd, pts);
+		pid = pidfd_pid(file);
+	}
+
+	if (IS_ERR(pid) && PTR_ERR(pid) == -ESRCH &&
+	    pidfs_future_file_terminal(future))
+		return EPOLLERR | EPOLLHUP;
 	if (IS_ERR(pid))
 		return PTR_ERR(pid) == -ESRCH ? 0 : EPOLLHUP;
 
@@ -927,6 +963,8 @@ static void pidfs_evict_inode(struct inode *inode)
 		return;
 
 	future = container_of(node, struct pidfs_future_file, node);
+	if (future->pid_wait_attached)
+		remove_wait_queue(&future->pid->wait_pidfd, &future->pid_wait);
 	if (future->ops->release)
 		future->ops->release(future->data);
 	if (future->attr)
@@ -934,6 +972,16 @@ static void pidfs_evict_inode(struct inode *inode)
 	kfree(future);
 }
 
+static int pidfs_future_pid_wake(wait_queue_entry_t *wait,
+				 unsigned int mode, int sync, void *key)
+{
+	struct pidfs_future_file *future;
+
+	future = container_of(wait, struct pidfs_future_file, pid_wait);
+	wake_up_all(&future->wait_pidfd);
+	return 0;
+}
+
 static const struct super_operations pidfs_sops = {
 	.drop_inode	= inode_just_drop,
 	.evict_inode	= pidfs_evict_inode,
@@ -1068,12 +1116,16 @@ static int pidfs_export_permission(struct handle_to_path_ctx *ctx,
 static struct file *pidfs_export_open(const struct path *path, unsigned int oflags)
 {
 	struct file *file;
+	int ret;
 
 	/*
 	 * Clear O_LARGEFILE as open_by_handle_at() forces it and raise
 	 * O_RDWR as pidfds always are.
 	 */
 	oflags &= ~O_LARGEFILE;
+	ret = pidfs_wait_for_published_pid(d_inode(path->dentry));
+	if (ret)
+		return ERR_PTR(ret);
 	file = dentry_open(path, oflags | O_RDWR, current_cred());
 	/* do_dentry_open() strips O_EXCL, which encodes PIDFD_THREAD. */
 	if (!IS_ERR(file))
@@ -1289,6 +1341,17 @@ struct file *pidfs_alloc_file(struct pid *pid, unsigned int flags)
 	ret = path_from_stashed(&pid->stashed, pidfs_mnt, get_pid(pid), &path);
 	if (ret < 0)
 		return ERR_PTR(ret);
+	/*
+	 * A future dentry can be stashed before copy_process() makes its task
+	 * visible. Non-stale callers have already observed the task, so do not
+	 * return that inode until its producer has published the process state.
+	 * PIDFD_STALE is used by pre-task internal callers and must not wait here.
+	 */
+	if (!(flags & PIDFD_STALE)) {
+		ret = pidfs_wait_for_published_pid(d_inode(path.dentry));
+		if (ret)
+			return ERR_PTR(ret);
+	}
 
 	VFS_WARN_ON_ONCE(!pid->attr);
 
@@ -1317,7 +1380,8 @@ struct file *pidfs_alloc_file(struct pid *pid, unsigned int flags)
  * 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().
+ * finally wake waiters with pidfs_future_file_notify(). A producer that
+ * becomes terminal without publishing must also notify its waiters.
  *
  * A task associated with the future pid must not become runnable before
  * publication metadata and exit-wakeup forwarding are installed.
@@ -1378,6 +1442,7 @@ struct file *pidfs_alloc_future_file(const char *name, void *data,
 	future->data = data;
 	future->ops = ops;
 	future->ino = ino;
+	init_waitqueue_head(&future->wait_pidfd);
 	inode->i_private = &future->node;
 	return file;
 }
@@ -1422,6 +1487,60 @@ int pidfs_future_file_set_pid(struct file *file, struct pid *pid)
 	return 0;
 }
 
+/**
+ * pidfs_future_file_publish_pid - publish pidfs metadata for a future pid
+ * @file: future pidfs file
+ * @pid: pid previously associated with @file
+ *
+ * This transfers the future inode attributes to @pid and forwards pid exit
+ * wakeups. After it returns, the producer must publish the state used by
+ * @ops->get_pid() before calling pidfs_future_file_notify().
+ */
+void pidfs_future_file_publish_pid(struct file *file, struct pid *pid)
+{
+	struct pidfs_future_file *future = pidfs_future_file(file_inode(file));
+	struct pidfs_attr *unused = NULL;
+
+	if (WARN_ON_ONCE(!future || READ_ONCE(future->pid) != pid))
+		return;
+
+	scoped_guard(spinlock_irq, &pid->wait_pidfd.lock) {
+		if (WARN_ON_ONCE(pid->attr == PIDFS_PID_DEAD))
+			return;
+		if (!pid->attr) {
+			pid->attr = future->attr;
+			future->attr = NULL;
+		} else {
+			unused = future->attr;
+			future->attr = NULL;
+		}
+	}
+	if (unused)
+		kmem_cache_free(pidfs_attr_cachep, unused);
+
+	init_waitqueue_func_entry(&future->pid_wait, pidfs_future_pid_wake);
+	add_wait_queue(&pid->wait_pidfd, &future->pid_wait);
+	future->pid_wait_attached = true;
+}
+
+/**
+ * pidfs_future_file_notify - wake future-pidfd state waiters
+ * @file: future pidfs file
+ *
+ * For publication, the producer must call this only after @ops->get_pid() can
+ * return the live pid. The publication must use release ordering paired with
+ * the producer's acquire-side lookup. A taskless terminal producer may call
+ * this after making @ops->is_terminal() return true.
+ */
+void pidfs_future_file_notify(struct file *file)
+{
+	struct pidfs_future_file *future = pidfs_future_file(file_inode(file));
+
+	if (WARN_ON_ONCE(!future))
+		return;
+	wake_up_all(&future->wait_pidfd);
+}
+
 /**
  * pidfs_future_file_clear_pid - undo an unpublished future-pid association
  * @file: future pidfs file
@@ -1444,6 +1563,10 @@ void pidfs_future_file_clear_pid(struct file *file, struct pid *pid)
 	if (WARN_ON_ONCE(!future || READ_ONCE(future->pid) != pid))
 		return;
 
+	if (future->pid_wait_attached) {
+		remove_wait_queue(&pid->wait_pidfd, &future->pid_wait);
+		future->pid_wait_attached = false;
+	}
 	WARN_ON_ONCE(cmpxchg(&pid->stashed, dentry, NULL) != dentry);
 	dentry->d_fsdata = NULL;
 	WARN_ON_ONCE(pid->attr);
diff --git a/include/linux/pidfs.h b/include/linux/pidfs.h
index 828e96f769ba1..e4df7f7748487 100644
--- a/include/linux/pidfs.h
+++ b/include/linux/pidfs.h
@@ -13,11 +13,14 @@ struct pid;
  * @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.
+ * @is_terminal: Return true when a taskless producer can no longer publish a
+ *	process. Optional; a producer without terminal failures may leave it NULL.
  * @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);
+	bool (*is_terminal)(void *data);
 	void (*release)(void *data);
 };
 
@@ -29,6 +32,8 @@ 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);
 int pidfs_future_file_set_pid(struct file *file, struct pid *pid);
+void pidfs_future_file_publish_pid(struct file *file, struct pid *pid);
+void pidfs_future_file_notify(struct file *file);
 void pidfs_future_file_clear_pid(struct file *file, struct pid *pid);
 void __init pidfs_init(void);
 void pidfs_prepare_pid(struct pid *pid);
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 09/24] fork: let process builders supply preallocated pids
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>

Process builders can reserve a pidfs identity before copy_process()
makes a task visible. Add an alloc_pid() variant for a reserved pidfs
inode, and let copy_process() consume a caller-provided struct pid on
success.

Keep caller ownership on failure. The legacy NULL-pid path continues to
allocate and free its own pid. Validate the target PID namespace before
using a preallocated identity.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 include/linux/pid.h |  3 +++
 kernel/fork.c       | 16 ++++++++++++++--
 kernel/pid.c        | 22 ++++++++++++++++++++--
 3 files changed, 37 insertions(+), 4 deletions(-)

diff --git a/include/linux/pid.h b/include/linux/pid.h
index a29ffe2a5fa8e..fa8acae336f7c 100644
--- a/include/linux/pid.h
+++ b/include/linux/pid.h
@@ -142,6 +142,9 @@ extern struct pid *find_ge_pid(int nr, struct pid_namespace *);
 
 extern struct pid *alloc_pid(struct pid_namespace *ns, pid_t *set_tid,
 			     size_t set_tid_size);
+struct pid *alloc_pid_with_pidfs_ino(struct pid_namespace *ns,
+				     pid_t *set_tid, size_t set_tid_size,
+				     u64 pidfs_ino);
 extern void free_pid(struct pid *pid);
 void free_pids(struct pid **pids);
 extern void disable_pid_allocation(struct pid_namespace *ns);
diff --git a/kernel/fork.c b/kernel/fork.c
index 970810a01bbf6..d16405c037c2f 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -2006,6 +2006,9 @@ static bool need_futex_hash_allocate_default(u64 clone_flags)
  * It copies the registers, and all the appropriate
  * parts of the process environment (as per the clone
  * flags). The actual kick-off is left to the caller.
+ *
+ * Except for init_struct_pid, a caller-supplied pid reference remains owned
+ * by the caller on failure and is transferred to the new task on success.
  */
 __latent_entropy struct task_struct *copy_process(
 					struct pid *pid,
@@ -2017,6 +2020,7 @@ __latent_entropy struct task_struct *copy_process(
 	struct task_struct *p;
 	struct multiprocess_signals delayed;
 	struct file *pidfile = NULL;
+	bool allocated_pid = false;
 	const u64 clone_flags = args->flags;
 	struct nsproxy *nsp = current->nsproxy;
 
@@ -2317,13 +2321,21 @@ __latent_entropy struct task_struct *copy_process(
 
 	stackleak_task_init(p);
 
-	if (pid != &init_struct_pid) {
+	if (!pid) {
 		pid = alloc_pid(p->nsproxy->pid_ns_for_children, args->set_tid,
 				args->set_tid_size);
 		if (IS_ERR(pid)) {
 			retval = PTR_ERR(pid);
 			goto bad_fork_cleanup_thread;
 		}
+		allocated_pid = true;
+	} else if (pid != &init_struct_pid) {
+		if (args->set_tid_size ||
+		    ns_of_pid(pid) != p->nsproxy->pid_ns_for_children ||
+		    WARN_ON_ONCE(pid_has_task(pid, PIDTYPE_PID))) {
+			retval = -EINVAL;
+			goto bad_fork_cleanup_thread;
+		}
 	}
 
 	/*
@@ -2587,7 +2599,7 @@ __latent_entropy struct task_struct *copy_process(
 		put_unused_fd(pidfd);
 	}
 bad_fork_free_pid:
-	if (pid != &init_struct_pid)
+	if (allocated_pid)
 		free_pid(pid);
 bad_fork_cleanup_thread:
 	exit_thread(p);
diff --git a/kernel/pid.c b/kernel/pid.c
index f55189a3d07d4..010f80177cac8 100644
--- a/kernel/pid.c
+++ b/kernel/pid.c
@@ -156,8 +156,8 @@ void free_pids(struct pid **pids)
 			free_pid(pids[tmp]);
 }
 
-struct pid *alloc_pid(struct pid_namespace *ns, pid_t *arg_set_tid,
-		      size_t arg_set_tid_size)
+static struct pid *__alloc_pid(struct pid_namespace *ns, pid_t *arg_set_tid,
+			       size_t arg_set_tid_size, u64 pidfs_ino)
 {
 	int set_tid[MAX_PID_NS_LEVEL + 1] = {};
 	int pid_max[MAX_PID_NS_LEVEL + 1] = {};
@@ -198,6 +198,7 @@ struct pid *alloc_pid(struct pid_namespace *ns, pid_t *arg_set_tid,
 	init_waitqueue_head(&pid->wait_pidfd);
 	INIT_HLIST_HEAD(&pid->inodes);
 	pidfs_prepare_pid(pid);
+	pid->ino = pidfs_ino;
 
 	/*
 	 * 2. perm check checkpoint_restore_ns_capable()
@@ -358,6 +359,23 @@ struct pid *alloc_pid(struct pid_namespace *ns, pid_t *arg_set_tid,
 	return ERR_PTR(retval);
 }
 
+struct pid *alloc_pid(struct pid_namespace *ns, pid_t *arg_set_tid,
+		      size_t arg_set_tid_size)
+{
+	return __alloc_pid(ns, arg_set_tid, arg_set_tid_size, 0);
+}
+
+struct pid *alloc_pid_with_pidfs_ino(struct pid_namespace *ns,
+				     pid_t *arg_set_tid,
+				     size_t arg_set_tid_size,
+				     u64 pidfs_ino)
+{
+	if (!pidfs_ino)
+		return ERR_PTR(-EINVAL);
+
+	return __alloc_pid(ns, arg_set_tid, arg_set_tid_size, pidfs_ino);
+}
+
 void disable_pid_allocation(struct pid_namespace *ns)
 {
 	spin_lock(&pidmap_lock);
-- 
2.52.0


^ permalink raw reply related

* [RFC PATCH 08/24] pidfs: attach pids to future pidfd files
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>

Associate a future pidfs inode with a preallocated struct pid and reuse
the future dentry as the canonical stashed dentry. Preserve the
reserved inode identity when the pid is registered.

Keep attachment reversible until a task owns the pid so copy_process()
failure can return the future file to its taskless state. File-handle
lookup requires both the stash and an attached task before accepting an
unpublished pid, preventing failed allocations from being reopened
through stale state.

Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
 fs/pidfs.c            | 89 +++++++++++++++++++++++++++++++++++++++++--
 include/linux/pidfs.h |  2 +
 2 files changed, 87 insertions(+), 4 deletions(-)

diff --git a/fs/pidfs.c b/fs/pidfs.c
index 28464fe274c9e..36ee4f210f73f 100644
--- a/fs/pidfs.c
+++ b/fs/pidfs.c
@@ -115,6 +115,7 @@ struct pidfs_future_file {
 	void *data;
 	const struct pidfs_future_file_ops *ops;
 	struct pidfs_attr *attr;
+	struct pid *pid;
 	u64 ino;
 };
 
@@ -238,7 +239,8 @@ int pidfs_add_pid(struct pid *pid)
 {
 	int ret;
 
-	pid->ino = pidfs_alloc_ino();
+	if (!pid->ino)
+		pid->ino = pidfs_alloc_ino();
 	ret = rhashtable_insert_fast(&pidfs_ino_ht, &pid->pidfs_hash,
 				     pidfs_ino_ht_params);
 	if (unlikely(ret))
@@ -987,9 +989,18 @@ static struct pid *pidfs_ino_get_pid(u64 ino)
 	if (!pid)
 		return NULL;
 	attr = READ_ONCE(pid->attr);
-	if (IS_ERR_OR_NULL(attr))
+	if (IS_ERR(attr))
+		return NULL;
+	/*
+	 * A task can become visible before its future pidfd is published. Let
+	 * ->open() wait across that window, but never expose a dentry for a
+	 * caller-supplied pid when copy_process() has failed. That failure path
+	 * clears the stash before freeing the pid allocation.
+	 */
+	if (!attr && (!READ_ONCE(pid->stashed) ||
+		      !pid_has_task(pid, PIDTYPE_PID)))
 		return NULL;
-	if (test_bit(PIDFS_ATTR_BIT_EXIT, &attr->attr_mask))
+	if (attr && test_bit(PIDFS_ATTR_BIT_EXIT, &attr->attr_mask))
 		return NULL;
 	/* Within our pid namespace hierarchy? */
 	if (pid_vnr(pid) == 0)
@@ -1025,7 +1036,8 @@ static struct dentry *pidfs_fh_to_dentry(struct super_block *sb,
 	if (ret < 0)
 		return ERR_PTR(ret);
 
-	VFS_WARN_ON_ONCE(!pid->attr);
+	VFS_WARN_ON_ONCE(!pid->attr &&
+			 !pidfs_future_file(d_inode(path.dentry)));
 
 	mntput(path.mnt);
 	return path.dentry;
@@ -1103,6 +1115,10 @@ static int pidfs_init_inode(struct inode *inode, void *data)
 static bool pidfs_inode_data_matches(const struct inode *inode,
 				     const void *data)
 {
+	struct pidfs_future_file *future = pidfs_future_file(inode);
+
+	if (future)
+		return READ_ONCE(future->pid) == data;
 	return pidfs_inode_pid(inode) == data;
 }
 
@@ -1266,6 +1282,10 @@ struct file *pidfs_alloc_file(struct pid *pid, unsigned int flags)
 				PIDFD_AUTOKILL) != 5);
 	BUILD_BUG_ON(PIDFD_EMPTY & VALID_OPEN_FLAGS);
 
+	ret = pidfs_register_pid(pid);
+	if (ret)
+		return ERR_PTR(ret);
+
 	ret = path_from_stashed(&pid->stashed, pidfs_mnt, get_pid(pid), &path);
 	if (ret < 0)
 		return ERR_PTR(ret);
@@ -1369,6 +1389,67 @@ u64 pidfs_future_file_ino(const struct file *file)
 	return future ? future->ino : 0;
 }
 
+/**
+ * pidfs_future_file_set_pid - associate a preallocated pid with a future file
+ * @file: future pidfs file
+ * @pid: preallocated pid whose pidfs inode number matches @file
+ *
+ * This stashes the future dentry in @pid before copy_process() publishes the
+ * task. No pid reference is transferred. On a later creation failure the
+ * producer must call pidfs_future_file_clear_pid() before freeing @pid.
+ *
+ * Return: Zero on success or a negative error code.
+ */
+int pidfs_future_file_set_pid(struct file *file, struct pid *pid)
+{
+	struct inode *inode = file_inode(file);
+	struct pidfs_future_file *future = pidfs_future_file(inode);
+	struct dentry *dentry = file->f_path.dentry;
+	struct dentry *stashed;
+
+	if (!future || !future->ops->get_pid || future->pid ||
+	    pid->ino != future->ino)
+		return -EBADF;
+
+	WRITE_ONCE(future->pid, pid);
+	dentry->d_fsdata = &pid->stashed;
+	stashed = stash_dentry(&pid->stashed, dentry);
+	if (stashed != dentry) {
+		dput(stashed);
+		pidfs_future_file_clear_pid(file, pid);
+		return -EBUSY;
+	}
+	return 0;
+}
+
+/**
+ * pidfs_future_file_clear_pid - undo an unpublished future-pid association
+ * @file: future pidfs file
+ * @pid: pid previously associated with @file
+ *
+ * This may be called only after pidfs_future_file_set_pid() and before
+ * pidfs_future_file_publish_pid(). It removes the stashed dentry so the
+ * producer can free @pid after a task-creation failure.
+ *
+ * The producer must clear the association before attaching @pid to a task.
+ * File-handle lookup treats an attached pid as a publication-in-progress
+ * guarantee, so clearing the association later would violate that contract.
+ */
+void pidfs_future_file_clear_pid(struct file *file, struct pid *pid)
+{
+	struct inode *inode = file_inode(file);
+	struct pidfs_future_file *future = pidfs_future_file(inode);
+	struct dentry *dentry = file->f_path.dentry;
+
+	if (WARN_ON_ONCE(!future || READ_ONCE(future->pid) != pid))
+		return;
+
+	WARN_ON_ONCE(cmpxchg(&pid->stashed, dentry, NULL) != dentry);
+	dentry->d_fsdata = NULL;
+	WARN_ON_ONCE(pid->attr);
+	WRITE_ONCE(future->pid, NULL);
+}
+
 void __init pidfs_init(void)
 {
 	if (rhashtable_init(&pidfs_ino_ht, &pidfs_ino_ht_params))
diff --git a/include/linux/pidfs.h b/include/linux/pidfs.h
index 6b7fbc54ab388..828e96f769ba1 100644
--- a/include/linux/pidfs.h
+++ b/include/linux/pidfs.h
@@ -28,6 +28,8 @@ struct file *pidfs_alloc_future_file(const char *name, void *data,
 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);
+int pidfs_future_file_set_pid(struct file *file, struct pid *pid);
+void pidfs_future_file_clear_pid(struct file *file, struct pid *pid);
 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 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


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