Unverified Commit 6092c501 authored by Christian Brauner's avatar Christian Brauner
Browse files

Merge patch series "pidfs: provide information after task has been reaped"

Christian Brauner <brauner@kernel.org> says:

Various tools need access to information about a process/task even after
it has already been reaped. For example, systemd's journal logs and uses
such information as the cgroup id and exit status to deal with processes
that have been sent via SCM_PIDFD or SCM_PEERPIDFD. By the time the
pidfd is received the process might have already been reaped.

This series aims to provide information by extending the PIDFD_GET_INFO
ioctl to retrieve the exit code and cgroup id. There might be other
stuff that we would want in the future.

Pidfd polling allows waiting on either task exit or for a task to have
been reaped. The contract for PIDFD_INFO_EXIT is simply that EPOLLHUP
must be observed before exit information can be retrieved, i.e., exit
information is only provided once the task has been reaped.

Note, that if a thread-group leader exits before other threads in the
thread-group then exit information will only be available once the
thread-group is empty. This aligns with wait() as well, where reaping of
a thread-group leader that exited before the thread-group was empty is
delayed until the thread-group is empty.

With PIDFD_INFO_EXIT autoreaping might actually become usable because it
means a parent can ignore SIGCHLD or set SA_NOCLDWAIT and simply use
pidfd polling and PIDFD_INFO_EXIT to get get status information for its
children. The kernel will autocleanup right away instead of delaying.

This includes expansive selftests including for thread-group behior and
multi-threaded exec by a non-thread-group leader thread.

* patches from https://lore.kernel.org/r/20250305-work-pidfs-kill_on_last_close-v3-0-c8c3d8361705@kernel.org:
  selftests/pidfd: add seventh PIDFD_INFO_EXIT selftest
  selftests/pidfd: add sixth PIDFD_INFO_EXIT selftest
  selftests/pidfd: add fifth PIDFD_INFO_EXIT selftest
  selftests/pidfd: add fourth PIDFD_INFO_EXIT selftest
  selftests/pidfd: add third PIDFD_INFO_EXIT selftest
  selftests/pidfd: add second PIDFD_INFO_EXIT selftest
  selftests/pidfd: add first PIDFD_INFO_EXIT selftest
  selftests/pidfd: expand common pidfd header
  pidfs/selftests: ensure correct headers for ioctl handling
  selftests/pidfd: fix header inclusion
  pidfs: allow to retrieve exit information
  pidfs: record exit code and cgroupid at exit
  pidfs: use private inode slab cache
  pidfs: move setting flags into pidfs_alloc_file()
  pidfd: rely on automatic cleanup in __pidfd_prepare()
  pidfs: switch to copy_struct_to_user()

Link: https://lore.kernel.org/r/20250305-work-pidfs-kill_on_last_close-v3-0-c8c3d8361705@kernel.org


