[RFC PATCH 03/24] pidfs: add taskless future pidfd inodes
From: Li Chen
Date: Thu Jul 16 2026 - 11:56:17 EST
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