Merge patch series "binfmt_misc: transparent interpreters and PT_INTERP loader substitution"

Christian Brauner <brauner@kernel.org> says:

binfmt_misc has exactly one execution model where the registered
interpreter becomes the executed program and the matched binary is
handed to it as an argument. For wine or qemu-user that is the point.
For a per-binary loader it is backwards. The interpreter is an
implementation detail of running the binary, yet it owns the entire
process identity:

- argv[0] and /proc/pid/cmdline show the interpreter invocation, not
  what the caller executed.

- /proc/self/exe names the interpreter. Relocatable programs commonly
  locate themselves through it and find the dynamic linker instead.

- A binary passed to execveat() as an inaccessible O_CLOEXEC fd
  cannot run at all as the interpreter has no path to open it by.

- gdb cross-validates AT_ENTRY/AT_PHDR against the exe file and
  discards the load displacement on mismatch leaving PIE symbols
  unrelocated.

This series adds two dispatch modes that close the gap from opposite
ends:

(1) transparent dispatch

    Registered with the 'T' flag or chosen per exec with
    BPF_BINPRM_TRANSPARENT. The binary is sent to the interpreter through
    AT_EXECFD, the argument vector stays exactly as the caller built it,
    and the kernel labels mm->exe_file and comm with the binary. A new
    AT_FLAGS_TRANSPARENT_INTERP aux vector bit is raised indicating that
    nothing was spliced, argv belongs to the program, and to load it from
    the descriptor.

    The interpreter keeps control of mapping the binary, so the mode
    covers foreign architectures and non-ELF payloads.

    The exe label is not a new privilege. It names precisely the file the
    caller passed to execve(), not a file of the process's choosing. That
    file is permission-checked, write-denied while the process runs and
    recorded by audit. Credential derivation does not change exactly as
    today.

(2) loader substitution

    The kernel executes the matched binary natively as the main image
    and substitutes the registered interpreter for the binary's
    PT_INTERP. binfmt_misc functions as a PT_INTERP override. There is
    no contract and no identity to reconstruct. So a stock dynamic
    loader works unchanged. Hence, 'L' is for native-arch ELF with
    PT_INTERP.

The two modes compose. A bpf handler reads the ELF header from bprm->buf
and grades per binary, picking 'L' where it applies and 'T' or classic
dispatch for the rest. If userspace control over relocation is wanted
'T' is the way to go.

* patches from https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-0-e57866e4ae0f@kernel.org: (21 commits)
  binfmt_misc: document loader substitution
  selftests/exec: test binfmt_misc loader substitution
  binfmt_misc: let a bpf handler request loader substitution
  binfmt_misc: add the 'L' loader substitution flag
  binfmt_elf_fdpic: consume a stashed PT_INTERP substitute
  binfmt_elf: consume a stashed PT_INTERP substitute
  exec: carry a PT_INTERP substitute in struct linux_binprm
  binfmt_misc: document the transparent identity contract
  selftests/exec: test the transparent binfmt_misc mode
  binfmt_misc: let a bpf handler run the interpreter transparently
  binfmt_misc: add a static transparent flag 'T'
  binfmt_misc: add transparent interpreter dispatch
  exec: label mm->exe_file with the binary for a transparent dispatch
  exec: add AT_FLAGS_TRANSPARENT_INTERP
  selftests/exec: convert the binfmt_misc bpf test to the kselftest harness
  exec: release the replaced file with do_close_execat()
  binfmt_misc: split out entry_open_interpreter() and build_interp_argv()
  binfmt_misc: normalize the per-exec invocation flags
  binfmt_misc: table-drive the register string flags
  docs, binfmt_misc: keep general usage out of the handler sections
  ...

Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-0-e57866e4ae0f@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
This commit is contained in:
Christian Brauner
2026-07-25 11:11:48 +02:00
20 changed files with 1881 additions and 348 deletions

View File

@@ -90,6 +90,23 @@ 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.
``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:
@@ -98,66 +115,12 @@ 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`` 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
bpf-backed handlers
-------------------
With ``CONFIG_BINFMT_MISC_BPF`` both the matching and the interpreter
selection can be delegated to bpf programs. A handler is an instance of the
``binfmt_misc_ops`` struct_ops with a ``match`` and a ``load`` program and a
``name``. Once the struct_ops map is registered the handler can be activated
with a ``B`` entry that references it by name in the ``interpreter`` field
and carries neither offset, magic, nor mask::
echo ':qemu:B::::my_handler:' > register
Both programs receive the ``linux_binprm`` of the binary and both can
sleep. The ``match`` program decides whether the handler applies: it is
consulted during the entry walk exactly like magic and extension matching,
in the same registration order with the same first-match-wins semantics.
Unlike static matching it is not limited to the prefetched first bytes of
the file in ``bprm->buf``: it can read the file, e.g. to parse ELF program
headers whose data sits at arbitrary offsets. It only decides, though: the
selection kfuncs below are rejected in it. The ``load`` program of the
matched handler then selects the interpreter: it can equally read the file
and derive the interpreter from the binary's location. It selects the
interpreter by calling the ``bpf_binprm_set_interp()`` kfunc with an
absolute path and returning ``0``. A match is committed: a failing
``load`` fails the exec with its error instead of falling through to later
entries; ``-ENOEXEC`` lets the remaining binary formats have a go. The
interpreter is opened with the credentials of the task doing the exec,
exactly as a statically registered interpreter would be.
The ``load`` program can also pass a single argument to the interpreter with
the ``bpf_binprm_set_interp_arg()`` kfunc. It is inserted between the
interpreter and the binary, exactly like the optional argument of a ``#!``
interpreter line, e.g. for a handler that resolves ``$ORIGIN`` in a script's
``#!`` path and needs to preserve the argument that followed it.
The invocation flags a static entry fixes at registration - ``P``, ``C``
and ``O`` - are per-exec choices for a bpf handler, made by the ``load``
program with the ``bpf_binprm_set_flags()`` kfunc, so a single handler can
decide them differently for each binary it handles:
- ``BPF_BINPRM_PRESERVE_ARGV0`` keeps the caller's ``argv[0]`` (the ``P``
flag).
- ``BPF_BINPRM_CREDENTIALS`` computes credentials from the binary (the ``C``
flag), bounded to user namespaces that map the binary's owner just like
any other setuid exec.
- ``BPF_BINPRM_EXECFD`` opens the binary on the interpreter's behalf and
passes it through the ``AT_EXECFD`` aux vector entry (the ``O`` flag), so
the interpreter can run binaries it could not open by path.
Because these are program choices, a ``B`` entry carries no flags in the
register string; ``F`` (pre-open a fixed interpreter) has no meaning for it.
A handler is looked up only in the user namespace the struct_ops map was
registered in. Handlers are not inherited, so an entry can only reference a
handler registered in the same user namespace as its binfmt_misc instance.
The entry keeps the handler alive; deleting the struct_ops map only prevents
new activations.
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
@@ -198,6 +161,152 @@ or ``/proc/sys/fs/binfmt_misc/status``. A single entry can also be removed
by simply unlinking (``rm``) ``/proc/.../the_name``.
bpf-backed handlers
-------------------
With ``CONFIG_BINFMT_MISC_BPF`` both the matching and the interpreter
selection can be delegated to bpf programs. A handler is an instance of the
``binfmt_misc_ops`` struct_ops with a ``match`` and a ``load`` program and a
``name``. Once the struct_ops map is registered the handler can be activated
with a ``B`` entry that references it by name in the ``interpreter`` field
and carries neither offset, magic, nor mask::
echo ':qemu:B::::my_handler:' > register
Both programs receive the ``linux_binprm`` of the binary and both can
sleep. The ``match`` program decides whether the handler applies: it is
consulted during the entry walk exactly like magic and extension matching,
in the same registration order with the same first-match-wins semantics.
Unlike static matching it is not limited to the prefetched first bytes of
the file in ``bprm->buf``: it can read the file, e.g. to parse ELF program
headers whose data sits at arbitrary offsets. It only decides, though: the
selection kfuncs below are rejected in it. The ``load`` program of the
matched handler then selects the interpreter: it can equally read the file
and derive the interpreter from the binary's location. It selects the
interpreter by calling the ``bpf_binprm_set_interp()`` kfunc with an
absolute path and returning ``0``. A match is committed: a failing
``load`` fails the exec with its error instead of falling through to later
entries; ``-ENOEXEC`` lets the remaining binary formats have a go. The
interpreter is opened with the credentials of the task doing the exec,
exactly as a statically registered interpreter would be.
The ``load`` program can also pass a single argument to the interpreter with
the ``bpf_binprm_set_interp_arg()`` kfunc. It is inserted between the
interpreter and the binary, exactly like the optional argument of a ``#!``
interpreter line, e.g. for a handler that resolves ``$ORIGIN`` in a script's
``#!`` path and needs to preserve the argument that followed it.
The invocation flags a static entry fixes at registration - ``P``, ``C``,
``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).
- ``BPF_BINPRM_CREDENTIALS`` computes credentials from the binary (the ``C``
flag), bounded to user namespaces that map the binary's owner just like
any other setuid exec.
- ``BPF_BINPRM_EXECFD`` opens the binary on the interpreter's behalf and
passes it through the ``AT_EXECFD`` aux vector entry (the ``O`` flag), so
the interpreter can run binaries it could not open by path.
- ``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.
- ``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.
A handler is looked up only in the user namespace the struct_ops map was
registered in. Handlers are not inherited, so an entry can only reference a
handler registered in the same user namespace as its binfmt_misc instance.
The entry keeps the handler alive; deleting the struct_ops map only prevents
new activations.
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.
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
-----

