* [RFC PATCH] fs: allow opening overlayfs/erofs layers through O_ALT
From: Miklos Szeredi @ 2026-07-15 10:11 UTC (permalink / raw)
To: Giuseppe Scrivano
Cc: linux-fsdevel, linux-unionfs, linux-api, linux-erofs,
Amir Goldstein, Gao Xiang
This is a prototype patch. Needs to be split up!
1) O_ALT / OPEN_TREE_ALT
These open an alternative namespace rooted at dfd - instead of resolving
the path in the real filesystem, it resolves it in a virtual tree that
exposes metadata about that file.
This is intended to provide an alternative to adding new ioctls:
- provides structured namespace
- allows accessing data through fs ops
Could call this O_META, but O_ALT seems more generic and there could be
uses beyond metadata (e.g. seekable, in-place decompression).
This has been discussed previously:
https://lore.kernel.org/all/CAHk-=wjzLmMRf=QG-n+1HnxWCx4KTQn9+OhVvUSJ=ZCQd6Y1WA@mail.gmail.com/
2/a) s_op.get_options(), sb_add_option()
.get_options() is a generic version of .show_options().
After converting everyting to seq_show_option() the rest is trivial:
- change first arg to "struct sb_opt_ctx *ctx"
- replace "seq_show_option(m" with "sb_add_option(ctx"
2/b) sb_add_option_path()
The last arg is "struct path *". This is for options that designate a
path (e.g. stacked fs layers).
Will allow opening the path through O_ALT. Ignored when retrieving the
string value of the option.
3) MNT_CLONABLE
Allow an internal mount to be cloned. Nsfs and pidfs are already special
cased, move these over to this flag.
Also allow overlayfs layers to be cloned.
4) mnt.mnt_devname -> sb.s_devname
This is historical thing, mnt_devname was just copied when cloning the
mount, sharing the same value for all mounts belonging to a super block.
5) mount options in metafs
Support retrieving mount options through O_ALT opens:
mount/options/OPT/N: Nth instance of OPT
This will be a symlink pointint to the option value. If the option refers
to a path (as per 2/b), following the symlink jumps to the given path.
At this point only overlayfs and erofs are converted.
Signed-off-by: Miklos Szeredi <mszeredi@redhat.com>
---
Documentation/filesystems/vfs.rst | 2 +-
fs/Makefile | 2 +
fs/erofs/super.c | 29 +-
fs/fcntl.c | 2 +-
fs/internal.h | 1 +
fs/metafs/Makefile | 6 +
fs/metafs/metafs.c | 572 ++++++++++++++++++++++++++++++
fs/metafs/metafs.h | 15 +
fs/mount.h | 1 -
fs/namei.c | 16 +
fs/namespace.c | 84 +++--
fs/nsfs.c | 1 +
fs/open.c | 5 +-
fs/overlayfs/params.c | 43 ++-
fs/overlayfs/params.h | 2 +-
fs/overlayfs/super.c | 5 +-
fs/pidfs.c | 4 +-
fs/proc_namespace.c | 50 ++-
fs/super.c | 12 +-
include/linux/fcntl.h | 2 +-
include/linux/fs/super.h | 10 +
include/linux/fs/super_types.h | 4 +
include/linux/mount.h | 3 +-
include/linux/namei.h | 3 +-
include/linux/pidfs.h | 1 -
include/uapi/asm-generic/fcntl.h | 4 +
include/uapi/linux/mount.h | 1 +
scripts/gdb/linux/proc.py | 5 +-
28 files changed, 774 insertions(+), 111 deletions(-)
create mode 100644 fs/metafs/Makefile
create mode 100644 fs/metafs/metafs.c
create mode 100644 fs/metafs/metafs.h
diff --git a/Documentation/filesystems/vfs.rst b/Documentation/filesystems/vfs.rst
index 7c753148af88..3dc431b6cdbb 100644
--- a/Documentation/filesystems/vfs.rst
+++ b/Documentation/filesystems/vfs.rst
@@ -314,7 +314,7 @@ or bottom half).
``show_devname``
Optional. Called by the VFS to show device name for
/proc/<pid>/{mounts,mountinfo,mountstats}. If not provided then
- '(struct mount).mnt_devname' will be used.
+ '(struct super_block).s_devname' will be used.
``show_path``
Optional. Called by the VFS (for /proc/<pid>/mountinfo) to show
diff --git a/fs/Makefile b/fs/Makefile
index 89a8a9d207d1..87a4c23d5a08 100644
--- a/fs/Makefile
+++ b/fs/Makefile
@@ -58,6 +58,8 @@ obj-$(CONFIG_CONFIGFS_FS) += configfs/
obj-y += devpts/
obj-$(CONFIG_DLM) += dlm/
+
+obj-y += metafs/
# Do not add any filesystems before this line
obj-$(CONFIG_NETFS_SUPPORT) += netfs/
diff --git a/fs/erofs/super.c b/fs/erofs/super.c
index 86fa5c6a0c70..257517c28e1d 100644
--- a/fs/erofs/super.c
+++ b/fs/erofs/super.c
@@ -1028,29 +1028,34 @@ static int erofs_statfs(struct dentry *dentry, struct kstatfs *buf)
return 0;
}
-static int erofs_show_options(struct seq_file *seq, struct dentry *root)
+static int erofs_get_options(struct sb_opt_ctx *ctx, struct dentry *root)
{
struct erofs_sb_info *sbi = EROFS_SB(root->d_sb);
struct erofs_mount_opts *opt = &sbi->opt;
+ if (sbi->dif0.file)
+ sb_add_option_path(ctx, "source", NULL, &sbi->dif0.file->f_path);
+
if (IS_ENABLED(CONFIG_EROFS_FS_XATTR))
- seq_puts(seq, test_opt(opt, XATTR_USER) ?
- ",user_xattr" : ",nouser_xattr");
+ sb_add_option(ctx, test_opt(opt, XATTR_USER) ?
+ "user_xattr" : "nouser_xattr", NULL);
if (IS_ENABLED(CONFIG_EROFS_FS_POSIX_ACL))
- seq_puts(seq, test_opt(opt, POSIX_ACL) ? ",acl" : ",noacl");
+ sb_add_option(ctx, test_opt(opt, POSIX_ACL) ? "acl" : "noacl", NULL);
if (IS_ENABLED(CONFIG_EROFS_FS_ZIP))
- seq_printf(seq, ",cache_strategy=%s",
+ sb_add_option(ctx, "cache_strategy",
erofs_param_cache_strategy[opt->cache_strategy].name);
if (test_opt(opt, DAX_ALWAYS))
- seq_puts(seq, ",dax=always");
+ sb_add_option(ctx, "dax", "always");
if (test_opt(opt, DAX_NEVER))
- seq_puts(seq, ",dax=never");
+ sb_add_option(ctx, "dax", "never");
if (erofs_is_fileio_mode(sbi) && test_opt(opt, DIRECT_IO))
- seq_puts(seq, ",directio");
- if (sbi->dif0.fsoff)
- seq_printf(seq, ",fsoffset=%llu", sbi->dif0.fsoff);
+ sb_add_option(ctx, "directio", NULL);
+ if (sbi->dif0.fsoff) {
+ char *val __free(kfree) = kasprintf(GFP_KERNEL, "%llu", sbi->dif0.fsoff);
+ sb_add_option(ctx, "fsoffset", val);
+ }
if (test_opt(opt, INODE_SHARE))
- seq_puts(seq, ",inode_share");
+ sb_add_option(ctx, "inode_share", NULL);
return 0;
}
@@ -1069,7 +1074,7 @@ const struct super_operations erofs_sops = {
.free_inode = erofs_free_inode,
.evict_inode = erofs_evict_inode,
.statfs = erofs_statfs,
- .show_options = erofs_show_options,
+ .get_options = erofs_get_options,
};
module_init(erofs_module_init);
diff --git a/fs/fcntl.c b/fs/fcntl.c
index c158f082f1da..01b765cfac84 100644
--- a/fs/fcntl.c
+++ b/fs/fcntl.c
@@ -1169,7 +1169,7 @@ static int __init fcntl_init(void)
* Exceptions: O_NONBLOCK is a two bit define on parisc; O_NDELAY
* is defined as O_NONBLOCK on some platforms and not on others.
*/
- BUILD_BUG_ON(22 - 1 /* for O_RDONLY being 0 */ !=
+ BUILD_BUG_ON(23 - 1 /* for O_RDONLY being 0 */ !=
HWEIGHT32(
(VALID_OPEN_FLAGS & ~(O_NONBLOCK | O_NDELAY)) |
__FMODE_EXEC | __O_REGULAR));
diff --git a/fs/internal.h b/fs/internal.h
index 355d93f92208..e73a5c1d5b4e 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -95,6 +95,7 @@ int path_umount(const struct path *path, int flags);
int path_pivot_root(struct path *new, struct path *old);
int show_path(struct seq_file *m, struct dentry *root);
+int mnt_show_options(struct seq_file *seq, struct vfsmount *mnt);
/*
* fs_struct.c
diff --git a/fs/metafs/Makefile b/fs/metafs/Makefile
new file mode 100644
index 000000000000..f855b48d9c2b
--- /dev/null
+++ b/fs/metafs/Makefile
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Makefile for meta filesystem
+#
+
+obj-y := metafs.o
diff --git a/fs/metafs/metafs.c b/fs/metafs/metafs.c
new file mode 100644
index 000000000000..15ec0b6064da
--- /dev/null
+++ b/fs/metafs/metafs.c
@@ -0,0 +1,572 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * metafs - virtual filesystem for metadata access
+ *
+ * This provides a way to access metadata about files and filesystems
+ * through O_ALT file descriptors.
+ */
+
+#include <linux/fs.h>
+#include <linux/init.h>
+#include <linux/kernel.h>
+#include <linux/magic.h>
+#include <linux/mount.h>
+#include <linux/namei.h>
+#include <linux/slab.h>
+#include <linux/dcache.h>
+#include <linux/pseudo_fs.h>
+#include <linux/fs/super.h>
+#include "metafs.h"
+
+/*
+ * metafs superblock and mount
+ */
+static struct vfsmount *metafs_mnt __ro_after_init;
+
+static const struct super_operations metafs_ops = {
+ .statfs = simple_statfs,
+ .drop_inode = inode_just_drop,
+};
+
+static int metafs_init_fs_context(struct fs_context *fc)
+{
+ struct pseudo_fs_context *ctx = init_pseudo(fc, 0x4d455441); /* 'META' */
+ if (!ctx)
+ return -ENOMEM;
+ ctx->ops = &metafs_ops;
+ return 0;
+}
+
+static struct file_system_type metafs_type = {
+ .name = "metafs",
+ .init_fs_context = metafs_init_fs_context,
+ .kill_sb = kill_anon_super,
+};
+
+/*
+ * Mount option collection structures
+ */
+enum metafs_dentry_type {
+ METAFS_ROOT,
+ METAFS_OPTION_VALUES,
+ METAFS_SYMLINK,
+};
+
+struct metafs_option_entry {
+ char *value;
+ struct path path;
+};
+
+struct metafs_dentry_info {
+ enum metafs_dentry_type type;
+ union {
+ struct {
+ struct file *base;
+ } root;
+ struct {
+ struct metafs_option_entry *entries;
+ int count;
+ } opt_values;
+ struct metafs_option_entry symlink;
+ };
+};
+
+struct metafs_collect_ctx {
+ struct sb_opt_ctx ctx;
+ const char *target_name;
+ struct super_block *sb;
+ struct metafs_option_entry *entries;
+ int count;
+ int size;
+ int err;
+ bool is_source;
+};
+
+/*
+ * Collect specific mount option values from the base filesystem
+ */
+static void metafs_collect_option(struct sb_opt_ctx *ctx, const char *name,
+ const char *value, const struct path *path)
+{
+ struct metafs_collect_ctx *collect_ctx = ctx->data;
+ struct metafs_option_entry *entry;
+
+ if (strcmp(name, collect_ctx->target_name) != 0)
+ return;
+
+ if (!value && collect_ctx->is_source)
+ value = collect_ctx->sb->s_devname;
+
+ if (collect_ctx->count >= collect_ctx->size) {
+ int new_size = collect_ctx->size ? collect_ctx->size * 2 : 4;
+ struct metafs_option_entry *new_entries;
+
+ new_entries = krealloc_array(collect_ctx->entries, new_size, sizeof(*new_entries),
+ GFP_KERNEL);
+ if (!new_entries) {
+ collect_ctx->err = -ENOMEM;
+ return;
+ }
+ collect_ctx->entries = new_entries;
+ collect_ctx->size = new_size;
+ }
+
+ entry = &collect_ctx->entries[collect_ctx->count];
+ entry->value = value ? kstrdup(value, GFP_KERNEL) : NULL;
+ if (!entry->value && value) {
+ collect_ctx->err = -ENOMEM;
+ return;
+ }
+ if (path) {
+ entry->path = *path;
+ path_get(&entry->path);
+ } else {
+ memset(&entry->path, 0, sizeof(entry->path));
+ }
+ collect_ctx->count++;
+}
+
+static void metafs_free_entries(struct metafs_option_entry *entries, int count)
+{
+ int i;
+
+ for (i = 0; i < count; i++) {
+ kfree(entries[i].value);
+ if (entries[i].path.mnt)
+ path_put(&entries[i].path);
+ }
+ kfree(entries);
+}
+
+static int metafs_get_option_values(struct file *base, const char *option_name,
+ struct metafs_option_entry **entries_out,
+ int *count_out)
+{
+ const struct path *path = &base->f_path;
+ struct super_block *sb = path->mnt->mnt_sb;
+ struct metafs_collect_ctx collect_ctx = {
+ .ctx.data = &collect_ctx,
+ .ctx.actor = metafs_collect_option,
+ .target_name = option_name,
+ .sb = sb,
+ .is_source = !strcmp(option_name, "source"),
+ };
+ int ret;
+
+ if (sb->s_op->get_options) {
+ ret = sb->s_op->get_options(&collect_ctx.ctx, path->mnt->mnt_root);
+ if (ret || collect_ctx.err) {
+ metafs_free_entries(collect_ctx.entries, collect_ctx.count);
+ return ret ?: collect_ctx.err;
+ }
+ }
+
+ /* special case: add "source" option if filesystem didn't add one*/
+ if (!collect_ctx.count && collect_ctx.is_source) {
+ struct metafs_option_entry *ent __free(kfree) = kzalloc_obj(*ent);
+
+ if (!ent)
+ return -ENOMEM;
+
+ ent->value = kstrdup(sb->s_devname, GFP_KERNEL);
+ if (!ent->value)
+ return -ENOMEM;
+
+ collect_ctx.entries = no_free_ptr(ent);
+ collect_ctx.count = 1;
+ }
+
+ *entries_out = collect_ctx.entries;
+ *count_out = collect_ctx.count;
+ return 0;
+}
+
+/*
+ * metafs symlink operations
+ */
+static const char *metafs_get_link(struct dentry *dentry, struct inode *inode,
+ struct delayed_call *done)
+{
+ struct metafs_dentry_info *info;
+
+ if (!dentry)
+ return ERR_PTR(-ECHILD);
+
+ info = dentry->d_fsdata;
+ if (info->symlink.path.mnt) {
+ int error;
+
+ path_get(&info->symlink.path);
+ error = nd_jump_link(&info->symlink.path);
+ return ERR_PTR(error);
+ }
+
+ return ERR_PTR(-ELOOP);
+}
+
+static int metafs_readlink(struct dentry *dentry, char __user *buffer,
+ int buflen)
+{
+ struct metafs_dentry_info *info = dentry->d_fsdata;
+ const char *value = info->symlink.value;
+
+ if (!value)
+ value = "";
+
+ return readlink_copy(buffer, buflen, value, strlen(value));
+}
+
+static const struct inode_operations metafs_symlink_iops = {
+ .get_link = metafs_get_link,
+ .readlink = metafs_readlink,
+};
+
+/*
+ * Dentry operations - cleanup
+ */
+static void metafs_d_release(struct dentry *dentry)
+{
+ struct metafs_dentry_info *info = dentry->d_fsdata;
+
+ if (!info)
+ return;
+
+ switch (info->type) {
+ case METAFS_ROOT:
+ fput(info->root.base);
+ break;
+ case METAFS_OPTION_VALUES:
+ metafs_free_entries(info->opt_values.entries,
+ info->opt_values.count);
+ break;
+ case METAFS_SYMLINK:
+ kfree(info->symlink.value);
+ if (info->symlink.path.mnt)
+ path_put(&info->symlink.path);
+ break;
+ default:
+ WARN_ON(1);
+ }
+ kfree(info);
+}
+
+static const struct dentry_operations metafs_dentry_ops = {
+ .d_release = metafs_d_release,
+};
+
+/*
+ * metafs directory operations - forward declarations
+ */
+static struct file *metafs_get_base_file(struct dentry *dentry);
+static struct dentry *metafs_root_lookup(struct inode *dir, struct dentry *dentry,
+ unsigned int flags);
+static struct dentry *metafs_mount_lookup(struct inode *dir, struct dentry *dentry,
+ unsigned int flags);
+static struct dentry *metafs_options_lookup(struct inode *dir, struct dentry *dentry,
+ unsigned int flags);
+static struct dentry *metafs_option_values_lookup(struct inode *dir, struct dentry *dentry,
+ unsigned int flags);
+
+static const struct inode_operations metafs_root_dir_iops = {
+ .lookup = metafs_root_lookup,
+};
+
+static const struct inode_operations metafs_mount_dir_iops = {
+ .lookup = metafs_mount_lookup,
+};
+
+static const struct inode_operations metafs_options_dir_iops = {
+ .lookup = metafs_options_lookup,
+};
+
+static const struct inode_operations metafs_option_values_dir_iops = {
+ .lookup = metafs_option_values_lookup,
+};
+
+/*
+ * Create a new inode in metafs
+ */
+static struct inode *metafs_get_inode(struct super_block *sb, umode_t mode,
+ const struct inode_operations *iops)
+{
+ struct inode *inode = new_inode(sb);
+
+ if (!inode)
+ return NULL;
+
+ inode->i_ino = get_next_ino();
+ inode->i_mode = mode;
+ simple_inode_init_ts(inode);
+
+ if (S_ISDIR(mode)) {
+ inode->i_op = iops;
+ inc_nlink(inode);
+ } else if (S_ISLNK(mode)) {
+ inode->i_op = &metafs_symlink_iops;
+ }
+
+ return inode;
+}
+
+/*
+ * Lookup in root directory (/)
+ * Only "mount" is valid here
+ */
+static struct dentry *metafs_root_lookup(struct inode *dir, struct dentry *dentry,
+ unsigned int flags)
+{
+ const char *name = dentry->d_name.name;
+ struct inode *inode;
+
+ if (strcmp(name, "mount") != 0)
+ return ERR_PTR(-ENOENT);
+
+ inode = metafs_get_inode(dir->i_sb, S_IFDIR | 0555, &metafs_mount_dir_iops);
+ if (!inode)
+ return ERR_PTR(-ENOMEM);
+
+ d_add(dentry, inode);
+ d_mark_dontcache(inode);
+ return NULL;
+}
+
+/*
+ * Lookup in /mount directory
+ * Only "options" is valid here
+ */
+static struct dentry *metafs_mount_lookup(struct inode *dir, struct dentry *dentry,
+ unsigned int flags)
+{
+ const char *name = dentry->d_name.name;
+ struct inode *inode;
+
+ if (strcmp(name, "options") != 0)
+ return ERR_PTR(-ENOENT);
+
+ inode = metafs_get_inode(dir->i_sb, S_IFDIR | 0555, &metafs_options_dir_iops);
+ if (!inode)
+ return ERR_PTR(-ENOMEM);
+
+ d_add(dentry, inode);
+ d_mark_dontcache(inode);
+ return NULL;
+}
+
+/*
+ * Lookup in /mount/options directory
+ * Collect values for the requested option name
+ */
+static struct dentry *metafs_options_lookup(struct inode *dir, struct dentry *dentry,
+ unsigned int flags)
+{
+ const char *target_name = dentry->d_name.name;
+ struct file *base;
+ struct metafs_dentry_info *info;
+ struct metafs_option_entry *entries = NULL;
+ int count = 0;
+ struct inode *inode;
+ int ret;
+
+ base = metafs_get_base_file(dentry->d_parent);
+ if (!base)
+ return ERR_PTR(-ENOENT);
+
+ ret = metafs_get_option_values(base, target_name, &entries, &count);
+ if (ret)
+ return ERR_PTR(ret);
+
+ if (count == 0) {
+ kfree(entries);
+ return ERR_PTR(-ENOENT);
+ }
+
+ info = kzalloc(sizeof(*info), GFP_KERNEL);
+ if (!info) {
+ metafs_free_entries(entries, count);
+ return ERR_PTR(-ENOMEM);
+ }
+
+ info->type = METAFS_OPTION_VALUES;
+ info->opt_values.entries = entries;
+ info->opt_values.count = count;
+
+ inode = metafs_get_inode(dir->i_sb, S_IFDIR | 0555, &metafs_option_values_dir_iops);
+ if (!inode) {
+ metafs_free_entries(entries, count);
+ kfree(info);
+ return ERR_PTR(-ENOMEM);
+ }
+
+ dentry->d_fsdata = info;
+ d_add(dentry, inode);
+ d_mark_dontcache(inode);
+ return NULL;
+}
+
+/*
+ * Lookup in option-specific directory (e.g., /mount/options/lowerdir/)
+ * Return numbered entries (0, 1, 2, ...) as symlinks
+ */
+static struct dentry *metafs_option_values_lookup(struct inode *dir,
+ struct dentry *dentry,
+ unsigned int flags)
+{
+ const char *target_name = dentry->d_name.name;
+ struct metafs_dentry_info *parent_info, *info;
+ struct metafs_option_entry *entry;
+ struct inode *inode;
+ unsigned long index;
+ int err;
+
+ parent_info = dentry->d_parent->d_fsdata;
+ if (WARN_ON(!parent_info || parent_info->type != METAFS_OPTION_VALUES))
+ return ERR_PTR(-ENOENT);
+
+ if (target_name[0] == '0' && target_name[1])
+ return ERR_PTR(-ENOENT);
+
+ err = kstrtoul(target_name, 10, &index);
+ if (err || index >= parent_info->opt_values.count)
+ return ERR_PTR(-ENOENT);
+
+ entry = &parent_info->opt_values.entries[index];
+
+ info = kzalloc(sizeof(*info), GFP_KERNEL);
+ if (!info)
+ return ERR_PTR(-ENOMEM);
+
+ info->type = METAFS_SYMLINK;
+ if (entry->value) {
+ info->symlink.value = kstrdup(entry->value, GFP_KERNEL);
+ if (!info->symlink.value) {
+ kfree(info);
+ return ERR_PTR(-ENOMEM);
+ }
+ }
+ if (entry->path.mnt) {
+ info->symlink.path = entry->path;
+ path_get(&info->symlink.path);
+ }
+
+ inode = metafs_get_inode(dir->i_sb, S_IFLNK | 0777, NULL);
+ if (!inode) {
+ kfree(info->symlink.value);
+ if (info->symlink.path.mnt)
+ path_put(&info->symlink.path);
+ kfree(info);
+ return ERR_PTR(-ENOMEM);
+ }
+
+ dentry->d_fsdata = info;
+ d_add(dentry, inode);
+ d_mark_dontcache(inode);
+ return NULL;
+}
+
+/*
+ * Helper to get base file from any metafs dentry
+ */
+static struct file *metafs_get_base_file(struct dentry *dentry)
+{
+ while (dentry) {
+ struct metafs_dentry_info *info = dentry->d_fsdata;
+
+ if (info && info->type == METAFS_ROOT)
+ return info->root.base;
+ if (dentry == dentry->d_parent)
+ break;
+ dentry = dentry->d_parent;
+ }
+ return NULL;
+}
+
+/*
+ * Create the root directory structure for a metadata open
+ */
+static struct dentry *metafs_create_root(struct file *base)
+{
+ struct inode *inode;
+ struct dentry *root;
+ struct super_block *sb = metafs_mnt->mnt_sb;
+ struct metafs_dentry_info *info;
+
+ info = kzalloc(sizeof(*info), GFP_KERNEL);
+ if (!info)
+ return ERR_PTR(-ENOMEM);
+
+ info->type = METAFS_ROOT;
+ info->root.base = get_file(base);
+
+ inode = metafs_get_inode(sb, S_IFDIR | 0555, &metafs_root_dir_iops);
+ if (!inode) {
+ fput(info->root.base);
+ kfree(info);
+ return ERR_PTR(-ENOMEM);
+ }
+
+ root = d_alloc_anon(sb);
+ if (!root) {
+ iput(inode);
+ fput(info->root.base);
+ kfree(info);
+ return ERR_PTR(-ENOMEM);
+ }
+
+ /* Store the info in d_fsdata before instantiating */
+ root->d_fsdata = info;
+ d_instantiate(root, inode);
+ d_mark_dontcache(inode);
+
+ return root;
+}
+
+/*
+ * Get metadata root path for a base file.
+ *
+ * This is called from path_init() when O_ALT is used.
+ * It returns a path in the metafs that serves as the root
+ * for the metadata lookup.
+ */
+int metafs_get_root(struct file *base, struct path *path)
+{
+ struct dentry *root;
+
+ if (!base)
+ return -EBADF;
+
+ /* Create the metadata root for this base file */
+ root = metafs_create_root(base);
+ if (IS_ERR(root))
+ return PTR_ERR(root);
+
+ /*
+ * Return the path to the metadata root.
+ * Normal VFS lookup will take over from here.
+ */
+ path->mnt = mntget(metafs_mnt);
+ path->dentry = root;
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(metafs_get_root);
+
+static int __init metafs_init(void)
+{
+ int err;
+
+ err = register_filesystem(&metafs_type);
+ if (err)
+ return err;
+
+ metafs_mnt = kern_mount(&metafs_type);
+ if (IS_ERR(metafs_mnt)) {
+ err = PTR_ERR(metafs_mnt);
+ unregister_filesystem(&metafs_type);
+ return err;
+ }
+ set_default_d_op(metafs_mnt->mnt_sb, &metafs_dentry_ops);
+
+ return 0;
+}
+
+fs_initcall(metafs_init);
diff --git a/fs/metafs/metafs.h b/fs/metafs/metafs.h
new file mode 100644
index 000000000000..d257714260a7
--- /dev/null
+++ b/fs/metafs/metafs.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _LINUX_METAFS_H
+#define _LINUX_METAFS_H
+
+struct path;
+struct file;
+
+/*
+ * Get metadata root path for a base file.
+ * Called from path_init() when O_ALT flag is used.
+ * Returns 0 on success with path filled in, -errno on error.
+ */
+int metafs_get_root(struct file *base, struct path *path);
+
+#endif /* _LINUX_METAFS_H */
diff --git a/fs/mount.h b/fs/mount.h
index 94fcc306d21e..69a953bc112b 100644
--- a/fs/mount.h
+++ b/fs/mount.h
@@ -65,7 +65,6 @@ struct mount {
struct mount * __aligned(1) *mnt_pprev_for_sb;
/* except that LSB of pprev is stolen */
#define WRITE_HOLD 1 /* ... for use by mnt_hold_writers() */
- const char *mnt_devname; /* Name of device e.g. /dev/dsk/hda1 */
struct list_head mnt_list;
struct list_head mnt_expire; /* link in fs-specific expiry list */
struct list_head mnt_share; /* circular list of shared mounts */
diff --git a/fs/namei.c b/fs/namei.c
index 19ce43c9a6e6..437a14c432e6 100644
--- a/fs/namei.c
+++ b/fs/namei.c
@@ -45,6 +45,7 @@
#include "internal.h"
#include "mount.h"
+#include "metafs/metafs.h"
/* [Feb-1997 T. Schoebel-Theuer]
* Fundamental changes in the pathname lookup mechanisms (namei)
@@ -2753,6 +2754,21 @@ static const char *path_init(struct nameidata *nd, unsigned flags)
return ERR_PTR(-ENOENT);
}
+ if (unlikely(flags & LOOKUP_ALT)) {
+ if (flags & LOOKUP_RCU)
+ return ERR_PTR(-ECHILD);
+
+ /* Handle O_ALT: jump to metafs root */
+ error = metafs_get_root(fd_file(f), &nd->path);
+ if (error)
+ return ERR_PTR(error);
+
+ nd->inode = nd->path.dentry->d_inode;
+ nd->state |= ND_JUMPED;
+
+ return s;
+ }
+
dentry = fd_file(f)->f_path.dentry;
if (*s && unlikely(!d_can_lookup(dentry)))
diff --git a/fs/namespace.c b/fs/namespace.c
index 3d5cd5bf3b05..62cc71c401a2 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -282,7 +282,7 @@ int mnt_get_count(struct mount *mnt)
#endif
}
-static struct mount *alloc_vfsmnt(const char *name)
+static struct mount *alloc_vfsmnt(void)
{
struct mount *mnt = kmem_cache_zalloc(mnt_cache, GFP_KERNEL);
if (mnt) {
@@ -292,18 +292,10 @@ static struct mount *alloc_vfsmnt(const char *name)
if (err)
goto out_free_cache;
- if (name)
- mnt->mnt_devname = kstrdup_const(name,
- GFP_KERNEL_ACCOUNT);
- else
- mnt->mnt_devname = "none";
- if (!mnt->mnt_devname)
- goto out_free_id;
-
#ifdef CONFIG_SMP
mnt->mnt_pcp = alloc_percpu(struct mnt_pcp);
if (!mnt->mnt_pcp)
- goto out_free_devname;
+ goto out_free_id;
this_cpu_add(mnt->mnt_pcp->mnt_count, 1);
#else
@@ -328,11 +320,9 @@ static struct mount *alloc_vfsmnt(const char *name)
return mnt;
#ifdef CONFIG_SMP
-out_free_devname:
- kfree_const(mnt->mnt_devname);
-#endif
out_free_id:
mnt_free_id(mnt);
+#endif
out_free_cache:
kmem_cache_free(mnt_cache, mnt);
return NULL;
@@ -725,7 +715,6 @@ int sb_prepare_remount_readonly(struct super_block *sb)
static void free_vfsmnt(struct mount *mnt)
{
mnt_idmap_put(mnt_idmap(&mnt->mnt));
- kfree_const(mnt->mnt_devname);
#ifdef CONFIG_SMP
free_percpu(mnt->mnt_pcp);
#endif
@@ -1180,7 +1169,7 @@ struct vfsmount *vfs_create_mount(struct fs_context *fc)
if (!fc->root)
return ERR_PTR(-EINVAL);
- mnt = alloc_vfsmnt(fc->source);
+ mnt = alloc_vfsmnt();
if (!mnt)
return ERR_PTR(-ENOMEM);
@@ -1248,7 +1237,7 @@ static struct mount *clone_mnt(struct mount *old, struct dentry *root,
struct mount *mnt;
int err;
- mnt = alloc_vfsmnt(old->mnt_devname);
+ mnt = alloc_vfsmnt();
if (!mnt)
return ERR_PTR(-ENOMEM);
@@ -2957,16 +2946,12 @@ static int do_change_type(const struct path *path, int ms_flags)
static inline bool may_copy_tree(const struct path *path)
{
struct mount *mnt = real_mount(path->mnt);
- const struct dentry_operations *d_op;
if (check_mnt(mnt))
return true;
- d_op = path->dentry->d_op;
- if (d_op == &ns_dentry_operations)
- return true;
-
- if (d_op == &pidfs_dentry_operations)
+ /* Make an exception for internal mounts explicitly marked as clonable */
+ if (mnt->mnt_ns == MNT_NS_INTERNAL && mnt->mnt.mnt_flags & MNT_CLONABLE)
return true;
if (!is_mounted(path->mnt))
@@ -3201,7 +3186,7 @@ static struct file *vfs_open_tree(int dfd, const char __user *filename, unsigned
if (flags & ~(AT_EMPTY_PATH | AT_NO_AUTOMOUNT | AT_RECURSIVE |
AT_SYMLINK_NOFOLLOW | OPEN_TREE_CLONE |
- OPEN_TREE_CLOEXEC | OPEN_TREE_NAMESPACE))
+ OPEN_TREE_CLOEXEC | OPEN_TREE_NAMESPACE | OPEN_TREE_ALT))
return ERR_PTR(-EINVAL);
if ((flags & (AT_RECURSIVE | OPEN_TREE_CLONE | OPEN_TREE_NAMESPACE)) ==
@@ -3215,6 +3200,8 @@ static struct file *vfs_open_tree(int dfd, const char __user *filename, unsigned
lookup_flags &= ~LOOKUP_AUTOMOUNT;
if (flags & AT_SYMLINK_NOFOLLOW)
lookup_flags &= ~LOOKUP_FOLLOW;
+ if (flags & OPEN_TREE_ALT)
+ lookup_flags |= LOOKUP_ALT;
/*
* If we create a new mount namespace with the cloned mount tree we
@@ -5387,7 +5374,6 @@ static void statmount_fs_subtype(struct kstatmount *s, struct seq_file *seq)
static int statmount_sb_source(struct kstatmount *s, struct seq_file *seq)
{
struct super_block *sb = s->mnt->mnt_sb;
- struct mount *r = real_mount(s->mnt);
if (sb->s_op->show_devname) {
size_t start = seq->count;
@@ -5405,7 +5391,7 @@ static int statmount_sb_source(struct kstatmount *s, struct seq_file *seq)
seq->count = start;
seq_commit(seq, string_unescape_inplace(seq->buf + start, UNESCAPE_OCTAL));
} else {
- seq_puts(seq, r->mnt_devname);
+ seq_puts(seq, sb->s_devname);
}
return 0;
}
@@ -5416,6 +5402,40 @@ static void statmount_mnt_ns_id(struct kstatmount *s, struct mnt_namespace *ns)
s->sm.mnt_ns_id = ns->ns.ns_id;
}
+void sb_add_option(struct sb_opt_ctx *ctx, const char *name, const char *value)
+{
+ sb_add_option_path(ctx, name, value, NULL);
+}
+
+void sb_add_option_path(struct sb_opt_ctx *ctx, const char *name, const char *value,
+ const struct path *path)
+{
+ ctx->actor(ctx, name, value, path);
+}
+
+static void do_show_option(struct sb_opt_ctx *ctx, const char *name, const char *value,
+ const struct path *path)
+{
+ if (strcmp(name, "source") != 0)
+ seq_show_option(ctx->data, name, value);
+}
+
+int mnt_show_options(struct seq_file *seq, struct vfsmount *mnt)
+{
+ struct super_block *sb = mnt->mnt_sb;
+
+ if (sb->s_op->show_options)
+ return sb->s_op->show_options(seq, mnt->mnt_root);
+
+ if (sb->s_op->get_options) {
+ struct sb_opt_ctx ctx = { .data = seq, .actor = do_show_option };
+
+ return sb->s_op->get_options(&ctx, mnt->mnt_root);
+ }
+
+ return 0;
+}
+
static int statmount_mnt_opts(struct kstatmount *s, struct seq_file *seq)
{
struct vfsmount *mnt = s->mnt;
@@ -5427,11 +5447,9 @@ static int statmount_mnt_opts(struct kstatmount *s, struct seq_file *seq)
if (err)
return err;
- if (sb->s_op->show_options) {
- err = sb->s_op->show_options(seq, mnt->mnt_root);
- if (err)
- return err;
- }
+ err = mnt_show_options(seq, mnt);
+ if (err)
+ return err;
if (unlikely(seq_has_overflowed(seq)))
return -EAGAIN;
@@ -5479,14 +5497,10 @@ static inline int statmount_opt_process(struct seq_file *seq, size_t start)
static int statmount_opt_array(struct kstatmount *s, struct seq_file *seq)
{
struct vfsmount *mnt = s->mnt;
- struct super_block *sb = mnt->mnt_sb;
size_t start = seq->count;
int err;
- if (!sb->s_op->show_options)
- return 0;
-
- err = sb->s_op->show_options(seq, mnt->mnt_root);
+ err = mnt_show_options(seq, mnt);
if (err)
return err;
diff --git a/fs/nsfs.c b/fs/nsfs.c
index c3b6ae76594a..e110130d0760 100644
--- a/fs/nsfs.c
+++ b/fs/nsfs.c
@@ -684,6 +684,7 @@ void __init nsfs_init(void)
if (IS_ERR(nsfs_mnt))
panic("can't set nsfs up\n");
nsfs_mnt->mnt_sb->s_flags &= ~SB_NOUSER;
+ nsfs_mnt->mnt_flags |= MNT_CLONABLE;
nsfs_root_path.mnt = nsfs_mnt;
nsfs_root_path.dentry = nsfs_mnt->mnt_root;
}
diff --git a/fs/open.c b/fs/open.c
index 408925d7bd0b..3a081a9e3062 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -1131,7 +1131,8 @@ struct file *kernel_file_open(const struct path *path, int flags,
EXPORT_SYMBOL_GPL(kernel_file_open);
#define WILL_CREATE(flags) (flags & (O_CREAT | __O_TMPFILE))
-#define O_PATH_FLAGS (O_DIRECTORY | O_NOFOLLOW | O_PATH | O_CLOEXEC | O_EMPTYPATH)
+#define O_PATH_FLAGS (O_DIRECTORY | O_NOFOLLOW | O_PATH | O_CLOEXEC | O_EMPTYPATH | \
+ O_ALT)
inline struct open_how build_open_how(int flags, umode_t mode)
{
@@ -1283,6 +1284,8 @@ inline int build_open_flags(const struct open_how *how, struct open_flags *op)
lookup_flags |= LOOKUP_FOLLOW;
if (flags & O_EMPTYPATH)
lookup_flags |= LOOKUP_EMPTY;
+ if (flags & O_ALT)
+ lookup_flags |= LOOKUP_ALT;
if (how->resolve & RESOLVE_NO_XDEV)
lookup_flags |= LOOKUP_NO_XDEV;
diff --git a/fs/overlayfs/params.c b/fs/overlayfs/params.c
index c93fcaa45d4a..c5a9e858e366 100644
--- a/fs/overlayfs/params.c
+++ b/fs/overlayfs/params.c
@@ -1046,10 +1046,11 @@ int ovl_fs_params_verify(const struct ovl_fs_context *ctx,
* Prints the mount options for a given superblock.
* Returns zero; does not fail.
*/
-int ovl_show_options(struct seq_file *m, struct dentry *dentry)
+int ovl_get_options(struct sb_opt_ctx *ctx, struct dentry *dentry)
{
struct super_block *sb = dentry->d_sb;
struct ovl_fs *ofs = OVL_FS(sb);
+ struct path upperpath;
size_t nr, nr_merged_lower, nr_lower = 0;
char **lowerdirs = ofs->config.lowerdirs;
@@ -1062,43 +1063,47 @@ int ovl_show_options(struct seq_file *m, struct dentry *dentry)
* with the new lowerdir+ and datadir+ mount options.
*/
if (lowerdirs[0]) {
- seq_show_option(m, "lowerdir", lowerdirs[0]);
+ sb_add_option(ctx, "lowerdir", lowerdirs[0]);
} else {
nr_lower = ofs->numlayer;
nr_merged_lower = nr_lower - ofs->numdatalayer;
}
for (nr = 1; nr < nr_lower; nr++) {
+ struct path lowerpath = {
+ .mnt = ofs->layers[nr].mnt,
+ .dentry = ofs->layers[nr].mnt->mnt_root,
+ };
if (nr < nr_merged_lower)
- seq_show_option(m, "lowerdir+", lowerdirs[nr]);
+ sb_add_option_path(ctx, "lowerdir+", lowerdirs[nr],
+ &lowerpath);
else
- seq_show_option(m, "datadir+", lowerdirs[nr]);
+ sb_add_option_path(ctx, "datadir+", lowerdirs[nr],
+ &lowerpath);
}
if (ofs->config.upperdir) {
- seq_show_option(m, "upperdir", ofs->config.upperdir);
- seq_show_option(m, "workdir", ofs->config.workdir);
+ ovl_path_upper(sb->s_root, &upperpath);
+ sb_add_option_path(ctx, "upperdir", ofs->config.upperdir, &upperpath);
+ sb_add_option(ctx, "workdir", ofs->config.workdir);
}
if (ofs->config.default_permissions)
- seq_puts(m, ",default_permissions");
+ sb_add_option(ctx, "default_permissions", NULL);
if (ofs->config.redirect_mode != ovl_redirect_mode_def())
- seq_printf(m, ",redirect_dir=%s",
- ovl_redirect_mode(&ofs->config));
+ sb_add_option(ctx, "redirect_dir", ovl_redirect_mode(&ofs->config));
if (ofs->config.index != ovl_index_def)
- seq_printf(m, ",index=%s", str_on_off(ofs->config.index));
+ sb_add_option(ctx, "index", str_on_off(ofs->config.index));
if (ofs->config.uuid != ovl_uuid_def())
- seq_printf(m, ",uuid=%s", ovl_uuid_mode(&ofs->config));
+ sb_add_option(ctx, "uuid", ovl_uuid_mode(&ofs->config));
if (ofs->config.nfs_export != ovl_nfs_export_def)
- seq_printf(m, ",nfs_export=%s",
- str_on_off(ofs->config.nfs_export));
+ sb_add_option(ctx, "nfs_export", str_on_off(ofs->config.nfs_export));
if (ofs->config.xino != ovl_xino_def() && !ovl_same_fs(ofs))
- seq_printf(m, ",xino=%s", ovl_xino_mode(&ofs->config));
+ sb_add_option(ctx, "xino", ovl_xino_mode(&ofs->config));
if (ofs->config.metacopy != ovl_metacopy_def)
- seq_printf(m, ",metacopy=%s", str_on_off(ofs->config.metacopy));
+ sb_add_option(ctx, "metacopy", str_on_off(ofs->config.metacopy));
if (ofs->config.fsync_mode != ovl_fsync_mode_def())
- seq_printf(m, ",fsync=%s", ovl_fsync_mode(&ofs->config));
+ sb_add_option(ctx, "fsync", ovl_fsync_mode(&ofs->config));
if (ofs->config.userxattr)
- seq_puts(m, ",userxattr");
+ sb_add_option(ctx, "userxattr", NULL);
if (ofs->config.verity_mode != ovl_verity_mode_def())
- seq_printf(m, ",verity=%s",
- ovl_verity_mode(&ofs->config));
+ sb_add_option(ctx, "verity", ovl_verity_mode(&ofs->config));
return 0;
}
diff --git a/fs/overlayfs/params.h b/fs/overlayfs/params.h
index ffd53cdd8482..2cf9c4f188a9 100644
--- a/fs/overlayfs/params.h
+++ b/fs/overlayfs/params.h
@@ -40,5 +40,5 @@ int ovl_init_fs_context(struct fs_context *fc);
void ovl_free_fs(struct ovl_fs *ofs);
int ovl_fs_params_verify(const struct ovl_fs_context *ctx,
struct ovl_config *config);
-int ovl_show_options(struct seq_file *m, struct dentry *dentry);
+int ovl_get_options(struct sb_opt_ctx *ctx, struct dentry *dentry);
const char *ovl_xino_mode(struct ovl_config *config);
diff --git a/fs/overlayfs/super.c b/fs/overlayfs/super.c
index 60f0b7ceef0a..4b57b2db0852 100644
--- a/fs/overlayfs/super.c
+++ b/fs/overlayfs/super.c
@@ -302,7 +302,7 @@ static const struct super_operations ovl_super_operations = {
.put_super = ovl_put_super,
.sync_fs = ovl_sync_fs,
.statfs = ovl_statfs,
- .show_options = ovl_show_options,
+ .get_options = ovl_get_options,
};
#define OVL_WORKDIR_NAME "work"
@@ -529,6 +529,7 @@ static int ovl_get_upper(struct super_block *sb, struct ovl_fs *ofs,
/* Don't inherit atime flags */
upper_mnt->mnt_flags &= ~(MNT_NOATIME | MNT_NODIRATIME | MNT_RELATIME);
+ upper_mnt->mnt_flags |= MNT_CLONABLE;
upper_layer->mnt = upper_mnt;
upper_layer->idx = 0;
upper_layer->fsid = 0;
@@ -1105,7 +1106,7 @@ static int ovl_get_layers(struct super_block *sb, struct ovl_fs *ofs,
* Make lower layers R/O. That way fchmod/fchown on lower file
* will fail instead of modifying lower fs.
*/
- mnt->mnt_flags |= MNT_READONLY | MNT_NOATIME;
+ mnt->mnt_flags |= MNT_READONLY | MNT_NOATIME | MNT_CLONABLE;
layers[ofs->numlayer].trap = trap;
layers[ofs->numlayer].mnt = mnt;
diff --git a/fs/pidfs.c b/fs/pidfs.c
index aaa609ddab04..3843acd6594d 100644
--- a/fs/pidfs.c
+++ b/fs/pidfs.c
@@ -840,7 +840,7 @@ static char *pidfs_dname(struct dentry *dentry, char *buffer, int buflen)
return dynamic_dname(buffer, buflen, "anon_inode:[pidfd]");
}
-const struct dentry_operations pidfs_dentry_operations = {
+static const struct dentry_operations pidfs_dentry_operations = {
.d_dname = pidfs_dname,
.d_prune = stashed_dentry_prune,
};
@@ -1151,6 +1151,8 @@ void __init pidfs_init(void)
if (IS_ERR(pidfs_mnt))
panic("Failed to mount pidfs pseudo filesystem");
+ pidfs_mnt->mnt_flags |= MNT_CLONABLE;
+
pidfs_root_path.mnt = pidfs_mnt;
pidfs_root_path.dentry = pidfs_mnt->mnt_root;
}
diff --git a/fs/proc_namespace.c b/fs/proc_namespace.c
index 5c555db68aa2..94bc4c9aa43f 100644
--- a/fs/proc_namespace.c
+++ b/fs/proc_namespace.c
@@ -98,21 +98,27 @@ static void show_type(struct seq_file *m, struct super_block *sb)
}
}
+static int show_devname(struct seq_file *m, struct vfsmount *mnt)
+{
+ struct super_block *sb = mnt->mnt_sb;
+
+ if (sb->s_op->show_devname)
+ return sb->s_op->show_devname(m, mnt->mnt_root);
+
+ mangle(m, sb->s_devname);
+ return 0;
+}
+
static int show_vfsmnt(struct seq_file *m, struct vfsmount *mnt)
{
struct proc_mounts *p = m->private;
- struct mount *r = real_mount(mnt);
struct path mnt_path = { .dentry = mnt->mnt_root, .mnt = mnt };
struct super_block *sb = mnt_path.dentry->d_sb;
int err;
- if (sb->s_op->show_devname) {
- err = sb->s_op->show_devname(m, mnt_path.dentry);
- if (err)
- goto out;
- } else {
- mangle(m, r->mnt_devname);
- }
+ err = show_devname(m, mnt);
+ if (err)
+ goto out;
seq_putc(m, ' ');
/* mountpoints outside of chroot jail will give SEQ_SKIP on this */
err = seq_path_root(m, &mnt_path, &p->root, " \t\n\\");
@@ -125,8 +131,7 @@ static int show_vfsmnt(struct seq_file *m, struct vfsmount *mnt)
if (err)
goto out;
show_vfsmnt_opts(m, mnt);
- if (sb->s_op->show_options)
- err = sb->s_op->show_options(m, mnt_path.dentry);
+ err = mnt_show_options(m, mnt);
seq_puts(m, " 0 0\n");
out:
return err;
@@ -172,19 +177,14 @@ static int show_mountinfo(struct seq_file *m, struct vfsmount *mnt)
seq_puts(m, " - ");
show_type(m, sb);
seq_putc(m, ' ');
- if (sb->s_op->show_devname) {
- err = sb->s_op->show_devname(m, mnt->mnt_root);
- if (err)
- goto out;
- } else {
- mangle(m, r->mnt_devname);
- }
+ err = show_devname(m, mnt);
+ if (err)
+ goto out;
seq_puts(m, sb_rdonly(sb) ? " ro" : " rw");
err = show_sb_opts(m, sb);
if (err)
goto out;
- if (sb->s_op->show_options)
- err = sb->s_op->show_options(m, mnt->mnt_root);
+ err = mnt_show_options(m, mnt);
seq_putc(m, '\n');
out:
return err;
@@ -193,21 +193,15 @@ static int show_mountinfo(struct seq_file *m, struct vfsmount *mnt)
static int show_vfsstat(struct seq_file *m, struct vfsmount *mnt)
{
struct proc_mounts *p = m->private;
- struct mount *r = real_mount(mnt);
struct path mnt_path = { .dentry = mnt->mnt_root, .mnt = mnt };
struct super_block *sb = mnt_path.dentry->d_sb;
int err;
/* device */
seq_puts(m, "device ");
- if (sb->s_op->show_devname) {
- err = sb->s_op->show_devname(m, mnt_path.dentry);
- if (err)
- goto out;
- } else {
- mangle(m, r->mnt_devname);
- }
-
+ err = show_devname(m, mnt);
+ if (err)
+ goto out;
/* mount point */
seq_puts(m, " mounted on ");
/* mountpoints outside of chroot jail will give SEQ_SKIP on this */
diff --git a/fs/super.c b/fs/super.c
index a8fd61136aaf..e5531cb88f89 100644
--- a/fs/super.c
+++ b/fs/super.c
@@ -280,6 +280,7 @@ static void destroy_super_work(struct work_struct *work)
security_sb_free(s);
put_user_ns(s->s_user_ns);
kfree(s->s_subtype);
+ kfree_const(s->s_devname);
for (int i = 0; i < SB_FREEZE_LEVELS; i++)
percpu_free_rwsem(&s->s_writers.rw_sem[i]);
kfree(s);
@@ -315,7 +316,7 @@ static void destroy_unused_super(struct super_block *s)
* returns a pointer new superblock or %NULL if allocation had failed.
*/
static struct super_block *alloc_super(struct file_system_type *type, int flags,
- struct user_namespace *user_ns)
+ struct user_namespace *user_ns, const char *name)
{
struct super_block *s = kzalloc_obj(struct super_block);
static const struct super_operations default_op;
@@ -344,6 +345,13 @@ static struct super_block *alloc_super(struct file_system_type *type, int flags,
*/
down_write_nested(&s->s_umount, SINGLE_DEPTH_NESTING);
+ if (name)
+ s->s_devname = kstrdup_const(name, GFP_KERNEL_ACCOUNT);
+ else
+ s->s_devname = "none";
+ if (!s->s_devname)
+ goto fail;
+
if (security_sb_alloc(s))
goto fail;
@@ -763,7 +771,7 @@ struct super_block *sget_fc(struct fs_context *fc,
}
if (!s) {
spin_unlock(&sb_lock);
- s = alloc_super(fc->fs_type, fc->sb_flags, user_ns);
+ s = alloc_super(fc->fs_type, fc->sb_flags, user_ns, fc->source);
if (!s)
return ERR_PTR(-ENOMEM);
goto retry;
diff --git a/include/linux/fcntl.h b/include/linux/fcntl.h
index 6ad6b9e7a226..f942411c8ed7 100644
--- a/include/linux/fcntl.h
+++ b/include/linux/fcntl.h
@@ -11,7 +11,7 @@
(O_RDONLY | O_WRONLY | O_RDWR | O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC | \
O_APPEND | O_NDELAY | O_NONBLOCK | __O_SYNC | O_DSYNC | \
FASYNC | O_DIRECT | O_LARGEFILE | O_DIRECTORY | O_NOFOLLOW | \
- O_NOATIME | O_CLOEXEC | O_PATH | __O_TMPFILE | O_EMPTYPATH)
+ O_NOATIME | O_CLOEXEC | O_PATH | __O_TMPFILE | O_EMPTYPATH | O_ALT)
/* List of all valid flags for openat2(2)'s how->flags argument. */
#define VALID_OPENAT2_FLAGS (VALID_OPEN_FLAGS | OPENAT2_REGULAR)
diff --git a/include/linux/fs/super.h b/include/linux/fs/super.h
index 405612678115..6f0d778940f9 100644
--- a/include/linux/fs/super.h
+++ b/include/linux/fs/super.h
@@ -237,4 +237,14 @@ int thaw_super(struct super_block *super, enum freeze_holder who,
int sb_init_dio_done_wq(struct super_block *sb);
+struct sb_opt_ctx {
+ void (*actor)(struct sb_opt_ctx *ctx, const char *name, const char *value,
+ const struct path *path);
+ void *data;
+};
+
+void sb_add_option(struct sb_opt_ctx *ctx, const char *name, const char *value);
+void sb_add_option_path(struct sb_opt_ctx *ctx, const char *name, const char *value,
+ const struct path *path);
+
#endif /* _LINUX_FS_SUPER_H */
diff --git a/include/linux/fs/super_types.h b/include/linux/fs/super_types.h
index ef7941e9dc79..f205759af1a6 100644
--- a/include/linux/fs/super_types.h
+++ b/include/linux/fs/super_types.h
@@ -36,6 +36,7 @@ struct workqueue_struct;
struct writeback_control;
struct xattr_handler;
struct fserror_event;
+struct sb_opt_ctx;
extern struct super_block *blockdev_superblock;
@@ -99,6 +100,7 @@ struct super_operations {
int (*statfs)(struct dentry *dentry, struct kstatfs *kstatfs);
void (*umount_begin)(struct super_block *sb);
+ int (*get_options)(struct sb_opt_ctx *ctx, struct dentry *dentry);
int (*show_options)(struct seq_file *seq, struct dentry *dentry);
int (*show_devname)(struct seq_file *seq, struct dentry *dentry);
int (*show_path)(struct seq_file *seq, struct dentry *dentry);
@@ -283,6 +285,8 @@ struct super_block {
*/
atomic_t s_isw_nr_in_flight;
#endif
+ /* Name of device e.g. /dev/dsk/hda1 */
+ const char *s_devname;
} __randomize_layout;
/*
diff --git a/include/linux/mount.h b/include/linux/mount.h
index acfe7ef86a1b..d071855ca4ed 100644
--- a/include/linux/mount.h
+++ b/include/linux/mount.h
@@ -44,6 +44,7 @@ enum mount_flags {
MNT_LOCKED = 0x800000,
MNT_DOOMED = 0x1000000,
MNT_SYNC_UMOUNT = 0x2000000,
+ MNT_CLONABLE = 0x4000000,
MNT_UMOUNT = 0x8000000,
MNT_USER_SETTABLE_MASK = MNT_NOSUID | MNT_NODEV | MNT_NOEXEC
@@ -52,7 +53,7 @@ enum mount_flags {
MNT_ATIME_MASK = MNT_NOATIME | MNT_NODIRATIME | MNT_RELATIME,
MNT_INTERNAL_FLAGS = MNT_INTERNAL | MNT_DOOMED |
- MNT_SYNC_UMOUNT | MNT_LOCKED
+ MNT_SYNC_UMOUNT | MNT_LOCKED | MNT_CLONABLE
};
struct vfsmount {
diff --git a/include/linux/namei.h b/include/linux/namei.h
index ebe6e29f7e93..c21dc6536900 100644
--- a/include/linux/namei.h
+++ b/include/linux/namei.h
@@ -25,7 +25,8 @@ enum { MAX_NESTED_LINKS = 8 };
#define LOOKUP_RCU BIT(8) /* RCU pathwalk mode; semi-internal */
#define LOOKUP_CACHED BIT(9) /* Only do cached lookup */
#define LOOKUP_PARENT BIT(10) /* Looking up final parent in path */
-/* 5 spare bits for pathwalk */
+#define LOOKUP_ALT BIT(11) /* Look up in alternative namespace */
+/* 4 spare bits for pathwalk */
/* These tell filesystem methods that we are dealing with the final component... */
#define LOOKUP_OPEN BIT(16) /* ... in open */
diff --git a/include/linux/pidfs.h b/include/linux/pidfs.h
index 0abf7da9ab23..fab6f48ff203 100644
--- a/include/linux/pidfs.h
+++ b/include/linux/pidfs.h
@@ -15,7 +15,6 @@ void pidfs_exit(struct task_struct *tsk);
#ifdef CONFIG_COREDUMP
void pidfs_coredump(const struct coredump_params *cprm);
#endif
-extern const struct dentry_operations pidfs_dentry_operations;
int pidfs_register_pid_gfp(struct pid *pid, gfp_t gfp);
/**
diff --git a/include/uapi/asm-generic/fcntl.h b/include/uapi/asm-generic/fcntl.h
index 883cfd7de2ef..70dbbac1c520 100644
--- a/include/uapi/asm-generic/fcntl.h
+++ b/include/uapi/asm-generic/fcntl.h
@@ -96,6 +96,10 @@
#define O_EMPTYPATH (1 << 26) /* allow empty path */
#endif
+#ifndef O_ALT
+#define O_ALT (1 << 27) /* Open in alternative namespace */
+#endif
+
/* a horrid kludge trying to make sure that this will fail on old kernels */
#define O_TMPFILE (__O_TMPFILE | O_DIRECTORY)
diff --git a/include/uapi/linux/mount.h b/include/uapi/linux/mount.h
index 2204708dbf7a..6e794d214cf9 100644
--- a/include/uapi/linux/mount.h
+++ b/include/uapi/linux/mount.h
@@ -63,6 +63,7 @@
*/
#define OPEN_TREE_CLONE (1 << 0) /* Clone the target tree and attach the clone */
#define OPEN_TREE_NAMESPACE (1 << 1) /* Clone the target tree into a new mount namespace */
+#define OPEN_TREE_ALT (1 << 2) /* Lookup in alternative namespace */
#define OPEN_TREE_CLOEXEC O_CLOEXEC /* Close the file on execve() */
/*
diff --git a/scripts/gdb/linux/proc.py b/scripts/gdb/linux/proc.py
index 65dd1bd12964..5aaf2dd8db05 100644
--- a/scripts/gdb/linux/proc.py
+++ b/scripts/gdb/linux/proc.py
@@ -174,9 +174,6 @@ values of that process namespace"""
"mount", "super_block", "devname", "pathname", "fstype"))
for mnt in rbtree.rb_inorder_for_each_entry(namespace['mounts'], mount_ptr_type, "mnt_node"):
- devname = mnt['mnt_devname'].string()
- devname = devname if devname else "none"
-
pathname = ""
parent = mnt
while True:
@@ -190,6 +187,8 @@ values of that process namespace"""
pathname = "/"
superblock = mnt['mnt']['mnt_sb']
+ devname = superblock['s_devname'].string()
+ devname = devname if devname else "none"
fstype = superblock['s_type']['name'].string()
s_flags = int(superblock['s_flags'])
m_flags = int(mnt['mnt']['mnt_flags'])
--
2.54.0
^ permalink raw reply related
* Re: [RFC PATCH] fs: allow opening overlayfs/erofs layers through O_ALT
From: Giuseppe Scrivano @ 2026-07-15 16:43 UTC (permalink / raw)
To: Miklos Szeredi
Cc: linux-fsdevel, linux-unionfs, linux-api, linux-erofs,
Amir Goldstein, Gao Xiang
In-Reply-To: <20260715101107.973997-1-mszeredi@redhat.com>
Miklos Szeredi <mszeredi@redhat.com> writes:
> This is a prototype patch. Needs to be split up!
>
> 1) O_ALT / OPEN_TREE_ALT
>
> These open an alternative namespace rooted at dfd - instead of resolving
> the path in the real filesystem, it resolves it in a virtual tree that
> exposes metadata about that file.
>
> This is intended to provide an alternative to adding new ioctls:
>
> - provides structured namespace
> - allows accessing data through fs ops
>
> Could call this O_META, but O_ALT seems more generic and there could be
> uses beyond metadata (e.g. seekable, in-place decompression).
>
> This has been discussed previously:
> https://lore.kernel.org/all/CAHk-=wjzLmMRf=QG-n+1HnxWCx4KTQn9+OhVvUSJ=ZCQd6Y1WA@mail.gmail.com/
>
> 2/a) s_op.get_options(), sb_add_option()
>
> .get_options() is a generic version of .show_options().
>
> After converting everyting to seq_show_option() the rest is trivial:
>
> - change first arg to "struct sb_opt_ctx *ctx"
> - replace "seq_show_option(m" with "sb_add_option(ctx"
>
> 2/b) sb_add_option_path()
>
> The last arg is "struct path *". This is for options that designate a
> path (e.g. stacked fs layers).
>
> Will allow opening the path through O_ALT. Ignored when retrieving the
> string value of the option.
>
> 3) MNT_CLONABLE
>
> Allow an internal mount to be cloned. Nsfs and pidfs are already special
> cased, move these over to this flag.
>
> Also allow overlayfs layers to be cloned.
>
> 4) mnt.mnt_devname -> sb.s_devname
>
> This is historical thing, mnt_devname was just copied when cloning the
> mount, sharing the same value for all mounts belonging to a super block.
>
> 5) mount options in metafs
>
> Support retrieving mount options through O_ALT opens:
>
> mount/options/OPT/N: Nth instance of OPT
>
> This will be a symlink pointint to the option value. If the option refers
> to a path (as per 2/b), following the symlink jumps to the given path.
>
> At this point only overlayfs and erofs are converted.
>
> Signed-off-by: Miklos Szeredi <mszeredi@redhat.com>
> ---
> Documentation/filesystems/vfs.rst | 2 +-
> fs/Makefile | 2 +
> fs/erofs/super.c | 29 +-
> fs/fcntl.c | 2 +-
> fs/internal.h | 1 +
> fs/metafs/Makefile | 6 +
> fs/metafs/metafs.c | 572 ++++++++++++++++++++++++++++++
> fs/metafs/metafs.h | 15 +
> fs/mount.h | 1 -
> fs/namei.c | 16 +
> fs/namespace.c | 84 +++--
> fs/nsfs.c | 1 +
> fs/open.c | 5 +-
> fs/overlayfs/params.c | 43 ++-
> fs/overlayfs/params.h | 2 +-
> fs/overlayfs/super.c | 5 +-
> fs/pidfs.c | 4 +-
> fs/proc_namespace.c | 50 ++-
> fs/super.c | 12 +-
> include/linux/fcntl.h | 2 +-
> include/linux/fs/super.h | 10 +
> include/linux/fs/super_types.h | 4 +
> include/linux/mount.h | 3 +-
> include/linux/namei.h | 3 +-
> include/linux/pidfs.h | 1 -
> include/uapi/asm-generic/fcntl.h | 4 +
> include/uapi/linux/mount.h | 1 +
> scripts/gdb/linux/proc.py | 5 +-
> 28 files changed, 774 insertions(+), 111 deletions(-)
> create mode 100644 fs/metafs/Makefile
> create mode 100644 fs/metafs/metafs.c
> create mode 100644 fs/metafs/metafs.h
>
> diff --git a/Documentation/filesystems/vfs.rst b/Documentation/filesystems/vfs.rst
> index 7c753148af88..3dc431b6cdbb 100644
> --- a/Documentation/filesystems/vfs.rst
> +++ b/Documentation/filesystems/vfs.rst
> @@ -314,7 +314,7 @@ or bottom half).
> ``show_devname``
> Optional. Called by the VFS to show device name for
> /proc/<pid>/{mounts,mountinfo,mountstats}. If not provided then
> - '(struct mount).mnt_devname' will be used.
> + '(struct super_block).s_devname' will be used.
>
> ``show_path``
> Optional. Called by the VFS (for /proc/<pid>/mountinfo) to show
> diff --git a/fs/Makefile b/fs/Makefile
> index 89a8a9d207d1..87a4c23d5a08 100644
> --- a/fs/Makefile
> +++ b/fs/Makefile
> @@ -58,6 +58,8 @@ obj-$(CONFIG_CONFIGFS_FS) += configfs/
> obj-y += devpts/
>
> obj-$(CONFIG_DLM) += dlm/
> +
> +obj-y += metafs/
>
> # Do not add any filesystems before this line
> obj-$(CONFIG_NETFS_SUPPORT) += netfs/
> diff --git a/fs/erofs/super.c b/fs/erofs/super.c
> index 86fa5c6a0c70..257517c28e1d 100644
> --- a/fs/erofs/super.c
> +++ b/fs/erofs/super.c
> @@ -1028,29 +1028,34 @@ static int erofs_statfs(struct dentry *dentry, struct kstatfs *buf)
> return 0;
> }
>
> -static int erofs_show_options(struct seq_file *seq, struct dentry *root)
> +static int erofs_get_options(struct sb_opt_ctx *ctx, struct dentry *root)
> {
> struct erofs_sb_info *sbi = EROFS_SB(root->d_sb);
> struct erofs_mount_opts *opt = &sbi->opt;
>
> + if (sbi->dif0.file)
> + sb_add_option_path(ctx, "source", NULL, &sbi->dif0.file->f_path);
> +
> if (IS_ENABLED(CONFIG_EROFS_FS_XATTR))
> - seq_puts(seq, test_opt(opt, XATTR_USER) ?
> - ",user_xattr" : ",nouser_xattr");
> + sb_add_option(ctx, test_opt(opt, XATTR_USER) ?
> + "user_xattr" : "nouser_xattr", NULL);
> if (IS_ENABLED(CONFIG_EROFS_FS_POSIX_ACL))
> - seq_puts(seq, test_opt(opt, POSIX_ACL) ? ",acl" : ",noacl");
> + sb_add_option(ctx, test_opt(opt, POSIX_ACL) ? "acl" : "noacl", NULL);
> if (IS_ENABLED(CONFIG_EROFS_FS_ZIP))
> - seq_printf(seq, ",cache_strategy=%s",
> + sb_add_option(ctx, "cache_strategy",
> erofs_param_cache_strategy[opt->cache_strategy].name);
> if (test_opt(opt, DAX_ALWAYS))
> - seq_puts(seq, ",dax=always");
> + sb_add_option(ctx, "dax", "always");
> if (test_opt(opt, DAX_NEVER))
> - seq_puts(seq, ",dax=never");
> + sb_add_option(ctx, "dax", "never");
> if (erofs_is_fileio_mode(sbi) && test_opt(opt, DIRECT_IO))
> - seq_puts(seq, ",directio");
> - if (sbi->dif0.fsoff)
> - seq_printf(seq, ",fsoffset=%llu", sbi->dif0.fsoff);
> + sb_add_option(ctx, "directio", NULL);
> + if (sbi->dif0.fsoff) {
> + char *val __free(kfree) = kasprintf(GFP_KERNEL, "%llu", sbi->dif0.fsoff);
> + sb_add_option(ctx, "fsoffset", val);
> + }
> if (test_opt(opt, INODE_SHARE))
> - seq_puts(seq, ",inode_share");
> + sb_add_option(ctx, "inode_share", NULL);
> return 0;
> }
>
> @@ -1069,7 +1074,7 @@ const struct super_operations erofs_sops = {
> .free_inode = erofs_free_inode,
> .evict_inode = erofs_evict_inode,
> .statfs = erofs_statfs,
> - .show_options = erofs_show_options,
> + .get_options = erofs_get_options,
> };
>
> module_init(erofs_module_init);
> diff --git a/fs/fcntl.c b/fs/fcntl.c
> index c158f082f1da..01b765cfac84 100644
> --- a/fs/fcntl.c
> +++ b/fs/fcntl.c
> @@ -1169,7 +1169,7 @@ static int __init fcntl_init(void)
> * Exceptions: O_NONBLOCK is a two bit define on parisc; O_NDELAY
> * is defined as O_NONBLOCK on some platforms and not on others.
> */
> - BUILD_BUG_ON(22 - 1 /* for O_RDONLY being 0 */ !=
> + BUILD_BUG_ON(23 - 1 /* for O_RDONLY being 0 */ !=
> HWEIGHT32(
> (VALID_OPEN_FLAGS & ~(O_NONBLOCK | O_NDELAY)) |
> __FMODE_EXEC | __O_REGULAR));
> diff --git a/fs/internal.h b/fs/internal.h
> index 355d93f92208..e73a5c1d5b4e 100644
> --- a/fs/internal.h
> +++ b/fs/internal.h
> @@ -95,6 +95,7 @@ int path_umount(const struct path *path, int flags);
> int path_pivot_root(struct path *new, struct path *old);
>
> int show_path(struct seq_file *m, struct dentry *root);
> +int mnt_show_options(struct seq_file *seq, struct vfsmount *mnt);
>
> /*
> * fs_struct.c
> diff --git a/fs/metafs/Makefile b/fs/metafs/Makefile
> new file mode 100644
> index 000000000000..f855b48d9c2b
> --- /dev/null
> +++ b/fs/metafs/Makefile
> @@ -0,0 +1,6 @@
> +# SPDX-License-Identifier: GPL-2.0-only
> +#
> +# Makefile for meta filesystem
> +#
> +
> +obj-y := metafs.o
> diff --git a/fs/metafs/metafs.c b/fs/metafs/metafs.c
> new file mode 100644
> index 000000000000..15ec0b6064da
> --- /dev/null
> +++ b/fs/metafs/metafs.c
> @@ -0,0 +1,572 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * metafs - virtual filesystem for metadata access
> + *
> + * This provides a way to access metadata about files and filesystems
> + * through O_ALT file descriptors.
> + */
> +
> +#include <linux/fs.h>
> +#include <linux/init.h>
> +#include <linux/kernel.h>
> +#include <linux/magic.h>
> +#include <linux/mount.h>
> +#include <linux/namei.h>
> +#include <linux/slab.h>
> +#include <linux/dcache.h>
> +#include <linux/pseudo_fs.h>
> +#include <linux/fs/super.h>
> +#include "metafs.h"
> +
> +/*
> + * metafs superblock and mount
> + */
> +static struct vfsmount *metafs_mnt __ro_after_init;
> +
> +static const struct super_operations metafs_ops = {
> + .statfs = simple_statfs,
> + .drop_inode = inode_just_drop,
> +};
> +
> +static int metafs_init_fs_context(struct fs_context *fc)
> +{
> + struct pseudo_fs_context *ctx = init_pseudo(fc, 0x4d455441); /* 'META' */
> + if (!ctx)
> + return -ENOMEM;
> + ctx->ops = &metafs_ops;
> + return 0;
> +}
> +
> +static struct file_system_type metafs_type = {
> + .name = "metafs",
> + .init_fs_context = metafs_init_fs_context,
> + .kill_sb = kill_anon_super,
> +};
> +
> +/*
> + * Mount option collection structures
> + */
> +enum metafs_dentry_type {
> + METAFS_ROOT,
> + METAFS_OPTION_VALUES,
> + METAFS_SYMLINK,
> +};
> +
> +struct metafs_option_entry {
> + char *value;
> + struct path path;
> +};
> +
> +struct metafs_dentry_info {
> + enum metafs_dentry_type type;
> + union {
> + struct {
> + struct file *base;
> + } root;
> + struct {
> + struct metafs_option_entry *entries;
> + int count;
> + } opt_values;
> + struct metafs_option_entry symlink;
> + };
> +};
> +
> +struct metafs_collect_ctx {
> + struct sb_opt_ctx ctx;
> + const char *target_name;
> + struct super_block *sb;
> + struct metafs_option_entry *entries;
> + int count;
> + int size;
> + int err;
> + bool is_source;
> +};
> +
> +/*
> + * Collect specific mount option values from the base filesystem
> + */
> +static void metafs_collect_option(struct sb_opt_ctx *ctx, const char *name,
> + const char *value, const struct path *path)
> +{
> + struct metafs_collect_ctx *collect_ctx = ctx->data;
> + struct metafs_option_entry *entry;
> +
> + if (strcmp(name, collect_ctx->target_name) != 0)
> + return;
> +
> + if (!value && collect_ctx->is_source)
> + value = collect_ctx->sb->s_devname;
> +
> + if (collect_ctx->count >= collect_ctx->size) {
> + int new_size = collect_ctx->size ? collect_ctx->size * 2 : 4;
> + struct metafs_option_entry *new_entries;
> +
> + new_entries = krealloc_array(collect_ctx->entries, new_size, sizeof(*new_entries),
> + GFP_KERNEL);
> + if (!new_entries) {
> + collect_ctx->err = -ENOMEM;
> + return;
> + }
> + collect_ctx->entries = new_entries;
> + collect_ctx->size = new_size;
> + }
> +
> + entry = &collect_ctx->entries[collect_ctx->count];
> + entry->value = value ? kstrdup(value, GFP_KERNEL) : NULL;
> + if (!entry->value && value) {
> + collect_ctx->err = -ENOMEM;
> + return;
> + }
> + if (path) {
> + entry->path = *path;
> + path_get(&entry->path);
> + } else {
> + memset(&entry->path, 0, sizeof(entry->path));
> + }
> + collect_ctx->count++;
> +}
> +
> +static void metafs_free_entries(struct metafs_option_entry *entries, int count)
> +{
> + int i;
> +
> + for (i = 0; i < count; i++) {
> + kfree(entries[i].value);
> + if (entries[i].path.mnt)
> + path_put(&entries[i].path);
> + }
> + kfree(entries);
> +}
> +
> +static int metafs_get_option_values(struct file *base, const char *option_name,
> + struct metafs_option_entry **entries_out,
> + int *count_out)
> +{
> + const struct path *path = &base->f_path;
> + struct super_block *sb = path->mnt->mnt_sb;
> + struct metafs_collect_ctx collect_ctx = {
> + .ctx.data = &collect_ctx,
> + .ctx.actor = metafs_collect_option,
> + .target_name = option_name,
> + .sb = sb,
> + .is_source = !strcmp(option_name, "source"),
> + };
> + int ret;
> +
> + if (sb->s_op->get_options) {
> + ret = sb->s_op->get_options(&collect_ctx.ctx, path->mnt->mnt_root);
> + if (ret || collect_ctx.err) {
> + metafs_free_entries(collect_ctx.entries, collect_ctx.count);
> + return ret ?: collect_ctx.err;
> + }
> + }
> +
> + /* special case: add "source" option if filesystem didn't add one*/
> + if (!collect_ctx.count && collect_ctx.is_source) {
> + struct metafs_option_entry *ent __free(kfree) = kzalloc_obj(*ent);
> +
> + if (!ent)
> + return -ENOMEM;
> +
> + ent->value = kstrdup(sb->s_devname, GFP_KERNEL);
> + if (!ent->value)
> + return -ENOMEM;
> +
> + collect_ctx.entries = no_free_ptr(ent);
> + collect_ctx.count = 1;
> + }
> +
> + *entries_out = collect_ctx.entries;
> + *count_out = collect_ctx.count;
> + return 0;
> +}
> +
> +/*
> + * metafs symlink operations
> + */
> +static const char *metafs_get_link(struct dentry *dentry, struct inode *inode,
> + struct delayed_call *done)
> +{
> + struct metafs_dentry_info *info;
> +
> + if (!dentry)
> + return ERR_PTR(-ECHILD);
> +
> + info = dentry->d_fsdata;
> + if (info->symlink.path.mnt) {
> + int error;
> +
> + path_get(&info->symlink.path);
> + error = nd_jump_link(&info->symlink.path);
> + return ERR_PTR(error);
> + }
> +
> + return ERR_PTR(-ELOOP);
> +}
> +
> +static int metafs_readlink(struct dentry *dentry, char __user *buffer,
> + int buflen)
> +{
> + struct metafs_dentry_info *info = dentry->d_fsdata;
> + const char *value = info->symlink.value;
> +
> + if (!value)
> + value = "";
> +
> + return readlink_copy(buffer, buflen, value, strlen(value));
> +}
> +
> +static const struct inode_operations metafs_symlink_iops = {
> + .get_link = metafs_get_link,
> + .readlink = metafs_readlink,
> +};
> +
> +/*
> + * Dentry operations - cleanup
> + */
> +static void metafs_d_release(struct dentry *dentry)
> +{
> + struct metafs_dentry_info *info = dentry->d_fsdata;
> +
> + if (!info)
> + return;
> +
> + switch (info->type) {
> + case METAFS_ROOT:
> + fput(info->root.base);
> + break;
> + case METAFS_OPTION_VALUES:
> + metafs_free_entries(info->opt_values.entries,
> + info->opt_values.count);
> + break;
> + case METAFS_SYMLINK:
> + kfree(info->symlink.value);
> + if (info->symlink.path.mnt)
> + path_put(&info->symlink.path);
> + break;
> + default:
> + WARN_ON(1);
> + }
> + kfree(info);
> +}
> +
> +static const struct dentry_operations metafs_dentry_ops = {
> + .d_release = metafs_d_release,
> +};
> +
> +/*
> + * metafs directory operations - forward declarations
> + */
> +static struct file *metafs_get_base_file(struct dentry *dentry);
> +static struct dentry *metafs_root_lookup(struct inode *dir, struct dentry *dentry,
> + unsigned int flags);
> +static struct dentry *metafs_mount_lookup(struct inode *dir, struct dentry *dentry,
> + unsigned int flags);
> +static struct dentry *metafs_options_lookup(struct inode *dir, struct dentry *dentry,
> + unsigned int flags);
> +static struct dentry *metafs_option_values_lookup(struct inode *dir, struct dentry *dentry,
> + unsigned int flags);
> +
> +static const struct inode_operations metafs_root_dir_iops = {
> + .lookup = metafs_root_lookup,
> +};
> +
> +static const struct inode_operations metafs_mount_dir_iops = {
> + .lookup = metafs_mount_lookup,
> +};
> +
> +static const struct inode_operations metafs_options_dir_iops = {
> + .lookup = metafs_options_lookup,
> +};
> +
> +static const struct inode_operations metafs_option_values_dir_iops = {
> + .lookup = metafs_option_values_lookup,
> +};
> +
> +/*
> + * Create a new inode in metafs
> + */
> +static struct inode *metafs_get_inode(struct super_block *sb, umode_t mode,
> + const struct inode_operations *iops)
> +{
> + struct inode *inode = new_inode(sb);
> +
> + if (!inode)
> + return NULL;
> +
> + inode->i_ino = get_next_ino();
> + inode->i_mode = mode;
> + simple_inode_init_ts(inode);
> +
> + if (S_ISDIR(mode)) {
> + inode->i_op = iops;
> + inc_nlink(inode);
> + } else if (S_ISLNK(mode)) {
> + inode->i_op = &metafs_symlink_iops;
> + }
> +
> + return inode;
> +}
> +
> +/*
> + * Lookup in root directory (/)
> + * Only "mount" is valid here
> + */
> +static struct dentry *metafs_root_lookup(struct inode *dir, struct dentry *dentry,
> + unsigned int flags)
> +{
> + const char *name = dentry->d_name.name;
> + struct inode *inode;
> +
> + if (strcmp(name, "mount") != 0)
> + return ERR_PTR(-ENOENT);
> +
> + inode = metafs_get_inode(dir->i_sb, S_IFDIR | 0555, &metafs_mount_dir_iops);
> + if (!inode)
> + return ERR_PTR(-ENOMEM);
> +
> + d_add(dentry, inode);
> + d_mark_dontcache(inode);
> + return NULL;
> +}
> +
> +/*
> + * Lookup in /mount directory
> + * Only "options" is valid here
> + */
> +static struct dentry *metafs_mount_lookup(struct inode *dir, struct dentry *dentry,
> + unsigned int flags)
> +{
> + const char *name = dentry->d_name.name;
> + struct inode *inode;
> +
> + if (strcmp(name, "options") != 0)
> + return ERR_PTR(-ENOENT);
> +
> + inode = metafs_get_inode(dir->i_sb, S_IFDIR | 0555, &metafs_options_dir_iops);
> + if (!inode)
> + return ERR_PTR(-ENOMEM);
> +
> + d_add(dentry, inode);
> + d_mark_dontcache(inode);
> + return NULL;
> +}
> +
> +/*
> + * Lookup in /mount/options directory
> + * Collect values for the requested option name
> + */
> +static struct dentry *metafs_options_lookup(struct inode *dir, struct dentry *dentry,
> + unsigned int flags)
> +{
> + const char *target_name = dentry->d_name.name;
> + struct file *base;
> + struct metafs_dentry_info *info;
> + struct metafs_option_entry *entries = NULL;
> + int count = 0;
> + struct inode *inode;
> + int ret;
> +
> + base = metafs_get_base_file(dentry->d_parent);
> + if (!base)
> + return ERR_PTR(-ENOENT);
> +
> + ret = metafs_get_option_values(base, target_name, &entries, &count);
> + if (ret)
> + return ERR_PTR(ret);
> +
> + if (count == 0) {
> + kfree(entries);
> + return ERR_PTR(-ENOENT);
> + }
> +
> + info = kzalloc(sizeof(*info), GFP_KERNEL);
> + if (!info) {
> + metafs_free_entries(entries, count);
> + return ERR_PTR(-ENOMEM);
> + }
> +
> + info->type = METAFS_OPTION_VALUES;
> + info->opt_values.entries = entries;
> + info->opt_values.count = count;
> +
> + inode = metafs_get_inode(dir->i_sb, S_IFDIR | 0555, &metafs_option_values_dir_iops);
> + if (!inode) {
> + metafs_free_entries(entries, count);
> + kfree(info);
> + return ERR_PTR(-ENOMEM);
> + }
> +
> + dentry->d_fsdata = info;
> + d_add(dentry, inode);
> + d_mark_dontcache(inode);
> + return NULL;
> +}
> +
> +/*
> + * Lookup in option-specific directory (e.g., /mount/options/lowerdir/)
> + * Return numbered entries (0, 1, 2, ...) as symlinks
> + */
> +static struct dentry *metafs_option_values_lookup(struct inode *dir,
> + struct dentry *dentry,
> + unsigned int flags)
> +{
> + const char *target_name = dentry->d_name.name;
> + struct metafs_dentry_info *parent_info, *info;
> + struct metafs_option_entry *entry;
> + struct inode *inode;
> + unsigned long index;
> + int err;
> +
> + parent_info = dentry->d_parent->d_fsdata;
> + if (WARN_ON(!parent_info || parent_info->type != METAFS_OPTION_VALUES))
> + return ERR_PTR(-ENOENT);
> +
> + if (target_name[0] == '0' && target_name[1])
> + return ERR_PTR(-ENOENT);
> +
> + err = kstrtoul(target_name, 10, &index);
> + if (err || index >= parent_info->opt_values.count)
> + return ERR_PTR(-ENOENT);
> +
> + entry = &parent_info->opt_values.entries[index];
> +
> + info = kzalloc(sizeof(*info), GFP_KERNEL);
> + if (!info)
> + return ERR_PTR(-ENOMEM);
> +
> + info->type = METAFS_SYMLINK;
> + if (entry->value) {
> + info->symlink.value = kstrdup(entry->value, GFP_KERNEL);
> + if (!info->symlink.value) {
> + kfree(info);
> + return ERR_PTR(-ENOMEM);
> + }
> + }
> + if (entry->path.mnt) {
> + info->symlink.path = entry->path;
> + path_get(&info->symlink.path);
> + }
> +
> + inode = metafs_get_inode(dir->i_sb, S_IFLNK | 0777, NULL);
> + if (!inode) {
> + kfree(info->symlink.value);
> + if (info->symlink.path.mnt)
> + path_put(&info->symlink.path);
> + kfree(info);
> + return ERR_PTR(-ENOMEM);
> + }
> +
> + dentry->d_fsdata = info;
> + d_add(dentry, inode);
> + d_mark_dontcache(inode);
> + return NULL;
> +}
> +
> +/*
> + * Helper to get base file from any metafs dentry
> + */
> +static struct file *metafs_get_base_file(struct dentry *dentry)
> +{
> + while (dentry) {
> + struct metafs_dentry_info *info = dentry->d_fsdata;
> +
> + if (info && info->type == METAFS_ROOT)
> + return info->root.base;
> + if (dentry == dentry->d_parent)
> + break;
> + dentry = dentry->d_parent;
> + }
> + return NULL;
> +}
> +
> +/*
> + * Create the root directory structure for a metadata open
> + */
> +static struct dentry *metafs_create_root(struct file *base)
> +{
> + struct inode *inode;
> + struct dentry *root;
> + struct super_block *sb = metafs_mnt->mnt_sb;
> + struct metafs_dentry_info *info;
> +
> + info = kzalloc(sizeof(*info), GFP_KERNEL);
> + if (!info)
> + return ERR_PTR(-ENOMEM);
> +
> + info->type = METAFS_ROOT;
> + info->root.base = get_file(base);
> +
> + inode = metafs_get_inode(sb, S_IFDIR | 0555, &metafs_root_dir_iops);
> + if (!inode) {
> + fput(info->root.base);
> + kfree(info);
> + return ERR_PTR(-ENOMEM);
> + }
> +
> + root = d_alloc_anon(sb);
> + if (!root) {
> + iput(inode);
> + fput(info->root.base);
> + kfree(info);
> + return ERR_PTR(-ENOMEM);
> + }
> +
> + /* Store the info in d_fsdata before instantiating */
> + root->d_fsdata = info;
> + d_instantiate(root, inode);
> + d_mark_dontcache(inode);
> +
> + return root;
> +}
> +
> +/*
> + * Get metadata root path for a base file.
> + *
> + * This is called from path_init() when O_ALT is used.
> + * It returns a path in the metafs that serves as the root
> + * for the metadata lookup.
> + */
> +int metafs_get_root(struct file *base, struct path *path)
> +{
> + struct dentry *root;
> +
> + if (!base)
> + return -EBADF;
> +
> + /* Create the metadata root for this base file */
> + root = metafs_create_root(base);
> + if (IS_ERR(root))
> + return PTR_ERR(root);
> +
> + /*
> + * Return the path to the metadata root.
> + * Normal VFS lookup will take over from here.
> + */
> + path->mnt = mntget(metafs_mnt);
> + path->dentry = root;
> +
> + return 0;
> +}
> +EXPORT_SYMBOL_GPL(metafs_get_root);
> +
> +static int __init metafs_init(void)
> +{
> + int err;
> +
> + err = register_filesystem(&metafs_type);
> + if (err)
> + return err;
> +
> + metafs_mnt = kern_mount(&metafs_type);
> + if (IS_ERR(metafs_mnt)) {
> + err = PTR_ERR(metafs_mnt);
> + unregister_filesystem(&metafs_type);
> + return err;
> + }
> + set_default_d_op(metafs_mnt->mnt_sb, &metafs_dentry_ops);
> +
> + return 0;
> +}
> +
> +fs_initcall(metafs_init);
> diff --git a/fs/metafs/metafs.h b/fs/metafs/metafs.h
> new file mode 100644
> index 000000000000..d257714260a7
> --- /dev/null
> +++ b/fs/metafs/metafs.h
> @@ -0,0 +1,15 @@
> +/* SPDX-License-Identifier: GPL-2.0 */
> +#ifndef _LINUX_METAFS_H
> +#define _LINUX_METAFS_H
> +
> +struct path;
> +struct file;
> +
> +/*
> + * Get metadata root path for a base file.
> + * Called from path_init() when O_ALT flag is used.
> + * Returns 0 on success with path filled in, -errno on error.
> + */
> +int metafs_get_root(struct file *base, struct path *path);
> +
> +#endif /* _LINUX_METAFS_H */
> diff --git a/fs/mount.h b/fs/mount.h
> index 94fcc306d21e..69a953bc112b 100644
> --- a/fs/mount.h
> +++ b/fs/mount.h
> @@ -65,7 +65,6 @@ struct mount {
> struct mount * __aligned(1) *mnt_pprev_for_sb;
> /* except that LSB of pprev is stolen */
> #define WRITE_HOLD 1 /* ... for use by mnt_hold_writers() */
> - const char *mnt_devname; /* Name of device e.g. /dev/dsk/hda1 */
> struct list_head mnt_list;
> struct list_head mnt_expire; /* link in fs-specific expiry list */
> struct list_head mnt_share; /* circular list of shared mounts */
> diff --git a/fs/namei.c b/fs/namei.c
> index 19ce43c9a6e6..437a14c432e6 100644
> --- a/fs/namei.c
> +++ b/fs/namei.c
> @@ -45,6 +45,7 @@
>
> #include "internal.h"
> #include "mount.h"
> +#include "metafs/metafs.h"
>
> /* [Feb-1997 T. Schoebel-Theuer]
> * Fundamental changes in the pathname lookup mechanisms (namei)
> @@ -2753,6 +2754,21 @@ static const char *path_init(struct nameidata *nd, unsigned flags)
> return ERR_PTR(-ENOENT);
> }
>
> + if (unlikely(flags & LOOKUP_ALT)) {
> + if (flags & LOOKUP_RCU)
> + return ERR_PTR(-ECHILD);
> +
> + /* Handle O_ALT: jump to metafs root */
> + error = metafs_get_root(fd_file(f), &nd->path);
> + if (error)
> + return ERR_PTR(error);
> +
> + nd->inode = nd->path.dentry->d_inode;
> + nd->state |= ND_JUMPED;
> +
> + return s;
> + }
> +
> dentry = fd_file(f)->f_path.dentry;
>
> if (*s && unlikely(!d_can_lookup(dentry)))
> diff --git a/fs/namespace.c b/fs/namespace.c
> index 3d5cd5bf3b05..62cc71c401a2 100644
> --- a/fs/namespace.c
> +++ b/fs/namespace.c
> @@ -282,7 +282,7 @@ int mnt_get_count(struct mount *mnt)
> #endif
> }
>
> -static struct mount *alloc_vfsmnt(const char *name)
> +static struct mount *alloc_vfsmnt(void)
> {
> struct mount *mnt = kmem_cache_zalloc(mnt_cache, GFP_KERNEL);
> if (mnt) {
> @@ -292,18 +292,10 @@ static struct mount *alloc_vfsmnt(const char *name)
> if (err)
> goto out_free_cache;
>
> - if (name)
> - mnt->mnt_devname = kstrdup_const(name,
> - GFP_KERNEL_ACCOUNT);
> - else
> - mnt->mnt_devname = "none";
> - if (!mnt->mnt_devname)
> - goto out_free_id;
> -
> #ifdef CONFIG_SMP
> mnt->mnt_pcp = alloc_percpu(struct mnt_pcp);
> if (!mnt->mnt_pcp)
> - goto out_free_devname;
> + goto out_free_id;
>
> this_cpu_add(mnt->mnt_pcp->mnt_count, 1);
> #else
> @@ -328,11 +320,9 @@ static struct mount *alloc_vfsmnt(const char *name)
> return mnt;
>
> #ifdef CONFIG_SMP
> -out_free_devname:
> - kfree_const(mnt->mnt_devname);
> -#endif
> out_free_id:
> mnt_free_id(mnt);
> +#endif
> out_free_cache:
> kmem_cache_free(mnt_cache, mnt);
> return NULL;
> @@ -725,7 +715,6 @@ int sb_prepare_remount_readonly(struct super_block *sb)
> static void free_vfsmnt(struct mount *mnt)
> {
> mnt_idmap_put(mnt_idmap(&mnt->mnt));
> - kfree_const(mnt->mnt_devname);
> #ifdef CONFIG_SMP
> free_percpu(mnt->mnt_pcp);
> #endif
> @@ -1180,7 +1169,7 @@ struct vfsmount *vfs_create_mount(struct fs_context *fc)
> if (!fc->root)
> return ERR_PTR(-EINVAL);
>
> - mnt = alloc_vfsmnt(fc->source);
> + mnt = alloc_vfsmnt();
> if (!mnt)
> return ERR_PTR(-ENOMEM);
>
> @@ -1248,7 +1237,7 @@ static struct mount *clone_mnt(struct mount *old, struct dentry *root,
> struct mount *mnt;
> int err;
>
> - mnt = alloc_vfsmnt(old->mnt_devname);
> + mnt = alloc_vfsmnt();
> if (!mnt)
> return ERR_PTR(-ENOMEM);
>
> @@ -2957,16 +2946,12 @@ static int do_change_type(const struct path *path, int ms_flags)
> static inline bool may_copy_tree(const struct path *path)
> {
> struct mount *mnt = real_mount(path->mnt);
> - const struct dentry_operations *d_op;
>
> if (check_mnt(mnt))
> return true;
>
> - d_op = path->dentry->d_op;
> - if (d_op == &ns_dentry_operations)
> - return true;
> -
> - if (d_op == &pidfs_dentry_operations)
> + /* Make an exception for internal mounts explicitly marked as clonable */
> + if (mnt->mnt_ns == MNT_NS_INTERNAL && mnt->mnt.mnt_flags & MNT_CLONABLE)
> return true;
>
> if (!is_mounted(path->mnt))
> @@ -3201,7 +3186,7 @@ static struct file *vfs_open_tree(int dfd, const char __user *filename, unsigned
>
> if (flags & ~(AT_EMPTY_PATH | AT_NO_AUTOMOUNT | AT_RECURSIVE |
> AT_SYMLINK_NOFOLLOW | OPEN_TREE_CLONE |
> - OPEN_TREE_CLOEXEC | OPEN_TREE_NAMESPACE))
> + OPEN_TREE_CLOEXEC | OPEN_TREE_NAMESPACE | OPEN_TREE_ALT))
> return ERR_PTR(-EINVAL);
>
> if ((flags & (AT_RECURSIVE | OPEN_TREE_CLONE | OPEN_TREE_NAMESPACE)) ==
> @@ -3215,6 +3200,8 @@ static struct file *vfs_open_tree(int dfd, const char __user *filename, unsigned
> lookup_flags &= ~LOOKUP_AUTOMOUNT;
> if (flags & AT_SYMLINK_NOFOLLOW)
> lookup_flags &= ~LOOKUP_FOLLOW;
> + if (flags & OPEN_TREE_ALT)
> + lookup_flags |= LOOKUP_ALT;
>
> /*
> * If we create a new mount namespace with the cloned mount tree we
> @@ -5387,7 +5374,6 @@ static void statmount_fs_subtype(struct kstatmount *s, struct seq_file *seq)
> static int statmount_sb_source(struct kstatmount *s, struct seq_file *seq)
> {
> struct super_block *sb = s->mnt->mnt_sb;
> - struct mount *r = real_mount(s->mnt);
>
> if (sb->s_op->show_devname) {
> size_t start = seq->count;
> @@ -5405,7 +5391,7 @@ static int statmount_sb_source(struct kstatmount *s, struct seq_file *seq)
> seq->count = start;
> seq_commit(seq, string_unescape_inplace(seq->buf + start, UNESCAPE_OCTAL));
> } else {
> - seq_puts(seq, r->mnt_devname);
> + seq_puts(seq, sb->s_devname);
> }
> return 0;
> }
> @@ -5416,6 +5402,40 @@ static void statmount_mnt_ns_id(struct kstatmount *s, struct mnt_namespace *ns)
> s->sm.mnt_ns_id = ns->ns.ns_id;
> }
>
> +void sb_add_option(struct sb_opt_ctx *ctx, const char *name, const char *value)
> +{
> + sb_add_option_path(ctx, name, value, NULL);
> +}
> +
> +void sb_add_option_path(struct sb_opt_ctx *ctx, const char *name, const char *value,
> + const struct path *path)
> +{
> + ctx->actor(ctx, name, value, path);
> +}
> +
> +static void do_show_option(struct sb_opt_ctx *ctx, const char *name, const char *value,
> + const struct path *path)
> +{
> + if (strcmp(name, "source") != 0)
> + seq_show_option(ctx->data, name, value);
> +}
> +
> +int mnt_show_options(struct seq_file *seq, struct vfsmount *mnt)
> +{
> + struct super_block *sb = mnt->mnt_sb;
> +
> + if (sb->s_op->show_options)
> + return sb->s_op->show_options(seq, mnt->mnt_root);
> +
> + if (sb->s_op->get_options) {
> + struct sb_opt_ctx ctx = { .data = seq, .actor = do_show_option };
> +
> + return sb->s_op->get_options(&ctx, mnt->mnt_root);
> + }
> +
> + return 0;
> +}
> +
> static int statmount_mnt_opts(struct kstatmount *s, struct seq_file *seq)
> {
> struct vfsmount *mnt = s->mnt;
> @@ -5427,11 +5447,9 @@ static int statmount_mnt_opts(struct kstatmount *s, struct seq_file *seq)
> if (err)
> return err;
>
> - if (sb->s_op->show_options) {
> - err = sb->s_op->show_options(seq, mnt->mnt_root);
> - if (err)
> - return err;
> - }
> + err = mnt_show_options(seq, mnt);
> + if (err)
> + return err;
>
> if (unlikely(seq_has_overflowed(seq)))
> return -EAGAIN;
> @@ -5479,14 +5497,10 @@ static inline int statmount_opt_process(struct seq_file *seq, size_t start)
> static int statmount_opt_array(struct kstatmount *s, struct seq_file *seq)
> {
> struct vfsmount *mnt = s->mnt;
> - struct super_block *sb = mnt->mnt_sb;
> size_t start = seq->count;
> int err;
>
> - if (!sb->s_op->show_options)
> - return 0;
> -
> - err = sb->s_op->show_options(seq, mnt->mnt_root);
> + err = mnt_show_options(seq, mnt);
> if (err)
> return err;
>
> diff --git a/fs/nsfs.c b/fs/nsfs.c
> index c3b6ae76594a..e110130d0760 100644
> --- a/fs/nsfs.c
> +++ b/fs/nsfs.c
> @@ -684,6 +684,7 @@ void __init nsfs_init(void)
> if (IS_ERR(nsfs_mnt))
> panic("can't set nsfs up\n");
> nsfs_mnt->mnt_sb->s_flags &= ~SB_NOUSER;
> + nsfs_mnt->mnt_flags |= MNT_CLONABLE;
> nsfs_root_path.mnt = nsfs_mnt;
> nsfs_root_path.dentry = nsfs_mnt->mnt_root;
> }
> diff --git a/fs/open.c b/fs/open.c
> index 408925d7bd0b..3a081a9e3062 100644
> --- a/fs/open.c
> +++ b/fs/open.c
> @@ -1131,7 +1131,8 @@ struct file *kernel_file_open(const struct path *path, int flags,
> EXPORT_SYMBOL_GPL(kernel_file_open);
>
> #define WILL_CREATE(flags) (flags & (O_CREAT | __O_TMPFILE))
> -#define O_PATH_FLAGS (O_DIRECTORY | O_NOFOLLOW | O_PATH | O_CLOEXEC | O_EMPTYPATH)
> +#define O_PATH_FLAGS (O_DIRECTORY | O_NOFOLLOW | O_PATH | O_CLOEXEC | O_EMPTYPATH | \
> + O_ALT)
>
> inline struct open_how build_open_how(int flags, umode_t mode)
> {
> @@ -1283,6 +1284,8 @@ inline int build_open_flags(const struct open_how *how, struct open_flags *op)
> lookup_flags |= LOOKUP_FOLLOW;
> if (flags & O_EMPTYPATH)
> lookup_flags |= LOOKUP_EMPTY;
> + if (flags & O_ALT)
> + lookup_flags |= LOOKUP_ALT;
>
> if (how->resolve & RESOLVE_NO_XDEV)
> lookup_flags |= LOOKUP_NO_XDEV;
> diff --git a/fs/overlayfs/params.c b/fs/overlayfs/params.c
> index c93fcaa45d4a..c5a9e858e366 100644
> --- a/fs/overlayfs/params.c
> +++ b/fs/overlayfs/params.c
> @@ -1046,10 +1046,11 @@ int ovl_fs_params_verify(const struct ovl_fs_context *ctx,
> * Prints the mount options for a given superblock.
> * Returns zero; does not fail.
> */
> -int ovl_show_options(struct seq_file *m, struct dentry *dentry)
> +int ovl_get_options(struct sb_opt_ctx *ctx, struct dentry *dentry)
> {
> struct super_block *sb = dentry->d_sb;
> struct ovl_fs *ofs = OVL_FS(sb);
> + struct path upperpath;
> size_t nr, nr_merged_lower, nr_lower = 0;
> char **lowerdirs = ofs->config.lowerdirs;
>
> @@ -1062,43 +1063,47 @@ int ovl_show_options(struct seq_file *m, struct dentry *dentry)
> * with the new lowerdir+ and datadir+ mount options.
> */
> if (lowerdirs[0]) {
> - seq_show_option(m, "lowerdir", lowerdirs[0]);
> + sb_add_option(ctx, "lowerdir", lowerdirs[0]);
> } else {
> nr_lower = ofs->numlayer;
> nr_merged_lower = nr_lower - ofs->numdatalayer;
> }
> for (nr = 1; nr < nr_lower; nr++) {
> + struct path lowerpath = {
> + .mnt = ofs->layers[nr].mnt,
> + .dentry = ofs->layers[nr].mnt->mnt_root,
> + };
> if (nr < nr_merged_lower)
> - seq_show_option(m, "lowerdir+", lowerdirs[nr]);
> + sb_add_option_path(ctx, "lowerdir+", lowerdirs[nr],
> + &lowerpath);
> else
> - seq_show_option(m, "datadir+", lowerdirs[nr]);
> + sb_add_option_path(ctx, "datadir+", lowerdirs[nr],
> + &lowerpath);
> }
> if (ofs->config.upperdir) {
> - seq_show_option(m, "upperdir", ofs->config.upperdir);
> - seq_show_option(m, "workdir", ofs->config.workdir);
> + ovl_path_upper(sb->s_root, &upperpath);
> + sb_add_option_path(ctx, "upperdir", ofs->config.upperdir, &upperpath);
> + sb_add_option(ctx, "workdir", ofs->config.workdir);
> }
> if (ofs->config.default_permissions)
> - seq_puts(m, ",default_permissions");
> + sb_add_option(ctx, "default_permissions", NULL);
> if (ofs->config.redirect_mode != ovl_redirect_mode_def())
> - seq_printf(m, ",redirect_dir=%s",
> - ovl_redirect_mode(&ofs->config));
> + sb_add_option(ctx, "redirect_dir", ovl_redirect_mode(&ofs->config));
> if (ofs->config.index != ovl_index_def)
> - seq_printf(m, ",index=%s", str_on_off(ofs->config.index));
> + sb_add_option(ctx, "index", str_on_off(ofs->config.index));
> if (ofs->config.uuid != ovl_uuid_def())
> - seq_printf(m, ",uuid=%s", ovl_uuid_mode(&ofs->config));
> + sb_add_option(ctx, "uuid", ovl_uuid_mode(&ofs->config));
> if (ofs->config.nfs_export != ovl_nfs_export_def)
> - seq_printf(m, ",nfs_export=%s",
> - str_on_off(ofs->config.nfs_export));
> + sb_add_option(ctx, "nfs_export", str_on_off(ofs->config.nfs_export));
> if (ofs->config.xino != ovl_xino_def() && !ovl_same_fs(ofs))
> - seq_printf(m, ",xino=%s", ovl_xino_mode(&ofs->config));
> + sb_add_option(ctx, "xino", ovl_xino_mode(&ofs->config));
> if (ofs->config.metacopy != ovl_metacopy_def)
> - seq_printf(m, ",metacopy=%s", str_on_off(ofs->config.metacopy));
> + sb_add_option(ctx, "metacopy", str_on_off(ofs->config.metacopy));
> if (ofs->config.fsync_mode != ovl_fsync_mode_def())
> - seq_printf(m, ",fsync=%s", ovl_fsync_mode(&ofs->config));
> + sb_add_option(ctx, "fsync", ovl_fsync_mode(&ofs->config));
> if (ofs->config.userxattr)
> - seq_puts(m, ",userxattr");
> + sb_add_option(ctx, "userxattr", NULL);
> if (ofs->config.verity_mode != ovl_verity_mode_def())
> - seq_printf(m, ",verity=%s",
> - ovl_verity_mode(&ofs->config));
> + sb_add_option(ctx, "verity", ovl_verity_mode(&ofs->config));
> return 0;
> }
> diff --git a/fs/overlayfs/params.h b/fs/overlayfs/params.h
> index ffd53cdd8482..2cf9c4f188a9 100644
> --- a/fs/overlayfs/params.h
> +++ b/fs/overlayfs/params.h
> @@ -40,5 +40,5 @@ int ovl_init_fs_context(struct fs_context *fc);
> void ovl_free_fs(struct ovl_fs *ofs);
> int ovl_fs_params_verify(const struct ovl_fs_context *ctx,
> struct ovl_config *config);
> -int ovl_show_options(struct seq_file *m, struct dentry *dentry);
> +int ovl_get_options(struct sb_opt_ctx *ctx, struct dentry *dentry);
> const char *ovl_xino_mode(struct ovl_config *config);
> diff --git a/fs/overlayfs/super.c b/fs/overlayfs/super.c
> index 60f0b7ceef0a..4b57b2db0852 100644
> --- a/fs/overlayfs/super.c
> +++ b/fs/overlayfs/super.c
> @@ -302,7 +302,7 @@ static const struct super_operations ovl_super_operations = {
> .put_super = ovl_put_super,
> .sync_fs = ovl_sync_fs,
> .statfs = ovl_statfs,
> - .show_options = ovl_show_options,
> + .get_options = ovl_get_options,
> };
>
> #define OVL_WORKDIR_NAME "work"
> @@ -529,6 +529,7 @@ static int ovl_get_upper(struct super_block *sb, struct ovl_fs *ofs,
>
> /* Don't inherit atime flags */
> upper_mnt->mnt_flags &= ~(MNT_NOATIME | MNT_NODIRATIME | MNT_RELATIME);
> + upper_mnt->mnt_flags |= MNT_CLONABLE;
> upper_layer->mnt = upper_mnt;
> upper_layer->idx = 0;
> upper_layer->fsid = 0;
> @@ -1105,7 +1106,7 @@ static int ovl_get_layers(struct super_block *sb, struct ovl_fs *ofs,
> * Make lower layers R/O. That way fchmod/fchown on lower file
> * will fail instead of modifying lower fs.
> */
> - mnt->mnt_flags |= MNT_READONLY | MNT_NOATIME;
> + mnt->mnt_flags |= MNT_READONLY | MNT_NOATIME | MNT_CLONABLE;
>
> layers[ofs->numlayer].trap = trap;
> layers[ofs->numlayer].mnt = mnt;
> diff --git a/fs/pidfs.c b/fs/pidfs.c
> index aaa609ddab04..3843acd6594d 100644
> --- a/fs/pidfs.c
> +++ b/fs/pidfs.c
> @@ -840,7 +840,7 @@ static char *pidfs_dname(struct dentry *dentry, char *buffer, int buflen)
> return dynamic_dname(buffer, buflen, "anon_inode:[pidfd]");
> }
>
> -const struct dentry_operations pidfs_dentry_operations = {
> +static const struct dentry_operations pidfs_dentry_operations = {
> .d_dname = pidfs_dname,
> .d_prune = stashed_dentry_prune,
> };
> @@ -1151,6 +1151,8 @@ void __init pidfs_init(void)
> if (IS_ERR(pidfs_mnt))
> panic("Failed to mount pidfs pseudo filesystem");
>
> + pidfs_mnt->mnt_flags |= MNT_CLONABLE;
> +
> pidfs_root_path.mnt = pidfs_mnt;
> pidfs_root_path.dentry = pidfs_mnt->mnt_root;
> }
> diff --git a/fs/proc_namespace.c b/fs/proc_namespace.c
> index 5c555db68aa2..94bc4c9aa43f 100644
> --- a/fs/proc_namespace.c
> +++ b/fs/proc_namespace.c
> @@ -98,21 +98,27 @@ static void show_type(struct seq_file *m, struct super_block *sb)
> }
> }
>
> +static int show_devname(struct seq_file *m, struct vfsmount *mnt)
> +{
> + struct super_block *sb = mnt->mnt_sb;
> +
> + if (sb->s_op->show_devname)
> + return sb->s_op->show_devname(m, mnt->mnt_root);
> +
> + mangle(m, sb->s_devname);
> + return 0;
> +}
> +
> static int show_vfsmnt(struct seq_file *m, struct vfsmount *mnt)
> {
> struct proc_mounts *p = m->private;
> - struct mount *r = real_mount(mnt);
> struct path mnt_path = { .dentry = mnt->mnt_root, .mnt = mnt };
> struct super_block *sb = mnt_path.dentry->d_sb;
> int err;
>
> - if (sb->s_op->show_devname) {
> - err = sb->s_op->show_devname(m, mnt_path.dentry);
> - if (err)
> - goto out;
> - } else {
> - mangle(m, r->mnt_devname);
> - }
> + err = show_devname(m, mnt);
> + if (err)
> + goto out;
> seq_putc(m, ' ');
> /* mountpoints outside of chroot jail will give SEQ_SKIP on this */
> err = seq_path_root(m, &mnt_path, &p->root, " \t\n\\");
> @@ -125,8 +131,7 @@ static int show_vfsmnt(struct seq_file *m, struct vfsmount *mnt)
> if (err)
> goto out;
> show_vfsmnt_opts(m, mnt);
> - if (sb->s_op->show_options)
> - err = sb->s_op->show_options(m, mnt_path.dentry);
> + err = mnt_show_options(m, mnt);
> seq_puts(m, " 0 0\n");
> out:
> return err;
> @@ -172,19 +177,14 @@ static int show_mountinfo(struct seq_file *m, struct vfsmount *mnt)
> seq_puts(m, " - ");
> show_type(m, sb);
> seq_putc(m, ' ');
> - if (sb->s_op->show_devname) {
> - err = sb->s_op->show_devname(m, mnt->mnt_root);
> - if (err)
> - goto out;
> - } else {
> - mangle(m, r->mnt_devname);
> - }
> + err = show_devname(m, mnt);
> + if (err)
> + goto out;
> seq_puts(m, sb_rdonly(sb) ? " ro" : " rw");
> err = show_sb_opts(m, sb);
> if (err)
> goto out;
> - if (sb->s_op->show_options)
> - err = sb->s_op->show_options(m, mnt->mnt_root);
> + err = mnt_show_options(m, mnt);
> seq_putc(m, '\n');
> out:
> return err;
> @@ -193,21 +193,15 @@ static int show_mountinfo(struct seq_file *m, struct vfsmount *mnt)
> static int show_vfsstat(struct seq_file *m, struct vfsmount *mnt)
> {
> struct proc_mounts *p = m->private;
> - struct mount *r = real_mount(mnt);
> struct path mnt_path = { .dentry = mnt->mnt_root, .mnt = mnt };
> struct super_block *sb = mnt_path.dentry->d_sb;
> int err;
>
> /* device */
> seq_puts(m, "device ");
> - if (sb->s_op->show_devname) {
> - err = sb->s_op->show_devname(m, mnt_path.dentry);
> - if (err)
> - goto out;
> - } else {
> - mangle(m, r->mnt_devname);
> - }
> -
> + err = show_devname(m, mnt);
> + if (err)
> + goto out;
> /* mount point */
> seq_puts(m, " mounted on ");
> /* mountpoints outside of chroot jail will give SEQ_SKIP on this */
> diff --git a/fs/super.c b/fs/super.c
> index a8fd61136aaf..e5531cb88f89 100644
> --- a/fs/super.c
> +++ b/fs/super.c
> @@ -280,6 +280,7 @@ static void destroy_super_work(struct work_struct *work)
> security_sb_free(s);
> put_user_ns(s->s_user_ns);
> kfree(s->s_subtype);
> + kfree_const(s->s_devname);
> for (int i = 0; i < SB_FREEZE_LEVELS; i++)
> percpu_free_rwsem(&s->s_writers.rw_sem[i]);
> kfree(s);
> @@ -315,7 +316,7 @@ static void destroy_unused_super(struct super_block *s)
> * returns a pointer new superblock or %NULL if allocation had failed.
> */
> static struct super_block *alloc_super(struct file_system_type *type, int flags,
> - struct user_namespace *user_ns)
> + struct user_namespace *user_ns, const char *name)
> {
> struct super_block *s = kzalloc_obj(struct super_block);
> static const struct super_operations default_op;
> @@ -344,6 +345,13 @@ static struct super_block *alloc_super(struct file_system_type *type, int flags,
> */
> down_write_nested(&s->s_umount, SINGLE_DEPTH_NESTING);
>
> + if (name)
> + s->s_devname = kstrdup_const(name, GFP_KERNEL_ACCOUNT);
> + else
> + s->s_devname = "none";
> + if (!s->s_devname)
> + goto fail;
> +
> if (security_sb_alloc(s))
> goto fail;
>
> @@ -763,7 +771,7 @@ struct super_block *sget_fc(struct fs_context *fc,
> }
> if (!s) {
> spin_unlock(&sb_lock);
> - s = alloc_super(fc->fs_type, fc->sb_flags, user_ns);
> + s = alloc_super(fc->fs_type, fc->sb_flags, user_ns, fc->source);
> if (!s)
> return ERR_PTR(-ENOMEM);
> goto retry;
> diff --git a/include/linux/fcntl.h b/include/linux/fcntl.h
> index 6ad6b9e7a226..f942411c8ed7 100644
> --- a/include/linux/fcntl.h
> +++ b/include/linux/fcntl.h
> @@ -11,7 +11,7 @@
> (O_RDONLY | O_WRONLY | O_RDWR | O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC | \
> O_APPEND | O_NDELAY | O_NONBLOCK | __O_SYNC | O_DSYNC | \
> FASYNC | O_DIRECT | O_LARGEFILE | O_DIRECTORY | O_NOFOLLOW | \
> - O_NOATIME | O_CLOEXEC | O_PATH | __O_TMPFILE | O_EMPTYPATH)
> + O_NOATIME | O_CLOEXEC | O_PATH | __O_TMPFILE | O_EMPTYPATH | O_ALT)
>
> /* List of all valid flags for openat2(2)'s how->flags argument. */
> #define VALID_OPENAT2_FLAGS (VALID_OPEN_FLAGS | OPENAT2_REGULAR)
> diff --git a/include/linux/fs/super.h b/include/linux/fs/super.h
> index 405612678115..6f0d778940f9 100644
> --- a/include/linux/fs/super.h
> +++ b/include/linux/fs/super.h
> @@ -237,4 +237,14 @@ int thaw_super(struct super_block *super, enum freeze_holder who,
>
> int sb_init_dio_done_wq(struct super_block *sb);
>
> +struct sb_opt_ctx {
> + void (*actor)(struct sb_opt_ctx *ctx, const char *name, const char *value,
> + const struct path *path);
> + void *data;
> +};
> +
> +void sb_add_option(struct sb_opt_ctx *ctx, const char *name, const char *value);
> +void sb_add_option_path(struct sb_opt_ctx *ctx, const char *name, const char *value,
> + const struct path *path);
> +
> #endif /* _LINUX_FS_SUPER_H */
> diff --git a/include/linux/fs/super_types.h b/include/linux/fs/super_types.h
> index ef7941e9dc79..f205759af1a6 100644
> --- a/include/linux/fs/super_types.h
> +++ b/include/linux/fs/super_types.h
> @@ -36,6 +36,7 @@ struct workqueue_struct;
> struct writeback_control;
> struct xattr_handler;
> struct fserror_event;
> +struct sb_opt_ctx;
>
> extern struct super_block *blockdev_superblock;
>
> @@ -99,6 +100,7 @@ struct super_operations {
> int (*statfs)(struct dentry *dentry, struct kstatfs *kstatfs);
> void (*umount_begin)(struct super_block *sb);
>
> + int (*get_options)(struct sb_opt_ctx *ctx, struct dentry *dentry);
> int (*show_options)(struct seq_file *seq, struct dentry *dentry);
> int (*show_devname)(struct seq_file *seq, struct dentry *dentry);
> int (*show_path)(struct seq_file *seq, struct dentry *dentry);
> @@ -283,6 +285,8 @@ struct super_block {
> */
> atomic_t s_isw_nr_in_flight;
> #endif
> + /* Name of device e.g. /dev/dsk/hda1 */
> + const char *s_devname;
> } __randomize_layout;
>
> /*
> diff --git a/include/linux/mount.h b/include/linux/mount.h
> index acfe7ef86a1b..d071855ca4ed 100644
> --- a/include/linux/mount.h
> +++ b/include/linux/mount.h
> @@ -44,6 +44,7 @@ enum mount_flags {
> MNT_LOCKED = 0x800000,
> MNT_DOOMED = 0x1000000,
> MNT_SYNC_UMOUNT = 0x2000000,
> + MNT_CLONABLE = 0x4000000,
> MNT_UMOUNT = 0x8000000,
>
> MNT_USER_SETTABLE_MASK = MNT_NOSUID | MNT_NODEV | MNT_NOEXEC
> @@ -52,7 +53,7 @@ enum mount_flags {
> MNT_ATIME_MASK = MNT_NOATIME | MNT_NODIRATIME | MNT_RELATIME,
>
> MNT_INTERNAL_FLAGS = MNT_INTERNAL | MNT_DOOMED |
> - MNT_SYNC_UMOUNT | MNT_LOCKED
> + MNT_SYNC_UMOUNT | MNT_LOCKED | MNT_CLONABLE
> };
>
> struct vfsmount {
> diff --git a/include/linux/namei.h b/include/linux/namei.h
> index ebe6e29f7e93..c21dc6536900 100644
> --- a/include/linux/namei.h
> +++ b/include/linux/namei.h
> @@ -25,7 +25,8 @@ enum { MAX_NESTED_LINKS = 8 };
> #define LOOKUP_RCU BIT(8) /* RCU pathwalk mode; semi-internal */
> #define LOOKUP_CACHED BIT(9) /* Only do cached lookup */
> #define LOOKUP_PARENT BIT(10) /* Looking up final parent in path */
> -/* 5 spare bits for pathwalk */
> +#define LOOKUP_ALT BIT(11) /* Look up in alternative namespace */
> +/* 4 spare bits for pathwalk */
>
> /* These tell filesystem methods that we are dealing with the final component... */
> #define LOOKUP_OPEN BIT(16) /* ... in open */
> diff --git a/include/linux/pidfs.h b/include/linux/pidfs.h
> index 0abf7da9ab23..fab6f48ff203 100644
> --- a/include/linux/pidfs.h
> +++ b/include/linux/pidfs.h
> @@ -15,7 +15,6 @@ void pidfs_exit(struct task_struct *tsk);
> #ifdef CONFIG_COREDUMP
> void pidfs_coredump(const struct coredump_params *cprm);
> #endif
> -extern const struct dentry_operations pidfs_dentry_operations;
> int pidfs_register_pid_gfp(struct pid *pid, gfp_t gfp);
>
> /**
> diff --git a/include/uapi/asm-generic/fcntl.h b/include/uapi/asm-generic/fcntl.h
> index 883cfd7de2ef..70dbbac1c520 100644
> --- a/include/uapi/asm-generic/fcntl.h
> +++ b/include/uapi/asm-generic/fcntl.h
> @@ -96,6 +96,10 @@
> #define O_EMPTYPATH (1 << 26) /* allow empty path */
> #endif
>
> +#ifndef O_ALT
> +#define O_ALT (1 << 27) /* Open in alternative namespace */
> +#endif
> +
> /* a horrid kludge trying to make sure that this will fail on old kernels */
> #define O_TMPFILE (__O_TMPFILE | O_DIRECTORY)
>
> diff --git a/include/uapi/linux/mount.h b/include/uapi/linux/mount.h
> index 2204708dbf7a..6e794d214cf9 100644
> --- a/include/uapi/linux/mount.h
> +++ b/include/uapi/linux/mount.h
> @@ -63,6 +63,7 @@
> */
> #define OPEN_TREE_CLONE (1 << 0) /* Clone the target tree and attach the clone */
> #define OPEN_TREE_NAMESPACE (1 << 1) /* Clone the target tree into a new mount namespace */
> +#define OPEN_TREE_ALT (1 << 2) /* Lookup in alternative namespace */
> #define OPEN_TREE_CLOEXEC O_CLOEXEC /* Close the file on execve() */
>
> /*
> diff --git a/scripts/gdb/linux/proc.py b/scripts/gdb/linux/proc.py
> index 65dd1bd12964..5aaf2dd8db05 100644
> --- a/scripts/gdb/linux/proc.py
> +++ b/scripts/gdb/linux/proc.py
> @@ -174,9 +174,6 @@ values of that process namespace"""
> "mount", "super_block", "devname", "pathname", "fstype"))
>
> for mnt in rbtree.rb_inorder_for_each_entry(namespace['mounts'], mount_ptr_type, "mnt_node"):
> - devname = mnt['mnt_devname'].string()
> - devname = devname if devname else "none"
> -
> pathname = ""
> parent = mnt
> while True:
> @@ -190,6 +187,8 @@ values of that process namespace"""
> pathname = "/"
>
> superblock = mnt['mnt']['mnt_sb']
> + devname = superblock['s_devname'].string()
> + devname = devname if devname else "none"
> fstype = superblock['s_type']['name'].string()
> s_flags = int(superblock['s_flags'])
> m_flags = int(mnt['mnt']['mnt_flags'])
Thanks for the patch! I've tested it for my use case, and it works
well.
I had to amend the following diff though:
diff --git a/fs/namespace.c b/fs/namespace.c
index 62cc71c401a2..cf528e92e17e 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -5406,12 +5406,14 @@ void sb_add_option(struct sb_opt_ctx *ctx, const char *name, const char *value)
{
sb_add_option_path(ctx, name, value, NULL);
}
+EXPORT_SYMBOL_GPL(sb_add_option);
void sb_add_option_path(struct sb_opt_ctx *ctx, const char *name, const char *value,
const struct path *path)
{
ctx->actor(ctx, name, value, path);
}
+EXPORT_SYMBOL_GPL(sb_add_option_path);
static void do_show_option(struct sb_opt_ctx *ctx, const char *name, const char *value,
const struct path *path)
Regards,
Giuseppe
^ 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
* [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 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 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 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 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 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 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 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 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 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 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 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 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 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,
+ ¤t->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 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 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 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 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, ¤t->pending.signal,
+ ¤t->signal->shared_pending.signal);
+ sigandnsets(&pending, &pending, ¤t->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 19/24] file: expose spawn file-action helpers
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 apply close_range() and fchdir() while
preparing a child. Split the syscall bodies into internal helpers so
callers in fs can reuse existing validation and state updates. Keep the
helpers internal to fs rather than adding new global interfaces.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
fs/file.c | 11 ++++++++---
fs/internal.h | 2 ++
fs/open.c | 7 ++++++-
3 files changed, 16 insertions(+), 4 deletions(-)
diff --git a/fs/file.c b/fs/file.c
index 628ca07dc4b17..4f4cb1d5752c2 100644
--- a/fs/file.c
+++ b/fs/file.c
@@ -805,7 +805,7 @@ static inline void __range_close(struct files_struct *files, unsigned int fd,
}
/**
- * sys_close_range() - Close all file descriptors in a given range.
+ * do_close_range() - Close all file descriptors in a given range.
*
* @fd: starting file descriptor to close
* @max_fd: last file descriptor to close
@@ -815,8 +815,7 @@ static inline void __range_close(struct files_struct *files, unsigned int fd,
* from @fd up to and including @max_fd are closed.
* Currently, errors to close a given file descriptor are ignored.
*/
-SYSCALL_DEFINE3(close_range, unsigned int, fd, unsigned int, max_fd,
- unsigned int, flags)
+int do_close_range(unsigned int fd, unsigned int max_fd, unsigned int flags)
{
struct task_struct *me = current;
struct files_struct *cur_fds = me->files, *fds = NULL;
@@ -867,6 +866,12 @@ SYSCALL_DEFINE3(close_range, unsigned int, fd, unsigned int, max_fd,
return 0;
}
+SYSCALL_DEFINE3(close_range, unsigned int, fd, unsigned int, max_fd,
+ unsigned int, flags)
+{
+ return do_close_range(fd, max_fd, flags);
+}
+
/**
* file_close_fd - return file associated with fd
* @fd: file descriptor to retrieve file for
diff --git a/fs/internal.h b/fs/internal.h
index 71cc43e72b33e..25492194e2883 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -198,6 +198,8 @@ extern struct file *do_file_open_root(const struct path *,
extern struct open_how build_open_how(int flags, umode_t mode);
extern int build_open_flags(const struct open_how *how, struct open_flags *op);
struct file *file_close_fd_locked(struct files_struct *files, unsigned fd);
+int do_close_range(unsigned int fd, unsigned int max_fd, unsigned int flags);
+int do_fchdir(unsigned int fd);
int do_ftruncate(struct file *file, loff_t length, unsigned int flags);
int chmod_common(const struct path *path, umode_t mode);
diff --git a/fs/open.c b/fs/open.c
index 408925d7bd0b7..bf9d7bdcf487c 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -568,7 +568,7 @@ SYSCALL_DEFINE1(chdir, const char __user *, filename)
return error;
}
-SYSCALL_DEFINE1(fchdir, unsigned int, fd)
+int do_fchdir(unsigned int fd)
{
CLASS(fd_raw, f)(fd);
int error;
@@ -585,6 +585,11 @@ SYSCALL_DEFINE1(fchdir, unsigned int, fd)
return error;
}
+SYSCALL_DEFINE1(fchdir, unsigned int, fd)
+{
+ return do_fchdir(fd);
+}
+
SYSCALL_DEFINE1(chroot, const char __user *, filename)
{
struct path path;
--
2.52.0
^ permalink raw reply related
* [RFC PATCH 20/24] pidfd: add initial spawn file actions
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 pidfd_spawn_run() to apply an ordered list of DUP2, FCHDIR, and
CLOSE_RANGE actions before exec. Copy and validate the complete action
list before creating the child so malformed input cannot leave a
partially started process. Use a versioned action stride so userspace can
pass larger future records. Bound the payload by 64 KiB instead of a fixed
action count, keeping allocation bounded without imposing an arbitrary
limit on small records.
Apply actions before completing the delayed executable name. This makes an
FCHDIR action establish the cwd used by both relative executable lookup and
the child transaction's AUDIT_CWD record.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
fs/pidfd_spawn.c | 171 ++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 163 insertions(+), 8 deletions(-)
diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
index 0e52503976c0c..3e46a12c0ba07 100644
--- a/fs/pidfd_spawn.c
+++ b/fs/pidfd_spawn.c
@@ -5,9 +5,11 @@
#include <asm/syscall.h>
#include <linux/audit.h>
+#include <linux/close_range.h>
#include <linux/cgroup.h>
#include <linux/completion.h>
#include <linux/cred.h>
+#include <linux/fdtable.h>
#include <linux/file.h>
#include <linux/fs.h>
#include <linux/mm.h>
@@ -23,15 +25,18 @@
#include <linux/sched/signal.h>
#include <linux/sched/task.h>
#include <linux/security.h>
+#include <linux/sizes.h>
#include <linux/slab.h>
#include <linux/syscalls.h>
#include <linux/task_work.h>
#include <linux/uaccess.h>
+#include <linux/vmalloc.h>
#include <uapi/linux/pidfd.h>
#include <uapi/linux/wait.h>
#include <trace/events/sched.h>
#include "exec_internal.h"
+#include "internal.h"
static inline void __user *pidfd_spawn_user_ptr(__u64 p)
{
@@ -59,6 +64,9 @@ static inline struct user_arg_ptr pidfd_spawn_user_arg(__u64 p)
#define PIDFD_SPAWN_EXIT_FAILURE (127 << 8)
#define PIDFD_SPAWN_MAX_CONFIG_KEY_SIZE 256
+/* Bound allocation without imposing a fixed number of action records. */
+#define PIDFD_SPAWN_MAX_ACTIONS_SIZE SZ_64K
+
DEFINE_FREE(pidfd_spawn_dismiss_filename, struct delayed_filename,
dismiss_delayed_filename(&_T))
@@ -82,9 +90,11 @@ struct pidfd_spawn_state {
struct pid_namespace *creator_pid_ns;
struct delayed_filename filename;
char *staged_path;
+ struct pidfd_spawn_action *actions;
struct user_arg_ptr argv;
struct user_arg_ptr envp;
unsigned long audit_args[4];
+ unsigned int nr_actions;
int audit_syscall;
int result;
enum pidfd_spawn_status status;
@@ -139,6 +149,7 @@ static void pidfd_spawn_free_state(struct pidfd_spawn_state *state)
put_cred(state->creator_cred);
if (state->creator_pid_ns)
put_pid_ns(state->creator_pid_ns);
+ kvfree(state->actions);
put_pid(state->pid);
kfree(state);
}
@@ -192,6 +203,7 @@ static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
struct filename *filename, int result)
{
const struct cred *creator_cred;
+ struct pidfd_spawn_action *actions;
struct mm_struct *creator_mm;
struct pid_namespace *creator_pid_ns;
char *staged_path;
@@ -201,14 +213,17 @@ static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
* after the child no longer needs the shared setup payload.
*/
mutex_lock(&state->lock);
+ actions = state->actions;
creator_mm = state->creator_mm;
creator_cred = state->creator_cred;
creator_pid_ns = state->creator_pid_ns;
staged_path = state->staged_path;
+ state->actions = NULL;
state->creator_mm = NULL;
state->creator_cred = NULL;
state->creator_pid_ns = NULL;
state->staged_path = NULL;
+ state->nr_actions = 0;
state->argv = native_arg(NULL);
state->envp = native_arg(NULL);
memset(state->audit_args, 0, sizeof(state->audit_args));
@@ -227,6 +242,7 @@ static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
if (creator_pid_ns)
put_pid_ns(creator_pid_ns);
kfree(staged_path);
+ kvfree(actions);
}
static bool pidfd_spawn_cancel(struct pidfd_spawn_state *state,
@@ -441,6 +457,114 @@ static bool pidfd_spawn_setup_done(struct pidfd_spawn_state *state)
return done;
}
+static int pidfd_spawn_validate_action(const struct pidfd_spawn_action *action)
+{
+ if (action->reserved[0] || action->reserved[1])
+ return -EINVAL;
+
+ switch (action->type) {
+ case PIDFD_SPAWN_ACTION_DUP2:
+ if (action->flags)
+ return -EINVAL;
+ return 0;
+ case PIDFD_SPAWN_ACTION_FCHDIR:
+ if (action->flags || action->newfd)
+ return -EINVAL;
+ return 0;
+ case PIDFD_SPAWN_ACTION_CLOSE_RANGE:
+ if (action->newfd < action->fd)
+ return -EINVAL;
+ if (action->flags & ~(CLOSE_RANGE_UNSHARE |
+ CLOSE_RANGE_CLOEXEC))
+ return -EINVAL;
+ return 0;
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int pidfd_spawn_copy_actions(struct pidfd_spawn_action **actions,
+ __u64 uactions, unsigned int nr_actions,
+ unsigned int action_size)
+{
+ const char __user *uptr;
+ struct pidfd_spawn_action *kactions __free(kvfree) = NULL;
+ unsigned int i;
+ int ret;
+
+ if (!nr_actions)
+ return 0;
+
+ BUILD_BUG_ON(sizeof(struct pidfd_spawn_action) !=
+ PIDFD_SPAWN_ACTION_SIZE_VER0);
+
+ uptr = pidfd_spawn_user_ptr(uactions);
+ kactions = kvmalloc_array(nr_actions, sizeof(*kactions),
+ GFP_KERNEL_ACCOUNT);
+ if (!kactions)
+ return -ENOMEM;
+
+ for (i = 0; i < nr_actions; i++) {
+ ret = copy_struct_from_user(&kactions[i], sizeof(kactions[i]),
+ uptr + (size_t)i * action_size,
+ action_size);
+ if (ret)
+ return ret;
+
+ ret = pidfd_spawn_validate_action(&kactions[i]);
+ if (ret)
+ return ret;
+ }
+
+ *actions = no_free_ptr(kactions);
+ return 0;
+}
+
+static int pidfd_spawn_do_dup2(unsigned int oldfd, unsigned int newfd)
+{
+ struct file *file __free(fput) = NULL;
+
+ file = fget_raw(oldfd);
+ if (!file)
+ return -EBADF;
+ if (oldfd == newfd) {
+ set_close_on_exec(newfd, 0);
+ return 0;
+ }
+
+ return replace_fd(newfd, file, 0);
+}
+
+static int pidfd_spawn_apply_actions(struct pidfd_spawn_state *state)
+{
+ unsigned int i;
+ int ret;
+
+ for (i = 0; i < state->nr_actions; i++) {
+ const struct pidfd_spawn_action *action = &state->actions[i];
+
+ switch (action->type) {
+ case PIDFD_SPAWN_ACTION_DUP2:
+ ret = pidfd_spawn_do_dup2(action->fd, action->newfd);
+ break;
+ case PIDFD_SPAWN_ACTION_FCHDIR:
+ ret = do_fchdir(action->fd);
+ break;
+ case PIDFD_SPAWN_ACTION_CLOSE_RANGE:
+ ret = do_close_range(action->fd, action->newfd,
+ action->flags);
+ break;
+ default:
+ ret = -EOPNOTSUPP;
+ break;
+ }
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
static void pidfd_spawn_save_audit_context(struct pidfd_spawn_state *state)
{
struct pt_regs *regs = current_pt_regs();
@@ -543,24 +667,32 @@ 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;
+ bool cancelled;
int fatal_sig;
int ret;
pidfd_spawn_audit_entry(state);
+ cancelled = pidfd_spawn_child_cancelled(state);
+ fatal_sig = 0;
+ ret = 0;
+ if (!cancelled) {
+ fatal_sig = pidfd_spawn_pending_fatal_signal();
+ if (!fatal_sig)
+ ret = pidfd_spawn_apply_actions(state);
+ }
+
+ /* FCHDIR must establish the cwd captured by audit_getname(). */
filename = complete_getname(&state->filename);
- if (IS_ERR(filename))
+ if (!ret && !cancelled && !fatal_sig && IS_ERR(filename))
ret = PTR_ERR(filename);
- else
- ret = 0;
- if (pidfd_spawn_child_cancelled(state)) {
+ if (cancelled) {
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);
@@ -589,6 +721,7 @@ static int pidfd_spawn_copy_run_args(struct pidfd_spawn_run_args *kargs,
struct pidfd_spawn_run_args __user *uargs,
size_t usize)
{
+ size_t actions_size;
int ret;
BUILD_BUG_ON(sizeof(struct pidfd_spawn_run_args) !=
@@ -606,8 +739,17 @@ static int pidfd_spawn_copy_run_args(struct pidfd_spawn_run_args *kargs,
return -EINVAL;
if (!kargs->argv)
return -EINVAL;
- if (kargs->nr_actions || kargs->actions || kargs->action_size)
+ if (kargs->nr_actions) {
+ if (!kargs->actions ||
+ kargs->action_size < PIDFD_SPAWN_ACTION_SIZE_VER0 ||
+ !IS_ALIGNED(kargs->action_size, sizeof(__u64)))
+ return -EINVAL;
+ } else if (kargs->actions || kargs->action_size) {
return -EINVAL;
+ }
+ actions_size = array_size(kargs->nr_actions, kargs->action_size);
+ if (actions_size > PIDFD_SPAWN_MAX_ACTIONS_SIZE)
+ return -E2BIG;
if (!pidfd_spawn_user_ptr_fits(kargs->path) ||
!pidfd_spawn_user_ptr_fits(kargs->argv) ||
!pidfd_spawn_user_ptr_fits(kargs->envp) ||
@@ -713,10 +855,12 @@ static int pidfd_spawn_finish_vfork(struct pidfd_spawn_state *state,
static int pidfd_spawn_start(struct file *file,
struct pidfd_spawn_state *state,
- const struct pidfd_spawn_run_args *kargs)
+ const struct pidfd_spawn_run_args *kargs,
+ struct pidfd_spawn_action **actions)
{
struct delayed_filename filename
__free(pidfd_spawn_dismiss_filename) = {};
+ struct pidfd_spawn_action *drop_actions __free(kvfree) = NULL;
struct delayed_filename drop_filename
__free(pidfd_spawn_dismiss_filename) = {};
struct kernel_clone_args clone_args = {
@@ -757,8 +901,11 @@ static int pidfd_spawn_start(struct file *file,
state->argv = pidfd_spawn_user_arg(kargs->argv);
state->envp = pidfd_spawn_user_arg(kargs->envp);
+ state->actions = *actions;
+ state->nr_actions = kargs->nr_actions;
pidfd_spawn_set_status(state, PIDFD_SPAWN_STARTING);
pidfd_spawn_save_audit_context(state);
+ *actions = NULL;
reinit_completion(&state->done);
task = pidfd_spawn_create_task(file, state, &clone_args);
@@ -766,6 +913,9 @@ static int pidfd_spawn_start(struct file *file,
ret = PTR_ERR(task);
drop_filename = state->filename;
INIT_DELAYED_FILENAME(&state->filename);
+ drop_actions = state->actions;
+ state->actions = NULL;
+ state->nr_actions = 0;
state->argv = native_arg(NULL);
state->envp = native_arg(NULL);
memset(state->audit_args, 0,
@@ -854,6 +1004,7 @@ int pidfd_empty_open(unsigned int flags)
SYSCALL_DEFINE3(pidfd_spawn_run, int, fd,
struct pidfd_spawn_run_args __user *, uargs, size_t, usize)
{
+ struct pidfd_spawn_action *actions __free(kvfree) = NULL;
struct pidfd_spawn_run_args kargs;
struct pidfd_spawn_state *state __free(pidfd_spawn_state) = NULL;
int ret;
@@ -869,6 +1020,10 @@ SYSCALL_DEFINE3(pidfd_spawn_run, int, fd,
state = pidfd_spawn_state_get_file(fd_file(f));
if (IS_ERR(state))
return PTR_ERR(state);
+ ret = pidfd_spawn_copy_actions(&actions, kargs.actions,
+ kargs.nr_actions, kargs.action_size);
+ if (ret)
+ return ret;
scoped_cond_guard(mutex_intr, return -ERESTARTNOINTR,
¤t->signal->cred_guard_mutex) {
@@ -877,7 +1032,7 @@ SYSCALL_DEFINE3(pidfd_spawn_run, int, fd,
ret = pidfd_spawn_check_startable(state);
if (ret)
return ret;
- ret = pidfd_spawn_start(fd_file(f), state, &kargs);
+ ret = pidfd_spawn_start(fd_file(f), state, &kargs, &actions);
}
if (!ret)
ret = pid_vnr(state->pid);
--
2.52.0
^ permalink raw reply related
* [RFC PATCH 21/24] pidfd: consume spawn builders on the first run attempt
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 pre-task run failure currently returns the builder to CONFIGURING. A
second thread can then publish another invocation before the first
caller probes the pidfd, making the observed process state ambiguous.
Claim the builder after type, authority, and ptrace checks but before
run argument access. Any later validation, uaccess, allocation, or
task-creation failure leaves a terminal taskless builder. Later config
and run operations return EBUSY, while process-dependent pidfd
operations return ESRCH.
Normalize every internal restart error after the claim to EINTR so the
architecture cannot restart a consumed invocation and replace its
result with EBUSY. Preserve normal restart behavior before the claim.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
fs/pidfd_spawn.c | 164 +++++++++++++++++++++++++++++++----------------
1 file changed, 109 insertions(+), 55 deletions(-)
diff --git a/fs/pidfd_spawn.c b/fs/pidfd_spawn.c
index 3e46a12c0ba07..1309fe99ec091 100644
--- a/fs/pidfd_spawn.c
+++ b/fs/pidfd_spawn.c
@@ -73,6 +73,7 @@ DEFINE_FREE(pidfd_spawn_dismiss_filename, struct delayed_filename,
enum pidfd_spawn_status {
PIDFD_SPAWN_CONFIGURING,
PIDFD_SPAWN_STARTING,
+ PIDFD_SPAWN_FAILED_TASKLESS,
PIDFD_SPAWN_SETUP_DONE,
PIDFD_SPAWN_STARTED,
PIDFD_SPAWN_CANCELLED,
@@ -100,6 +101,14 @@ struct pidfd_spawn_state {
enum pidfd_spawn_status status;
};
+struct pidfd_spawn_resources {
+ struct pidfd_spawn_action *actions;
+ struct mm_struct *creator_mm;
+ const struct cred *creator_cred;
+ struct pid_namespace *creator_pid_ns;
+ char *staged_path;
+};
+
static struct pid *
pidfd_spawn_load_pid(const struct pidfd_spawn_state *state)
{
@@ -183,8 +192,16 @@ static struct pid *pidfd_spawn_file_pid(void *data)
return pid ? pid : ERR_PTR(-ESRCH);
}
+static bool pidfd_spawn_file_terminal(void *data)
+{
+ struct pidfd_spawn_state *state = data;
+
+ return pidfd_spawn_get_status(state) == PIDFD_SPAWN_FAILED_TASKLESS;
+}
+
static const struct pidfs_future_file_ops pidfd_spawn_future_ops = {
.get_pid = pidfd_spawn_file_pid,
+ .is_terminal = pidfd_spawn_file_terminal,
.release = pidfd_spawn_state_release,
};
@@ -199,31 +216,47 @@ pidfd_spawn_state_get_file(struct file *file)
return state;
}
+static void
+pidfd_spawn_detach_resources(struct pidfd_spawn_state *state,
+ struct pidfd_spawn_resources *resources)
+{
+ resources->actions = state->actions;
+ resources->creator_mm = state->creator_mm;
+ resources->creator_cred = state->creator_cred;
+ resources->creator_pid_ns = state->creator_pid_ns;
+ resources->staged_path = state->staged_path;
+ state->actions = NULL;
+ state->creator_mm = NULL;
+ state->creator_cred = NULL;
+ state->creator_pid_ns = NULL;
+ state->staged_path = NULL;
+ state->nr_actions = 0;
+}
+
+static void
+pidfd_spawn_put_resources(struct pidfd_spawn_resources *resources)
+{
+ if (resources->creator_mm)
+ mmdrop(resources->creator_mm);
+ if (resources->creator_cred)
+ put_cred(resources->creator_cred);
+ if (resources->creator_pid_ns)
+ put_pid_ns(resources->creator_pid_ns);
+ kfree(resources->staged_path);
+ kvfree(resources->actions);
+}
+
static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
struct filename *filename, int result)
{
- const struct cred *creator_cred;
- struct pidfd_spawn_action *actions;
- struct mm_struct *creator_mm;
- struct pid_namespace *creator_pid_ns;
- char *staged_path;
+ struct pidfd_spawn_resources resources = {};
/*
* 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);
- actions = state->actions;
- creator_mm = state->creator_mm;
- creator_cred = state->creator_cred;
- creator_pid_ns = state->creator_pid_ns;
- staged_path = state->staged_path;
- state->actions = NULL;
- state->creator_mm = NULL;
- state->creator_cred = NULL;
- state->creator_pid_ns = NULL;
- state->staged_path = NULL;
- state->nr_actions = 0;
+ pidfd_spawn_detach_resources(state, &resources);
state->argv = native_arg(NULL);
state->envp = native_arg(NULL);
memset(state->audit_args, 0, sizeof(state->audit_args));
@@ -235,14 +268,7 @@ static void pidfd_spawn_drop_run_data(struct pidfd_spawn_state *state,
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);
- kvfree(actions);
+ pidfd_spawn_put_resources(&resources);
}
static bool pidfd_spawn_cancel(struct pidfd_spawn_state *state,
@@ -291,16 +317,44 @@ static bool pidfd_spawn_same_creator(struct pidfd_spawn_state *state)
pidfd_spawn_same_pid_ns(state);
}
-static int pidfd_spawn_check_startable(struct pidfd_spawn_state *state)
+static int pidfd_spawn_claim(struct pidfd_spawn_state *state)
{
int ret;
- scoped_cond_guard(mutex_intr, return -EINTR, &state->lock) {
+ scoped_guard(mutex, &state->lock) {
ret = pidfd_spawn_configuring_error(state);
- if (!ret && !pidfd_spawn_same_creator(state))
- ret = -EPERM;
+ if (ret)
+ return ret;
+ if (!pidfd_spawn_same_creator(state))
+ return -EPERM;
+
+ pidfd_spawn_set_status(state, PIDFD_SPAWN_STARTING);
}
- return ret;
+ return 0;
+}
+
+static void pidfd_spawn_fail_taskless(struct file *file,
+ struct pidfd_spawn_state *state, int result)
+{
+ struct pidfd_spawn_resources resources = {};
+
+ mutex_lock(&state->lock);
+ if (WARN_ON_ONCE(pidfd_spawn_get_status(state) !=
+ PIDFD_SPAWN_STARTING || pidfd_spawn_load_pid(state))) {
+ mutex_unlock(&state->lock);
+ return;
+ }
+ state->result = result;
+ pidfd_spawn_detach_resources(state, &resources);
+ state->argv = native_arg(NULL);
+ state->envp = native_arg(NULL);
+ memset(state->audit_args, 0, sizeof(state->audit_args));
+ state->audit_syscall = 0;
+ pidfd_spawn_set_status(state, PIDFD_SPAWN_FAILED_TASKLESS);
+ mutex_unlock(&state->lock);
+
+ pidfd_spawn_put_resources(&resources);
+ pidfs_future_file_notify(file);
}
static struct filename *pidfd_spawn_get_path(const char __user *value)
@@ -718,7 +772,7 @@ static void pidfd_spawn_child(struct callback_head *work)
}
static int pidfd_spawn_copy_run_args(struct pidfd_spawn_run_args *kargs,
- struct pidfd_spawn_run_args __user *uargs,
+ const struct pidfd_spawn_run_args __user *uargs,
size_t usize)
{
size_t actions_size;
@@ -873,6 +927,13 @@ static int pidfd_spawn_start(struct file *file,
struct task_struct *task;
int ret;
+ scoped_cond_guard(mutex_intr, return -EINTR, &state->lock) {
+ if (pidfd_spawn_get_status(state) != PIDFD_SPAWN_STARTING)
+ return -EBUSY;
+ if (!!state->staged_path == !!kargs->path)
+ return -EINVAL;
+ }
+
if (kargs->path) {
ret = pidfd_spawn_get_delayed_path(&filename,
pidfd_spawn_user_ptr(kargs->path));
@@ -881,15 +942,8 @@ static int pidfd_spawn_start(struct file *file,
}
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 (pidfd_spawn_get_status(state) != PIDFD_SPAWN_STARTING)
+ return -EBUSY;
if (state->staged_path) {
ret = pidfd_spawn_get_delayed_kernel_path(&filename,
state->staged_path);
@@ -903,7 +957,6 @@ static int pidfd_spawn_start(struct file *file,
state->envp = pidfd_spawn_user_arg(kargs->envp);
state->actions = *actions;
state->nr_actions = kargs->nr_actions;
- pidfd_spawn_set_status(state, PIDFD_SPAWN_STARTING);
pidfd_spawn_save_audit_context(state);
*actions = NULL;
reinit_completion(&state->done);
@@ -921,9 +974,6 @@ static int pidfd_spawn_start(struct file *file,
memset(state->audit_args, 0,
sizeof(state->audit_args));
state->audit_syscall = 0;
- state->result = 0;
- pidfd_spawn_set_status(state,
- PIDFD_SPAWN_CONFIGURING);
return ret;
}
@@ -1002,17 +1052,13 @@ int pidfd_empty_open(unsigned int flags)
}
SYSCALL_DEFINE3(pidfd_spawn_run, int, fd,
- struct pidfd_spawn_run_args __user *, uargs, size_t, usize)
+ const struct pidfd_spawn_run_args __user *, uargs, size_t, usize)
{
struct pidfd_spawn_action *actions __free(kvfree) = NULL;
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;
@@ -1020,19 +1066,27 @@ SYSCALL_DEFINE3(pidfd_spawn_run, int, fd,
state = pidfd_spawn_state_get_file(fd_file(f));
if (IS_ERR(state))
return PTR_ERR(state);
- ret = pidfd_spawn_copy_actions(&actions, kargs.actions,
- kargs.nr_actions, kargs.action_size);
- if (ret)
- return ret;
-
scoped_cond_guard(mutex_intr, return -ERESTARTNOINTR,
¤t->signal->cred_guard_mutex) {
if (READ_ONCE(current->ptrace))
return -EPERM;
- ret = pidfd_spawn_check_startable(state);
+ ret = pidfd_spawn_claim(state);
if (ret)
return ret;
- ret = pidfd_spawn_start(fd_file(f), state, &kargs, &actions);
+
+ ret = pidfd_spawn_copy_run_args(&kargs, uargs, usize);
+ if (!ret)
+ ret = pidfd_spawn_copy_actions(&actions, kargs.actions,
+ kargs.nr_actions,
+ kargs.action_size);
+ if (!ret)
+ ret = pidfd_spawn_start(fd_file(f), state, &kargs,
+ &actions);
+ if (ret) {
+ ret = pidfd_spawn_normalize_result(ret);
+ if (!pidfd_spawn_load_pid(state))
+ pidfd_spawn_fail_taskless(fd_file(f), state, ret);
+ }
}
if (!ret)
ret = pid_vnr(state->pid);
--
2.52.0
^ permalink raw reply related
* [RFC PATCH 22/24] pidfd: expose spawn builder system calls
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 preceding commits define the future-pidfd lifetime, configuration, and
run state machine without making them reachable from userspace. Each
preparatory step therefore remains bisectable without exposing a partial
process builder.
Make pidfd_open(0, PIDFD_EMPTY) create a taskless future pidfd and expose
pidfd_config() and pidfd_spawn_run(). Add the syscall declarations and
table entries together so the complete initial interface becomes reachable
at one commit boundary. Synchronize the lagging arm64 AArch32 perf mirror
through the current syscall table while adding the builder entries.
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Li Chen <me@linux.beauty>
---
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 ++
include/linux/syscalls.h | 7 +++++++
include/uapi/asm-generic/unistd.h | 8 +++++++-
kernel/pid.c | 19 ++++++++++++++++++-
scripts/syscall.tbl | 2 ++
tools/include/uapi/asm-generic/unistd.h | 8 +++++++-
.../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 ++
34 files changed, 108 insertions(+), 3 deletions(-)
diff --git a/arch/alpha/kernel/syscalls/syscall.tbl b/arch/alpha/kernel/syscalls/syscall.tbl
index f31b7afffc345..8dd0b22a407c5 100644
--- a/arch/alpha/kernel/syscalls/syscall.tbl
+++ b/arch/alpha/kernel/syscalls/syscall.tbl
@@ -511,3 +511,5 @@
579 common file_setattr sys_file_setattr
580 common listns sys_listns
581 common rseq_slice_yield sys_rseq_slice_yield
+582 common pidfd_config sys_pidfd_config
+583 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/arm/tools/syscall.tbl b/arch/arm/tools/syscall.tbl
index 94351e22bfcf7..9830c4600fc78 100644
--- a/arch/arm/tools/syscall.tbl
+++ b/arch/arm/tools/syscall.tbl
@@ -486,3 +486,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/arm64/tools/syscall_32.tbl b/arch/arm64/tools/syscall_32.tbl
index 62d93d88e0fef..9c3281b3612c2 100644
--- a/arch/arm64/tools/syscall_32.tbl
+++ b/arch/arm64/tools/syscall_32.tbl
@@ -483,3 +483,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/m68k/kernel/syscalls/syscall.tbl b/arch/m68k/kernel/syscalls/syscall.tbl
index 2489342571014..2db719475e061 100644
--- a/arch/m68k/kernel/syscalls/syscall.tbl
+++ b/arch/m68k/kernel/syscalls/syscall.tbl
@@ -471,3 +471,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/microblaze/kernel/syscalls/syscall.tbl b/arch/microblaze/kernel/syscalls/syscall.tbl
index 223d263036272..41ead93dba5c4 100644
--- a/arch/microblaze/kernel/syscalls/syscall.tbl
+++ b/arch/microblaze/kernel/syscalls/syscall.tbl
@@ -477,3 +477,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/mips/kernel/syscalls/syscall_n32.tbl b/arch/mips/kernel/syscalls/syscall_n32.tbl
index 7430714e2b8f8..b6bfca9ddedc6 100644
--- a/arch/mips/kernel/syscalls/syscall_n32.tbl
+++ b/arch/mips/kernel/syscalls/syscall_n32.tbl
@@ -410,3 +410,5 @@
469 n32 file_setattr sys_file_setattr
470 n32 listns sys_listns
471 n32 rseq_slice_yield sys_rseq_slice_yield
+472 n32 pidfd_config sys_pidfd_config
+473 n32 pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/mips/kernel/syscalls/syscall_n64.tbl b/arch/mips/kernel/syscalls/syscall_n64.tbl
index 630aab9e54259..229a1a5bff0e9 100644
--- a/arch/mips/kernel/syscalls/syscall_n64.tbl
+++ b/arch/mips/kernel/syscalls/syscall_n64.tbl
@@ -386,3 +386,5 @@
469 n64 file_setattr sys_file_setattr
470 n64 listns sys_listns
471 n64 rseq_slice_yield sys_rseq_slice_yield
+472 n64 pidfd_config sys_pidfd_config
+473 n64 pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/mips/kernel/syscalls/syscall_o32.tbl b/arch/mips/kernel/syscalls/syscall_o32.tbl
index 128653112284b..4ee804186e399 100644
--- a/arch/mips/kernel/syscalls/syscall_o32.tbl
+++ b/arch/mips/kernel/syscalls/syscall_o32.tbl
@@ -459,3 +459,5 @@
469 o32 file_setattr sys_file_setattr
470 o32 listns sys_listns
471 o32 rseq_slice_yield sys_rseq_slice_yield
+472 o32 pidfd_config sys_pidfd_config
+473 o32 pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/parisc/kernel/syscalls/syscall.tbl b/arch/parisc/kernel/syscalls/syscall.tbl
index c6331dad94613..ff6760df2dc51 100644
--- a/arch/parisc/kernel/syscalls/syscall.tbl
+++ b/arch/parisc/kernel/syscalls/syscall.tbl
@@ -470,3 +470,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/powerpc/kernel/syscalls/syscall.tbl b/arch/powerpc/kernel/syscalls/syscall.tbl
index 4fcc7c58a105d..021b334a8e1a0 100644
--- a/arch/powerpc/kernel/syscalls/syscall.tbl
+++ b/arch/powerpc/kernel/syscalls/syscall.tbl
@@ -562,3 +562,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 nospu rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/s390/kernel/syscalls/syscall.tbl b/arch/s390/kernel/syscalls/syscall.tbl
index 09a7ef04d9791..eea88f94a7624 100644
--- a/arch/s390/kernel/syscalls/syscall.tbl
+++ b/arch/s390/kernel/syscalls/syscall.tbl
@@ -398,3 +398,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/sh/kernel/syscalls/syscall.tbl b/arch/sh/kernel/syscalls/syscall.tbl
index 70b315cbe710c..37166286c4045 100644
--- a/arch/sh/kernel/syscalls/syscall.tbl
+++ b/arch/sh/kernel/syscalls/syscall.tbl
@@ -475,3 +475,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/sparc/kernel/syscalls/syscall.tbl b/arch/sparc/kernel/syscalls/syscall.tbl
index 7e71bf7fcd14f..70f2165b9cfb3 100644
--- a/arch/sparc/kernel/syscalls/syscall.tbl
+++ b/arch/sparc/kernel/syscalls/syscall.tbl
@@ -517,3 +517,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/x86/entry/syscalls/syscall_32.tbl b/arch/x86/entry/syscalls/syscall_32.tbl
index f832ebd2d79b0..b2eeddbe99be9 100644
--- a/arch/x86/entry/syscalls/syscall_32.tbl
+++ b/arch/x86/entry/syscalls/syscall_32.tbl
@@ -477,3 +477,5 @@
469 i386 file_setattr sys_file_setattr
470 i386 listns sys_listns
471 i386 rseq_slice_yield sys_rseq_slice_yield
+472 i386 pidfd_config sys_pidfd_config
+473 i386 pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/arch/x86/entry/syscalls/syscall_64.tbl b/arch/x86/entry/syscalls/syscall_64.tbl
index 524155d655da1..f0952f34c10a5 100644
--- a/arch/x86/entry/syscalls/syscall_64.tbl
+++ b/arch/x86/entry/syscalls/syscall_64.tbl
@@ -396,6 +396,8 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
#
# Due to a historical design error, certain syscalls are numbered differently
diff --git a/arch/xtensa/kernel/syscalls/syscall.tbl b/arch/xtensa/kernel/syscalls/syscall.tbl
index a9bca4e484dec..45dd80ab94cf9 100644
--- a/arch/xtensa/kernel/syscalls/syscall.tbl
+++ b/arch/xtensa/kernel/syscalls/syscall.tbl
@@ -442,3 +442,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/include/linux/syscalls.h b/include/linux/syscalls.h
index 874d9067a43b5..ec90454d01214 100644
--- a/include/linux/syscalls.h
+++ b/include/linux/syscalls.h
@@ -79,6 +79,7 @@ struct mnt_id_req;
struct ns_id_req;
struct xattr_args;
struct file_attr;
+struct pidfd_spawn_run_args;
#include <linux/types.h>
#include <linux/aio_abi.h>
@@ -908,6 +909,12 @@ asmlinkage long sys_clock_adjtime32(clockid_t which_clock,
asmlinkage long sys_syncfs(int fd);
asmlinkage long sys_setns(int fd, int nstype);
asmlinkage long sys_pidfd_open(pid_t pid, unsigned int flags);
+asmlinkage long sys_pidfd_config(int fd, unsigned int cmd,
+ const char __user *key,
+ const void __user *value, int aux);
+asmlinkage long sys_pidfd_spawn_run(int fd,
+ const struct pidfd_spawn_run_args __user *uargs,
+ size_t usize);
asmlinkage long sys_sendmmsg(int fd, struct mmsghdr __user *msg,
unsigned int vlen, unsigned flags);
asmlinkage long sys_process_vm_readv(pid_t pid,
diff --git a/include/uapi/asm-generic/unistd.h b/include/uapi/asm-generic/unistd.h
index a627acc8fb5fe..9ab8ae5100ec1 100644
--- a/include/uapi/asm-generic/unistd.h
+++ b/include/uapi/asm-generic/unistd.h
@@ -863,8 +863,14 @@ __SYSCALL(__NR_listns, sys_listns)
#define __NR_rseq_slice_yield 471
__SYSCALL(__NR_rseq_slice_yield, sys_rseq_slice_yield)
+#define __NR_pidfd_config 472
+__SYSCALL(__NR_pidfd_config, sys_pidfd_config)
+
+#define __NR_pidfd_spawn_run 473
+__SYSCALL(__NR_pidfd_spawn_run, sys_pidfd_spawn_run)
+
#undef __NR_syscalls
-#define __NR_syscalls 472
+#define __NR_syscalls 474
/*
* 32 bit systems traditionally used different
diff --git a/kernel/pid.c b/kernel/pid.c
index 010f80177cac8..a1d05ce10ac25 100644
--- a/kernel/pid.c
+++ b/kernel/pid.c
@@ -43,6 +43,7 @@
#include <linux/sched/task.h>
#include <linux/idr.h>
#include <linux/pidfs.h>
+#include <linux/pidfd_spawn.h>
#include <net/sock.h>
#include <uapi/linux/pidfd.h>
@@ -715,10 +716,26 @@ SYSCALL_DEFINE2(pidfd_open, pid_t, pid, unsigned int, flags)
int fd;
struct pid *p;
+ /*
+ * pidfd_open(0, PIDFD_EMPTY) is the spawn-builder entry point.
+ * It creates a future pidfd instead of looking up pid 0.
+ */
+ if (pid == 0) {
+ if (!(flags & PIDFD_EMPTY))
+ return -EINVAL;
+ if (flags & ~(PIDFD_EMPTY | PIDFD_NONBLOCK))
+ return -EINVAL;
+
+ return pidfd_empty_open(flags & PIDFD_NONBLOCK);
+ }
+
+ /* PIDFD_EMPTY is only meaningful for the pid == 0 builder form. */
+ if (flags & PIDFD_EMPTY)
+ return -EINVAL;
if (flags & ~(PIDFD_NONBLOCK | PIDFD_THREAD))
return -EINVAL;
- if (pid <= 0)
+ if (pid < 0)
return -EINVAL;
p = find_get_pid(pid);
diff --git a/scripts/syscall.tbl b/scripts/syscall.tbl
index 7a42b32b65776..29248f0ac1a07 100644
--- a/scripts/syscall.tbl
+++ b/scripts/syscall.tbl
@@ -412,3 +412,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/include/uapi/asm-generic/unistd.h b/tools/include/uapi/asm-generic/unistd.h
index a627acc8fb5fe..9ab8ae5100ec1 100644
--- a/tools/include/uapi/asm-generic/unistd.h
+++ b/tools/include/uapi/asm-generic/unistd.h
@@ -863,8 +863,14 @@ __SYSCALL(__NR_listns, sys_listns)
#define __NR_rseq_slice_yield 471
__SYSCALL(__NR_rseq_slice_yield, sys_rseq_slice_yield)
+#define __NR_pidfd_config 472
+__SYSCALL(__NR_pidfd_config, sys_pidfd_config)
+
+#define __NR_pidfd_spawn_run 473
+__SYSCALL(__NR_pidfd_spawn_run, sys_pidfd_spawn_run)
+
#undef __NR_syscalls
-#define __NR_syscalls 472
+#define __NR_syscalls 474
/*
* 32 bit systems traditionally used different
diff --git a/tools/perf/arch/alpha/entry/syscalls/syscall.tbl b/tools/perf/arch/alpha/entry/syscalls/syscall.tbl
index 74720667fe091..a20f81b950349 100644
--- a/tools/perf/arch/alpha/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/alpha/entry/syscalls/syscall.tbl
@@ -502,3 +502,5 @@
570 common lsm_set_self_attr sys_lsm_set_self_attr
571 common lsm_list_modules sys_lsm_list_modules
572 common mseal sys_mseal
+582 common pidfd_config sys_pidfd_config
+583 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/perf/arch/arm/entry/syscalls/syscall.tbl b/tools/perf/arch/arm/entry/syscalls/syscall.tbl
index 94351e22bfcf7..9830c4600fc78 100644
--- a/tools/perf/arch/arm/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/arm/entry/syscalls/syscall.tbl
@@ -486,3 +486,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/perf/arch/arm64/entry/syscalls/syscall_32.tbl b/tools/perf/arch/arm64/entry/syscalls/syscall_32.tbl
index 9a37930d4e26f..9c3281b3612c2 100644
--- a/tools/perf/arch/arm64/entry/syscalls/syscall_32.tbl
+++ b/tools/perf/arch/arm64/entry/syscalls/syscall_32.tbl
@@ -474,3 +474,14 @@
460 common lsm_set_self_attr sys_lsm_set_self_attr
461 common lsm_list_modules sys_lsm_list_modules
462 common mseal sys_mseal
+463 common setxattrat sys_setxattrat
+464 common getxattrat sys_getxattrat
+465 common listxattrat sys_listxattrat
+466 common removexattrat sys_removexattrat
+467 common open_tree_attr sys_open_tree_attr
+468 common file_getattr sys_file_getattr
+469 common file_setattr sys_file_setattr
+470 common listns sys_listns
+471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/perf/arch/mips/entry/syscalls/syscall_n64.tbl b/tools/perf/arch/mips/entry/syscalls/syscall_n64.tbl
index 630aab9e54259..229a1a5bff0e9 100644
--- a/tools/perf/arch/mips/entry/syscalls/syscall_n64.tbl
+++ b/tools/perf/arch/mips/entry/syscalls/syscall_n64.tbl
@@ -386,3 +386,5 @@
469 n64 file_setattr sys_file_setattr
470 n64 listns sys_listns
471 n64 rseq_slice_yield sys_rseq_slice_yield
+472 n64 pidfd_config sys_pidfd_config
+473 n64 pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/perf/arch/parisc/entry/syscalls/syscall.tbl b/tools/perf/arch/parisc/entry/syscalls/syscall.tbl
index 66dc406b12e44..88311785913f8 100644
--- a/tools/perf/arch/parisc/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/parisc/entry/syscalls/syscall.tbl
@@ -461,3 +461,5 @@
460 common lsm_set_self_attr sys_lsm_set_self_attr
461 common lsm_list_modules sys_lsm_list_modules
462 common mseal sys_mseal
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/perf/arch/powerpc/entry/syscalls/syscall.tbl b/tools/perf/arch/powerpc/entry/syscalls/syscall.tbl
index 4fcc7c58a105d..021b334a8e1a0 100644
--- a/tools/perf/arch/powerpc/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/powerpc/entry/syscalls/syscall.tbl
@@ -562,3 +562,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 nospu rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/perf/arch/s390/entry/syscalls/syscall.tbl b/tools/perf/arch/s390/entry/syscalls/syscall.tbl
index 09a7ef04d9791..eea88f94a7624 100644
--- a/tools/perf/arch/s390/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/s390/entry/syscalls/syscall.tbl
@@ -398,3 +398,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/perf/arch/sh/entry/syscalls/syscall.tbl b/tools/perf/arch/sh/entry/syscalls/syscall.tbl
index 70b315cbe710c..37166286c4045 100644
--- a/tools/perf/arch/sh/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/sh/entry/syscalls/syscall.tbl
@@ -475,3 +475,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/perf/arch/sparc/entry/syscalls/syscall.tbl b/tools/perf/arch/sparc/entry/syscalls/syscall.tbl
index 7e71bf7fcd14f..70f2165b9cfb3 100644
--- a/tools/perf/arch/sparc/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/sparc/entry/syscalls/syscall.tbl
@@ -517,3 +517,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/perf/arch/x86/entry/syscalls/syscall_32.tbl b/tools/perf/arch/x86/entry/syscalls/syscall_32.tbl
index f832ebd2d79b0..b2eeddbe99be9 100644
--- a/tools/perf/arch/x86/entry/syscalls/syscall_32.tbl
+++ b/tools/perf/arch/x86/entry/syscalls/syscall_32.tbl
@@ -477,3 +477,5 @@
469 i386 file_setattr sys_file_setattr
470 i386 listns sys_listns
471 i386 rseq_slice_yield sys_rseq_slice_yield
+472 i386 pidfd_config sys_pidfd_config
+473 i386 pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/perf/arch/x86/entry/syscalls/syscall_64.tbl b/tools/perf/arch/x86/entry/syscalls/syscall_64.tbl
index 524155d655da1..f0952f34c10a5 100644
--- a/tools/perf/arch/x86/entry/syscalls/syscall_64.tbl
+++ b/tools/perf/arch/x86/entry/syscalls/syscall_64.tbl
@@ -396,6 +396,8 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
#
# Due to a historical design error, certain syscalls are numbered differently
diff --git a/tools/perf/arch/xtensa/entry/syscalls/syscall.tbl b/tools/perf/arch/xtensa/entry/syscalls/syscall.tbl
index a9bca4e484dec..45dd80ab94cf9 100644
--- a/tools/perf/arch/xtensa/entry/syscalls/syscall.tbl
+++ b/tools/perf/arch/xtensa/entry/syscalls/syscall.tbl
@@ -442,3 +442,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
diff --git a/tools/scripts/syscall.tbl b/tools/scripts/syscall.tbl
index 7a42b32b65776..29248f0ac1a07 100644
--- a/tools/scripts/syscall.tbl
+++ b/tools/scripts/syscall.tbl
@@ -412,3 +412,5 @@
469 common file_setattr sys_file_setattr
470 common listns sys_listns
471 common rseq_slice_yield sys_rseq_slice_yield
+472 common pidfd_config sys_pidfd_config
+473 common pidfd_spawn_run sys_pidfd_spawn_run
--
2.52.0
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox