From 7830e96d001c86c3dd34a2434277d86bba14c8c6 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 21 Jul 2026 16:13:43 +0200 Subject: [PATCH 01/21] 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 02/21] 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 03/21] 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 04/21] 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 05/21] 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 06/21] 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 07/21] 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 08/21] 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 09/21] 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 10/21] 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 11/21] 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 12/21] 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 13/21] 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 14/21] 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 15/21] 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 16/21] 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 17/21] 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 18/21] 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 19/21] 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 20/21] 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 21/21] 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 -----