Signed-off-by: default avatarChristian Brauner <brauner@kernel.org>
parents b1e809e7 56f235da
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -325,6 +325,7 @@ struct stashed_operations {
int path_from_stashed(struct dentry **stashed, struct vfsmount *mnt, void *data,
		      struct path *path);
void stashed_dentry_prune(struct dentry *dentry);
struct dentry *stashed_dentry_get(struct dentry **stashed);
/**
 * path_mounted - check whether path is mounted
 * @path: path to check
+2 −2
Original line number Diff line number Diff line
@@ -2113,7 +2113,7 @@ struct timespec64 simple_inode_init_ts(struct inode *inode)
}
EXPORT_SYMBOL(simple_inode_init_ts);

static inline struct dentry *get_stashed_dentry(struct dentry **stashed)
struct dentry *stashed_dentry_get(struct dentry **stashed)
{
	struct dentry *dentry;

@@ -2215,7 +2215,7 @@ int path_from_stashed(struct dentry **stashed, struct vfsmount *mnt, void *data,
	const struct stashed_operations *sops = mnt->mnt_sb->s_fs_info;

	/* See if dentry can be reused. */
	path->dentry = get_stashed_dentry(stashed);
	path->dentry = stashed_dentry_get(stashed);
	if (path->dentry) {
		sops->put_data(data);
		goto out_path;
+163 −19
Original line number Diff line number Diff line
@@ -24,6 +24,28 @@
#include "internal.h"
#include "mount.h"

static struct kmem_cache *pidfs_cachep __ro_after_init;

/*
 * Stashes information that userspace needs to access even after the
 * process has been reaped.
 */
struct pidfs_exit_info {
	__u64 cgroupid;
	__s32 exit_code;
};

struct pidfs_inode {
	struct pidfs_exit_info __pei;
	struct pidfs_exit_info *exit_info;
	struct inode vfs_inode;
};

static inline struct pidfs_inode *pidfs_i(struct inode *inode)
{
	return container_of(inode, struct pidfs_inode, vfs_inode);
}

static struct rb_root pidfs_ino_tree = RB_ROOT;

#if BITS_PER_LONG == 32
@@ -207,17 +229,28 @@ static __poll_t pidfd_poll(struct file *file, struct poll_table_struct *pts)
	return poll_flags;
}

static long pidfd_info(struct task_struct *task, unsigned int cmd, unsigned long arg)
static inline bool pid_in_current_pidns(const struct pid *pid)
{
	const struct pid_namespace *ns = task_active_pid_ns(current);

	if (ns->level <= pid->level)
		return pid->numbers[ns->level].ns == ns;

	return false;
}

static long pidfd_info(struct file *file, unsigned int cmd, unsigned long arg)
{
	struct pidfd_info __user *uinfo = (struct pidfd_info __user *)arg;
	struct inode *inode = file_inode(file);
	struct pid *pid = pidfd_pid(file);
	size_t usize = _IOC_SIZE(cmd);
	struct pidfd_info kinfo = {};
	struct pidfs_exit_info *exit_info;
	struct user_namespace *user_ns;
	struct task_struct *task;
	const struct cred *c;
	__u64 mask;
#ifdef CONFIG_CGROUPS
	struct cgroup *cgrp;
#endif

	if (!uinfo)
		return -EINVAL;
@@ -227,6 +260,37 @@ static long pidfd_info(struct task_struct *task, unsigned int cmd, unsigned long
	if (copy_from_user(&mask, &uinfo->mask, sizeof(mask)))
		return -EFAULT;

	/*
	 * Restrict information retrieval to tasks within the caller's pid
	 * namespace hierarchy.
	 */
	if (!pid_in_current_pidns(pid))
		return -ESRCH;

	if (mask & PIDFD_INFO_EXIT) {
		exit_info = READ_ONCE(pidfs_i(inode)->exit_info);
		if (exit_info) {
			kinfo.mask |= PIDFD_INFO_EXIT;
#ifdef CONFIG_CGROUPS
			kinfo.cgroupid = exit_info->cgroupid;
			kinfo.mask |= PIDFD_INFO_CGROUPID;
#endif
			kinfo.exit_code = exit_info->exit_code;
		}
	}

	task = get_pid_task(pid, PIDTYPE_PID);
	if (!task) {
		/*
		 * If the task has already been reaped, only exit
		 * information is available
		 */
		if (!(mask & PIDFD_INFO_EXIT))
			return -ESRCH;

		goto copy_out;
	}

	c = get_task_cred(task);
	if (!c)
		return -ESRCH;
@@ -246,11 +310,15 @@ static long pidfd_info(struct task_struct *task, unsigned int cmd, unsigned long
	put_cred(c);

#ifdef CONFIG_CGROUPS
	if (!kinfo.cgroupid) {
		struct cgroup *cgrp;

		rcu_read_lock();
		cgrp = task_dfl_cgroup(task);
		kinfo.cgroupid = cgroup_id(cgrp);
		kinfo.mask |= PIDFD_INFO_CGROUPID;
		rcu_read_unlock();
	}
#endif

	/*
@@ -270,16 +338,14 @@ static long pidfd_info(struct task_struct *task, unsigned int cmd, unsigned long
	if (kinfo.pid == 0 || kinfo.tgid == 0 || (kinfo.ppid == 0 && kinfo.pid != 1))
		return -ESRCH;

copy_out:
	/*
	 * If userspace and the kernel have the same struct size it can just
	 * be copied. If userspace provides an older struct, only the bits that
	 * userspace knows about will be copied. If userspace provides a new
	 * struct, only the bits that the kernel knows about will be copied.
	 */
	if (copy_to_user(uinfo, &kinfo, min(usize, sizeof(kinfo))))
		return -EFAULT;

	return 0;
	return copy_struct_to_user(uinfo, usize, &kinfo, sizeof(kinfo), NULL);
}

static bool pidfs_ioctl_valid(unsigned int cmd)
@@ -307,7 +373,6 @@ 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 pid *pid = pidfd_pid(file);
	struct ns_common *ns_common = NULL;
	struct pid_namespace *pid_ns;

@@ -322,13 +387,13 @@ static long pidfd_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
		return put_user(file_inode(file)->i_generation, argp);
	}

	task = get_pid_task(pid, PIDTYPE_PID);
	if (!task)
		return -ESRCH;

	/* Extensible IOCTL that does not open namespace FDs, take a shortcut */
	if (_IOC_NR(cmd) == _IOC_NR(PIDFD_GET_INFO))
		return pidfd_info(task, cmd, arg);
		return pidfd_info(file, cmd, arg);

	task = get_pid_task(pidfd_pid(file), PIDTYPE_PID);
	if (!task)
		return -ESRCH;

	if (arg)
		return -EINVAL;
@@ -440,6 +505,49 @@ struct pid *pidfd_pid(const struct file *file)
	return file_inode(file)->i_private;
}

/*
 * We're called from release_task(). We know there's at least one
 * reference to struct pid being held that won't be released until the
 * task has been reaped which cannot happen until we're out of
 * release_task().
 *
 * If this struct pid is referred to by a pidfd then
 * stashed_dentry_get() will return the dentry and inode for that struct
 * pid. Since we've taken a reference on it there's now an additional
 * reference from the exit path on it. Which is fine. We're going to put
 * it again in a second and we know that the pid is kept alive anyway.
 *
 * Worst case is that we've filled in the info and immediately free the
 * dentry and inode afterwards since the pidfd has been closed. Since
 * pidfs_exit() currently is placed after exit_task_work() we know that
 * it cannot be us aka the exiting task holding a pidfd to ourselves.
 */
void pidfs_exit(struct task_struct *tsk)
{
	struct dentry *dentry;

	might_sleep();

	dentry = stashed_dentry_get(&task_pid(tsk)->stashed);
	if (dentry) {
		struct inode *inode = d_inode(dentry);
		struct pidfs_exit_info *exit_info = &pidfs_i(inode)->__pei;
#ifdef CONFIG_CGROUPS
		struct cgroup *cgrp;

		rcu_read_lock();
		cgrp = task_dfl_cgroup(tsk);
		exit_info->cgroupid = cgroup_id(cgrp);
		rcu_read_unlock();
#endif
		exit_info->exit_code = tsk->exit_code;

		/* Ensure that PIDFD_GET_INFO sees either all or nothing. */
		smp_store_release(&pidfs_i(inode)->exit_info, &pidfs_i(inode)->__pei);
		dput(dentry);
	}
}

static struct vfsmount *pidfs_mnt __ro_after_init;

/*
@@ -495,9 +603,30 @@ static void pidfs_evict_inode(struct inode *inode)
	put_pid(pid);
}

static struct inode *pidfs_alloc_inode(struct super_block *sb)
{
	struct pidfs_inode *pi;

	pi = alloc_inode_sb(sb, pidfs_cachep, GFP_KERNEL);
	if (!pi)
		return NULL;

	memset(&pi->__pei, 0, sizeof(pi->__pei));
	pi->exit_info = NULL;

	return &pi->vfs_inode;
}

static void pidfs_free_inode(struct inode *inode)
{
	kmem_cache_free(pidfs_cachep, pidfs_i(inode));
}

static const struct super_operations pidfs_sops = {
	.alloc_inode	= pidfs_alloc_inode,
	.drop_inode	= generic_delete_inode,
	.evict_inode	= pidfs_evict_inode,
	.free_inode	= pidfs_free_inode,
	.statfs		= simple_statfs,
};

@@ -699,12 +828,27 @@ struct file *pidfs_alloc_file(struct pid *pid, unsigned int flags)
		return ERR_PTR(ret);

	pidfd_file = dentry_open(&path, flags, current_cred());
	/* Raise PIDFD_THREAD explicitly as do_dentry_open() strips it. */
	if (!IS_ERR(pidfd_file))
		pidfd_file->f_flags |= (flags & PIDFD_THREAD);

	path_put(&path);
	return pidfd_file;
}

