From 7a8b81e8b9c73cfb7343fe90e575ec0c31a0c47a Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 10 Jul 2026 11:33:05 +0200 Subject: [PATCH 01/63] binfmt_misc: convert entry list to an hlist The upcoming conversion of the handler lookup to RCU walks cannot use list_del_init(): reinitializing the forward pointer of a removed entry would make a concurrent lockless walker standing on that entry loop back onto it indefinitely. The removal paths do rely on reinitialization though because bm_{entry,status}_write() and bm_evict_inode() need to detect whether an entry has already been unlinked. hlists support exactly this pattern: hlist_del_init_rcu() keeps the forward pointer of the removed entry intact for concurrent walkers and only zeroes ->pprev with hlist_unhashed() serving as the linked test. Convert the entry list to an hlist now while keeping the rwlock so the subsequent RCU conversion is a pure locking change. hlist_add_head() inserts at the head just as list_add() did so lookup precedence between registered handlers is unchanged. Link: https://patch.msgid.link/20260710-work-binfmt_misc-locking-v3-4-a162f7cb58d6@kernel.org Reviewed-by: Jori Koolstra Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 25 +++++++++++++------------ include/linux/binfmts.h | 2 +- kernel/user.c | 2 +- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index c97f10b48b5b..86be578787a7 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -48,7 +48,7 @@ enum {Enabled, Magic}; #define MISC_FMT_OPEN_FILE (1UL << 28) typedef struct { - struct list_head list; + struct hlist_node node; unsigned long flags; /* type, status, etc. */ int offset; /* offset of magic */ int size; /* size of magic/mask */ @@ -95,7 +95,7 @@ static Node *search_binfmt_handler(struct binfmt_misc *misc, Node *e; /* Walk all the registered handlers. */ - list_for_each_entry(e, &misc->entries, list) { + hlist_for_each_entry(e, &misc->entries, node) { char *s; int j; @@ -665,8 +665,8 @@ static struct binfmt_misc *i_binfmt_misc(struct inode *inode) * * If the ->evict call was not caused by a super block shutdown but by a write * to remove the entry or all entries via bm_{entry,status}_write() the entry - * will have already been removed from the list. We keep the list_empty() check - * to make that explicit. + * will have already been removed from the list. We keep the hlist_unhashed() + * check to make that explicit. */ static void bm_evict_inode(struct inode *inode) { @@ -679,8 +679,8 @@ static void bm_evict_inode(struct inode *inode) misc = i_binfmt_misc(inode); write_lock(&misc->entries_lock); - if (!list_empty(&e->list)) - list_del_init(&e->list); + if (!hlist_unhashed(&e->node)) + hlist_del_init(&e->node); write_unlock(&misc->entries_lock); put_binfmt_handler(e); } @@ -701,7 +701,7 @@ static void bm_evict_inode(struct inode *inode) static void remove_binfmt_handler(struct binfmt_misc *misc, Node *e) { write_lock(&misc->entries_lock); - list_del_init(&e->list); + hlist_del_init(&e->node); write_unlock(&misc->entries_lock); locked_recursive_removal(e->dentry, NULL); } @@ -757,7 +757,7 @@ static ssize_t bm_entry_write(struct file *file, const char __user *buffer, * read-only. So we only need to take the write lock when we * actually remove the entry from the list. */ - if (!list_empty(&e->list)) + if (!hlist_unhashed(&e->node)) remove_binfmt_handler(i_binfmt_misc(inode), e); inode_unlock(inode); @@ -801,7 +801,7 @@ static int add_entry(Node *e, struct super_block *sb) d_make_persistent(dentry, inode); misc = i_binfmt_misc(inode); write_lock(&misc->entries_lock); - list_add(&e->list, &misc->entries); + hlist_add_head(&e->node, &misc->entries); write_unlock(&misc->entries_lock); simple_done_creating(dentry); return 0; @@ -874,8 +874,9 @@ static ssize_t bm_status_write(struct file *file, const char __user *buffer, { struct binfmt_misc *misc; int res = parse_command(buffer, count); - Node *e, *next; + struct hlist_node *next; struct inode *inode; + Node *e; misc = i_binfmt_misc(file_inode(file)); switch (res) { @@ -901,7 +902,7 @@ static ssize_t bm_status_write(struct file *file, const char __user *buffer, * read-only. So we only need to take the write lock when we * actually remove the entry from the list. */ - list_for_each_entry_safe(e, next, &misc->entries, list) + hlist_for_each_entry_safe(e, next, &misc->entries, node) remove_binfmt_handler(misc, e); inode_unlock(inode); @@ -971,7 +972,7 @@ static int bm_fill_super(struct super_block *sb, struct fs_context *fc) if (!misc) return -ENOMEM; - INIT_LIST_HEAD(&misc->entries); + INIT_HLIST_HEAD(&misc->entries); rwlock_init(&misc->entries_lock); /* Pairs with smp_load_acquire() in load_binfmt_misc(). */ diff --git a/include/linux/binfmts.h b/include/linux/binfmts.h index 2c77e383e737..071da63f2b48 100644 --- a/include/linux/binfmts.h +++ b/include/linux/binfmts.h @@ -101,7 +101,7 @@ struct linux_binfmt { #if IS_ENABLED(CONFIG_BINFMT_MISC) struct binfmt_misc { - struct list_head entries; + struct hlist_head entries; rwlock_t entries_lock; bool enabled; } __randomize_layout; diff --git a/kernel/user.c b/kernel/user.c index 7aef4e679a6a..c6a2bfb4d918 100644 --- a/kernel/user.c +++ b/kernel/user.c @@ -23,7 +23,7 @@ #if IS_ENABLED(CONFIG_BINFMT_MISC) struct binfmt_misc init_binfmt_misc = { - .entries = LIST_HEAD_INIT(init_binfmt_misc.entries), + .entries = HLIST_HEAD_INIT, .enabled = true, .entries_lock = __RW_LOCK_UNLOCKED(init_binfmt_misc.entries_lock), }; From fd77da3efbedd7b442fbab86a6dbea5e2a1b32f8 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 10 Jul 2026 11:33:06 +0200 Subject: [PATCH 02/63] binfmt_misc: use RCU for the handler lookup Once binfmt_misc is loaded load_misc_binary() runs for every execve() on the system since binfmt_misc registers at the head of the formats list. Every exec therefore performs read_lock() and read_unlock() on the entries_lock of the relevant binfmt_misc instance, i.e., two atomic read-modify-writes on a shared cacheline. User namespaces without their own binfmt_misc mount fall back to an ancestor's instance so on container-heavy systems every exec on the machine typically ends up hammering the cacheline of init_binfmt_misc. On PREEMPT_RT the rwlock additionally turns the handler lookup into a sleeping lock on the exec fast path. The lock protects very little. Entries are immutable after publication except for the Enabled bit which is already toggled locklessly via set_bit()/clear_bit() and entry lifetime is already handled by the users refcount via get_binfmt_handler()/put_binfmt_handler(). The read lock's only remaining job is to make "the entry is still linked" and "take a reference" atomic with respect to the unlink sites. Switch the lookup to an RCU walk: * Lookup walks the entry list under rcu_read_lock() and acquires a reference via refcount_inc_not_zero(). The refcount can only drop to zero after an entry has been unlinked so a failed increment means the walk raced with an unlink. Restarting the search is bounded because an unlinked entry cannot be found again. * The unlink sites use hlist_del_init_rcu() which keeps the forward pointer intact for concurrent walkers and preserves hlist_unhashed() as the protection against double removal. * The final put frees the entry via kfree_rcu() as a concurrent walker may still dereference its flags, magic, mask, and inline strings. They all live in the entry allocation itself and thus stay valid until a grace period has elapsed. Closing the interpreter file stays synchronous. It is only used with a reference already held and all final puts run in process context. * Writers remain serialized by the inode lock of the root dentry with one exception. bm_evict_inode() called from generic_shutdown_super() during umount unlinks entries without holding it. Keep a spinlock around the unlink sites instead of relying on superblock lifetime rules to make that exclusion implicit. Handler removal semantics are unchanged. An exec that acquired a reference just before its handler was unregistered already completes with the removed handler today. The read lock never protected against that, it only made the window smaller. With this an exec that matches no binfmt_misc entry, the common case, no longer writes to any shared cacheline at all. Link: https://patch.msgid.link/20260710-work-binfmt_misc-locking-v3-5-a162f7cb58d6@kernel.org Reviewed-by: Jori Koolstra Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 59 ++++++++++++++++++++++++----------------- include/linux/binfmts.h | 2 +- kernel/user.c | 2 +- 3 files changed, 37 insertions(+), 26 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index 86be578787a7..236ebaf3be5c 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -59,6 +60,7 @@ typedef struct { struct dentry *dentry; struct file *interp_file; refcount_t users; /* sync removal with load_misc_binary() */ + struct rcu_head rcu; } Node; static struct file_system_type bm_fs_type; @@ -86,6 +88,8 @@ static struct file_system_type bm_fs_type; * Search for a binary type handler for @bprm in the list of registered binary * type handlers. * + * The caller must hold the RCU read lock. + * * Return: binary type list entry on success, NULL on failure */ static Node *search_binfmt_handler(struct binfmt_misc *misc, @@ -95,7 +99,7 @@ static Node *search_binfmt_handler(struct binfmt_misc *misc, Node *e; /* Walk all the registered handlers. */ - hlist_for_each_entry(e, &misc->entries, node) { + hlist_for_each_entry_rcu(e, &misc->entries, node) { char *s; int j; @@ -134,7 +138,10 @@ static Node *search_binfmt_handler(struct binfmt_misc *misc, * @bprm: binary for which we are looking for a handler * * Try to find a binfmt handler for the binary type. If one is found take a - * reference to protect against removal via bm_{entry,status}_write(). + * reference to protect against removal via bm_{entry,status}_write(). The + * refcount of an entry can only drop to zero once it has been unlinked and + * a restarted search cannot find an unlinked entry again so the retry loop + * is bounded. * * Return: binary type list entry on success, NULL on failure */ @@ -143,11 +150,10 @@ static Node *get_binfmt_handler(struct binfmt_misc *misc, { Node *e; - read_lock(&misc->entries_lock); - e = search_binfmt_handler(misc, bprm); - if (e) - refcount_inc(&e->users); - read_unlock(&misc->entries_lock); + guard(rcu)(); + do { + e = search_binfmt_handler(misc, bprm); + } while (e && !refcount_inc_not_zero(&e->users)); return e; } @@ -166,7 +172,8 @@ static void put_binfmt_handler(Node *e) exe_file_allow_write_access(e->interp_file); filp_close(e->interp_file, NULL); } - kfree(e); + /* Lockless walkers may still dereference this entry. */ + kfree_rcu(e, rcu); } } @@ -678,10 +685,10 @@ static void bm_evict_inode(struct inode *inode) struct binfmt_misc *misc; misc = i_binfmt_misc(inode); - write_lock(&misc->entries_lock); + spin_lock(&misc->entries_lock); if (!hlist_unhashed(&e->node)) - hlist_del_init(&e->node); - write_unlock(&misc->entries_lock); + hlist_del_init_rcu(&e->node); + spin_unlock(&misc->entries_lock); put_binfmt_handler(e); } } @@ -700,9 +707,9 @@ static void bm_evict_inode(struct inode *inode) */ static void remove_binfmt_handler(struct binfmt_misc *misc, Node *e) { - write_lock(&misc->entries_lock); - hlist_del_init(&e->node); - write_unlock(&misc->entries_lock); + spin_lock(&misc->entries_lock); + hlist_del_init_rcu(&e->node); + spin_unlock(&misc->entries_lock); locked_recursive_removal(e->dentry, NULL); } @@ -753,9 +760,11 @@ static ssize_t bm_entry_write(struct file *file, const char __user *buffer, * via bm_{entry,register,status}_write() inode_lock() on the * root inode must be held. * The lock is exclusive ensuring that the list can't be - * modified. Only load_misc_binary() can access but does so - * read-only. So we only need to take the write lock when we - * actually remove the entry from the list. + * modified. Only load_misc_binary() can access the list + * concurrently and it does so under RCU. So entries_lock only + * needs to be held when an entry is actually unlinked to + * serialize against bm_evict_inode() during umount which + * unlinks without holding inode_lock. */ if (!hlist_unhashed(&e->node)) remove_binfmt_handler(i_binfmt_misc(inode), e); @@ -800,9 +809,9 @@ static int add_entry(Node *e, struct super_block *sb) d_make_persistent(dentry, inode); misc = i_binfmt_misc(inode); - write_lock(&misc->entries_lock); - hlist_add_head(&e->node, &misc->entries); - write_unlock(&misc->entries_lock); + spin_lock(&misc->entries_lock); + hlist_add_head_rcu(&e->node, &misc->entries); + spin_unlock(&misc->entries_lock); simple_done_creating(dentry); return 0; } @@ -898,9 +907,11 @@ static ssize_t bm_status_write(struct file *file, const char __user *buffer, * via bm_{entry,register,status}_write() inode_lock() on the * root inode must be held. * The lock is exclusive ensuring that the list can't be - * modified. Only load_misc_binary() can access but does so - * read-only. So we only need to take the write lock when we - * actually remove the entry from the list. + * modified. Only load_misc_binary() can access the list + * concurrently and it does so under RCU. So entries_lock only + * needs to be held when an entry is actually unlinked to + * serialize against bm_evict_inode() during umount which + * unlinks without holding inode_lock. */ hlist_for_each_entry_safe(e, next, &misc->entries, node) remove_binfmt_handler(misc, e); @@ -973,7 +984,7 @@ static int bm_fill_super(struct super_block *sb, struct fs_context *fc) return -ENOMEM; INIT_HLIST_HEAD(&misc->entries); - rwlock_init(&misc->entries_lock); + spin_lock_init(&misc->entries_lock); /* Pairs with smp_load_acquire() in load_binfmt_misc(). */ smp_store_release(&user_ns->binfmt_misc, misc); diff --git a/include/linux/binfmts.h b/include/linux/binfmts.h index 071da63f2b48..7e7333b7bb0f 100644 --- a/include/linux/binfmts.h +++ b/include/linux/binfmts.h @@ -102,7 +102,7 @@ struct linux_binfmt { #if IS_ENABLED(CONFIG_BINFMT_MISC) struct binfmt_misc { struct hlist_head entries; - rwlock_t entries_lock; + spinlock_t entries_lock; bool enabled; } __randomize_layout; diff --git a/kernel/user.c b/kernel/user.c index c6a2bfb4d918..21bafdc11379 100644 --- a/kernel/user.c +++ b/kernel/user.c @@ -25,7 +25,7 @@ struct binfmt_misc init_binfmt_misc = { .entries = HLIST_HEAD_INIT, .enabled = true, - .entries_lock = __RW_LOCK_UNLOCKED(init_binfmt_misc.entries_lock), + .entries_lock = __SPIN_LOCK_UNLOCKED(init_binfmt_misc.entries_lock), }; EXPORT_SYMBOL_GPL(init_binfmt_misc); #endif From 1dc88208cfdce26858c59609242f2bb0e2b5c031 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 10 Jul 2026 11:33:07 +0200 Subject: [PATCH 03/63] binfmt_misc: annotate racy accesses to ->enabled ->enabled has always been read and written locklessly: every exec reads it in load_misc_binary() while bm_status_write() or a concurrent remount via bm_fill_super() may flip it. That is fine as it is an independent boolean toggle but the accesses should be marked accordingly for KCSAN. Annotate them with READ_ONCE()/WRITE_ONCE(). Link: https://patch.msgid.link/20260710-work-binfmt_misc-locking-v3-6-a162f7cb58d6@kernel.org Reviewed-by: Jori Koolstra Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index 236ebaf3be5c..0e56eb225862 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -217,7 +217,7 @@ static int load_misc_binary(struct linux_binprm *bprm) struct binfmt_misc *misc; misc = load_binfmt_misc(); - if (!misc->enabled) + if (!READ_ONCE(misc->enabled)) return retval; fmt = get_binfmt_handler(misc, bprm); @@ -874,7 +874,7 @@ bm_status_read(struct file *file, char __user *buf, size_t nbytes, loff_t *ppos) char *s; misc = i_binfmt_misc(file_inode(file)); - s = misc->enabled ? "enabled\n" : "disabled\n"; + s = READ_ONCE(misc->enabled) ? "enabled\n" : "disabled\n"; return simple_read_from_buffer(buf, nbytes, ppos, s, strlen(s)); } @@ -891,11 +891,11 @@ static ssize_t bm_status_write(struct file *file, const char __user *buffer, switch (res) { case 1: /* Disable all handlers. */ - misc->enabled = false; + WRITE_ONCE(misc->enabled, false); break; case 2: /* Enable all handlers. */ - misc->enabled = true; + WRITE_ONCE(misc->enabled, true); break; case 3: /* Delete all handlers. */ @@ -1000,7 +1000,7 @@ static int bm_fill_super(struct super_block *sb, struct fs_context *fc) * is true. Instead, if someone mounts binfmt_misc for the first time or * again we simply reset ->enabled to true. */ - misc->enabled = true; + WRITE_ONCE(misc->enabled, true); err = simple_fill_super(sb, BINFMTFS_MAGIC, bm_files); if (!err) From c9fa1f1ccf427e181df27d5450079ef06d6b236b Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 10 Jul 2026 11:33:08 +0200 Subject: [PATCH 04/63] binfmt_misc: turn the entry bit numbers into a proper enum Enabled and Magic are bit numbers in the flags word of an entry but are declared as bare, unprefixed enumerators with implicit values in a style that predates the git history. Give the enum a name, explicit bit numbers and namespaced names and use BIT() instead of open-coding the shifts when building the initial flags word in create_entry(). No functional change. Link: https://patch.msgid.link/20260710-work-binfmt_misc-locking-v3-7-a162f7cb58d6@kernel.org Reviewed-by: Jori Koolstra Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index 0e56eb225862..42b4378ffab6 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -42,7 +42,11 @@ enum { VERBOSE_STATUS = 1 /* make it zero to save 400 bytes kernel memory */ }; -enum {Enabled, Magic}; +/* Entry status and match type bit numbers. */ +enum binfmt_misc_entry_bits { + MISC_FMT_ENABLED_BIT = 0, + MISC_FMT_MAGIC_BIT = 1, +}; #define MISC_FMT_PRESERVE_ARGV0 (1UL << 31) #define MISC_FMT_OPEN_BINARY (1UL << 30) #define MISC_FMT_CREDENTIALS (1UL << 29) @@ -104,11 +108,11 @@ static Node *search_binfmt_handler(struct binfmt_misc *misc, int j; /* Make sure this one is currently enabled. */ - if (!test_bit(Enabled, &e->flags)) + if (!test_bit(MISC_FMT_ENABLED_BIT, &e->flags)) continue; /* Do matching based on extension if applicable. */ - if (!test_bit(Magic, &e->flags)) { + if (!test_bit(MISC_FMT_MAGIC_BIT, &e->flags)) { if (p && !strcmp(e->magic, p + 1)) return e; continue; @@ -416,11 +420,11 @@ static Node *create_entry(const char __user *buffer, size_t count) switch (*p++) { case 'E': pr_debug("register: type: E (extension)\n"); - e->flags = 1 << Enabled; + e->flags = BIT(MISC_FMT_ENABLED_BIT); break; case 'M': pr_debug("register: type: M (magic)\n"); - e->flags = (1 << Enabled) | (1 << Magic); + e->flags = BIT(MISC_FMT_ENABLED_BIT) | BIT(MISC_FMT_MAGIC_BIT); break; default: goto einval; @@ -428,7 +432,7 @@ static Node *create_entry(const char __user *buffer, size_t count) if (*p++ != del) goto einval; - if (test_bit(Magic, &e->flags)) { + if (test_bit(MISC_FMT_MAGIC_BIT, &e->flags)) { /* Handle the 'M' (magic) format. */ char *s; @@ -598,7 +602,7 @@ static void entry_status(Node *e, char *page) char *dp = page; const char *status = "disabled"; - if (test_bit(Enabled, &e->flags)) + if (test_bit(MISC_FMT_ENABLED_BIT, &e->flags)) status = "enabled"; if (!VERBOSE_STATUS) { @@ -620,7 +624,7 @@ static void entry_status(Node *e, char *page) *dp++ = 'F'; *dp++ = '\n'; - if (!test_bit(Magic, &e->flags)) { + if (!test_bit(MISC_FMT_MAGIC_BIT, &e->flags)) { sprintf(dp, "extension .%s\n", e->magic); } else { dp += sprintf(dp, "offset %i\nmagic ", e->offset); @@ -744,11 +748,11 @@ static ssize_t bm_entry_write(struct file *file, const char __user *buffer, switch (res) { case 1: /* Disable this handler. */ - clear_bit(Enabled, &e->flags); + clear_bit(MISC_FMT_ENABLED_BIT, &e->flags); break; case 2: /* Enable this handler. */ - set_bit(Enabled, &e->flags); + set_bit(MISC_FMT_ENABLED_BIT, &e->flags); break; case 3: /* Delete this handler. */ From 9eca1a625c4bdf3b2a4f36dc88722264c7fb4379 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 10 Jul 2026 11:33:09 +0200 Subject: [PATCH 05/63] binfmt_misc: turn the entry behavior flags into an enum The MISC_FMT_* behavior flags are macros using unsigned long literals while the entry bit numbers right above them are now a proper enum. Move the flags into an enum as well so every flags word constant is declared in one form and shows up in debuginfo. (1U << N) keeps the enumerators within unsigned int range which is well-defined for enum constants and the values are unchanged when promoted to the unsigned long flags word. No functional change. Link: https://patch.msgid.link/20260710-work-binfmt_misc-locking-v3-8-a162f7cb58d6@kernel.org Reviewed-by: Jori Koolstra Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index 42b4378ffab6..9d4bbc398737 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -47,10 +47,14 @@ enum binfmt_misc_entry_bits { MISC_FMT_ENABLED_BIT = 0, MISC_FMT_MAGIC_BIT = 1, }; -#define MISC_FMT_PRESERVE_ARGV0 (1UL << 31) -#define MISC_FMT_OPEN_BINARY (1UL << 30) -#define MISC_FMT_CREDENTIALS (1UL << 29) -#define MISC_FMT_OPEN_FILE (1UL << 28) + +/* Entry behavior flags, fixed at registration time. */ +enum binfmt_misc_entry_flags { + MISC_FMT_PRESERVE_ARGV0 = (1U << 31), + MISC_FMT_OPEN_BINARY = (1U << 30), + MISC_FMT_CREDENTIALS = (1U << 29), + MISC_FMT_OPEN_FILE = (1U << 28), +}; typedef struct { struct hlist_node node; From e22835c83df441e8588d06c60a71cf5c2801f196 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 10 Jul 2026 11:33:10 +0200 Subject: [PATCH 06/63] binfmt_misc: rename Node to struct binfmt_misc_entry The CamelCase Node typedef is a 1997 leftover and hides that this is a plain struct. Call it what it is: struct binfmt_misc_entry, matching struct binfmt_misc that it hangs off of and the entry bit and flag enums. Drop the typedef, switch the size computations in create_entry() to sizeof(*e) and adjust the comments that still referred to the old name. No functional change. Link: https://patch.msgid.link/20260710-work-binfmt_misc-locking-v3-9-a162f7cb58d6@kernel.org Reviewed-by: Jori Koolstra Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 60 +++++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index 9d4bbc398737..a4206c0ee401 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -56,7 +56,7 @@ enum binfmt_misc_entry_flags { MISC_FMT_OPEN_FILE = (1U << 28), }; -typedef struct { +struct binfmt_misc_entry { struct hlist_node node; unsigned long flags; /* type, status, etc. */ int offset; /* offset of magic */ @@ -69,7 +69,7 @@ typedef struct { struct file *interp_file; refcount_t users; /* sync removal with load_misc_binary() */ struct rcu_head rcu; -} Node; +}; static struct file_system_type bm_fs_type; @@ -84,7 +84,7 @@ static struct file_system_type bm_fs_type; * - interp: ~50 bytes * - flags: 5 bytes * Round that up a bit, and then back off to hold the internal data - * (like struct Node). + * (like struct binfmt_misc_entry). */ #define MAX_REGISTER_LENGTH 1920 @@ -100,11 +100,11 @@ static struct file_system_type bm_fs_type; * * Return: binary type list entry on success, NULL on failure */ -static Node *search_binfmt_handler(struct binfmt_misc *misc, - struct linux_binprm *bprm) +static struct binfmt_misc_entry * +search_binfmt_handler(struct binfmt_misc *misc, struct linux_binprm *bprm) { char *p = strrchr(bprm->interp, '.'); - Node *e; + struct binfmt_misc_entry *e; /* Walk all the registered handlers. */ hlist_for_each_entry_rcu(e, &misc->entries, node) { @@ -153,10 +153,10 @@ static Node *search_binfmt_handler(struct binfmt_misc *misc, * * Return: binary type list entry on success, NULL on failure */ -static Node *get_binfmt_handler(struct binfmt_misc *misc, - struct linux_binprm *bprm) +static struct binfmt_misc_entry *get_binfmt_handler(struct binfmt_misc *misc, + struct linux_binprm *bprm) { - Node *e; + struct binfmt_misc_entry *e; guard(rcu)(); do { @@ -166,14 +166,14 @@ static Node *get_binfmt_handler(struct binfmt_misc *misc, } /** - * put_binfmt_handler - put binary handler node - * @e: node to put + * put_binfmt_handler - put binary handler entry + * @e: entry to put * - * Free node syncing with load_misc_binary() and defer final free to + * Free entry syncing with load_misc_binary() and defer final free to * load_misc_binary() in case it is using the binary type handler we were * requested to remove. */ -static void put_binfmt_handler(Node *e) +static void put_binfmt_handler(struct binfmt_misc_entry *e) { if (refcount_dec_and_test(&e->users)) { if (e->flags & MISC_FMT_OPEN_FILE) { @@ -219,7 +219,7 @@ static struct binfmt_misc *load_binfmt_misc(void) */ static int load_misc_binary(struct linux_binprm *bprm) { - Node *fmt; + struct binfmt_misc_entry *fmt; struct file *interp_file = NULL; int retval = -ENOEXEC; struct binfmt_misc *misc; @@ -289,7 +289,7 @@ static int load_misc_binary(struct linux_binprm *bprm) ret: /* - * If we actually put the node here all concurrent calls to + * If we actually put the entry here all concurrent calls to * load_misc_binary() will have finished. We also know * that for the refcount to be zero someone must have concurently * removed the binary type handler from the list and it's our job to @@ -325,7 +325,7 @@ static char *scanarg(char *s, char del) return s; } -static char *check_special_flags(char *sfs, Node *e) +static char *check_special_flags(char *sfs, struct binfmt_misc_entry *e) { char *p = sfs; int cont = 1; @@ -369,9 +369,10 @@ static char *check_special_flags(char *sfs, Node *e) * ':name:type:offset:magic:mask:interpreter:flags' * where the ':' is the IFS, that can be chosen with the first char */ -static Node *create_entry(const char __user *buffer, size_t count) +static struct binfmt_misc_entry *create_entry(const char __user *buffer, + size_t count) { - Node *e; + struct binfmt_misc_entry *e; int memsize, err; char *buf, *p; char del; @@ -384,14 +385,14 @@ static Node *create_entry(const char __user *buffer, size_t count) goto out; err = -ENOMEM; - memsize = sizeof(Node) + count + 8; + memsize = sizeof(*e) + count + 8; e = kmalloc(memsize, GFP_KERNEL_ACCOUNT); if (!e) goto out; - p = buf = (char *)e + sizeof(Node); + p = buf = (char *)e + sizeof(*e); - memset(e, 0, sizeof(Node)); + memset(e, 0, sizeof(*e)); if (copy_from_user(buf, buffer, count)) goto efault; @@ -601,7 +602,7 @@ static int parse_command(const char __user *buffer, size_t count) /* generic stuff */ -static void entry_status(Node *e, char *page) +static void entry_status(struct binfmt_misc_entry *e, char *page) { char *dp = page; const char *status = "disabled"; @@ -685,7 +686,7 @@ static struct binfmt_misc *i_binfmt_misc(struct inode *inode) */ static void bm_evict_inode(struct inode *inode) { - Node *e = inode->i_private; + struct binfmt_misc_entry *e = inode->i_private; clear_inode(inode); @@ -713,7 +714,8 @@ static void bm_evict_inode(struct inode *inode) * to use writes to files in order to delete binary type handlers. But it has * worked for so long that it's not a pressing issue. */ -static void remove_binfmt_handler(struct binfmt_misc *misc, Node *e) +static void remove_binfmt_handler(struct binfmt_misc *misc, + struct binfmt_misc_entry *e) { spin_lock(&misc->entries_lock); hlist_del_init_rcu(&e->node); @@ -726,7 +728,7 @@ static void remove_binfmt_handler(struct binfmt_misc *misc, Node *e) static ssize_t bm_entry_read(struct file *file, char __user *buf, size_t nbytes, loff_t *ppos) { - Node *e = file_inode(file)->i_private; + struct binfmt_misc_entry *e = file_inode(file)->i_private; ssize_t res; char *page; @@ -746,7 +748,7 @@ static ssize_t bm_entry_write(struct file *file, const char __user *buffer, size_t count, loff_t *ppos) { struct inode *inode = file_inode(file); - Node *e = inode->i_private; + struct binfmt_misc_entry *e = inode->i_private; int res = parse_command(buffer, count); switch (res) { @@ -795,7 +797,7 @@ static const struct file_operations bm_entry_operations = { /* /register */ /* add to filesystem */ -static int add_entry(Node *e, struct super_block *sb) +static int add_entry(struct binfmt_misc_entry *e, struct super_block *sb) { struct dentry *dentry = simple_start_creating(sb->s_root, e->name); struct inode *inode; @@ -827,7 +829,7 @@ static int add_entry(Node *e, struct super_block *sb) static ssize_t bm_register_write(struct file *file, const char __user *buffer, size_t count, loff_t *ppos) { - Node *e; + struct binfmt_misc_entry *e; struct super_block *sb = file_inode(file)->i_sb; int err = 0; struct file *f = NULL; @@ -893,7 +895,7 @@ static ssize_t bm_status_write(struct file *file, const char __user *buffer, int res = parse_command(buffer, count); struct hlist_node *next; struct inode *inode; - Node *e; + struct binfmt_misc_entry *e; misc = i_binfmt_misc(file_inode(file)); switch (res) { From e496ea42ced2540135baf7dfd208ea33be3a9c1d Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 10 Jul 2026 11:33:11 +0200 Subject: [PATCH 07/63] binfmt_misc: remove the VERBOSE_STATUS toggle VERBOSE_STATUS is a compile-time constant that has been fixed to 1 for as long as git history reaches. Turning it off requires editing the source and yields entry files that only ever report "enabled"/"disabled", a format nothing has ever seen in the wild. Remove the pretend knob and the dead branch it guards. Link: https://patch.msgid.link/20260710-work-binfmt_misc-locking-v3-10-a162f7cb58d6@kernel.org Reviewed-by: Jori Koolstra Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 9 --------- 1 file changed, 9 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index a4206c0ee401..0880b058d3b6 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -38,10 +38,6 @@ # define USE_DEBUG 0 #endif -enum { - VERBOSE_STATUS = 1 /* make it zero to save 400 bytes kernel memory */ -}; - /* Entry status and match type bit numbers. */ enum binfmt_misc_entry_bits { MISC_FMT_ENABLED_BIT = 0, @@ -610,11 +606,6 @@ static void entry_status(struct binfmt_misc_entry *e, char *page) if (test_bit(MISC_FMT_ENABLED_BIT, &e->flags)) status = "enabled"; - if (!VERBOSE_STATUS) { - sprintf(page, "%s\n", status); - return; - } - dp += sprintf(dp, "%s\ninterpreter %s\n", status, e->interpreter); /* print the special flags */ From 18698b35b48bd6198c576d889bec70c50acf5758 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 10 Jul 2026 11:33:12 +0200 Subject: [PATCH 08/63] binfmt_misc: use print_hex_dump_debug() for the register debug output The hex dumps in create_entry() are compiled out unless someone edits the file to define DEBUG while the pr_debug() calls right next to them are dynamic-debug aware. Switch the dumps to print_hex_dump_debug() which follows the same rules as pr_debug() so the register parsing debug output is uniformly controlled through dynamic debug, and remove the USE_DEBUG machinery. Drop the magic[masked] dump instead of converting it: it printed the bitwise AND of two buffers dumped right above it and required a temporary allocation on every registration just to recompute what the reader can derive from the magic and mask dumps directly. Link: https://patch.msgid.link/20260710-work-binfmt_misc-locking-v3-11-a162f7cb58d6@kernel.org Reviewed-by: Jori Koolstra Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 52 ++++++++++++++---------------------------------- 1 file changed, 15 insertions(+), 37 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index 0880b058d3b6..ab715618142e 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -32,12 +32,6 @@ #include "internal.h" -#ifdef DEBUG -# define USE_DEBUG 1 -#else -# define USE_DEBUG 0 -#endif - /* Entry status and match type bit numbers. */ enum binfmt_misc_entry_bits { MISC_FMT_ENABLED_BIT = 0, @@ -459,10 +453,9 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, goto einval; if (!e->magic[0]) goto einval; - if (USE_DEBUG) - print_hex_dump_bytes( - KBUILD_MODNAME ": register: magic[raw]: ", - DUMP_PREFIX_NONE, e->magic, p - e->magic); + print_hex_dump_debug( + KBUILD_MODNAME ": register: magic[raw]: ", + DUMP_PREFIX_NONE, 16, 1, e->magic, p - e->magic, true); /* Parse the 'mask' field. */ e->mask = p; @@ -472,10 +465,12 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, if (!e->mask[0]) { e->mask = NULL; pr_debug("register: mask[raw]: none\n"); - } else if (USE_DEBUG) - print_hex_dump_bytes( + } else { + print_hex_dump_debug( KBUILD_MODNAME ": register: mask[raw]: ", - DUMP_PREFIX_NONE, e->mask, p - e->mask); + DUMP_PREFIX_NONE, 16, 1, e->mask, p - e->mask, + true); + } /* * Decode the magic & mask fields. @@ -491,30 +486,13 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, BINPRM_BUF_SIZE - e->size < e->offset) goto einval; pr_debug("register: magic/mask length: %i\n", e->size); - if (USE_DEBUG) { - print_hex_dump_bytes( - KBUILD_MODNAME ": register: magic[decoded]: ", - DUMP_PREFIX_NONE, e->magic, e->size); - - if (e->mask) { - int i; - char *masked = kmalloc(e->size, GFP_KERNEL_ACCOUNT); - - print_hex_dump_bytes( - KBUILD_MODNAME ": register: mask[decoded]: ", - DUMP_PREFIX_NONE, e->mask, e->size); - - if (masked) { - for (i = 0; i < e->size; ++i) - masked[i] = e->magic[i] & e->mask[i]; - print_hex_dump_bytes( - KBUILD_MODNAME ": register: magic[masked]: ", - DUMP_PREFIX_NONE, masked, e->size); - - kfree(masked); - } - } - } + print_hex_dump_debug( + KBUILD_MODNAME ": register: magic[decoded]: ", + DUMP_PREFIX_NONE, 16, 1, e->magic, e->size, true); + if (e->mask) + print_hex_dump_debug( + KBUILD_MODNAME ": register: mask[decoded]: ", + DUMP_PREFIX_NONE, 16, 1, e->mask, e->size, true); } else { /* Handle the 'E' (extension) format. */ From 811b7e43ff834bdacc2d7714b478cd3db195d18e Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 10 Jul 2026 11:33:13 +0200 Subject: [PATCH 09/63] binfmt_misc: convert the entry file to seq_file Reading an entry file allocates a whole page and formats the status into it with a chain of manually advanced sprintf() calls, silently relying on MAX_REGISTER_LENGTH plus the hex-expanded magic and mask always staying below PAGE_SIZE. Convert the read side to seq_file which sizes its buffer as needed and gets rid of the open-coded pointer arithmetic including the last bin2hex() user in the file. The output is byte for byte identical. seq_open() clears FMODE_PWRITE for historical reasons and would silently turn pwrite() on entry files into -ESPIPE even though bm_entry_write() accepts writes at any offset. Restore the flag in bm_entry_open() the same way kernfs does for its seq_file backed files so pwrite() keeps working. The only user-visible difference is that seeking is now bound by seq_lseek() instead of default_llseek(), i.e. SEEK_END stops working on entry files, which nothing can sensibly use anyway. The status file keeps its simple_read_from_buffer() as it only ever returns one of two fixed strings. Link: https://patch.msgid.link/20260710-work-binfmt_misc-locking-v3-12-a162f7cb58d6@kernel.org Reviewed-by: Jori Koolstra Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 74 +++++++++++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 35 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index ab715618142e..c1abd4fec7d7 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -12,7 +12,6 @@ #include #include -#include #include #include #include @@ -25,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -576,40 +576,47 @@ static int parse_command(const char __user *buffer, size_t count) /* generic stuff */ -static void entry_status(struct binfmt_misc_entry *e, char *page) +static void bm_seq_hex(struct seq_file *m, const u8 *data, int size) { - char *dp = page; - const char *status = "disabled"; + for (int i = 0; i < size; i++) + seq_printf(m, "%02x", data[i]); +} + +static int bm_entry_show(struct seq_file *m, void *unused) +{ + struct binfmt_misc_entry *e = m->private; if (test_bit(MISC_FMT_ENABLED_BIT, &e->flags)) - status = "enabled"; + seq_puts(m, "enabled\n"); + else + seq_puts(m, "disabled\n"); - dp += sprintf(dp, "%s\ninterpreter %s\n", status, e->interpreter); + seq_printf(m, "interpreter %s\n", e->interpreter); /* print the special flags */ - dp += sprintf(dp, "flags: "); + seq_puts(m, "flags: "); if (e->flags & MISC_FMT_PRESERVE_ARGV0) - *dp++ = 'P'; + seq_putc(m, 'P'); if (e->flags & MISC_FMT_OPEN_BINARY) - *dp++ = 'O'; + seq_putc(m, 'O'); if (e->flags & MISC_FMT_CREDENTIALS) - *dp++ = 'C'; + seq_putc(m, 'C'); if (e->flags & MISC_FMT_OPEN_FILE) - *dp++ = 'F'; - *dp++ = '\n'; + seq_putc(m, 'F'); + seq_putc(m, '\n'); if (!test_bit(MISC_FMT_MAGIC_BIT, &e->flags)) { - sprintf(dp, "extension .%s\n", e->magic); + seq_printf(m, "extension .%s\n", e->magic); } else { - dp += sprintf(dp, "offset %i\nmagic ", e->offset); - dp = bin2hex(dp, e->magic, e->size); + seq_printf(m, "offset %i\nmagic ", e->offset); + bm_seq_hex(m, e->magic, e->size); if (e->mask) { - dp += sprintf(dp, "\nmask "); - dp = bin2hex(dp, e->mask, e->size); + seq_puts(m, "\nmask "); + bm_seq_hex(m, e->mask, e->size); } - *dp++ = '\n'; - *dp = '\0'; + seq_putc(m, '\n'); } + return 0; } static struct inode *bm_get_inode(struct super_block *sb, int mode) @@ -694,23 +701,18 @@ static void remove_binfmt_handler(struct binfmt_misc *misc, /* / */ -static ssize_t -bm_entry_read(struct file *file, char __user *buf, size_t nbytes, loff_t *ppos) +static int bm_entry_open(struct inode *inode, struct file *file) { - struct binfmt_misc_entry *e = file_inode(file)->i_private; - ssize_t res; - char *page; + int ret; - page = kmalloc(PAGE_SIZE, GFP_KERNEL); - if (!page) - return -ENOMEM; + ret = single_open(file, bm_entry_show, inode->i_private); + if (ret) + return ret; - entry_status(e, page); - - res = simple_read_from_buffer(buf, nbytes, ppos, page, strlen(page)); - - kfree(page); - return res; + /* seq_open() clears FMODE_PWRITE, bm_entry_write() takes any offset */ + if (file->f_mode & FMODE_WRITE) + file->f_mode |= FMODE_PWRITE; + return 0; } static ssize_t bm_entry_write(struct file *file, const char __user *buffer, @@ -758,9 +760,11 @@ static ssize_t bm_entry_write(struct file *file, const char __user *buffer, } static const struct file_operations bm_entry_operations = { - .read = bm_entry_read, + .open = bm_entry_open, + .read = seq_read, .write = bm_entry_write, - .llseek = default_llseek, + .llseek = seq_lseek, + .release = single_release, }; /* /register */ From f9321c9f95a819aa298d81ad5f5c3b83d8c62698 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 10 Jul 2026 11:33:14 +0200 Subject: [PATCH 10/63] binfmt_misc: factor out the entry matching search_binfmt_handler() open-codes both match types in one loop body with the maskless magic comparison spelled as a manual xor loop that is just memcmp() in disguise. Move the extension and magic checks into helpers so the walk reads as policy - skip disabled entries, match by entry type - and the maskless case actually uses memcmp(). No functional change. Link: https://patch.msgid.link/20260710-work-binfmt_misc-locking-v3-13-a162f7cb58d6@kernel.org Reviewed-by: Jori Koolstra Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 50 ++++++++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index c1abd4fec7d7..f6b75f1ed06c 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -78,6 +78,29 @@ static struct file_system_type bm_fs_type; */ #define MAX_REGISTER_LENGTH 1920 +/* Check if @e's magic matches @bprm's buffer, applying the mask if set. */ +static bool entry_matches_magic(const struct binfmt_misc_entry *e, + const struct linux_binprm *bprm) +{ + const char *s = bprm->buf + e->offset; + int i; + + if (!e->mask) + return !memcmp(s, e->magic, e->size); + + for (i = 0; i < e->size; i++) + if ((s[i] ^ e->magic[i]) & e->mask[i]) + return false; + return true; +} + +/* Check if @e's registered extension matches @ext, NULL if there is none. */ +static bool entry_matches_extension(const struct binfmt_misc_entry *e, + const char *ext) +{ + return ext && !strcmp(e->magic, ext); +} + /** * search_binfmt_handler - search for a binary handler for @bprm * @misc: handle to binfmt_misc instance @@ -93,38 +116,23 @@ static struct file_system_type bm_fs_type; static struct binfmt_misc_entry * search_binfmt_handler(struct binfmt_misc *misc, struct linux_binprm *bprm) { - char *p = strrchr(bprm->interp, '.'); + char *dot = strrchr(bprm->interp, '.'); + const char *ext = dot ? dot + 1 : NULL; struct binfmt_misc_entry *e; /* Walk all the registered handlers. */ hlist_for_each_entry_rcu(e, &misc->entries, node) { - char *s; - int j; - /* Make sure this one is currently enabled. */ if (!test_bit(MISC_FMT_ENABLED_BIT, &e->flags)) continue; - /* Do matching based on extension if applicable. */ - if (!test_bit(MISC_FMT_MAGIC_BIT, &e->flags)) { - if (p && !strcmp(e->magic, p + 1)) + if (test_bit(MISC_FMT_MAGIC_BIT, &e->flags)) { + if (entry_matches_magic(e, bprm)) return e; - continue; - } - - /* Do matching based on magic & mask. */ - s = bprm->buf + e->offset; - if (e->mask) { - for (j = 0; j < e->size; j++) - if ((*s++ ^ e->magic[j]) & e->mask[j]) - break; } else { - for (j = 0; j < e->size; j++) - if ((*s++ ^ e->magic[j])) - break; + if (entry_matches_extension(e, ext)) + return e; } - if (j == e->size) - return e; } return NULL; From 9c17e93afa36a568fcc97a9da66f3c91821fbf95 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 10 Jul 2026 11:33:15 +0200 Subject: [PATCH 11/63] binfmt_misc: rename load_binfmt_misc() to current_binfmt_misc() load_binfmt_misc() is one word swap away from load_misc_binary(), the binfmt loader it serves. It doesn't load anything, it looks up the binfmt_misc instance of the caller's user namespace, so name it after what it returns in the style of current_user_ns() and friends. Tighten the parent walk into a for loop and fix the stale wording and typos in the kernel-doc while at it. Link: https://patch.msgid.link/20260710-work-binfmt_misc-locking-v3-14-a162f7cb58d6@kernel.org Reviewed-by: Jori Koolstra Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index f6b75f1ed06c..7c631001d394 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -184,29 +184,27 @@ static void put_binfmt_handler(struct binfmt_misc_entry *e) } /** - * load_binfmt_misc - load the binfmt_misc of the caller's user namespace + * current_binfmt_misc - get the binfmt_misc instance of the caller's user namespace * - * To be called in load_misc_binary() to load the relevant struct binfmt_misc. - * If a user namespace doesn't have its own binfmt_misc mount it can make use - * of its ancestor's binfmt_misc handlers. This mimicks the behavior of - * pre-namespaced binfmt_misc where all registered binfmt_misc handlers where - * available to all user and user namespaces on the system. + * If a user namespace doesn't have its own binfmt_misc mount it uses the + * handlers of its closest ancestor with one. This mimics the behavior of + * pre-namespaced binfmt_misc where all registered handlers were available + * to all users and user namespaces on the system. The init user namespace + * instance is statically set up so the fallback is never reached in + * practice. * * Return: the binfmt_misc instance of the caller's user namespace */ -static struct binfmt_misc *load_binfmt_misc(void) +static struct binfmt_misc *current_binfmt_misc(void) { const struct user_namespace *user_ns; struct binfmt_misc *misc; - user_ns = current_user_ns(); - while (user_ns) { + for (user_ns = current_user_ns(); user_ns; user_ns = user_ns->parent) { /* Pairs with smp_store_release() in bm_fill_super(). */ misc = smp_load_acquire(&user_ns->binfmt_misc); if (misc) return misc; - - user_ns = user_ns->parent; } return &init_binfmt_misc; @@ -222,7 +220,7 @@ static int load_misc_binary(struct linux_binprm *bprm) int retval = -ENOEXEC; struct binfmt_misc *misc; - misc = load_binfmt_misc(); + misc = current_binfmt_misc(); if (!READ_ONCE(misc->enabled)) return retval; @@ -977,7 +975,7 @@ static int bm_fill_super(struct super_block *sb, struct fs_context *fc) INIT_HLIST_HEAD(&misc->entries); spin_lock_init(&misc->entries_lock); - /* Pairs with smp_load_acquire() in load_binfmt_misc(). */ + /* Pairs with smp_load_acquire() in current_binfmt_misc(). */ smp_store_release(&user_ns->binfmt_misc, misc); } From 0eec8a042817b9a70fd183689e55969d00965d4e Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 10 Jul 2026 11:33:16 +0200 Subject: [PATCH 12/63] binfmt_misc: return errors directly in load_misc_binary() load_misc_binary() seeds retval with the error for checks that happen further down, reassigns it along the way and funnels every exit through a ret label whose only job is dropping the entry reference, so figuring out what an early return actually returns means replaying the assignment history. Give put_binfmt_handler() a cleanup class and take the reference with __free() so every failure can return its error right where the condition is checked. The comment at the label restated what the put_binfmt_handler() kernel-doc already explains, it goes with the label. Drop the dead NULL initialization of interp_file which is assigned on all paths before use. No functional change. Link: https://patch.msgid.link/20260710-work-binfmt_misc-locking-v3-15-a162f7cb58d6@kernel.org Reviewed-by: Jori Koolstra Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 41 ++++++++++++++--------------------------- 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index 7c631001d394..cb66f40eb145 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -183,6 +183,8 @@ static void put_binfmt_handler(struct binfmt_misc_entry *e) } } +DEFINE_FREE(put_binfmt_handler, struct binfmt_misc_entry *, if (_T) put_binfmt_handler(_T)) + /** * current_binfmt_misc - get the binfmt_misc instance of the caller's user namespace * @@ -215,48 +217,47 @@ static struct binfmt_misc *current_binfmt_misc(void) */ static int load_misc_binary(struct linux_binprm *bprm) { - struct binfmt_misc_entry *fmt; - struct file *interp_file = NULL; - int retval = -ENOEXEC; + struct binfmt_misc_entry *fmt __free(put_binfmt_handler) = NULL; + struct file *interp_file; struct binfmt_misc *misc; + int retval; misc = current_binfmt_misc(); if (!READ_ONCE(misc->enabled)) - return retval; + return -ENOEXEC; fmt = get_binfmt_handler(misc, bprm); if (!fmt) - return retval; + return -ENOEXEC; /* Need to be able to load the file after exec */ - retval = -ENOENT; if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE) - goto ret; + return -ENOENT; if (fmt->flags & MISC_FMT_PRESERVE_ARGV0) { bprm->interp_flags |= BINPRM_FLAGS_PRESERVE_ARGV0; } else { retval = remove_arg_zero(bprm); if (retval) - goto ret; + return retval; } /* make argv[1] be the path to the binary */ retval = copy_string_kernel(bprm->interp, bprm); if (retval < 0) - goto ret; + return retval; bprm->argc++; /* add the interp as argv[0] */ retval = copy_string_kernel(fmt->interpreter, bprm); if (retval < 0) - goto ret; + return retval; bprm->argc++; /* Update interp in case binfmt_script needs it. */ retval = bprm_change_interp(fmt->interpreter, bprm); if (retval < 0) - goto ret; + return retval; if (fmt->flags & MISC_FMT_OPEN_FILE) { interp_file = file_clone_open(fmt->interp_file); @@ -271,29 +272,15 @@ static int load_misc_binary(struct linux_binprm *bprm) } else { interp_file = open_exec(fmt->interpreter); } - retval = PTR_ERR(interp_file); if (IS_ERR(interp_file)) - goto ret; + return PTR_ERR(interp_file); bprm->interpreter = interp_file; if (fmt->flags & MISC_FMT_OPEN_BINARY) bprm->have_execfd = 1; if (fmt->flags & MISC_FMT_CREDENTIALS) bprm->execfd_creds = 1; - - retval = 0; -ret: - - /* - * If we actually put the entry here all concurrent calls to - * load_misc_binary() will have finished. We also know - * that for the refcount to be zero someone must have concurently - * removed the binary type handler from the list and it's our job to - * free it. - */ - put_binfmt_handler(fmt); - - return retval; + return 0; } /* Command parsers */ From 9eeca53dacbe1eee15c91c3674bfa9c0c113afe7 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 10 Jul 2026 11:33:17 +0200 Subject: [PATCH 13/63] binfmt_misc: give the parse_command() results names parse_command() maps "0" to 1, "1" to 2 and "-1" to 3 and the write handlers switch on those bare numbers, leaving every reader to redo the mapping in their head. Name the commands and drop the per-case comments that only existed to translate the numbers back. No functional change. Link: https://patch.msgid.link/20260710-work-binfmt_misc-locking-v3-16-a162f7cb58d6@kernel.org Reviewed-by: Jori Koolstra Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index cb66f40eb145..8d5adddaa043 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -542,9 +542,17 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, return ERR_PTR(-EINVAL); } +/* Commands accepted by the /status and / files. */ +enum bm_command { + BM_CMD_IGNORE, /* empty write */ + BM_CMD_DISABLE, /* "0" */ + BM_CMD_ENABLE, /* "1" */ + BM_CMD_REMOVE, /* "-1" */ +}; + /* - * Set status of entry/binfmt_misc: - * '1' enables, '0' disables and '-1' clears entry/binfmt_misc + * Parse what userspace wrote to /status or an entry file: '1' enables, + * '0' disables and '-1' removes the entry or all entries. */ static int parse_command(const char __user *buffer, size_t count) { @@ -555,15 +563,15 @@ static int parse_command(const char __user *buffer, size_t count) if (copy_from_user(s, buffer, count)) return -EFAULT; if (!count) - return 0; + return BM_CMD_IGNORE; if (s[count - 1] == '\n') count--; if (count == 1 && s[0] == '0') - return 1; + return BM_CMD_DISABLE; if (count == 1 && s[0] == '1') - return 2; + return BM_CMD_ENABLE; if (count == 2 && s[0] == '-' && s[1] == '1') - return 3; + return BM_CMD_REMOVE; return -EINVAL; } @@ -716,16 +724,13 @@ static ssize_t bm_entry_write(struct file *file, const char __user *buffer, int res = parse_command(buffer, count); switch (res) { - case 1: - /* Disable this handler. */ + case BM_CMD_DISABLE: clear_bit(MISC_FMT_ENABLED_BIT, &e->flags); break; - case 2: - /* Enable this handler. */ + case BM_CMD_ENABLE: set_bit(MISC_FMT_ENABLED_BIT, &e->flags); break; - case 3: - /* Delete this handler. */ + case BM_CMD_REMOVE: inode = d_inode(inode->i_sb->s_root); inode_lock_nested(inode, I_MUTEX_PARENT); @@ -865,16 +870,13 @@ static ssize_t bm_status_write(struct file *file, const char __user *buffer, misc = i_binfmt_misc(file_inode(file)); switch (res) { - case 1: - /* Disable all handlers. */ + case BM_CMD_DISABLE: WRITE_ONCE(misc->enabled, false); break; - case 2: - /* Enable all handlers. */ + case BM_CMD_ENABLE: WRITE_ONCE(misc->enabled, true); break; - case 3: - /* Delete all handlers. */ + case BM_CMD_REMOVE: inode = d_inode(file_inode(file)->i_sb->s_root); inode_lock_nested(inode, I_MUTEX_PARENT); From b0e42f0dbe61fb92bfa1f76453b4793a5f7dacd3 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 10 Jul 2026 11:33:18 +0200 Subject: [PATCH 14/63] binfmt_misc: factor out the entry removal Both write handlers open-code the same removal dance - grab the root inode lock, unlink, unlock - each carrying a verbatim copy of the same eleven-line locking comment, and bm_entry_write() reuses its inode variable for the root inode halfway through to pull it off. Move the dance into bm_remove_entry() and bm_remove_all_entries() and the locking rules into the kernel-doc of remove_binfmt_handler() which both helpers wrap. No functional change. Link: https://patch.msgid.link/20260710-work-binfmt_misc-locking-v3-17-a162f7cb58d6@kernel.org Reviewed-by: Jori Koolstra Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 84 +++++++++++++++++++++++------------------------- 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index 8d5adddaa043..c354dcd4a3e3 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -685,11 +685,19 @@ static void bm_evict_inode(struct inode *inode) * @e: binary type handler to remove * * Remove a binary type handler from the list of binary type handlers and - * remove its associated dentry. This is called from - * binfmt_{entry,status}_write(). In the future, we might want to think about - * adding a proper ->unlink() method to binfmt_misc instead of forcing caller's - * to use writes to files in order to delete binary type handlers. But it has - * worked for so long that it's not a pressing issue. + * remove its associated dentry. + * + * Adding and removing entries via bm_{entry,register,status}_write() + * happens under the exclusively held inode lock of the root dentry keeping + * the list stable for writers. load_misc_binary() walks it concurrently + * under RCU. The entries_lock is only held around the actual unlink to + * serialize against bm_evict_inode() which unlinks entries during umount + * without holding the root inode lock. + * + * In the future, we might want to think about adding a proper ->unlink() + * method to binfmt_misc instead of forcing callers to use writes to files + * in order to delete binary type handlers. But it has worked for so long + * that it's not a pressing issue. */ static void remove_binfmt_handler(struct binfmt_misc *misc, struct binfmt_misc_entry *e) @@ -700,6 +708,31 @@ static void remove_binfmt_handler(struct binfmt_misc *misc, locked_recursive_removal(e->dentry, NULL); } +/* Remove @e unless a concurrent write already unlinked it. */ +static void bm_remove_entry(struct binfmt_misc_entry *e, struct super_block *sb) +{ + struct inode *root = d_inode(sb->s_root); + + inode_lock_nested(root, I_MUTEX_PARENT); + if (!hlist_unhashed(&e->node)) + remove_binfmt_handler(i_binfmt_misc(root), e); + inode_unlock(root); +} + +/* Remove all entries of the binfmt_misc instance @misc belonging to @sb. */ +static void bm_remove_all_entries(struct binfmt_misc *misc, + struct super_block *sb) +{ + struct inode *root = d_inode(sb->s_root); + struct binfmt_misc_entry *e; + struct hlist_node *next; + + inode_lock_nested(root, I_MUTEX_PARENT); + hlist_for_each_entry_safe(e, next, &misc->entries, node) + remove_binfmt_handler(misc, e); + inode_unlock(root); +} + /* / */ static int bm_entry_open(struct inode *inode, struct file *file) @@ -731,24 +764,7 @@ static ssize_t bm_entry_write(struct file *file, const char __user *buffer, set_bit(MISC_FMT_ENABLED_BIT, &e->flags); break; case BM_CMD_REMOVE: - inode = d_inode(inode->i_sb->s_root); - inode_lock_nested(inode, I_MUTEX_PARENT); - - /* - * In order to add new element or remove elements from the list - * via bm_{entry,register,status}_write() inode_lock() on the - * root inode must be held. - * The lock is exclusive ensuring that the list can't be - * modified. Only load_misc_binary() can access the list - * concurrently and it does so under RCU. So entries_lock only - * needs to be held when an entry is actually unlinked to - * serialize against bm_evict_inode() during umount which - * unlinks without holding inode_lock. - */ - if (!hlist_unhashed(&e->node)) - remove_binfmt_handler(i_binfmt_misc(inode), e); - - inode_unlock(inode); + bm_remove_entry(e, inode->i_sb); break; default: return res; @@ -864,9 +880,6 @@ static ssize_t bm_status_write(struct file *file, const char __user *buffer, { struct binfmt_misc *misc; int res = parse_command(buffer, count); - struct hlist_node *next; - struct inode *inode; - struct binfmt_misc_entry *e; misc = i_binfmt_misc(file_inode(file)); switch (res) { @@ -877,24 +890,7 @@ static ssize_t bm_status_write(struct file *file, const char __user *buffer, WRITE_ONCE(misc->enabled, true); break; case BM_CMD_REMOVE: - inode = d_inode(file_inode(file)->i_sb->s_root); - inode_lock_nested(inode, I_MUTEX_PARENT); - - /* - * In order to add new element or remove elements from the list - * via bm_{entry,register,status}_write() inode_lock() on the - * root inode must be held. - * The lock is exclusive ensuring that the list can't be - * modified. Only load_misc_binary() can access the list - * concurrently and it does so under RCU. So entries_lock only - * needs to be held when an entry is actually unlinked to - * serialize against bm_evict_inode() during umount which - * unlinks without holding inode_lock. - */ - hlist_for_each_entry_safe(e, next, &misc->entries, node) - remove_binfmt_handler(misc, e); - - inode_unlock(inode); + bm_remove_all_entries(misc, file_inode(file)->i_sb); break; default: return res; From 30f53f322f9d85e27751a81bbd5a8a920721fc0b Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 10 Jul 2026 11:33:19 +0200 Subject: [PATCH 15/63] binfmt_misc: simplify check_special_flags() Replace the cont flag and the pointer increment repeated in every case with a for loop that returns from the default case, and shrink the multi-line 'C implies O' remark to one line. No functional change. Link: https://patch.msgid.link/20260710-work-binfmt_misc-locking-v3-18-a162f7cb58d6@kernel.org Reviewed-by: Jori Koolstra Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index c354dcd4a3e3..50984d59b96d 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -308,43 +308,31 @@ static char *scanarg(char *s, char del) return s; } -static char *check_special_flags(char *sfs, struct binfmt_misc_entry *e) +static char *check_special_flags(char *p, struct binfmt_misc_entry *e) { - char *p = sfs; - int cont = 1; - - /* special flags */ - while (cont) { + for (;; p++) { switch (*p) { case 'P': pr_debug("register: flag: P (preserve argv0)\n"); - p++; e->flags |= MISC_FMT_PRESERVE_ARGV0; break; case 'O': pr_debug("register: flag: O (open binary)\n"); - p++; e->flags |= MISC_FMT_OPEN_BINARY; break; case 'C': pr_debug("register: flag: C (preserve creds)\n"); - p++; - /* this flags also implies the - open-binary flag */ - e->flags |= (MISC_FMT_CREDENTIALS | - MISC_FMT_OPEN_BINARY); + /* C implies O */ + e->flags |= MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_BINARY; break; case 'F': pr_debug("register: flag: F: open interpreter file now\n"); - p++; e->flags |= MISC_FMT_OPEN_FILE; break; default: - cont = 0; + return p; } } - - return p; } /* From d9f7f1ebf56d2a571513f0654d6d69544a9504f8 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 10 Jul 2026 11:33:20 +0200 Subject: [PATCH 16/63] binfmt_misc: use a flexible array member for the register string create_entry() allocates the entry and the register string it parses into in one chunk and finds the string part again through manual pointer arithmetic behind a cast. Make the layout explicit with a flexible array member and struct_size(), and give the magic pad of trailing delimiters a name while at it. No functional change. Link: https://patch.msgid.link/20260710-work-binfmt_misc-locking-v3-19-a162f7cb58d6@kernel.org Reviewed-by: Jori Koolstra Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index 50984d59b96d..30a10514cf94 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -59,6 +59,7 @@ struct binfmt_misc_entry { struct file *interp_file; refcount_t users; /* sync removal with load_misc_binary() */ struct rcu_head rcu; + char buf[]; /* register string, fields point in here */ }; static struct file_system_type bm_fs_type; @@ -78,6 +79,9 @@ static struct file_system_type bm_fs_type; */ #define MAX_REGISTER_LENGTH 1920 +/* Trailing delimiter pad so field parsing always terminates at a delimiter. */ +#define MISC_DELIM_PAD 8 + /* Check if @e's magic matches @bprm's buffer, applying the mask if set. */ static bool entry_matches_magic(const struct binfmt_misc_entry *e, const struct linux_binprm *bprm) @@ -344,9 +348,9 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, size_t count) { struct binfmt_misc_entry *e; - int memsize, err; char *buf, *p; char del; + int err; pr_debug("register: received %zu bytes\n", count); @@ -356,12 +360,12 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, goto out; err = -ENOMEM; - memsize = sizeof(*e) + count + 8; - e = kmalloc(memsize, GFP_KERNEL_ACCOUNT); + e = kmalloc(struct_size(e, buf, count + MISC_DELIM_PAD), + GFP_KERNEL_ACCOUNT); if (!e) goto out; - p = buf = (char *)e + sizeof(*e); + p = buf = e->buf; memset(e, 0, sizeof(*e)); if (copy_from_user(buf, buffer, count)) @@ -376,7 +380,7 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, goto einval; /* Pad the buffer with the delim to simplify parsing below. */ - memset(buf + count, del, 8); + memset(buf + count, del, MISC_DELIM_PAD); /* Parse the 'name' field. */ e->name = p; From f98d6db17e0a4ca5aebdc36ec9c321733f4aa3d8 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 10 Jul 2026 11:33:21 +0200 Subject: [PATCH 17/63] binfmt_misc: split the field parsing out of create_entry() create_entry() is a two hundred line parser with the M and E field handling inlined as the two arms of its largest branch. Move them into parse_magic_fields() and parse_extension_fields() which return the new parse position or NULL so create_entry() itself reads like the register string grammar again. The offset parsing loses a provably dead check on the way: after *s = '\0' and p = s the subsequent if (*p++) always reads the just written NUL byte and can never fail, it only obscured that the code simply advances past the delimiter. With the field parsing gone every remaining failure unwinds the same way, so hand the entry to __free(kfree), return errors directly and pass ownership out via no_free_ptr() on success instead of routing every exit through goto tails. Link: https://patch.msgid.link/20260710-work-binfmt_misc-locking-v3-20-a162f7cb58d6@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 225 +++++++++++++++++++++++------------------------ 1 file changed, 108 insertions(+), 117 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index 30a10514cf94..161d7202d895 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -339,6 +339,95 @@ static char *check_special_flags(char *p, struct binfmt_misc_entry *e) } } +/* Parse the 'offset', 'magic' and 'mask' fields of an 'M' entry. */ +static char *parse_magic_fields(struct binfmt_misc_entry *e, char *p, char del) +{ + char *s; + + /* Parse the 'offset' field. */ + s = strchr(p, del); + if (!s) + return NULL; + *s = '\0'; + if (p != s) { + if (kstrtoint(p, 10, &e->offset) || e->offset < 0) + return NULL; + } + p = s + 1; + pr_debug("register: offset: %#x\n", e->offset); + + /* Parse the 'magic' field. */ + e->magic = p; + p = scanarg(p, del); + if (!p || !e->magic[0]) + return NULL; + print_hex_dump_debug( + KBUILD_MODNAME ": register: magic[raw]: ", + DUMP_PREFIX_NONE, 16, 1, e->magic, p - e->magic, true); + + /* Parse the 'mask' field. */ + e->mask = p; + p = scanarg(p, del); + if (!p) + return NULL; + if (!e->mask[0]) { + e->mask = NULL; + pr_debug("register: mask[raw]: none\n"); + } else { + print_hex_dump_debug( + KBUILD_MODNAME ": register: mask[raw]: ", + DUMP_PREFIX_NONE, 16, 1, e->mask, p - e->mask, true); + } + + /* + * Decode the magic & mask fields. Note: while we might have accepted + * embedded NUL bytes from above, the unescape helpers will stop at + * the first one they encounter. + */ + e->size = string_unescape_inplace(e->magic, UNESCAPE_HEX); + if (e->mask && string_unescape_inplace(e->mask, UNESCAPE_HEX) != e->size) + return NULL; + if (e->size > BINPRM_BUF_SIZE || BINPRM_BUF_SIZE - e->size < e->offset) + return NULL; + pr_debug("register: magic/mask length: %i\n", e->size); + print_hex_dump_debug( + KBUILD_MODNAME ": register: magic[decoded]: ", + DUMP_PREFIX_NONE, 16, 1, e->magic, e->size, true); + if (e->mask) + print_hex_dump_debug( + KBUILD_MODNAME ": register: mask[decoded]: ", + DUMP_PREFIX_NONE, 16, 1, e->mask, e->size, true); + return p; +} + +/* Parse the 'magic' field of an 'E' entry: the filename extension. */ +static char *parse_extension_fields(struct binfmt_misc_entry *e, char *p, + char del) +{ + /* Skip the 'offset' field. */ + p = strchr(p, del); + if (!p) + return NULL; + *p++ = '\0'; + + /* Parse the 'magic' field. */ + e->magic = p; + p = strchr(p, del); + if (!p) + return NULL; + *p++ = '\0'; + if (!e->magic[0] || strchr(e->magic, '/')) + return NULL; + pr_debug("register: extension: {%s}\n", e->magic); + + /* Skip the 'mask' field. */ + p = strchr(p, del); + if (!p) + return NULL; + *p++ = '\0'; + return p; +} + /* * This registers a new binary format, it recognises the syntax * ':name:type:offset:magic:mask:interpreter:flags' @@ -347,29 +436,26 @@ static char *check_special_flags(char *p, struct binfmt_misc_entry *e) static struct binfmt_misc_entry *create_entry(const char __user *buffer, size_t count) { - struct binfmt_misc_entry *e; + struct binfmt_misc_entry *e __free(kfree) = NULL; char *buf, *p; char del; - int err; pr_debug("register: received %zu bytes\n", count); /* some sanity checks */ - err = -EINVAL; if ((count < 11) || (count > MAX_REGISTER_LENGTH)) - goto out; + return ERR_PTR(-EINVAL); - err = -ENOMEM; e = kmalloc(struct_size(e, buf, count + MISC_DELIM_PAD), GFP_KERNEL_ACCOUNT); if (!e) - goto out; + return ERR_PTR(-ENOMEM); p = buf = e->buf; memset(e, 0, sizeof(*e)); if (copy_from_user(buf, buffer, count)) - goto efault; + return ERR_PTR(-EFAULT); del = *p++; /* delimeter */ @@ -377,7 +463,7 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, /* A flag-char delimiter runs the flag scan off the buffer. */ if (del == 'P' || del == 'O' || del == 'C' || del == 'F') - goto einval; + return ERR_PTR(-EINVAL); /* Pad the buffer with the delim to simplify parsing below. */ memset(buf + count, del, MISC_DELIM_PAD); @@ -386,13 +472,13 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, e->name = p; p = strchr(p, del); if (!p) - goto einval; + return ERR_PTR(-EINVAL); *p++ = '\0'; if (!e->name[0] || !strcmp(e->name, ".") || !strcmp(e->name, "..") || strchr(e->name, '/')) - goto einval; + return ERR_PTR(-EINVAL); pr_debug("register: name: {%s}\n", e->name); @@ -407,111 +493,26 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, e->flags = BIT(MISC_FMT_ENABLED_BIT) | BIT(MISC_FMT_MAGIC_BIT); break; default: - goto einval; + return ERR_PTR(-EINVAL); } if (*p++ != del) - goto einval; + return ERR_PTR(-EINVAL); - if (test_bit(MISC_FMT_MAGIC_BIT, &e->flags)) { - /* Handle the 'M' (magic) format. */ - char *s; - - /* Parse the 'offset' field. */ - s = strchr(p, del); - if (!s) - goto einval; - *s = '\0'; - if (p != s) { - int r = kstrtoint(p, 10, &e->offset); - if (r != 0 || e->offset < 0) - goto einval; - } - p = s; - if (*p++) - goto einval; - pr_debug("register: offset: %#x\n", e->offset); - - /* Parse the 'magic' field. */ - e->magic = p; - p = scanarg(p, del); - if (!p) - goto einval; - if (!e->magic[0]) - goto einval; - print_hex_dump_debug( - KBUILD_MODNAME ": register: magic[raw]: ", - DUMP_PREFIX_NONE, 16, 1, e->magic, p - e->magic, true); - - /* Parse the 'mask' field. */ - e->mask = p; - p = scanarg(p, del); - if (!p) - goto einval; - if (!e->mask[0]) { - e->mask = NULL; - pr_debug("register: mask[raw]: none\n"); - } else { - print_hex_dump_debug( - KBUILD_MODNAME ": register: mask[raw]: ", - DUMP_PREFIX_NONE, 16, 1, e->mask, p - e->mask, - true); - } - - /* - * Decode the magic & mask fields. - * Note: while we might have accepted embedded NUL bytes from - * above, the unescape helpers here will stop at the first one - * it encounters. - */ - e->size = string_unescape_inplace(e->magic, UNESCAPE_HEX); - if (e->mask && - string_unescape_inplace(e->mask, UNESCAPE_HEX) != e->size) - goto einval; - if (e->size > BINPRM_BUF_SIZE || - BINPRM_BUF_SIZE - e->size < e->offset) - goto einval; - pr_debug("register: magic/mask length: %i\n", e->size); - print_hex_dump_debug( - KBUILD_MODNAME ": register: magic[decoded]: ", - DUMP_PREFIX_NONE, 16, 1, e->magic, e->size, true); - if (e->mask) - print_hex_dump_debug( - KBUILD_MODNAME ": register: mask[decoded]: ", - DUMP_PREFIX_NONE, 16, 1, e->mask, e->size, true); - } else { - /* Handle the 'E' (extension) format. */ - - /* Skip the 'offset' field. */ - p = strchr(p, del); - if (!p) - goto einval; - *p++ = '\0'; - - /* Parse the 'magic' field. */ - e->magic = p; - p = strchr(p, del); - if (!p) - goto einval; - *p++ = '\0'; - if (!e->magic[0] || strchr(e->magic, '/')) - goto einval; - pr_debug("register: extension: {%s}\n", e->magic); - - /* Skip the 'mask' field. */ - p = strchr(p, del); - if (!p) - goto einval; - *p++ = '\0'; - } + if (test_bit(MISC_FMT_MAGIC_BIT, &e->flags)) + p = parse_magic_fields(e, p, del); + else + p = parse_extension_fields(e, p, del); + if (!p) + return ERR_PTR(-EINVAL); /* Parse the 'interpreter' field. */ e->interpreter = p; p = strchr(p, del); if (!p) - goto einval; + return ERR_PTR(-EINVAL); *p++ = '\0'; if (!e->interpreter[0]) - goto einval; + return ERR_PTR(-EINVAL); pr_debug("register: interpreter: {%s}\n", e->interpreter); /* Parse the 'flags' field. */ @@ -519,19 +520,9 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, if (*p == '\n') p++; if (p != buf + count) - goto einval; + return ERR_PTR(-EINVAL); - return e; - -out: - return ERR_PTR(err); - -efault: - kfree(e); - return ERR_PTR(-EFAULT); -einval: - kfree(e); - return ERR_PTR(-EINVAL); + return no_free_ptr(e); } /* Commands accepted by the /status and / files. */ From 8ecfd520eaa46bd79e6b0361f5bb55b144d221b2 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 10 Jul 2026 11:33:22 +0200 Subject: [PATCH 18/63] binfmt_misc: use __free(kfree) in bm_register_write() bm_register_write() has to free the entry it got from create_entry() on every failure until add_entry() has linked it into the filesystem and made the inode its owner. Arm the entry with __free(kfree) so the error branches can simply return and disarm it via retain_and_null_ptr() once ownership has been handed to the inode. The interpreter file keeps its manual error cleanup as freeing the entry would not close it. No functional change. Link: https://patch.msgid.link/20260710-work-binfmt_misc-locking-v3-21-a162f7cb58d6@kernel.org Reviewed-by: Jori Koolstra Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index 161d7202d895..4939e185e24d 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -799,13 +799,12 @@ static int add_entry(struct binfmt_misc_entry *e, struct super_block *sb) static ssize_t bm_register_write(struct file *file, const char __user *buffer, size_t count, loff_t *ppos) { - struct binfmt_misc_entry *e; + struct binfmt_misc_entry *e __free(kfree) = NULL; struct super_block *sb = file_inode(file)->i_sb; - int err = 0; struct file *f = NULL; + int err; e = create_entry(buffer, count); - if (IS_ERR(e)) return PTR_ERR(e); @@ -822,7 +821,6 @@ static ssize_t bm_register_write(struct file *file, const char __user *buffer, if (IS_ERR(f)) { pr_notice("register: failed to install interpreter file %s\n", e->interpreter); - kfree(e); return PTR_ERR(f); } e->interp_file = f; @@ -834,9 +832,11 @@ static ssize_t bm_register_write(struct file *file, const char __user *buffer, exe_file_allow_write_access(f); filp_close(f, NULL); } - kfree(e); return err; } + + /* The entry is owned by its inode now. */ + retain_and_null_ptr(e); return count; } From 1e3fe7ad06f91c08f0f292d6999cc1d31ee2085d Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 10 Jul 2026 11:33:23 +0200 Subject: [PATCH 19/63] binfmt_misc: assorted small cleanups Use umode_t for the mode argument of bm_get_inode(), constify the fixed status strings in bm_status_read(), give the super_operations the bm_ prefix everything else in this file uses, replace the stale scanarg() comment which still described parameters and an err variable it lost decades ago and fix the delimiter typo plus a missing space nearby. No functional change. Link: https://patch.msgid.link/20260710-work-binfmt_misc-locking-v3-22-a162f7cb58d6@kernel.org Reviewed-by: Jori Koolstra Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index 4939e185e24d..c6d7ba459737 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -290,10 +290,9 @@ static int load_misc_binary(struct linux_binprm *bprm) /* Command parsers */ /* - * parses and copies one argument enclosed in del from *sp to *dp, - * recognising the \x special. - * returns pointer to the copied argument or NULL in case of an - * error (and sets err) or null argument length. + * Scan the argument starting at @s up to the delimiter @del, recognising + * the \x escape. Terminates the argument with a NUL and returns a pointer + * past it or NULL on a malformed escape. */ static char *scanarg(char *s, char del) { @@ -308,7 +307,7 @@ static char *scanarg(char *s, char del) return NULL; } } - s[-1] ='\0'; + s[-1] = '\0'; return s; } @@ -457,7 +456,7 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, if (copy_from_user(buf, buffer, count)) return ERR_PTR(-EFAULT); - del = *p++; /* delimeter */ + del = *p++; /* delimiter */ pr_debug("register: delim: %#x {%c}\n", del, del); @@ -603,7 +602,7 @@ static int bm_entry_show(struct seq_file *m, void *unused) return 0; } -static struct inode *bm_get_inode(struct super_block *sb, int mode) +static struct inode *bm_get_inode(struct super_block *sb, umode_t mode) { struct inode *inode = new_inode(sb); @@ -851,7 +850,7 @@ static ssize_t bm_status_read(struct file *file, char __user *buf, size_t nbytes, loff_t *ppos) { struct binfmt_misc *misc; - char *s; + const char *s; misc = i_binfmt_misc(file_inode(file)); s = READ_ONCE(misc->enabled) ? "enabled\n" : "disabled\n"; @@ -890,7 +889,7 @@ static const struct file_operations bm_status_operations = { /* Superblock handling */ -static const struct super_operations s_ops = { +static const struct super_operations bm_super_ops = { .statfs = simple_statfs, .evict_inode = bm_evict_inode, }; @@ -961,7 +960,7 @@ static int bm_fill_super(struct super_block *sb, struct fs_context *fc) err = simple_fill_super(sb, BINFMTFS_MAGIC, bm_files); if (!err) - sb->s_op = &s_ops; + sb->s_op = &bm_super_ops; return err; } From 3ca485a067c650ca8d6d146faae41d130d7324aa Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 10 Jul 2026 11:33:24 +0200 Subject: [PATCH 20/63] binfmt_misc: include what is used The include list still reflects code that left this file years ago: nothing here uses sched/mm.h, pagemap.h, namei.h, syscalls.h or anything from fs/internal.h anymore, mount.h and the bm_fs_type forward declaration lost their last user when the pinned bm_mnt machinery was removed. Drop all of that and instead spell out the headers the file actually relies on but so far pulled in transitively: bitops, bits, bug, cleanup, cred, kstrtox, printk, refcount, string and user_namespace. With that nothing needs the kernel.h grab bag anymore, so it goes too, and the list is sorted alphabetically. Link: https://patch.msgid.link/20260710-work-binfmt_misc-locking-v3-23-a162f7cb58d6@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index c6d7ba459737..62dbf99ca667 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -10,27 +10,29 @@ #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt -#include -#include -#include -#include -#include #include -#include +#include +#include +#include +#include +#include #include -#include #include -#include -#include -#include -#include -#include -#include -#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include - -#include "internal.h" +#include /* Entry status and match type bit numbers. */ enum binfmt_misc_entry_bits { @@ -62,8 +64,6 @@ struct binfmt_misc_entry { char buf[]; /* register string, fields point in here */ }; -static struct file_system_type bm_fs_type; - /* * Max length of the register string. Determined by: * - 7 delimiters From 22c879a60d8248f9941e03145edea7cbd44ad864 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 10 Jul 2026 11:33:25 +0200 Subject: [PATCH 21/63] binfmt_misc: allow removing entries via unlink(2) Removing a binary type handler requires echoing -1 into its entry file which works but is an odd interface to discover for something that already looks like a plain file in a filesystem. The comment on remove_binfmt_handler() has been suggesting a proper ->unlink() method for years, so add one: unlinking an entry file unhashes the entry from the handler list and removes the file, exactly like writing -1 to it does. The status and register control files refuse removal with EPERM the same way binderfs protects binder-control. Writing -1 keeps working. Permission-wise nothing new is exposed: unlink(2) requires write access to the root directory which is owned by the (user namespace) root with mode 0755, matching the privilege needed to write to the 0644 entry files. The VFS calls ->unlink() with the root inode lock held so the existing writer serialization scheme applies unchanged, and eviction of the unlinked inode drops the entry reference exactly as for the write based removal. Document the new way in admin-guide/binfmt-misc.rst. Link: https://patch.msgid.link/20260710-work-binfmt_misc-locking-v3-24-a162f7cb58d6@kernel.org Reviewed-by: Jori Koolstra Signed-off-by: Christian Brauner (Amutable) --- Documentation/admin-guide/binfmt-misc.rst | 3 +- fs/binfmt_misc.c | 77 ++++++++++++++++------- 2 files changed, 58 insertions(+), 22 deletions(-) diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst index c0a34fbf8022..306ef48f5de6 100644 --- a/Documentation/admin-guide/binfmt-misc.rst +++ b/Documentation/admin-guide/binfmt-misc.rst @@ -133,7 +133,8 @@ or 1 (to enable) to ``/proc/sys/fs/binfmt_misc/status`` or Catting the file tells you the current status of ``binfmt_misc/the_entry``. You can remove one entry or all entries by echoing -1 to ``/proc/.../the_name`` -or ``/proc/sys/fs/binfmt_misc/status``. +or ``/proc/sys/fs/binfmt_misc/status``. A single entry can also be removed +by simply unlinking (``rm``) ``/proc/.../the_name``. Hints diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index 62dbf99ca667..7896a50af80d 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -638,8 +638,8 @@ static struct binfmt_misc *i_binfmt_misc(struct inode *inode) * entry is removed or the filesystem is unmounted and the super block is * shutdown. * - * If the ->evict call was not caused by a super block shutdown but by a write - * to remove the entry or all entries via bm_{entry,status}_write() the entry + * If the ->evict call was not caused by a super block shutdown but by + * removing the entry via bm_{entry,status}_write() or unlink(2) the entry * will have already been removed from the list. We keep the hlist_unhashed() * check to make that explicit. */ @@ -661,6 +661,26 @@ static void bm_evict_inode(struct inode *inode) } } +/** + * unlink_binfmt_handler - unhash a binary type handler + * @misc: handle to binfmt_misc instance + * @e: binary type handler to unhash + * + * Adding and removing entries via bm_{entry,register,status}_write() and + * unlink(2) happens under the exclusively held inode lock of the root + * dentry keeping the list stable for writers. load_misc_binary() walks it + * concurrently under RCU. The entries_lock is only held around the actual + * unlink to serialize against bm_evict_inode() which unlinks entries + * during umount without holding the root inode lock. + */ +static void unlink_binfmt_handler(struct binfmt_misc *misc, + struct binfmt_misc_entry *e) +{ + spin_lock(&misc->entries_lock); + hlist_del_init_rcu(&e->node); + spin_unlock(&misc->entries_lock); +} + /** * remove_binfmt_handler - remove a binary type handler * @misc: handle to binfmt_misc instance @@ -668,29 +688,15 @@ static void bm_evict_inode(struct inode *inode) * * Remove a binary type handler from the list of binary type handlers and * remove its associated dentry. - * - * Adding and removing entries via bm_{entry,register,status}_write() - * happens under the exclusively held inode lock of the root dentry keeping - * the list stable for writers. load_misc_binary() walks it concurrently - * under RCU. The entries_lock is only held around the actual unlink to - * serialize against bm_evict_inode() which unlinks entries during umount - * without holding the root inode lock. - * - * In the future, we might want to think about adding a proper ->unlink() - * method to binfmt_misc instead of forcing callers to use writes to files - * in order to delete binary type handlers. But it has worked for so long - * that it's not a pressing issue. */ static void remove_binfmt_handler(struct binfmt_misc *misc, struct binfmt_misc_entry *e) { - spin_lock(&misc->entries_lock); - hlist_del_init_rcu(&e->node); - spin_unlock(&misc->entries_lock); + unlink_binfmt_handler(misc, e); locked_recursive_removal(e->dentry, NULL); } -/* Remove @e unless a concurrent write already unlinked it. */ +/* Remove @e unless it was already removed. */ static void bm_remove_entry(struct binfmt_misc_entry *e, struct super_block *sb) { struct inode *root = d_inode(sb->s_root); @@ -715,6 +721,32 @@ static void bm_remove_all_entries(struct binfmt_misc *misc, inode_unlock(root); } +/** + * bm_unlink - remove a binary type handler via unlink(2) + * @dir: inode of the root directory + * @dentry: entry file to remove + * + * Removing the entry file removes its binary type handler, exactly like + * writing -1 to it does. The status and register control files can't be + * removed. The VFS calls this with the root inode lock held which + * serializes against the write based add and remove paths. + */ +static int bm_unlink(struct inode *dir, struct dentry *dentry) +{ + struct binfmt_misc_entry *e = d_inode(dentry)->i_private; + + if (!e) + return -EPERM; + + unlink_binfmt_handler(i_binfmt_misc(dir), e); + return simple_unlink(dir, dentry); +} + +static const struct inode_operations bm_dir_inode_operations = { + .lookup = simple_lookup, + .unlink = bm_unlink, +}; + /* / */ static int bm_entry_open(struct inode *inode, struct file *file) @@ -959,9 +991,12 @@ static int bm_fill_super(struct super_block *sb, struct fs_context *fc) WRITE_ONCE(misc->enabled, true); err = simple_fill_super(sb, BINFMTFS_MAGIC, bm_files); - if (!err) - sb->s_op = &bm_super_ops; - return err; + if (err) + return err; + + sb->s_op = &bm_super_ops; + d_inode(sb->s_root)->i_op = &bm_dir_inode_operations; + return 0; } static void bm_free(struct fs_context *fc) From dd55a3a9a7a808257bb5d4a1208a814f10107b6b Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 14 Jul 2026 21:58:06 +0200 Subject: [PATCH 22/63] exec: stash bpf-selected interpreter state in struct linux_binprm The upcoming bpf-backed binfmt_misc handlers decide how a binary is run programmatically at exec time: the interpreter itself, an optional single argument to pass to it, and the invocation flags that a static binfmt_misc entry fixes at registration time. The selection runs before load_misc_binary() has copied the binary path from bprm->interp into the argument vector, so the selecting program cannot go through bprm_change_interp() directly without clobbering argv[1]. Stage the selected state in the bprm instead, grouped in struct binfmt_misc_bpf and embedded anonymously in struct linux_binprm so the bprm->bpf_* accesses stay direct. The bprm is exclusively owned by the task doing the exec so no synchronization is needed. The consumers free and clear the fields once the exec attempt that set them is finished; free_bprm() covers all error paths. Link: https://patch.msgid.link/20260714-work-bpf-binfmt_misc-v2-1-57b7529c002c@kernel.org Reviewed-by: Farid Zakaria Signed-off-by: Christian Brauner (Amutable) --- fs/exec.c | 2 ++ include/linux/binfmts.h | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/fs/exec.c b/fs/exec.c index c7b8f2d6366c..41e1684d999c 100644 --- a/fs/exec.c +++ b/fs/exec.c @@ -1418,6 +1418,8 @@ static void free_bprm(struct linux_binprm *bprm) /* If a binfmt changed the interp, free it. */ if (bprm->interp != bprm->filename) kfree(bprm->interp); + kfree(bprm->bpf_interp); + kfree(bprm->bpf_interp_arg); kfree(bprm->fdpath); kfree(bprm); } diff --git a/include/linux/binfmts.h b/include/linux/binfmts.h index 7e7333b7bb0f..03e1794b5cbb 100644 --- a/include/linux/binfmts.h +++ b/include/linux/binfmts.h @@ -12,6 +12,13 @@ struct coredump_params; #define CORENAME_MAX_SIZE 128 +/* Interpreter selection staged by a bpf binfmt_misc handler. */ +struct binfmt_misc_bpf { + const char *bpf_interp; /* interpreter selected by a bpf handler */ + const char *bpf_interp_arg; /* interpreter argument from a bpf handler */ + u64 bpf_flags; /* enum bpf_binprm_flags from a bpf handler */ +}; + /* * This structure is used to hold the arguments that are used when loading binaries. */ @@ -65,6 +72,7 @@ struct linux_binprm { of the time same as filename, but could be different for binfmt_{misc,script} */ const char *fdpath; /* generated filename for execveat */ + struct binfmt_misc_bpf; /* bpf handler interpreter selection */ unsigned interp_flags; int execfd; /* File descriptor of the executable */ unsigned long exec; From b4bfe2f6b0117f3d8de6430bdaee10094383e97a Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 14 Jul 2026 21:58:07 +0200 Subject: [PATCH 23/63] binfmt_misc: add binfmt_misc_ops bpf struct_ops Add the bpf plumbing for binary type handlers whose matching and interpreter selection are implemented by bpf programs instead of a fixed magic/extension and a fixed interpreter string recorded at registration time. This serves relocatable binary formats where the interpreter must be computed per binary, e.g. relative to the location of the binary itself, as discussed for hermetic Nix-style executables. A handler is an instance of the new binfmt_misc_ops struct_ops with a name that binfmt_misc entries reference it by and two ops: bool (*match)(struct linux_binprm *bprm); int (*load)(struct linux_binprm *bprm); struct_ops is the sanctioned mechanism for this kind of user-supplied policy callback: program types, attach types, and the uapi helper list are frozen, and every recently added subsystem hook (bpf qdisc, SMC handshake control, io_uring loop ops, sched_ext) is a struct_ops user. The ops receive the bprm as a trusted BTF pointer, so a program can match on the header in bprm->buf, read arbitrary file content via bpf_dynptr_from_file() to parse e.g. ELF program headers, and inspect the binary's location. No dedicated program type, ctx blob, or uapi helper is needed. The two ops split along what they decide, not what they may do: the match program decides whether the handler applies to a binary, the load program decides how a matched binary is run. Both are required to be sleepable. Matching cannot be limited to the prefetched 256 bytes in bprm->buf: deciding whether a handler applies takes e.g. parsing the ELF program headers to find an interpreter segment, which sits at an arbitrary file offset, and non-sleepable file reads are limited to whatever happens to be resident in the page cache. A match program that cannot read the file reliably would have to match broadly and leave the rejection to its load program, which breaks first-match-wins entry semantics the moment more than one handler is registered. Reliable file reads at exec time fault in the file's pages, so both ops must be able to sleep. This also constrains the caller: binfmt_misc must invoke both from sleepable context, which a later patch takes care of. Both ops are required; a handler that wants to decide everything from the load program supplies a match program that just returns true. The load program communicates its decisions through three new kfuncs: int bpf_binprm_set_interp(struct linux_binprm *bprm, const char *path, size_t path__sz); selects the interpreter and enforces an absolute path shorter than PATH_MAX. int bpf_binprm_set_interp_arg(struct linux_binprm *bprm, const char *arg, size_t arg__sz); passes a single optional argument to the interpreter, mirroring the optional argument of a #! interpreter line - something a static entry cannot express at all. int bpf_binprm_set_flags(struct linux_binprm *bprm, enum bpf_binprm_flags flags); chooses the invocation flags for this exec, with BPF_BINPRM_PRESERVE_ARGV0, BPF_BINPRM_CREDENTIALS and BPF_BINPRM_EXECFD mapping to 'P', 'C' and 'O'. Unknown bits are rejected so a program built against a newer kernel fails loudly on an older one rather than silently losing a flag. Repeated calls replace the staged flags and a zero argument clears them again - the set-or-clear semantics of bpf_bprm_opts_set() on the same struct. A flags word carries this better than a kfunc per flag: it is one call, it is set atomically, and new behaviour is a new bit rather than new surface - the same shape the register string's flags field already has. All three stage their result in the bprm; consuming it from load_misc_binary() is wired up by the following patches. The bprm is exclusively owned by the task doing the exec, so no shared or per-CPU state is involved and nothing here can race. The kfuncs are registered for struct_ops programs with a filter that limits them to the load program of a binfmt_misc_ops instance, keyed off the struct_ops member offset the program attaches to: match decides whether a handler applies, load decides how the binary is run, and the verifier enforces that split at program load time. Registering an ops instance (updating the struct_ops map or attaching its link) publishes the handler under its name in a registry keyed by the registering task's user namespace. Lookups do not walk that hierarchy: a handler is only visible in the user namespace it was registered in, so an entry can only reference a handler registered in the same user namespace as its binfmt_misc instance. Consumers take a reference on the ops via bpf_struct_ops_get() which pins the underlying map and programs, so an activated handler keeps working even if the map is deleted or the registering container goes away; deregistration only prevents new activations, exactly like unregistering a tcp congestion ops with live users. Link: https://lore.kernel.org/20260704211409.1978485-1-farid.m.zakaria@gmail.com Link: https://patch.msgid.link/20260714-work-bpf-binfmt_misc-v2-2-57b7529c002c@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/Kconfig.binfmt | 14 ++ fs/Makefile | 1 + fs/binfmt_misc_bpf.c | 351 ++++++++++++++++++++++++++++++++++++ include/linux/binfmt_misc.h | 71 ++++++++ 4 files changed, 437 insertions(+) create mode 100644 fs/binfmt_misc_bpf.c create mode 100644 include/linux/binfmt_misc.h diff --git a/fs/Kconfig.binfmt b/fs/Kconfig.binfmt index 1949e25c7741..daeac4889d03 100644 --- a/fs/Kconfig.binfmt +++ b/fs/Kconfig.binfmt @@ -168,6 +168,20 @@ config BINFMT_MISC you have use for it; the module is called binfmt_misc. If you don't know what to answer at this point, say Y. +config BINFMT_MISC_BPF + bool "BPF-selected interpreters for misc binaries" + depends on BINFMT_MISC=y + depends on BPF_SYSCALL && BPF_JIT && DEBUG_INFO_BTF + help + Allow binfmt_misc binary type handlers to be implemented as bpf + struct_ops programs. Instead of matching a fixed magic and + redirecting to a fixed interpreter recorded at registration time + such handlers match binaries programmatically and compute the + interpreter to use per binary, e.g. relative to the location of + the binary itself. + + If you don't know what to answer at this point, say N. + config COREDUMP bool "Enable core dump support" if EXPERT default y diff --git a/fs/Makefile b/fs/Makefile index 89a8a9d207d1..499c6670f0c1 100644 --- a/fs/Makefile +++ b/fs/Makefile @@ -33,6 +33,7 @@ obj-$(CONFIG_FS_ENCRYPTION) += crypto/ obj-$(CONFIG_FS_VERITY) += verity/ obj-$(CONFIG_FILE_LOCKING) += locks.o obj-$(CONFIG_BINFMT_MISC) += binfmt_misc.o +obj-$(CONFIG_BINFMT_MISC_BPF) += binfmt_misc_bpf.o obj-$(CONFIG_BINFMT_SCRIPT) += binfmt_script.o obj-$(CONFIG_BINFMT_ELF) += binfmt_elf.o obj-$(CONFIG_COMPAT_BINFMT_ELF) += compat_binfmt_elf.o diff --git a/fs/binfmt_misc_bpf.c b/fs/binfmt_misc_bpf.c new file mode 100644 index 000000000000..e3dcf8330df0 --- /dev/null +++ b/fs/binfmt_misc_bpf.c @@ -0,0 +1,351 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * BPF-backed binary type handlers for binfmt_misc. + * + * A handler is a struct binfmt_misc_ops struct_ops map. Loading and + * registering it makes the handler available under its name in the user + * namespace it was registered in. A binfmt_misc 'B' entry activates it: + * + * echo ':entry:B:::::' > /register + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct bm_bpf_ops_reg { + struct list_head list; + const struct binfmt_misc_ops *ops; + struct bpf_link *link; + struct user_namespace *user_ns; +}; + +static DEFINE_SPINLOCK(bm_bpf_ops_lock); +static LIST_HEAD(bm_bpf_ops_list); + +static struct bpf_struct_ops bpf_binfmt_misc_ops; + +static struct bm_bpf_ops_reg *bm_bpf_ops_find(const struct user_namespace *user_ns, + const char *name) +{ + struct bm_bpf_ops_reg *reg; + + lockdep_assert_held(&bm_bpf_ops_lock); + + list_for_each_entry(reg, &bm_bpf_ops_list, list) { + if (reg->user_ns == user_ns && !strcmp(reg->ops->name, name)) + return reg; + } + return NULL; +} + +/** + * binfmt_misc_get_ops - look up a bpf binary type handler by name + * @user_ns: user namespace of the binfmt_misc instance + * @name: name the handler was registered under + * + * Look for a handler named @name registered in @user_ns. A handler is not + * inherited from ancestor user namespaces: an entry can only name a handler + * registered in the same user namespace as its instance. The returned handler + * stays callable until binfmt_misc_put_ops() even if the backing struct_ops + * map is detached or deleted in the meantime. + * + * Return: the handler on success, NULL on failure + */ +const struct binfmt_misc_ops *binfmt_misc_get_ops(struct user_namespace *user_ns, + const char *name) +{ + struct bm_bpf_ops_reg *reg; + + guard(spinlock)(&bm_bpf_ops_lock); + + reg = bm_bpf_ops_find(user_ns, name); + if (!reg) + return NULL; + if (!bpf_struct_ops_get(reg->ops)) + return NULL; + return reg->ops; +} + +void binfmt_misc_put_ops(const struct binfmt_misc_ops *ops) +{ + bpf_struct_ops_put(ops); +} + +bool bpf_prog_is_binfmt_misc_ops(const struct bpf_prog *prog) +{ + return prog->type == BPF_PROG_TYPE_STRUCT_OPS && + prog->aux->st_ops == &bpf_binfmt_misc_ops; +} + +__bpf_kfunc_start_defs(); + +/** + * bpf_binprm_set_interp - select the interpreter for the current exec + * @bprm: binary that is being executed + * @path: absolute path to the interpreter + * @path__sz: size of the @path buffer, including the terminating NUL + * + * To be called from the load program of a struct binfmt_misc_ops handler + * before returning zero; the verifier rejects the call from any other + * program, including the handler's own match program. The path is opened + * with the credentials of the task doing the exec after the program + * returns. + * + * Return: 0 on success, a negative errno on failure + */ +__bpf_kfunc int bpf_binprm_set_interp(struct linux_binprm *bprm, + const char *path, size_t path__sz) +{ + size_t len; + char *interp; + + if (!path__sz) + return -EINVAL; + len = strnlen(path, path__sz); + if (len == path__sz) + return -EINVAL; + if (path[0] != '/') + return -EINVAL; + if (len >= PATH_MAX) + return -ENAMETOOLONG; + + interp = kmemdup_nul(path, len, GFP_KERNEL); + if (!interp) + return -ENOMEM; + + kfree(bprm->bpf_interp); + bprm->bpf_interp = interp; + return 0; +} + +/** + * bpf_binprm_set_interp_arg - set a single argument for the interpreter + * @bprm: binary that is being executed + * @arg: argument to pass to the interpreter + * @arg__sz: size of the @arg buffer, including the terminating NUL + * + * To be called from the load program of a struct binfmt_misc_ops handler. The + * argument is passed to the interpreter ahead of the binary, mirroring the + * single optional argument of a #! interpreter line. Calling it again + * replaces the argument. + * + * Return: 0 on success, a negative errno on failure + */ +__bpf_kfunc int bpf_binprm_set_interp_arg(struct linux_binprm *bprm, + const char *arg, size_t arg__sz) +{ + size_t len; + char *val; + + if (!arg__sz) + return -EINVAL; + len = strnlen(arg, arg__sz); + if (len == arg__sz) + return -EINVAL; + if (!len) + return -EINVAL; + + val = kmemdup_nul(arg, len, GFP_KERNEL); + if (!val) + return -ENOMEM; + + kfree(bprm->bpf_interp_arg); + bprm->bpf_interp_arg = val; + return 0; +} + +/** + * bpf_binprm_set_flags - choose the interpreter invocation flags for this exec + * @bprm: binary that is being executed + * @flags: an OR of enum bpf_binprm_flags values + * + * To be called from the load program of a struct binfmt_misc_ops handler. It + * decides per exec what a static entry fixes at registration with the P, C and + * O flags: BPF_BINPRM_PRESERVE_ARGV0 keeps the caller's argv[0], + * BPF_BINPRM_CREDENTIALS computes credentials from the binary, and + * BPF_BINPRM_EXECFD hands the binary to the interpreter through AT_EXECFD. + * Calling it again replaces the flags, passing zero clears them again. + * + * Return: 0 on success, -EINVAL if @flags contains an unknown bit + */ +__bpf_kfunc int bpf_binprm_set_flags(struct linux_binprm *bprm, + enum bpf_binprm_flags flags) +{ + if (flags & ~(BPF_BINPRM_PRESERVE_ARGV0 | BPF_BINPRM_CREDENTIALS | + BPF_BINPRM_EXECFD)) + return -EINVAL; + + bprm->bpf_flags = flags; + return 0; +} + +__bpf_kfunc_end_defs(); + +BTF_KFUNCS_START(bm_bpf_kfunc_ids) +BTF_ID_FLAGS(func, bpf_binprm_set_interp, KF_SLEEPABLE) +BTF_ID_FLAGS(func, bpf_binprm_set_interp_arg, KF_SLEEPABLE) +BTF_ID_FLAGS(func, bpf_binprm_set_flags, KF_SLEEPABLE) +BTF_KFUNCS_END(bm_bpf_kfunc_ids) + +static int bm_bpf_kfunc_filter(const struct bpf_prog *prog, u32 kfunc_id) +{ + if (!btf_id_set8_contains(&bm_bpf_kfunc_ids, kfunc_id)) + return 0; + if (prog->type != BPF_PROG_TYPE_STRUCT_OPS) + return -EACCES; + /* ->st_ops is unset during the cfg pass; enforced once it is set. */ + if (!prog->aux->st_ops) + return 0; + /* Only the load program decides how a binary is run. */ + if (bpf_prog_is_binfmt_misc_ops(prog) && + prog->aux->attach_st_ops_member_off == offsetof(struct binfmt_misc_ops, load)) + return 0; + return -EACCES; +} + +static const struct btf_kfunc_id_set bm_bpf_kfunc_set = { + .owner = THIS_MODULE, + .set = &bm_bpf_kfunc_ids, + .filter = bm_bpf_kfunc_filter, +}; + +static bool bm_bpf_ops__match(struct linux_binprm *bprm) +{ + return false; +} + +static int bm_bpf_ops__load(struct linux_binprm *bprm) +{ + return 0; +} + +static struct binfmt_misc_ops bm_bpf_ops_stubs = { + .match = bm_bpf_ops__match, + .load = bm_bpf_ops__load, +}; + +static int bm_bpf_init(struct btf *btf) +{ + return register_btf_kfunc_id_set(BPF_PROG_TYPE_STRUCT_OPS, + &bm_bpf_kfunc_set); +} + +static int bm_bpf_check_member(const struct btf_type *t, + const struct btf_member *member, + const struct bpf_prog *prog) +{ + u32 moff = __btf_member_bit_offset(t, member) / 8; + + switch (moff) { + case offsetof(struct binfmt_misc_ops, match): + case offsetof(struct binfmt_misc_ops, load): + /* Reliable file reads at exec time require sleeping. */ + if (!prog->sleepable) + return -EINVAL; + break; + } + return 0; +} + +static int bm_bpf_init_member(const struct btf_type *t, + const struct btf_member *member, + void *kdata, const void *udata) +{ + const struct binfmt_misc_ops *uops = udata; + struct binfmt_misc_ops *ops = kdata; + u32 moff = __btf_member_bit_offset(t, member) / 8; + + switch (moff) { + case offsetof(struct binfmt_misc_ops, name): + if (bpf_obj_name_cpy(ops->name, uops->name, + sizeof(ops->name)) <= 0) + return -EINVAL; + return 1; + } + return 0; +} + +static int bm_bpf_validate(void *kdata) +{ + struct binfmt_misc_ops *ops = kdata; + + if (!ops->match || !ops->load) + return -EINVAL; + return 0; +} + +static int bm_bpf_reg(void *kdata, struct bpf_link *link) +{ + struct binfmt_misc_ops *ops = kdata; + struct bm_bpf_ops_reg *reg; + + reg = kzalloc_obj(*reg, GFP_KERNEL_ACCOUNT); + if (!reg) + return -ENOMEM; + + reg->ops = ops; + reg->link = link; + reg->user_ns = get_user_ns(current_user_ns()); + + guard(spinlock)(&bm_bpf_ops_lock); + + if (bm_bpf_ops_find(reg->user_ns, ops->name)) { + put_user_ns(reg->user_ns); + kfree(reg); + return -EEXIST; + } + + list_add(®->list, &bm_bpf_ops_list); + return 0; +} + +static void bm_bpf_unreg(void *kdata, struct bpf_link *link) +{ + struct bm_bpf_ops_reg *reg; + + guard(spinlock)(&bm_bpf_ops_lock); + + list_for_each_entry(reg, &bm_bpf_ops_list, list) { + if (reg->ops == kdata && reg->link == link) { + list_del(®->list); + put_user_ns(reg->user_ns); + kfree(reg); + return; + } + } +} + +static const struct bpf_verifier_ops bm_bpf_verifier_ops = { + .get_func_proto = bpf_base_func_proto, + .is_valid_access = bpf_tracing_btf_ctx_access, +}; + +static struct bpf_struct_ops bpf_binfmt_misc_ops = { + .verifier_ops = &bm_bpf_verifier_ops, + .init = bm_bpf_init, + .check_member = bm_bpf_check_member, + .init_member = bm_bpf_init_member, + .validate = bm_bpf_validate, + .reg = bm_bpf_reg, + .unreg = bm_bpf_unreg, + .cfi_stubs = &bm_bpf_ops_stubs, + .name = "binfmt_misc_ops", + .owner = THIS_MODULE, +}; + +static int __init bm_bpf_struct_ops_init(void) +{ + return register_bpf_struct_ops(&bpf_binfmt_misc_ops, binfmt_misc_ops); +} +late_initcall(bm_bpf_struct_ops_init); diff --git a/include/linux/binfmt_misc.h b/include/linux/binfmt_misc.h new file mode 100644 index 000000000000..d3112a00cc19 --- /dev/null +++ b/include/linux/binfmt_misc.h @@ -0,0 +1,71 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _LINUX_BINFMT_MISC_H +#define _LINUX_BINFMT_MISC_H + +#include + +struct bpf_prog; +struct linux_binprm; +struct user_namespace; + +#define BINFMT_MISC_OPS_NAME_MAX 16 + +/** + * enum bpf_binprm_flags - per-exec invocation flags a load program can request + * @BPF_BINPRM_PRESERVE_ARGV0: keep the caller's argv[0] (like the 'P' flag) + * @BPF_BINPRM_CREDENTIALS: compute credentials from the binary; implies execfd + * (like the 'C' flag) + * @BPF_BINPRM_EXECFD: pass the binary via AT_EXECFD (like the 'O' flag) + * + * Set from a load program with bpf_binprm_set_flags(). Unlike a static entry, + * a bpf handler chooses these per exec rather than once at registration. + */ +enum bpf_binprm_flags { + BPF_BINPRM_PRESERVE_ARGV0 = (1ULL << 0), + BPF_BINPRM_CREDENTIALS = (1ULL << 1), + BPF_BINPRM_EXECFD = (1ULL << 2), +}; + +/** + * struct binfmt_misc_ops - bpf-backed binary type handler + * @match: decide whether the handler applies to @bprm; consulted from the + * entry lookup walk like static magic and extension matching, in + * registration order with first-match-wins semantics; sleepable, + * so it can read the binary to decide, but the verifier rejects + * the interpreter selection kfuncs in it + * @load: select an interpreter for the matched @bprm via + * bpf_binprm_set_interp() and return zero; a match is committed, so + * a failure fails the exec instead of falling through to later + * entries; -ENOEXEC does not fail the exec but moves on to the + * remaining binary formats + * @name: name that 'B' entries reference the handler by + */ +struct binfmt_misc_ops { + bool (*match)(struct linux_binprm *bprm); + int (*load)(struct linux_binprm *bprm); + char name[BINFMT_MISC_OPS_NAME_MAX]; +}; + +#ifdef CONFIG_BINFMT_MISC_BPF +const struct binfmt_misc_ops *binfmt_misc_get_ops(struct user_namespace *user_ns, + const char *name); +void binfmt_misc_put_ops(const struct binfmt_misc_ops *ops); +bool bpf_prog_is_binfmt_misc_ops(const struct bpf_prog *prog); +#else +static inline const struct binfmt_misc_ops * +binfmt_misc_get_ops(struct user_namespace *user_ns, const char *name) +{ + return NULL; +} + +static inline void binfmt_misc_put_ops(const struct binfmt_misc_ops *ops) +{ +} + +static inline bool bpf_prog_is_binfmt_misc_ops(const struct bpf_prog *prog) +{ + return false; +} +#endif /* CONFIG_BINFMT_MISC_BPF */ + +#endif /* _LINUX_BINFMT_MISC_H */ From 1ffc8d2473a1d618dcc1b66af3339f150b7db097 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 14 Jul 2026 21:58:08 +0200 Subject: [PATCH 24/63] binfmt_misc: let the entry lookup walk sleep The upcoming bpf-backed binary type handlers run a match program from the entry lookup walk in load_misc_binary(). Deciding whether a handler applies means reading the binary - parsing ELF program headers sitting at arbitrary file offsets, say - and reliable file reads at exec time fault in the file's pages, so the walk must tolerate an entry's evaluation sleeping. Switch the walk from RCU to SRCU in its fast flavor: srcu-fast read sections may block while the read side stays practically as cheap as the RCU read lock it replaces, so the common static-entry lookup does not pay for the new capability. Entry freeing moves from kfree_rcu() to call_srcu(). Removal still unlinks the entry immediately and never blocks: a walker sleeping inside an entry's evaluation just keeps the entry alive until it leaves the read section. The module exit path flushes pending callbacks with srcu_barrier(). Take the reference on a matched entry at the match point inside the walk instead of retrying the whole search when the refcount raise fails. A restarted search was harmless when an entry's evaluation was a memcmp() on bprm->buf, but re-running match programs that may sleep on entries that were already consulted is not. An entry whose refcount hit zero is unlinked and dying, so treating it as absent and walking on is exactly what the bounded retry loop converged to, without ever evaluating an entry twice. Link: https://patch.msgid.link/20260714-work-bpf-binfmt_misc-v2-3-57b7529c002c@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 56 +++++++++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index 7896a50af80d..5e557a82227e 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -82,6 +83,9 @@ struct binfmt_misc_entry { /* Trailing delimiter pad so field parsing always terminates at a delimiter. */ #define MISC_DELIM_PAD 8 +/* Protects the entry walk in load_misc_binary(), which may sleep in it. */ +DEFINE_STATIC_SRCU_FAST(bm_entries_srcu); + /* Check if @e's magic matches @bprm's buffer, applying the mask if set. */ static bool entry_matches_magic(const struct binfmt_misc_entry *e, const struct linux_binprm *bprm) @@ -111,11 +115,14 @@ static bool entry_matches_extension(const struct binfmt_misc_entry *e, * @bprm: binary for which we are looking for a handler * * Search for a binary type handler for @bprm in the list of registered binary - * type handlers. + * type handlers. The matched entry is returned with a reference taken while + * the walk still held it; a dying entry - unlinked with its last reference + * gone - cannot be matched and the walk moves on. * - * The caller must hold the RCU read lock. + * The caller must hold the bm_entries_srcu read lock, which allows an + * entry's evaluation to sleep. * - * Return: binary type list entry on success, NULL on failure + * Return: referenced binary type list entry on success, NULL on failure */ static struct binfmt_misc_entry * search_binfmt_handler(struct binfmt_misc *misc, struct linux_binprm *bprm) @@ -125,18 +132,23 @@ search_binfmt_handler(struct binfmt_misc *misc, struct linux_binprm *bprm) struct binfmt_misc_entry *e; /* Walk all the registered handlers. */ - hlist_for_each_entry_rcu(e, &misc->entries, node) { + hlist_for_each_entry_rcu(e, &misc->entries, node, + srcu_read_lock_held(&bm_entries_srcu)) { /* Make sure this one is currently enabled. */ if (!test_bit(MISC_FMT_ENABLED_BIT, &e->flags)) continue; if (test_bit(MISC_FMT_MAGIC_BIT, &e->flags)) { - if (entry_matches_magic(e, bprm)) - return e; + if (!entry_matches_magic(e, bprm)) + continue; } else { - if (entry_matches_extension(e, ext)) - return e; + if (!entry_matches_extension(e, ext)) + continue; } + + /* A dying entry cannot be matched, walk on. */ + if (refcount_inc_not_zero(&e->users)) + return e; } return NULL; @@ -147,24 +159,22 @@ search_binfmt_handler(struct binfmt_misc *misc, struct linux_binprm *bprm) * @misc: handle to binfmt_misc instance * @bprm: binary for which we are looking for a handler * - * Try to find a binfmt handler for the binary type. If one is found take a - * reference to protect against removal via bm_{entry,status}_write(). The - * refcount of an entry can only drop to zero once it has been unlinked and - * a restarted search cannot find an unlinked entry again so the retry loop - * is bounded. + * Try to find a binfmt handler for the binary type. If one is found it is + * returned with a reference protecting it against removal via + * bm_{entry,status}_write(). * * Return: binary type list entry on success, NULL on failure */ static struct binfmt_misc_entry *get_binfmt_handler(struct binfmt_misc *misc, struct linux_binprm *bprm) { - struct binfmt_misc_entry *e; + guard(srcu_fast)(&bm_entries_srcu); + return search_binfmt_handler(misc, bprm); +} - guard(rcu)(); - do { - e = search_binfmt_handler(misc, bprm); - } while (e && !refcount_inc_not_zero(&e->users)); - return e; +static void bm_entry_free_rcu(struct rcu_head *rcu) +{ + kfree(container_of(rcu, struct binfmt_misc_entry, rcu)); } /** @@ -182,8 +192,8 @@ static void put_binfmt_handler(struct binfmt_misc_entry *e) exe_file_allow_write_access(e->interp_file); filp_close(e->interp_file, NULL); } - /* Lockless walkers may still dereference this entry. */ - kfree_rcu(e, rcu); + /* Walkers may still dereference this entry, even sleeping. */ + call_srcu(&bm_entries_srcu, &e->rcu, bm_entry_free_rcu); } } @@ -669,7 +679,7 @@ static void bm_evict_inode(struct inode *inode) * Adding and removing entries via bm_{entry,register,status}_write() and * unlink(2) happens under the exclusively held inode lock of the root * dentry keeping the list stable for writers. load_misc_binary() walks it - * concurrently under RCU. The entries_lock is only held around the actual + * concurrently under SRCU. The entries_lock is only held around the actual * unlink to serialize against bm_evict_inode() which unlinks entries * during umount without holding the root inode lock. */ @@ -1055,6 +1065,8 @@ static void __exit exit_misc_binfmt(void) { unregister_binfmt(&misc_format); unregister_filesystem(&bm_fs_type); + /* Flush pending bm_entry_free_rcu() callbacks before the text goes. */ + srcu_barrier(&bm_entries_srcu); } core_initcall(init_misc_binfmt); From ceb912149e5e60fbb1c762603f8c4ce257b97501 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 14 Jul 2026 21:58:09 +0200 Subject: [PATCH 25/63] binfmt_misc: wire up bpf-backed 'B' entries Activate a registered binfmt_misc_ops handler through the existing text interface with the new 'B' entry type: echo ':name:B:::::' > /register The offset, magic, and mask fields must be empty since the program does the matching; the interpreter field carries the handler name since the program supplies the interpreter. Reusing the register file keeps the existing permission model intact: activating a handler requires the same write access to a binfmt_misc instance as any other registration, and the per user namespace instance semantics apply unchanged. A 'B' entry in a container's own instance shadows the host's handlers just like any other entry, and the privilege needed to shadow e.g. all ELF binaries is the same as for a static 'M' entry matching \x7fELF today; the only novelty is that matching becomes programmable. The entry takes its own reference on the ops for its whole lifetime. It is dropped from the SRCU callback that frees the entry rather than synchronously on the final put: a walker may be asleep inside the handler's match program while the entry's last reference goes away, so the ops must stay callable until every walker has left the read section - the same deferral the entry's own memory already gets. The registration failure path, where the users refcount is not live yet, drops it explicitly. The match program runs from the lookup walk like magic and extension matching and under the same rules: strict registration order, first match wins. The walk became an SRCU read-side section in the previous patch, so the program can sleep: it decides on the actual file content - program headers beyond the prefetched bprm->buf, say - not just on whatever happens to be resident in the page cache. A match commits the exec to the handler. The sleepable load program then selects the interpreter from load_misc_binary() by calling bpf_binprm_set_interp() and returning zero; a failure fails the exec instead of falling through to later entries. The walk is never left and re-entered, so 'B' entries need no special semantics against concurrent registration and removal whatsoever. -ENOEXEC keeps its usual meaning and moves on to the remaining binary formats - a handler whose load program discovers that it cannot serve the binary after all hands it back to them - and so does returning zero without having selected an interpreter; other program-supplied errors are clamped to the errno range. The 'F' flag is rejected for 'B' entries: it exists to pre-open a fixed interpreter at registration time in the registrar's context, and a 'B' entry has no fixed interpreter to pre-open. 'C' is accepted and behaves exactly as it does for a static entry. It honors the suid bits of the matched binary while executing the interpreter, which makes 'B' handlers usable for the setuid case, e.g. a per-binary loader. This does not let the program's registrant widen access: bprm_fill_uid() gates the credential transition on vfsuid_has_mapping() in the caller's user namespace, so the interpreter can only ever run as a uid that is mapped there, identical to a static 'C' entry. The computed path is opened with open_exec() under the caller's credentials with the usual LSM and noexec checks, and the programs run before the transition with the caller's credentials, never elevated. Link: https://lore.kernel.org/20260704211409.1978485-1-farid.m.zakaria@gmail.com Link: https://patch.msgid.link/20260714-work-bpf-binfmt_misc-v2-4-57b7529c002c@kernel.org Reviewed-by: Farid Zakaria Signed-off-by: Christian Brauner (Amutable) --- Documentation/admin-guide/binfmt-misc.rst | 48 ++++++- fs/binfmt_misc.c | 148 ++++++++++++++++++++-- 2 files changed, 181 insertions(+), 15 deletions(-) diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst index 306ef48f5de6..113cc51e038c 100644 --- a/Documentation/admin-guide/binfmt-misc.rst +++ b/Documentation/admin-guide/binfmt-misc.rst @@ -26,7 +26,8 @@ Here is what the fields mean: name below ``/proc/sys/fs/binfmt_misc``; cannot contain slashes ``/`` for obvious reasons. - ``type`` - is the type of recognition. Give ``M`` for magic and ``E`` for extension. + is the type of recognition. Give ``M`` for magic, ``E`` for extension and + ``B`` for a bpf-backed handler (see below). - ``offset`` is the offset of the magic/mask in the file, counted in bytes. This defaults to 0 if you omit it (i.e. you write ``:name:type::magic...``). @@ -48,7 +49,8 @@ Here is what the fields mean: filename extension matching. - ``interpreter`` is the program that should be invoked with the binary as first - argument (specify the full path) + argument (specify the full path). For ``B`` entries this field + carries the name of the bpf handler instead (see below). - ``flags`` is an optional field that controls several aspects of the invocation of the interpreter. It is a string of capital letters, each controls a @@ -97,6 +99,48 @@ There are some restrictions: offset+size(magic) has to be less than 128 - the interpreter string may not exceed 127 characters + +bpf-backed handlers +------------------- + +With ``CONFIG_BINFMT_MISC_BPF`` both the matching and the interpreter +selection can be delegated to bpf programs. A handler is an instance of the +``binfmt_misc_ops`` struct_ops with a ``match`` and a ``load`` program and a +``name``. Once the struct_ops map is registered the handler can be activated +with a ``B`` entry that references it by name in the ``interpreter`` field +and carries neither offset, magic, nor mask:: + + echo ':qemu:B::::my_handler:' > register + +Both programs receive the ``linux_binprm`` of the binary and both can +sleep. The ``match`` program decides whether the handler applies: it is +consulted during the entry walk exactly like magic and extension matching, +in the same registration order with the same first-match-wins semantics. +Unlike static matching it is not limited to the prefetched first bytes of +the file in ``bprm->buf``: it can read the file, e.g. to parse ELF program +headers whose data sits at arbitrary offsets. It only decides, though: the +selection kfuncs below are rejected in it. The ``load`` program of the +matched handler then selects the interpreter: it can equally read the file +and derive the interpreter from the binary's location. It selects the +interpreter by calling the ``bpf_binprm_set_interp()`` kfunc with an +absolute path and returning ``0``. A match is committed: a failing +``load`` fails the exec with its error instead of falling through to later +entries; ``-ENOEXEC`` lets the remaining binary formats have a go. The +interpreter is opened with the credentials of the task doing the exec, +exactly as a statically registered interpreter would be. + +A handler is looked up only in the user namespace the struct_ops map was +registered in. Handlers are not inherited, so an entry can only reference a +handler registered in the same user namespace as its binfmt_misc instance. +The entry keeps the handler alive; deleting the struct_ops map only prevents +new activations. + +The ``F`` flag cannot be combined with ``B`` entries: it pre-opens a fixed +interpreter at registration time and a ``B`` entry has none. The ``C`` flag +works as it does for a static entry: the interpreter runs with the matched +binary's credentials, bounded to user namespaces that map the binary's owner +just like any other setuid exec. + To use binfmt_misc you have to mount it first. You can mount it with ``mount -t binfmt_misc none /proc/sys/fs/binfmt_misc`` command, or you can add a line ``none /proc/sys/fs/binfmt_misc binfmt_misc defaults 0 0`` to your diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index 5e557a82227e..d5bb63b048ea 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -10,6 +10,7 @@ #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt +#include #include #include #include @@ -39,6 +40,7 @@ enum binfmt_misc_entry_bits { MISC_FMT_ENABLED_BIT = 0, MISC_FMT_MAGIC_BIT = 1, + MISC_FMT_BPF_BIT = 2, }; /* Entry behavior flags, fixed at registration time. */ @@ -60,6 +62,8 @@ struct binfmt_misc_entry { char *name; struct dentry *dentry; struct file *interp_file; + const struct binfmt_misc_ops *bpf_ops; /* bpf-backed handler ('B') */ + const char *bpf_ops_name; refcount_t users; /* sync removal with load_misc_binary() */ struct rcu_head rcu; char buf[]; /* register string, fields point in here */ @@ -115,9 +119,11 @@ static bool entry_matches_extension(const struct binfmt_misc_entry *e, * @bprm: binary for which we are looking for a handler * * Search for a binary type handler for @bprm in the list of registered binary - * type handlers. The matched entry is returned with a reference taken while - * the walk still held it; a dying entry - unlinked with its last reference - * gone - cannot be matched and the walk moves on. + * type handlers. A 'B' entry's match program decides whether the handler + * applies; it may sleep to read the binary. The matched entry is returned + * with a reference taken while the walk still held it; a dying entry - + * unlinked with its last reference gone - cannot be matched and the walk + * moves on. * * The caller must hold the bm_entries_srcu read lock, which allows an * entry's evaluation to sleep. @@ -138,7 +144,10 @@ search_binfmt_handler(struct binfmt_misc *misc, struct linux_binprm *bprm) if (!test_bit(MISC_FMT_ENABLED_BIT, &e->flags)) continue; - if (test_bit(MISC_FMT_MAGIC_BIT, &e->flags)) { + if (test_bit(MISC_FMT_BPF_BIT, &e->flags)) { + if (!e->bpf_ops->match(bprm)) + continue; + } else if (test_bit(MISC_FMT_MAGIC_BIT, &e->flags)) { if (!entry_matches_magic(e, bprm)) continue; } else { @@ -174,7 +183,12 @@ static struct binfmt_misc_entry *get_binfmt_handler(struct binfmt_misc *misc, static void bm_entry_free_rcu(struct rcu_head *rcu) { - kfree(container_of(rcu, struct binfmt_misc_entry, rcu)); + struct binfmt_misc_entry *e = container_of(rcu, struct binfmt_misc_entry, rcu); + + /* No walker that could sleep in the handler's programs is left. */ + if (e->bpf_ops) + binfmt_misc_put_ops(e->bpf_ops); + kfree(e); } /** @@ -226,12 +240,51 @@ static struct binfmt_misc *current_binfmt_misc(void) return &init_binfmt_misc; } +/** + * entry_select_interpreter - get the interpreter for the matched @e + * @e: matched binary type handler + * @bprm: binary that is being executed + * + * A static entry carries its interpreter path, for a 'B' entry the + * handler's load program selects it. The match is committed, so a failing + * program fails the exec. + * + * Return: the interpreter on success, an ERR_PTR on failure + */ +static const char *entry_select_interpreter(const struct binfmt_misc_entry *e, + struct linux_binprm *bprm) +{ + int retval; + + if (!test_bit(MISC_FMT_BPF_BIT, &e->flags)) + return e->interpreter; + + /* Drop any interpreter a previous chain level staged. */ + kfree(bprm->bpf_interp); + bprm->bpf_interp = NULL; + + retval = e->bpf_ops->load(bprm); + if (retval) { + /* Keep a program-supplied error within errno range. */ + if (retval > 0 || retval < -MAX_ERRNO) + retval = -ENOEXEC; + return ERR_PTR(retval); + } + + /* Selecting an interpreter is part of the contract. */ + if (!bprm->bpf_interp) + return ERR_PTR(-ENOEXEC); + + return bprm->bpf_interp; +} + /* * the loader itself */ static int load_misc_binary(struct linux_binprm *bprm) { struct binfmt_misc_entry *fmt __free(put_binfmt_handler) = NULL; + const char *interpreter; struct file *interp_file; struct binfmt_misc *misc; int retval; @@ -248,6 +301,10 @@ static int load_misc_binary(struct linux_binprm *bprm) if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE) return -ENOENT; + interpreter = entry_select_interpreter(fmt, bprm); + if (IS_ERR(interpreter)) + return PTR_ERR(interpreter); + if (fmt->flags & MISC_FMT_PRESERVE_ARGV0) { bprm->interp_flags |= BINPRM_FLAGS_PRESERVE_ARGV0; } else { @@ -263,13 +320,13 @@ static int load_misc_binary(struct linux_binprm *bprm) bprm->argc++; /* add the interp as argv[0] */ - retval = copy_string_kernel(fmt->interpreter, bprm); + retval = copy_string_kernel(interpreter, bprm); if (retval < 0) return retval; bprm->argc++; /* Update interp in case binfmt_script needs it. */ - retval = bprm_change_interp(fmt->interpreter, bprm); + retval = bprm_change_interp(interpreter, bprm); if (retval < 0) return retval; @@ -284,7 +341,7 @@ static int load_misc_binary(struct linux_binprm *bprm) } } } else { - interp_file = open_exec(fmt->interpreter); + interp_file = open_exec(interpreter); } if (IS_ERR(interp_file)) return PTR_ERR(interp_file); @@ -437,6 +494,27 @@ static char *parse_extension_fields(struct binfmt_misc_entry *e, char *p, return p; } +/* + * Parse the fields of a 'B' entry: the 'offset', 'magic' and 'mask' fields + * must be empty. The handler name is carried in the 'interpreter' field. + */ +static char *parse_bpf_fields(struct binfmt_misc_entry *e, char *p, char del) +{ + /* The 'offset' field must be empty. */ + if (*p++ != del) + return NULL; + + /* The 'magic' field must be empty. */ + if (*p++ != del) + return NULL; + + /* The 'mask' field must be empty. */ + if (*p++ != del) + return NULL; + + return p; +} + /* * This registers a new binary format, it recognises the syntax * ':name:type:offset:magic:mask:interpreter:flags' @@ -501,13 +579,21 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, pr_debug("register: type: M (magic)\n"); e->flags = BIT(MISC_FMT_ENABLED_BIT) | BIT(MISC_FMT_MAGIC_BIT); break; + case 'B': + pr_debug("register: type: B (bpf)\n"); + if (!IS_ENABLED(CONFIG_BINFMT_MISC_BPF)) + return ERR_PTR(-EINVAL); + e->flags = BIT(MISC_FMT_ENABLED_BIT) | BIT(MISC_FMT_BPF_BIT); + break; default: return ERR_PTR(-EINVAL); } if (*p++ != del) return ERR_PTR(-EINVAL); - if (test_bit(MISC_FMT_MAGIC_BIT, &e->flags)) + if (test_bit(MISC_FMT_BPF_BIT, &e->flags)) + p = parse_bpf_fields(e, p, del); + else if (test_bit(MISC_FMT_MAGIC_BIT, &e->flags)) p = parse_magic_fields(e, p, del); else p = parse_extension_fields(e, p, del); @@ -520,9 +606,18 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, if (!p) return ERR_PTR(-EINVAL); *p++ = '\0'; - if (!e->interpreter[0]) + if (test_bit(MISC_FMT_BPF_BIT, &e->flags)) { + /* The 'interpreter' field carries the handler name. */ + e->bpf_ops_name = e->interpreter; + e->interpreter = NULL; + if (!e->bpf_ops_name[0]) + return ERR_PTR(-EINVAL); + pr_debug("register: bpf handler: {%s}\n", e->bpf_ops_name); + } else if (!e->interpreter[0]) { return ERR_PTR(-EINVAL); - pr_debug("register: interpreter: {%s}\n", e->interpreter); + } else { + pr_debug("register: interpreter: {%s}\n", e->interpreter); + } /* Parse the 'flags' field. */ p = check_special_flags(p, e); @@ -531,6 +626,17 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, if (p != buf + count) return ERR_PTR(-EINVAL); + /* + * 'F' pre-opens a fixed interpreter at registration time which is + * meaningless for a per-exec computed path. 'C' is fine: it honors the + * suid bits of the matched binary exactly like a static entry, gated by + * the same vfsuid_has_mapping() check in bprm_fill_uid() that keeps the + * transition to uids mapped in the caller's user namespace. + */ + if (test_bit(MISC_FMT_BPF_BIT, &e->flags) && + (e->flags & MISC_FMT_OPEN_FILE)) + return ERR_PTR(-EINVAL); + return no_free_ptr(e); } @@ -584,7 +690,10 @@ static int bm_entry_show(struct seq_file *m, void *unused) else seq_puts(m, "disabled\n"); - seq_printf(m, "interpreter %s\n", e->interpreter); + if (test_bit(MISC_FMT_BPF_BIT, &e->flags)) + seq_printf(m, "bpf %s\n", e->bpf_ops->name); + else + seq_printf(m, "interpreter %s\n", e->interpreter); /* print the special flags */ seq_puts(m, "flags: "); @@ -598,7 +707,9 @@ static int bm_entry_show(struct seq_file *m, void *unused) seq_putc(m, 'F'); seq_putc(m, '\n'); - if (!test_bit(MISC_FMT_MAGIC_BIT, &e->flags)) { + if (test_bit(MISC_FMT_BPF_BIT, &e->flags)) { + /* The program does the matching. */ + } else if (!test_bit(MISC_FMT_MAGIC_BIT, &e->flags)) { seq_printf(m, "extension .%s\n", e->magic); } else { seq_printf(m, "offset %i\nmagic ", e->offset); @@ -849,6 +960,15 @@ static ssize_t bm_register_write(struct file *file, const char __user *buffer, if (IS_ERR(e)) return PTR_ERR(e); + if (test_bit(MISC_FMT_BPF_BIT, &e->flags)) { + e->bpf_ops = binfmt_misc_get_ops(sb->s_user_ns, e->bpf_ops_name); + if (!e->bpf_ops) { + pr_notice("register: no bpf handler named %s\n", + e->bpf_ops_name); + return -ENOENT; + } + } + if (e->flags & MISC_FMT_OPEN_FILE) { /* * Now that we support unprivileged binfmt_misc mounts make @@ -873,6 +993,8 @@ static ssize_t bm_register_write(struct file *file, const char __user *buffer, exe_file_allow_write_access(f); filp_close(f, NULL); } + if (e->bpf_ops) + binfmt_misc_put_ops(e->bpf_ops); return err; } From 7bddf0e9f1081935d11d01d9344dbe7c3fda07f6 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 14 Jul 2026 21:58:10 +0200 Subject: [PATCH 26/63] bpf: allow fs kfuncs for binfmt_misc_ops programs The fs kfuncs are currently exclusive to LSM programs. A binfmt_misc handler needs a subset of them to do anything interesting: computing an interpreter relative to the binary's location wants bpf_path_d_path() on bprm->file->f_path from the load program, and matching on per-binary metadata wants bpf_get_file_xattr() and friends right from the match program. Register the fs kfunc set for struct_ops programs as well and extend the filter to admit binfmt_misc_ops programs. The xattr setters stay exclusive to LSM programs: a binary type handler decides how to run a binary, it has no business modifying filesystem state. This only takes effect in builds that have the fs kfunc set at all, i.e. CONFIG_BPF_LSM. Without it a binfmt_misc handler is limited to bprm fields and the file-backed dynptr, which are provided by the common kfunc set. Link: https://lore.kernel.org/20260704211409.1978485-1-farid.m.zakaria@gmail.com Link: https://patch.msgid.link/20260714-work-bpf-binfmt_misc-v2-5-57b7529c002c@kernel.org Reviewed-by: Farid Zakaria Signed-off-by: Christian Brauner (Amutable) --- fs/bpf_fs_kfuncs.c | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/fs/bpf_fs_kfuncs.c b/fs/bpf_fs_kfuncs.c index f1863a891db6..5b7d03e4fc6d 100644 --- a/fs/bpf_fs_kfuncs.c +++ b/fs/bpf_fs_kfuncs.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0 /* Copyright (c) 2024 Google LLC. */ +#include #include #include #include @@ -392,10 +393,25 @@ BTF_ID_FLAGS(func, bpf_remove_dentry_xattr, KF_SLEEPABLE) BTF_ID_FLAGS(func, bpf_real_data_inode, KF_SLEEPABLE | KF_RET_NULL) BTF_KFUNCS_END(bpf_fs_kfunc_set_ids) +/* Side-effecting kfuncs that stay exclusive to LSM programs. */ +BTF_SET_START(bpf_fs_kfunc_lsm_only_ids) +BTF_ID(func, bpf_set_dentry_xattr) +BTF_ID(func, bpf_remove_dentry_xattr) +BTF_SET_END(bpf_fs_kfunc_lsm_only_ids) + static int bpf_fs_kfuncs_filter(const struct bpf_prog *prog, u32 kfunc_id) { - if (!btf_id_set8_contains(&bpf_fs_kfunc_set_ids, kfunc_id) || - prog->type == BPF_PROG_TYPE_LSM) + if (!btf_id_set8_contains(&bpf_fs_kfunc_set_ids, kfunc_id)) + return 0; + if (prog->type == BPF_PROG_TYPE_LSM) + return 0; + if (prog->type != BPF_PROG_TYPE_STRUCT_OPS) + return -EACCES; + /* ->st_ops is unset during the cfg pass; enforced once it is set. */ + if (!prog->aux->st_ops) + return 0; + if (bpf_prog_is_binfmt_misc_ops(prog) && + !btf_id_set_contains(&bpf_fs_kfunc_lsm_only_ids, kfunc_id)) return 0; return -EACCES; } @@ -438,7 +454,13 @@ static const struct btf_kfunc_id_set bpf_fs_kfunc_set = { static int __init bpf_fs_kfuncs_init(void) { - return register_btf_kfunc_id_set(BPF_PROG_TYPE_LSM, &bpf_fs_kfunc_set); + int ret; + + ret = register_btf_kfunc_id_set(BPF_PROG_TYPE_LSM, &bpf_fs_kfunc_set); + if (ret || !IS_ENABLED(CONFIG_BINFMT_MISC_BPF)) + return ret; + return register_btf_kfunc_id_set(BPF_PROG_TYPE_STRUCT_OPS, + &bpf_fs_kfunc_set); } late_initcall(bpf_fs_kfuncs_init); From c008c972a6c932eeafdebba51a6fe27f23696fee Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 14 Jul 2026 21:58:11 +0200 Subject: [PATCH 27/63] binfmt_misc: let bpf handlers pass an argument to the interpreter A bpf binfmt_misc handler selects an interpreter but, unlike binfmt_script, load_misc_binary() builds the argument vector as just [interpreter, binary, ...] with no slot for an argument to the interpreter. A handler that wants to reproduce a #! line therefore cannot express its single optional argument, e.g. a handler that resolves $ORIGIN in a script's #! path loses the argument that followed the interpreter. Have load_misc_binary() consume the argument staged through the bpf_binprm_set_interp_arg() kfunc and insert it between the interpreter and the binary - the same position and single-argument semantics binfmt_script gives the argument of a #! line. The argument is cleared once spliced into the argument vector, and a load program that fails after staging one has it dropped on the way out: whether the exec fails or -ENOEXEC hands the binary back to the remaining formats, a stale argument cannot leak into a nested interpreter's argv. This also lets static-style handlers pass a fixed interpreter argument, which plain binfmt_misc has never been able to express. Link: https://patch.msgid.link/20260714-work-bpf-binfmt_misc-v2-6-57b7529c002c@kernel.org Reviewed-by: Farid Zakaria Signed-off-by: Christian Brauner (Amutable) --- Documentation/admin-guide/binfmt-misc.rst | 6 +++++ fs/binfmt_misc.c | 33 +++++++++++++++++++---- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst index 113cc51e038c..c2c18ca9ff8e 100644 --- a/Documentation/admin-guide/binfmt-misc.rst +++ b/Documentation/admin-guide/binfmt-misc.rst @@ -129,6 +129,12 @@ entries; ``-ENOEXEC`` lets the remaining binary formats have a go. The interpreter is opened with the credentials of the task doing the exec, exactly as a statically registered interpreter would be. +The ``load`` program can also pass a single argument to the interpreter with +the ``bpf_binprm_set_interp_arg()`` kfunc. It is inserted between the +interpreter and the binary, exactly like the optional argument of a ``#!`` +interpreter line, e.g. for a handler that resolves ``$ORIGIN`` in a script's +``#!`` path and needs to preserve the argument that followed it. + A handler is looked up only in the user namespace the struct_ops map was registered in. Handlers are not inherited, so an entry can only reference a handler registered in the same user namespace as its binfmt_misc instance. diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index d5bb63b048ea..507f833a3179 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -259,23 +259,32 @@ static const char *entry_select_interpreter(const struct binfmt_misc_entry *e, if (!test_bit(MISC_FMT_BPF_BIT, &e->flags)) return e->interpreter; - /* Drop any interpreter a previous chain level staged. */ + /* Drop any interpreter or flags a previous chain level staged. */ kfree(bprm->bpf_interp); bprm->bpf_interp = NULL; + bprm->bpf_flags = 0; retval = e->bpf_ops->load(bprm); if (retval) { /* Keep a program-supplied error within errno range. */ if (retval > 0 || retval < -MAX_ERRNO) retval = -ENOEXEC; - return ERR_PTR(retval); + goto drop_staged; } /* Selecting an interpreter is part of the contract. */ - if (!bprm->bpf_interp) - return ERR_PTR(-ENOEXEC); + if (!bprm->bpf_interp) { + retval = -ENOEXEC; + goto drop_staged; + } return bprm->bpf_interp; + +drop_staged: + /* A failing load leaves nothing behind for later entries. */ + kfree(bprm->bpf_interp_arg); + bprm->bpf_interp_arg = NULL; + return ERR_PTR(retval); } /* @@ -313,12 +322,26 @@ static int load_misc_binary(struct linux_binprm *bprm) return retval; } - /* make argv[1] be the path to the binary */ + /* make the binary the last argument to the interpreter */ retval = copy_string_kernel(bprm->interp, bprm); if (retval < 0) return retval; bprm->argc++; + /* + * A single optional argument to the interpreter, inserted between it + * and the binary just like the argument of a #! interpreter line. + */ + if (bprm->bpf_interp_arg) { + retval = copy_string_kernel(bprm->bpf_interp_arg, bprm); + if (retval < 0) + return retval; + bprm->argc++; + /* Consumed - don't let it leak into a nested interpreter's argv. */ + kfree(bprm->bpf_interp_arg); + bprm->bpf_interp_arg = NULL; + } + /* add the interp as argv[0] */ retval = copy_string_kernel(interpreter, bprm); if (retval < 0) From 186aaff0d12b0e5b8af44cfb439b978e58ed74e4 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 14 Jul 2026 21:58:12 +0200 Subject: [PATCH 28/63] binfmt_misc: let a bpf handler choose the invocation flags per exec The 'P', 'C' and 'O' flags of a binfmt_misc entry - preserve argv[0], compute credentials from the binary, and pass the binary as an open file descriptor - are fixed at registration and apply to every binary the entry matches. A bpf handler matches, selects the interpreter and reads the binary per exec, so the flags should be its per-exec decision too: one handler may match both setuid and non-setuid binaries, argv[0]-sensitive ones and not. Honor the flags the load program stages in bprm->bpf_flags through the bpf_binprm_set_flags() kfunc: BPF_BINPRM_PRESERVE_ARGV0, BPF_BINPRM_CREDENTIALS and BPF_BINPRM_EXECFD map to 'P', 'C' and 'O' and keep the semantics of their static counterparts, credentials implying the open file descriptor included. Flags staged by a load program that then fails are dropped on the way out so they cannot leak into a later handler's exec, and the argv[0] decision acts on the entry's own choice instead of testing the accumulated bprm->interp_flags bit, which an earlier chain level may have left set and binfmt_misc never clears. Since a 'B' entry's flags come from the program, it carries none in the register string: 'P', 'C' and 'O' are rejected there alongside 'F', which was already meaningless for it. load_misc_binary() takes the flags from the entry for a static handler and from bprm->bpf_flags for a bpf one. Link: https://patch.msgid.link/20260714-work-bpf-binfmt_misc-v2-7-57b7529c002c@kernel.org Signed-off-by: Christian Brauner (Amutable) --- Documentation/admin-guide/binfmt-misc.rst | 23 +++++++++---- fs/binfmt_misc.c | 41 ++++++++++++++++++----- 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst index c2c18ca9ff8e..85bbf4845f99 100644 --- a/Documentation/admin-guide/binfmt-misc.rst +++ b/Documentation/admin-guide/binfmt-misc.rst @@ -135,18 +135,29 @@ interpreter and the binary, exactly like the optional argument of a ``#!`` interpreter line, e.g. for a handler that resolves ``$ORIGIN`` in a script's ``#!`` path and needs to preserve the argument that followed it. +The invocation flags a static entry fixes at registration - ``P``, ``C`` +and ``O`` - are per-exec choices for a bpf handler, made by the ``load`` +program with the ``bpf_binprm_set_flags()`` kfunc, so a single handler can +decide them differently for each binary it handles: + +- ``BPF_BINPRM_PRESERVE_ARGV0`` keeps the caller's ``argv[0]`` (the ``P`` + flag). +- ``BPF_BINPRM_CREDENTIALS`` computes credentials from the binary (the ``C`` + flag), bounded to user namespaces that map the binary's owner just like + any other setuid exec. +- ``BPF_BINPRM_EXECFD`` opens the binary on the interpreter's behalf and + passes it through the ``AT_EXECFD`` aux vector entry (the ``O`` flag), so + the interpreter can run binaries it could not open by path. + +Because these are program choices, a ``B`` entry carries no flags in the +register string; ``F`` (pre-open a fixed interpreter) has no meaning for it. + A handler is looked up only in the user namespace the struct_ops map was registered in. Handlers are not inherited, so an entry can only reference a handler registered in the same user namespace as its binfmt_misc instance. The entry keeps the handler alive; deleting the struct_ops map only prevents new activations. -The ``F`` flag cannot be combined with ``B`` entries: it pre-opens a fixed -interpreter at registration time and a ``B`` entry has none. The ``C`` flag -works as it does for a static entry: the interpreter runs with the matched -binary's credentials, bounded to user namespaces that map the binary's owner -just like any other setuid exec. - To use binfmt_misc you have to mount it first. You can mount it with ``mount -t binfmt_misc none /proc/sys/fs/binfmt_misc`` command, or you can add a line ``none /proc/sys/fs/binfmt_misc binfmt_misc defaults 0 0`` to your diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index 507f833a3179..c3064f2557ca 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -284,6 +284,7 @@ static const char *entry_select_interpreter(const struct binfmt_misc_entry *e, /* A failing load leaves nothing behind for later entries. */ kfree(bprm->bpf_interp_arg); bprm->bpf_interp_arg = NULL; + bprm->bpf_flags = 0; return ERR_PTR(retval); } @@ -296,6 +297,7 @@ static int load_misc_binary(struct linux_binprm *bprm) const char *interpreter; struct file *interp_file; struct binfmt_misc *misc; + bool preserve_argv0, want_execfd, want_creds; int retval; misc = current_binfmt_misc(); @@ -314,7 +316,28 @@ static int load_misc_binary(struct linux_binprm *bprm) if (IS_ERR(interpreter)) return PTR_ERR(interpreter); - if (fmt->flags & MISC_FMT_PRESERVE_ARGV0) { + /* + * The invocation flags are fixed at registration for a static handler + * and chosen per exec by the load program, via bpf_binprm_set_flags(), + * for a bpf one. + */ + if (test_bit(MISC_FMT_BPF_BIT, &fmt->flags)) { + u64 f = bprm->bpf_flags; + + /* Clear so it can't accumulate into a nested interpreter level. */ + bprm->bpf_flags = 0; + + preserve_argv0 = f & BPF_BINPRM_PRESERVE_ARGV0; + want_creds = f & BPF_BINPRM_CREDENTIALS; + want_execfd = f & (BPF_BINPRM_CREDENTIALS | BPF_BINPRM_EXECFD); + } else { + preserve_argv0 = fmt->flags & MISC_FMT_PRESERVE_ARGV0; + want_creds = fmt->flags & MISC_FMT_CREDENTIALS; + want_execfd = fmt->flags & MISC_FMT_OPEN_BINARY; + } + + /* The entry's own choice - not one accumulated from an earlier level. */ + if (preserve_argv0) { bprm->interp_flags |= BINPRM_FLAGS_PRESERVE_ARGV0; } else { retval = remove_arg_zero(bprm); @@ -370,9 +393,9 @@ static int load_misc_binary(struct linux_binprm *bprm) return PTR_ERR(interp_file); bprm->interpreter = interp_file; - if (fmt->flags & MISC_FMT_OPEN_BINARY) + if (want_execfd) bprm->have_execfd = 1; - if (fmt->flags & MISC_FMT_CREDENTIALS) + if (want_creds) bprm->execfd_creds = 1; return 0; } @@ -650,14 +673,14 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, return ERR_PTR(-EINVAL); /* - * 'F' pre-opens a fixed interpreter at registration time which is - * meaningless for a per-exec computed path. 'C' is fine: it honors the - * suid bits of the matched binary exactly like a static entry, gated by - * the same vfsuid_has_mapping() check in bprm_fill_uid() that keeps the - * transition to uids mapped in the caller's user namespace. + * A bpf handler decides the invocation flags per exec with + * bpf_binprm_set_flags() rather than fixing them at registration, so a + * 'B' entry carries no flags: 'P', 'C' and 'O' become per-exec choices + * and 'F' (pre-open a fixed interpreter) is meaningless for it. */ if (test_bit(MISC_FMT_BPF_BIT, &e->flags) && - (e->flags & MISC_FMT_OPEN_FILE)) + (e->flags & (MISC_FMT_PRESERVE_ARGV0 | MISC_FMT_OPEN_BINARY | + MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_FILE))) return ERR_PTR(-EINVAL); return no_free_ptr(e); From 277d787feb2edeb3adae81a99fc47294d523a106 Mon Sep 17 00:00:00 2001 From: Farid Zakaria Date: Tue, 14 Jul 2026 21:58:14 +0200 Subject: [PATCH 29/63] selftests/exec: add binfmt_misc bpf-backed handler test Exercise the bpf-backed ('B') binfmt_misc handlers end to end. A handler is a struct binfmt_misc_ops struct_ops map; the test loads and attaches it (which publishes it by name), activates it with a 'B' entry, and checks that a matched binary is routed to the interpreter the program selected via bpf_binprm_set_interp(). Two self-contained cases are covered: - bpf_interp: the match program matches a synthetic aarch64 ELF header from the prefetched bprm->buf and the load program routes it to a fixed interpreter of its choosing. - nix_origin: the match program parses the program headers to commit only to a "$ORIGIN/..."-relative PT_INTERP and the load program resolves it to an interpreter co-located with the binary -- the relocatable-loader case the kernel ELF loader cannot express. The relocatable binary is linked with PT_INTERP set to the literal "$ORIGIN/binfmt_bpf_interp" (-Wl,--dynamic-linker), which the kernel cannot resolve on its own. Both route to a small test interpreter that prints a marker, proving the program-selected interpreter actually ran. The bpf objects are compiled against the running kernel's BTF: the Makefile generates vmlinux.h with bpftool and the harness links libbpf. Override CLANG/BPFTOOL/VMLINUX_BTF/LIBBPF_CFLAGS/LIBBPF_LDLIBS as needed. The bpf pieces are only built when clang, bpftool, the vmlinux BTF and libbpf are all present (HAVE_BPF_TOOLCHAIN=y forces them) so the other exec selftests keep building without a bpf toolchain. Christian Brauner (Amutable) says: Adapted to the two-op contract: 'B' entries carry the handler name in the interpreter field, both programs are sleepable, the match programs decide. nix_origin reads PT_INTERP from the match program and load returns zero on success. Skip on kernels without binfmt_misc_ops in BTF. Build the bpf pieces only when the toolchain is present and gitignore the generated artifacts. Signed-off-by: Farid Zakaria Link: https://patch.msgid.link/20260714-work-bpf-binfmt_misc-v2-9-57b7529c002c@kernel.org Signed-off-by: Christian Brauner (Amutable) --- tools/testing/selftests/exec/.gitignore | 5 + tools/testing/selftests/exec/Makefile | 48 +++ tools/testing/selftests/exec/binfmt_bpf_app.c | 12 + .../selftests/exec/binfmt_bpf_interp.c | 15 + .../testing/selftests/exec/binfmt_misc_bpf.c | 277 ++++++++++++++++++ tools/testing/selftests/exec/bpf_interp.bpf.c | 61 ++++ tools/testing/selftests/exec/nix_origin.bpf.c | 224 ++++++++++++++ 7 files changed, 642 insertions(+) create mode 100644 tools/testing/selftests/exec/binfmt_bpf_app.c create mode 100644 tools/testing/selftests/exec/binfmt_bpf_interp.c create mode 100644 tools/testing/selftests/exec/binfmt_misc_bpf.c create mode 100644 tools/testing/selftests/exec/bpf_interp.bpf.c create mode 100644 tools/testing/selftests/exec/nix_origin.bpf.c diff --git a/tools/testing/selftests/exec/.gitignore b/tools/testing/selftests/exec/.gitignore index 7f3d1ae762ec..8b93b405c424 100644 --- a/tools/testing/selftests/exec/.gitignore +++ b/tools/testing/selftests/exec/.gitignore @@ -19,3 +19,8 @@ null-argv xxxxxxxx* pipe S_I*.test +binfmt_misc_bpf +binfmt_bpf_interp +binfmt_bpf_app +*.bpf.o +vmlinux.h diff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile index 45a3cfc435cf..ec66c1fecfc0 100644 --- a/tools/testing/selftests/exec/Makefile +++ b/tools/testing/selftests/exec/Makefile @@ -21,6 +21,26 @@ TEST_GEN_PROGS += recursion-depth TEST_GEN_PROGS += null-argv TEST_GEN_PROGS += check-exec +# binfmt_misc bpf-backed ('B') handler test: a libbpf harness plus its +# struct_ops objects and the test interpreter/app it routes between. Only +# built when clang, bpftool, the vmlinux BTF and libbpf are all present +# (HAVE_BPF_TOOLCHAIN=y forces it) so the other exec selftests don't grow +# a bpf toolchain dependency. +CLANG ?= clang +BPFTOOL ?= bpftool +VMLINUX_BTF ?= /sys/kernel/btf/vmlinux +HAVE_BPF_TOOLCHAIN ?= $(shell command -v $(CLANG) >/dev/null 2>&1 && \ + command -v $(BPFTOOL) >/dev/null 2>&1 && \ + test -r $(VMLINUX_BTF) && \ + pkg-config --exists libbpf 2>/dev/null && echo y) +ifeq ($(HAVE_BPF_TOOLCHAIN),y) +TEST_GEN_PROGS += binfmt_misc_bpf +TEST_GEN_FILES += bpf_interp.bpf.o nix_origin.bpf.o +TEST_GEN_FILES += binfmt_bpf_interp binfmt_bpf_app +else +$(info exec selftests: skipping binfmt_misc_bpf, needs clang, bpftool, vmlinux BTF and libbpf) +endif + EXTRA_CLEAN := $(OUTPUT)/subdir.moved $(OUTPUT)/execveat.moved $(OUTPUT)/xxxxx* \ $(OUTPUT)/S_I*.test @@ -55,3 +75,31 @@ $(OUTPUT)/script-exec.inc: $(CHECK_EXEC_SAMPLES)/script-exec.inc cp $< $@ $(OUTPUT)/script-noexec.inc: $(CHECK_EXEC_SAMPLES)/script-noexec.inc cp $< $@ + +# --- binfmt_misc bpf ('B') handler test --------------------------------- +# The struct_ops bpf objects are compiled against the running kernel's BTF. +# CLANG/BPFTOOL/VMLINUX_BTF are set above next to the toolchain check; +# override LIBBPF_CFLAGS/LDLIBS to point at a libbpf install. +BPF_CFLAGS ?= -I$(OUTPUT) +LIBBPF_CFLAGS ?= +LIBBPF_LDLIBS ?= -lbpf -lelf -lz + +$(OUTPUT)/vmlinux.h: + $(BPFTOOL) btf dump file $(VMLINUX_BTF) format c > $@ + sed -i '/__ksym;$$/d' $@ + +$(OUTPUT)/%.bpf.o: %.bpf.c $(OUTPUT)/vmlinux.h + $(CLANG) -g -O2 -target bpf -mcpu=v3 $(BPF_CFLAGS) $(LIBBPF_CFLAGS) -c $< -o $@ + +$(OUTPUT)/binfmt_misc_bpf: binfmt_misc_bpf.c + $(CC) $(CFLAGS) $(LIBBPF_CFLAGS) $(LDFLAGS) $< $(LIBBPF_LDLIBS) -o $@ + +$(OUTPUT)/binfmt_bpf_interp: binfmt_bpf_interp.c + $(CC) $(CFLAGS) $(LDFLAGS) $< -o $@ + +# PT_INTERP is set to the literal "$ORIGIN/binfmt_bpf_interp"; the nix_origin +# handler resolves it relative to the binary at run time. +$(OUTPUT)/binfmt_bpf_app: binfmt_bpf_app.c + $(CC) $(CFLAGS) $(LDFLAGS) -Wl,--dynamic-linker,'$$ORIGIN/binfmt_bpf_interp' $< -o $@ + +EXTRA_CLEAN += $(OUTPUT)/vmlinux.h $(OUTPUT)/bpf_interp.bpf.o $(OUTPUT)/nix_origin.bpf.o diff --git a/tools/testing/selftests/exec/binfmt_bpf_app.c b/tools/testing/selftests/exec/binfmt_bpf_app.c new file mode 100644 index 000000000000..472270f148bc --- /dev/null +++ b/tools/testing/selftests/exec/binfmt_bpf_app.c @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * A relocatable binary for the binfmt_misc_bpf $ORIGIN case. The Makefile + * links it with PT_INTERP set to the literal "$ORIGIN/binfmt_bpf_interp" + * (-Wl,--dynamic-linker), which the kernel ELF loader cannot resolve. The + * nix_origin bpf handler resolves it relative to this binary's directory and + * routes execution to the co-located interpreter. + */ +int main(void) +{ + return 0; +} diff --git a/tools/testing/selftests/exec/binfmt_bpf_interp.c b/tools/testing/selftests/exec/binfmt_bpf_interp.c new file mode 100644 index 000000000000..2db205f095b2 --- /dev/null +++ b/tools/testing/selftests/exec/binfmt_bpf_interp.c @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Test interpreter for the binfmt_misc_bpf selftest. A bpf-backed 'B' handler + * routes a matched binary here; printing this marker proves the program's + * chosen interpreter actually ran. + */ +#include + +int main(int argc, char **argv) +{ + (void)argc; + (void)argv; + write(1, "BPF_INTERP_RAN\n", 15); + return 0; +} diff --git a/tools/testing/selftests/exec/binfmt_misc_bpf.c b/tools/testing/selftests/exec/binfmt_misc_bpf.c new file mode 100644 index 000000000000..cb89d2766fe2 --- /dev/null +++ b/tools/testing/selftests/exec/binfmt_misc_bpf.c @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Selftest for binfmt_misc bpf-backed ('B') handlers. + * + * A handler is a struct binfmt_misc_ops struct_ops map with a sleepable match + * and a sleepable load program. Attaching it publishes it by name in the + * caller's user namespace; a 'B' entry referencing it by name in the + * interpreter field activates it: + * + * echo ':name:B:::::' > /proc/sys/fs/binfmt_misc/register + * + * Two self-contained cases are exercised: + * + * 1. bpf_interp: the match program matches a synthetic aarch64 ELF header + * from the prefetched bprm->buf and the load program routes it to a + * fixed interpreter of its choosing. + * 2. nix_origin: the match program reads the binary's program headers to + * commit only to a "$ORIGIN/..."-relative PT_INTERP and the load program + * resolves it to an interpreter co-located with the binary (the + * relocatable-loader case the kernel ELF loader cannot express). + * + * Both route to a test interpreter that prints BPF_INTERP_RAN, proving the + * program's chosen interpreter actually ran. + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define INTERP_PATH "/tmp/binfmt_bpf_interp" +#define AARCH64_PATH "/tmp/binfmt_bpf_aarch64" +#define RELOC_DIR "/tmp/binfmt_reloc" +#define BINFMT_REG "/proc/sys/fs/binfmt_misc/register" +#define EXPECT "BPF_INTERP_RAN" + +static char testdir[512]; /* directory holding this test's built artifacts */ + +static int copy_file(const char *src, const char *dst) +{ + char buf[4096]; + int in, out; + ssize_t n; + + in = open(src, O_RDONLY); + if (in < 0) + return -1; + out = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0755); + if (out < 0) { + close(in); + return -1; + } + while ((n = read(in, buf, sizeof(buf))) > 0) { + if (write(out, buf, n) != n) { + close(in); + close(out); + return -1; + } + } + close(in); + close(out); + return n < 0 ? -1 : 0; +} + +/* A minimal 64-bit little-endian aarch64 ELF header, padded to the read size. */ +static int create_fake_aarch64(const char *path) +{ + unsigned char hdr[256] = {0}; + int fd; + + hdr[0] = 0x7f; hdr[1] = 'E'; hdr[2] = 'L'; hdr[3] = 'F'; + hdr[4] = 2; /* ELFCLASS64 */ + hdr[5] = 1; /* ELFDATA2LSB */ + hdr[6] = 1; /* EV_CURRENT */ + hdr[16] = 2; /* e_type = ET_EXEC */ + hdr[18] = 183 & 0xff; /* e_machine = EM_AARCH64 */ + hdr[19] = (183 >> 8) & 0xff; + hdr[20] = 1; /* e_version */ + + fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0755); + if (fd < 0) + return -1; + if (write(fd, hdr, sizeof(hdr)) != (ssize_t)sizeof(hdr)) { + close(fd); + return -1; + } + close(fd); + return 0; +} + +static int register_entry(const char *name, const char *handler) +{ + char rule[128]; + int fd; + ssize_t n; + + snprintf(rule, sizeof(rule), ":%s:B::::%s:", name, handler); + fd = open(BINFMT_REG, O_WRONLY); + if (fd < 0) + return -1; + n = write(fd, rule, strlen(rule)); + close(fd); + return n < 0 ? -1 : 0; +} + +static void unregister_entry(const char *name) +{ + char path[128]; + int fd; + + snprintf(path, sizeof(path), "/proc/sys/fs/binfmt_misc/%s", name); + fd = open(path, O_WRONLY); + if (fd >= 0) { + if (write(fd, "-1", 2) < 0) + ; /* best effort */ + close(fd); + } +} + +static int check_output(const char *cmd, const char *expected) +{ + char buf[128]; + FILE *fp; + + fp = popen(cmd, "r"); + if (!fp) + return -1; + if (!fgets(buf, sizeof(buf), fp)) { + pclose(fp); + return -1; + } + pclose(fp); + return strncmp(buf, expected, strlen(expected)) ? -1 : 0; +} + +/* + * Load @objfile, attach its struct_ops map @handler (which publishes the + * handler), activate a 'B' entry named @entry that references it, run @target + * and check it produced @expect. + */ +static int run_case(const char *objfile, const char *handler, + const char *entry, const char *target, const char *expect) +{ + struct bpf_object *obj; + struct bpf_map *map; + struct bpf_link *link; + int ret = -1; + + obj = bpf_object__open_file(objfile, NULL); + if (!obj || libbpf_get_error(obj)) { + fprintf(stderr, "open %s failed\n", objfile); + return -1; + } + if (bpf_object__load(obj)) { + fprintf(stderr, "load %s failed (check dmesg for the verifier log)\n", + objfile); + goto close; + } + map = bpf_object__find_map_by_name(obj, handler); + if (!map) { + fprintf(stderr, "no struct_ops map '%s' in %s\n", handler, objfile); + goto close; + } + link = bpf_map__attach_struct_ops(map); + if (!link || libbpf_get_error(link)) { + fprintf(stderr, "attach struct_ops '%s' failed\n", handler); + goto close; + } + if (register_entry(entry, handler)) { + fprintf(stderr, "register 'B' entry '%s' failed\n", entry); + goto detach; + } + ret = check_output(target, expect); + unregister_entry(entry); +detach: + bpf_link__destroy(link); +close: + bpf_object__close(obj); + return ret; +} + +int main(void) +{ + char src[600], obj[600], appdst[600], interpdst[600]; + char exe[512]; + ssize_t n; + int fail = 0; + struct stat st; + struct btf *btf; + + if (getuid() != 0) { + fprintf(stderr, "Skipping: test must be run as root\n"); + return 4; /* KSFT_SKIP */ + } + + /* The kernel must know struct binfmt_misc_ops (CONFIG_BINFMT_MISC_BPF). */ + btf = btf__load_vmlinux_btf(); + if (!btf || btf__find_by_name_kind(btf, "binfmt_misc_ops", + BTF_KIND_STRUCT) < 0) { + fprintf(stderr, + "Skipping: no struct binfmt_misc_ops in the kernel BTF (CONFIG_BINFMT_MISC_BPF)\n"); + btf__free(btf); + return 4; /* KSFT_SKIP */ + } + btf__free(btf); + + n = readlink("/proc/self/exe", exe, sizeof(exe) - 1); + if (n < 0) { + perror("readlink"); + return 1; + } + exe[n] = '\0'; + snprintf(testdir, sizeof(testdir), "%s", dirname(exe)); + + if (stat("/sys/fs/bpf", &st) < 0) + mkdir("/sys/fs/bpf", 0755); + mount("bpf", "/sys/fs/bpf", "bpf", 0, NULL); + if (access(BINFMT_REG, F_OK) < 0) + mount("binfmt_misc", "/proc/sys/fs/binfmt_misc", "binfmt_misc", 0, NULL); + + /* Shared test interpreter. */ + snprintf(src, sizeof(src), "%s/binfmt_bpf_interp", testdir); + if (copy_file(src, INTERP_PATH)) { + fprintf(stderr, "cannot install %s\n", INTERP_PATH); + return 1; + } + + /* Case 1: match a synthetic aarch64 header -> fixed interpreter. */ + printf("[*] case 1: match aarch64 header -> program-chosen interpreter\n"); + if (create_fake_aarch64(AARCH64_PATH)) { + fprintf(stderr, "cannot create %s\n", AARCH64_PATH); + return 1; + } + snprintf(obj, sizeof(obj), "%s/bpf_interp.bpf.o", testdir); + if (run_case(obj, "bpf_interp", "test_bpf_interp", AARCH64_PATH, EXPECT) == 0) + printf("[+] case 1 passed\n"); + else { + printf("[-] case 1 FAILED\n"); + fail = 1; + } + unlink(AARCH64_PATH); + + /* Case 2: $ORIGIN-relative PT_INTERP -> co-located interpreter. */ + printf("[*] case 2: $ORIGIN interpreter resolved relative to the binary\n"); + mkdir(RELOC_DIR, 0755); + snprintf(appdst, sizeof(appdst), "%s/app", RELOC_DIR); + snprintf(interpdst, sizeof(interpdst), "%s/binfmt_bpf_interp", RELOC_DIR); + snprintf(src, sizeof(src), "%s/binfmt_bpf_app", testdir); + if (copy_file(src, appdst) || + copy_file(INTERP_PATH, interpdst)) { + fprintf(stderr, "cannot set up %s\n", RELOC_DIR); + fail = 1; + } else { + snprintf(obj, sizeof(obj), "%s/nix_origin.bpf.o", testdir); + if (run_case(obj, "nix_origin", "test_bpf_origin", appdst, EXPECT) == 0) + printf("[+] case 2 passed\n"); + else { + printf("[-] case 2 FAILED\n"); + fail = 1; + } + } + unlink(appdst); + unlink(interpdst); + rmdir(RELOC_DIR); + unlink(INTERP_PATH); + + if (!fail) + printf("[*] all binfmt_misc bpf cases passed\n"); + return fail; +} diff --git a/tools/testing/selftests/exec/bpf_interp.bpf.c b/tools/testing/selftests/exec/bpf_interp.bpf.c new file mode 100644 index 000000000000..8df2d2d01e25 --- /dev/null +++ b/tools/testing/selftests/exec/bpf_interp.bpf.c @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * binfmt_misc_ops handler for the selftest's fixed-interpreter case: match a + * 64-bit aarch64 ELF header from the prefetched buffer and route it to a fixed + * interpreter chosen by the program. This is the portable, self-contained + * equivalent of routing a foreign binary to an emulator: it matches + * programmatically and computes the interpreter, but points at a test binary + * the harness installs rather than a system emulator. + */ +#include "vmlinux.h" +#include +#include + +char _license[] SEC("license") = "GPL"; + +#define EI_CLASS 4 +#define ELFCLASS64 2 +#define EM_AARCH64 183 + +extern int bpf_binprm_set_interp(struct linux_binprm *bprm, const char *path, + size_t path__sz) __ksym; + +/* + * A magic-style decision needs nothing beyond the prefetched bprm->buf, + * even though the match program could read the file. + */ +SEC("struct_ops.s/match") +bool BPF_PROG(bpf_interp_match, struct linux_binprm *bprm) +{ + __u16 machine; + + if (bprm->buf[0] != 0x7f || bprm->buf[1] != 'E' || + bprm->buf[2] != 'L' || bprm->buf[3] != 'F' || + bprm->buf[EI_CLASS] != ELFCLASS64) + return false; + + /* e_machine is a 16-bit little-endian field at offset 18. */ + machine = (__u8)bprm->buf[18] | ((__u16)(__u8)bprm->buf[19] << 8); + return machine == EM_AARCH64; +} + +SEC("struct_ops.s/load") +int BPF_PROG(bpf_interp_load, struct linux_binprm *bprm) +{ + /* + * Keep the path on the (writable) stack: bpf_binprm_set_interp() takes + * a sized memory arg and the verifier rejects a read-only .rodata + * buffer for it. The harness installs the interpreter at this path. + */ + char interp[] = "/tmp/binfmt_bpf_interp"; + + /* @path__sz includes the terminating NUL; 0 commits the selection. */ + return bpf_binprm_set_interp(bprm, interp, sizeof(interp)); +} + +SEC(".struct_ops.link") +struct binfmt_misc_ops bpf_interp = { + .match = (void *)bpf_interp_match, + .load = (void *)bpf_interp_load, + .name = "bpf_interp", +}; diff --git a/tools/testing/selftests/exec/nix_origin.bpf.c b/tools/testing/selftests/exec/nix_origin.bpf.c new file mode 100644 index 000000000000..378e22a4c43b --- /dev/null +++ b/tools/testing/selftests/exec/nix_origin.bpf.c @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * nix_origin.bpf.c - $ORIGIN-relative PT_INTERP resolution + * + * A binfmt_misc_ops handler that makes relocatable (Nix-style) ELF + * binaries work: if PT_INTERP starts with "$ORIGIN/", the loader is + * resolved relative to the directory of the binary being executed and + * selected via bpf_binprm_set_interp(). The match program reads the + * program headers itself, so anything else never commits to this + * handler and passes through untouched. + * + * Activate with: + * bpftool struct_ops register nix_origin.bpf.o /sys/fs/bpf + * echo ':nix-origin:B::::nix_origin:' > /proc/sys/fs/binfmt_misc/register + */ +#include "vmlinux.h" +#include +#include + +char _license[] SEC("license") = "GPL"; + +#define PATH_MAX 4096 +#define EI_CLASS 4 +#define ELFCLASSXX 2 /* ELFCLASS64; flip to 1 for 32-bit */ +#define PT_INTERP 3 +#define MAX_PHDRS 64 + +#define ORIGIN "$ORIGIN" +#define ORIGIN_LEN (sizeof(ORIGIN) - 1) + +#define ENOENT 2 +#define ENOEXEC 8 +#define ENAMETOOLONG 36 + +extern int bpf_dynptr_from_file(struct file *file, __u32 flags, + struct bpf_dynptr *ptr__uninit) __ksym; +extern int bpf_dynptr_file_discard(struct bpf_dynptr *dynptr) __ksym; +extern int bpf_path_d_path(const struct path *path, char *buf, + size_t buf__sz) __ksym; +extern int bpf_binprm_set_interp(struct linux_binprm *bprm, const char *path, + size_t path__sz) __ksym; + +struct scratch { + char interp[PATH_MAX]; /* PT_INTERP as embedded in the binary */ + char path[PATH_MAX]; /* d_path of the binary, becomes the result */ +}; + +/* Keyed by pid: execs run concurrently and the programs can sleep. */ +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 512); + __type(key, __u64); + __type(value, struct scratch); +} scratch_map SEC(".maps"); + +static const struct scratch zero_scratch; + +/* An ELF64 binary per the prefetched header? */ +static bool is_elf64(struct linux_binprm *bprm) +{ + return bprm->buf[0] == 0x7f && bprm->buf[1] == 'E' && + bprm->buf[2] == 'L' && bprm->buf[3] == 'F' && + bprm->buf[EI_CLASS] == ELFCLASSXX; +} + +/* Locate PT_INTERP; false if the file has none or looks malformed. */ +static bool find_pt_interp(struct bpf_dynptr *dp, struct elf64_phdr *phdr) +{ + struct elf64_hdr ehdr; + bool found = false; + int i; + + if (bpf_dynptr_read(&ehdr, sizeof(ehdr), dp, 0, 0)) + return false; + if (ehdr.e_phentsize != sizeof(struct elf64_phdr)) + return false; + + bpf_for(i, 0, ehdr.e_phnum) { + if (i >= MAX_PHDRS) + break; + if (bpf_dynptr_read(phdr, sizeof(*phdr), dp, + ehdr.e_phoff + i * sizeof(*phdr), 0)) + return false; + if (phdr->p_type == PT_INTERP) { + found = true; + break; + } + } + return found; +} + +/* + * An ELF64 binary whose PT_INTERP starts with "$ORIGIN/" is ours. The + * match can sleep and read the file, so the decision is made here and + * regular binaries never commit to this handler: later binfmt_misc + * entries and binfmt_elf see them as if we did not exist. + */ +SEC("struct_ops.s/match") +bool BPF_PROG(nix_origin_match, struct linux_binprm *bprm) +{ + char prefix[ORIGIN_LEN + 1] = {}; + struct elf64_phdr phdr; + struct bpf_dynptr dp; + bool ours = false; + + if (!is_elf64(bprm)) + return false; + + /* The dynptr must be discarded on every path once requested. */ + if (bpf_dynptr_from_file(bprm->file, 0, &dp)) + goto out; + if (find_pt_interp(&dp, &phdr) && + phdr.p_filesz > ORIGIN_LEN + 1 && + !bpf_dynptr_read(prefix, sizeof(prefix), &dp, phdr.p_offset, 0)) + ours = !bpf_strncmp(prefix, sizeof(prefix), ORIGIN "/"); +out: + bpf_dynptr_file_discard(&dp); + return ours; +} + +/* + * The match is committed and already vetted the "$ORIGIN/" prefix, so + * everything here reads the file again from scratch: -ENOEXEC only + * covers a binary that changed under us and stopped being ours. + */ +SEC("struct_ops.s/load") +int BPF_PROG(nix_origin_load, struct linux_binprm *bprm) +{ + __u32 isz, sfx, rsz, slash; + struct elf64_phdr phdr; + struct bpf_dynptr dp; + struct scratch *sc; + __u64 id; + int ret = -ENOEXEC, len, i; + + if (bpf_dynptr_from_file(bprm->file, 0, &dp)) + goto out; + + if (!find_pt_interp(&dp, &phdr)) + goto out; + + isz = phdr.p_filesz; + if (isz <= ORIGIN_LEN + 1 || isz >= sizeof(sc->interp)) + goto out; + /* + * The range check above compiles to a test on a zero-extended copy of + * the u64 p_filesz, so the verifier does not carry the bound to the + * dynptr_read() length below ("unbounded memory access"). Mask isz to + * the buffer size (a power of two) and force the masked value to be + * materialized with a barrier so the read uses the bounded register. + */ + isz &= sizeof(sc->interp) - 1; + barrier_var(isz); + + id = bpf_get_current_pid_tgid(); + if (bpf_map_update_elem(&scratch_map, &id, &zero_scratch, BPF_ANY)) + goto out; + sc = bpf_map_lookup_elem(&scratch_map, &id); + if (!sc) + goto out_del; + + if (bpf_dynptr_read(sc->interp, isz, &dp, phdr.p_offset, 0)) + goto out_del; + if (sc->interp[isz - 1] != '\0') + goto out_del; + + /* Not "$ORIGIN/..." anymore? Then it is not ours anymore either. */ + if (sc->interp[0] != '$' || sc->interp[1] != 'O' || + sc->interp[2] != 'R' || sc->interp[3] != 'I' || + sc->interp[4] != 'G' || sc->interp[5] != 'I' || + sc->interp[6] != 'N' || sc->interp[7] != '/') + goto out_del; + + /* + * From here on resolution failures fail the exec instead of falling + * back to binfmt_elf, which would resolve the literal "$ORIGIN/..." + * relative to the caller's cwd. + */ + ret = -ENOENT; + len = bpf_path_d_path(&bprm->file->f_path, sc->path, sizeof(sc->path)); + if (len <= 0 || len > sizeof(sc->path)) + goto out_del; + /* Unreachable or unlinked ("... (deleted)") binaries can't resolve. */ + if (sc->path[0] != '/') + goto out_del; + + /* $ORIGIN = dirname of the binary. */ + slash = 0; + bpf_for(i, 1, len - 1) { + if (i >= sizeof(sc->path)) + break; + if (sc->path[i] == '/') + slash = i; + } + + /* Splice the suffix (leading '/' and NUL included) onto the dir. */ + sfx = isz - ORIGIN_LEN; + rsz = slash + sfx; + if (rsz > sizeof(sc->path)) { + ret = -ENAMETOOLONG; + goto out_del; + } + bpf_for(i, 0, sfx) { + __u32 s = ORIGIN_LEN + i, d = slash + i; + + if (s >= sizeof(sc->interp) || d >= sizeof(sc->path)) + break; + sc->path[d] = sc->interp[s]; + } + + ret = bpf_binprm_set_interp(bprm, sc->path, rsz); +out_del: + bpf_map_delete_elem(&scratch_map, &id); +out: + bpf_dynptr_file_discard(&dp); + return ret; +} + +SEC(".struct_ops.link") +struct binfmt_misc_ops nix_origin = { + .match = (void *)nix_origin_match, + .load = (void *)nix_origin_load, + .name = "nix_origin", +}; From 7830e96d001c86c3dd34a2434277d86bba14c8c6 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 16:13:43 +0200 Subject: [PATCH 30/63] binfmt_misc: require an absolute interpreter path with 'C' A 'C' entry computes the credentials from the matched binary instead of from the interpreter. So a set*id binary hands its credentials to whatever the entry names as its interpreter. Without 'F' that interpreter is not opened until the exec happens and open_exec() resolves the path relative to the current working directory. The working directory at that point belongs to whoever runs the binary not to whoever registered the entry. So :x:M::\x7fELF::interp:C lets every user who execs a matching set*id binary from a directory they control run their own interp with that binary's credentials. A relative interpreter has no sensible use here to begin with. The registering task cannot know what the working directory will be. Make the register string reject the combination at registration time. This does refuse register strings that used to be accepted. The 'F' flag covers the case where the interpreter really is meant to be resolved in the registrant's context, and it resolves it once, at registration. Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-1-e57866e4ae0f@kernel.org Reviewed-by: Farid Zakaria Signed-off-by: Christian Brauner (Amutable) --- Documentation/admin-guide/binfmt-misc.rst | 4 ++++ fs/binfmt_misc.c | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst index 85bbf4845f99..557c8eadb9df 100644 --- a/Documentation/admin-guide/binfmt-misc.rst +++ b/Documentation/admin-guide/binfmt-misc.rst @@ -98,6 +98,10 @@ There are some restrictions: - the magic must reside in the first 128 bytes of the file, i.e. offset+size(magic) has to be less than 128 - the interpreter string may not exceed 127 characters + - an interpreter used with ``C`` but without ``F`` has to be named by an + absolute path. It is opened when the binary is executed, so a relative + one would be resolved against the working directory of whoever runs + the binary bpf-backed handlers diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index c3064f2557ca..70a18623a22b 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -683,6 +683,12 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_FILE))) return ERR_PTR(-EINVAL); + /* Non-F opens the interp at exec against the caller's cwd; require absolute. */ + if ((e->flags & MISC_FMT_CREDENTIALS) && + !(e->flags & MISC_FMT_OPEN_FILE) && + e->interpreter[0] != '/') + return ERR_PTR(-EINVAL); + return no_free_ptr(e); } From ee3db4b8660d709be0a112e56d379b918a607234 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 16:13:44 +0200 Subject: [PATCH 31/63] docs, binfmt_misc: keep general usage out of the handler sections The general usage trails the bpf-backed handlers section and therefore reads as part of it. It predates that section and applies to binfmt_misc as a whole. Move it back up so the handler section ends where the file does. Upcoming sections describing the transparent and loader dispatch modes append after it without swallowing the general prose again. Pure text move, no content changes. Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-2-e57866e4ae0f@kernel.org Reviewed-by: Farid Zakaria Signed-off-by: Christian Brauner (Amutable) --- Documentation/admin-guide/binfmt-misc.rst | 79 ++++++++++++----------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst index 557c8eadb9df..9f0d9132723f 100644 --- a/Documentation/admin-guide/binfmt-misc.rst +++ b/Documentation/admin-guide/binfmt-misc.rst @@ -104,6 +104,46 @@ There are some restrictions: the binary +To use binfmt_misc you have to mount it first. You can mount it with +``mount -t binfmt_misc none /proc/sys/fs/binfmt_misc`` command, or you can add +a line ``none /proc/sys/fs/binfmt_misc binfmt_misc defaults 0 0`` to your +``/etc/fstab`` so it auto mounts on boot. + +You may want to add the binary formats in one of your ``/etc/rc`` scripts during +boot-up. Read the manual of your init program to figure out how to do this +right. + +Think about the order of adding entries! Later added entries are matched first! + + +A few examples (assumed you are in ``/proc/sys/fs/binfmt_misc``): + +- enable support for em86 (like binfmt_em86, for Alpha AXP only):: + + echo ':i386:M::\x7fELF\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x03:\xff\xff\xff\xff\xff\xfe\xfe\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfb\xff\xff:/bin/em86:' > register + echo ':i486:M::\x7fELF\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x06:\xff\xff\xff\xff\xff\xfe\xfe\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfb\xff\xff:/bin/em86:' > register + +- enable support for packed DOS applications (pre-configured dosemu hdimages):: + + echo ':DEXE:M::\x0eDEX::/usr/bin/dosexec:' > register + +- enable support for Windows executables using wine:: + + echo ':DOSWin:M::MZ::/usr/local/bin/wine:' > register + +For java support see Documentation/admin-guide/java.rst + + +You can enable/disable binfmt_misc or one binary type by echoing 0 (to disable) +or 1 (to enable) to ``/proc/sys/fs/binfmt_misc/status`` or +``/proc/.../the_name``. +Catting the file tells you the current status of ``binfmt_misc/the_entry``. + +You can remove one entry or all entries by echoing -1 to ``/proc/.../the_name`` +or ``/proc/sys/fs/binfmt_misc/status``. A single entry can also be removed +by simply unlinking (``rm``) ``/proc/.../the_name``. + + bpf-backed handlers ------------------- @@ -162,45 +202,6 @@ handler registered in the same user namespace as its binfmt_misc instance. The entry keeps the handler alive; deleting the struct_ops map only prevents new activations. -To use binfmt_misc you have to mount it first. You can mount it with -``mount -t binfmt_misc none /proc/sys/fs/binfmt_misc`` command, or you can add -a line ``none /proc/sys/fs/binfmt_misc binfmt_misc defaults 0 0`` to your -``/etc/fstab`` so it auto mounts on boot. - -You may want to add the binary formats in one of your ``/etc/rc`` scripts during -boot-up. Read the manual of your init program to figure out how to do this -right. - -Think about the order of adding entries! Later added entries are matched first! - - -A few examples (assumed you are in ``/proc/sys/fs/binfmt_misc``): - -- enable support for em86 (like binfmt_em86, for Alpha AXP only):: - - echo ':i386:M::\x7fELF\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x03:\xff\xff\xff\xff\xff\xfe\xfe\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfb\xff\xff:/bin/em86:' > register - echo ':i486:M::\x7fELF\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x06:\xff\xff\xff\xff\xff\xfe\xfe\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfb\xff\xff:/bin/em86:' > register - -- enable support for packed DOS applications (pre-configured dosemu hdimages):: - - echo ':DEXE:M::\x0eDEX::/usr/bin/dosexec:' > register - -- enable support for Windows executables using wine:: - - echo ':DOSWin:M::MZ::/usr/local/bin/wine:' > register - -For java support see Documentation/admin-guide/java.rst - - -You can enable/disable binfmt_misc or one binary type by echoing 0 (to disable) -or 1 (to enable) to ``/proc/sys/fs/binfmt_misc/status`` or -``/proc/.../the_name``. -Catting the file tells you the current status of ``binfmt_misc/the_entry``. - -You can remove one entry or all entries by echoing -1 to ``/proc/.../the_name`` -or ``/proc/sys/fs/binfmt_misc/status``. A single entry can also be removed -by simply unlinking (``rm``) ``/proc/.../the_name``. - Hints ----- From 23c703f9595de9b39d99e392c3879fcf0e800ee9 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 16:13:45 +0200 Subject: [PATCH 32/63] binfmt_misc: table-drive the register string flags Every flag character of the register string is spelled out three times: in the parser, in the entry's /proc output and in the delimiter blacklist that keeps a flag character from sending the flag scan off the end of the buffer. The three lists have to agree, and each new flag has to be added to all of them. Describe a flag once - character, entry flag, implied flags and a description for the registration debug output - and drive all three from the table. While at it, express the "a 'B' entry carries no flags" check as what it is, an empty flags field, rather than as a fourth list of every flag character. Equivalent: the check runs right after check_special_flags(), which advances past exactly the flag characters it consumed and sets exactly their flags. No functional change. Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-3-e57866e4ae0f@kernel.org Reviewed-by: Farid Zakaria Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 92 +++++++++++++++++++++++++++--------------------- 1 file changed, 52 insertions(+), 40 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index 70a18623a22b..d568cd5cc928 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -10,6 +10,7 @@ #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt +#include #include #include #include @@ -51,6 +52,36 @@ enum binfmt_misc_entry_flags { MISC_FMT_OPEN_FILE = (1U << 28), }; +/** + * struct binfmt_misc_flag - a flag character of the register string + * @c: the character userspace writes and reads back + * @flag: the entry flag it sets + * @implies: entry flags it turns on in addition + * @desc: what it does, for the registration debug output + */ +struct binfmt_misc_flag { + char c; + unsigned long flag; + unsigned long implies; + const char *desc; +}; + +static const struct binfmt_misc_flag misc_flags[] = { + { 'P', MISC_FMT_PRESERVE_ARGV0, 0, "preserve argv0" }, + { 'O', MISC_FMT_OPEN_BINARY, 0, "open binary" }, + { 'C', MISC_FMT_CREDENTIALS, MISC_FMT_OPEN_BINARY, "credentials from the binary" }, + { 'F', MISC_FMT_OPEN_FILE, 0, "open interpreter file now" }, +}; + +/* Look up a flag character, NULL if @c is not one. */ +static const struct binfmt_misc_flag *misc_flag_by_char(const char c) +{ + for (int i = 0; i < ARRAY_SIZE(misc_flags); i++) + if (misc_flags[i].c == c) + return &misc_flags[i]; + return NULL; +} + struct binfmt_misc_entry { struct hlist_node node; unsigned long flags; /* type, status, etc. */ @@ -424,30 +455,16 @@ static char *scanarg(char *s, char del) return s; } +/* Parse the 'flags' field, stopping at the first character that is not one. */ static char *check_special_flags(char *p, struct binfmt_misc_entry *e) { for (;; p++) { - switch (*p) { - case 'P': - pr_debug("register: flag: P (preserve argv0)\n"); - e->flags |= MISC_FMT_PRESERVE_ARGV0; - break; - case 'O': - pr_debug("register: flag: O (open binary)\n"); - e->flags |= MISC_FMT_OPEN_BINARY; - break; - case 'C': - pr_debug("register: flag: C (preserve creds)\n"); - /* C implies O */ - e->flags |= MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_BINARY; - break; - case 'F': - pr_debug("register: flag: F: open interpreter file now\n"); - e->flags |= MISC_FMT_OPEN_FILE; - break; - default: + const struct binfmt_misc_flag *f = misc_flag_by_char(*p); + + if (!f) return p; - } + pr_debug("register: flag: %c (%s)\n", f->c, f->desc); + e->flags |= f->flag | f->implies; } } @@ -570,7 +587,7 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, size_t count) { struct binfmt_misc_entry *e __free(kfree) = NULL; - char *buf, *p; + char *buf, *p, *flags; char del; pr_debug("register: received %zu bytes\n", count); @@ -595,7 +612,7 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, pr_debug("register: delim: %#x {%c}\n", del, del); /* A flag-char delimiter runs the flag scan off the buffer. */ - if (del == 'P' || del == 'O' || del == 'C' || del == 'F') + if (misc_flag_by_char(del)) return ERR_PTR(-EINVAL); /* Pad the buffer with the delim to simplify parsing below. */ @@ -666,21 +683,21 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, } /* Parse the 'flags' field. */ + flags = p; p = check_special_flags(p, e); - if (*p == '\n') - p++; - if (p != buf + count) - return ERR_PTR(-EINVAL); /* * A bpf handler decides the invocation flags per exec with - * bpf_binprm_set_flags() rather than fixing them at registration, so a - * 'B' entry carries no flags: 'P', 'C' and 'O' become per-exec choices - * and 'F' (pre-open a fixed interpreter) is meaningless for it. + * bpf_binprm_set_flags() rather than fixing them at registration, and + * 'F' (pre-open a fixed interpreter) is meaningless for it, so a 'B' + * entry's flags field has to be empty. */ - if (test_bit(MISC_FMT_BPF_BIT, &e->flags) && - (e->flags & (MISC_FMT_PRESERVE_ARGV0 | MISC_FMT_OPEN_BINARY | - MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_FILE))) + if (test_bit(MISC_FMT_BPF_BIT, &e->flags) && p != flags) + return ERR_PTR(-EINVAL); + + if (*p == '\n') + p++; + if (p != buf + count) return ERR_PTR(-EINVAL); /* Non-F opens the interp at exec against the caller's cwd; require absolute. */ @@ -749,14 +766,9 @@ static int bm_entry_show(struct seq_file *m, void *unused) /* print the special flags */ seq_puts(m, "flags: "); - if (e->flags & MISC_FMT_PRESERVE_ARGV0) - seq_putc(m, 'P'); - if (e->flags & MISC_FMT_OPEN_BINARY) - seq_putc(m, 'O'); - if (e->flags & MISC_FMT_CREDENTIALS) - seq_putc(m, 'C'); - if (e->flags & MISC_FMT_OPEN_FILE) - seq_putc(m, 'F'); + for (int i = 0; i < ARRAY_SIZE(misc_flags); i++) + if (e->flags & misc_flags[i].flag) + seq_putc(m, misc_flags[i].c); seq_putc(m, '\n'); if (test_bit(MISC_FMT_BPF_BIT, &e->flags)) { From 08915b9f1837de77bda150abc43e6e26e72175e1 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 16:13:46 +0200 Subject: [PATCH 33/63] binfmt_misc: normalize the per-exec invocation flags A static entry fixes its invocation flags at registration. A 'B' entry's load program picks them per exec. Since load_misc_binary() branches on which kind of entry matched and then applies the two flag sets side by side every flag is handled twice and each new one has to be added to both arms. Translate the 'B' flags into the entry flags they mirror and let the dispatch act on a single set of flags. The boolean the two arms communicated 'P' can be removed. No functional change. Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-4-e57866e4ae0f@kernel.org Reviewed-by: Farid Zakaria Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 62 ++++++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index d568cd5cc928..e87da5ece641 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -319,6 +319,40 @@ static const char *entry_select_interpreter(const struct binfmt_misc_entry *e, return ERR_PTR(retval); } +/** + * entry_invocation_flags - the invocation flags in effect for this exec + * @e: matched binary type handler + * @bprm: binary that is being executed + * + * A static entry fixes its flags at registration, a 'B' entry's load program + * picks them per exec with bpf_binprm_set_flags(). Translate the latter into + * the former, implications included, so the dispatch has one set to act on. + * + * Return: the invocation flags for this exec + */ +static unsigned long entry_invocation_flags(const struct binfmt_misc_entry *e, + struct linux_binprm *bprm) +{ + unsigned long flags = 0; + u64 bpf_flags; + + if (!test_bit(MISC_FMT_BPF_BIT, &e->flags)) + return e->flags; + + bpf_flags = bprm->bpf_flags; + /* Clear so they can't accumulate into a nested interpreter level. */ + bprm->bpf_flags = 0; + + if (bpf_flags & BPF_BINPRM_PRESERVE_ARGV0) + flags |= MISC_FMT_PRESERVE_ARGV0; + if (bpf_flags & BPF_BINPRM_EXECFD) + flags |= MISC_FMT_OPEN_BINARY; + if (bpf_flags & BPF_BINPRM_CREDENTIALS) + flags |= MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_BINARY; + + return flags; +} + /* * the loader itself */ @@ -328,7 +362,7 @@ static int load_misc_binary(struct linux_binprm *bprm) const char *interpreter; struct file *interp_file; struct binfmt_misc *misc; - bool preserve_argv0, want_execfd, want_creds; + unsigned long flags; int retval; misc = current_binfmt_misc(); @@ -347,28 +381,10 @@ static int load_misc_binary(struct linux_binprm *bprm) if (IS_ERR(interpreter)) return PTR_ERR(interpreter); - /* - * The invocation flags are fixed at registration for a static handler - * and chosen per exec by the load program, via bpf_binprm_set_flags(), - * for a bpf one. - */ - if (test_bit(MISC_FMT_BPF_BIT, &fmt->flags)) { - u64 f = bprm->bpf_flags; - - /* Clear so it can't accumulate into a nested interpreter level. */ - bprm->bpf_flags = 0; - - preserve_argv0 = f & BPF_BINPRM_PRESERVE_ARGV0; - want_creds = f & BPF_BINPRM_CREDENTIALS; - want_execfd = f & (BPF_BINPRM_CREDENTIALS | BPF_BINPRM_EXECFD); - } else { - preserve_argv0 = fmt->flags & MISC_FMT_PRESERVE_ARGV0; - want_creds = fmt->flags & MISC_FMT_CREDENTIALS; - want_execfd = fmt->flags & MISC_FMT_OPEN_BINARY; - } + flags = entry_invocation_flags(fmt, bprm); /* The entry's own choice - not one accumulated from an earlier level. */ - if (preserve_argv0) { + if (flags & MISC_FMT_PRESERVE_ARGV0) { bprm->interp_flags |= BINPRM_FLAGS_PRESERVE_ARGV0; } else { retval = remove_arg_zero(bprm); @@ -424,9 +440,9 @@ static int load_misc_binary(struct linux_binprm *bprm) return PTR_ERR(interp_file); bprm->interpreter = interp_file; - if (want_execfd) + if (flags & MISC_FMT_OPEN_BINARY) bprm->have_execfd = 1; - if (want_creds) + if (flags & MISC_FMT_CREDENTIALS) bprm->execfd_creds = 1; return 0; } From c41b9cd8cf49120ae6f5f4e78d3080a697902a19 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 16:13:47 +0200 Subject: [PATCH 34/63] binfmt_misc: split out entry_open_interpreter() and build_interp_argv() Opening the interpreter is a property of the matched entry: an 'F' entry hands out a clone of the file it pre-opened at registration time, any other entry opens the selected path. Give that its own helper instead of an if/else in the middle of load_misc_binary(), and let it fail early rather than carrying an ERR_PTR through the successful branch. Building the interpreter's argument vector is the bulk of what remains and the one part of load_misc_binary() that is specific to the classic dispatch. Move it into its own helper too, so the dispatch reads as what it is: pick a handler, pick an interpreter, build the invocation, open it. No functional change. Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-5-e57866e4ae0f@kernel.org Reviewed-by: Farid Zakaria Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 110 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 76 insertions(+), 34 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index e87da5ece641..a47a0a677e93 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -353,35 +353,52 @@ static unsigned long entry_invocation_flags(const struct binfmt_misc_entry *e, return flags; } -/* - * the loader itself +/** + * entry_open_interpreter - open the entry's interpreter for execution + * @e: matched binary type handler + * @interpreter: the interpreter selected for this exec + * + * An 'F' entry hands out a clone of the file it pre-opened at registration, + * any other entry opens the selected path. + * + * Return: the opened interpreter on success, an ERR_PTR on failure */ -static int load_misc_binary(struct linux_binprm *bprm) +static struct file *entry_open_interpreter(const struct binfmt_misc_entry *e, + const char *interpreter) { - struct binfmt_misc_entry *fmt __free(put_binfmt_handler) = NULL; - const char *interpreter; - struct file *interp_file; - struct binfmt_misc *misc; - unsigned long flags; + struct file *interp_file __free(fput) = NULL; int retval; - misc = current_binfmt_misc(); - if (!READ_ONCE(misc->enabled)) - return -ENOEXEC; + if (!(e->flags & MISC_FMT_OPEN_FILE)) + return open_exec(interpreter); - fmt = get_binfmt_handler(misc, bprm); - if (!fmt) - return -ENOEXEC; + interp_file = file_clone_open(e->interp_file); + if (IS_ERR(interp_file)) + return interp_file; - /* Need to be able to load the file after exec */ - if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE) - return -ENOENT; + retval = exe_file_deny_write_access(interp_file); + if (retval) + return ERR_PTR(retval); - interpreter = entry_select_interpreter(fmt, bprm); - if (IS_ERR(interpreter)) - return PTR_ERR(interpreter); + return no_free_ptr(interp_file); +} - flags = entry_invocation_flags(fmt, bprm); +/** + * build_interp_argv - splice the interpreter invocation into the argv + * @bprm: binary that is being executed + * @interpreter: the interpreter selected for this exec + * @flags: invocation flags in effect for this exec + * + * The interpreter becomes argv[0] and the binary its last argument, with an + * optional staged argument in between. The caller's argv[0] is dropped + * unless 'P' keeps it. + * + * Return: 0 on success, a negative error code on failure + */ +static int build_interp_argv(struct linux_binprm *bprm, const char *interpreter, + unsigned long flags) +{ + int retval; /* The entry's own choice - not one accumulated from an earlier level. */ if (flags & MISC_FMT_PRESERVE_ARGV0) { @@ -418,24 +435,49 @@ static int load_misc_binary(struct linux_binprm *bprm) return retval; bprm->argc++; + return 0; +} + +/* + * the loader itself + */ +static int load_misc_binary(struct linux_binprm *bprm) +{ + struct binfmt_misc_entry *fmt __free(put_binfmt_handler) = NULL; + const char *interpreter; + struct file *interp_file; + struct binfmt_misc *misc; + unsigned long flags; + int retval; + + misc = current_binfmt_misc(); + if (!READ_ONCE(misc->enabled)) + return -ENOEXEC; + + fmt = get_binfmt_handler(misc, bprm); + if (!fmt) + return -ENOEXEC; + + /* Need to be able to load the file after exec */ + if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE) + return -ENOENT; + + interpreter = entry_select_interpreter(fmt, bprm); + if (IS_ERR(interpreter)) + return PTR_ERR(interpreter); + + flags = entry_invocation_flags(fmt, bprm); + + retval = build_interp_argv(bprm, interpreter, flags); + if (retval) + return retval; + /* Update interp in case binfmt_script needs it. */ retval = bprm_change_interp(interpreter, bprm); if (retval < 0) return retval; - if (fmt->flags & MISC_FMT_OPEN_FILE) { - interp_file = file_clone_open(fmt->interp_file); - if (!IS_ERR(interp_file)) { - int err = exe_file_deny_write_access(interp_file); - - if (err) { - fput(interp_file); - interp_file = ERR_PTR(err); - } - } - } else { - interp_file = open_exec(interpreter); - } + interp_file = entry_open_interpreter(fmt, interpreter); if (IS_ERR(interp_file)) return PTR_ERR(interp_file); From 9c50e37ca7498550d6af26345902f56381bfd101 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 16:13:48 +0200 Subject: [PATCH 35/63] exec: release the replaced file with do_close_execat() When the format search stages an interpreter exec_binprm() swaps it in and releases the file it replaces. Dropping the write denial the open took is done manually ahead of both release paths. The one path that keeps the file silently relies on it not being called. Let's just use do_close_execat() on the two paths that release the file and drop the denial explicitly on the one that does not. No functional change. Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-6-e57866e4ae0f@kernel.org Reviewed-by: Farid Zakaria Signed-off-by: Christian Brauner (Amutable) --- fs/exec.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/fs/exec.c b/fs/exec.c index 41e1684d999c..061e0f9fb4ef 100644 --- a/fs/exec.c +++ b/fs/exec.c @@ -1735,15 +1735,17 @@ static int exec_binprm(struct linux_binprm *bprm) bprm->file = bprm->interpreter; bprm->interpreter = NULL; - exe_file_allow_write_access(exec); if (unlikely(bprm->have_execfd)) { if (bprm->executable) { - fput(exec); + do_close_execat(exec); return -ENOEXEC; } + /* Only the reference is kept, for AT_EXECFD. */ + exe_file_allow_write_access(exec); bprm->executable = exec; - } else - fput(exec); + } else { + do_close_execat(exec); + } } audit_bprm(bprm); From 2686010586df2d8d01f44c670c943b3815dedc22 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 16:13:49 +0200 Subject: [PATCH 36/63] selftests/exec: convert the binfmt_misc bpf test to the kselftest harness The test reports its own pass and fail lines, returns a bare 4 for KSFT_SKIP and runs both cases in one process, so a failure in the first takes the second with it. It also open-codes the register, unregister, file-copy and mount helpers that the tests for the upcoming transparent and loader dispatch modes need again. Convert it to the kselftest harness: a fixture for the common setup and teardown, one TEST_F per case so each is reported and isolated separately, and SKIP() for the root, BTF and binfmt_misc preconditions. Move the helpers to a shared header on the way, with the register helper preserving the write's errno so a caller can tell a rejected flag combination (EINVAL) from a kernel that does not know the flag at all. The synthetic ELF header gains an e_machine argument and uses the elf.h constants instead of open-coded numbers. The fixture no longer mounts bpffs. The handler is attached with bpf_map__attach_struct_ops() and nothing is ever pinned, the mount was carried along from a bpftool-based draft. The bpf objects are compiled with -DBPF_NO_KFUNC_PROTOTYPES - the guard bpftool emits for exactly this - instead of sed'ing the prototypes out of the generated vmlinux.h. And the config fragment records the options the binfmt_misc tests need so a merge-config kernel can run them. No change in what is tested. Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-7-e57866e4ae0f@kernel.org Signed-off-by: Christian Brauner (Amutable) --- tools/testing/selftests/exec/Makefile | 11 +- .../testing/selftests/exec/binfmt_misc_bpf.c | 216 ++++++------------ .../selftests/exec/binfmt_misc_common.h | 100 ++++++++ tools/testing/selftests/exec/config | 7 + 4 files changed, 189 insertions(+), 145 deletions(-) create mode 100644 tools/testing/selftests/exec/binfmt_misc_common.h diff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile index ec66c1fecfc0..d2a5a58f9432 100644 --- a/tools/testing/selftests/exec/Makefile +++ b/tools/testing/selftests/exec/Makefile @@ -44,6 +44,8 @@ endif EXTRA_CLEAN := $(OUTPUT)/subdir.moved $(OUTPUT)/execveat.moved $(OUTPUT)/xxxxx* \ $(OUTPUT)/S_I*.test +LOCAL_HDRS += binfmt_misc_common.h + include ../lib.mk CHECK_EXEC_SAMPLES := $(top_srcdir)/samples/check-exec @@ -86,12 +88,13 @@ LIBBPF_LDLIBS ?= -lbpf -lelf -lz $(OUTPUT)/vmlinux.h: $(BPFTOOL) btf dump file $(VMLINUX_BTF) format c > $@ - sed -i '/__ksym;$$/d' $@ +# BPF_NO_KFUNC_PROTOTYPES: the programs declare the kfuncs they use themselves. $(OUTPUT)/%.bpf.o: %.bpf.c $(OUTPUT)/vmlinux.h - $(CLANG) -g -O2 -target bpf -mcpu=v3 $(BPF_CFLAGS) $(LIBBPF_CFLAGS) -c $< -o $@ + $(CLANG) -g -O2 -target bpf -mcpu=v3 -DBPF_NO_KFUNC_PROTOTYPES \ + $(BPF_CFLAGS) $(LIBBPF_CFLAGS) -c $< -o $@ -$(OUTPUT)/binfmt_misc_bpf: binfmt_misc_bpf.c +$(OUTPUT)/binfmt_misc_bpf: binfmt_misc_bpf.c binfmt_misc_common.h $(CC) $(CFLAGS) $(LIBBPF_CFLAGS) $(LDFLAGS) $< $(LIBBPF_LDLIBS) -o $@ $(OUTPUT)/binfmt_bpf_interp: binfmt_bpf_interp.c @@ -102,4 +105,4 @@ $(OUTPUT)/binfmt_bpf_interp: binfmt_bpf_interp.c $(OUTPUT)/binfmt_bpf_app: binfmt_bpf_app.c $(CC) $(CFLAGS) $(LDFLAGS) -Wl,--dynamic-linker,'$$ORIGIN/binfmt_bpf_interp' $< -o $@ -EXTRA_CLEAN += $(OUTPUT)/vmlinux.h $(OUTPUT)/bpf_interp.bpf.o $(OUTPUT)/nix_origin.bpf.o +EXTRA_CLEAN += $(OUTPUT)/vmlinux.h $(OUTPUT)/*.bpf.o diff --git a/tools/testing/selftests/exec/binfmt_misc_bpf.c b/tools/testing/selftests/exec/binfmt_misc_bpf.c index cb89d2766fe2..c41fb80f2a72 100644 --- a/tools/testing/selftests/exec/binfmt_misc_bpf.c +++ b/tools/testing/selftests/exec/binfmt_misc_bpf.c @@ -23,68 +23,42 @@ * program's chosen interpreter actually ran. */ #define _GNU_SOURCE +#include +#include #include #include #include #include #include -#include -#include -#include #include #include +#include "binfmt_misc_common.h" +#include "kselftest_harness.h" + #define INTERP_PATH "/tmp/binfmt_bpf_interp" #define AARCH64_PATH "/tmp/binfmt_bpf_aarch64" -#define RELOC_DIR "/tmp/binfmt_reloc" -#define BINFMT_REG "/proc/sys/fs/binfmt_misc/register" +#define RELOC_TEMPLATE "/tmp/binfmt_relocXXXXXX" #define EXPECT "BPF_INTERP_RAN" -static char testdir[512]; /* directory holding this test's built artifacts */ - -static int copy_file(const char *src, const char *dst) -{ - char buf[4096]; - int in, out; - ssize_t n; - - in = open(src, O_RDONLY); - if (in < 0) - return -1; - out = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0755); - if (out < 0) { - close(in); - return -1; - } - while ((n = read(in, buf, sizeof(buf))) > 0) { - if (write(out, buf, n) != n) { - close(in); - close(out); - return -1; - } - } - close(in); - close(out); - return n < 0 ? -1 : 0; -} - -/* A minimal 64-bit little-endian aarch64 ELF header, padded to the read size. */ -static int create_fake_aarch64(const char *path) +/* A minimal 64-bit little-endian ELF header, padded to the read size. */ +static int create_fake_elf(const char *path, unsigned short machine) { unsigned char hdr[256] = {0}; int fd; hdr[0] = 0x7f; hdr[1] = 'E'; hdr[2] = 'L'; hdr[3] = 'F'; - hdr[4] = 2; /* ELFCLASS64 */ - hdr[5] = 1; /* ELFDATA2LSB */ - hdr[6] = 1; /* EV_CURRENT */ - hdr[16] = 2; /* e_type = ET_EXEC */ - hdr[18] = 183 & 0xff; /* e_machine = EM_AARCH64 */ - hdr[19] = (183 >> 8) & 0xff; - hdr[20] = 1; /* e_version */ + hdr[4] = ELFCLASS64; + hdr[5] = ELFDATA2LSB; + hdr[6] = EV_CURRENT; + hdr[16] = ET_EXEC; + hdr[18] = machine & 0xff; /* e_machine, little-endian */ + hdr[19] = machine >> 8; + hdr[20] = EV_CURRENT; - fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0755); + unlink(path); + fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0755); if (fd < 0) return -1; if (write(fd, hdr, sizeof(hdr)) != (ssize_t)sizeof(hdr)) { @@ -97,31 +71,10 @@ static int create_fake_aarch64(const char *path) static int register_entry(const char *name, const char *handler) { - char rule[128]; - int fd; - ssize_t n; + char rule[PATH_MAX]; snprintf(rule, sizeof(rule), ":%s:B::::%s:", name, handler); - fd = open(BINFMT_REG, O_WRONLY); - if (fd < 0) - return -1; - n = write(fd, rule, strlen(rule)); - close(fd); - return n < 0 ? -1 : 0; -} - -static void unregister_entry(const char *name) -{ - char path[128]; - int fd; - - snprintf(path, sizeof(path), "/proc/sys/fs/binfmt_misc/%s", name); - fd = open(path, O_WRONLY); - if (fd >= 0) { - if (write(fd, "-1", 2) < 0) - ; /* best effort */ - close(fd); - } + return write_reg(rule); } static int check_output(const char *cmd, const char *expected) @@ -178,7 +131,7 @@ static int run_case(const char *objfile, const char *handler, goto detach; } ret = check_output(target, expect); - unregister_entry(entry); + unregister(entry); detach: bpf_link__destroy(link); close: @@ -186,92 +139,73 @@ static int run_case(const char *objfile, const char *handler, return ret; } -int main(void) +FIXTURE(bpf_handler) { + char obj[PATH_MAX]; /* struct_ops object of the case under test */ +}; + +FIXTURE_SETUP(bpf_handler) { - char src[600], obj[600], appdst[600], interpdst[600]; - char exe[512]; - ssize_t n; - int fail = 0; - struct stat st; + char src[PATH_MAX]; struct btf *btf; - if (getuid() != 0) { - fprintf(stderr, "Skipping: test must be run as root\n"); - return 4; /* KSFT_SKIP */ - } + if (getuid() != 0) + SKIP(return, "test must be run as root"); /* The kernel must know struct binfmt_misc_ops (CONFIG_BINFMT_MISC_BPF). */ btf = btf__load_vmlinux_btf(); if (!btf || btf__find_by_name_kind(btf, "binfmt_misc_ops", BTF_KIND_STRUCT) < 0) { - fprintf(stderr, - "Skipping: no struct binfmt_misc_ops in the kernel BTF (CONFIG_BINFMT_MISC_BPF)\n"); btf__free(btf); - return 4; /* KSFT_SKIP */ + SKIP(return, + "no struct binfmt_misc_ops in the kernel BTF (CONFIG_BINFMT_MISC_BPF)"); } btf__free(btf); - n = readlink("/proc/self/exe", exe, sizeof(exe) - 1); - if (n < 0) { - perror("readlink"); - return 1; - } - exe[n] = '\0'; - snprintf(testdir, sizeof(testdir), "%s", dirname(exe)); - - if (stat("/sys/fs/bpf", &st) < 0) - mkdir("/sys/fs/bpf", 0755); - mount("bpf", "/sys/fs/bpf", "bpf", 0, NULL); - if (access(BINFMT_REG, F_OK) < 0) - mount("binfmt_misc", "/proc/sys/fs/binfmt_misc", "binfmt_misc", 0, NULL); + if (!binfmt_misc_available()) + SKIP(return, "no binfmt_misc"); /* Shared test interpreter. */ - snprintf(src, sizeof(src), "%s/binfmt_bpf_interp", testdir); - if (copy_file(src, INTERP_PATH)) { - fprintf(stderr, "cannot install %s\n", INTERP_PATH); - return 1; - } - - /* Case 1: match a synthetic aarch64 header -> fixed interpreter. */ - printf("[*] case 1: match aarch64 header -> program-chosen interpreter\n"); - if (create_fake_aarch64(AARCH64_PATH)) { - fprintf(stderr, "cannot create %s\n", AARCH64_PATH); - return 1; - } - snprintf(obj, sizeof(obj), "%s/bpf_interp.bpf.o", testdir); - if (run_case(obj, "bpf_interp", "test_bpf_interp", AARCH64_PATH, EXPECT) == 0) - printf("[+] case 1 passed\n"); - else { - printf("[-] case 1 FAILED\n"); - fail = 1; - } - unlink(AARCH64_PATH); - - /* Case 2: $ORIGIN-relative PT_INTERP -> co-located interpreter. */ - printf("[*] case 2: $ORIGIN interpreter resolved relative to the binary\n"); - mkdir(RELOC_DIR, 0755); - snprintf(appdst, sizeof(appdst), "%s/app", RELOC_DIR); - snprintf(interpdst, sizeof(interpdst), "%s/binfmt_bpf_interp", RELOC_DIR); - snprintf(src, sizeof(src), "%s/binfmt_bpf_app", testdir); - if (copy_file(src, appdst) || - copy_file(INTERP_PATH, interpdst)) { - fprintf(stderr, "cannot set up %s\n", RELOC_DIR); - fail = 1; - } else { - snprintf(obj, sizeof(obj), "%s/nix_origin.bpf.o", testdir); - if (run_case(obj, "nix_origin", "test_bpf_origin", appdst, EXPECT) == 0) - printf("[+] case 2 passed\n"); - else { - printf("[-] case 2 FAILED\n"); - fail = 1; - } - } - unlink(appdst); - unlink(interpdst); - rmdir(RELOC_DIR); - unlink(INTERP_PATH); - - if (!fail) - printf("[*] all binfmt_misc bpf cases passed\n"); - return fail; + ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_bpf_interp"), 0); + ASSERT_EQ(copy_file(src, INTERP_PATH), 0); } + +FIXTURE_TEARDOWN(bpf_handler) +{ + unlink(INTERP_PATH); +} + +/* The match program matches a synthetic header, the load program routes it. */ +TEST_F(bpf_handler, fixed_interpreter) +{ + ASSERT_EQ(create_fake_elf(AARCH64_PATH, EM_AARCH64), 0); + ASSERT_EQ(artifact_path(self->obj, sizeof(self->obj), + "bpf_interp.bpf.o"), 0); + EXPECT_EQ(run_case(self->obj, "bpf_interp", "test_bpf_interp", + AARCH64_PATH, EXPECT), 0); + unlink(AARCH64_PATH); +} + +/* A "$ORIGIN/..." PT_INTERP resolved to an interpreter next to the binary. */ +TEST_F(bpf_handler, origin_relative_interpreter) +{ + char src[PATH_MAX], app[PATH_MAX], interp[PATH_MAX]; + char dir[] = RELOC_TEMPLATE; + + ASSERT_NE(mkdtemp(dir), NULL); + snprintf(app, sizeof(app), "%s/app", dir); + snprintf(interp, sizeof(interp), "%s/binfmt_bpf_interp", dir); + ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_bpf_app"), 0); + ASSERT_EQ(copy_file(src, app), 0); + ASSERT_EQ(copy_file(INTERP_PATH, interp), 0); + + ASSERT_EQ(artifact_path(self->obj, sizeof(self->obj), + "nix_origin.bpf.o"), 0); + EXPECT_EQ(run_case(self->obj, "nix_origin", "test_bpf_origin", + app, EXPECT), 0); + + unlink(app); + unlink(interp); + rmdir(dir); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/exec/binfmt_misc_common.h b/tools/testing/selftests/exec/binfmt_misc_common.h new file mode 100644 index 000000000000..70ae66082e40 --- /dev/null +++ b/tools/testing/selftests/exec/binfmt_misc_common.h @@ -0,0 +1,100 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* Helpers shared by the binfmt_misc selftests. */ +#ifndef __SELFTESTS_EXEC_BINFMT_MISC_COMMON_H +#define __SELFTESTS_EXEC_BINFMT_MISC_COMMON_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define BINFMT_DIR "/proc/sys/fs/binfmt_misc" +#define BINFMT_REG BINFMT_DIR "/register" + +static inline int copy_file(const char *src, const char *dst) +{ + char buf[4096]; + int in, out; + ssize_t n; + + in = open(src, O_RDONLY); + if (in < 0) + return -1; + /* The tests share /tmp, so never write through a name they don't own. */ + unlink(dst); + out = open(dst, O_WRONLY | O_CREAT | O_EXCL, 0755); + if (out < 0) { + close(in); + return -1; + } + while ((n = read(in, buf, sizeof(buf))) > 0) { + if (write(out, buf, n) != n) { + close(in); + close(out); + return -1; + } + } + close(in); + close(out); + return n < 0 ? -1 : 0; +} + +/* Write @rule to the register file, preserving the write's errno. */ +static inline int write_reg(const char *rule) +{ + int fd, saved; + ssize_t n; + + fd = open(BINFMT_REG, O_WRONLY); + if (fd < 0) + return -1; + n = write(fd, rule, strlen(rule)); + saved = errno; + close(fd); + errno = saved; + return n < 0 ? -1 : 0; +} + +static inline void unregister(const char *name) +{ + char path[PATH_MAX]; + int fd; + + snprintf(path, sizeof(path), BINFMT_DIR "/%s", name); + fd = open(path, O_WRONLY); + if (fd >= 0) { + if (write(fd, "-1", 2) < 0) + ; /* best effort */ + close(fd); + } +} + +/* Mount binfmt_misc unless it already is, and report whether it is usable. */ +static inline bool binfmt_misc_available(void) +{ + if (access(BINFMT_REG, F_OK) < 0) + mount("binfmt_misc", BINFMT_DIR, "binfmt_misc", 0, NULL); + return access(BINFMT_REG, F_OK) == 0; +} + +/* Absolute path of @name in the directory this test was built into. */ +static inline int artifact_path(char *out, size_t sz, const char *name) +{ + char exe[PATH_MAX]; + ssize_t n; + + n = readlink("/proc/self/exe", exe, sizeof(exe) - 1); + if (n < 0) + return -1; + exe[n] = '\0'; + if ((size_t)snprintf(out, sz, "%s/%s", dirname(exe), name) >= sz) + return -1; + return 0; +} + +#endif /* __SELFTESTS_EXEC_BINFMT_MISC_COMMON_H */ diff --git a/tools/testing/selftests/exec/config b/tools/testing/selftests/exec/config index c308079867b3..2b1973e14291 100644 --- a/tools/testing/selftests/exec/config +++ b/tools/testing/selftests/exec/config @@ -1,2 +1,9 @@ CONFIG_BLK_DEV=y CONFIG_BLK_DEV_LOOP=y +CONFIG_BINFMT_MISC=y +CONFIG_BINFMT_MISC_BPF=y +CONFIG_BPF_JIT=y +CONFIG_BPF_SYSCALL=y +CONFIG_DEBUG_INFO=y +CONFIG_DEBUG_INFO_BTF=y +CONFIG_DEBUG_INFO_DWARF4=y From b0f09c07966b05a5d46830b4c2581a266c4a2baa Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 16:13:50 +0200 Subject: [PATCH 37/63] exec: add AT_FLAGS_TRANSPARENT_INTERP A transparent binfmt_misc dispatch hands the binary to the interpreter through AT_EXECFD and leaves the argument vector exactly as the caller built it. The loader on the receiving end has to know which contract it got. On the classic 'O'/'C' entries the binary's path is spliced into the argument vector and the loader consumes arguments. In transparent mode nothing was spliced and argv belongs entirely to the program. This cannot be inferred from AT_EXECFD alone. Raise a new AT_FLAGS bit following the AT_FLAGS_PRESERVE_ARGV0 precedent added for qemu-user in commit 2347961b11d4 ("binfmt_misc: pass binfmt_misc flags to the interpreter"). The bit also announces that mm->exe_file names the binary rather than the interpreter (added in the next commit). A loader that sees the bit may finish the identity polish by fixing up AT_PHDR/AT_ENTRY/AT_BASE in saved_auxv and fix the code/data markers via one uncapped PR_SET_MM_MAP once it has mapped the binary. I've got glibc patches for this as well but it's useful for any loader. BINPRM_FLAGS_TRANSPARENT_INTERP carries the mode from binfmt_misc to the ELF loaders. Both had their own copy of the AT_FLAGS translation, so give them one bprm_at_flags() to share instead of a second copy that can drift. Nothing sets the bprm flag yet. Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-8-e57866e4ae0f@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_elf.c | 5 +---- fs/binfmt_elf_fdpic.c | 5 +---- include/linux/binfmts.h | 22 ++++++++++++++++++++++ include/uapi/linux/binfmts.h | 7 +++++++ 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/fs/binfmt_elf.c b/fs/binfmt_elf.c index 16a56b6b3f6c..be8fd437b5a3 100644 --- a/fs/binfmt_elf.c +++ b/fs/binfmt_elf.c @@ -179,7 +179,6 @@ create_elf_tables(struct linux_binprm *bprm, const struct elfhdr *exec, unsigned char k_rand_bytes[16]; int items; elf_addr_t *elf_info; - elf_addr_t flags = 0; int ei_index; const struct cred *cred = current_cred(); struct vm_area_struct *vma; @@ -254,9 +253,7 @@ create_elf_tables(struct linux_binprm *bprm, const struct elfhdr *exec, NEW_AUX_ENT(AT_PHENT, sizeof(struct elf_phdr)); NEW_AUX_ENT(AT_PHNUM, exec->e_phnum); NEW_AUX_ENT(AT_BASE, interp_load_addr); - if (bprm->interp_flags & BINPRM_FLAGS_PRESERVE_ARGV0) - flags |= AT_FLAGS_PRESERVE_ARGV0; - NEW_AUX_ENT(AT_FLAGS, flags); + NEW_AUX_ENT(AT_FLAGS, bprm_at_flags(bprm)); NEW_AUX_ENT(AT_ENTRY, e_entry); NEW_AUX_ENT(AT_UID, from_kuid_munged(cred->user_ns, cred->uid)); NEW_AUX_ENT(AT_EUID, from_kuid_munged(cred->user_ns, cred->euid)); diff --git a/fs/binfmt_elf_fdpic.c b/fs/binfmt_elf_fdpic.c index fe0b5c5ed2bc..0a3cdf280307 100644 --- a/fs/binfmt_elf_fdpic.c +++ b/fs/binfmt_elf_fdpic.c @@ -509,7 +509,6 @@ static int create_elf_fdpic_tables(struct linux_binprm *bprm, char *k_platform, *k_base_platform; char __user *u_platform, *u_base_platform, *p; int loop; - unsigned long flags = 0; int ei_index; elf_addr_t *elf_info; @@ -649,9 +648,7 @@ static int create_elf_fdpic_tables(struct linux_binprm *bprm, NEW_AUX_ENT(AT_PHENT, sizeof(struct elf_phdr)); NEW_AUX_ENT(AT_PHNUM, exec_params->hdr.e_phnum); NEW_AUX_ENT(AT_BASE, interp_params->elfhdr_addr); - if (bprm->interp_flags & BINPRM_FLAGS_PRESERVE_ARGV0) - flags |= AT_FLAGS_PRESERVE_ARGV0; - NEW_AUX_ENT(AT_FLAGS, flags); + NEW_AUX_ENT(AT_FLAGS, bprm_at_flags(bprm)); NEW_AUX_ENT(AT_ENTRY, exec_params->entry_addr); NEW_AUX_ENT(AT_UID, (elf_addr_t) from_kuid_munged(cred->user_ns, cred->uid)); NEW_AUX_ENT(AT_EUID, (elf_addr_t) from_kuid_munged(cred->user_ns, cred->euid)); diff --git a/include/linux/binfmts.h b/include/linux/binfmts.h index 03e1794b5cbb..62465574e2a0 100644 --- a/include/linux/binfmts.h +++ b/include/linux/binfmts.h @@ -93,6 +93,28 @@ struct linux_binprm { #define BINPRM_FLAGS_PRESERVE_ARGV0_BIT 3 #define BINPRM_FLAGS_PRESERVE_ARGV0 (1 << BINPRM_FLAGS_PRESERVE_ARGV0_BIT) +/* binfmt_misc dispatched to the interpreter transparently */ +#define BINPRM_FLAGS_TRANSPARENT_INTERP_BIT 4 +#define BINPRM_FLAGS_TRANSPARENT_INTERP (1 << BINPRM_FLAGS_TRANSPARENT_INTERP_BIT) + +/** + * bprm_at_flags - the AT_FLAGS this invocation implies + * @bprm: binary that is being executed + * + * Tell the program on the receiving end which dispatch contract it got. + * + * Return: the AT_FLAGS value for this exec + */ +static inline unsigned long bprm_at_flags(const struct linux_binprm *bprm) +{ + /* Transparency preserves the whole argv, argv[0] included. */ + if (bprm->interp_flags & BINPRM_FLAGS_TRANSPARENT_INTERP) + return AT_FLAGS_TRANSPARENT_INTERP; + if (bprm->interp_flags & BINPRM_FLAGS_PRESERVE_ARGV0) + return AT_FLAGS_PRESERVE_ARGV0; + return 0; +} + /* * This structure defines the functions that are used to load the binary formats that * linux accepts. diff --git a/include/uapi/linux/binfmts.h b/include/uapi/linux/binfmts.h index c6f9450efc12..aafc07d78b80 100644 --- a/include/uapi/linux/binfmts.h +++ b/include/uapi/linux/binfmts.h @@ -22,4 +22,11 @@ struct pt_regs; #define AT_FLAGS_PRESERVE_ARGV0_BIT 0 #define AT_FLAGS_PRESERVE_ARGV0 (1 << AT_FLAGS_PRESERVE_ARGV0_BIT) +/* + * The interpreter runs transparently: the argument vector and the exe + * link belong to the binary passed in AT_EXECFD. + */ +#define AT_FLAGS_TRANSPARENT_INTERP_BIT 1 +#define AT_FLAGS_TRANSPARENT_INTERP (1 << AT_FLAGS_TRANSPARENT_INTERP_BIT) + #endif /* _UAPI_LINUX_BINFMTS_H */ From f1ec2b5604a7c5f239baf2acf894fef67b1dcc90 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 16:13:51 +0200 Subject: [PATCH 38/63] exec: label mm->exe_file with the binary for a transparent dispatch When binfmt_misc dispatches a binary to an interpreter, the interpreter becomes bprm->file and begin_new_exec() labels mm->exe_file with it. For wine or qemu-user that is the point. For the transparent mode it defeats the point. The interpreter is an implementation detail and the process's identity is the binary. Relocatable programs that locate themselves via /proc/self/exe find the dynamic linker instead [1]. Userspace cannot get this right on its own. PR_SET_MM_MAP's exe_fd is gated on checkpoint_restore_ns_capable() in the caller's own user namespace - that is how CRIU restores an exe link - so the ability to retarget mm->exe_file is not what this adds. What userspace cannot do is have the link be right from the first instruction. Credentials are unaffected either way: they still derive from the interpreter unless 'C' says otherwise. bprm->executable is the file execve() access-checked and kept open for AT_EXECFD. It is already the file would_dump() bases the dumpability decision on and the file bprm->execfd_creds derives credentials from. Label mm->exe_file with it when the dispatch is transparent and the identity is correct from the start. The label names precisely the file the caller passed to execve(). Write-denial moves along with the label. Rather than tracking per mode who still owes a release, the denial do_open_execat() took stays on bprm->executable until the file is handed over. begin_new_exec() drops it right before installing the descriptor - set_mm_exe_file() has taken its own denial on the identity file by then - and free_bprm() releases an unconsumed executable with do_close_execat() like the other exec files. For a transparent dispatch the result is exact parity with a direct execution: a concurrently written binary fails execve() with -ETXTBSY at open and a running one cannot be opened for writing. The interpreter consequently is not exe-pinned and matches the role it has in a native PT_INTERP exec. A classic execfd dispatch now keeps the binary write-denied until the exec completes rather than only until the interpreter swap; the difference is confined to the exec itself. Nothing sets BINPRM_FLAGS_TRANSPARENT_INTERP yet; the transparent dispatch machinery in binfmt_misc follows and raises it from birth, so the label and the aux vector bit that announces it appear together. Link: https://inbox.sourceware.org/libc-alpha/87ik6fymha.fsf@oldenburg.str.redhat.com [1] Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-9-e57866e4ae0f@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/exec.c | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/fs/exec.c b/fs/exec.c index 061e0f9fb4ef..128964d1e9d6 100644 --- a/fs/exec.c +++ b/fs/exec.c @@ -1101,6 +1101,17 @@ void __set_task_comm(struct task_struct *tsk, const char *buf, bool exec) perf_event_comm(tsk, exec); } +/* + * The file the process presents as: its exe link and comm. A transparent + * dispatch presents as the binary, which is bprm->executable. + */ +static struct file *bprm_identity_file(const struct linux_binprm *bprm) +{ + if (bprm->interp_flags & BINPRM_FLAGS_TRANSPARENT_INTERP) + return bprm->executable; + return bprm->file; +} + /* * Calling this is the point of no return. None of the failures will be * seen by userspace since either the process is already taking a fatal @@ -1151,7 +1162,7 @@ int begin_new_exec(struct linux_binprm * bprm) * not visible until then. Doing it here also ensures * we don't race against replace_mm_exe_file(). */ - retval = set_mm_exe_file(bprm->mm, bprm->file); + retval = set_mm_exe_file(bprm->mm, bprm_identity_file(bprm)); if (retval) goto out; @@ -1241,6 +1252,8 @@ int begin_new_exec(struct linux_binprm * bprm) * Let's fix it up to be something reasonable. */ if (bprm->comm_from_dentry) { + struct file *comm_file = bprm_identity_file(bprm); + /* * Hold RCU lock to keep the name from being freed behind our back. * Use acquire semantics to make sure the terminating NUL from @@ -1250,7 +1263,7 @@ int begin_new_exec(struct linux_binprm * bprm) * detecting a concurrent rename and just want a terminated name. */ rcu_read_lock(); - __set_task_comm(me, smp_load_acquire(&bprm->file->f_path.dentry->d_name.name), + __set_task_comm(me, smp_load_acquire(&comm_file->f_path.dentry->d_name.name), true); rcu_read_unlock(); } else { @@ -1291,10 +1304,17 @@ int begin_new_exec(struct linux_binprm * bprm) /* Pass the opened binary to the interpreter. */ if (bprm->have_execfd) { - retval = FD_ADD(0, bprm->executable); - if (retval < 0) - goto out_unlock; + struct file *executable = bprm->executable; + + /* mm->exe_file carries its own write denial now so drop it. */ + exe_file_allow_write_access(executable); bprm->executable = NULL; + retval = FD_ADD(0, executable); + if (retval < 0) { + /* The reference was not consumed. */ + fput(executable); + goto out_unlock; + } bprm->execfd = retval; } return 0; @@ -1413,8 +1433,7 @@ static void free_bprm(struct linux_binprm *bprm) if (bprm->old_mm) exec_mm_put_old(bprm->old_mm); do_close_execat(bprm->file); - if (bprm->executable) - fput(bprm->executable); + do_close_execat(bprm->executable); /* If a binfmt changed the interp, free it. */ if (bprm->interp != bprm->filename) kfree(bprm->interp); @@ -1740,8 +1759,7 @@ static int exec_binprm(struct linux_binprm *bprm) do_close_execat(exec); return -ENOEXEC; } - /* Only the reference is kept, for AT_EXECFD. */ - exe_file_allow_write_access(exec); + /* Kept for AT_EXECFD; the write denial rides along until hand-over. */ bprm->executable = exec; } else { do_close_execat(exec); From a4bdab2be4fdaf5f90a7fc445452167cb624cdf2 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 16:13:52 +0200 Subject: [PATCH 39/63] binfmt_misc: add transparent interpreter dispatch A binfmt_misc interpreter is visible to the binary it runs. argv[0] becomes the interpreter path and the binary's path is appended as an argument and /proc/pid/cmdline shows both. For wine or qemu-user that is the point. For a per-binary loader the interpreter is an implementation detail of running the binary that has no business in the argument vector. And a binary handed to execveat() as an O_CLOEXEC fd without a usable path cannot be run through binfmt_misc at all. The interpreter would have no path to open the binary by. Add the dispatch machinery for a transparent mode. The binary is handed to the interpreter through AT_EXECFD. The argument vector is left exactly as the caller set it. argv[0] and /proc/pid/cmdline look like a direct execution of the binary. bprm->interp still names the interpreter: it drives the next format lookup and the sched_prepare_exec tracepoint, not what the process sees. The interpreter loads the binary from AT_EXECFD for this. A relocatable loader can and glibc's ld.so is gaining AT_EXECFD support [1]. A staged interpreter argument is rejected: no argv slot is built for it to land in. The transparent branch raises BINPRM_FLAGS_TRANSPARENT_INTERP. A dispatch through it labels mm->exe_file with the binary and raises AT_FLAGS_TRANSPARENT_INTERP next to AT_EXECFD. The aux vector bit is the loader's hint to retarget saved_auxv and the statistics markers to the binary, which is only correct while the exe link names the binary too. The inaccessible-path bail moves after handler selection and into the path-building branch. A transparent interpreter takes the binary from AT_EXECFD instead of a path, so the restriction does not apply to it and the O_CLOEXEC execveat() case above can work. Nothing can take the transparent branch yet. Link: https://inbox.sourceware.org/libc-alpha/20260717-work-glibc-binfmt_misc-v3-0-45129bfb13fe@kernel.org [1] Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-10-e57866e4ae0f@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index a47a0a677e93..c49e88283f12 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -50,6 +50,7 @@ enum binfmt_misc_entry_flags { MISC_FMT_OPEN_BINARY = (1U << 30), MISC_FMT_CREDENTIALS = (1U << 29), MISC_FMT_OPEN_FILE = (1U << 28), + MISC_FMT_TRANSPARENT = (1U << 27), }; /** @@ -400,6 +401,10 @@ static int build_interp_argv(struct linux_binprm *bprm, const char *interpreter, { int retval; + /* The interpreter has to be able to load the binary by path. */ + if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE) + return -ENOENT; + /* The entry's own choice - not one accumulated from an earlier level. */ if (flags & MISC_FMT_PRESERVE_ARGV0) { bprm->interp_flags |= BINPRM_FLAGS_PRESERVE_ARGV0; @@ -458,21 +463,23 @@ static int load_misc_binary(struct linux_binprm *bprm) if (!fmt) return -ENOEXEC; - /* Need to be able to load the file after exec */ - if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE) - return -ENOENT; - interpreter = entry_select_interpreter(fmt, bprm); if (IS_ERR(interpreter)) return PTR_ERR(interpreter); flags = entry_invocation_flags(fmt, bprm); - retval = build_interp_argv(bprm, interpreter, flags); - if (retval) - return retval; + /* No argv is built for a staged argument to land in. */ + if ((flags & MISC_FMT_TRANSPARENT) && bprm->bpf_interp_arg) + return -EINVAL; - /* Update interp in case binfmt_script needs it. */ + if (!(flags & MISC_FMT_TRANSPARENT)) { + retval = build_interp_argv(bprm, interpreter, flags); + if (retval) + return retval; + } + + /* Update interp for the next round; sched_prepare_exec reports it. */ retval = bprm_change_interp(interpreter, bprm); if (retval < 0) return retval; @@ -481,6 +488,10 @@ static int load_misc_binary(struct linux_binprm *bprm) if (IS_ERR(interp_file)) return PTR_ERR(interp_file); + /* Raise only past the last failure, or an -ENOEXEC decline leaks it. */ + if (flags & MISC_FMT_TRANSPARENT) + bprm->interp_flags |= BINPRM_FLAGS_TRANSPARENT_INTERP; + bprm->interpreter = interp_file; if (flags & MISC_FMT_OPEN_BINARY) bprm->have_execfd = 1; From 75e536852f9a5f1880091d58f46cdf2fce2101b4 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 16:13:53 +0200 Subject: [PATCH 40/63] binfmt_misc: add a static transparent flag 'T' Let a registration opt into transparent dispatch. The 'T' flag lets a matched binary keep its argument vector and is sent to the interpreter through AT_EXECFD. The process's identity is the binary's. 'T' implies 'O' exactly like 'C' does. 'P' is rejected in combination with it. Transparency preserves the whole argument vector so there is nothing left for 'P' to say. 'C' remains an independent choice and 'F' keeps working. A pre-opened interpreter is orthogonal to how the binary is handed over. Like the other flag characters 'T' cannot be used as the field delimiter. The flag scan would run off the registration buffer. Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-11-e57866e4ae0f@kernel.org Signed-off-by: Christian Brauner (Amutable) --- Documentation/admin-guide/binfmt-misc.rst | 10 ++++++++++ fs/binfmt_misc.c | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst index 9f0d9132723f..62088468350b 100644 --- a/Documentation/admin-guide/binfmt-misc.rst +++ b/Documentation/admin-guide/binfmt-misc.rst @@ -90,6 +90,16 @@ Here is what the fields mean: emulation is installed and uses the opened image to spawn the emulator, meaning it is always available once installed, regardless of how the environment changes. + ``T`` - transparent + Run the interpreter transparently. The binary is handed to + the interpreter through ``AT_EXECFD`` (``T`` implies ``O``), + the argument vector is left exactly as the caller built it + and the kernel labels ``/proc/pid/exe`` with the binary + instead of the interpreter. The interpreter has to load the + binary from ``AT_EXECFD`` and follow the + ``AT_FLAGS_TRANSPARENT_INTERP`` contract. Combining ``T`` + with ``P`` is rejected: transparency preserves the whole + argument vector, argv[0] included. There are some restrictions: diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index c49e88283f12..d32ef07c810f 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -72,6 +72,7 @@ static const struct binfmt_misc_flag misc_flags[] = { { 'O', MISC_FMT_OPEN_BINARY, 0, "open binary" }, { 'C', MISC_FMT_CREDENTIALS, MISC_FMT_OPEN_BINARY, "credentials from the binary" }, { 'F', MISC_FMT_OPEN_FILE, 0, "open interpreter file now" }, + { 'T', MISC_FMT_TRANSPARENT, MISC_FMT_OPEN_BINARY, "transparent" }, }; /* Look up a flag character, NULL if @c is not one. */ @@ -764,6 +765,11 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, if (test_bit(MISC_FMT_BPF_BIT, &e->flags) && p != flags) return ERR_PTR(-EINVAL); + /* Transparency preserves the whole argv, argv[0] included. */ + if ((e->flags & MISC_FMT_TRANSPARENT) && + (e->flags & MISC_FMT_PRESERVE_ARGV0)) + return ERR_PTR(-EINVAL); + if (*p == '\n') p++; if (p != buf + count) From 21e04378e0b1b2a9bdf34f2458d4950716b14b2e Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 16:13:54 +0200 Subject: [PATCH 41/63] binfmt_misc: let a bpf handler run the interpreter transparently Expose transparent mode 'T' to the bpf handler via a new BPF_BINPRM_TRANSPARENT flag. A bpf handler can decide per binary whether the dispatch is transparent. This way users may choose a native-looking loader for one binary and a visible wrapper invocation for the next. Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-12-e57866e4ae0f@kernel.org Signed-off-by: Christian Brauner (Amutable) --- Documentation/admin-guide/binfmt-misc.rst | 15 +++++++++++++-- fs/binfmt_misc.c | 2 ++ fs/binfmt_misc_bpf.c | 17 ++++++++++++----- include/linux/binfmt_misc.h | 4 ++++ 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst index 62088468350b..4547ebdfcaa5 100644 --- a/Documentation/admin-guide/binfmt-misc.rst +++ b/Documentation/admin-guide/binfmt-misc.rst @@ -189,8 +189,8 @@ interpreter and the binary, exactly like the optional argument of a ``#!`` interpreter line, e.g. for a handler that resolves ``$ORIGIN`` in a script's ``#!`` path and needs to preserve the argument that followed it. -The invocation flags a static entry fixes at registration - ``P``, ``C`` -and ``O`` - are per-exec choices for a bpf handler, made by the ``load`` +The invocation flags a static entry fixes at registration - ``P``, ``C``, +``O`` and ``T`` - are per-exec choices for a bpf handler, made by the ``load`` program with the ``bpf_binprm_set_flags()`` kfunc, so a single handler can decide them differently for each binary it handles: @@ -202,6 +202,17 @@ decide them differently for each binary it handles: - ``BPF_BINPRM_EXECFD`` opens the binary on the interpreter's behalf and passes it through the ``AT_EXECFD`` aux vector entry (the ``O`` flag), so the interpreter can run binaries it could not open by path. +- ``BPF_BINPRM_TRANSPARENT`` runs the interpreter transparently (the ``T`` + flag): the binary is handed over through ``AT_EXECFD`` as + with ``BPF_BINPRM_EXECFD``, but the argument vector is also left as the + caller passed it. An interpreter that loads the binary from ``AT_EXECFD`` + then appears in ``argv[0]`` and ``/proc/pid/cmdline`` as a direct + execution of the binary. ``BPF_BINPRM_PRESERVE_ARGV0`` and a staged + interpreter argument are rejected in combination with it, just as ``P`` + is with ``T``. It also lets a handler + run a binary passed as an inaccessible ``O_CLOEXEC`` file descriptor to + ``execveat()``, which a path-splicing dispatch cannot: the interpreter + has no path by which to open it. Because these are program choices, a ``B`` entry carries no flags in the register string; ``F`` (pre-open a fixed interpreter) has no meaning for it. diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index d32ef07c810f..98f9208e8188 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -351,6 +351,8 @@ static unsigned long entry_invocation_flags(const struct binfmt_misc_entry *e, flags |= MISC_FMT_OPEN_BINARY; if (bpf_flags & BPF_BINPRM_CREDENTIALS) flags |= MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_BINARY; + if (bpf_flags & BPF_BINPRM_TRANSPARENT) + flags |= MISC_FMT_TRANSPARENT | MISC_FMT_OPEN_BINARY; return flags; } diff --git a/fs/binfmt_misc_bpf.c b/fs/binfmt_misc_bpf.c index e3dcf8330df0..d279ffa9c4ad 100644 --- a/fs/binfmt_misc_bpf.c +++ b/fs/binfmt_misc_bpf.c @@ -171,19 +171,26 @@ __bpf_kfunc int bpf_binprm_set_interp_arg(struct linux_binprm *bprm, * @flags: an OR of enum bpf_binprm_flags values * * To be called from the load program of a struct binfmt_misc_ops handler. It - * decides per exec what a static entry fixes at registration with the P, C and - * O flags: BPF_BINPRM_PRESERVE_ARGV0 keeps the caller's argv[0], + * decides per exec what a static entry fixes at registration with the P, C, O + * and T flags: BPF_BINPRM_PRESERVE_ARGV0 keeps the caller's argv[0], * BPF_BINPRM_CREDENTIALS computes credentials from the binary, and * BPF_BINPRM_EXECFD hands the binary to the interpreter through AT_EXECFD. - * Calling it again replaces the flags, passing zero clears them again. + * BPF_BINPRM_TRANSPARENT additionally leaves the argument vector untouched, + * making the exec look like a direct execution of the binary. Calling it + * again replaces the flags, passing zero clears them again. * - * Return: 0 on success, -EINVAL if @flags contains an unknown bit + * Return: 0 on success, -EINVAL if @flags contains an unknown bit or an + * invalid combination */ __bpf_kfunc int bpf_binprm_set_flags(struct linux_binprm *bprm, enum bpf_binprm_flags flags) { if (flags & ~(BPF_BINPRM_PRESERVE_ARGV0 | BPF_BINPRM_CREDENTIALS | - BPF_BINPRM_EXECFD)) + BPF_BINPRM_EXECFD | BPF_BINPRM_TRANSPARENT)) + return -EINVAL; + + /* Transparency preserves the whole argv, argv[0] included. */ + if ((flags & BPF_BINPRM_TRANSPARENT) && (flags & BPF_BINPRM_PRESERVE_ARGV0)) return -EINVAL; bprm->bpf_flags = flags; diff --git a/include/linux/binfmt_misc.h b/include/linux/binfmt_misc.h index d3112a00cc19..26da749391b4 100644 --- a/include/linux/binfmt_misc.h +++ b/include/linux/binfmt_misc.h @@ -16,6 +16,9 @@ struct user_namespace; * @BPF_BINPRM_CREDENTIALS: compute credentials from the binary; implies execfd * (like the 'C' flag) * @BPF_BINPRM_EXECFD: pass the binary via AT_EXECFD (like the 'O' flag) + * @BPF_BINPRM_TRANSPARENT: leave argv untouched, the interpreter takes the + * binary from AT_EXECFD (like the 'T' flag); implies + * execfd, excludes preserve-argv0 * * Set from a load program with bpf_binprm_set_flags(). Unlike a static entry, * a bpf handler chooses these per exec rather than once at registration. @@ -24,6 +27,7 @@ enum bpf_binprm_flags { BPF_BINPRM_PRESERVE_ARGV0 = (1ULL << 0), BPF_BINPRM_CREDENTIALS = (1ULL << 1), BPF_BINPRM_EXECFD = (1ULL << 2), + BPF_BINPRM_TRANSPARENT = (1ULL << 3), }; /** From 7baee96f8356fbd01db1dc2641c13104413f6434 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 16:13:55 +0200 Subject: [PATCH 42/63] selftests/exec: test the transparent binfmt_misc mode Verify the identity a transparent dispatch constructs, from both activation paths. - binfmt_misc_transparent: registers a magic entry with the static 'T' flag and execs a matched binary with arguments. - binfmt_misc_bpf: a handler whose load program sets BPF_BINPRM_TRANSPARENT. Both dispatch to a shared asserting interpreter that runs in place of the binary and checks the contract from the inside: - AT_FLAGS carries AT_FLAGS_TRANSPARENT_INTERP - AT_EXECFD refers to the very inode of the binary - /proc/self/exe resolves to the binary - argv and /proc/self/cmdline are exactly what the caller passed with nothing spliced in - comm is the binary's basename - the binary is write-denied while it runs The static test also validates the registration. 'T' combined with 'P' must be rejected. A kernel that does not know 'T' turns the test into a skip. The asserting interpreter and the static test build without the bpf toolchain so the core transparent semantics stay covered on systems where the bpf cases are skipped. The flag support probe, the canonical payload argv with the run_payload() helper that execs it, and the identity assertions (exe link, comm, write denial) live in binfmt_misc_common.h; the loader substitution test reuses all of them. Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-13-e57866e4ae0f@kernel.org Signed-off-by: Christian Brauner (Amutable) --- tools/testing/selftests/exec/.gitignore | 2 + tools/testing/selftests/exec/Makefile | 7 +- .../testing/selftests/exec/binfmt_misc_bpf.c | 37 +++++- .../selftests/exec/binfmt_misc_common.h | 93 +++++++++++++++ .../selftests/exec/binfmt_misc_transparent.c | 95 +++++++++++++++ .../exec/binfmt_transparent_interp.c | 112 ++++++++++++++++++ .../testing/selftests/exec/transparent.bpf.c | 57 +++++++++ 7 files changed, 399 insertions(+), 4 deletions(-) create mode 100644 tools/testing/selftests/exec/binfmt_misc_transparent.c create mode 100644 tools/testing/selftests/exec/binfmt_transparent_interp.c create mode 100644 tools/testing/selftests/exec/transparent.bpf.c diff --git a/tools/testing/selftests/exec/.gitignore b/tools/testing/selftests/exec/.gitignore index 8b93b405c424..94b9ab4eb46c 100644 --- a/tools/testing/selftests/exec/.gitignore +++ b/tools/testing/selftests/exec/.gitignore @@ -22,5 +22,7 @@ S_I*.test binfmt_misc_bpf binfmt_bpf_interp binfmt_bpf_app +binfmt_misc_transparent +binfmt_transparent_interp *.bpf.o vmlinux.h diff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile index d2a5a58f9432..978b8bb572fe 100644 --- a/tools/testing/selftests/exec/Makefile +++ b/tools/testing/selftests/exec/Makefile @@ -21,6 +21,11 @@ TEST_GEN_PROGS += recursion-depth TEST_GEN_PROGS += null-argv TEST_GEN_PROGS += check-exec +# Static ('T' flag) transparent binfmt_misc test; the asserting interpreter +# is shared with the bpf harness's transparent case. No bpf toolchain needed. +TEST_GEN_PROGS += binfmt_misc_transparent +TEST_GEN_FILES += binfmt_transparent_interp + # binfmt_misc bpf-backed ('B') handler test: a libbpf harness plus its # struct_ops objects and the test interpreter/app it routes between. Only # built when clang, bpftool, the vmlinux BTF and libbpf are all present @@ -35,7 +40,7 @@ HAVE_BPF_TOOLCHAIN ?= $(shell command -v $(CLANG) >/dev/null 2>&1 && \ pkg-config --exists libbpf 2>/dev/null && echo y) ifeq ($(HAVE_BPF_TOOLCHAIN),y) TEST_GEN_PROGS += binfmt_misc_bpf -TEST_GEN_FILES += bpf_interp.bpf.o nix_origin.bpf.o +TEST_GEN_FILES += bpf_interp.bpf.o nix_origin.bpf.o transparent.bpf.o TEST_GEN_FILES += binfmt_bpf_interp binfmt_bpf_app else $(info exec selftests: skipping binfmt_misc_bpf, needs clang, bpftool, vmlinux BTF and libbpf) diff --git a/tools/testing/selftests/exec/binfmt_misc_bpf.c b/tools/testing/selftests/exec/binfmt_misc_bpf.c index c41fb80f2a72..31bc7dded585 100644 --- a/tools/testing/selftests/exec/binfmt_misc_bpf.c +++ b/tools/testing/selftests/exec/binfmt_misc_bpf.c @@ -9,7 +9,7 @@ * * echo ':name:B:::::' > /proc/sys/fs/binfmt_misc/register * - * Two self-contained cases are exercised: + * Three self-contained cases are exercised: * * 1. bpf_interp: the match program matches a synthetic aarch64 ELF header * from the prefetched bprm->buf and the load program routes it to a @@ -18,9 +18,13 @@ * commit only to a "$ORIGIN/..."-relative PT_INTERP and the load program * resolves it to an interpreter co-located with the binary (the * relocatable-loader case the kernel ELF loader cannot express). + * 3. transparent: the load program sets BPF_BINPRM_TRANSPARENT; the + * asserting interpreter (binfmt_transparent_interp) verifies the + * identity the kernel constructed (exe link, argv, cmdline, comm, + * AT_EXECFD, write denial) from inside the process. * - * Both route to a test interpreter that prints BPF_INTERP_RAN, proving the - * program's chosen interpreter actually ran. + * The first two route to a test interpreter that prints BPF_INTERP_RAN, + * proving the program's chosen interpreter actually ran. */ #define _GNU_SOURCE #include @@ -40,7 +44,10 @@ #define INTERP_PATH "/tmp/binfmt_bpf_interp" #define AARCH64_PATH "/tmp/binfmt_bpf_aarch64" #define RELOC_TEMPLATE "/tmp/binfmt_relocXXXXXX" +#define TRANS_INTERP "/tmp/binfmt_transparent_interp" +#define TRANS_PATH "/tmp/binfmt_bpf_riscv" #define EXPECT "BPF_INTERP_RAN" +#define TRANS_EXPECT "TRANSPARENT_OK" /* A minimal 64-bit little-endian ELF header, padded to the read size. */ static int create_fake_elf(const char *path, unsigned short machine) @@ -208,4 +215,28 @@ TEST_F(bpf_handler, origin_relative_interpreter) rmdir(dir); } +/* A transparent dispatch: the process presents as the binary, not the interp. */ +TEST_F(bpf_handler, transparent_dispatch) +{ + char src[PATH_MAX], cmd[PATH_MAX + 16]; + + /* Probe for transparent-mode support via its static counterpart. */ + if (binfmt_flag_supported('T')) + SKIP(return, "kernel without transparent mode"); + + ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_transparent_interp"), 0); + ASSERT_EQ(copy_file(src, TRANS_INTERP), 0); + ASSERT_EQ(create_fake_elf(TRANS_PATH, EM_RISCV), 0); + + setenv("BINFMT_TEST_BINARY", TRANS_PATH, 1); + snprintf(cmd, sizeof(cmd), "%s argone argtwo", TRANS_PATH); + ASSERT_EQ(artifact_path(self->obj, sizeof(self->obj), + "transparent.bpf.o"), 0); + EXPECT_EQ(run_case(self->obj, "transparent", "test_bpf_transparent", + cmd, TRANS_EXPECT), 0); + + unlink(TRANS_PATH); + unlink(TRANS_INTERP); +} + TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/exec/binfmt_misc_common.h b/tools/testing/selftests/exec/binfmt_misc_common.h index 70ae66082e40..0bd37e92421b 100644 --- a/tools/testing/selftests/exec/binfmt_misc_common.h +++ b/tools/testing/selftests/exec/binfmt_misc_common.h @@ -9,13 +9,27 @@ #include #include #include +#include #include #include +#include +#include #include #define BINFMT_DIR "/proc/sys/fs/binfmt_misc" #define BINFMT_REG BINFMT_DIR "/register" +/* comm holds 15 usable chars; a read of /proc/self/comm appends a newline. */ +#define TASK_COMM_LEN 16 + +/* The canonical payload argv: run_payload() passes it, the payloads assert it. */ +#define PAYLOAD_ARGV0 "payload-argv0" +#define PAYLOAD_ARG1 "argone" +#define PAYLOAD_ARG2 "argtwo" + +/* Exit status run_payload() reports when the exec was refused as unhandled. */ +#define RUN_ENOEXEC 42 + static inline int copy_file(const char *src, const char *dst) { char buf[4096]; @@ -97,4 +111,83 @@ static inline int artifact_path(char *out, size_t sz, const char *name) return 0; } +/* Probe kernel support for a registration flag with a throwaway entry. */ +static inline int binfmt_flag_supported(char flag) +{ + char rule[64]; + + snprintf(rule, sizeof(rule), ":bm_flag_probe:E::bmprobe::/bin/true:%c", + flag); + if (write_reg(rule)) + return -1; + unregister("bm_flag_probe"); + return 0; +} + +/* + * Run @path with the canonical payload argv and return its exit status, or + * RUN_ENOEXEC when the exec itself was refused as unhandled. + */ +static inline int run_payload(const char *path) +{ + int status; + pid_t pid; + + pid = fork(); + if (pid == 0) { + execl(path, PAYLOAD_ARGV0, PAYLOAD_ARG1, PAYLOAD_ARG2, + (char *)NULL); + _exit(errno == ENOEXEC ? RUN_ENOEXEC : 126); + } + if (pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status)) + return -1; + return WEXITSTATUS(status); +} + +/* Does the exe link name @path? */ +static inline bool exe_is(const char *path) +{ + char exe[PATH_MAX], real[PATH_MAX]; + ssize_t n; + + n = readlink("/proc/self/exe", exe, sizeof(exe) - 1); + if (n <= 0 || !realpath(path, real)) + return false; + exe[n] = '\0'; + return !strcmp(exe, real); +} + +/* Is comm @name truncated to what a comm can hold? */ +static inline bool comm_is(const char *name) +{ + char comm[TASK_COMM_LEN + 2], expect[TASK_COMM_LEN]; + ssize_t n; + int fd; + + fd = open("/proc/self/comm", O_RDONLY); + if (fd < 0) + return false; + n = read(fd, comm, sizeof(comm) - 1); + close(fd); + if (n <= 0) + return false; + if (comm[n - 1] == '\n') + n--; + comm[n] = '\0'; + snprintf(expect, sizeof(expect), "%s", name); + return !strcmp(comm, expect); +} + +/* Opening @path for writing has to fail with ETXTBSY. */ +static inline bool write_denied(const char *path) +{ + int fd = open(path, O_WRONLY); + + if (fd >= 0) { + close(fd); + return false; + } + return errno == ETXTBSY; +} + #endif /* __SELFTESTS_EXEC_BINFMT_MISC_COMMON_H */ diff --git a/tools/testing/selftests/exec/binfmt_misc_transparent.c b/tools/testing/selftests/exec/binfmt_misc_transparent.c new file mode 100644 index 000000000000..d0cb845df1d3 --- /dev/null +++ b/tools/testing/selftests/exec/binfmt_misc_transparent.c @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Test the static transparent flag 'T' of binfmt_misc. A magic-matched + * binary is dispatched to an interpreter with the argument vector left + * untouched, the binary passed through AT_EXECFD and mm->exe_file labeled + * with the binary. The asserting interpreter (binfmt_transparent_interp) + * verifies the constructed identity from inside the process and exits 0. + * + * Needs root for the registration; no bpf toolchain involved. + */ +#define _GNU_SOURCE +#include +#include + +#include "binfmt_misc_common.h" +#include "kselftest_harness.h" + +#define MAGIC "#TRANSPARENT-SELFTEST#" +#define TARGET_PATH "/tmp/binfmt_transparent_target" +#define INTERP_PATH "/tmp/binfmt_transparent_interp" +#define ENTRY "test_transparent" +#define RULE(flags) ":" ENTRY ":M:0:" MAGIC "::" INTERP_PATH ":" flags + +/* The target only has to carry the magic; it is never actually loaded. */ +static int create_target(void) +{ + char buf[128] = MAGIC "\n"; + int fd; + + unlink(TARGET_PATH); + fd = open(TARGET_PATH, O_WRONLY | O_CREAT | O_EXCL, 0755); + if (fd < 0) + return -1; + if (write(fd, buf, sizeof(buf)) != (ssize_t)sizeof(buf)) { + close(fd); + return -1; + } + close(fd); + return 0; +} + +FIXTURE(transparent) { +}; + +FIXTURE_SETUP(transparent) +{ + char src[PATH_MAX]; + + if (getuid() != 0) + SKIP(return, "test must be run as root"); + if (!binfmt_misc_available()) + SKIP(return, "no binfmt_misc"); + + ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_transparent_interp"), 0); + ASSERT_EQ(copy_file(src, INTERP_PATH), 0); + ASSERT_EQ(create_target(), 0); + + /* Skip the whole suite on a kernel that does not know 'T'. */ + if (binfmt_flag_supported('T')) { + ASSERT_EQ(errno, EINVAL); + SKIP(return, "kernel without the 'T' flag"); + } +} + +FIXTURE_TEARDOWN(transparent) +{ + unregister(ENTRY); + unlink(TARGET_PATH); + unlink(INTERP_PATH); +} + +/* Grammar sanity check: the same entry without 'T' has to register. */ +TEST_F(transparent, plain_entry_registers) +{ + ASSERT_EQ(write_reg(RULE("")), 0); +} + +/* 'T' preserves the whole argv, so combining it with 'P' is rejected. */ +TEST_F(transparent, rejects_preserve_argv0) +{ + ASSERT_NE(write_reg(RULE("TP")), 0); + EXPECT_EQ(errno, EINVAL); +} + +/* The interpreter asserts the identity the kernel built for it. */ +TEST_F(transparent, dispatch) +{ + ASSERT_EQ(write_reg(RULE("T")), 0); + + setenv("BINFMT_TEST_BINARY", TARGET_PATH, 1); + setenv("BINFMT_TEST_ARGV0", PAYLOAD_ARGV0, 1); + EXPECT_EQ(run_payload(TARGET_PATH), 0); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/exec/binfmt_transparent_interp.c b/tools/testing/selftests/exec/binfmt_transparent_interp.c new file mode 100644 index 000000000000..d4c4a538c9aa --- /dev/null +++ b/tools/testing/selftests/exec/binfmt_transparent_interp.c @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Asserting interpreter for the transparent binfmt_misc mode. It runs in + * place of the dispatched binary and verifies the identity the kernel + * constructed: the aux vector contract, the exe link, argv, cmdline, comm + * and the write denial on the binary. BINFMT_TEST_BINARY names the binary; + * the harness execs it with the arguments "argone argtwo". Prints + * TRANSPARENT_OK and exits 0 when every check holds. + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "binfmt_misc_common.h" +#include "kselftest.h" + +#ifndef AT_FLAGS_TRANSPARENT_INTERP +#define AT_FLAGS_TRANSPARENT_INTERP (1 << 1) +#endif + +static int fail; + +static void ok(int cond, const char *what) +{ + if (!cond) { + fprintf(stderr, "TRANSPARENT_FAIL: %s (errno %d)\n", what, errno); + fail = 1; + } +} + +int main(int argc, char **argv) +{ + const char *binary = getenv("BINFMT_TEST_BINARY"); + const char *argv0 = getenv("BINFMT_TEST_ARGV0"); + char expect[PATH_MAX + 32], buf[PATH_MAX]; + unsigned long execfd; + struct stat stb, stfd; + const char *want[3]; + const char *base; + size_t expect_len, i; + int fd, have_stb, have_stfd; + ssize_t n; + + if (!binary) { + fprintf(stderr, "TRANSPARENT_FAIL: BINFMT_TEST_BINARY unset\n"); + return 1; + } + /* Distinct from the binary path, so a classic argv splice is caught. */ + want[0] = argv0 ? argv0 : binary; + want[1] = PAYLOAD_ARG1; + want[2] = PAYLOAD_ARG2; + + /* The aux vector announces the transparent contract. */ + ok(getauxval(AT_FLAGS) & AT_FLAGS_TRANSPARENT_INTERP, + "AT_FLAGS lacks AT_FLAGS_TRANSPARENT_INTERP"); + + /* AT_EXECFD refers to the very file that was executed. */ + execfd = getauxval(AT_EXECFD); + ok(execfd > 2, "no AT_EXECFD"); + have_stb = !stat(binary, &stb); + ok(have_stb, "cannot stat the binary"); + have_stfd = !fstat((int)execfd, &stfd); + ok(have_stfd, "cannot fstat AT_EXECFD"); + ok(have_stb && have_stfd && stb.st_dev == stfd.st_dev && + stb.st_ino == stfd.st_ino, "AT_EXECFD is not the binary"); + + /* The exe link names the binary, not this interpreter. */ + ok(exe_is(binary), "/proc/self/exe is not the binary"); + + /* argv arrived unspliced. */ + ok(argc == (int)ARRAY_SIZE(want), "argv was rewritten"); + for (i = 0; i < ARRAY_SIZE(want) && i < (size_t)argc; i++) + ok(!strcmp(argv[i], want[i]), "argv was rewritten"); + + /* And so did the kernel's copy of it: the same strings, NUL separated. */ + for (i = 0, expect_len = 0; i < ARRAY_SIZE(want); i++) { + size_t len = strlen(want[i]) + 1; + + if (expect_len + len > sizeof(expect)) { + ok(0, "argv does not fit the expectation buffer"); + break; + } + memcpy(expect + expect_len, want[i], len); + expect_len += len; + } + fd = open("/proc/self/cmdline", O_RDONLY); + n = fd >= 0 ? read(fd, buf, sizeof(buf)) : -1; + if (fd >= 0) + close(fd); + ok(n == (ssize_t)expect_len && !memcmp(buf, expect, expect_len), + "/proc/self/cmdline was rewritten"); + + /* comm is the binary's basename. */ + base = strrchr(binary, '/'); + base = base ? base + 1 : binary; + ok(comm_is(base), "comm is not the binary's basename"); + + /* The binary is write-denied while it runs, like a direct exec. */ + ok(write_denied(binary), "binary is writable while running"); + ok(write_denied("/proc/self/exe"), "exe link is writable while running"); + + if (!fail) + printf("TRANSPARENT_OK\n"); + return fail; +} diff --git a/tools/testing/selftests/exec/transparent.bpf.c b/tools/testing/selftests/exec/transparent.bpf.c new file mode 100644 index 000000000000..7632019ebe69 --- /dev/null +++ b/tools/testing/selftests/exec/transparent.bpf.c @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * binfmt_misc_ops handler for the transparent-mode case: match a synthetic + * riscv ELF header and run the asserting interpreter transparently - the + * argument vector untouched, the binary in AT_EXECFD and mm->exe_file + * labeled with the binary. + */ +#include "vmlinux.h" +#include +#include + +char _license[] SEC("license") = "GPL"; + +#define EI_CLASS 4 +#define ELFCLASS64 2 +#define EM_RISCV 243 + +extern int bpf_binprm_set_interp(struct linux_binprm *bprm, const char *path, + size_t path__sz) __ksym; +extern int bpf_binprm_set_flags(struct linux_binprm *bprm, + enum bpf_binprm_flags flags) __ksym; + +SEC("struct_ops.s/match") +bool BPF_PROG(transparent_match, struct linux_binprm *bprm) +{ + __u16 machine; + + if (bprm->buf[0] != 0x7f || bprm->buf[1] != 'E' || + bprm->buf[2] != 'L' || bprm->buf[3] != 'F' || + bprm->buf[EI_CLASS] != ELFCLASS64) + return false; + + /* e_machine is a 16-bit little-endian field at offset 18. */ + machine = (__u8)bprm->buf[18] | ((__u16)(__u8)bprm->buf[19] << 8); + return machine == EM_RISCV; +} + +SEC("struct_ops.s/load") +int BPF_PROG(transparent_load, struct linux_binprm *bprm) +{ + char interp[] = "/tmp/binfmt_transparent_interp"; + int err; + + err = bpf_binprm_set_flags(bprm, BPF_BINPRM_TRANSPARENT); + if (err) + return err; + + /* @path__sz includes the terminating NUL; 0 commits the selection. */ + return bpf_binprm_set_interp(bprm, interp, sizeof(interp)); +} + +SEC(".struct_ops.link") +struct binfmt_misc_ops transparent = { + .match = (void *)transparent_match, + .load = (void *)transparent_load, + .name = "transparent", +}; From 5fa1e68f9978708db43aa82f70c92fe992419bb0 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 16:13:56 +0200 Subject: [PATCH 43/63] binfmt_misc: document the transparent identity contract Describe what a transparent dispatch constructs and the loader contract behind AT_FLAGS_TRANSPARENT_INTERP. Also note what deliberately stays different (the address space layout) and what stays unchanged (credential derivation without 'C'). Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-14-e57866e4ae0f@kernel.org Signed-off-by: Christian Brauner (Amutable) --- Documentation/admin-guide/binfmt-misc.rst | 26 +++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst index 4547ebdfcaa5..d46130be0891 100644 --- a/Documentation/admin-guide/binfmt-misc.rst +++ b/Documentation/admin-guide/binfmt-misc.rst @@ -224,6 +224,32 @@ The entry keeps the handler alive; deleting the struct_ops map only prevents new activations. +Transparent interpreters +------------------------ + +With the ``T`` flag or ``BPF_BINPRM_TRANSPARENT`` the dispatch is invisible +to the resulting process. The argument vector is left exactly as the caller +built it. The binary is passed through ``AT_EXECFD``. The kernel also labels +``/proc/pid/exe`` correctly. The binary's file is write-denied while the +process runs and the interpreter's is not, exactly as if the binary had been +executed directly. A transparent entry does not change how credentials are +derived. As +with any other entry, set*id bits of the binary are only honored with ``C`` (or +``BPF_BINPRM_CREDENTIALS``). + +The interpreter has to be built for this contract. The kernel announces it +with ``AT_FLAGS_TRANSPARENT_INTERP`` in the ``AT_FLAGS`` aux vector entry +next to ``AT_EXECFD``. The argument vector belongs entirely to the program, +nothing was spliced in, so the interpreter doesn't consume arguments and +simply loads the program from the descriptor. The bit is also the loader's +license to finish the identity. After mapping the program it may retarget the +``AT_PHDR``/``AT_ENTRY``/``AT_BASE`` entries of ``/proc/pid/auxv`` and the +code/data statistics markers via one ``PR_SET_MM_MAP`` which completes +what attaching debuggers observe. What remains visibly different from a +direct execution is the address space layout. The interpreter occupies +the main-image position and the program lives in the mmap region. + + Hints ----- From 73808bc5fd98eb055c12fa9afd954cea5417817f Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 16:13:57 +0200 Subject: [PATCH 44/63] exec: carry a PT_INTERP substitute in struct linux_binprm binfmt_misc currently supports an execution model where the registered interpreter becomes the executed program and the matched binary is handed to it as payload. The upcoming binfmt_misc loader mode inverts this. The matched binary remains the executed program and the registered interpreter is substituted into the role the binary's PT_INTERP would have played. Add the channel for that hand-over. bprm->loader carries an open_exec-style struct file reference from the binfmt_misc match to the binary format that consumes it. Unlike bprm->interpreter it does not request a restart of the format search. The stashing handler declines the exec with -ENOEXEC and the search continues to the real format in the same round. Both ELF loaders consume it, so give them the two helpers to do it with rather than a copy each. bprm_open_interpreter() hands out the substitute in place of what PT_INTERP names and bprm_drop_loader() releases one that turned out not to apply. Establish the complete lifecycle up front so a stashed loader can neither leak nor be silently ignored. - Chain restart: if another format wins the round by staging bprm->interpreter (binfmt_script) the stashed loader belonged to the file being replaced. Drop it at the top of the swap block in exec_binprm(). - Unclaimed or error: free_bprm() releases a still-stashed loader next to the other bprm file references. - Silent non-substitution: a final format that reaches begin_new_exec() with a pending loader would run the binary while ignoring the override. Refuse with -ENOEXEC before the point of no return. Formats that do not know about the override (binfmt_flat, out-of-tree) need no changes. Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-15-e57866e4ae0f@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/exec.c | 42 +++++++++++++++++++++++++++++++++++++++++ include/linux/binfmts.h | 3 +++ 2 files changed, 45 insertions(+) diff --git a/fs/exec.c b/fs/exec.c index 128964d1e9d6..856731f78d05 100644 --- a/fs/exec.c +++ b/fs/exec.c @@ -1123,6 +1123,10 @@ int begin_new_exec(struct linux_binprm * bprm) struct task_struct *me = current; int retval; + /* A pending PT_INTERP substitution this format cannot consume. */ + if (bprm->loader) + return -ENOEXEC; + /* Once we are committed compute the creds */ retval = bprm_creds_from_file(bprm); if (retval) @@ -1414,6 +1418,39 @@ static void do_close_execat(struct file *file) fput(file); } +/** + * bprm_open_interpreter - open the interpreter the binary asks for + * @bprm: binary that is being executed + * @path: the interpreter path named in the binary's PT_INTERP + * + * A binfmt_misc loader entry substitutes for the interpreter the binary + * names. Hand out the stashed substitute if there is one and open @path + * if there is not. The caller owns the reference either way and releases + * it like any other open_exec() one. + * + * Return: the interpreter on success, an ERR_PTR on failure + */ +struct file *bprm_open_interpreter(struct linux_binprm *bprm, const char *path) +{ + if (bprm->loader) + return no_free_ptr(bprm->loader); + return open_exec(path); +} + +/** + * bprm_drop_loader - discard a PT_INTERP substitute that does not apply + * @bprm: binary that is being executed + * + * A binary without PT_INTERP has nothing to substitute for, so drop the + * override and let the binary load natively rather than have + * begin_new_exec() refuse it. A no-op once bprm_open_interpreter() took + * the substitute. + */ +void bprm_drop_loader(struct linux_binprm *bprm) +{ + do_close_execat(no_free_ptr(bprm->loader)); +} + static void free_bprm(struct linux_binprm *bprm) { if (bprm->mm) { @@ -1433,6 +1470,8 @@ static void free_bprm(struct linux_binprm *bprm) if (bprm->old_mm) exec_mm_put_old(bprm->old_mm); do_close_execat(bprm->file); + /* An unconsumed PT_INTERP substitute from a binfmt_misc loader entry. */ + bprm_drop_loader(bprm); do_close_execat(bprm->executable); /* If a binfmt changed the interp, free it. */ if (bprm->interp != bprm->filename) @@ -1750,6 +1789,9 @@ static int exec_binprm(struct linux_binprm *bprm) if (!bprm->interpreter) break; + /* A stashed PT_INTERP substitute belonged to the replaced file. */ + bprm_drop_loader(bprm); + exec = bprm->file; bprm->file = bprm->interpreter; bprm->interpreter = NULL; diff --git a/include/linux/binfmts.h b/include/linux/binfmts.h index 62465574e2a0..a2daecbb01d6 100644 --- a/include/linux/binfmts.h +++ b/include/linux/binfmts.h @@ -62,6 +62,7 @@ struct linux_binprm { is_check:1; struct file *executable; /* Executable to pass to the interpreter */ struct file *interpreter; + struct file *loader; struct file *file; struct cred *cred; /* new credentials */ int unsafe; /* how unsafe this exec is (mask of LSM_UNSAFE_*) */ @@ -159,6 +160,8 @@ extern int begin_new_exec(struct linux_binprm * bprm); extern void setup_new_exec(struct linux_binprm * bprm); extern void finalize_exec(struct linux_binprm *bprm); extern void would_dump(struct linux_binprm *, struct file *); +struct file *bprm_open_interpreter(struct linux_binprm *bprm, const char *path); +void bprm_drop_loader(struct linux_binprm *bprm); extern int suid_dumpable; From 2a4d517681e105dcfabd7ec7d2dae6285c593ee3 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 16:13:58 +0200 Subject: [PATCH 45/63] binfmt_elf: consume a stashed PT_INTERP substitute When a binfmt_misc loader entry stashed bprm->loader use it instead of opening the path named in PT_INTERP. The substitution deliberately changes as little as possible. Ownership transfers into the local interpreter reference which the existing success and error paths already release. A binary without PT_INTERP has nothing to substitute for. Drop the override at the end of the segment scan and load the binary natively. Nothing sets bprm->loader yet. Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-16-e57866e4ae0f@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_elf.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fs/binfmt_elf.c b/fs/binfmt_elf.c index be8fd437b5a3..00ff35cad441 100644 --- a/fs/binfmt_elf.c +++ b/fs/binfmt_elf.c @@ -901,7 +901,7 @@ static int load_elf_binary(struct linux_binprm *bprm) if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0') goto out_free_interp; - interpreter = open_exec(elf_interpreter); + interpreter = bprm_open_interpreter(bprm, elf_interpreter); kfree(elf_interpreter); retval = PTR_ERR(interpreter); if (IS_ERR(interpreter)) @@ -932,6 +932,9 @@ static int load_elf_binary(struct linux_binprm *bprm) goto out_free_ph; } + /* No PT_INTERP to substitute for: the override does not apply. */ + bprm_drop_loader(bprm); + elf_ppnt = elf_phdata; for (i = 0; i < elf_ex->e_phnum; i++, elf_ppnt++) switch (elf_ppnt->p_type) { From 08e4b1c05ed0d77480301a996fab33baca8201a2 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 16:13:59 +0200 Subject: [PATCH 46/63] binfmt_elf_fdpic: consume a stashed PT_INTERP substitute Do what binfmt_elf does. When a binfmt_misc loader entry stashed bprm->loader use it in place of the path named in PT_INTERP, and drop the override when the binary names no interpreter at all. Without this 'L' is unusable on nommu, where fdpic is the only ELF loader. On ARM with an MMU both loaders are registered but split the ELF space between them along elf_check_fdpic(), so an fdpic binary is never picked up by binfmt_elf either. Declining is what fdpic did so far, but it declined late. The pending override was only caught in begin_new_exec(), by which point the segment scan had opened the interpreter the binary itself names and overwritten bprm->buf with its header, leaving the next format in the round to inspect a buffer that no longer describes the file it is offered. The scan consumes the override now, so of the in-tree formats only binfmt_flat still relies on the refusal, and it reads bprm->buf without writing it. Transparent dispatch needs nothing on top of the AT_FLAGS translation both loaders already share. The binary travels in AT_EXECFD, which create_elf_fdpic_tables() emits, and the exe and comm labelling is done in exec.c for every format. Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-17-e57866e4ae0f@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_elf_fdpic.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fs/binfmt_elf_fdpic.c b/fs/binfmt_elf_fdpic.c index 0a3cdf280307..068c46875c74 100644 --- a/fs/binfmt_elf_fdpic.c +++ b/fs/binfmt_elf_fdpic.c @@ -263,7 +263,8 @@ static int load_elf_fdpic_binary(struct linux_binprm *bprm) kdebug("Using ELF interpreter %s", interpreter_name); /* replace the program with the interpreter */ - interpreter = open_exec(interpreter_name); + interpreter = bprm_open_interpreter(bprm, + interpreter_name); retval = PTR_ERR(interpreter); if (IS_ERR(interpreter)) { interpreter = NULL; @@ -299,6 +300,9 @@ static int load_elf_fdpic_binary(struct linux_binprm *bprm) } + /* No PT_INTERP to substitute for: the override does not apply. */ + bprm_drop_loader(bprm); + if (is_constdisp(&exec_params.hdr)) exec_params.flags |= ELF_FDPIC_FLAG_CONSTDISP; From 83cd3989ba0971693461088d35142ad52d862135 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 16:14:00 +0200 Subject: [PATCH 47/63] binfmt_misc: add the 'L' loader substitution flag Add the first activation of the PT_INTERP substitution machinery. A static entry registered with the new 'L' flag no longer runs the registered interpreter with the binary as payload. It stashes the interpreter as bprm->loader and declines the match with -ENOEXEC. The format search continues in the same round. binfmt_elf claims the binary as a fully native exec and substitutes the stashed file for the binary's PT_INTERP. 'L' rejects every classic-dispatch flag at registration. 'T', 'P' and 'O' have nothing to act on (no argv splice, no execfd) and 'C' is subsumed (credentials derive from the binary natively). 'F' composes and is valuable: with it the substitute is pre-opened at registration time and immune to mount namespace changes. Without it the substitute is opened at exec time in the exec'ing task's context, so 'L' joins 'C' in the requirement that the interpreter be named by an absolute path. As with 'C', only trusted interpreters should be registered. The substituted loader runs with credentials derived from the binary. Like the other flag characters 'L' cannot be used as the field delimiter. The flag scan would run off the registration buffer. The interpreter open is shared with the classic path via the entry_open_interpreter() helper. An open error fails the exec. Map -ENOEXEC to -EACCES to avoid letting the binary run with its own PT_INTERP. Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-18-e57866e4ae0f@kernel.org Signed-off-by: Christian Brauner (Amutable) --- Documentation/admin-guide/binfmt-misc.rst | 15 ++++++++--- fs/binfmt_misc.c | 33 +++++++++++++++++++++-- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst index d46130be0891..22aefab2c21e 100644 --- a/Documentation/admin-guide/binfmt-misc.rst +++ b/Documentation/admin-guide/binfmt-misc.rst @@ -100,6 +100,13 @@ Here is what the fields mean: ``AT_FLAGS_TRANSPARENT_INTERP`` contract. Combining ``T`` with ``P`` is rejected: transparency preserves the whole argument vector, argv[0] included. + ``L`` - loader substitution + Do not run the interpreter on the binary at all: load the + binary itself as a fully native exec and substitute the + interpreter for the loader named in the binary's + ``PT_INTERP``. See the "Loader substitution" section + below. ``L`` rejects ``T``, ``P``, ``O`` and ``C``; + ``F`` composes. There are some restrictions: @@ -108,10 +115,10 @@ There are some restrictions: - the magic must reside in the first 128 bytes of the file, i.e. offset+size(magic) has to be less than 128 - the interpreter string may not exceed 127 characters - - an interpreter used with ``C`` but without ``F`` has to be named by an - absolute path. It is opened when the binary is executed, so a relative - one would be resolved against the working directory of whoever runs - the binary + - an interpreter used with ``C`` or ``L`` but without ``F`` has to be + named by an absolute path. It is opened when the binary is executed, so + a relative one would be resolved against the working directory of + whoever runs the binary To use binfmt_misc you have to mount it first. You can mount it with diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index 98f9208e8188..33835aebc8eb 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -51,6 +51,7 @@ enum binfmt_misc_entry_flags { MISC_FMT_CREDENTIALS = (1U << 29), MISC_FMT_OPEN_FILE = (1U << 28), MISC_FMT_TRANSPARENT = (1U << 27), + MISC_FMT_LOADER = (1U << 26), }; /** @@ -73,6 +74,7 @@ static const struct binfmt_misc_flag misc_flags[] = { { 'C', MISC_FMT_CREDENTIALS, MISC_FMT_OPEN_BINARY, "credentials from the binary" }, { 'F', MISC_FMT_OPEN_FILE, 0, "open interpreter file now" }, { 'T', MISC_FMT_TRANSPARENT, MISC_FMT_OPEN_BINARY, "transparent" }, + { 'L', MISC_FMT_LOADER, 0, "loader substitution" }, }; /* Look up a flag character, NULL if @c is not one. */ @@ -458,6 +460,9 @@ static int load_misc_binary(struct linux_binprm *bprm) unsigned long flags; int retval; + /* Only binfmt_misc stages one and exec_binprm() clears it per round. */ + WARN_ON_ONCE(bprm->loader); + misc = current_binfmt_misc(); if (!READ_ONCE(misc->enabled)) return -ENOEXEC; @@ -473,9 +478,27 @@ static int load_misc_binary(struct linux_binprm *bprm) flags = entry_invocation_flags(fmt, bprm); /* No argv is built for a staged argument to land in. */ - if ((flags & MISC_FMT_TRANSPARENT) && bprm->bpf_interp_arg) + if ((flags & (MISC_FMT_LOADER | MISC_FMT_TRANSPARENT)) && + bprm->bpf_interp_arg) return -EINVAL; + /* + * Stash the interpreter for binfmt_elf to consume in place of the + * binary's PT_INTERP and decline the match, so the search continues + * to the real format in the same round. + */ + if (flags & MISC_FMT_LOADER) { + interp_file = entry_open_interpreter(fmt, interpreter); + if (IS_ERR(interp_file)) { + retval = PTR_ERR(interp_file); + /* Declining here would run the binary's own PT_INTERP. */ + return retval == -ENOEXEC ? -EACCES : retval; + } + + bprm->loader = interp_file; + return -ENOEXEC; + } + if (!(flags & MISC_FMT_TRANSPARENT)) { retval = build_interp_argv(bprm, interpreter, flags); if (retval) @@ -772,13 +795,19 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, (e->flags & MISC_FMT_PRESERVE_ARGV0)) return ERR_PTR(-EINVAL); + /* A native exec splices no argv, passes no execfd and needs no creds. */ + if ((e->flags & MISC_FMT_LOADER) && + (e->flags & (MISC_FMT_TRANSPARENT | MISC_FMT_PRESERVE_ARGV0 | + MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_BINARY))) + return ERR_PTR(-EINVAL); + if (*p == '\n') p++; if (p != buf + count) return ERR_PTR(-EINVAL); /* Non-F opens the interp at exec against the caller's cwd; require absolute. */ - if ((e->flags & MISC_FMT_CREDENTIALS) && + if ((e->flags & (MISC_FMT_LOADER | MISC_FMT_CREDENTIALS)) && !(e->flags & MISC_FMT_OPEN_FILE) && e->interpreter[0] != '/') return ERR_PTR(-EINVAL); From 375e8a31a8b069bf0ebf6398815057660fe059b1 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 16:14:01 +0200 Subject: [PATCH 48/63] binfmt_misc: let a bpf handler request loader substitution Give bpf handlers the per-exec equivalent of the static 'L' flag. A load program that sets BPF_BINPRM_LOADER has its selected interpreter substituted for the binary's PT_INTERP instead of run with the binary as payload. The binary otherwise executes as a fully native exec. A single handler can now grade its dispatch per binary: native-arch ELF with PT_INTERP gets loader substitution for full native identity. Anything else, such as foreign arch, static, non-ELF can use transparent or classic dispatch. The load program can read the binary's ELF header from bprm->buf to make that call. Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-19-e57866e4ae0f@kernel.org Signed-off-by: Christian Brauner (Amutable) --- Documentation/admin-guide/binfmt-misc.rst | 9 ++++++--- fs/binfmt_misc.c | 2 ++ fs/binfmt_misc_bpf.c | 17 ++++++++++++----- include/linux/binfmt_misc.h | 4 ++++ 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst index 22aefab2c21e..03c6806785f5 100644 --- a/Documentation/admin-guide/binfmt-misc.rst +++ b/Documentation/admin-guide/binfmt-misc.rst @@ -197,9 +197,9 @@ interpreter line, e.g. for a handler that resolves ``$ORIGIN`` in a script's ``#!`` path and needs to preserve the argument that followed it. The invocation flags a static entry fixes at registration - ``P``, ``C``, -``O`` and ``T`` - are per-exec choices for a bpf handler, made by the ``load`` -program with the ``bpf_binprm_set_flags()`` kfunc, so a single handler can -decide them differently for each binary it handles: +``O``, ``T`` and ``L`` - are per-exec choices for a bpf handler, made by the +``load`` program with the ``bpf_binprm_set_flags()`` kfunc, so a single +handler can decide them differently for each binary it handles: - ``BPF_BINPRM_PRESERVE_ARGV0`` keeps the caller's ``argv[0]`` (the ``P`` flag). @@ -220,6 +220,9 @@ decide them differently for each binary it handles: run a binary passed as an inaccessible ``O_CLOEXEC`` file descriptor to ``execveat()``, which a path-splicing dispatch cannot: the interpreter has no path by which to open it. +- ``BPF_BINPRM_LOADER`` substitutes the interpreter for the binary's + ``PT_INTERP`` and runs the binary as a fully native exec (the ``L`` + flag). It excludes the other flags and a staged interpreter argument. Because these are program choices, a ``B`` entry carries no flags in the register string; ``F`` (pre-open a fixed interpreter) has no meaning for it. diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index 33835aebc8eb..707f8a14f8a6 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -355,6 +355,8 @@ static unsigned long entry_invocation_flags(const struct binfmt_misc_entry *e, flags |= MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_BINARY; if (bpf_flags & BPF_BINPRM_TRANSPARENT) flags |= MISC_FMT_TRANSPARENT | MISC_FMT_OPEN_BINARY; + if (bpf_flags & BPF_BINPRM_LOADER) + flags |= MISC_FMT_LOADER; return flags; } diff --git a/fs/binfmt_misc_bpf.c b/fs/binfmt_misc_bpf.c index d279ffa9c4ad..5bf0e46b867c 100644 --- a/fs/binfmt_misc_bpf.c +++ b/fs/binfmt_misc_bpf.c @@ -171,13 +171,15 @@ __bpf_kfunc int bpf_binprm_set_interp_arg(struct linux_binprm *bprm, * @flags: an OR of enum bpf_binprm_flags values * * To be called from the load program of a struct binfmt_misc_ops handler. It - * decides per exec what a static entry fixes at registration with the P, C, O - * and T flags: BPF_BINPRM_PRESERVE_ARGV0 keeps the caller's argv[0], + * decides per exec what a static entry fixes at registration with the P, C, + * O, T and L flags: BPF_BINPRM_PRESERVE_ARGV0 keeps the caller's argv[0], * BPF_BINPRM_CREDENTIALS computes credentials from the binary, and * BPF_BINPRM_EXECFD hands the binary to the interpreter through AT_EXECFD. * BPF_BINPRM_TRANSPARENT additionally leaves the argument vector untouched, - * making the exec look like a direct execution of the binary. Calling it - * again replaces the flags, passing zero clears them again. + * making the exec look like a direct execution of the binary. + * BPF_BINPRM_LOADER substitutes the interpreter for the binary's PT_INTERP + * and runs the binary as a native exec; it excludes every other flag. + * Calling it again replaces the flags, passing zero clears them again. * * Return: 0 on success, -EINVAL if @flags contains an unknown bit or an * invalid combination @@ -186,7 +188,12 @@ __bpf_kfunc int bpf_binprm_set_flags(struct linux_binprm *bprm, enum bpf_binprm_flags flags) { if (flags & ~(BPF_BINPRM_PRESERVE_ARGV0 | BPF_BINPRM_CREDENTIALS | - BPF_BINPRM_EXECFD | BPF_BINPRM_TRANSPARENT)) + BPF_BINPRM_EXECFD | BPF_BINPRM_TRANSPARENT | + BPF_BINPRM_LOADER)) + return -EINVAL; + + /* Loader substitution is a native exec: no splice, execfd or creds work. */ + if ((flags & BPF_BINPRM_LOADER) && (flags & ~BPF_BINPRM_LOADER)) return -EINVAL; /* Transparency preserves the whole argv, argv[0] included. */ diff --git a/include/linux/binfmt_misc.h b/include/linux/binfmt_misc.h index 26da749391b4..4abdfd36b3fa 100644 --- a/include/linux/binfmt_misc.h +++ b/include/linux/binfmt_misc.h @@ -19,6 +19,9 @@ struct user_namespace; * @BPF_BINPRM_TRANSPARENT: leave argv untouched, the interpreter takes the * binary from AT_EXECFD (like the 'T' flag); implies * execfd, excludes preserve-argv0 + * @BPF_BINPRM_LOADER: substitute the interpreter for the binary's PT_INTERP + * and run the binary as a native exec (like the 'L' + * flag); excludes every other flag * * Set from a load program with bpf_binprm_set_flags(). Unlike a static entry, * a bpf handler chooses these per exec rather than once at registration. @@ -28,6 +31,7 @@ enum bpf_binprm_flags { BPF_BINPRM_CREDENTIALS = (1ULL << 1), BPF_BINPRM_EXECFD = (1ULL << 2), BPF_BINPRM_TRANSPARENT = (1ULL << 3), + BPF_BINPRM_LOADER = (1ULL << 4), }; /** From 87c50a5855cf0e1a4a42448b2245f6e90df20a4c Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 16:14:02 +0200 Subject: [PATCH 49/63] selftests/exec: test binfmt_misc loader substitution Exercise the 'L' flag end to end. The payload runs as the main image with a copy of the system loader substituted for its PT_INTERP, and asserts the native identity from inside: - argv exactly as the caller built it - no AT_EXECFD - AT_FLAGS clear - AT_BASE set but outside its own image - AT_PHDR/AT_ENTRY inside it - /proc/self/{exe,comm,stat} and AT_EXECFN all describing the binary - ETXTBSY on the running binary - the substituted loader visible in /proc/self/maps under its real path Magic matching pokes a marker into the ELF header's e_ident padding (EI_PAD, offset 9), which sits inside the match window and is ignored by kernel and loader alike. the same binary is also matched by extension. Two cases cover the paths where the substitution does not happen. A '#!' file that matched an 'L' entry is claimed by binfmt_script rather than by binfmt_elf, so the staged substitute has to be released when the interpreter replaces the file; the test opens the loader for writing afterwards, which fails with ETXTBSY if the write denial was leaked instead. A relative interpreter path is rejected at registration for both 'L' and 'C', neither of which may resolve one against the working directory of whoever runs the binary. The bpf-side BPF_BINPRM_LOADER path shares all machinery past the flag mapping. A harness case for it can join the bpf runtime coverage of the transparent series. Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-20-e57866e4ae0f@kernel.org Signed-off-by: Christian Brauner (Amutable) --- tools/testing/selftests/exec/.gitignore | 3 + tools/testing/selftests/exec/Makefile | 14 + .../selftests/exec/binfmt_loader_payload.c | 146 +++++++ .../testing/selftests/exec/binfmt_misc_bpf.c | 112 ++++-- .../selftests/exec/binfmt_misc_common.h | 83 ++++ .../selftests/exec/binfmt_misc_loader.c | 372 ++++++++++++++++++ tools/testing/selftests/exec/loader.bpf.c | 56 +++ 7 files changed, 764 insertions(+), 22 deletions(-) create mode 100644 tools/testing/selftests/exec/binfmt_loader_payload.c create mode 100644 tools/testing/selftests/exec/binfmt_misc_loader.c create mode 100644 tools/testing/selftests/exec/loader.bpf.c diff --git a/tools/testing/selftests/exec/.gitignore b/tools/testing/selftests/exec/.gitignore index 94b9ab4eb46c..fbbb1600ddb9 100644 --- a/tools/testing/selftests/exec/.gitignore +++ b/tools/testing/selftests/exec/.gitignore @@ -24,5 +24,8 @@ binfmt_bpf_interp binfmt_bpf_app binfmt_misc_transparent binfmt_transparent_interp +binfmt_misc_loader +binfmt_loader_payload +binfmt_loader_payload_static *.bpf.o vmlinux.h diff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile index 978b8bb572fe..67d4d54f6286 100644 --- a/tools/testing/selftests/exec/Makefile +++ b/tools/testing/selftests/exec/Makefile @@ -26,6 +26,13 @@ TEST_GEN_PROGS += check-exec TEST_GEN_PROGS += binfmt_misc_transparent TEST_GEN_FILES += binfmt_transparent_interp +# 'L' (loader substitution) binfmt_misc test: the payload runs as the main +# image with a copy of the system loader substituted for its PT_INTERP and +# asserts the native identity from inside; the static build proves the +# override is dropped for a binary without PT_INTERP. +TEST_GEN_PROGS += binfmt_misc_loader +TEST_GEN_FILES += binfmt_loader_payload binfmt_loader_payload_static + # binfmt_misc bpf-backed ('B') handler test: a libbpf harness plus its # struct_ops objects and the test interpreter/app it routes between. Only # built when clang, bpftool, the vmlinux BTF and libbpf are all present @@ -41,6 +48,7 @@ HAVE_BPF_TOOLCHAIN ?= $(shell command -v $(CLANG) >/dev/null 2>&1 && \ ifeq ($(HAVE_BPF_TOOLCHAIN),y) TEST_GEN_PROGS += binfmt_misc_bpf TEST_GEN_FILES += bpf_interp.bpf.o nix_origin.bpf.o transparent.bpf.o +TEST_GEN_FILES += loader.bpf.o TEST_GEN_FILES += binfmt_bpf_interp binfmt_bpf_app else $(info exec selftests: skipping binfmt_misc_bpf, needs clang, bpftool, vmlinux BTF and libbpf) @@ -105,6 +113,12 @@ $(OUTPUT)/binfmt_misc_bpf: binfmt_misc_bpf.c binfmt_misc_common.h $(OUTPUT)/binfmt_bpf_interp: binfmt_bpf_interp.c $(CC) $(CFLAGS) $(LDFLAGS) $< -o $@ +$(OUTPUT)/binfmt_loader_payload: binfmt_loader_payload.c binfmt_misc_common.h + $(CC) $(CFLAGS) $(LDFLAGS) -fPIE -pie $< -o $@ + +$(OUTPUT)/binfmt_loader_payload_static: binfmt_loader_payload.c binfmt_misc_common.h + $(CC) $(CFLAGS) $(LDFLAGS) -static $< -o $@ + # PT_INTERP is set to the literal "$ORIGIN/binfmt_bpf_interp"; the nix_origin # handler resolves it relative to the binary at run time. $(OUTPUT)/binfmt_bpf_app: binfmt_bpf_app.c diff --git a/tools/testing/selftests/exec/binfmt_loader_payload.c b/tools/testing/selftests/exec/binfmt_loader_payload.c new file mode 100644 index 000000000000..272db8efb4b5 --- /dev/null +++ b/tools/testing/selftests/exec/binfmt_loader_payload.c @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Payload for the binfmt_misc 'L' (loader substitution) selftest. It is + * executed as the MAIN image - a fully native exec - with the registered + * interpreter substituted for its PT_INTERP, and asserts the native + * identity from the inside. Exits 0 when every surface checks out. + * + * Modes, selected by the orchestrator via the environment: + * - default: full assertions, path-based ones included + * - BINFMT_TEST_MEMFD=1: executed from an inaccessible memfd, skip + * the path-based assertions + * - BINFMT_TEST_STATIC=1: static build; the override was dropped, so + * expect no interpreter at all + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "binfmt_misc_common.h" + +/* Start of our own mapped image, courtesy of the linker. */ +extern const char __ehdr_start[]; + +/* An image is never this large; used to bracket "within our image". */ +#define IMAGE_SPAN (16UL << 20) + +static int failed; + +static void check(int cond, const char *what) +{ + if (cond) + return; + fprintf(stderr, "[payload] FAILED: %s (errno %d)\n", what, errno); + failed = 1; +} + +/* Return whether /proc/self/maps names a path starting with @prefix. */ +static int maps_has_prefix(const char *prefix) +{ + char *line = NULL; + size_t len = 0; + int found = 0; + FILE *f; + + f = fopen("/proc/self/maps", "r"); + if (!f) + return -1; + while (getline(&line, &len, f) > 0) { + char *path = strchr(line, '/'); + + if (path && !strncmp(path, prefix, strlen(prefix))) { + found = 1; + break; + } + } + free(line); + fclose(f); + return found; +} + +int main(int argc, char *argv[]) +{ + const char *binary = getenv("BINFMT_TEST_BINARY"); + const char *interp = getenv("BINFMT_TEST_INTERP"); + int memfd_mode = getenv("BINFMT_TEST_MEMFD") != NULL; + int static_mode = getenv("BINFMT_TEST_STATIC") != NULL; + unsigned long self = (unsigned long)__ehdr_start; + unsigned long base = getauxval(AT_BASE); + unsigned long phdr = getauxval(AT_PHDR); + unsigned long entry = getauxval(AT_ENTRY); + unsigned long start_code, end_code; + + /* The argument vector is exactly what the caller built. */ + check(argc == 3 && !strcmp(argv[0], PAYLOAD_ARGV0) && + !strcmp(argv[1], PAYLOAD_ARG1) && !strcmp(argv[2], PAYLOAD_ARG2), + "argv was rewritten"); + + /* Native from birth: no execfd, no dispatch marker. */ + check(getauxval(AT_EXECFD) == 0, "AT_EXECFD present"); + check(getauxval(AT_FLAGS) == 0, "AT_FLAGS not native"); + + if (static_mode) { + /* The override was dropped: no interpreter was loaded. */ + check(base == 0, "AT_BASE set for a static payload"); + } else { + /* A loader is mapped in the interpreter slot, not our image. */ + check(base != 0, "AT_BASE missing"); + check(base < self || base >= self + IMAGE_SPAN, + "AT_BASE inside our own image"); + } + + /* We occupy the main-image slot. */ + check(phdr >= self && phdr < self + IMAGE_SPAN, + "AT_PHDR outside our image"); + check(entry >= self && entry < self + IMAGE_SPAN, + "AT_ENTRY outside our image"); + + /* The code statistics markers describe our image, natively placed. */ + if (stat_codes(getpid(), &start_code, &end_code) == 0) { + check(start_code >= self && start_code < end_code && + end_code < self + IMAGE_SPAN, + "stat start_code/end_code not our image"); + check(entry >= start_code && entry < end_code, + "AT_ENTRY outside [start_code, end_code)"); + } else { + check(0, "cannot parse /proc/self/stat"); + } + + if (!memfd_mode && binary) { + const char *execfn = (const char *)getauxval(AT_EXECFN); + const char *base_name = strrchr(binary, '/'); + + base_name = base_name ? base_name + 1 : binary; + + /* exe link, AT_EXECFN and comm all follow the binary. */ + check(exe_is(binary), "/proc/self/exe"); + check(execfn && !strcmp(execfn, binary), "AT_EXECFN"); + check(comm_is(base_name), "comm"); + + /* The running binary is write-denied, natively. */ + check(write_denied(binary), "no ETXTBSY on the binary"); + } + + if (interp) { + int found = maps_has_prefix(interp); + + if (static_mode) + /* Nothing was substituted, nothing may be mapped. */ + check(found == 0, "loader mapped for a static payload"); + else + /* The substituted loader shows under its real path. */ + check(found == 1, "loader path not in /proc/self/maps"); + } + + if (failed) + return 1; + printf("[payload] native identity checks out\n"); + return 0; +} diff --git a/tools/testing/selftests/exec/binfmt_misc_bpf.c b/tools/testing/selftests/exec/binfmt_misc_bpf.c index 31bc7dded585..069768a66ba0 100644 --- a/tools/testing/selftests/exec/binfmt_misc_bpf.c +++ b/tools/testing/selftests/exec/binfmt_misc_bpf.c @@ -22,6 +22,10 @@ * asserting interpreter (binfmt_transparent_interp) verifies the * identity the kernel constructed (exe link, argv, cmdline, comm, * AT_EXECFD, write denial) from inside the process. + * 4. loader: the load program sets BPF_BINPRM_LOADER; the payload + * (binfmt_loader_payload) runs as the main image with the selected + * interpreter substituted for its PT_INTERP and asserts the native + * identity from inside. * * The first two route to a test interpreter that prints BPF_INTERP_RAN, * proving the program's chosen interpreter actually ran. @@ -48,6 +52,8 @@ #define TRANS_PATH "/tmp/binfmt_bpf_riscv" #define EXPECT "BPF_INTERP_RAN" #define TRANS_EXPECT "TRANSPARENT_OK" +#define LOADER_INTERP "/tmp/binfmt_loader_interp" +#define LOADER_PATH "/tmp/binfmt_bpf_loader.ldrtest" /* A minimal 64-bit little-endian ELF header, padded to the read size. */ static int create_fake_elf(const char *path, unsigned short machine) @@ -100,49 +106,80 @@ static int check_output(const char *cmd, const char *expected) return strncmp(buf, expected, strlen(expected)) ? -1 : 0; } +/* An attached handler with its 'B' entry activated. */ +struct bpf_case { + struct bpf_object *obj; + struct bpf_link *link; + const char *entry; +}; + /* * Load @objfile, attach its struct_ops map @handler (which publishes the - * handler), activate a 'B' entry named @entry that references it, run @target - * and check it produced @expect. + * handler) and activate a 'B' entry named @entry that references it. */ -static int run_case(const char *objfile, const char *handler, - const char *entry, const char *target, const char *expect) +static int bpf_case_start(struct bpf_case *c, const char *objfile, + const char *handler, const char *entry) { - struct bpf_object *obj; struct bpf_map *map; - struct bpf_link *link; - int ret = -1; - obj = bpf_object__open_file(objfile, NULL); - if (!obj || libbpf_get_error(obj)) { + c->obj = NULL; + c->link = NULL; + c->entry = entry; + + c->obj = bpf_object__open_file(objfile, NULL); + if (!c->obj || libbpf_get_error(c->obj)) { fprintf(stderr, "open %s failed\n", objfile); + c->obj = NULL; return -1; } - if (bpf_object__load(obj)) { + if (bpf_object__load(c->obj)) { fprintf(stderr, "load %s failed (check dmesg for the verifier log)\n", objfile); - goto close; + goto fail; } - map = bpf_object__find_map_by_name(obj, handler); + map = bpf_object__find_map_by_name(c->obj, handler); if (!map) { fprintf(stderr, "no struct_ops map '%s' in %s\n", handler, objfile); - goto close; + goto fail; } - link = bpf_map__attach_struct_ops(map); - if (!link || libbpf_get_error(link)) { + c->link = bpf_map__attach_struct_ops(map); + if (!c->link || libbpf_get_error(c->link)) { fprintf(stderr, "attach struct_ops '%s' failed\n", handler); - goto close; + c->link = NULL; + goto fail; } if (register_entry(entry, handler)) { fprintf(stderr, "register 'B' entry '%s' failed\n", entry); - goto detach; + goto fail; } + return 0; + +fail: + bpf_link__destroy(c->link); + bpf_object__close(c->obj); + c->obj = NULL; + c->link = NULL; + return -1; +} + +static void bpf_case_stop(struct bpf_case *c) +{ + unregister(c->entry); + bpf_link__destroy(c->link); + bpf_object__close(c->obj); +} + +/* Activate @handler, run @target and check it produced @expect. */ +static int run_case(const char *objfile, const char *handler, + const char *entry, const char *target, const char *expect) +{ + struct bpf_case c; + int ret; + + if (bpf_case_start(&c, objfile, handler, entry)) + return -1; ret = check_output(target, expect); - unregister(entry); -detach: - bpf_link__destroy(link); -close: - bpf_object__close(obj); + bpf_case_stop(&c); return ret; } @@ -239,4 +276,35 @@ TEST_F(bpf_handler, transparent_dispatch) unlink(TRANS_INTERP); } +/* A per-exec loader substitution: the payload runs as a native exec. */ +TEST_F(bpf_handler, loader_substitution) +{ + char src[PATH_MAX], loader[PATH_MAX]; + struct bpf_case c; + int status; + + if (find_loader(loader, sizeof(loader))) + SKIP(return, "cannot determine own PT_INTERP"); + + ASSERT_EQ(copy_file(loader, LOADER_INTERP), 0); + ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_loader_payload"), 0); + ASSERT_EQ(copy_file(src, LOADER_PATH), 0); + ASSERT_EQ(patch_file(LOADER_PATH, EI_PAD, LOADER_MARKER, + strlen(LOADER_MARKER)), 0); + ASSERT_EQ(artifact_path(self->obj, sizeof(self->obj), + "loader.bpf.o"), 0); + + setenv("BINFMT_TEST_BINARY", LOADER_PATH, 1); + setenv("BINFMT_TEST_INTERP", LOADER_INTERP, 1); + + ASSERT_EQ(bpf_case_start(&c, self->obj, "loader", "test_bpf_loader"), 0); + status = run_payload(LOADER_PATH); + bpf_case_stop(&c); + EXPECT_EQ(status, 0); + + unsetenv("BINFMT_TEST_INTERP"); + unlink(LOADER_PATH); + unlink(LOADER_INTERP); +} + TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/exec/binfmt_misc_common.h b/tools/testing/selftests/exec/binfmt_misc_common.h index 0bd37e92421b..c6900ded019f 100644 --- a/tools/testing/selftests/exec/binfmt_misc_common.h +++ b/tools/testing/selftests/exec/binfmt_misc_common.h @@ -3,10 +3,12 @@ #ifndef __SELFTESTS_EXEC_BINFMT_MISC_COMMON_H #define __SELFTESTS_EXEC_BINFMT_MISC_COMMON_H +#include #include #include #include #include +#include #include #include #include @@ -27,6 +29,9 @@ #define PAYLOAD_ARG1 "argone" #define PAYLOAD_ARG2 "argtwo" +/* Marker the loader tests poke into the payload's e_ident padding. */ +#define LOADER_MARKER "LDRTST" + /* Exit status run_payload() reports when the exec was refused as unhandled. */ #define RUN_ENOEXEC 42 @@ -190,4 +195,82 @@ static inline bool write_denied(const char *path) return errno == ETXTBSY; } +static inline int patch_file(const char *path, off_t off, const void *data, size_t len) +{ + ssize_t n; + int fd; + + fd = open(path, O_WRONLY); + if (fd < 0) + return -1; + n = pwrite(fd, data, len, off); + close(fd); + return n == (ssize_t)len ? 0 : -1; +} + +/* start_code and end_code are the 26th and 27th fields of /proc/pid/stat. */ +static inline int stat_codes(pid_t pid, unsigned long *start_code, + unsigned long *end_code) +{ + char buf[4096], path[64], *p; + ssize_t n; + int fd, i; + + snprintf(path, sizeof(path), "/proc/%d/stat", pid); + fd = open(path, O_RDONLY); + if (fd < 0) + return -1; + n = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (n <= 0) + return -1; + buf[n] = '\0'; + + /* Skip "pid (comm)", then start_code is the 24th field after it. */ + p = strrchr(buf, ')'); + if (!p) + return -1; + p++; + for (i = 0; i < 23; i++) { + p = strchr(p + 1, ' '); + if (!p) + return -1; + } + if (sscanf(p, " %lu %lu", start_code, end_code) != 2) + return -1; + return 0; +} + +/* Find the system loader through our own PT_INTERP. */ +static inline int find_loader(char *out, size_t sz) +{ + ElfW(Ehdr) eh; + ElfW(Phdr) ph; + int fd, i, ret = -1; + + fd = open("/proc/self/exe", O_RDONLY); + if (fd < 0) + return -1; + if (pread(fd, &eh, sizeof(eh), 0) != sizeof(eh)) + goto out; + for (i = 0; i < eh.e_phnum; i++) { + if (pread(fd, &ph, sizeof(ph), + eh.e_phoff + i * eh.e_phentsize) != sizeof(ph)) + goto out; + if (ph.p_type != PT_INTERP) + continue; + if (!ph.p_filesz || ph.p_filesz > sz) + goto out; + if (pread(fd, out, ph.p_filesz, ph.p_offset) != + (ssize_t)ph.p_filesz) + goto out; + out[ph.p_filesz - 1] = '\0'; + ret = 0; + break; + } +out: + close(fd); + return ret; +} + #endif /* __SELFTESTS_EXEC_BINFMT_MISC_COMMON_H */ diff --git a/tools/testing/selftests/exec/binfmt_misc_loader.c b/tools/testing/selftests/exec/binfmt_misc_loader.c new file mode 100644 index 000000000000..1e14dcd274af --- /dev/null +++ b/tools/testing/selftests/exec/binfmt_misc_loader.c @@ -0,0 +1,372 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Test the 'L' (loader substitution) flag of binfmt_misc. A matched + * binary runs as the MAIN image - a fully native exec - with the + * registered interpreter substituted for its PT_INTERP. The payload + * (binfmt_loader_payload) asserts the native identity from inside. + * + * The substitute is a copy of the system loader found via our own + * PT_INTERP; magic matching pokes a marker into the ELF header's + * e_ident padding, which kernel and loader ignore. + * + * Needs root for the registration; no bpf toolchain involved. + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "binfmt_misc_common.h" +#include "kselftest_harness.h" + +#define ENTRY "test_loader" +#define INTERP_PATH "/tmp/binfmt_loader_interp" +#define MOVED_PATH INTERP_PATH ".moved" +#define TARGET_PATH "/tmp/binfmt_loader_target.ldrtest" +#define STATIC_PATH "/tmp/binfmt_loader_static.ldrtest" +#define FOREIGN_PATH "/tmp/binfmt_loader_foreign.ldrtest" +#define SCRIPT_PATH "/tmp/binfmt_loader_script.ldrtest" +#define M_RULE ":" ENTRY ":M:9:" LOADER_MARKER "::" INTERP_PATH ":L" +#define E_RULE ":" ENTRY ":E::ldrtest::" INTERP_PATH ":L" +#define FL_RULE ":" ENTRY ":E::ldrtest::" INTERP_PATH ":FL" + +/* Execute the binary from an inaccessible O_CLOEXEC memfd. */ +static int run_memfd(const char *path) +{ + int status; + pid_t pid; + + pid = fork(); + if (pid == 0) { + char *argv[] = { PAYLOAD_ARGV0, PAYLOAD_ARG1, PAYLOAD_ARG2, NULL }; + char buf[4096]; + int in, mfd; + ssize_t n; + + mfd = memfd_create("loader-test", MFD_CLOEXEC); + in = open(path, O_RDONLY); + if (mfd < 0 || in < 0) + _exit(125); + while ((n = read(in, buf, sizeof(buf))) > 0) + if (write(mfd, buf, n) != n) + _exit(125); + close(in); + setenv("BINFMT_TEST_MEMFD", "1", 1); + unsetenv("BINFMT_TEST_BINARY"); + syscall(SYS_execveat, mfd, "", argv, environ, AT_EMPTY_PATH); + _exit(126); + } + if (pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status)) + return -1; + return WEXITSTATUS(status); +} + +/* + * The differentiator against the transparent mode: at PTRACE_EVENT_EXEC + * the identity is already complete - exe, auxv and the stat code markers + * are mutually consistent with no window a debugger could observe. + */ +static int ptrace_probe(const char *target) +{ + unsigned long auxv[2 * 64], base = 0, entry = 0, at_flags = 0; + unsigned long start_code = 0, end_code = 0; + int status, fd, execfd_seen = 0, failed = 0; + char path[64], buf[PATH_MAX]; + ssize_t n; + pid_t pid; + int i; + + pid = fork(); + if (pid == 0) { + ptrace(PTRACE_TRACEME, 0, NULL, NULL); + raise(SIGSTOP); + execl(target, PAYLOAD_ARGV0, PAYLOAD_ARG1, PAYLOAD_ARG2, (char *)NULL); + _exit(126); + } + if (pid < 0) + return -1; + if (waitpid(pid, &status, 0) != pid || !WIFSTOPPED(status)) + goto fail_kill; + if (ptrace(PTRACE_SETOPTIONS, pid, NULL, (void *)PTRACE_O_TRACEEXEC)) + goto fail_kill; + if (ptrace(PTRACE_CONT, pid, NULL, NULL)) + goto fail_kill; + if (waitpid(pid, &status, 0) != pid || !WIFSTOPPED(status) || + status >> 8 != (SIGTRAP | (PTRACE_EVENT_EXEC << 8))) { + fprintf(stderr, "no exec stop (status %#x)\n", status); + goto fail_kill; + } + + snprintf(path, sizeof(path), "/proc/%d/exe", pid); + n = readlink(path, buf, sizeof(buf) - 1); + if (n <= 0) { + failed = 1; + } else { + buf[n] = '\0'; + if (strcmp(buf, target)) { + fprintf(stderr, "exe at exec stop: %s\n", buf); + failed = 1; + } + } + + snprintf(path, sizeof(path), "/proc/%d/auxv", pid); + fd = open(path, O_RDONLY); + if (fd < 0) { + n = -1; + } else { + n = read(fd, auxv, sizeof(auxv)); + close(fd); + } + if (n <= 0) { + failed = 1; + n = 0; + } + for (i = 0; i + 1 < (int)(n / sizeof(unsigned long)); i += 2) { + switch (auxv[i]) { + case AT_BASE: + base = auxv[i + 1]; + break; + case AT_ENTRY: + entry = auxv[i + 1]; + break; + case AT_FLAGS: + at_flags = auxv[i + 1]; + break; + case AT_EXECFD: + execfd_seen = 1; + break; + } + } + + if (stat_codes(pid, &start_code, &end_code)) + failed = 1; + + if (!base || execfd_seen || at_flags) { + fprintf(stderr, "auxv at exec stop not native\n"); + failed = 1; + } + if (!start_code || entry < start_code || entry >= end_code) { + fprintf(stderr, "auxv/stat inconsistent at exec stop\n"); + failed = 1; + } + + if (ptrace(PTRACE_CONT, pid, NULL, NULL)) + goto fail_kill; + if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status) || + WEXITSTATUS(status)) + failed = 1; + return failed ? -1 : 0; + +fail_kill: + kill(pid, SIGKILL); + waitpid(pid, &status, 0); + return -1; +} + +FIXTURE(loader) { + bool have_static; +}; + +FIXTURE_SETUP(loader) +{ + unsigned short foreign_machine = 0xdead; + char src[PATH_MAX], loader[PATH_MAX]; + + if (getuid() != 0) + SKIP(return, "test must be run as root"); + if (!binfmt_misc_available()) + SKIP(return, "no binfmt_misc"); + if (find_loader(loader, sizeof(loader))) + SKIP(return, "cannot determine own PT_INTERP"); + + ASSERT_EQ(copy_file(loader, INTERP_PATH), 0); + + ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_loader_payload"), 0); + ASSERT_EQ(copy_file(src, TARGET_PATH), 0); + ASSERT_EQ(patch_file(TARGET_PATH, EI_PAD, LOADER_MARKER, + strlen(LOADER_MARKER)), 0); + + /* The same payload with a machine type this kernel cannot load. */ + ASSERT_EQ(copy_file(src, FOREIGN_PATH), 0); + ASSERT_EQ(patch_file(FOREIGN_PATH, EI_PAD, LOADER_MARKER, + strlen(LOADER_MARKER)), 0); + ASSERT_EQ(patch_file(FOREIGN_PATH, offsetof(ElfW(Ehdr), e_machine), + &foreign_machine, sizeof(foreign_machine)), 0); + + self->have_static = + artifact_path(src, sizeof(src), "binfmt_loader_payload_static") == 0 && + copy_file(src, STATIC_PATH) == 0; + + setenv("BINFMT_TEST_BINARY", TARGET_PATH, 1); + setenv("BINFMT_TEST_INTERP", INTERP_PATH, 1); + + /* Everything below needs the flag; find out once. */ + if (write_reg(E_RULE)) { + ASSERT_EQ(errno, EINVAL); + SKIP(return, "kernel without the 'L' flag"); + } + unregister(ENTRY); +} + +FIXTURE_TEARDOWN(loader) +{ + unregister(ENTRY); + if (access(MOVED_PATH, F_OK) == 0) + rename(MOVED_PATH, INTERP_PATH); + unlink(TARGET_PATH); + unlink(STATIC_PATH); + unlink(FOREIGN_PATH); + unlink(SCRIPT_PATH); + unlink(INTERP_PATH); +} + +/* Grammar sanity check: the same entry without 'L' has to register. */ +TEST_F(loader, plain_entry_registers) +{ + ASSERT_EQ(write_reg(":" ENTRY ":E::ldrtest::" INTERP_PATH ":"), 0); +} + +/* 'L' is a native exec: every classic-dispatch flag is rejected. */ +TEST_F(loader, rejects_classic_flags) +{ + static const char * const combos[] = { "LT", "LP", "LC", "LO" }; + char rule[PATH_MAX]; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(combos); i++) { + int rc; + + snprintf(rule, sizeof(rule), + ":" ENTRY ":E::ldrtest::" INTERP_PATH ":%s", combos[i]); + rc = write_reg(rule); + EXPECT_EQ(rc, -1) + TH_LOG("'%s' was not rejected", combos[i]); + if (rc == 0) { + unregister(ENTRY); + continue; + } + EXPECT_EQ(errno, EINVAL); + } +} + +/* + * Without 'F' the interpreter is opened when the binary is executed, so a + * relative path would be resolved against the caller's working directory. + */ +TEST_F(loader, rejects_relative_interpreter) +{ + static const char * const flags[] = { "L", "C" }; + char rule[PATH_MAX]; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(flags); i++) { + int rc; + + snprintf(rule, sizeof(rule), + ":" ENTRY ":E::ldrtest::binfmt_loader_interp:%s", + flags[i]); + rc = write_reg(rule); + EXPECT_EQ(rc, -1) + TH_LOG("'%s' accepted a relative interpreter", flags[i]); + if (rc == 0) { + unregister(ENTRY); + continue; + } + EXPECT_EQ(errno, EINVAL); + } +} + +TEST_F(loader, extension_matched) +{ + ASSERT_EQ(write_reg(E_RULE), 0); + EXPECT_EQ(run_payload(TARGET_PATH), 0); +} + +TEST_F(loader, magic_matched) +{ + ASSERT_EQ(write_reg(M_RULE), 0); + EXPECT_EQ(run_payload(TARGET_PATH), 0); +} + +/* + * The differentiator against the transparent mode: at PTRACE_EVENT_EXEC the + * identity is already complete, with no window a debugger could observe. + */ +TEST_F(loader, exec_stop_consistency) +{ + ASSERT_EQ(write_reg(E_RULE), 0); + EXPECT_EQ(ptrace_probe(TARGET_PATH), 0); +} + +/* A binary without PT_INTERP drops the override and runs natively. */ +TEST_F(loader, static_binary_runs_natively) +{ + if (!self->have_static) + SKIP(return, "no static payload built"); + + ASSERT_EQ(write_reg(E_RULE), 0); + setenv("BINFMT_TEST_BINARY", STATIC_PATH, 1); + setenv("BINFMT_TEST_STATIC", "1", 1); + EXPECT_EQ(run_payload(STATIC_PATH), 0); + unsetenv("BINFMT_TEST_STATIC"); + setenv("BINFMT_TEST_BINARY", TARGET_PATH, 1); +} + +/* + * A '#!' file that matched an 'L' entry is claimed by binfmt_script, which + * sits ahead of binfmt_elf. The substitute the entry staged has to be + * released when the interpreter replaces the file, not leaked. + */ +TEST_F(loader, script_claims_the_file) +{ + static const char script[] = "#!/bin/sh\nexit 0\n"; + int fd; + + unlink(SCRIPT_PATH); + fd = open(SCRIPT_PATH, O_WRONLY | O_CREAT | O_EXCL, 0755); + ASSERT_GE(fd, 0); + ASSERT_EQ(write(fd, script, sizeof(script) - 1), + (ssize_t)sizeof(script) - 1); + ASSERT_EQ(close(fd), 0); + + ASSERT_EQ(write_reg(E_RULE), 0); + EXPECT_EQ(run_payload(SCRIPT_PATH), 0); + + /* A leaked substitute keeps its write denial on the loader. */ + fd = open(INTERP_PATH, O_WRONLY); + EXPECT_GE(fd, 0) + TH_LOG("loader still write denied (errno %d)", errno); + if (fd >= 0) + close(fd); +} + +/* Nothing needs the binary's path, so an inaccessible fd works. */ +TEST_F(loader, inaccessible_memfd) +{ + ASSERT_EQ(write_reg(M_RULE), 0); + EXPECT_EQ(run_memfd(TARGET_PATH), 0); +} + +/* The whole exec of a wrong-arch binary fails as if unhandled. */ +TEST_F(loader, foreign_arch_enoexec) +{ + ASSERT_EQ(write_reg(M_RULE), 0); + EXPECT_EQ(run_payload(FOREIGN_PATH), RUN_ENOEXEC); +} + +/* 'F' pre-opens the substitute, so it survives losing its path. */ +TEST_F(loader, fixed_interpreter_survives_rename) +{ + ASSERT_EQ(write_reg(FL_RULE), 0); + ASSERT_EQ(rename(INTERP_PATH, MOVED_PATH), 0); + EXPECT_EQ(run_payload(TARGET_PATH), 0); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/exec/loader.bpf.c b/tools/testing/selftests/exec/loader.bpf.c new file mode 100644 index 000000000000..108e51dd4961 --- /dev/null +++ b/tools/testing/selftests/exec/loader.bpf.c @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * binfmt_misc_ops handler for the loader-substitution case: match the + * marker the harness poked into the payload's e_ident padding and ask for + * the selected interpreter to be substituted for the binary's PT_INTERP, + * so the binary itself runs as a fully native exec. + */ +#include "vmlinux.h" +#include +#include + +char _license[] SEC("license") = "GPL"; + +#define EI_CLASS 4 +#define EI_PAD 9 +#define ELFCLASS64 2 + +extern int bpf_binprm_set_interp(struct linux_binprm *bprm, const char *path, + size_t path__sz) __ksym; +extern int bpf_binprm_set_flags(struct linux_binprm *bprm, + enum bpf_binprm_flags flags) __ksym; + +SEC("struct_ops.s/match") +bool BPF_PROG(loader_match, struct linux_binprm *bprm) +{ + if (bprm->buf[0] != 0x7f || bprm->buf[1] != 'E' || + bprm->buf[2] != 'L' || bprm->buf[3] != 'F' || + bprm->buf[EI_CLASS] != ELFCLASS64) + return false; + + /* The harness marks the payload with "LDRTST" at EI_PAD. */ + return bprm->buf[EI_PAD + 0] == 'L' && bprm->buf[EI_PAD + 1] == 'D' && + bprm->buf[EI_PAD + 2] == 'R' && bprm->buf[EI_PAD + 3] == 'T' && + bprm->buf[EI_PAD + 4] == 'S' && bprm->buf[EI_PAD + 5] == 'T'; +} + +SEC("struct_ops.s/load") +int BPF_PROG(loader_load, struct linux_binprm *bprm) +{ + char interp[] = "/tmp/binfmt_loader_interp"; + int err; + + err = bpf_binprm_set_flags(bprm, BPF_BINPRM_LOADER); + if (err) + return err; + + /* @path__sz includes the terminating NUL; 0 commits the selection. */ + return bpf_binprm_set_interp(bprm, interp, sizeof(interp)); +} + +SEC(".struct_ops.link") +struct binfmt_misc_ops loader = { + .match = (void *)loader_match, + .load = (void *)loader_load, + .name = "loader", +}; From bf9008534ed0faa220cfc44a9fc6d8b9f1317b74 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 16:14:03 +0200 Subject: [PATCH 50/63] binfmt_misc: document loader substitution Describe the L mode next to the transparent one. Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-21-e57866e4ae0f@kernel.org Signed-off-by: Christian Brauner (Amutable) --- Documentation/admin-guide/binfmt-misc.rst | 47 +++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst index 03c6806785f5..27391fcb43fa 100644 --- a/Documentation/admin-guide/binfmt-misc.rst +++ b/Documentation/admin-guide/binfmt-misc.rst @@ -260,6 +260,53 @@ direct execution is the address space layout. The interpreter occupies the main-image position and the program lives in the mmap region. +Loader substitution +------------------- + +The ``L`` flag turns the execution model around. Instead of running the +registered interpreter with the binary as its payload the kernel loads +the matched binary itself as the main image and substitutes the registered +interpreter for the loader named in the binary's ``PT_INTERP``. + +Because the exec is native, there is no dispatch identity to +reconstruct and no contract the substitute has to implement. A stock +dynamic loader works unchanged. The argument vector is untouched, +credentials and ``AT_SECURE`` derive from the binary, there is no +``AT_EXECFD`` and no marker in the aux vector, the binary sits in the +main-image slot with the native brk placement so ``/proc/pid/maps``, +core dumps and perf mmap records have the native shape, and the +identity is already complete when ``PTRACE_EVENT_EXEC`` stops the +tracee. So launching under a debugger works, not just attaching. ``L`` +entries are for ELF binaries of a native architecture. Foreign-arch +emulation and non-ELF payloads remain the domain of the classic and +transparent modes. + +The override applies when the format that finally claims the file is +ELF with a ``PT_INTERP``. A matched binary without one or an +interpreter-less ``ET_DYN`` drops the override and runs natively. A file +claimed by another format - a ``#!`` script, say - is handled by that +format as if the entry had not matched. ``L`` is therefore not an +enforcement mechanism: it decides how a binary that asks for a loader is +run, it does not guarantee that everything matching the entry runs under +the substitute. A format that cannot consume the override at all instead +refuses the exec with ``ENOEXEC`` before the point of no return. + +A wrong-architecture ELF fails the whole exec with ``ENOEXEC`` exactly +as if no entry had matched. A substitute that is not ELF of the right +architecture fails with ``ELIBBAD``. The usual ``PT_INTERP`` sanity +checks on the binary still apply. But the segment's content is otherwise +irrelevant. + +``L`` rejects the classic-dispatch flags ``T``, ``P``, ``O`` and ``C`` +at registration. ``F`` composes and is valuable: with it the substitute +is opened at registration time, so later mount namespace or path changes +cannot redirect it. Without it the substitute is opened when the binary +is executed, and the path is resolved in the mount namespace and root of +whoever runs the binary, which is why it has to be absolute. As with +``C``, register only trusted interpreters. The substituted loader runs +with credentials derived from the binary. + + Hints ----- From 9984a51e3aa760aefa36569c730f4ebf172ba368 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Thu, 30 Jul 2026 15:34:03 +0200 Subject: [PATCH 51/63] binfmt_misc: let a register string create an entry disabled An entry is matchable as soon as it is registered. create_entry() sets the enabled bit for every type and add_entry() links it straight into the instance, so everything an entry needs has to fit in the write that creates it. Add a 'D' flag. The entry is created disabled and has to be enabled by writing '1' to its entry file before it can match anything. That splits a registration into create and activate, which a later patch uses to configure an entry beyond what one register string can carry. It is useful on its own too. Entries can be staged without dispatching the moment they are written. A staged entry stays out of the search list entirely. add_entry() only hashes an entry that is born matchable, and the first '1' written to the entry file hashes a staged one, which takes its place in the search order at that point. The rcu insertion publishes the fully configured entry, so the exec side keeps the plain enabled test it always had. Removal cannot rely on the search list anymore. Whether an entry was already removed is now decided by its dentry, '-1' to the status file walks the directory instead of the list so staged entries do not survive it, and a '1' through a file handle held across a removal publishes nothing. 'D' is consumed at registration and not recorded. What matters afterwards is whether the entry is enabled, and the entry file already reports that. A 'B' entry's flags field had to be empty so far because every flag it could name shaped the invocation, which a bpf handler picks per exec with bpf_binprm_set_flags(). 'D' shapes the registration instead. So the rule becomes what it always meant: a 'B' entry carries no invocation flags, and 'D' composes. Link: https://patch.msgid.link/20260730-work-binfmt_misc-preopen-v1-1-4a0b0da71f16@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 98 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 81 insertions(+), 17 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index 707f8a14f8a6..ca7840b01a2b 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -37,6 +37,8 @@ #include #include +#include "internal.h" + /* Entry status and match type bit numbers. */ enum binfmt_misc_entry_bits { MISC_FMT_ENABLED_BIT = 0, @@ -52,8 +54,17 @@ enum binfmt_misc_entry_flags { MISC_FMT_OPEN_FILE = (1U << 28), MISC_FMT_TRANSPARENT = (1U << 27), MISC_FMT_LOADER = (1U << 26), + MISC_FMT_DISABLED = (1U << 25), }; +/* The flags that shape the invocation; a 'B' handler picks those per exec. */ +#define MISC_FMT_INVOCATION_FLAGS (MISC_FMT_PRESERVE_ARGV0 | \ + MISC_FMT_OPEN_BINARY | \ + MISC_FMT_CREDENTIALS | \ + MISC_FMT_OPEN_FILE | \ + MISC_FMT_TRANSPARENT | \ + MISC_FMT_LOADER) + /** * struct binfmt_misc_flag - a flag character of the register string * @c: the character userspace writes and reads back @@ -75,6 +86,7 @@ static const struct binfmt_misc_flag misc_flags[] = { { 'F', MISC_FMT_OPEN_FILE, 0, "open interpreter file now" }, { 'T', MISC_FMT_TRANSPARENT, MISC_FMT_OPEN_BINARY, "transparent" }, { 'L', MISC_FMT_LOADER, 0, "loader substitution" }, + { 'D', MISC_FMT_DISABLED, 0, "register disabled" }, }; /* Look up a flag character, NULL if @c is not one. */ @@ -175,7 +187,12 @@ search_binfmt_handler(struct binfmt_misc *misc, struct linux_binprm *bprm) /* Walk all the registered handlers. */ hlist_for_each_entry_rcu(e, &misc->entries, node, srcu_read_lock_held(&bm_entries_srcu)) { - /* Make sure this one is currently enabled. */ + /* + * Make sure this one is currently enabled. An entry enters + * the list at most once and only whole: its configuration is + * ordered before the rcu insertion that makes it visible + * here. + */ if (!test_bit(MISC_FMT_ENABLED_BIT, &e->flags)) continue; @@ -684,7 +701,7 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, size_t count) { struct binfmt_misc_entry *e __free(kfree) = NULL; - char *buf, *p, *flags; + char *buf, *p; char del; pr_debug("register: received %zu bytes\n", count); @@ -780,18 +797,29 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, } /* Parse the 'flags' field. */ - flags = p; p = check_special_flags(p, e); /* * A bpf handler decides the invocation flags per exec with * bpf_binprm_set_flags() rather than fixing them at registration, and * 'F' (pre-open a fixed interpreter) is meaningless for it, so a 'B' - * entry's flags field has to be empty. + * entry carries no invocation flags. */ - if (test_bit(MISC_FMT_BPF_BIT, &e->flags) && p != flags) + if (test_bit(MISC_FMT_BPF_BIT, &e->flags) && + (e->flags & MISC_FMT_INVOCATION_FLAGS)) return ERR_PTR(-EINVAL); + /* + * 'D' is a directive for this registration rather than a lasting + * property, so consume it: the entry is created disabled and stays + * out of the search list until '1' is written to its entry file. + * The first enable publishes it, for good. + */ + if (e->flags & MISC_FMT_DISABLED) { + e->flags &= ~MISC_FMT_DISABLED; + clear_bit(MISC_FMT_ENABLED_BIT, &e->flags); + } + /* Transparency preserves the whole argv, argv[0] included. */ if ((e->flags & MISC_FMT_TRANSPARENT) && (e->flags & MISC_FMT_PRESERVE_ARGV0)) @@ -852,6 +880,12 @@ static int parse_command(const char __user *buffer, size_t count) /* generic stuff */ +/* The root directory's inode; its lock serializes configuring an instance. */ +static struct inode *bm_root_inode(struct super_block *sb) +{ + return d_inode(sb->s_root); +} + static void bm_seq_hex(struct seq_file *m, const u8 *data, int size) { for (int i = 0; i < size; i++) @@ -992,10 +1026,11 @@ static void remove_binfmt_handler(struct binfmt_misc *misc, /* Remove @e unless it was already removed. */ static void bm_remove_entry(struct binfmt_misc_entry *e, struct super_block *sb) { - struct inode *root = d_inode(sb->s_root); + struct inode *root = bm_root_inode(sb); inode_lock_nested(root, I_MUTEX_PARENT); - if (!hlist_unhashed(&e->node)) + /* A staged entry is not hashed; the dentry says if it was removed. */ + if (!d_unhashed(e->dentry)) remove_binfmt_handler(i_binfmt_misc(root), e); inode_unlock(root); } @@ -1004,13 +1039,21 @@ static void bm_remove_entry(struct binfmt_misc_entry *e, struct super_block *sb) static void bm_remove_all_entries(struct binfmt_misc *misc, struct super_block *sb) { - struct inode *root = d_inode(sb->s_root); - struct binfmt_misc_entry *e; - struct hlist_node *next; + struct inode *root = bm_root_inode(sb); + struct dentry *child = NULL; inode_lock_nested(root, I_MUTEX_PARENT); - hlist_for_each_entry_safe(e, next, &misc->entries, node) - remove_binfmt_handler(misc, e); + /* + * Walk the directory rather than the search list: a staged entry + * is in the former but not yet in the latter. The control files + * carry no entry and stay. + */ + while ((child = find_next_child(sb->s_root, child))) { + struct binfmt_misc_entry *e = d_inode(child)->i_private; + + if (e) + remove_binfmt_handler(misc, e); + } inode_unlock(root); } @@ -1067,9 +1110,27 @@ static ssize_t bm_entry_write(struct file *file, const char __user *buffer, case BM_CMD_DISABLE: clear_bit(MISC_FMT_ENABLED_BIT, &e->flags); break; - case BM_CMD_ENABLE: + case BM_CMD_ENABLE: { + struct inode *root = bm_root_inode(inode->i_sb); + + /* + * The first enable publishes a 'D' entry into the search + * list, whole. The lock keeps that ordered against a second + * enable and against removal; a removed entry has nothing + * left to publish. + */ + inode_lock(root); set_bit(MISC_FMT_ENABLED_BIT, &e->flags); + if (hlist_unhashed(&e->node) && !d_unhashed(e->dentry)) { + struct binfmt_misc *misc = i_binfmt_misc(inode); + + spin_lock(&misc->entries_lock); + hlist_add_head_rcu(&e->node, &misc->entries); + spin_unlock(&misc->entries_lock); + } + inode_unlock(root); break; + } case BM_CMD_REMOVE: bm_remove_entry(e, inode->i_sb); break; @@ -1112,10 +1173,13 @@ static int add_entry(struct binfmt_misc_entry *e, struct super_block *sb) inode->i_fop = &bm_entry_operations; d_make_persistent(dentry, inode); - misc = i_binfmt_misc(inode); - spin_lock(&misc->entries_lock); - hlist_add_head_rcu(&e->node, &misc->entries); - spin_unlock(&misc->entries_lock); + /* A 'D' entry stays out of the search list until its first enable. */ + if (test_bit(MISC_FMT_ENABLED_BIT, &e->flags)) { + misc = i_binfmt_misc(inode); + spin_lock(&misc->entries_lock); + hlist_add_head_rcu(&e->node, &misc->entries); + spin_unlock(&misc->entries_lock); + } simple_done_creating(dentry); return 0; } From 25757bc855e388eedf86c69c382857ef2c67b08e Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 28 Jul 2026 14:26:33 +0200 Subject: [PATCH 52/63] selftests/exec: check that a binfmt_misc instance cannot be pinned An 'F' entry whose interpreter keeps the binfmt_misc superblock alive pins the instance that owns it forever. Cover both ways to build that: - an interpreter on the instance's own files, control file and entry file alike - and an instance used as an overlayfs lower layer. Check that an ordinary 'F' registration still succeeds so the fix stays honest about not changing what 'F' promises. Link: https://patch.msgid.link/20260728-work-binfmt_misc-selfpin-v1-2-74df5daeca5b@kernel.org Signed-off-by: Christian Brauner (Amutable) --- tools/testing/selftests/exec/Makefile | 10 ++ .../selftests/exec/binfmt_misc_selfpin.c | 158 ++++++++++++++++++ tools/testing/selftests/exec/config | 3 + 3 files changed, 171 insertions(+) create mode 100644 tools/testing/selftests/exec/binfmt_misc_selfpin.c diff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile index 67d4d54f6286..390fe11a7bed 100644 --- a/tools/testing/selftests/exec/Makefile +++ b/tools/testing/selftests/exec/Makefile @@ -21,6 +21,10 @@ TEST_GEN_PROGS += recursion-depth TEST_GEN_PROGS += null-argv TEST_GEN_PROGS += check-exec +# binfmt_misc must not be reachable as an exec source or as a stacking layer, +# or an 'F' entry can pin the instance that owns it. Unprivileged, no bpf. +TEST_GEN_PROGS += binfmt_misc_selfpin + # Static ('T' flag) transparent binfmt_misc test; the asserting interpreter # is shared with the bpf harness's transparent case. No bpf toolchain needed. TEST_GEN_PROGS += binfmt_misc_transparent @@ -91,6 +95,12 @@ $(OUTPUT)/script-exec.inc: $(CHECK_EXEC_SAMPLES)/script-exec.inc $(OUTPUT)/script-noexec.inc: $(CHECK_EXEC_SAMPLES)/script-noexec.inc cp $< $@ +# Reuses setup_userns()/write_file() from the filesystems selftests. Their +# wrappers.h wants the uapi headers, so ask for them here rather than widening +# CFLAGS for every program in this directory. +$(OUTPUT)/binfmt_misc_selfpin: CFLAGS += $(TOOLS_INCLUDES) +$(OUTPUT)/binfmt_misc_selfpin: ../filesystems/utils.c + # --- binfmt_misc bpf ('B') handler test --------------------------------- # The struct_ops bpf objects are compiled against the running kernel's BTF. # CLANG/BPFTOOL/VMLINUX_BTF are set above next to the toolchain check; diff --git a/tools/testing/selftests/exec/binfmt_misc_selfpin.c b/tools/testing/selftests/exec/binfmt_misc_selfpin.c new file mode 100644 index 000000000000..5286b0604eed --- /dev/null +++ b/tools/testing/selftests/exec/binfmt_misc_selfpin.c @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * An 'F' entry keeps its interpreter open for as long as the entry exists, + * and the entry only goes away when the binfmt_misc superblock is destroyed. + * An interpreter that lives on a mount which in turn keeps that superblock + * alive therefore pins the instance that owns it, and nothing can break the + * cycle. Check the two ways userspace could arrange for that: an interpreter + * on the binfmt_misc instance itself, and one on a filesystem stacked on it. + * + * Runs unprivileged in a user namespace; binfmt_misc is FS_USERNS_MOUNT. + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include + +#include "../filesystems/utils.h" +#include "kselftest_harness.h" + +#define MNT "/tmp/binfmt_selfpin" +#define BACKING "/tmp/binfmt_selfpin_back" +#define LOWER BACKING "/lower" +#define MERGED "/tmp/binfmt_selfpin_merged" + +#define MAGIC "\\xde\\xad" +#define RULE(interp) ":selfpin:M::" MAGIC "::" interp ":F" +/* Not on the instance, and unlike /bin/true it always exists. */ +#define INTERP "/proc/self/exe" + +#define OPTS_MAX (3 * PATH_MAX + 64) + +static int ensure_dir(const char *path) +{ + if (mkdir(path, 0755) && errno != EEXIST) + return -1; + return 0; +} + +/* Write @rule to this instance's register file, preserving write(2)'s errno. */ +static int register_at(struct __test_metadata *_metadata, const char *rule) +{ + int fd, saved; + ssize_t n; + + fd = open(MNT "/register", O_WRONLY); + ASSERT_GE(fd, 0); + n = write(fd, rule, strlen(rule)); + saved = errno; + close(fd); + errno = saved; + return n < 0 ? -1 : 0; +} + +/* + * Mount an overlay over @lower using a private upper/work pair, so the two + * mounts this test performs cannot interfere with each other and neither + * overlaps the lower layer. + */ +static int mount_overlay(const char *lower, int nr) +{ + char opts[OPTS_MAX], upper[PATH_MAX], work[PATH_MAX]; + + snprintf(upper, sizeof(upper), "%s/upper%d", BACKING, nr); + snprintf(work, sizeof(work), "%s/work%d", BACKING, nr); + if (mkdir(upper, 0755) || mkdir(work, 0755)) + return -1; + + snprintf(opts, sizeof(opts), "lowerdir=%s,upperdir=%s,workdir=%s", + lower, upper, work); + return mount("ovl", MERGED, "overlay", 0, opts); +} + +FIXTURE(selfpin) { +}; + +FIXTURE_SETUP(selfpin) +{ + /* setup_userns() exits rather than returns if this is not there. */ + if (access("/proc/self/ns/user", F_OK)) + SKIP(return, "kernel without user namespaces"); + ASSERT_EQ(setup_userns(), 0); + + ASSERT_EQ(ensure_dir(MNT), 0); + if (mount("binfmt_misc", MNT, "binfmt_misc", 0, NULL)) { + int saved = errno; + + /* Teardown doesn't run when setup skips, so clean up here. */ + rmdir(MNT); + SKIP(return, "no binfmt_misc: %s", strerror(saved)); + } +} + +FIXTURE_TEARDOWN(selfpin) +{ + /* The namespaces go with the process; just don't litter /tmp. */ + umount2(MERGED, MNT_DETACH); + umount2(BACKING, MNT_DETACH); + umount2(MNT, MNT_DETACH); + rmdir(MERGED); + rmdir(BACKING); + rmdir(MNT); +} + +/* + * The instance's own files are regular files the mounter owns, so they can be + * made executable. Opening one for exec still has to fail, otherwise the entry + * pins the very superblock it lives in. + */ +TEST_F(selfpin, interpreter_on_the_instance) +{ + ASSERT_EQ(chmod(MNT "/status", 0755), 0); + + ASSERT_NE(register_at(_metadata, RULE(MNT "/status")), 0); + EXPECT_EQ(errno, EACCES); +} + +/* Same for an entry file rather than one of the control files. */ +TEST_F(selfpin, interpreter_on_an_entry) +{ + ASSERT_EQ(register_at(_metadata, ":victim:M::" MAGIC "::" INTERP ":"), 0); + ASSERT_EQ(chmod(MNT "/victim", 0755), 0); + + ASSERT_NE(register_at(_metadata, RULE(MNT "/victim")), 0); + EXPECT_EQ(errno, EACCES); +} + +/* + * A stacking filesystem holds a private clone of each layer for its whole + * lifetime, so an instance used as a layer can be pinned by an interpreter + * that does not live on it at all. Refuse to be a layer. + */ +TEST_F(selfpin, refuses_to_be_stacked_on) +{ + ASSERT_EQ(ensure_dir(BACKING), 0); + ASSERT_EQ(mount("tmpfs", BACKING, "tmpfs", 0, NULL), 0); + ASSERT_EQ(mkdir(LOWER, 0755), 0); + ASSERT_EQ(ensure_dir(MERGED), 0); + + /* Nothing to prove unless overlayfs works here at all. */ + if (mount_overlay(LOWER, 1)) { + if (errno == ENODEV || errno == EPERM) + SKIP(return, "no unprivileged overlayfs"); + SKIP(return, "overlayfs unusable here: %s", strerror(errno)); + } + ASSERT_EQ(umount(MERGED), 0); + + EXPECT_NE(mount_overlay(MNT, 2), 0); +} + +/* An ordinary interpreter still registers with 'F'. */ +TEST_F(selfpin, ordinary_interpreter_still_works) +{ + EXPECT_EQ(register_at(_metadata, RULE(INTERP)), 0); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/exec/config b/tools/testing/selftests/exec/config index 2b1973e14291..ea359a929ae8 100644 --- a/tools/testing/selftests/exec/config +++ b/tools/testing/selftests/exec/config @@ -7,3 +7,6 @@ CONFIG_BPF_SYSCALL=y CONFIG_DEBUG_INFO=y CONFIG_DEBUG_INFO_BTF=y CONFIG_DEBUG_INFO_DWARF4=y +CONFIG_OVERLAY_FS=y +CONFIG_TMPFS=y +CONFIG_USER_NS=y From 686585ec270198690856077e55b7d579f84a765a Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Thu, 30 Jul 2026 15:34:04 +0200 Subject: [PATCH 53/63] selftests/exec: let binfmt_flag_supported() return a bool binfmt_flag_supported() returns 0 when the flag is supported and -1 when it is not, so every caller reads backwards: if (binfmt_flag_supported('T')) SKIP(return, "kernel without the 'T' flag"); Make it return a bool and flip the callers. errno from a failed probe is still set for callers that check it. Link: https://patch.msgid.link/20260730-work-binfmt_misc-preopen-v1-2-4a0b0da71f16@kernel.org Signed-off-by: Christian Brauner (Amutable) Reviewed-by: Farid Zakaria --- tools/testing/selftests/exec/binfmt_misc_bpf.c | 2 +- tools/testing/selftests/exec/binfmt_misc_common.h | 6 +++--- tools/testing/selftests/exec/binfmt_misc_transparent.c | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/testing/selftests/exec/binfmt_misc_bpf.c b/tools/testing/selftests/exec/binfmt_misc_bpf.c index 069768a66ba0..c6f5e8f34985 100644 --- a/tools/testing/selftests/exec/binfmt_misc_bpf.c +++ b/tools/testing/selftests/exec/binfmt_misc_bpf.c @@ -258,7 +258,7 @@ TEST_F(bpf_handler, transparent_dispatch) char src[PATH_MAX], cmd[PATH_MAX + 16]; /* Probe for transparent-mode support via its static counterpart. */ - if (binfmt_flag_supported('T')) + if (!binfmt_flag_supported('T')) SKIP(return, "kernel without transparent mode"); ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_transparent_interp"), 0); diff --git a/tools/testing/selftests/exec/binfmt_misc_common.h b/tools/testing/selftests/exec/binfmt_misc_common.h index c6900ded019f..e8d67908dbc4 100644 --- a/tools/testing/selftests/exec/binfmt_misc_common.h +++ b/tools/testing/selftests/exec/binfmt_misc_common.h @@ -117,16 +117,16 @@ static inline int artifact_path(char *out, size_t sz, const char *name) } /* Probe kernel support for a registration flag with a throwaway entry. */ -static inline int binfmt_flag_supported(char flag) +static inline bool binfmt_flag_supported(char flag) { char rule[64]; snprintf(rule, sizeof(rule), ":bm_flag_probe:E::bmprobe::/bin/true:%c", flag); if (write_reg(rule)) - return -1; + return false; unregister("bm_flag_probe"); - return 0; + return true; } /* diff --git a/tools/testing/selftests/exec/binfmt_misc_transparent.c b/tools/testing/selftests/exec/binfmt_misc_transparent.c index d0cb845df1d3..2ebf73de8018 100644 --- a/tools/testing/selftests/exec/binfmt_misc_transparent.c +++ b/tools/testing/selftests/exec/binfmt_misc_transparent.c @@ -56,7 +56,7 @@ FIXTURE_SETUP(transparent) ASSERT_EQ(create_target(), 0); /* Skip the whole suite on a kernel that does not know 'T'. */ - if (binfmt_flag_supported('T')) { + if (!binfmt_flag_supported('T')) { ASSERT_EQ(errno, EINVAL); SKIP(return, "kernel without the 'T' flag"); } From 6bd0c7aba69873e9944ac8853d167d32c294468c Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Thu, 30 Jul 2026 15:34:05 +0200 Subject: [PATCH 54/63] selftests/exec: test registering an entry disabled A magic entry registered with 'D' and the same entry without it, to pin down what the flag decides and what it leaves alone: - the entry reports itself disabled and nothing dispatches until '1' is written to it - without 'D' it dispatches straight away - 'D' is not read back among the entry's flags - enabling and disabling afterwards works as it does for any entry - 'D' composes with the flags that shape the invocation - '-1' to the status file removes a staged entry like any other - a file handle held across a removal cannot resurrect the entry Put the entry write and read-back helpers into binfmt_misc_common.h. The bpf suite will need them as well. Link: https://patch.msgid.link/20260730-work-binfmt_misc-preopen-v1-3-4a0b0da71f16@kernel.org Signed-off-by: Christian Brauner (Amutable) --- tools/testing/selftests/exec/Makefile | 4 + .../selftests/exec/binfmt_misc_common.h | 39 ++++ .../selftests/exec/binfmt_misc_disabled.c | 172 ++++++++++++++++++ 3 files changed, 215 insertions(+) create mode 100644 tools/testing/selftests/exec/binfmt_misc_disabled.c diff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile index 390fe11a7bed..ec7894a802e0 100644 --- a/tools/testing/selftests/exec/Makefile +++ b/tools/testing/selftests/exec/Makefile @@ -25,6 +25,10 @@ TEST_GEN_PROGS += check-exec # or an 'F' entry can pin the instance that owns it. Unprivileged, no bpf. TEST_GEN_PROGS += binfmt_misc_selfpin +# 'D' (register disabled) binfmt_misc test: an entry that exists but does +# not dispatch until it is enabled. Static magic entry, no bpf toolchain. +TEST_GEN_PROGS += binfmt_misc_disabled + # Static ('T' flag) transparent binfmt_misc test; the asserting interpreter # is shared with the bpf harness's transparent case. No bpf toolchain needed. TEST_GEN_PROGS += binfmt_misc_transparent diff --git a/tools/testing/selftests/exec/binfmt_misc_common.h b/tools/testing/selftests/exec/binfmt_misc_common.h index e8d67908dbc4..745aff84dc78 100644 --- a/tools/testing/selftests/exec/binfmt_misc_common.h +++ b/tools/testing/selftests/exec/binfmt_misc_common.h @@ -93,6 +93,45 @@ static inline void unregister(const char *name) } } +/* Write @line to @entry's file, reporting the errno it was refused with. */ +static inline int entry_command(const char *entry, const char *line) +{ + char path[PATH_MAX]; + int fd, retval = 0; + size_t len = strlen(line); + + snprintf(path, sizeof(path), BINFMT_DIR "/%s", entry); + fd = open(path, O_WRONLY | O_CLOEXEC); + if (fd < 0) + return -errno; + if (write(fd, line, len) != (ssize_t)len) + retval = -errno; + close(fd); + return retval; +} + +/* Does @entry's file report @line? */ +static inline bool entry_shows(const char *entry, const char *line) +{ + char path[PATH_MAX], buf[PATH_MAX]; + bool found = false; + FILE *fp; + + snprintf(path, sizeof(path), BINFMT_DIR "/%s", entry); + fp = fopen(path, "r"); + if (!fp) + return false; + while (fgets(buf, sizeof(buf), fp)) { + buf[strcspn(buf, "\n")] = '\0'; + if (!strcmp(buf, line)) { + found = true; + break; + } + } + fclose(fp); + return found; +} + /* Mount binfmt_misc unless it already is, and report whether it is usable. */ static inline bool binfmt_misc_available(void) { diff --git a/tools/testing/selftests/exec/binfmt_misc_disabled.c b/tools/testing/selftests/exec/binfmt_misc_disabled.c new file mode 100644 index 000000000000..47c9e8a4ee42 --- /dev/null +++ b/tools/testing/selftests/exec/binfmt_misc_disabled.c @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Test the 'D' (register disabled) flag of binfmt_misc. An entry + * registered with it exists but cannot be matched until userspace enables + * it, which splits a registration into create and activate. + * + * Needs root for the registration; no bpf toolchain involved. + */ +#define _GNU_SOURCE +#include +#include + +#include "binfmt_misc_common.h" +#include "kselftest_harness.h" + +#define MAGIC "#DISABLED-SELFTEST#" +#define TARGET_PATH "/tmp/binfmt_disabled_target" +#define INTERP_PATH "/tmp/binfmt_disabled_interp.sh" +#define ENTRY "test_disabled" +#define RULE(flags) ":" ENTRY ":M:0:" MAGIC "::" INTERP_PATH ":" flags + +/* The interpreter exits with a code the harness can recognise. */ +#define EXIT_INTERP 7 + +/* The target only has to carry the magic; it is never actually loaded. */ +static int create_target(void) +{ + char buf[128] = MAGIC "\n"; + int fd; + + unlink(TARGET_PATH); + fd = open(TARGET_PATH, O_WRONLY | O_CREAT | O_EXCL, 0755); + if (fd < 0) + return -1; + if (write(fd, buf, sizeof(buf)) != (ssize_t)sizeof(buf)) { + close(fd); + return -1; + } + close(fd); + return 0; +} + +static int create_interp(void) +{ + char buf[64]; + int fd; + + unlink(INTERP_PATH); + fd = open(INTERP_PATH, O_WRONLY | O_CREAT | O_EXCL, 0755); + if (fd < 0) + return -1; + snprintf(buf, sizeof(buf), "#!/bin/sh\nexit %d\n", EXIT_INTERP); + if (write(fd, buf, strlen(buf)) != (ssize_t)strlen(buf)) { + close(fd); + return -1; + } + return close(fd); +} + +FIXTURE(disabled) { +}; + +FIXTURE_SETUP(disabled) +{ + if (getuid() != 0) + SKIP(return, "test must be run as root"); + if (!binfmt_misc_available()) + SKIP(return, "no binfmt_misc"); + + /* Skip the whole suite on a kernel that does not know 'D'. */ + if (!binfmt_flag_supported('D')) { + ASSERT_EQ(errno, EINVAL); + SKIP(return, "kernel without the 'D' flag"); + } + + ASSERT_EQ(create_interp(), 0); + ASSERT_EQ(create_target(), 0); +} + +FIXTURE_TEARDOWN(disabled) +{ + unregister(ENTRY); + unlink(TARGET_PATH); + unlink(INTERP_PATH); +} + +/* The entry exists but does not dispatch until it is enabled. */ +TEST_F(disabled, inert_until_enabled) +{ + ASSERT_EQ(write_reg(RULE("D")), 0); + EXPECT_TRUE(entry_shows(ENTRY, "disabled")); + + /* Nothing matches it, so no binary format claims the target. */ + EXPECT_EQ(run_payload(TARGET_PATH), RUN_ENOEXEC); + + ASSERT_EQ(entry_command(ENTRY, "1\n"), 0); + EXPECT_TRUE(entry_shows(ENTRY, "enabled")); + EXPECT_EQ(run_payload(TARGET_PATH), EXIT_INTERP); +} + +/* Without 'D' an entry is matchable the moment it is registered. */ +TEST_F(disabled, enabled_without_the_flag) +{ + ASSERT_EQ(write_reg(RULE("")), 0); + EXPECT_TRUE(entry_shows(ENTRY, "enabled")); + EXPECT_EQ(run_payload(TARGET_PATH), EXIT_INTERP); +} + +/* 'D' is spent on the registration: the entry does not report it back. */ +TEST_F(disabled, flag_not_reported) +{ + ASSERT_EQ(write_reg(RULE("D")), 0); + EXPECT_FALSE(entry_shows(ENTRY, "flags: D")); + EXPECT_TRUE(entry_shows(ENTRY, "flags: ")); +} + +/* A disabled entry can be disabled and enabled like any other. */ +TEST_F(disabled, toggles_like_any_entry) +{ + ASSERT_EQ(write_reg(RULE("D")), 0); + + ASSERT_EQ(entry_command(ENTRY, "1\n"), 0); + ASSERT_EQ(run_payload(TARGET_PATH), EXIT_INTERP); + ASSERT_EQ(entry_command(ENTRY, "0\n"), 0); + EXPECT_EQ(run_payload(TARGET_PATH), RUN_ENOEXEC); + ASSERT_EQ(entry_command(ENTRY, "1\n"), 0); + EXPECT_EQ(run_payload(TARGET_PATH), EXIT_INTERP); +} + +/* 'D' composes with the invocation flags a static entry can carry. */ +TEST_F(disabled, composes_with_invocation_flags) +{ + ASSERT_EQ(write_reg(RULE("PD")), 0); + EXPECT_TRUE(entry_shows(ENTRY, "disabled")); + EXPECT_TRUE(entry_shows(ENTRY, "flags: P")); +} + +/* '-1' to the status file sweeps a staged entry with everything else. */ +TEST_F(disabled, removed_by_remove_all) +{ + int fd; + + ASSERT_EQ(write_reg(RULE("D")), 0); + EXPECT_TRUE(entry_shows(ENTRY, "disabled")); + + fd = open(BINFMT_DIR "/status", O_WRONLY | O_CLOEXEC); + ASSERT_GE(fd, 0); + ASSERT_EQ(write(fd, "-1", 2), 2); + close(fd); + + EXPECT_NE(access(BINFMT_DIR "/" ENTRY, F_OK), 0); +} + +/* A file handle held across a removal cannot resurrect the entry. */ +TEST_F(disabled, no_resurrection_after_remove) +{ + int fd; + + ASSERT_EQ(write_reg(RULE("D")), 0); + fd = open(BINFMT_DIR "/" ENTRY, O_WRONLY | O_CLOEXEC); + ASSERT_GE(fd, 0); + + ASSERT_EQ(write(fd, "-1", 2), 2); + EXPECT_NE(access(BINFMT_DIR "/" ENTRY, F_OK), 0); + + /* Accepted like any toggle of a removed entry, but publishes nothing. */ + EXPECT_EQ(write(fd, "1", 1), 1); + EXPECT_EQ(run_payload(TARGET_PATH), RUN_ENOEXEC); + close(fd); +} + +TEST_HARNESS_MAIN From d5ae8a7c4ddc6f5fedf759ca35503d61d7761556 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Thu, 30 Jul 2026 15:34:06 +0200 Subject: [PATCH 55/63] binfmt_misc: document registering an entry disabled Describe the 'D' flag and what it changes about a registration: - that the entry has to be enabled before it dispatches anything - and that the flag is not read back Scope the bpf section's "carries no flags" rule to invocation flags now that 'D' composes with 'B'. Link: https://patch.msgid.link/20260730-work-binfmt_misc-preopen-v1-4-4a0b0da71f16@kernel.org Signed-off-by: Christian Brauner (Amutable) --- Documentation/admin-guide/binfmt-misc.rst | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst index 27391fcb43fa..22639ed9c73c 100644 --- a/Documentation/admin-guide/binfmt-misc.rst +++ b/Documentation/admin-guide/binfmt-misc.rst @@ -107,6 +107,15 @@ Here is what the fields mean: ``PT_INTERP``. See the "Loader substitution" section below. ``L`` rejects ``T``, ``P``, ``O`` and ``C``; ``F`` composes. + ``D`` - registered disabled + The entry is created disabled instead of being matchable at + once, and has to be enabled by writing ``1`` to its file + before it dispatches anything. This splits a registration + into creating the entry and activating it, leaving room to + configure it in between - which is what a ``B`` entry that + binds interpreters needs; see the bpf section below. The flag + is spent on the registration and is not read back: what an + entry file reports afterwards is whether it is enabled. There are some restrictions: @@ -224,8 +233,10 @@ handler can decide them differently for each binary it handles: ``PT_INTERP`` and runs the binary as a fully native exec (the ``L`` flag). It excludes the other flags and a staged interpreter argument. -Because these are program choices, a ``B`` entry carries no flags in the -register string; ``F`` (pre-open a fixed interpreter) has no meaning for it. +Because these are program choices, a ``B`` entry carries no invocation +flags in the register string; ``F`` (pre-open a fixed interpreter) has no +meaning for it. The registration directive ``D`` is the exception: it +decides how the entry starts out, not how the interpreter is invoked. A handler is looked up only in the user namespace the struct_ops map was registered in. Handlers are not inherited, so an entry can only reference a From 145e675de6ca0b3865000876414b8a5f93246c6b Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Thu, 30 Jul 2026 15:34:07 +0200 Subject: [PATCH 56/63] selftests/exec: share the bpf handler preconditions The bpf handler fixture opens with three probes, each with its own SKIP. More fixtures with the same needs are about to be added, so hoist the probes into a helper that reports the first missing precondition. Link: https://patch.msgid.link/20260730-work-binfmt_misc-preopen-v1-5-4a0b0da71f16@kernel.org Signed-off-by: Christian Brauner (Amutable) --- .../testing/selftests/exec/binfmt_misc_bpf.c | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/tools/testing/selftests/exec/binfmt_misc_bpf.c b/tools/testing/selftests/exec/binfmt_misc_bpf.c index c6f5e8f34985..71bb6d8b4517 100644 --- a/tools/testing/selftests/exec/binfmt_misc_bpf.c +++ b/tools/testing/selftests/exec/binfmt_misc_bpf.c @@ -106,6 +106,30 @@ static int check_output(const char *cmd, const char *expected) return strncmp(buf, expected, strlen(expected)) ? -1 : 0; } +/* Does the kernel BTF know struct binfmt_misc_ops (CONFIG_BINFMT_MISC_BPF)? */ +static bool have_binfmt_misc_ops(void) +{ + struct btf *btf = btf__load_vmlinux_btf(); + bool have; + + have = btf && btf__find_by_name_kind(btf, "binfmt_misc_ops", + BTF_KIND_STRUCT) >= 0; + btf__free(btf); + return have; +} + +/* The reason bpf handler cases cannot run here, NULL if they can. */ +static const char *bpf_handler_unsupported(void) +{ + if (getuid() != 0) + return "test must be run as root"; + if (!have_binfmt_misc_ops()) + return "no struct binfmt_misc_ops in the kernel BTF (CONFIG_BINFMT_MISC_BPF)"; + if (!binfmt_misc_available()) + return "no binfmt_misc"; + return NULL; +} + /* An attached handler with its 'B' entry activated. */ struct bpf_case { struct bpf_object *obj; @@ -190,23 +214,10 @@ FIXTURE(bpf_handler) { FIXTURE_SETUP(bpf_handler) { char src[PATH_MAX]; - struct btf *btf; + const char *why = bpf_handler_unsupported(); - if (getuid() != 0) - SKIP(return, "test must be run as root"); - - /* The kernel must know struct binfmt_misc_ops (CONFIG_BINFMT_MISC_BPF). */ - btf = btf__load_vmlinux_btf(); - if (!btf || btf__find_by_name_kind(btf, "binfmt_misc_ops", - BTF_KIND_STRUCT) < 0) { - btf__free(btf); - SKIP(return, - "no struct binfmt_misc_ops in the kernel BTF (CONFIG_BINFMT_MISC_BPF)"); - } - btf__free(btf); - - if (!binfmt_misc_available()) - SKIP(return, "no binfmt_misc"); + if (why) + SKIP(return, "%s", why); /* Shared test interpreter. */ ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_bpf_interp"), 0); From a7b880449ab1b2389b31ed9da703691dcac54655 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Thu, 30 Jul 2026 15:34:08 +0200 Subject: [PATCH 57/63] binfmt_misc: carry pre-opened interpreters in struct binfmt_misc_interp An 'F' entry opens its interpreter at registration and every exec runs a clone of that file. The file lives in a bare struct file pointer next to the path it came from and put_binfmt_handler() closes it as a special case. Give the pre-opened interpreter a type of its own instead. struct binfmt_misc_interp carries the file, the path it was opened from and a selection name in a single allocation and is linked on a list that the entry owns and tears down in put_binfmt_handler(). An 'F' entry binds a single interpreter under the empty name and hands out clones of it as before. The open moves into open_interp_file() and works exactly as the open-coded block in bm_register_write() did. It is opened for execution at registration time, in the writer's context and with the credentials the register file was opened with. The entry can now own objects before it is published, so make put_binfmt_handler() the single teardown. create_entry() returns the entry with its reference held and every failure path in bm_register_write() simply puts it. That also replaces the open-coded bpf_ops release. No functional changes. A later patch lets a 'B' entry bind multiple interpreters selected by name per exec and reuses all of this. Link: https://patch.msgid.link/20260730-work-binfmt_misc-preopen-v1-6-4a0b0da71f16@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 151 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 119 insertions(+), 32 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index ca7840b01a2b..afd8a737c95b 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -98,6 +99,24 @@ static const struct binfmt_misc_flag *misc_flag_by_char(const char c) return NULL; } +/** + * struct binfmt_misc_interp - an interpreter an entry was registered with + * @list: link in the entry's list, in registration order + * @file: the file, opened at registration and never resolved again + * @path: the path it was registered under, used as the name the interpreter + * runs under; stored after @name in the same allocation + * @name: the name a load program selects it by; empty for the fixed + * interpreter of a static 'F' entry + * + * Owned by the entry and living exactly as long as it does. + */ +struct binfmt_misc_interp { + struct list_head list; + struct file *file; + const char *path; + char name[]; +}; + struct binfmt_misc_entry { struct hlist_node node; unsigned long flags; /* type, status, etc. */ @@ -108,9 +127,9 @@ struct binfmt_misc_entry { const char *interpreter; /* filename of interpreter */ char *name; struct dentry *dentry; - struct file *interp_file; const struct binfmt_misc_ops *bpf_ops; /* bpf-backed handler ('B') */ const char *bpf_ops_name; + struct list_head interps; /* the interpreters it bound */ refcount_t users; /* sync removal with load_misc_binary() */ struct rcu_head rcu; char buf[]; /* register string, fields point in here */ @@ -233,6 +252,82 @@ static struct binfmt_misc_entry *get_binfmt_handler(struct binfmt_misc *misc, return search_binfmt_handler(misc, bprm); } +/* Undo the open_exec() a pre-opened interpreter file came from. */ +static void close_interp_file(struct file *f) +{ + if (IS_ERR_OR_NULL(f)) + return; + exe_file_allow_write_access(f); + filp_close(f, NULL); +} + +/* + * Open an interpreter @path for execution: now, in the writer's context, + * and - since binfmt_misc mounts can be unprivileged - with @cred, the + * credentials the control file being written was opened with, not the + * writer's own. + */ +static struct file *open_interp_file(const struct cred *cred, const char *path) +{ + struct file *f; + + scoped_with_creds(cred) + f = open_exec(path); + if (IS_ERR(f)) + pr_notice("register: failed to install interpreter %s\n", path); + return f; +} + +/* Release the interpreters an entry was registered with. */ +static void entry_put_interpreters(struct binfmt_misc_entry *e) +{ + struct binfmt_misc_interp *interp, *tmp; + + list_for_each_entry_safe(interp, tmp, &e->interps, list) { + list_del(&interp->list); + close_interp_file(interp->file); + kfree(interp); + } +} + +/** + * entry_attach_interpreter - bind an opened interpreter to @e + * @e: entry being configured + * @name: name a load program can select it by; empty for the fixed + * interpreter of a static entry + * @path: the path @f was opened from + * @f: the interpreter, opened for execution + * + * Every exec runs a clone of @f, so the path decided which file is bound + * and nothing else: it is not resolved again, in any namespace. + * + * The caller has to have established that @e cannot be matched yet, and + * owns @f until this succeeds. + * + * Return: 0 on success, a negative errno on failure + */ +static int entry_attach_interpreter(struct binfmt_misc_entry *e, + const char *name, const char *path, + struct file *f) +{ + size_t nlen = strlen(name), plen = strlen(path); + struct binfmt_misc_interp *interp; + + /* One allocation, both strings in it, like the entry's own buffer. */ + interp = kmalloc(struct_size(interp, name, nlen + plen + 2), + GFP_KERNEL_ACCOUNT); + if (!interp) + return -ENOMEM; + + interp->path = interp->name + nlen + 1; + strscpy(interp->name, name, nlen + 1); + strscpy(interp->name + nlen + 1, path, plen + 1); + interp->file = f; + list_add_tail(&interp->list, &e->interps); + pr_debug("register: interpreter: %s {%s}\n", name, path); + return 0; +} + static void bm_entry_free_rcu(struct rcu_head *rcu) { struct binfmt_misc_entry *e = container_of(rcu, struct binfmt_misc_entry, rcu); @@ -249,21 +344,22 @@ static void bm_entry_free_rcu(struct rcu_head *rcu) * * Free entry syncing with load_misc_binary() and defer final free to * load_misc_binary() in case it is using the binary type handler we were - * requested to remove. + * requested to remove. Also the teardown for a registration that fails + * before add_entry() publishes the entry. */ static void put_binfmt_handler(struct binfmt_misc_entry *e) { + if (IS_ERR_OR_NULL(e)) + return; + if (refcount_dec_and_test(&e->users)) { - if (e->flags & MISC_FMT_OPEN_FILE) { - exe_file_allow_write_access(e->interp_file); - filp_close(e->interp_file, NULL); - } + entry_put_interpreters(e); /* Walkers may still dereference this entry, even sleeping. */ call_srcu(&bm_entries_srcu, &e->rcu, bm_entry_free_rcu); } } -DEFINE_FREE(put_binfmt_handler, struct binfmt_misc_entry *, if (_T) put_binfmt_handler(_T)) +DEFINE_FREE(put_binfmt_handler, struct binfmt_misc_entry *, put_binfmt_handler(_T)) /** * current_binfmt_misc - get the binfmt_misc instance of the caller's user namespace @@ -392,12 +488,15 @@ static struct file *entry_open_interpreter(const struct binfmt_misc_entry *e, const char *interpreter) { struct file *interp_file __free(fput) = NULL; + struct binfmt_misc_interp *interp; int retval; if (!(e->flags & MISC_FMT_OPEN_FILE)) return open_exec(interpreter); - interp_file = file_clone_open(e->interp_file); + /* An 'F' entry pre-opened exactly one interpreter. */ + interp = list_first_entry(&e->interps, struct binfmt_misc_interp, list); + interp_file = file_clone_open(interp->file); if (IS_ERR(interp_file)) return interp_file; @@ -718,6 +817,7 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, p = buf = e->buf; memset(e, 0, sizeof(*e)); + INIT_LIST_HEAD(&e->interps); if (copy_from_user(buf, buffer, count)) return ERR_PTR(-EFAULT); @@ -842,6 +942,8 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, e->interpreter[0] != '/') return ERR_PTR(-EINVAL); + /* Born holding one reference; put_binfmt_handler() is the teardown. */ + refcount_set(&e->users, 1); return no_free_ptr(e); } @@ -1167,7 +1269,6 @@ static int add_entry(struct binfmt_misc_entry *e, struct super_block *sb) return -ENOMEM; } - refcount_set(&e->users, 1); e->dentry = dentry; inode->i_private = e; inode->i_fop = &bm_entry_operations; @@ -1187,9 +1288,8 @@ static int add_entry(struct binfmt_misc_entry *e, struct super_block *sb) static ssize_t bm_register_write(struct file *file, const char __user *buffer, size_t count, loff_t *ppos) { - struct binfmt_misc_entry *e __free(kfree) = NULL; + struct binfmt_misc_entry *e __free(put_binfmt_handler) = NULL; struct super_block *sb = file_inode(file)->i_sb; - struct file *f = NULL; int err; e = create_entry(buffer, count); @@ -1206,33 +1306,20 @@ static ssize_t bm_register_write(struct file *file, const char __user *buffer, } if (e->flags & MISC_FMT_OPEN_FILE) { - /* - * Now that we support unprivileged binfmt_misc mounts make - * sure we use the credentials that the register @file was - * opened with to also open the interpreter. Before that this - * didn't matter much as only a privileged process could open - * the register file. - */ - scoped_with_creds(file->f_cred) - f = open_exec(e->interpreter); - if (IS_ERR(f)) { - pr_notice("register: failed to install interpreter file %s\n", - e->interpreter); + struct file *f = open_interp_file(file->f_cred, e->interpreter); + + if (IS_ERR(f)) return PTR_ERR(f); + err = entry_attach_interpreter(e, "", e->interpreter, f); + if (err) { + close_interp_file(f); + return err; } - e->interp_file = f; } err = add_entry(e, sb); - if (err) { - if (f) { - exe_file_allow_write_access(f); - filp_close(f, NULL); - } - if (e->bpf_ops) - binfmt_misc_put_ops(e->bpf_ops); + if (err) return err; - } /* The entry is owned by its inode now. */ retain_and_null_ptr(e); From 6ec7c96bee30a7d9f3982951aa19f712feb14f6a Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Thu, 30 Jul 2026 15:34:09 +0200 Subject: [PATCH 58/63] binfmt_misc: let a 'B' entry bind its interpreters A 'B' entry's load program selects its interpreter by absolute path, which open_exec() resolves at exec time in the mount namespace of whoever runs the binary. The handler names an interpreter but does not get to say which file that is. Whoever controls the filesystem view of the exec decides that instead. Static entries settled this long ago with 'F'. The interpreter is opened at registration in the registrant's context and every exec runs a clone of that file. Give a 'B' entry the same, for as many interpreters as it needs. An entry registered with 'D' cannot be matched yet, so it still belongs to whoever is configuring it and can be given interpreters one write at a time: echo ':qemu:B::::qemu_user:D' > register echo '+aarch64 /usr/bin/qemu-aarch64' > qemu echo '+arm /usr/bin/qemu-arm' > qemu echo 1 > qemu Each path is opened by its write, with the credentials the entry file was opened with, by the same helper that opens an 'F' interpreter. The load program picks one per exec with bpf_binprm_select_interp() and the entry hands out a clone of it. Nothing is resolved again, in any namespace. The path is everything past the first space, so no interpreter has to fit in a register string. An entry binds at most a hundred interpreters (BINFMT_MISC_INTERP_MAX). Every binding pins a struct file that no file descriptor accounts for, so RLIMIT_NOFILE does not apply and some cap is needed. A hundred is plenty and raising it later is cheap, lowering it is not. Selection is by name so the register string and the program need not agree on an order, and so the handler is not tied to where a distribution puts its interpreters. A name is a single word of printable ASCII so the entry file can report 'name path' lines. The interpreter runs under the path it was registered under. The entry file reads user memory once. bm_entry_write() copies the write in and dispatches on the first byte, and parse_command() takes the copied buffer. The status file has no binding to spell, so it keeps its own small copy in read_command(). That moves the length cap ahead of the dispatch. A write to an entry file longer than a binding can be is now refused with -E2BIG, and one from a bad address reports -EFAULT, where the command parser used to report -EINVAL for anything past three bytes. Configurations of one instance are kept apart by the lock removal already takes. Reading the set out of the entry file takes no lock. Bindings are rcu-published and the open entry file pins the entry together with everything it bound, so a reader either sees a whole node or misses it. The interpreter is opened before the configuration lock because resolving the path may walk this very filesystem, and only after the command has been parsed and the name validated from the copied buffer, so a write that can never bind opens nothing and the errno reflects the actual failure. Link: https://patch.msgid.link/20260730-work-binfmt_misc-preopen-v1-7-4a0b0da71f16@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 266 +++++++++++++++++++++++++++++------- fs/binfmt_misc_bpf.c | 75 +++++++++- fs/exec.c | 2 + include/linux/binfmt_misc.h | 39 +++++- include/linux/binfmts.h | 3 + 5 files changed, 326 insertions(+), 59 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index afd8a737c95b..ad8c4f64bf10 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -99,24 +100,6 @@ static const struct binfmt_misc_flag *misc_flag_by_char(const char c) return NULL; } -/** - * struct binfmt_misc_interp - an interpreter an entry was registered with - * @list: link in the entry's list, in registration order - * @file: the file, opened at registration and never resolved again - * @path: the path it was registered under, used as the name the interpreter - * runs under; stored after @name in the same allocation - * @name: the name a load program selects it by; empty for the fixed - * interpreter of a static 'F' entry - * - * Owned by the entry and living exactly as long as it does. - */ -struct binfmt_misc_interp { - struct list_head list; - struct file *file; - const char *path; - char name[]; -}; - struct binfmt_misc_entry { struct hlist_node node; unsigned long flags; /* type, status, etc. */ @@ -252,6 +235,24 @@ static struct binfmt_misc_entry *get_binfmt_handler(struct binfmt_misc *misc, return search_binfmt_handler(misc, bprm); } +/** + * binfmt_misc_find_interp - find a bound interpreter by name + * @interps: the interpreters the matched entry was registered with + * @name: the name to look for + * + * Return: the interpreter on success, NULL if @interps has none by that name + */ +const struct binfmt_misc_interp * +binfmt_misc_find_interp(const struct list_head *interps, const char *name) +{ + struct binfmt_misc_interp *interp; + + list_for_each_entry(interp, interps, list) + if (!strcmp(interp->name, name)) + return interp; + return NULL; +} + /* Undo the open_exec() a pre-opened interpreter file came from. */ static void close_interp_file(struct file *f) { @@ -261,6 +262,8 @@ static void close_interp_file(struct file *f) filp_close(f, NULL); } +DEFINE_FREE(close_interp_file, struct file *, close_interp_file(_T)) + /* * Open an interpreter @path for execution: now, in the writer's context, * and - since binfmt_misc mounts can be unprivileged - with @cred, the @@ -293,7 +296,7 @@ static void entry_put_interpreters(struct binfmt_misc_entry *e) /** * entry_attach_interpreter - bind an opened interpreter to @e * @e: entry being configured - * @name: name a load program can select it by; empty for the fixed + * @name: name the load program will select it by; empty for the fixed * interpreter of a static entry * @path: the path @f was opened from * @f: the interpreter, opened for execution @@ -301,8 +304,8 @@ static void entry_put_interpreters(struct binfmt_misc_entry *e) * Every exec runs a clone of @f, so the path decided which file is bound * and nothing else: it is not resolved again, in any namespace. * - * The caller has to have established that @e cannot be matched yet, and - * owns @f until this succeeds. + * The caller has to have validated @name and @path, established that @e + * cannot be matched yet, and owns @f until this succeeds. * * Return: 0 on success, a negative errno on failure */ @@ -313,6 +316,11 @@ static int entry_attach_interpreter(struct binfmt_misc_entry *e, size_t nlen = strlen(name), plen = strlen(path); struct binfmt_misc_interp *interp; + if (binfmt_misc_find_interp(&e->interps, name)) + return -EEXIST; + if (list_count_nodes(&e->interps) >= BINFMT_MISC_INTERP_MAX) + return -ENOSPC; + /* One allocation, both strings in it, like the entry's own buffer. */ interp = kmalloc(struct_size(interp, name, nlen + plen + 2), GFP_KERNEL_ACCOUNT); @@ -323,7 +331,8 @@ static int entry_attach_interpreter(struct binfmt_misc_entry *e, strscpy(interp->name, name, nlen + 1); strscpy(interp->name + nlen + 1, path, plen + 1); interp->file = f; - list_add_tail(&interp->list, &e->interps); + /* Publish the node: a lockless cat may be walking the list. */ + list_add_tail_rcu(&interp->list, &e->interps); pr_debug("register: interpreter: %s {%s}\n", name, path); return 0; } @@ -361,6 +370,20 @@ static void put_binfmt_handler(struct binfmt_misc_entry *e) DEFINE_FREE(put_binfmt_handler, struct binfmt_misc_entry *, put_binfmt_handler(_T)) +/* Drop everything a load program staged for this exec. */ +static void drop_staged_selection(struct linux_binprm *bprm) +{ + kfree(bprm->bpf_interp); + bprm->bpf_interp = NULL; + kfree(bprm->bpf_interp_arg); + bprm->bpf_interp_arg = NULL; + if (bprm->bpf_interp_file) { + fput(bprm->bpf_interp_file); + bprm->bpf_interp_file = NULL; + } + bprm->bpf_flags = 0; +} + /** * current_binfmt_misc - get the binfmt_misc instance of the caller's user namespace * @@ -394,7 +417,8 @@ static struct binfmt_misc *current_binfmt_misc(void) * @bprm: binary that is being executed * * A static entry carries its interpreter path, for a 'B' entry the - * handler's load program selects it. The match is committed, so a failing + * handler's load program selects it, either by path or by the name of one + * of the interpreters the entry bound. The match is committed, so a failing * program fails the exec. * * Return: the interpreter on success, an ERR_PTR on failure @@ -404,15 +428,20 @@ static const char *entry_select_interpreter(const struct binfmt_misc_entry *e, { int retval; + /* + * Drop what a previous chain level staged before anything can pick it + * up. A static entry stages nothing but consumes a staged file just + * like a 'B' entry does. + */ + drop_staged_selection(bprm); + if (!test_bit(MISC_FMT_BPF_BIT, &e->flags)) return e->interpreter; - /* Drop any interpreter or flags a previous chain level staged. */ - kfree(bprm->bpf_interp); - bprm->bpf_interp = NULL; - bprm->bpf_flags = 0; - + /* The interpreters this entry lets the program choose from. */ + bprm->bpf_interps = &e->interps; retval = e->bpf_ops->load(bprm); + bprm->bpf_interps = NULL; if (retval) { /* Keep a program-supplied error within errno range. */ if (retval > 0 || retval < -MAX_ERRNO) @@ -430,9 +459,7 @@ static const char *entry_select_interpreter(const struct binfmt_misc_entry *e, drop_staged: /* A failing load leaves nothing behind for later entries. */ - kfree(bprm->bpf_interp_arg); - bprm->bpf_interp_arg = NULL; - bprm->bpf_flags = 0; + drop_staged_selection(bprm); return ERR_PTR(retval); } @@ -477,26 +504,36 @@ static unsigned long entry_invocation_flags(const struct binfmt_misc_entry *e, /** * entry_open_interpreter - open the entry's interpreter for execution * @e: matched binary type handler + * @bprm: binary that is being executed * @interpreter: the interpreter selected for this exec * * An 'F' entry hands out a clone of the file it pre-opened at registration, - * any other entry opens the selected path. + * and so does a 'B' entry whose load program selected one of the + * interpreters it bound. Any other entry opens the selected path. * * Return: the opened interpreter on success, an ERR_PTR on failure */ static struct file *entry_open_interpreter(const struct binfmt_misc_entry *e, + struct linux_binprm *bprm, const char *interpreter) { struct file *interp_file __free(fput) = NULL; struct binfmt_misc_interp *interp; + struct file *bound; int retval; - if (!(e->flags & MISC_FMT_OPEN_FILE)) + if (bprm->bpf_interp_file) { + bound = bprm->bpf_interp_file; + } else if (e->flags & MISC_FMT_OPEN_FILE) { + /* An 'F' entry pre-opened exactly one interpreter. */ + interp = list_first_entry(&e->interps, + struct binfmt_misc_interp, list); + bound = interp->file; + } else { return open_exec(interpreter); + } - /* An 'F' entry pre-opened exactly one interpreter. */ - interp = list_first_entry(&e->interps, struct binfmt_misc_interp, list); - interp_file = file_clone_open(interp->file); + interp_file = file_clone_open(bound); if (IS_ERR(interp_file)) return interp_file; @@ -606,7 +643,7 @@ static int load_misc_binary(struct linux_binprm *bprm) * to the real format in the same round. */ if (flags & MISC_FMT_LOADER) { - interp_file = entry_open_interpreter(fmt, interpreter); + interp_file = entry_open_interpreter(fmt, bprm, interpreter); if (IS_ERR(interp_file)) { retval = PTR_ERR(interp_file); /* Declining here would run the binary's own PT_INTERP. */ @@ -628,7 +665,7 @@ static int load_misc_binary(struct linux_binprm *bprm) if (retval < 0) return retval; - interp_file = entry_open_interpreter(fmt, interpreter); + interp_file = entry_open_interpreter(fmt, bprm, interpreter); if (IS_ERR(interp_file)) return PTR_ERR(interp_file); @@ -902,7 +939,7 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, /* * A bpf handler decides the invocation flags per exec with * bpf_binprm_set_flags() rather than fixing them at registration, and - * 'F' (pre-open a fixed interpreter) is meaningless for it, so a 'B' + * the interpreters it binds pre-open what 'F' would have, so a 'B' * entry carries no invocation flags. */ if (test_bit(MISC_FMT_BPF_BIT, &e->flags) && @@ -913,7 +950,8 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer, * 'D' is a directive for this registration rather than a lasting * property, so consume it: the entry is created disabled and stays * out of the search list until '1' is written to its entry file. - * The first enable publishes it, for good. + * Staying out is what leaves it open to being given interpreters; + * the first enable publishes it, for good. */ if (e->flags & MISC_FMT_DISABLED) { e->flags &= ~MISC_FMT_DISABLED; @@ -955,18 +993,17 @@ enum bm_command { BM_CMD_REMOVE, /* "-1" */ }; +/* Longest of the commands above, "-1\n". */ +#define MAX_COMMAND_LENGTH 3 + /* * Parse what userspace wrote to /status or an entry file: '1' enables, * '0' disables and '-1' removes the entry or all entries. */ -static int parse_command(const char __user *buffer, size_t count) +static int parse_command(const char *s, size_t count) { - char s[4]; - - if (count > 3) + if (count > MAX_COMMAND_LENGTH) return -EINVAL; - if (copy_from_user(s, buffer, count)) - return -EFAULT; if (!count) return BM_CMD_IGNORE; if (s[count - 1] == '\n') @@ -980,6 +1017,18 @@ static int parse_command(const char __user *buffer, size_t count) return -EINVAL; } +/* Copy in a command from a file that takes nothing else, and parse it. */ +static int read_command(const char __user *buffer, size_t count) +{ + char s[MAX_COMMAND_LENGTH + 1]; + + if (count > sizeof(s) - 1) + return -EINVAL; + if (copy_from_user(s, buffer, count)) + return -EFAULT; + return parse_command(s, count); +} + /* generic stuff */ /* The root directory's inode; its lock serializes configuring an instance. */ @@ -1003,10 +1052,23 @@ static int bm_entry_show(struct seq_file *m, void *unused) else seq_puts(m, "disabled\n"); - if (test_bit(MISC_FMT_BPF_BIT, &e->flags)) + if (test_bit(MISC_FMT_BPF_BIT, &e->flags)) { + struct binfmt_misc_interp *interp; + seq_printf(m, "bpf %s\n", e->bpf_ops->name); - else + /* + * A staged entry's set can still grow, so every binding is + * rcu-published. The open file pins the entry and with it + * every node, so rcu is for the tearing, not the lifetime. + */ + rcu_read_lock(); + list_for_each_entry_rcu(interp, &e->interps, list) + seq_printf(m, "bpf-interpreter %s %s\n", + interp->name, interp->path); + rcu_read_unlock(); + } else { seq_printf(m, "interpreter %s\n", e->interpreter); + } /* print the special flags */ seq_puts(m, "flags: "); @@ -1201,12 +1263,111 @@ static int bm_entry_open(struct inode *inode, struct file *file) return 0; } +/* + * Longest '+ ' a write can spell, and with it the longest + * command an entry file takes: the two delimiters and a newline on top of + * the two names. + */ +#define MAX_BINDING_LENGTH (BINFMT_MISC_INTERP_NAME_MAX + PATH_MAX + 3) + +/** + * bm_entry_add_interp - bind another interpreter to a staged entry + * @e: the entry + * @file: the entry file being written to, for its credentials + * @buf: the '+ ' command, parsed in place and owned by the caller + * @count: its length + * + * A 'D' entry is registered outside the search list, which is what leaves + * it open to being configured: it cannot be matched, so no exec can be + * holding its interpreters and the set can still grow. Its first enable + * publishes it and ends that. One interpreter per write, up to + * BINFMT_MISC_INTERP_MAX of them, none of which has to fit in a register + * string. + * + * Return: @count on success, a negative errno on failure + */ +static ssize_t bm_entry_add_interp(struct binfmt_misc_entry *e, + struct file *file, char *buf, size_t count) +{ + struct file *f __free(close_interp_file) = NULL; + struct inode *root = bm_root_inode(file_inode(file)->i_sb); + size_t nlen, plen; + char *name, *path; + int retval; + + /* Settled before the open: type is fixed, publication is permanent. */ + if (!test_bit(MISC_FMT_BPF_BIT, &e->flags)) + return -EINVAL; + if (!hlist_unhashed_lockless(&e->node)) + return -EBUSY; + + /* '+ ': the path is everything past the first space. */ + name = buf + 1; + path = strchr(name, ' '); + if (!path) + return -EINVAL; + *path++ = '\0'; + + plen = strlen(path); + /* The command has to end at the write, like a register string. */ + if (path + plen != buf + count) + return -EINVAL; + if (plen && path[plen - 1] == '\n') + path[--plen] = '\0'; + /* Resolved now, so a relative path would name the writer's cwd. */ + if (path[0] != '/') + return -EINVAL; + + nlen = path - name - 1; + if (!nlen || nlen > BINFMT_MISC_INTERP_NAME_MAX) + return -EINVAL; + /* The name prints between delimiters, so keep it a printable word. */ + for (const char *p = name; *p; p++) + if (!isascii(*p) || !isgraph(*p)) + return -EINVAL; + + /* Opened before the lock: resolving it may walk this very filesystem. */ + f = open_interp_file(file->f_cred, path); + if (IS_ERR(f)) + return PTR_ERR(f); + + inode_lock(root); + if (d_unhashed(e->dentry)) + retval = -ENOENT; /* removed while we were opening it */ + else if (!hlist_unhashed(&e->node)) + retval = -EBUSY; /* published while we were opening it */ + else + retval = entry_attach_interpreter(e, name, path, f); + inode_unlock(root); + if (retval) + return retval; + + /* The file is owned by the entry now. */ + retain_and_null_ptr(f); + return count; +} + static ssize_t bm_entry_write(struct file *file, const char __user *buffer, size_t count, loff_t *ppos) { struct inode *inode = file_inode(file); struct binfmt_misc_entry *e = inode->i_private; - int res = parse_command(buffer, count); + char *buf __free(kfree) = NULL; + int res; + + /* A binding is the longest command this file takes. */ + if (count > MAX_BINDING_LENGTH) + return -E2BIG; + + buf = memdup_user_nul(buffer, count); + if (IS_ERR(buf)) + return PTR_ERR(buf); + + /* '+ ' binds an interpreter, everything else toggles. */ + if (buf[0] == '+') + return bm_entry_add_interp(e, file, buf, count); + + res = parse_command(buf, count); switch (res) { case BM_CMD_DISABLE: @@ -1218,8 +1379,9 @@ static ssize_t bm_entry_write(struct file *file, const char __user *buffer, /* * The first enable publishes a 'D' entry into the search * list, whole. The lock keeps that ordered against a second - * enable and against removal; a removed entry has nothing - * left to publish. + * enable, against removal - a removed entry has nothing left + * to publish - and against binding: what can be matched can + * no longer be configured. */ inode_lock(root); set_bit(MISC_FMT_ENABLED_BIT, &e->flags); @@ -1348,7 +1510,7 @@ static ssize_t bm_status_write(struct file *file, const char __user *buffer, size_t count, loff_t *ppos) { struct binfmt_misc *misc; - int res = parse_command(buffer, count); + int res = read_command(buffer, count); misc = i_binfmt_misc(file_inode(file)); switch (res) { diff --git a/fs/binfmt_misc_bpf.c b/fs/binfmt_misc_bpf.c index 5bf0e46b867c..91576ff05911 100644 --- a/fs/binfmt_misc_bpf.c +++ b/fs/binfmt_misc_bpf.c @@ -7,6 +7,15 @@ * namespace it was registered in. A binfmt_misc 'B' entry activates it: * * echo ':entry:B:::::' > /register + * + * The entry can bind the interpreters the handler may run its binaries + * with, each opened by the write that binds it and selected by name per + * exec. An entry registered with 'D' is not matchable yet, which is what + * leaves it open to being given them: + * + * echo ':entry:B:::::D' > /register + * echo '+ ' > /entry + * echo 1 > /entry */ #include @@ -16,6 +25,8 @@ #include #include #include +#include +#include #include #include #include @@ -88,6 +99,20 @@ bool bpf_prog_is_binfmt_misc_ops(const struct bpf_prog *prog) prog->aux->st_ops == &bpf_binfmt_misc_ops; } +/* + * Replace the staged interpreter selection: naming a path drops a bound + * file, selecting a bound interpreter carries its file along. + */ +static void bm_bpf_stage_selection(struct linux_binprm *bprm, char *path, + struct file *f) +{ + if (bprm->bpf_interp_file) + fput(bprm->bpf_interp_file); + kfree(bprm->bpf_interp); + bprm->bpf_interp = path; + bprm->bpf_interp_file = f; +} + __bpf_kfunc_start_defs(); /** @@ -100,7 +125,8 @@ __bpf_kfunc_start_defs(); * before returning zero; the verifier rejects the call from any other * program, including the handler's own match program. The path is opened * with the credentials of the task doing the exec after the program - * returns. + * returns. Calling it again replaces the selection, as does selecting an + * interpreter the entry bound with bpf_binprm_select_interp(). * * Return: 0 on success, a negative errno on failure */ @@ -124,8 +150,50 @@ __bpf_kfunc int bpf_binprm_set_interp(struct linux_binprm *bprm, if (!interp) return -ENOMEM; - kfree(bprm->bpf_interp); - bprm->bpf_interp = interp; + bm_bpf_stage_selection(bprm, interp, NULL); + return 0; +} + +/** + * bpf_binprm_select_interp - run this exec under an interpreter the entry bound + * @bprm: binary that is being executed + * @name: name the interpreter was registered under + * @name__sz: size of the @name buffer, including the terminating NUL + * + * To be called from the load program of a struct binfmt_misc_ops handler + * instead of bpf_binprm_set_interp(). It selects one of the interpreters + * the matched entry was registered with, each of which was opened once when + * the entry was registered. Nothing is resolved at exec time, so no + * filesystem view can redirect the interpreter. + * + * The interpreter runs under the path the entry registered it under. + * Calling it again replaces the selection. + * + * Return: 0 on success, -ENOENT if the matched entry bound no interpreter + * of that name, a negative errno on failure + */ +__bpf_kfunc int bpf_binprm_select_interp(struct linux_binprm *bprm, + const char *name, size_t name__sz) +{ + const struct binfmt_misc_interp *interp; + size_t len; + char *path; + + if (!name__sz) + return -EINVAL; + len = strnlen(name, name__sz); + if (len == name__sz || !len) + return -EINVAL; + + interp = binfmt_misc_find_interp(bprm->bpf_interps, name); + if (!interp) + return -ENOENT; + + path = kstrdup(interp->path, GFP_KERNEL); + if (!path) + return -ENOMEM; + + bm_bpf_stage_selection(bprm, path, get_file(interp->file)); return 0; } @@ -208,6 +276,7 @@ __bpf_kfunc_end_defs(); BTF_KFUNCS_START(bm_bpf_kfunc_ids) BTF_ID_FLAGS(func, bpf_binprm_set_interp, KF_SLEEPABLE) +BTF_ID_FLAGS(func, bpf_binprm_select_interp, KF_SLEEPABLE) BTF_ID_FLAGS(func, bpf_binprm_set_interp_arg, KF_SLEEPABLE) BTF_ID_FLAGS(func, bpf_binprm_set_flags, KF_SLEEPABLE) BTF_KFUNCS_END(bm_bpf_kfunc_ids) diff --git a/fs/exec.c b/fs/exec.c index 856731f78d05..a14f28b15607 100644 --- a/fs/exec.c +++ b/fs/exec.c @@ -1477,6 +1477,8 @@ static void free_bprm(struct linux_binprm *bprm) if (bprm->interp != bprm->filename) kfree(bprm->interp); kfree(bprm->bpf_interp); + if (bprm->bpf_interp_file) + fput(bprm->bpf_interp_file); kfree(bprm->bpf_interp_arg); kfree(bprm->fdpath); kfree(bprm); diff --git a/include/linux/binfmt_misc.h b/include/linux/binfmt_misc.h index 4abdfd36b3fa..072e4b3dd78d 100644 --- a/include/linux/binfmt_misc.h +++ b/include/linux/binfmt_misc.h @@ -5,11 +5,41 @@ #include struct bpf_prog; +struct file; struct linux_binprm; struct user_namespace; #define BINFMT_MISC_OPS_NAME_MAX 16 +/* Longest name a 'B' entry can bind an interpreter under. */ +#define BINFMT_MISC_INTERP_NAME_MAX 32 + +/* Most interpreters one entry can bind. */ +#define BINFMT_MISC_INTERP_MAX 100 + +/** + * struct binfmt_misc_interp - an interpreter an entry was registered with + * @list: link in the entry's list, in registration order + * @file: the file, opened at registration and never resolved again + * @path: the path it was registered under, used as the name the interpreter + * runs under; stored after @name in the same allocation + * @name: the name the load program selects it by; empty for the fixed + * interpreter of a static 'F' entry + * + * Owned by the entry and living exactly as long as it does. The list head + * is handed to the handler's load program for the duration of one exec, + * which picks one with bpf_binprm_select_interp(). + */ +struct binfmt_misc_interp { + struct list_head list; + struct file *file; + const char *path; + char name[]; +}; + +const struct binfmt_misc_interp * +binfmt_misc_find_interp(const struct list_head *interps, const char *name); + /** * enum bpf_binprm_flags - per-exec invocation flags a load program can request * @BPF_BINPRM_PRESERVE_ARGV0: keep the caller's argv[0] (like the 'P' flag) @@ -42,10 +72,11 @@ enum bpf_binprm_flags { * so it can read the binary to decide, but the verifier rejects * the interpreter selection kfuncs in it * @load: select an interpreter for the matched @bprm via - * bpf_binprm_set_interp() and return zero; a match is committed, so - * a failure fails the exec instead of falling through to later - * entries; -ENOEXEC does not fail the exec but moves on to the - * remaining binary formats + * bpf_binprm_set_interp(), or one the entry bound via + * bpf_binprm_select_interp(), and return zero; a match is + * committed, so a failure fails the exec instead of falling + * through to later entries; -ENOEXEC does not fail the exec but + * moves on to the remaining binary formats * @name: name that 'B' entries reference the handler by */ struct binfmt_misc_ops { diff --git a/include/linux/binfmts.h b/include/linux/binfmts.h index a2daecbb01d6..f686a37f7a0a 100644 --- a/include/linux/binfmts.h +++ b/include/linux/binfmts.h @@ -14,7 +14,10 @@ struct coredump_params; /* Interpreter selection staged by a bpf binfmt_misc handler. */ struct binfmt_misc_bpf { + /* interpreters the matched entry bound, selectable by name */ + const struct list_head *bpf_interps; const char *bpf_interp; /* interpreter selected by a bpf handler */ + struct file *bpf_interp_file; /* the bound interpreter it selected */ const char *bpf_interp_arg; /* interpreter argument from a bpf handler */ u64 bpf_flags; /* enum bpf_binprm_flags from a bpf handler */ }; From 7404b1472b111b62f061fc5e9244aacaa862a1e4 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Thu, 30 Jul 2026 15:34:10 +0200 Subject: [PATCH 59/63] selftests/exec: test interpreters bound to a 'B' entry One handler, one entry registered disabled, an interpreter per guest architecture bound to a file one write at a time. The load program picks one by name per exec: - an aarch64 binary runs the interpreter bound as "first" and a riscv one the interpreter bound as "second", from a single entry and a single handler - unlinking a bound interpreter and putting a different binary in its place changes nothing, which is what the binding exists for - the entry reports what it bound, under the names it bound them as - a name the entry did not bind fails the exec with -ENOENT rather than falling back to anything - activating the entry refuses further binding with -EBUSY, a later disable does not undo that, and an entry registered without 'D' never accepted a '+' write to begin with - a name binds one interpreter, and control characters are refused - the command has to end at the write, bytes past an embedded nul are refused - an entry binds at most 100 interpreters, the next one is refused with -ENOSPC The test interpreter prints its argv[0], which is the path the kernel ran that copy under, so one binary installed at two paths tells the harness which of them the program picked. Link: https://patch.msgid.link/20260730-work-binfmt_misc-preopen-v1-8-4a0b0da71f16@kernel.org Signed-off-by: Christian Brauner (Amutable) --- tools/testing/selftests/exec/Makefile | 7 +- .../selftests/exec/binfmt_bind_interp.c | 14 + .../testing/selftests/exec/binfmt_misc_bpf.c | 261 +++++++++++++++++- .../testing/selftests/exec/interp_bind.bpf.c | 76 +++++ 4 files changed, 349 insertions(+), 9 deletions(-) create mode 100644 tools/testing/selftests/exec/binfmt_bind_interp.c create mode 100644 tools/testing/selftests/exec/interp_bind.bpf.c diff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile index ec7894a802e0..410c93606a0c 100644 --- a/tools/testing/selftests/exec/Makefile +++ b/tools/testing/selftests/exec/Makefile @@ -56,8 +56,8 @@ HAVE_BPF_TOOLCHAIN ?= $(shell command -v $(CLANG) >/dev/null 2>&1 && \ ifeq ($(HAVE_BPF_TOOLCHAIN),y) TEST_GEN_PROGS += binfmt_misc_bpf TEST_GEN_FILES += bpf_interp.bpf.o nix_origin.bpf.o transparent.bpf.o -TEST_GEN_FILES += loader.bpf.o -TEST_GEN_FILES += binfmt_bpf_interp binfmt_bpf_app +TEST_GEN_FILES += loader.bpf.o interp_bind.bpf.o +TEST_GEN_FILES += binfmt_bpf_interp binfmt_bpf_app binfmt_bind_interp else $(info exec selftests: skipping binfmt_misc_bpf, needs clang, bpftool, vmlinux BTF and libbpf) endif @@ -127,6 +127,9 @@ $(OUTPUT)/binfmt_misc_bpf: binfmt_misc_bpf.c binfmt_misc_common.h $(OUTPUT)/binfmt_bpf_interp: binfmt_bpf_interp.c $(CC) $(CFLAGS) $(LDFLAGS) $< -o $@ +$(OUTPUT)/binfmt_bind_interp: binfmt_bind_interp.c + $(CC) $(CFLAGS) $(LDFLAGS) $< -o $@ + $(OUTPUT)/binfmt_loader_payload: binfmt_loader_payload.c binfmt_misc_common.h $(CC) $(CFLAGS) $(LDFLAGS) -fPIE -pie $< -o $@ diff --git a/tools/testing/selftests/exec/binfmt_bind_interp.c b/tools/testing/selftests/exec/binfmt_bind_interp.c new file mode 100644 index 000000000000..06d65062856b --- /dev/null +++ b/tools/testing/selftests/exec/binfmt_bind_interp.c @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Test interpreter for the bound-interpreter case of the binfmt_misc_bpf + * selftest. Two copies are installed at different paths and bound to one + * entry under different names; printing argv[0] - the path the kernel ran + * this copy under - tells the harness which of them the load program picked. + */ +#include + +int main(int argc, char **argv) +{ + printf("BIND_RAN %s\n", argc > 0 ? argv[0] : ""); + return 0; +} diff --git a/tools/testing/selftests/exec/binfmt_misc_bpf.c b/tools/testing/selftests/exec/binfmt_misc_bpf.c index 71bb6d8b4517..2c7b63075f1d 100644 --- a/tools/testing/selftests/exec/binfmt_misc_bpf.c +++ b/tools/testing/selftests/exec/binfmt_misc_bpf.c @@ -9,7 +9,7 @@ * * echo ':name:B:::::' > /proc/sys/fs/binfmt_misc/register * - * Three self-contained cases are exercised: + * Five self-contained cases are exercised: * * 1. bpf_interp: the match program matches a synthetic aarch64 ELF header * from the prefetched bprm->buf and the load program routes it to a @@ -26,6 +26,11 @@ * (binfmt_loader_payload) runs as the main image with the selected * interpreter substituted for its PT_INTERP and asserts the native * identity from inside. + * 5. interp_bind: an entry registered disabled with 'D' is given its + * interpreters one write at a time, and the load program picks one by + * name per exec. Replacing what the path holds afterwards changes + * nothing, which is the point of binding a file rather than resolving + * a name at exec time. Enabling the entry seals it. * * The first two route to a test interpreter that prints BPF_INTERP_RAN, * proving the program's chosen interpreter actually ran. @@ -54,6 +59,12 @@ #define TRANS_EXPECT "TRANSPARENT_OK" #define LOADER_INTERP "/tmp/binfmt_loader_interp" #define LOADER_PATH "/tmp/binfmt_bpf_loader.ldrtest" +#define BIND_FIRST "/tmp/binfmt_bind_first" +#define BIND_SECOND "/tmp/binfmt_bind_second" +#define BIND_ARM_PATH "/tmp/binfmt_bind_arm" +#define BIND_RISCV_PATH "/tmp/binfmt_bind_riscv" +#define BIND_EXPECT "BIND_RAN " +#define BIND_MAX 100 /* A minimal 64-bit little-endian ELF header, padded to the read size. */ static int create_fake_elf(const char *path, unsigned short machine) @@ -82,11 +93,17 @@ static int create_fake_elf(const char *path, unsigned short machine) return 0; } -static int register_entry(const char *name, const char *handler) +/* + * Register a 'B' entry for @handler. With @flags "D" the entry is created + * disabled, which is what leaves it open to being given interpreters. + */ +static int register_entry(const char *name, const char *handler, + const char *flags) { char rule[PATH_MAX]; - snprintf(rule, sizeof(rule), ":%s:B::::%s:", name, handler); + snprintf(rule, sizeof(rule), ":%s:B::::%s:%s", name, handler, + flags ? flags : ""); return write_reg(rule); } @@ -139,10 +156,12 @@ struct bpf_case { /* * Load @objfile, attach its struct_ops map @handler (which publishes the - * handler) and activate a 'B' entry named @entry that references it. + * handler) and register a 'B' entry named @entry that references it, with + * @flags as the entry's register-string flags. */ -static int bpf_case_start(struct bpf_case *c, const char *objfile, - const char *handler, const char *entry) +static int bpf_case_start_flags(struct bpf_case *c, const char *objfile, + const char *handler, const char *entry, + const char *flags) { struct bpf_map *map; @@ -172,7 +191,7 @@ static int bpf_case_start(struct bpf_case *c, const char *objfile, c->link = NULL; goto fail; } - if (register_entry(entry, handler)) { + if (register_entry(entry, handler, flags)) { fprintf(stderr, "register 'B' entry '%s' failed\n", entry); goto fail; } @@ -186,6 +205,12 @@ static int bpf_case_start(struct bpf_case *c, const char *objfile, return -1; } +static int bpf_case_start(struct bpf_case *c, const char *objfile, + const char *handler, const char *entry) +{ + return bpf_case_start_flags(c, objfile, handler, entry, NULL); +} + static void bpf_case_stop(struct bpf_case *c) { unregister(c->entry); @@ -318,4 +343,226 @@ TEST_F(bpf_handler, loader_substitution) unlink(LOADER_INTERP); } +/* The errno an exec of @path fails with, 0 if it succeeded. */ +static int exec_errno(const char *path) +{ + int status; + pid_t pid; + + pid = fork(); + if (pid == 0) { + execl(path, path, (char *)NULL); + _exit(errno); + } + if (pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status)) + return -1; + return WEXITSTATUS(status); +} + +/* Install a copy of the bound-interpreter test binary at @path. */ +static int install_interp(const char *path) +{ + char src[PATH_MAX]; + + if (artifact_path(src, sizeof(src), "binfmt_bind_interp")) + return -1; + return copy_file(src, path); +} + +/* Bind @path to @entry under @name, the '+' command of a disabled entry. */ +static int entry_bind(const char *entry, const char *name, const char *path) +{ + char cmd[PATH_MAX]; + + snprintf(cmd, sizeof(cmd), "+%s %s\n", name, path); + return entry_command(entry, cmd); +} + +FIXTURE(bound_interp) { + char obj[PATH_MAX]; + struct bpf_case c; + bool started; +}; + +FIXTURE_SETUP(bound_interp) +{ + const char *why = bpf_handler_unsupported(); + + if (why) + SKIP(return, "%s", why); + if (!binfmt_flag_supported('D')) { + ASSERT_EQ(errno, EINVAL); + SKIP(return, "kernel without the 'D' flag"); + } + + ASSERT_EQ(install_interp(BIND_FIRST), 0); + ASSERT_EQ(install_interp(BIND_SECOND), 0); + + ASSERT_EQ(artifact_path(self->obj, sizeof(self->obj), + "interp_bind.bpf.o"), 0); + + /* + * Registered disabled, so it cannot be matched yet and can still be + * given interpreters. Each path is resolved once, by its write(2); + * from here on the entry holds the files themselves. + */ + ASSERT_EQ(bpf_case_start_flags(&self->c, self->obj, "interp_bind", + "test_interp_bind", "D"), 0); + self->started = true; + + ASSERT_EQ(entry_bind("test_interp_bind", "first", BIND_FIRST), 0); + ASSERT_EQ(entry_bind("test_interp_bind", "second", BIND_SECOND), 0); +} + +FIXTURE_TEARDOWN(bound_interp) +{ + if (self->started) + bpf_case_stop(&self->c); + unlink(BIND_FIRST); + unlink(BIND_SECOND); + unlink(AARCH64_PATH); + unlink(BIND_RISCV_PATH); + unlink(BIND_ARM_PATH); +} + +/* Enabling is what makes the configured entry matchable. */ +static int activate(const char *entry) +{ + return entry_command(entry, "1\n"); +} + +/* One entry, one interpreter per guest architecture, picked per exec. */ +TEST_F(bound_interp, selects_by_name) +{ + ASSERT_EQ(create_fake_elf(AARCH64_PATH, EM_AARCH64), 0); + ASSERT_EQ(create_fake_elf(BIND_RISCV_PATH, EM_RISCV), 0); + + /* Disabled, so it does not match and no format claims the binary. */ + EXPECT_EQ(exec_errno(AARCH64_PATH), ENOEXEC); + + ASSERT_EQ(activate("test_interp_bind"), 0); + EXPECT_EQ(check_output(AARCH64_PATH, BIND_EXPECT BIND_FIRST), 0); + EXPECT_EQ(check_output(BIND_RISCV_PATH, BIND_EXPECT BIND_SECOND), 0); +} + +/* What was bound is what runs, whatever the path holds afterwards. */ +TEST_F(bound_interp, path_no_longer_decides) +{ + char other[PATH_MAX]; + + ASSERT_EQ(create_fake_elf(AARCH64_PATH, EM_AARCH64), 0); + ASSERT_EQ(activate("test_interp_bind"), 0); + + /* Bound interpreters are pinned against writes, exactly like 'F'. */ + EXPECT_TRUE(write_denied(BIND_FIRST)); + + /* Replace the path with a different binary: a new file, new inode. */ + ASSERT_EQ(artifact_path(other, sizeof(other), "binfmt_bpf_interp"), 0); + ASSERT_EQ(unlink(BIND_FIRST), 0); + ASSERT_EQ(copy_file(other, BIND_FIRST), 0); + + EXPECT_EQ(check_output(AARCH64_PATH, BIND_EXPECT BIND_FIRST), 0); +} + +/* The entry reports what it bound, under the names it bound them as. */ +TEST_F(bound_interp, entry_reports_bindings) +{ + EXPECT_TRUE(entry_shows("test_interp_bind", + "bpf-interpreter first " BIND_FIRST)); + EXPECT_TRUE(entry_shows("test_interp_bind", + "bpf-interpreter second " BIND_SECOND)); +} + +/* Selecting a name the entry did not bind fails the exec. */ +TEST_F(bound_interp, unbound_name_fails) +{ + ASSERT_EQ(create_fake_elf(BIND_ARM_PATH, EM_ARM), 0); + ASSERT_EQ(activate("test_interp_bind"), 0); + + EXPECT_EQ(exec_errno(BIND_ARM_PATH), ENOENT); +} + +/* Activating seals it: what can be matched cannot be changed. */ +TEST_F(bound_interp, sealed_once_active) +{ + ASSERT_EQ(activate("test_interp_bind"), 0); + + EXPECT_EQ(entry_bind("test_interp_bind", "third", BIND_SECOND), -EBUSY); + EXPECT_FALSE(entry_shows("test_interp_bind", + "bpf-interpreter third " BIND_SECOND)); +} + +/* The seal is for good: disabling the entry again reopens nothing. */ +TEST_F(bound_interp, disable_does_not_unseal) +{ + ASSERT_EQ(activate("test_interp_bind"), 0); + ASSERT_EQ(entry_command("test_interp_bind", "0\n"), 0); + + EXPECT_EQ(entry_bind("test_interp_bind", "third", BIND_SECOND), -EBUSY); +} + +/* An entry registered without 'D' is sealed from the start. */ +TEST_F(bound_interp, born_sealed) +{ + /* A second entry for the handler the fixture already published. */ + ASSERT_EQ(register_entry("test_born_sealed", "interp_bind", NULL), 0); + + EXPECT_EQ(entry_bind("test_born_sealed", "first", BIND_FIRST), -EBUSY); + unregister("test_born_sealed"); +} + +/* A name is bound once; a second use of it is refused. */ +TEST_F(bound_interp, duplicate_name_refused) +{ + EXPECT_EQ(entry_bind("test_interp_bind", "first", BIND_SECOND), -EEXIST); +} + +/* A name is a printable word: the entry file reports 'name path' lines. */ +TEST_F(bound_interp, name_must_be_printable) +{ + /* A control character would forge a line into the entry file. */ + EXPECT_EQ(entry_bind("test_interp_bind", "a\tb", BIND_FIRST), -EINVAL); + EXPECT_EQ(entry_bind("test_interp_bind", "a\nb", BIND_FIRST), -EINVAL); + + /* A space cannot even be spelled: the path starts after the first one. */ + EXPECT_EQ(entry_bind("test_interp_bind", "a b", BIND_FIRST), -EINVAL); +} + +/* The command ends at the write: bytes past an embedded nul are refused. */ +TEST_F(bound_interp, trailing_bytes_refused) +{ + char cmd[PATH_MAX]; + size_t len; + int fd; + + /* entry_command() cannot spell a nul, so write the buffer raw. */ + snprintf(cmd, sizeof(cmd), "+nul %s", BIND_FIRST); + len = strlen(cmd) + 1; + memcpy(cmd + len, "junk", sizeof("junk")); + len += sizeof("junk"); + + fd = open(BINFMT_DIR "/test_interp_bind", O_WRONLY | O_CLOEXEC); + ASSERT_GE(fd, 0); + EXPECT_EQ(write(fd, cmd, len), -1); + EXPECT_EQ(errno, EINVAL); + close(fd); + + EXPECT_FALSE(entry_shows("test_interp_bind", + "bpf-interpreter nul " BIND_FIRST)); +} + +/* An entry binds at most BIND_MAX interpreters. */ +TEST_F(bound_interp, capped_bindings) +{ + char name[16]; + int i; + + /* The fixture bound "first" and "second" already. */ + for (i = 2; i < BIND_MAX; i++) { + snprintf(name, sizeof(name), "n%d", i); + ASSERT_EQ(entry_bind("test_interp_bind", name, BIND_FIRST), 0); + } + EXPECT_EQ(entry_bind("test_interp_bind", "over", BIND_FIRST), -ENOSPC); +} + TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/exec/interp_bind.bpf.c b/tools/testing/selftests/exec/interp_bind.bpf.c new file mode 100644 index 000000000000..1ce45cca215f --- /dev/null +++ b/tools/testing/selftests/exec/interp_bind.bpf.c @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * binfmt_misc_ops handler for the selftest's bound-interpreter case: one + * handler, one entry, an interpreter per guest architecture - each bound to + * a file when the entry was registered rather than to a path resolved at + * exec time. The load program names the one it wants; a name the entry did + * not bind fails the exec, which the harness checks too. + */ +#include "vmlinux.h" +#include +#include + +char _license[] SEC("license") = "GPL"; + +#define EI_CLASS 4 +#define ELFCLASS64 2 +#define E_MACHINE_OFF 18 +#define EM_ARM 40 +#define EM_AARCH64 183 +#define EM_RISCV 243 + +extern int bpf_binprm_select_interp(struct linux_binprm *bprm, + const char *name, size_t name__sz) __ksym; + +/* The guest architecture of a 64-bit ELF, or zero if it is not one. */ +static __u16 elf_machine(struct linux_binprm *bprm) +{ + if (bprm->buf[0] != 0x7f || bprm->buf[1] != 'E' || + bprm->buf[2] != 'L' || bprm->buf[3] != 'F' || + bprm->buf[EI_CLASS] != ELFCLASS64) + return 0; + + /* Little-endian 16-bit field, read byte-wise for the verifier. */ + return (__u8)bprm->buf[E_MACHINE_OFF] | + ((__u16)(__u8)bprm->buf[E_MACHINE_OFF + 1] << 8); +} + +SEC("struct_ops.s/match") +bool BPF_PROG(interp_bind_match, struct linux_binprm *bprm) +{ + __u16 machine = elf_machine(bprm); + + return machine == EM_AARCH64 || machine == EM_RISCV || + machine == EM_ARM; +} + +SEC("struct_ops.s/load") +int BPF_PROG(interp_bind_load, struct linux_binprm *bprm) +{ + /* + * Names, not paths: each one selects a file the entry pre-opened, so + * nothing is resolved here or later, in any namespace. The buffers + * are on the stack because the verifier rejects .rodata for a sized + * memory argument. + */ + char first[] = "first"; + char second[] = "second"; + char unbound[] = "unbound"; + + switch (elf_machine(bprm)) { + case EM_AARCH64: + return bpf_binprm_select_interp(bprm, first, sizeof(first)); + case EM_RISCV: + return bpf_binprm_select_interp(bprm, second, sizeof(second)); + } + + /* The entry bound nothing under this name: -ENOENT fails the exec. */ + return bpf_binprm_select_interp(bprm, unbound, sizeof(unbound)); +} + +SEC(".struct_ops.link") +struct binfmt_misc_ops interp_bind = { + .match = (void *)interp_bind_match, + .load = (void *)interp_bind_load, + .name = "interp_bind", +}; From 1646e9927705637e4bb1ca7c768b91c3d721e92c Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Thu, 30 Jul 2026 15:34:11 +0200 Subject: [PATCH 60/63] binfmt_misc: document interpreters bound by a 'B' entry Describe the interpreters a 'B' entry can bind while it is disabled, what binding a file buys over naming a path the exec resolves, how a load program picks one, that an entry binds at most 100 interpreters, and that enabling the entry seals the set. Link: https://patch.msgid.link/20260730-work-binfmt_misc-preopen-v1-9-4a0b0da71f16@kernel.org Signed-off-by: Christian Brauner (Amutable) --- Documentation/admin-guide/binfmt-misc.rst | 66 ++++++++++++++++++++--- 1 file changed, 60 insertions(+), 6 deletions(-) diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst index 22639ed9c73c..622b5d8c8995 100644 --- a/Documentation/admin-guide/binfmt-misc.rst +++ b/Documentation/admin-guide/binfmt-misc.rst @@ -195,9 +195,62 @@ and derive the interpreter from the binary's location. It selects the interpreter by calling the ``bpf_binprm_set_interp()`` kfunc with an absolute path and returning ``0``. A match is committed: a failing ``load`` fails the exec with its error instead of falling through to later -entries; ``-ENOEXEC`` lets the remaining binary formats have a go. The -interpreter is opened with the credentials of the task doing the exec, -exactly as a statically registered interpreter would be. +entries; ``-ENOEXEC`` lets the remaining binary formats have a go. A path +selected this way is opened with the credentials of the task doing the +exec, exactly as a statically registered interpreter without ``F`` would +be. + +An entry can instead bind the interpreters its handler may use, so that no +path is resolved at exec time at all. An entry registered with ``D`` is not +matchable yet, which is what leaves it open to being given them, one +``+name path`` write at a time:: + + echo ':qemu:B::::my_handler:D' > register + echo '+aarch64 /usr/bin/qemu-aarch64' > qemu + echo '+arm /usr/bin/qemu-arm' > qemu + echo 1 > qemu + +Each path is opened during its write, in the writing process's context and +with the credentials the entry file was opened with, exactly the way ``F`` +pre-opens a static entry's interpreter; the paths must be absolute. The +path is everything past the first space, so there is nothing it cannot +express, and no interpreter has to fit in a register string. An entry +binds at most 100 interpreters; a write past that is refused with +``-ENOSPC``. To bind a file that has no path of its own - already +unlinked, a ``memfd``, or reachable only in another mount namespace - +open it and write ``/proc/self/fd/N``. + +The ``load`` program then selects one per exec by name with the +``bpf_binprm_select_interp()`` kfunc, and every exec runs a clone of the +file that was opened. The path decides which file is bound and nothing +else: it is not resolved again, in any namespace, so what it holds later - +or what it holds in the namespace of whoever runs the binary - no longer +decides anything. + +Enabling the entry ends this. Its interpreters are read at exec time with +nothing but a reference held on the entry, so an entry that has ever been +matchable can never have its set changed again: the first ``1`` seals it, +from then on ``+`` is refused with ``-EBUSY``, and an entry registered +without ``D`` is sealed from the start. Binding a name twice is refused +with ``-EEXIST``. + +Selection is by name so that the configuration and the program need not +agree on an order, and so that a handler is not tied to where a distribution +puts its interpreters. A name is a single word of printable ASCII, at most +32 characters; a name the entry did not bind gives the program ``-ENOENT``, +which it can act on or return. The interpreter runs under the path it was +registered under, and the entry reports what it bound:: + + $ cat /proc/sys/fs/binfmt_misc/qemu + enabled + bpf my_handler + bpf-interpreter aarch64 /usr/bin/qemu-aarch64 + bpf-interpreter arm /usr/bin/qemu-arm + flags: + +The path reported is the one the interpreter was bound under, which named +the file at that moment; it is not re-resolved, so it is a record of what +was bound rather than a promise about what that path holds now. The ``load`` program can also pass a single argument to the interpreter with the ``bpf_binprm_set_interp_arg()`` kfunc. It is inserted between the @@ -234,9 +287,10 @@ handler can decide them differently for each binary it handles: flag). It excludes the other flags and a staged interpreter argument. Because these are program choices, a ``B`` entry carries no invocation -flags in the register string; ``F`` (pre-open a fixed interpreter) has no -meaning for it. The registration directive ``D`` is the exception: it -decides how the entry starts out, not how the interpreter is invoked. +flags in the register string; ``F`` has none to spell for it either, since +the interpreters it binds already pre-open what ``F`` would. The +registration directive ``D`` is the exception: it decides how the entry +starts out, not how the interpreter is invoked. A handler is looked up only in the user namespace the struct_ops map was registered in. Handlers are not inherited, so an entry can only reference a From b604897764047229c6a931e1980cac1b2197d9d2 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 3 Aug 2026 14:15:00 +0200 Subject: [PATCH 61/63] binfmt_misc: correctly account pre-opened interpreters An 'F' entry, and every interpreter a 'B' entry binds, holds a file open from registration until the entry goes away, pinning the file, its inode, the mount it came from and that mount's superblock. Nothing bounds how many of those a user namespace can hold. An entry binds at most BINFMT_MISC_INTERP_MAX interpreters, but nothing caps the entries. Charge each binding to the user namespace and uid that makes it against a new UCOUNT_BINFMT_MISC_INTERPRETERS. Going over budget causes -ENOSPC. A per-instance cap would suck. Instances are keyed on the user namespace. So any constant is multiplied by the number of namespaces the caller creates. Creating those is virtually free. A ucount charges the namespace and every one of its ancestors. And a namespace can raise only its own limit. So nesting buys nothing. The knob is /proc/sys/user/max_binfmt_misc_interpreters. Leave it at the max_threads/2 default fork_init() gives a new type. No existing configuration comes close to that. binfmt_misc is tristate, which makes it the first ucount user that can be built as a module. Export inc_ucount() and dec_ucount(); without them CONFIG_BINFMT_MISC=m fails to link. Export them to binfmt_misc alone: charging a ucount type is not something a module has any business doing in general, and the list is trivial to extend if a second user shows up. init_user_ns and init_binfmt_misc are already exported for the same module. Link: https://patch.msgid.link/20260803-work-binfmt_misc-interplimit-v1-1-4a2435500bd9@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/binfmt_misc.c | 16 ++++++++++++++-- include/linux/binfmt_misc.h | 3 +++ include/linux/user_namespace.h | 3 +++ kernel/ucount.c | 6 ++++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/fs/binfmt_misc.c b/fs/binfmt_misc.c index ad8c4f64bf10..a3aa42fd5761 100644 --- a/fs/binfmt_misc.c +++ b/fs/binfmt_misc.c @@ -289,6 +289,7 @@ static void entry_put_interpreters(struct binfmt_misc_entry *e) list_for_each_entry_safe(interp, tmp, &e->interps, list) { list_del(&interp->list); close_interp_file(interp->file); + dec_ucount(interp->ucounts, UCOUNT_BINFMT_MISC_INTERPRETERS); kfree(interp); } } @@ -307,7 +308,8 @@ static void entry_put_interpreters(struct binfmt_misc_entry *e) * The caller has to have validated @name and @path, established that @e * cannot be matched yet, and owns @f until this succeeds. * - * Return: 0 on success, a negative errno on failure + * Return: 0 on success, -ENOSPC if the entry is full or the binder is out of + * UCOUNT_BINFMT_MISC_INTERPRETERS budget, a negative errno on failure */ static int entry_attach_interpreter(struct binfmt_misc_entry *e, const char *name, const char *path, @@ -315,22 +317,32 @@ static int entry_attach_interpreter(struct binfmt_misc_entry *e, { size_t nlen = strlen(name), plen = strlen(path); struct binfmt_misc_interp *interp; + struct ucounts *ucounts; if (binfmt_misc_find_interp(&e->interps, name)) return -EEXIST; if (list_count_nodes(&e->interps) >= BINFMT_MISC_INTERP_MAX) return -ENOSPC; + /* The binding keeps a file open, so charge it to whoever binds it. */ + ucounts = inc_ucount(current_user_ns(), current_euid(), + UCOUNT_BINFMT_MISC_INTERPRETERS); + if (!ucounts) + return -ENOSPC; + /* One allocation, both strings in it, like the entry's own buffer. */ interp = kmalloc(struct_size(interp, name, nlen + plen + 2), GFP_KERNEL_ACCOUNT); - if (!interp) + if (!interp) { + dec_ucount(ucounts, UCOUNT_BINFMT_MISC_INTERPRETERS); return -ENOMEM; + } interp->path = interp->name + nlen + 1; strscpy(interp->name, name, nlen + 1); strscpy(interp->name + nlen + 1, path, plen + 1); interp->file = f; + interp->ucounts = ucounts; /* Publish the node: a lockless cat may be walking the list. */ list_add_tail_rcu(&interp->list, &e->interps); pr_debug("register: interpreter: %s {%s}\n", name, path); diff --git a/include/linux/binfmt_misc.h b/include/linux/binfmt_misc.h index 072e4b3dd78d..8045b10dd3e5 100644 --- a/include/linux/binfmt_misc.h +++ b/include/linux/binfmt_misc.h @@ -7,6 +7,7 @@ struct bpf_prog; struct file; struct linux_binprm; +struct ucounts; struct user_namespace; #define BINFMT_MISC_OPS_NAME_MAX 16 @@ -21,6 +22,7 @@ struct user_namespace; * struct binfmt_misc_interp - an interpreter an entry was registered with * @list: link in the entry's list, in registration order * @file: the file, opened at registration and never resolved again + * @ucounts: the UCOUNT_BINFMT_MISC_INTERPRETERS charge the binding took * @path: the path it was registered under, used as the name the interpreter * runs under; stored after @name in the same allocation * @name: the name the load program selects it by; empty for the fixed @@ -33,6 +35,7 @@ struct user_namespace; struct binfmt_misc_interp { struct list_head list; struct file *file; + struct ucounts *ucounts; const char *path; char name[]; }; diff --git a/include/linux/user_namespace.h b/include/linux/user_namespace.h index 9c3be157397e..e38d9e60569f 100644 --- a/include/linux/user_namespace.h +++ b/include/linux/user_namespace.h @@ -57,6 +57,9 @@ enum ucount_type { #ifdef CONFIG_FANOTIFY UCOUNT_FANOTIFY_GROUPS, UCOUNT_FANOTIFY_MARKS, +#endif +#if IS_ENABLED(CONFIG_BINFMT_MISC) + UCOUNT_BINFMT_MISC_INTERPRETERS, #endif UCOUNT_COUNTS, }; diff --git a/kernel/ucount.c b/kernel/ucount.c index d6dc3e859f12..ec8b1445e287 100644 --- a/kernel/ucount.c +++ b/kernel/ucount.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -89,6 +90,9 @@ static const struct ctl_table user_table[] = { UCOUNT_ENTRY("max_fanotify_groups"), UCOUNT_ENTRY("max_fanotify_marks"), #endif +#if IS_ENABLED(CONFIG_BINFMT_MISC) + UCOUNT_ENTRY("max_binfmt_misc_interpreters"), +#endif }; #endif /* CONFIG_SYSCTL */ @@ -233,6 +237,7 @@ struct ucounts *inc_ucount(struct user_namespace *ns, kuid_t uid, put_ucounts(ucounts); return NULL; } +EXPORT_SYMBOL_FOR_MODULES(inc_ucount, "binfmt_misc"); void dec_ucount(struct ucounts *ucounts, enum ucount_type type) { @@ -243,6 +248,7 @@ void dec_ucount(struct ucounts *ucounts, enum ucount_type type) } put_ucounts(ucounts); } +EXPORT_SYMBOL_FOR_MODULES(dec_ucount, "binfmt_misc"); long inc_rlimit_ucounts(struct ucounts *ucounts, enum rlimit_type type, long v) { From f2b69ea2d1a017f0c8e848ff875f4cf2492d2bd0 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 3 Aug 2026 14:15:01 +0200 Subject: [PATCH 62/63] selftests/exec: test the pre-opened interpreter limit - an interpreter opened at registration is charged - an interpreter a 'B' entry binds is charged too - an entry that opens none is not - removing an entry gives the charge back - a nested user namespace cannot buy itself budget by raising its own limit Skips where the sysctl or binfmt_misc is missing. The 'B' case lives in binfmt_misc_bpf.c because binding needs a handler. It binds from a child in a user namespace of its own, through the fd the child inherited, so the charge lands on the child while the interpreter is still opened with the entry file's credentials, and nothing outside the child sees a changed limit. Link: https://patch.msgid.link/20260803-work-binfmt_misc-interplimit-v1-2-4a2435500bd9@kernel.org Signed-off-by: Christian Brauner (Amutable) --- tools/testing/selftests/exec/.gitignore | 1 + tools/testing/selftests/exec/Makefile | 6 + .../testing/selftests/exec/binfmt_misc_bpf.c | 70 ++++++ .../selftests/exec/binfmt_misc_interplimit.c | 232 ++++++++++++++++++ 4 files changed, 309 insertions(+) create mode 100644 tools/testing/selftests/exec/binfmt_misc_interplimit.c diff --git a/tools/testing/selftests/exec/.gitignore b/tools/testing/selftests/exec/.gitignore index fbbb1600ddb9..e42ecd4c908d 100644 --- a/tools/testing/selftests/exec/.gitignore +++ b/tools/testing/selftests/exec/.gitignore @@ -20,6 +20,7 @@ xxxxxxxx* pipe S_I*.test binfmt_misc_bpf +binfmt_misc_interplimit binfmt_bpf_interp binfmt_bpf_app binfmt_misc_transparent diff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile index 410c93606a0c..b640af8f02b5 100644 --- a/tools/testing/selftests/exec/Makefile +++ b/tools/testing/selftests/exec/Makefile @@ -25,6 +25,10 @@ TEST_GEN_PROGS += check-exec # or an 'F' entry can pin the instance that owns it. Unprivileged, no bpf. TEST_GEN_PROGS += binfmt_misc_selfpin +# The interpreters an 'F' or 'B' entry pre-opens are charged against +# UCOUNT_BINFMT_MISC_INTERPRETERS. Unprivileged, no bpf. +TEST_GEN_PROGS += binfmt_misc_interplimit + # 'D' (register disabled) binfmt_misc test: an entry that exists but does # not dispatch until it is enabled. Static magic entry, no bpf toolchain. TEST_GEN_PROGS += binfmt_misc_disabled @@ -104,6 +108,8 @@ $(OUTPUT)/script-noexec.inc: $(CHECK_EXEC_SAMPLES)/script-noexec.inc # CFLAGS for every program in this directory. $(OUTPUT)/binfmt_misc_selfpin: CFLAGS += $(TOOLS_INCLUDES) $(OUTPUT)/binfmt_misc_selfpin: ../filesystems/utils.c +$(OUTPUT)/binfmt_misc_interplimit: CFLAGS += $(TOOLS_INCLUDES) +$(OUTPUT)/binfmt_misc_interplimit: ../filesystems/utils.c # --- binfmt_misc bpf ('B') handler test --------------------------------- # The struct_ops bpf objects are compiled against the running kernel's BTF. diff --git a/tools/testing/selftests/exec/binfmt_misc_bpf.c b/tools/testing/selftests/exec/binfmt_misc_bpf.c index 2c7b63075f1d..b2a4518901b0 100644 --- a/tools/testing/selftests/exec/binfmt_misc_bpf.c +++ b/tools/testing/selftests/exec/binfmt_misc_bpf.c @@ -38,6 +38,7 @@ #define _GNU_SOURCE #include #include +#include #include #include #include @@ -65,6 +66,9 @@ #define BIND_RISCV_PATH "/tmp/binfmt_bind_riscv" #define BIND_EXPECT "BIND_RAN " #define BIND_MAX 100 +#define INTERP_LIMIT "/proc/sys/user/max_binfmt_misc_interpreters" +/* Exit status of the binding child when it cannot set up a budget of its own. */ +#define BIND_NO_BUDGET 200 /* A minimal 64-bit little-endian ELF header, padded to the read size. */ static int create_fake_elf(const char *path, unsigned short machine) @@ -378,6 +382,57 @@ static int entry_bind(const char *entry, const char *name, const char *path) return entry_command(entry, cmd); } +/* Set the interpreter budget of this namespace. */ +static int write_interp_limit(const char *val) +{ + ssize_t n; + int fd; + + fd = open(INTERP_LIMIT, O_WRONLY | O_CLOEXEC); + if (fd < 0) + return -1; + n = write(fd, val, strlen(val)); + close(fd); + return n < 0 ? -1 : 0; +} + +/* + * The errno a bind is refused with when the writer is a child that has spent + * the budget of a user namespace of its own, 0 if it succeeded and -1 if the + * child could not set itself up. The fd is opened here and inherited, so the + * interpreter is still opened with this process's credentials. + */ +static int bind_out_of_budget(const char *entry, const char *name, + const char *path) +{ + char cmd[PATH_MAX], file[PATH_MAX]; + int fd, status, retval; + pid_t pid; + + snprintf(file, sizeof(file), BINFMT_DIR "/%s", entry); + snprintf(cmd, sizeof(cmd), "+%s %s\n", name, path); + + fd = open(file, O_WRONLY | O_CLOEXEC); + if (fd < 0) + return -1; + + pid = fork(); + if (pid == 0) { + ssize_t n; + + /* A namespace of its own, with nothing left in it to spend. */ + if (unshare(CLONE_NEWUSER) || write_interp_limit("0")) + _exit(BIND_NO_BUDGET); + n = write(fd, cmd, strlen(cmd)); + _exit(n < 0 ? errno : 0); + } + close(fd); + if (pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status)) + return -1; + retval = WEXITSTATUS(status); + return retval == BIND_NO_BUDGET ? -1 : retval; +} + FIXTURE(bound_interp) { char obj[PATH_MAX]; struct bpf_case c; @@ -565,4 +620,19 @@ TEST_F(bound_interp, capped_bindings) EXPECT_EQ(entry_bind("test_interp_bind", "over", BIND_FIRST), -ENOSPC); } +/* A binding pins a file: it is charged, and refused once the budget is out. */ +TEST_F(bound_interp, bindings_are_charged) +{ + int err = bind_out_of_budget("test_interp_bind", "third", BIND_FIRST); + + if (err < 0) + SKIP(return, "no user namespaces or no " INTERP_LIMIT); + + /* The charge follows the writer, not the entry file it writes to. */ + EXPECT_EQ(err, ENOSPC); + + /* The budget was the only thing in the way. */ + EXPECT_EQ(entry_bind("test_interp_bind", "third", BIND_FIRST), 0); +} + TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/exec/binfmt_misc_interplimit.c b/tools/testing/selftests/exec/binfmt_misc_interplimit.c new file mode 100644 index 000000000000..bf611c551784 --- /dev/null +++ b/tools/testing/selftests/exec/binfmt_misc_interplimit.c @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * A pre-opened interpreter - what 'F' gives a static entry and what a 'B' + * entry binds - keeps a file open for as long as the entry lives, so it pins + * the mount it came from. It costs no file descriptor, and binfmt_misc is + * FS_USERNS_MOUNT, so an unprivileged user namespace can create them without + * bound. Check that UCOUNT_BINFMT_MISC_INTERPRETERS bounds it, that an entry + * that pre-opens nothing is not charged, that removing an entry gives the + * charge back, and that nesting a user namespace does not evade it. + * + * Runs unprivileged in a user namespace. + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../filesystems/utils.h" +#include "kselftest_harness.h" + +#define MNT "/tmp/binfmt_interplimit" +#define NESTED_MNT "/tmp/binfmt_interplimit_nested" +#define LIMIT_SYSCTL "/proc/sys/user/max_binfmt_misc_interpreters" + +#define MAGIC "\\xde\\xad" +/* Not on the instance, and unlike /bin/true it always exists. */ +#define INTERP "/proc/self/exe" + +/* Small enough to fill by hand, big enough that a refund is visible. */ +#define LIMIT 4 + +/* What UCOUNT_ENTRY() lets a namespace raise its own limit to. */ +#define LIMIT_MAX "2147483647" + +static int ensure_dir(const char *path) +{ + if (mkdir(path, 0755) && errno != EEXIST) + return -1; + return 0; +} + +/* Write @val to @path, preserving write(2)'s errno for the caller. */ +static int write_keep_errno(const char *path, const char *val) +{ + int fd, saved; + ssize_t n; + + fd = open(path, O_WRONLY | O_CLOEXEC); + if (fd < 0) + return -1; + n = write(fd, val, strlen(val)); + saved = errno; + close(fd); + errno = saved; + return n < 0 ? -1 : 0; +} + +static int set_limit(const char *val) +{ + return write_keep_errno(LIMIT_SYSCTL, val); +} + +static int register_at(const char *mnt, const char *rule) +{ + char path[PATH_MAX]; + + snprintf(path, sizeof(path), "%s/register", mnt); + return write_keep_errno(path, rule); +} + +/* An 'F' entry: one interpreter pre-opened at registration, one charge. */ +static int register_fixed(const char *mnt, const char *name) +{ + char rule[PATH_MAX]; + + snprintf(rule, sizeof(rule), ":%s:M::" MAGIC "::" INTERP ":F", name); + return register_at(mnt, rule); +} + +/* The same entry without 'F': the interpreter is opened per exec instead. */ +static int register_plain(const char *mnt, const char *name) +{ + char rule[PATH_MAX]; + + snprintf(rule, sizeof(rule), ":%s:M::" MAGIC "::" INTERP ":", name); + return register_at(mnt, rule); +} + +static int remove_entry(const char *mnt, const char *name) +{ + char path[PATH_MAX]; + + snprintf(path, sizeof(path), "%s/%s", mnt, name); + return write_keep_errno(path, "-1\n"); +} + +static bool entry_exists(const char *mnt, const char *name) +{ + char path[PATH_MAX]; + + snprintf(path, sizeof(path), "%s/%s", mnt, name); + return access(path, F_OK) == 0; +} + +/* Register @n 'F' entries, each with a name of its own. */ +static int fill_budget(const char *mnt, unsigned int n) +{ + char name[32]; + unsigned int i; + + for (i = 0; i < n; i++) { + snprintf(name, sizeof(name), "fixed%u", i); + if (register_fixed(mnt, name)) + return -1; + } + return 0; +} + +FIXTURE(interp_limit) { +}; + +FIXTURE_SETUP(interp_limit) +{ + /* setup_userns() exits rather than returns if this is not there. */ + if (access("/proc/self/ns/user", F_OK)) + SKIP(return, "kernel without user namespaces"); + ASSERT_EQ(setup_userns(), 0); + + /* CAP_SYS_RESOURCE in this namespace is what makes it writable. */ + if (set_limit(LIMIT_MAX)) { + if (errno == ENOENT) + SKIP(return, "kernel without " LIMIT_SYSCTL); + SKIP(return, "cannot set the limit: %s", strerror(errno)); + } + + ASSERT_EQ(ensure_dir(MNT), 0); + if (mount("binfmt_misc", MNT, "binfmt_misc", 0, NULL)) { + int saved = errno; + + /* Teardown doesn't run when setup skips, so clean up here. */ + rmdir(MNT); + SKIP(return, "no binfmt_misc: %s", strerror(saved)); + } +} + +FIXTURE_TEARDOWN(interp_limit) +{ + /* The namespaces go with the process; just don't litter /tmp. */ + umount2(NESTED_MNT, MNT_DETACH); + umount2(MNT, MNT_DETACH); + rmdir(NESTED_MNT); + rmdir(MNT); +} + +/* Every pre-opened interpreter is charged, and the budget is a hard stop. */ +TEST_F(interp_limit, fixed_interpreters_are_charged) +{ + char buf[32]; + + snprintf(buf, sizeof(buf), "%u", LIMIT); + ASSERT_EQ(set_limit(buf), 0); + + ASSERT_EQ(fill_budget(MNT, LIMIT), 0); + + EXPECT_NE(register_fixed(MNT, "over"), 0); + EXPECT_EQ(errno, ENOSPC); + + /* A refused registration leaves nothing behind. */ + EXPECT_FALSE(entry_exists(MNT, "over")); +} + +/* An entry that pre-opens nothing pins nothing, so it is not charged. */ +TEST_F(interp_limit, plain_entries_are_not_charged) +{ + ASSERT_EQ(set_limit("0"), 0); + + EXPECT_EQ(register_plain(MNT, "plain"), 0); + EXPECT_TRUE(entry_exists(MNT, "plain")); + + /* ... while the same entry with 'F' has nothing to spend. */ + EXPECT_NE(register_fixed(MNT, "fixed"), 0); + EXPECT_EQ(errno, ENOSPC); +} + +/* Removing an entry closes its interpreters and gives the charge back. */ +TEST_F(interp_limit, removal_refunds_the_charge) +{ + char buf[32]; + + snprintf(buf, sizeof(buf), "%u", LIMIT); + ASSERT_EQ(set_limit(buf), 0); + + ASSERT_EQ(fill_budget(MNT, LIMIT), 0); + ASSERT_NE(register_fixed(MNT, "over"), 0); + + ASSERT_EQ(remove_entry(MNT, "fixed0"), 0); + EXPECT_EQ(register_fixed(MNT, "over"), 0); +} + +/* + * The charge walks the ancestors, so a namespace cannot buy itself budget by + * nesting: it may raise only its own limit, and the parent it was created + * from is charged for every binding made below it. + */ +TEST_F(interp_limit, nesting_does_not_evade_it) +{ + char buf[32]; + + snprintf(buf, sizeof(buf), "%u", LIMIT); + ASSERT_EQ(set_limit(buf), 0); + ASSERT_EQ(fill_budget(MNT, LIMIT), 0); + + ASSERT_EQ(setup_userns(), 0); + ASSERT_EQ(set_limit(LIMIT_MAX), 0); + + ASSERT_EQ(ensure_dir(NESTED_MNT), 0); + ASSERT_EQ(mount("binfmt_misc", NESTED_MNT, "binfmt_misc", 0, NULL), 0); + + /* A fresh instance with an unlimited budget of its own, and yet: */ + EXPECT_NE(register_fixed(NESTED_MNT, "nested"), 0); + EXPECT_EQ(errno, ENOSPC); + + /* The nested instance works for anything that pins no file. */ + EXPECT_EQ(register_plain(NESTED_MNT, "nested_plain"), 0); +} + +TEST_HARNESS_MAIN From a0ff406303591e714ca82a3fede627f9f7c13231 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 3 Aug 2026 14:15:02 +0200 Subject: [PATCH 63/63] binfmt_misc: document the pre-opened interpreter limit Document how pre-opened interpreters are accounted. Link: https://patch.msgid.link/20260803-work-binfmt_misc-interplimit-v1-3-4a2435500bd9@kernel.org Signed-off-by: Christian Brauner (Amutable) --- Documentation/admin-guide/binfmt-misc.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Documentation/admin-guide/binfmt-misc.rst b/Documentation/admin-guide/binfmt-misc.rst index 622b5d8c8995..d26b63a27c25 100644 --- a/Documentation/admin-guide/binfmt-misc.rst +++ b/Documentation/admin-guide/binfmt-misc.rst @@ -128,6 +128,11 @@ There are some restrictions: named by an absolute path. It is opened when the binary is executed, so a relative one would be resolved against the working directory of whoever runs the binary + - the amount of pre-opened interpreters by ``F``, or bound to a ``B`` entry + is limited by the ``/proc/sys/user/max_binfmt_misc_interpreters`` sysctl. A + registration past the limit is refused with ``-ENOSPC``. This limits an + unprivileged namespace pinning files. A nested namespace can raise only its + own limit and every ancestor is charged too To use binfmt_misc you have to mount it first. You can mount it with @@ -215,10 +220,9 @@ with the credentials the entry file was opened with, exactly the way ``F`` pre-opens a static entry's interpreter; the paths must be absolute. The path is everything past the first space, so there is nothing it cannot express, and no interpreter has to fit in a register string. An entry -binds at most 100 interpreters; a write past that is refused with -``-ENOSPC``. To bind a file that has no path of its own - already -unlinked, a ``memfd``, or reachable only in another mount namespace - -open it and write ``/proc/self/fd/N``. +binds at most 100 interpreters, and each one is charged against +``max_binfmt_misc_interpreters`` like any other binding. A write past either +limit is refused with ``-ENOSPC``. The ``load`` program then selects one per exec by name with the ``bpf_binprm_select_interp()`` kfunc, and every exec runs a clone of the