[RFC PATCH 15/24] fork: keep embryonic tasks hidden until exec completes
From: Li Chen
Date: Thu Jul 16 2026 - 10:57:10 EST
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