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 <farid.m.zakaria@gmail.com>
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
This commit is contained in:
Christian Brauner 2026-07-21 16:13:45 +02:00
parent ee3db4b866
commit 23c703f959
No known key found for this signature in database
GPG Key ID: 91C61BC06578DCA2

View File

@ -10,6 +10,7 @@
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/array_size.h>
#include <linux/binfmt_misc.h>
#include <linux/binfmts.h>
#include <linux/bitops.h>
@ -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)) {