From 7a8b81e8b9c73cfb7343fe90e575ec0c31a0c47a Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 10 Jul 2026 11:33:05 +0200 Subject: [PATCH 01/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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)