View File

@@ -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));
@@ -904,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))
@@ -935,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) {

View File

@@ -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;
@@ -509,7 +513,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 +652,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));

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>
@@ -49,8 +50,42 @@ 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),
MISC_FMT_LOADER = (1U << 26),
};
/**
* 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" },
{ '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. */
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. */
@@ -288,56 +323,97 @@ static const char *entry_select_interpreter(const struct binfmt_misc_entry *e,
return ERR_PTR(retval);
}
/*
* the loader itself
/**
* 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 int load_misc_binary(struct linux_binprm *bprm)
static unsigned long entry_invocation_flags(const struct binfmt_misc_entry *e,
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;
bool preserve_argv0, want_execfd, want_creds;
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;
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;
}
/**
* 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 struct file *entry_open_interpreter(const struct binfmt_misc_entry *e,
const char *interpreter)
{
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 */
retval = exe_file_deny_write_access(interp_file);
if (retval)
return ERR_PTR(retval);
return no_free_ptr(interp_file);
}
/**
* 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 interpreter has to be able to load the binary by path. */
if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE)
return -ENOENT;
interpreter = entry_select_interpreter(fmt, 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;
}
/* 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);
@@ -371,31 +447,83 @@ static int load_misc_binary(struct linux_binprm *bprm)
return retval;
bprm->argc++;
/* Update interp in case binfmt_script needs it. */
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;
/* 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;
fmt = get_binfmt_handler(misc, bprm);
if (!fmt)
return -ENOEXEC;
interpreter = entry_select_interpreter(fmt, bprm);
if (IS_ERR(interpreter))
return PTR_ERR(interpreter);
flags = entry_invocation_flags(fmt, bprm);
/* No argv is built for a staged argument to land in. */
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)
return retval;
}
/* Update interp for the next round; sched_prepare_exec reports 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);
/* 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 (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;
}
@@ -424,30 +552,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 +684,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 +709,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 +780,38 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,
}
/* Parse the 'flags' field. */
flags = p;
p = check_special_flags(p, e);
/*
* A bpf handler decides the invocation flags per exec with
* bpf_binprm_set_flags() rather than fixing them at registration, and
* 'F' (pre-open a fixed interpreter) is meaningless for it, so a 'B'
* entry's flags field has to be empty.
*/
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);
/* 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);
/*
* A bpf handler decides the invocation flags per exec with
* bpf_binprm_set_flags() rather than fixing them at registration, so a
* 'B' entry carries no flags: 'P', 'C' and 'O' become per-exec choices
* and 'F' (pre-open a fixed interpreter) is meaningless for it.
*/
if (test_bit(MISC_FMT_BPF_BIT, &e->flags) &&
(e->flags & (MISC_FMT_PRESERVE_ARGV0 | MISC_FMT_OPEN_BINARY |
MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_FILE)))
/* Non-F opens the interp at exec against the caller's cwd; require absolute. */
if ((e->flags & (MISC_FMT_LOADER | MISC_FMT_CREDENTIALS)) &&
!(e->flags & MISC_FMT_OPEN_FILE) &&
e->interpreter[0] != '/')
return ERR_PTR(-EINVAL);
return no_free_ptr(e);
@@ -743,14 +874,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)) {

View File

@@ -171,19 +171,33 @@ __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, 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.
* 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
* 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 |
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. */
if ((flags & BPF_BINPRM_TRANSPARENT) && (flags & BPF_BINPRM_PRESERVE_ARGV0))
return -EINVAL;
bprm->bpf_flags = flags;

View File

@@ -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
@@ -1112,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)
@@ -1151,7 +1166,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 +1256,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 +1267,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 +1308,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;
@@ -1394,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) {
@@ -1413,8 +1470,9 @@ 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);
/* 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)
kfree(bprm->interp);
@@ -1731,19 +1789,23 @@ 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;
exe_file_allow_write_access(exec);
if (unlikely(bprm->have_execfd)) {
if (bprm->executable) {
fput(exec);
do_close_execat(exec);
return -ENOEXEC;
}
/* Kept for AT_EXECFD; the write denial rides along until hand-over. */
bprm->executable = exec;
} else
fput(exec);
} else {
do_close_execat(exec);
}
}
audit_bprm(bprm);

View File

@@ -16,6 +16,12 @@ 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
* @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.
@@ -24,6 +30,8 @@ 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),
BPF_BINPRM_LOADER = (1ULL << 4),
};
/**

View File

@@ -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_*) */
@@ -93,6 +94,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.
@@ -137,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;

View File

@@ -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 */

View File

@@ -22,5 +22,10 @@ S_I*.test
binfmt_misc_bpf
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

View File

@@ -21,6 +21,18 @@ 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
# '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
@@ -35,7 +47,8 @@ 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 += 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)
@@ -44,6 +57,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,20 +101,27 @@ 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
$(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
$(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

View File

@@ -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 <elf.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/auxv.h>
#include <unistd.h>
#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;
}

View File

@@ -9,7 +9,7 @@
*
* echo ':name:B::::<handler>:' > /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,73 +18,60 @@
* 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.
* 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.
*
* 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 <elf.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <libgen.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <bpf/btf.h>
#include <bpf/libbpf.h>
#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 TRANS_INTERP "/tmp/binfmt_transparent_interp"
#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"
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 +84,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)
@@ -140,138 +106,205 @@ 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(entry);
detach:
bpf_link__destroy(link);
close:
bpf_object__close(obj);
bpf_case_stop(&c);
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);
}
/* 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);
}
/* 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

View File

@@ -0,0 +1,276 @@
/* 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 <elf.h>
#include <errno.h>
#include <fcntl.h>
#include <libgen.h>
#include <limits.h>
#include <link.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#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"
/* 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
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;
}
/* 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;
}
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 */

View File

@@ -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 <elf.h>
#include <link.h>
#include <signal.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/ptrace.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#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

View File

@@ -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 <stdio.h>
#include <stdlib.h>
#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

View File

@@ -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 <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/auxv.h>
#include <sys/stat.h>
#include <unistd.h>
#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;
}

View File

@@ -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

View File

@@ -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 <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
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",
};

View File

@@ -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 <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
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",
};