static void pidfs_inode_init_once(void *data)
{
	struct pidfs_inode *pi = data;

	inode_init_once(&pi->vfs_inode);
}

void __init pidfs_init(void)
{
	pidfs_cachep = kmem_cache_create("pidfs_cache", sizeof(struct pidfs_inode), 0,
					 (SLAB_HWCACHE_ALIGN | SLAB_RECLAIM_ACCOUNT |
					  SLAB_ACCOUNT | SLAB_PANIC),
					 pidfs_inode_init_once);
	pidfs_mnt = kern_mount(&pidfs_type);
	if (IS_ERR(pidfs_mnt))
		panic("Failed to mount pidfs pseudo filesystem");
+1 −0
Original line number Diff line number Diff line
@@ -6,6 +6,7 @@ struct file *pidfs_alloc_file(struct pid *pid, unsigned int flags);
void __init pidfs_init(void);
void pidfs_add_pid(struct pid *pid);
void pidfs_remove_pid(struct pid *pid);
void pidfs_exit(struct task_struct *tsk);
extern const struct dentry_operations pidfs_dentry_operations;

#endif /* _LINUX_PID_FS_H */
+2 −1
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@
#define PIDFD_INFO_PID			(1UL << 0) /* Always returned, even if not requested */
#define PIDFD_INFO_CREDS		(1UL << 1) /* Always returned, even if not requested */
#define PIDFD_INFO_CGROUPID		(1UL << 2) /* Always returned if available, even if not requested */
#define PIDFD_INFO_EXIT			(1UL << 3) /* Only returned if requested. */

#define PIDFD_INFO_SIZE_VER0		64 /* sizeof first published struct */

@@ -86,7 +87,7 @@ struct pidfd_info {
	__u32 sgid;
	__u32 fsuid;
	__u32 fsgid;
	__u32 spare0[1];
	__s32 exit_code;
};

#define PIDFS_IOCTL_MAGIC 0xFF
Loading