Linux Security Modules development
 help / color / mirror / Atom feed
From: Li Chen <me@linux.beauty>
To: Christian Brauner <brauner@kernel.org>
Cc: "Kees Cook" <kees@kernel.org>,
	"Gabriel Krisman Bertazi" <krisman@kernel.org>,
	"Josh Triplett" <josh@joshtriplett.org>,
	"Mateusz Guzik" <mjguzik@gmail.com>,
	"Andy Lutomirski" <luto@kernel.org>,
	"John Ericson" <mail@johnericson.me>,
	"Jonathan Corbet" <corbet@lwn.net>,
	"Shuah Khan" <shuah@kernel.org>, "Arnd Bergmann" <arnd@arndb.de>,
	"Oleg Nesterov" <oleg@redhat.com>,
	"Andrew Morton" <akpm@linux-foundation.org>,
	"Paul Moore" <paul@paul-moore.com>,
	"Eric Paris" <eparis@redhat.com>,
	"Mickaël Salaün" <mic@digikod.net>,
	"Günther Noack" <gnoack@google.com>,
	"Alexander Viro" <viro@zeniv.linux.org.uk>,
	"Jan Kara" <jack@suse.cz>,
	linux-api@vger.kernel.org, linux-fsdevel@vger.kernel.org,
	linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org,
	linux-doc@vger.kernel.org, audit@vger.kernel.org,
	linux-security-module@vger.kernel.org,
	linux-arch@vger.kernel.org, linux-mm@kvack.org,
	"Li Chen" <me@linux.beauty>
Subject: [RFC PATCH 18/24] pidfd: make spawn builder execution signal-safe
Date: Thu, 16 Jul 2026 22:31:44 +0800	[thread overview]
Message-ID: <9689970b5b584db83756cb38608027876154b92a.1784204592.git.me@linux.beauty> (raw)
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


  parent reply	other threads:[~2026-07-16 14:42 UTC|newest]

Thread overview: 31+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-16 14:31 [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Li Chen
2026-07-16 14:31 ` [RFC PATCH 01/24] pidfd: add spawn builder uapi Li Chen
2026-07-16 14:31 ` [RFC PATCH 02/24] libfs: allow custom validation of stashed inode data Li Chen
2026-07-16 14:31 ` [RFC PATCH 03/24] pidfs: add taskless future pidfd inodes Li Chen
2026-07-16 14:31 ` [RFC PATCH 04/24] pidfd: create taskless spawn builders Li Chen
2026-07-16 14:31 ` [RFC PATCH 05/24] pidfd: add spawn builder path configuration Li Chen
2026-07-16 14:31 ` [RFC PATCH 06/24] exec: expose execveat internals to process builders Li Chen
2026-07-16 14:31 ` [RFC PATCH 07/24] fork: expose vfork completion helper Li Chen
2026-07-16 14:31 ` [RFC PATCH 08/24] pidfs: attach pids to future pidfd files Li Chen
2026-07-16 14:31 ` [RFC PATCH 09/24] fork: let process builders supply preallocated pids Li Chen
2026-07-16 14:31 ` [RFC PATCH 10/24] pidfs: publish future pidfd files Li Chen
2026-07-16 14:31 ` [RFC PATCH 11/24] pidfd: add spawn builder state tracking Li Chen
2026-07-16 14:31 ` [RFC PATCH 12/24] fork: let kernel callers create embryonic tasks Li Chen
2026-07-16 15:57   ` Andy Lutomirski
2026-08-19 10:19     ` Li Chen
2026-07-16 14:31 ` [RFC PATCH 13/24] fork: let new tasks start with task work Li Chen
2026-07-16 14:31 ` [RFC PATCH 14/24] pidfd: create and execute spawn builder tasks Li Chen
2026-07-16 14:31 ` [RFC PATCH 15/24] fork: keep embryonic tasks hidden until exec completes Li Chen
2026-07-16 14:31 ` [RFC PATCH 16/24] audit: add pidfd spawn child contexts Li Chen
2026-07-16 14:31 ` [RFC PATCH 17/24] pidfd: audit child spawn execution Li Chen
2026-07-16 14:31 ` Li Chen [this message]
2026-07-16 14:31 ` [RFC PATCH 19/24] file: expose spawn file-action helpers Li Chen
2026-07-16 14:31 ` [RFC PATCH 20/24] pidfd: add initial spawn file actions Li Chen
2026-07-16 14:31 ` [RFC PATCH 21/24] pidfd: consume spawn builders on the first run attempt Li Chen
2026-07-16 14:31 ` [RFC PATCH 22/24] pidfd: expose spawn builder system calls Li Chen
2026-07-16 14:31 ` [RFC PATCH 23/24] selftests/pidfd: cover pidfd spawn builders Li Chen
2026-07-16 14:31 ` [RFC PATCH 24/24] Documentation: describe " Li Chen
2026-08-04 20:25 ` [RFC PATCH 00/24] pidfd: add a minimal process spawn builder Justin Suess
2026-08-19  1:14   ` Li Chen
2026-08-25 21:27     ` Mateusz Guzik
2026-09-06 10:50       ` Li Chen

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=9689970b5b584db83756cb38608027876154b92a.1784204592.git.me@linux.beauty \
    --to=me@linux.beauty \
    --cc=akpm@linux-foundation.org \
    --cc=arnd@arndb.de \
    --cc=audit@vger.kernel.org \
    --cc=brauner@kernel.org \
    --cc=corbet@lwn.net \
    --cc=eparis@redhat.com \
    --cc=gnoack@google.com \
    --cc=jack@suse.cz \
    --cc=josh@joshtriplett.org \
    --cc=kees@kernel.org \
    --cc=krisman@kernel.org \
    --cc=linux-api@vger.kernel.org \
    --cc=linux-arch@vger.kernel.org \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-fsdevel@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=linux-security-module@vger.kernel.org \
    --cc=luto@kernel.org \
    --cc=mail@johnericson.me \
    --cc=mic@digikod.net \
    --cc=mjguzik@gmail.com \
    --cc=oleg@redhat.com \
    --cc=paul@paul-moore.com \
    --cc=shuah@kernel.org \
    --cc=viro@zeniv.linux.org.uk \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox