Merge tag 'vfs-7.3-rc1.binfmt' of git://git.kernel.org/pub/scm/linux/kernel/git/vfs/vfs

Pull binfmt updates from Christian Brauner:
 "This contains a bunch of work for binfmt_misc. It fixes a bunch of
  old bugs, reworks the locking, and then extends the format registry
  so a binary type can be matched programmatically and its interpreter
  computed per exec instead of being a fixed string recorded at
  registration time.

  This allows nixos and other to e.g., implement relocatable binaries
  meaning the interpreter/dynamic loader can be determined
  programatically, say found relative to the binary. The mechanism is
  flexible and can support other policies:

   - Handler lookup is now an rcu walk. An exec that matches no
     binfmt_misc entry should now never write to a shared cacheline

   - remove the VERBOSE_STATUS and USE_DEBUG compile time toggles

   - convert the entry file to a seq_file which simplifies things quite
     a bit and kills a lot of custom logic

   - make flags proper enums

   - rename struct Node to binfmt_misc_entry

   - allow entries to be removed with unlink(2)

   - Add the ability to attach bpf programs to binfmt_misc entries so
     it's possible to dynamically choose the execution environment such
     as the loader or interpreter on a per binary basis.

     A handler is an instance of a binfmt_misc_ops struct_ops with a
     ->match() and a ->load() program. match() decides from the entry
     lookup walk whether the handler applies under the same
     registration-order. It can read file content as needed not only the
     prefetched 256 bytes in bprm->buf.

     load() then selects the interpreter and stages it through the new
     bpf_binprm_set_interp(), bpf_binprm_set_interp_arg() and
     bpf_binprm_set_flags() kfuncs.

     Handlers are published in a registry keyed by the registering
     task's user namespace and activated through the existing text
     interface with a new 'B' type carrying the handler name:

	echo ':origin:B::::nix:' > /proc/sys/fs/binfmt_misc/register

     The permission and namespacing model is unchanged. Activating a
     handler requires the same write access to an instance as any other
     registration. A container mounting its own instance escapes the
     host's entries exactly as before. The computed interpreter is
     opened with open_exec() under the caller's credentials and goes
     through full LSM vetting as the next binprm level. A program can
     only ever redirect the caller to something the caller could exec
     anyway.

   - Two dispatch modes are added. So far the chosen interpreter owns
     the whole process identity (argv[0], /proc/pid/cmdline,
     /proc/self/exe all name interpreter information). So relocatable
     find the dynamic linker instead. Also a binary passed to execveat()
     as an inaccessible O_CLOEXEC fd cannot run at all and gdb trips
     because AT_ENTRY and AT_PHDR do not match the exe file. So PIE
     symbols are unrelocated.

     This adds transparent dispatch which allows the interpreter to load
     the binary through AT_EXECFD and leaves the argument vector exactly
     as the caller built it and labels mm->exe_file and comm with the
     binary. It also raises the AT_FLAGS_TRANSPARENT_INTERP aux vector
     bit. The interpreter keeps control of mapping the binary.

     The second mode is loader substitution. This allows a binary to be
     executed natively and only the interpreter to be changed.

   - Last, interpreters can be bound at registration time. Each
     interpreter is opened by its own write with the credentials the
     entry file was opened with. The program picks one per exec with
     bpf_binprm_select_interp().

     Ucounts are used to properly account for pre-opened interpreters
     via /proc/sys/user/max_binfmt_misc_interpreters"

* tag 'vfs-7.3-rc1.binfmt' of git://git.kernel.org/pub/scm/linux/kernel/git/vfs/vfs: (63 commits)
  binfmt_misc: document the pre-opened interpreter limit
  selftests/exec: test the pre-opened interpreter limit
  binfmt_misc: correctly account pre-opened interpreters
  binfmt_misc: document interpreters bound by a 'B' entry
  selftests/exec: test interpreters bound to a 'B' entry
  binfmt_misc: let a 'B' entry bind its interpreters
  binfmt_misc: carry pre-opened interpreters in struct binfmt_misc_interp
  selftests/exec: share the bpf handler preconditions
  binfmt_misc: document registering an entry disabled
  selftests/exec: test registering an entry disabled
  selftests/exec: let binfmt_flag_supported() return a bool
  selftests/exec: check that a binfmt_misc instance cannot be pinned
  binfmt_misc: let a register string create an entry disabled
  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
  ...
This commit is contained in:
Linus Torvalds
2026-08-17 08:35:25 -07:00
35 changed files with 5000 additions and 554 deletions

View File

@@ -26,7 +26,8 @@ Here is what the fields mean:
name below ``/proc/sys/fs/binfmt_misc``; cannot contain slashes ``/`` for
obvious reasons.
- ``type``
is the type of recognition. Give ``M`` for magic and ``E`` for extension.
is the type of recognition. Give ``M`` for magic, ``E`` for extension and
``B`` for a bpf-backed handler (see below).
- ``offset``
is the offset of the magic/mask in the file, counted in bytes. This
defaults to 0 if you omit it (i.e. you write ``:name:type::magic...``).
@@ -48,7 +49,8 @@ Here is what the fields mean:
filename extension matching.
- ``interpreter``
is the program that should be invoked with the binary as first
argument (specify the full path)
argument (specify the full path). For ``B`` entries this field
carries the name of the bpf handler instead (see below).
- ``flags``
is an optional field that controls several aspects of the invocation
of the interpreter. It is a string of capital letters, each controls a
@@ -88,6 +90,32 @@ 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.
``D`` - registered disabled
The entry is created disabled instead of being matchable at
once, and has to be enabled by writing ``1`` to its file
before it dispatches anything. This splits a registration
into creating the entry and activating it, leaving room to
configure it in between - which is what a ``B`` entry that
binds interpreters needs; see the bpf section below. The flag
is spent on the registration and is not read back: what an
entry file reports afterwards is whether it is enabled.
There are some restrictions:
@@ -96,6 +124,16 @@ 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
- the amount of pre-opened interpreters by ``F``, or bound to a ``B`` entry
is limited by the ``/proc/sys/user/max_binfmt_misc_interpreters`` sysctl. A
registration past the limit is refused with ``-ENOSPC``. This limits an
unprivileged namespace pinning files. A nested namespace can raise only its
own limit and every ancestor is charged too
To use binfmt_misc you have to mount it first. You can mount it with
``mount -t binfmt_misc none /proc/sys/fs/binfmt_misc`` command, or you can add
@@ -133,7 +171,209 @@ or 1 (to enable) to ``/proc/sys/fs/binfmt_misc/status`` or
Catting the file tells you the current status of ``binfmt_misc/the_entry``.
You can remove one entry or all entries by echoing -1 to ``/proc/.../the_name``
or ``/proc/sys/fs/binfmt_misc/status``.
or ``/proc/sys/fs/binfmt_misc/status``. A single entry can also be removed
by simply unlinking (``rm``) ``/proc/.../the_name``.
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. A path
selected this way is opened with the credentials of the task doing the
exec, exactly as a statically registered interpreter without ``F`` would
be.
An entry can instead bind the interpreters its handler may use, so that no
path is resolved at exec time at all. An entry registered with ``D`` is not
matchable yet, which is what leaves it open to being given them, one
``+name path`` write at a time::
echo ':qemu:B::::my_handler:D' > register
echo '+aarch64 /usr/bin/qemu-aarch64' > qemu
echo '+arm /usr/bin/qemu-arm' > qemu
echo 1 > qemu
Each path is opened during its write, in the writing process's context and
with the credentials the entry file was opened with, exactly the way ``F``
pre-opens a static entry's interpreter; the paths must be absolute. The
path is everything past the first space, so there is nothing it cannot
express, and no interpreter has to fit in a register string. An entry
binds at most 100 interpreters, and each one is charged against
``max_binfmt_misc_interpreters`` like any other binding. A write past either
limit is refused with ``-ENOSPC``.
The ``load`` program then selects one per exec by name with the
``bpf_binprm_select_interp()`` kfunc, and every exec runs a clone of the
file that was opened. The path decides which file is bound and nothing
else: it is not resolved again, in any namespace, so what it holds later -
or what it holds in the namespace of whoever runs the binary - no longer
decides anything.
Enabling the entry ends this. Its interpreters are read at exec time with
nothing but a reference held on the entry, so an entry that has ever been
matchable can never have its set changed again: the first ``1`` seals it,
from then on ``+`` is refused with ``-EBUSY``, and an entry registered
without ``D`` is sealed from the start. Binding a name twice is refused
with ``-EEXIST``.
Selection is by name so that the configuration and the program need not
agree on an order, and so that a handler is not tied to where a distribution
puts its interpreters. A name is a single word of printable ASCII, at most
32 characters; a name the entry did not bind gives the program ``-ENOENT``,
which it can act on or return. The interpreter runs under the path it was
registered under, and the entry reports what it bound::
$ cat /proc/sys/fs/binfmt_misc/qemu
enabled
bpf my_handler
bpf-interpreter aarch64 /usr/bin/qemu-aarch64
bpf-interpreter arm /usr/bin/qemu-arm
flags:
The path reported is the one the interpreter was bound under, which named
the file at that moment; it is not re-resolved, so it is a record of what
was bound rather than a promise about what that path holds now.
The ``load`` program can also pass a single argument to the interpreter with
the ``bpf_binprm_set_interp_arg()`` kfunc. It is inserted between the
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 invocation
flags in the register string; ``F`` has none to spell for it either, since
the interpreters it binds already pre-open what ``F`` would. The
registration directive ``D`` is the exception: it decides how the entry
starts out, not how the interpreter is invoked.
A handler is looked up only in the user namespace the struct_ops map was
registered in. Handlers are not inherited, so an entry can only reference a
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

@@ -168,6 +168,20 @@ config BINFMT_MISC
you have use for it; the module is called binfmt_misc. If you
don't know what to answer at this point, say Y.
config BINFMT_MISC_BPF
bool "BPF-selected interpreters for misc binaries"
depends on BINFMT_MISC=y
depends on BPF_SYSCALL && BPF_JIT && DEBUG_INFO_BTF
help
Allow binfmt_misc binary type handlers to be implemented as bpf
struct_ops programs. Instead of matching a fixed magic and
redirecting to a fixed interpreter recorded at registration time
such handlers match binaries programmatically and compute the
interpreter to use per binary, e.g. relative to the location of
the binary itself.
If you don't know what to answer at this point, say N.
config COREDUMP
bool "Enable core dump support" if EXPERT
default y

View File

@@ -33,6 +33,7 @@ obj-$(CONFIG_FS_ENCRYPTION) += crypto/
obj-$(CONFIG_FS_VERITY) += verity/
obj-$(CONFIG_FILE_LOCKING) += locks.o
obj-$(CONFIG_BINFMT_MISC) += binfmt_misc.o
obj-$(CONFIG_BINFMT_MISC_BPF) += binfmt_misc_bpf.o
obj-$(CONFIG_BINFMT_SCRIPT) += binfmt_script.o
obj-$(CONFIG_BINFMT_ELF) += binfmt_elf.o
obj-$(CONFIG_COMPAT_BINFMT_ELF) += compat_binfmt_elf.o

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));

File diff suppressed because it is too large Load Diff

434
fs/binfmt_misc_bpf.c Normal file
View File

@@ -0,0 +1,434 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* BPF-backed binary type handlers for binfmt_misc.
*
* A handler is a struct binfmt_misc_ops struct_ops map. Loading and
* registering it makes the handler available under its name in the user
* namespace it was registered in. A binfmt_misc 'B' entry activates it:
*
* echo ':entry:B::::<handler-name>:' > <binfmt_misc>/register
*
* The entry can bind the interpreters the handler may run its binaries
* with, each opened by the write that binds it and selected by name per
* exec. An entry registered with 'D' is not matchable yet, which is what
* leaves it open to being given them:
*
* echo ':entry:B::::<handler-name>:D' > <binfmt_misc>/register
* echo '+<name> <path>' > <binfmt_misc>/entry
* echo 1 > <binfmt_misc>/entry
*/
#include <linux/binfmt_misc.h>
#include <linux/binfmts.h>
#include <linux/bpf.h>
#include <linux/bpf_verifier.h>
#include <linux/btf.h>
#include <linux/btf_ids.h>
#include <linux/cred.h>
#include <linux/file.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/limits.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/string.h>
#include <linux/user_namespace.h>
struct bm_bpf_ops_reg {
struct list_head list;
const struct binfmt_misc_ops *ops;
struct bpf_link *link;
struct user_namespace *user_ns;
};
static DEFINE_SPINLOCK(bm_bpf_ops_lock);
static LIST_HEAD(bm_bpf_ops_list);
static struct bpf_struct_ops bpf_binfmt_misc_ops;
static struct bm_bpf_ops_reg *bm_bpf_ops_find(const struct user_namespace *user_ns,
const char *name)
{
struct bm_bpf_ops_reg *reg;
lockdep_assert_held(&bm_bpf_ops_lock);
list_for_each_entry(reg, &bm_bpf_ops_list, list) {
if (reg->user_ns == user_ns && !strcmp(reg->ops->name, name))
return reg;
}
return NULL;
}
/**
* binfmt_misc_get_ops - look up a bpf binary type handler by name
* @user_ns: user namespace of the binfmt_misc instance
* @name: name the handler was registered under
*
* Look for a handler named @name registered in @user_ns. A handler is not
* inherited from ancestor user namespaces: an entry can only name a handler
* registered in the same user namespace as its instance. The returned handler
* stays callable until binfmt_misc_put_ops() even if the backing struct_ops
* map is detached or deleted in the meantime.
*
* Return: the handler on success, NULL on failure
*/
const struct binfmt_misc_ops *binfmt_misc_get_ops(struct user_namespace *user_ns,
const char *name)
{
struct bm_bpf_ops_reg *reg;
guard(spinlock)(&bm_bpf_ops_lock);
reg = bm_bpf_ops_find(user_ns, name);
if (!reg)
return NULL;
if (!bpf_struct_ops_get(reg->ops))
return NULL;
return reg->ops;
}
void binfmt_misc_put_ops(const struct binfmt_misc_ops *ops)
{
bpf_struct_ops_put(ops);
}
bool bpf_prog_is_binfmt_misc_ops(const struct bpf_prog *prog)
{
return prog->type == BPF_PROG_TYPE_STRUCT_OPS &&
prog->aux->st_ops == &bpf_binfmt_misc_ops;
}
/*
* Replace the staged interpreter selection: naming a path drops a bound
* file, selecting a bound interpreter carries its file along.
*/
static void bm_bpf_stage_selection(struct linux_binprm *bprm, char *path,
struct file *f)
{
if (bprm->bpf_interp_file)
fput(bprm->bpf_interp_file);
kfree(bprm->bpf_interp);
bprm->bpf_interp = path;
bprm->bpf_interp_file = f;
}
__bpf_kfunc_start_defs();
/**
* bpf_binprm_set_interp - select the interpreter for the current exec
* @bprm: binary that is being executed
* @path: absolute path to the interpreter
* @path__sz: size of the @path buffer, including the terminating NUL
*
* To be called from the load program of a struct binfmt_misc_ops handler
* before returning zero; the verifier rejects the call from any other
* program, including the handler's own match program. The path is opened
* with the credentials of the task doing the exec after the program
* returns. Calling it again replaces the selection, as does selecting an
* interpreter the entry bound with bpf_binprm_select_interp().
*
* Return: 0 on success, a negative errno on failure
*/
__bpf_kfunc int bpf_binprm_set_interp(struct linux_binprm *bprm,
const char *path, size_t path__sz)
{
size_t len;
char *interp;
if (!path__sz)
return -EINVAL;
len = strnlen(path, path__sz);
if (len == path__sz)
return -EINVAL;
if (path[0] != '/')
return -EINVAL;
if (len >= PATH_MAX)
return -ENAMETOOLONG;
interp = kmemdup_nul(path, len, GFP_KERNEL);
if (!interp)
return -ENOMEM;
bm_bpf_stage_selection(bprm, interp, NULL);
return 0;
}
/**
* bpf_binprm_select_interp - run this exec under an interpreter the entry bound
* @bprm: binary that is being executed
* @name: name the interpreter was registered under
* @name__sz: size of the @name buffer, including the terminating NUL
*
* To be called from the load program of a struct binfmt_misc_ops handler
* instead of bpf_binprm_set_interp(). It selects one of the interpreters
* the matched entry was registered with, each of which was opened once when
* the entry was registered. Nothing is resolved at exec time, so no
* filesystem view can redirect the interpreter.
*
* The interpreter runs under the path the entry registered it under.
* Calling it again replaces the selection.
*
* Return: 0 on success, -ENOENT if the matched entry bound no interpreter
* of that name, a negative errno on failure
*/
__bpf_kfunc int bpf_binprm_select_interp(struct linux_binprm *bprm,
const char *name, size_t name__sz)
{
const struct binfmt_misc_interp *interp;
size_t len;
char *path;
if (!name__sz)
return -EINVAL;
len = strnlen(name, name__sz);
if (len == name__sz || !len)
return -EINVAL;
interp = binfmt_misc_find_interp(bprm->bpf_interps, name);
if (!interp)
return -ENOENT;
path = kstrdup(interp->path, GFP_KERNEL);
if (!path)
return -ENOMEM;
bm_bpf_stage_selection(bprm, path, get_file(interp->file));
return 0;
}
/**
* bpf_binprm_set_interp_arg - set a single argument for the interpreter
* @bprm: binary that is being executed
* @arg: argument to pass to the interpreter
* @arg__sz: size of the @arg buffer, including the terminating NUL
*
* To be called from the load program of a struct binfmt_misc_ops handler. The
* argument is passed to the interpreter ahead of the binary, mirroring the
* single optional argument of a #! interpreter line. Calling it again
* replaces the argument.
*
* Return: 0 on success, a negative errno on failure
*/
__bpf_kfunc int bpf_binprm_set_interp_arg(struct linux_binprm *bprm,
const char *arg, size_t arg__sz)
{
size_t len;
char *val;
if (!arg__sz)
return -EINVAL;
len = strnlen(arg, arg__sz);
if (len == arg__sz)
return -EINVAL;
if (!len)
return -EINVAL;
val = kmemdup_nul(arg, len, GFP_KERNEL);
if (!val)
return -ENOMEM;
kfree(bprm->bpf_interp_arg);
bprm->bpf_interp_arg = val;
return 0;
}
/**
* bpf_binprm_set_flags - choose the interpreter invocation flags for this exec
* @bprm: binary that is being executed
* @flags: an OR of enum bpf_binprm_flags values
*
* To be called from the load program of a struct binfmt_misc_ops handler. It
* decides per exec what a static entry fixes at registration with the P, C,
* 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 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_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;
return 0;
}
__bpf_kfunc_end_defs();
BTF_KFUNCS_START(bm_bpf_kfunc_ids)
BTF_ID_FLAGS(func, bpf_binprm_set_interp, KF_SLEEPABLE)
BTF_ID_FLAGS(func, bpf_binprm_select_interp, KF_SLEEPABLE)
BTF_ID_FLAGS(func, bpf_binprm_set_interp_arg, KF_SLEEPABLE)
BTF_ID_FLAGS(func, bpf_binprm_set_flags, KF_SLEEPABLE)
BTF_KFUNCS_END(bm_bpf_kfunc_ids)
static int bm_bpf_kfunc_filter(const struct bpf_prog *prog, u32 kfunc_id)
{
if (!btf_id_set8_contains(&bm_bpf_kfunc_ids, kfunc_id))
return 0;
if (prog->type != BPF_PROG_TYPE_STRUCT_OPS)
return -EACCES;
/* ->st_ops is unset during the cfg pass; enforced once it is set. */
if (!prog->aux->st_ops)
return 0;
/* Only the load program decides how a binary is run. */
if (bpf_prog_is_binfmt_misc_ops(prog) &&
prog->aux->attach_st_ops_member_off == offsetof(struct binfmt_misc_ops, load))
return 0;
return -EACCES;
}
static const struct btf_kfunc_id_set bm_bpf_kfunc_set = {
.owner = THIS_MODULE,
.set = &bm_bpf_kfunc_ids,
.filter = bm_bpf_kfunc_filter,
};
static bool bm_bpf_ops__match(struct linux_binprm *bprm)
{
return false;
}
static int bm_bpf_ops__load(struct linux_binprm *bprm)
{
return 0;
}
static struct binfmt_misc_ops bm_bpf_ops_stubs = {
.match = bm_bpf_ops__match,
.load = bm_bpf_ops__load,
};
static int bm_bpf_init(struct btf *btf)
{
return register_btf_kfunc_id_set(BPF_PROG_TYPE_STRUCT_OPS,
&bm_bpf_kfunc_set);
}
static int bm_bpf_check_member(const struct btf_type *t,
const struct btf_member *member,
const struct bpf_prog *prog)
{
u32 moff = __btf_member_bit_offset(t, member) / 8;
switch (moff) {
case offsetof(struct binfmt_misc_ops, match):
case offsetof(struct binfmt_misc_ops, load):
/* Reliable file reads at exec time require sleeping. */
if (!prog->sleepable)
return -EINVAL;
break;
}
return 0;
}
static int bm_bpf_init_member(const struct btf_type *t,
const struct btf_member *member,
void *kdata, const void *udata)
{
const struct binfmt_misc_ops *uops = udata;
struct binfmt_misc_ops *ops = kdata;
u32 moff = __btf_member_bit_offset(t, member) / 8;
switch (moff) {
case offsetof(struct binfmt_misc_ops, name):
if (bpf_obj_name_cpy(ops->name, uops->name,
sizeof(ops->name)) <= 0)
return -EINVAL;
return 1;
}
return 0;
}
static int bm_bpf_validate(void *kdata)
{
struct binfmt_misc_ops *ops = kdata;
if (!ops->match || !ops->load)
return -EINVAL;
return 0;
}
static int bm_bpf_reg(void *kdata, struct bpf_link *link)
{
struct binfmt_misc_ops *ops = kdata;
struct bm_bpf_ops_reg *reg;
reg = kzalloc_obj(*reg, GFP_KERNEL_ACCOUNT);
if (!reg)
return -ENOMEM;
reg->ops = ops;
reg->link = link;
reg->user_ns = get_user_ns(current_user_ns());
guard(spinlock)(&bm_bpf_ops_lock);
if (bm_bpf_ops_find(reg->user_ns, ops->name)) {
put_user_ns(reg->user_ns);
kfree(reg);
return -EEXIST;
}
list_add(&reg->list, &bm_bpf_ops_list);
return 0;
}
static void bm_bpf_unreg(void *kdata, struct bpf_link *link)
{
struct bm_bpf_ops_reg *reg;
guard(spinlock)(&bm_bpf_ops_lock);
list_for_each_entry(reg, &bm_bpf_ops_list, list) {
if (reg->ops == kdata && reg->link == link) {
list_del(&reg->list);
put_user_ns(reg->user_ns);
kfree(reg);
return;
}
}
}
static const struct bpf_verifier_ops bm_bpf_verifier_ops = {
.get_func_proto = bpf_base_func_proto,
.is_valid_access = bpf_tracing_btf_ctx_access,
};
static struct bpf_struct_ops bpf_binfmt_misc_ops = {
.verifier_ops = &bm_bpf_verifier_ops,
.init = bm_bpf_init,
.check_member = bm_bpf_check_member,
.init_member = bm_bpf_init_member,
.validate = bm_bpf_validate,
.reg = bm_bpf_reg,
.unreg = bm_bpf_unreg,
.cfi_stubs = &bm_bpf_ops_stubs,
.name = "binfmt_misc_ops",
.owner = THIS_MODULE,
};
static int __init bm_bpf_struct_ops_init(void)
{
return register_bpf_struct_ops(&bpf_binfmt_misc_ops, binfmt_misc_ops);
}
late_initcall(bm_bpf_struct_ops_init);

View File

@@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2024 Google LLC. */
#include <linux/binfmt_misc.h>
#include <linux/bpf.h>
#include <linux/bpf_lsm.h>
#include <linux/btf.h>
@@ -392,10 +393,25 @@ BTF_ID_FLAGS(func, bpf_remove_dentry_xattr, KF_SLEEPABLE)
BTF_ID_FLAGS(func, bpf_real_data_inode, KF_SLEEPABLE | KF_RET_NULL)
BTF_KFUNCS_END(bpf_fs_kfunc_set_ids)
/* Side-effecting kfuncs that stay exclusive to LSM programs. */
BTF_SET_START(bpf_fs_kfunc_lsm_only_ids)
BTF_ID(func, bpf_set_dentry_xattr)
BTF_ID(func, bpf_remove_dentry_xattr)
BTF_SET_END(bpf_fs_kfunc_lsm_only_ids)
static int bpf_fs_kfuncs_filter(const struct bpf_prog *prog, u32 kfunc_id)
{
if (!btf_id_set8_contains(&bpf_fs_kfunc_set_ids, kfunc_id) ||
prog->type == BPF_PROG_TYPE_LSM)
if (!btf_id_set8_contains(&bpf_fs_kfunc_set_ids, kfunc_id))
return 0;
if (prog->type == BPF_PROG_TYPE_LSM)
return 0;
if (prog->type != BPF_PROG_TYPE_STRUCT_OPS)
return -EACCES;
/* ->st_ops is unset during the cfg pass; enforced once it is set. */
if (!prog->aux->st_ops)
return 0;
if (bpf_prog_is_binfmt_misc_ops(prog) &&
!btf_id_set_contains(&bpf_fs_kfunc_lsm_only_ids, kfunc_id))
return 0;
return -EACCES;
}
@@ -438,7 +454,13 @@ static const struct btf_kfunc_id_set bpf_fs_kfunc_set = {
static int __init bpf_fs_kfuncs_init(void)
{
return register_btf_kfunc_id_set(BPF_PROG_TYPE_LSM, &bpf_fs_kfunc_set);
int ret;
ret = register_btf_kfunc_id_set(BPF_PROG_TYPE_LSM, &bpf_fs_kfunc_set);
if (ret || !IS_ENABLED(CONFIG_BINFMT_MISC_BPF))
return ret;
return register_btf_kfunc_id_set(BPF_PROG_TYPE_STRUCT_OPS,
&bpf_fs_kfunc_set);
}
late_initcall(bpf_fs_kfuncs_init);

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,11 +1470,16 @@ 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);
kfree(bprm->bpf_interp);
if (bprm->bpf_interp_file)
fput(bprm->bpf_interp_file);
kfree(bprm->bpf_interp_arg);
kfree(bprm->fdpath);
kfree(bprm);
}
@@ -1729,19 +1791,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);

113
include/linux/binfmt_misc.h Normal file
View File

@@ -0,0 +1,113 @@
/* SPDX-License-Identifier: GPL-2.0 */
#ifndef _LINUX_BINFMT_MISC_H
#define _LINUX_BINFMT_MISC_H
#include <linux/types.h>
struct bpf_prog;
struct file;
struct linux_binprm;
struct ucounts;
struct user_namespace;
#define BINFMT_MISC_OPS_NAME_MAX 16
/* Longest name a 'B' entry can bind an interpreter under. */
#define BINFMT_MISC_INTERP_NAME_MAX 32
/* Most interpreters one entry can bind. */
#define BINFMT_MISC_INTERP_MAX 100
/**
* struct binfmt_misc_interp - an interpreter an entry was registered with
* @list: link in the entry's list, in registration order
* @file: the file, opened at registration and never resolved again
* @ucounts: the UCOUNT_BINFMT_MISC_INTERPRETERS charge the binding took
* @path: the path it was registered under, used as the name the interpreter
* runs under; stored after @name in the same allocation
* @name: the name the load program selects it by; empty for the fixed
* interpreter of a static 'F' entry
*
* Owned by the entry and living exactly as long as it does. The list head
* is handed to the handler's load program for the duration of one exec,
* which picks one with bpf_binprm_select_interp().
*/
struct binfmt_misc_interp {
struct list_head list;
struct file *file;
struct ucounts *ucounts;
const char *path;
char name[];
};
const struct binfmt_misc_interp *
binfmt_misc_find_interp(const struct list_head *interps, const char *name);
/**
* enum bpf_binprm_flags - per-exec invocation flags a load program can request
* @BPF_BINPRM_PRESERVE_ARGV0: keep the caller's argv[0] (like the 'P' flag)
* @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.
*/
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),
};
/**
* struct binfmt_misc_ops - bpf-backed binary type handler
* @match: decide whether the handler applies to @bprm; consulted from the
* entry lookup walk like static magic and extension matching, in
* registration order with first-match-wins semantics; sleepable,
* so it can read the binary to decide, but the verifier rejects
* the interpreter selection kfuncs in it
* @load: select an interpreter for the matched @bprm via
* bpf_binprm_set_interp(), or one the entry bound via
* bpf_binprm_select_interp(), and return zero; a match is
* committed, so a failure fails the exec instead of falling
* through to later entries; -ENOEXEC does not fail the exec but
* moves on to the remaining binary formats
* @name: name that 'B' entries reference the handler by
*/
struct binfmt_misc_ops {
bool (*match)(struct linux_binprm *bprm);
int (*load)(struct linux_binprm *bprm);
char name[BINFMT_MISC_OPS_NAME_MAX];
};
#ifdef CONFIG_BINFMT_MISC_BPF
const struct binfmt_misc_ops *binfmt_misc_get_ops(struct user_namespace *user_ns,
const char *name);
void binfmt_misc_put_ops(const struct binfmt_misc_ops *ops);
bool bpf_prog_is_binfmt_misc_ops(const struct bpf_prog *prog);
#else
static inline const struct binfmt_misc_ops *
binfmt_misc_get_ops(struct user_namespace *user_ns, const char *name)
{
return NULL;
}
static inline void binfmt_misc_put_ops(const struct binfmt_misc_ops *ops)
{
}
static inline bool bpf_prog_is_binfmt_misc_ops(const struct bpf_prog *prog)
{
return false;
}
#endif /* CONFIG_BINFMT_MISC_BPF */
#endif /* _LINUX_BINFMT_MISC_H */

View File

@@ -12,6 +12,16 @@ struct coredump_params;
#define CORENAME_MAX_SIZE 128
/* Interpreter selection staged by a bpf binfmt_misc handler. */
struct binfmt_misc_bpf {
/* interpreters the matched entry bound, selectable by name */
const struct list_head *bpf_interps;
const char *bpf_interp; /* interpreter selected by a bpf handler */
struct file *bpf_interp_file; /* the bound interpreter it selected */
const char *bpf_interp_arg; /* interpreter argument from a bpf handler */
u64 bpf_flags; /* enum bpf_binprm_flags from a bpf handler */
};
/*
* This structure is used to hold the arguments that are used when loading binaries.
*/
@@ -55,6 +65,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_*) */
@@ -65,6 +76,7 @@ struct linux_binprm {
of the time same as filename, but could be
different for binfmt_{misc,script} */
const char *fdpath; /* generated filename for execveat */
struct binfmt_misc_bpf; /* bpf handler interpreter selection */
unsigned interp_flags;
int execfd; /* File descriptor of the executable */
unsigned long exec;
@@ -85,6 +97,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.
@@ -101,8 +135,8 @@ struct linux_binfmt {
#if IS_ENABLED(CONFIG_BINFMT_MISC)
struct binfmt_misc {
struct list_head entries;
rwlock_t entries_lock;
struct hlist_head entries;
spinlock_t entries_lock;
bool enabled;
} __randomize_layout;
@@ -129,6 +163,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

@@ -57,6 +57,9 @@ enum ucount_type {
#ifdef CONFIG_FANOTIFY
UCOUNT_FANOTIFY_GROUPS,
UCOUNT_FANOTIFY_MARKS,
#endif
#if IS_ENABLED(CONFIG_BINFMT_MISC)
UCOUNT_BINFMT_MISC_INTERPRETERS,
#endif
UCOUNT_COUNTS,
};

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

@@ -4,6 +4,7 @@
#include <linux/sysctl.h>
#include <linux/slab.h>
#include <linux/cred.h>
#include <linux/export.h>
#include <linux/hash.h>
#include <linux/kmemleak.h>
#include <linux/user_namespace.h>
@@ -89,6 +90,9 @@ static const struct ctl_table user_table[] = {
UCOUNT_ENTRY("max_fanotify_groups"),
UCOUNT_ENTRY("max_fanotify_marks"),
#endif
#if IS_ENABLED(CONFIG_BINFMT_MISC)
UCOUNT_ENTRY("max_binfmt_misc_interpreters"),
#endif
};
#endif /* CONFIG_SYSCTL */
@@ -233,6 +237,7 @@ struct ucounts *inc_ucount(struct user_namespace *ns, kuid_t uid,
put_ucounts(ucounts);
return NULL;
}
EXPORT_SYMBOL_FOR_MODULES(inc_ucount, "binfmt_misc");
void dec_ucount(struct ucounts *ucounts, enum ucount_type type)
{
@@ -243,6 +248,7 @@ void dec_ucount(struct ucounts *ucounts, enum ucount_type type)
}
put_ucounts(ucounts);
}
EXPORT_SYMBOL_FOR_MODULES(dec_ucount, "binfmt_misc");
long inc_rlimit_ucounts(struct ucounts *ucounts, enum rlimit_type type, long v)
{

View File

@@ -23,9 +23,9 @@
#if IS_ENABLED(CONFIG_BINFMT_MISC)
struct binfmt_misc init_binfmt_misc = {
.entries = LIST_HEAD_INIT(init_binfmt_misc.entries),
.entries = HLIST_HEAD_INIT,
.enabled = true,
.entries_lock = __RW_LOCK_UNLOCKED(init_binfmt_misc.entries_lock),
.entries_lock = __SPIN_LOCK_UNLOCKED(init_binfmt_misc.entries_lock),
};
EXPORT_SYMBOL_GPL(init_binfmt_misc);
#endif

View File

@@ -19,3 +19,14 @@ null-argv
xxxxxxxx*
pipe
S_I*.test
binfmt_misc_bpf
binfmt_misc_interplimit
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,9 +21,56 @@ TEST_GEN_PROGS += recursion-depth
TEST_GEN_PROGS += null-argv
TEST_GEN_PROGS += check-exec
# binfmt_misc must not be reachable as an exec source or as a stacking layer,
# or an 'F' entry can pin the instance that owns it. Unprivileged, no bpf.
TEST_GEN_PROGS += binfmt_misc_selfpin
# The interpreters an 'F' or 'B' entry pre-opens are charged against
# UCOUNT_BINFMT_MISC_INTERPRETERS. Unprivileged, no bpf.
TEST_GEN_PROGS += binfmt_misc_interplimit
# 'D' (register disabled) binfmt_misc test: an entry that exists but does
# not dispatch until it is enabled. Static magic entry, no bpf toolchain.
TEST_GEN_PROGS += binfmt_misc_disabled
# 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
# (HAVE_BPF_TOOLCHAIN=y forces it) so the other exec selftests don't grow
# a bpf toolchain dependency.
CLANG ?= clang
BPFTOOL ?= bpftool
VMLINUX_BTF ?= /sys/kernel/btf/vmlinux
HAVE_BPF_TOOLCHAIN ?= $(shell command -v $(CLANG) >/dev/null 2>&1 && \
command -v $(BPFTOOL) >/dev/null 2>&1 && \
test -r $(VMLINUX_BTF) && \
pkg-config --exists libbpf 2>/dev/null && echo y)
ifeq ($(HAVE_BPF_TOOLCHAIN),y)
TEST_GEN_PROGS += binfmt_misc_bpf
TEST_GEN_FILES += bpf_interp.bpf.o nix_origin.bpf.o transparent.bpf.o
TEST_GEN_FILES += loader.bpf.o interp_bind.bpf.o
TEST_GEN_FILES += binfmt_bpf_interp binfmt_bpf_app binfmt_bind_interp
else
$(info exec selftests: skipping binfmt_misc_bpf, needs clang, bpftool, vmlinux BTF and libbpf)
endif
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
@@ -55,3 +102,49 @@ $(OUTPUT)/script-exec.inc: $(CHECK_EXEC_SAMPLES)/script-exec.inc
cp $< $@
$(OUTPUT)/script-noexec.inc: $(CHECK_EXEC_SAMPLES)/script-noexec.inc
cp $< $@
# Reuses setup_userns()/write_file() from the filesystems selftests. Their
# wrappers.h wants the uapi headers, so ask for them here rather than widening
# CFLAGS for every program in this directory.
$(OUTPUT)/binfmt_misc_selfpin: CFLAGS += $(TOOLS_INCLUDES)
$(OUTPUT)/binfmt_misc_selfpin: ../filesystems/utils.c
$(OUTPUT)/binfmt_misc_interplimit: CFLAGS += $(TOOLS_INCLUDES)
$(OUTPUT)/binfmt_misc_interplimit: ../filesystems/utils.c
# --- binfmt_misc bpf ('B') handler test ---------------------------------
# The struct_ops bpf objects are compiled against the running kernel's BTF.
# CLANG/BPFTOOL/VMLINUX_BTF are set above next to the toolchain check;
# override LIBBPF_CFLAGS/LDLIBS to point at a libbpf install.
BPF_CFLAGS ?= -I$(OUTPUT)
LIBBPF_CFLAGS ?=
LIBBPF_LDLIBS ?= -lbpf -lelf -lz
$(OUTPUT)/vmlinux.h:
$(BPFTOOL) btf dump file $(VMLINUX_BTF) format c > $@
# 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 -DBPF_NO_KFUNC_PROTOTYPES \
$(BPF_CFLAGS) $(LIBBPF_CFLAGS) -c $< -o $@
$(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_bind_interp: binfmt_bind_interp.c
$(CC) $(CFLAGS) $(LDFLAGS) $< -o $@
$(OUTPUT)/binfmt_loader_payload: binfmt_loader_payload.c binfmt_misc_common.h
$(CC) $(CFLAGS) $(LDFLAGS) -fPIE -pie $< -o $@
$(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.o

View File

@@ -0,0 +1,14 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Test interpreter for the bound-interpreter case of the binfmt_misc_bpf
* selftest. Two copies are installed at different paths and bound to one
* entry under different names; printing argv[0] - the path the kernel ran
* this copy under - tells the harness which of them the load program picked.
*/
#include <stdio.h>
int main(int argc, char **argv)
{
printf("BIND_RAN %s\n", argc > 0 ? argv[0] : "");
return 0;
}

View File

@@ -0,0 +1,12 @@
// SPDX-License-Identifier: GPL-2.0
/*
* A relocatable binary for the binfmt_misc_bpf $ORIGIN case. The Makefile
* links it with PT_INTERP set to the literal "$ORIGIN/binfmt_bpf_interp"
* (-Wl,--dynamic-linker), which the kernel ELF loader cannot resolve. The
* nix_origin bpf handler resolves it relative to this binary's directory and
* routes execution to the co-located interpreter.
*/
int main(void)
{
return 0;
}

View File

@@ -0,0 +1,15 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Test interpreter for the binfmt_misc_bpf selftest. A bpf-backed 'B' handler
* routes a matched binary here; printing this marker proves the program's
* chosen interpreter actually ran.
*/
#include <unistd.h>
int main(int argc, char **argv)
{
(void)argc;
(void)argv;
write(1, "BPF_INTERP_RAN\n", 15);
return 0;
}

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

@@ -0,0 +1,638 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Selftest for binfmt_misc bpf-backed ('B') handlers.
*
* A handler is a struct binfmt_misc_ops struct_ops map with a sleepable match
* and a sleepable load program. Attaching it publishes it by name in the
* caller's user namespace; a 'B' entry referencing it by name in the
* interpreter field activates it:
*
* echo ':name:B::::<handler>:' > /proc/sys/fs/binfmt_misc/register
*
* Five self-contained cases are exercised:
*
* 1. bpf_interp: the match program matches a synthetic aarch64 ELF header
* from the prefetched bprm->buf and the load program routes it to a
* fixed interpreter of its choosing.
* 2. nix_origin: the match program reads the binary's program headers to
* commit only to a "$ORIGIN/..."-relative PT_INTERP and the load program
* resolves it to an interpreter co-located with the binary (the
* relocatable-loader case the kernel ELF loader cannot express).
* 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.
* 5. interp_bind: an entry registered disabled with 'D' is given its
* interpreters one write at a time, and the load program picks one by
* name per exec. Replacing what the path holds afterwards changes
* nothing, which is the point of binding a file rather than resolving
* a name at exec time. Enabling the entry seals it.
*
* The first two route to a test interpreter that prints BPF_INTERP_RAN,
* proving the program's chosen interpreter actually ran.
*/
#define _GNU_SOURCE
#include <elf.h>
#include <limits.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.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_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"
#define BIND_FIRST "/tmp/binfmt_bind_first"
#define BIND_SECOND "/tmp/binfmt_bind_second"
#define BIND_ARM_PATH "/tmp/binfmt_bind_arm"
#define BIND_RISCV_PATH "/tmp/binfmt_bind_riscv"
#define BIND_EXPECT "BIND_RAN "
#define BIND_MAX 100
#define INTERP_LIMIT "/proc/sys/user/max_binfmt_misc_interpreters"
/* Exit status of the binding child when it cannot set up a budget of its own. */
#define BIND_NO_BUDGET 200
/* A minimal 64-bit little-endian ELF header, padded to the read size. */
static int create_fake_elf(const char *path, unsigned short machine)
{
unsigned char hdr[256] = {0};
int fd;
hdr[0] = 0x7f; hdr[1] = 'E'; hdr[2] = 'L'; hdr[3] = 'F';
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;
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)) {
close(fd);
return -1;
}
close(fd);
return 0;
}
/*
* Register a 'B' entry for @handler. With @flags "D" the entry is created
* disabled, which is what leaves it open to being given interpreters.
*/
static int register_entry(const char *name, const char *handler,
const char *flags)
{
char rule[PATH_MAX];
snprintf(rule, sizeof(rule), ":%s:B::::%s:%s", name, handler,
flags ? flags : "");
return write_reg(rule);
}
static int check_output(const char *cmd, const char *expected)
{
char buf[128];
FILE *fp;
fp = popen(cmd, "r");
if (!fp)
return -1;
if (!fgets(buf, sizeof(buf), fp)) {
pclose(fp);
return -1;
}
pclose(fp);
return strncmp(buf, expected, strlen(expected)) ? -1 : 0;
}
/* Does the kernel BTF know struct binfmt_misc_ops (CONFIG_BINFMT_MISC_BPF)? */
static bool have_binfmt_misc_ops(void)
{
struct btf *btf = btf__load_vmlinux_btf();
bool have;
have = btf && btf__find_by_name_kind(btf, "binfmt_misc_ops",
BTF_KIND_STRUCT) >= 0;
btf__free(btf);
return have;
}
/* The reason bpf handler cases cannot run here, NULL if they can. */
static const char *bpf_handler_unsupported(void)
{
if (getuid() != 0)
return "test must be run as root";
if (!have_binfmt_misc_ops())
return "no struct binfmt_misc_ops in the kernel BTF (CONFIG_BINFMT_MISC_BPF)";
if (!binfmt_misc_available())
return "no binfmt_misc";
return NULL;
}
/* An attached handler with its 'B' entry activated. */
struct bpf_case {
struct bpf_object *obj;
struct bpf_link *link;
const char *entry;
};
/*
* Load @objfile, attach its struct_ops map @handler (which publishes the
* handler) and register a 'B' entry named @entry that references it, with
* @flags as the entry's register-string flags.
*/
static int bpf_case_start_flags(struct bpf_case *c, const char *objfile,
const char *handler, const char *entry,
const char *flags)
{
struct bpf_map *map;
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(c->obj)) {
fprintf(stderr, "load %s failed (check dmesg for the verifier log)\n",
objfile);
goto fail;
}
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 fail;
}
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);
c->link = NULL;
goto fail;
}
if (register_entry(entry, handler, flags)) {
fprintf(stderr, "register 'B' entry '%s' failed\n", entry);
goto fail;
}
return 0;
fail:
bpf_link__destroy(c->link);
bpf_object__close(c->obj);
c->obj = NULL;
c->link = NULL;
return -1;
}
static int bpf_case_start(struct bpf_case *c, const char *objfile,
const char *handler, const char *entry)
{
return bpf_case_start_flags(c, objfile, handler, entry, NULL);
}
static void bpf_case_stop(struct bpf_case *c)
{
unregister(c->entry);
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);
bpf_case_stop(&c);
return ret;
}
FIXTURE(bpf_handler) {
char obj[PATH_MAX]; /* struct_ops object of the case under test */
};
FIXTURE_SETUP(bpf_handler)
{
char src[PATH_MAX];
const char *why = bpf_handler_unsupported();
if (why)
SKIP(return, "%s", why);
/* Shared test interpreter. */
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);
}
/* The errno an exec of @path fails with, 0 if it succeeded. */
static int exec_errno(const char *path)
{
int status;
pid_t pid;
pid = fork();
if (pid == 0) {
execl(path, path, (char *)NULL);
_exit(errno);
}
if (pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
return -1;
return WEXITSTATUS(status);
}
/* Install a copy of the bound-interpreter test binary at @path. */
static int install_interp(const char *path)
{
char src[PATH_MAX];
if (artifact_path(src, sizeof(src), "binfmt_bind_interp"))
return -1;
return copy_file(src, path);
}
/* Bind @path to @entry under @name, the '+' command of a disabled entry. */
static int entry_bind(const char *entry, const char *name, const char *path)
{
char cmd[PATH_MAX];
snprintf(cmd, sizeof(cmd), "+%s %s\n", name, path);
return entry_command(entry, cmd);
}
/* Set the interpreter budget of this namespace. */
static int write_interp_limit(const char *val)
{
ssize_t n;
int fd;
fd = open(INTERP_LIMIT, O_WRONLY | O_CLOEXEC);
if (fd < 0)
return -1;
n = write(fd, val, strlen(val));
close(fd);
return n < 0 ? -1 : 0;
}
/*
* The errno a bind is refused with when the writer is a child that has spent
* the budget of a user namespace of its own, 0 if it succeeded and -1 if the
* child could not set itself up. The fd is opened here and inherited, so the
* interpreter is still opened with this process's credentials.
*/
static int bind_out_of_budget(const char *entry, const char *name,
const char *path)
{
char cmd[PATH_MAX], file[PATH_MAX];
int fd, status, retval;
pid_t pid;
snprintf(file, sizeof(file), BINFMT_DIR "/%s", entry);
snprintf(cmd, sizeof(cmd), "+%s %s\n", name, path);
fd = open(file, O_WRONLY | O_CLOEXEC);
if (fd < 0)
return -1;
pid = fork();
if (pid == 0) {
ssize_t n;
/* A namespace of its own, with nothing left in it to spend. */
if (unshare(CLONE_NEWUSER) || write_interp_limit("0"))
_exit(BIND_NO_BUDGET);
n = write(fd, cmd, strlen(cmd));
_exit(n < 0 ? errno : 0);
}
close(fd);
if (pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
return -1;
retval = WEXITSTATUS(status);
return retval == BIND_NO_BUDGET ? -1 : retval;
}
FIXTURE(bound_interp) {
char obj[PATH_MAX];
struct bpf_case c;
bool started;
};
FIXTURE_SETUP(bound_interp)
{
const char *why = bpf_handler_unsupported();
if (why)
SKIP(return, "%s", why);
if (!binfmt_flag_supported('D')) {
ASSERT_EQ(errno, EINVAL);
SKIP(return, "kernel without the 'D' flag");
}
ASSERT_EQ(install_interp(BIND_FIRST), 0);
ASSERT_EQ(install_interp(BIND_SECOND), 0);
ASSERT_EQ(artifact_path(self->obj, sizeof(self->obj),
"interp_bind.bpf.o"), 0);
/*
* Registered disabled, so it cannot be matched yet and can still be
* given interpreters. Each path is resolved once, by its write(2);
* from here on the entry holds the files themselves.
*/
ASSERT_EQ(bpf_case_start_flags(&self->c, self->obj, "interp_bind",
"test_interp_bind", "D"), 0);
self->started = true;
ASSERT_EQ(entry_bind("test_interp_bind", "first", BIND_FIRST), 0);
ASSERT_EQ(entry_bind("test_interp_bind", "second", BIND_SECOND), 0);
}
FIXTURE_TEARDOWN(bound_interp)
{
if (self->started)
bpf_case_stop(&self->c);
unlink(BIND_FIRST);
unlink(BIND_SECOND);
unlink(AARCH64_PATH);
unlink(BIND_RISCV_PATH);
unlink(BIND_ARM_PATH);
}
/* Enabling is what makes the configured entry matchable. */
static int activate(const char *entry)
{
return entry_command(entry, "1\n");
}
/* One entry, one interpreter per guest architecture, picked per exec. */
TEST_F(bound_interp, selects_by_name)
{
ASSERT_EQ(create_fake_elf(AARCH64_PATH, EM_AARCH64), 0);
ASSERT_EQ(create_fake_elf(BIND_RISCV_PATH, EM_RISCV), 0);
/* Disabled, so it does not match and no format claims the binary. */
EXPECT_EQ(exec_errno(AARCH64_PATH), ENOEXEC);
ASSERT_EQ(activate("test_interp_bind"), 0);
EXPECT_EQ(check_output(AARCH64_PATH, BIND_EXPECT BIND_FIRST), 0);
EXPECT_EQ(check_output(BIND_RISCV_PATH, BIND_EXPECT BIND_SECOND), 0);
}
/* What was bound is what runs, whatever the path holds afterwards. */
TEST_F(bound_interp, path_no_longer_decides)
{
char other[PATH_MAX];
ASSERT_EQ(create_fake_elf(AARCH64_PATH, EM_AARCH64), 0);
ASSERT_EQ(activate("test_interp_bind"), 0);
/* Bound interpreters are pinned against writes, exactly like 'F'. */
EXPECT_TRUE(write_denied(BIND_FIRST));
/* Replace the path with a different binary: a new file, new inode. */
ASSERT_EQ(artifact_path(other, sizeof(other), "binfmt_bpf_interp"), 0);
ASSERT_EQ(unlink(BIND_FIRST), 0);
ASSERT_EQ(copy_file(other, BIND_FIRST), 0);
EXPECT_EQ(check_output(AARCH64_PATH, BIND_EXPECT BIND_FIRST), 0);
}
/* The entry reports what it bound, under the names it bound them as. */
TEST_F(bound_interp, entry_reports_bindings)
{
EXPECT_TRUE(entry_shows("test_interp_bind",
"bpf-interpreter first " BIND_FIRST));
EXPECT_TRUE(entry_shows("test_interp_bind",
"bpf-interpreter second " BIND_SECOND));
}
/* Selecting a name the entry did not bind fails the exec. */
TEST_F(bound_interp, unbound_name_fails)
{
ASSERT_EQ(create_fake_elf(BIND_ARM_PATH, EM_ARM), 0);
ASSERT_EQ(activate("test_interp_bind"), 0);
EXPECT_EQ(exec_errno(BIND_ARM_PATH), ENOENT);
}
/* Activating seals it: what can be matched cannot be changed. */
TEST_F(bound_interp, sealed_once_active)
{
ASSERT_EQ(activate("test_interp_bind"), 0);
EXPECT_EQ(entry_bind("test_interp_bind", "third", BIND_SECOND), -EBUSY);
EXPECT_FALSE(entry_shows("test_interp_bind",
"bpf-interpreter third " BIND_SECOND));
}
/* The seal is for good: disabling the entry again reopens nothing. */
TEST_F(bound_interp, disable_does_not_unseal)
{
ASSERT_EQ(activate("test_interp_bind"), 0);
ASSERT_EQ(entry_command("test_interp_bind", "0\n"), 0);
EXPECT_EQ(entry_bind("test_interp_bind", "third", BIND_SECOND), -EBUSY);
}
/* An entry registered without 'D' is sealed from the start. */
TEST_F(bound_interp, born_sealed)
{
/* A second entry for the handler the fixture already published. */
ASSERT_EQ(register_entry("test_born_sealed", "interp_bind", NULL), 0);
EXPECT_EQ(entry_bind("test_born_sealed", "first", BIND_FIRST), -EBUSY);
unregister("test_born_sealed");
}
/* A name is bound once; a second use of it is refused. */
TEST_F(bound_interp, duplicate_name_refused)
{
EXPECT_EQ(entry_bind("test_interp_bind", "first", BIND_SECOND), -EEXIST);
}
/* A name is a printable word: the entry file reports 'name path' lines. */
TEST_F(bound_interp, name_must_be_printable)
{
/* A control character would forge a line into the entry file. */
EXPECT_EQ(entry_bind("test_interp_bind", "a\tb", BIND_FIRST), -EINVAL);
EXPECT_EQ(entry_bind("test_interp_bind", "a\nb", BIND_FIRST), -EINVAL);
/* A space cannot even be spelled: the path starts after the first one. */
EXPECT_EQ(entry_bind("test_interp_bind", "a b", BIND_FIRST), -EINVAL);
}
/* The command ends at the write: bytes past an embedded nul are refused. */
TEST_F(bound_interp, trailing_bytes_refused)
{
char cmd[PATH_MAX];
size_t len;
int fd;
/* entry_command() cannot spell a nul, so write the buffer raw. */
snprintf(cmd, sizeof(cmd), "+nul %s", BIND_FIRST);
len = strlen(cmd) + 1;
memcpy(cmd + len, "junk", sizeof("junk"));
len += sizeof("junk");
fd = open(BINFMT_DIR "/test_interp_bind", O_WRONLY | O_CLOEXEC);
ASSERT_GE(fd, 0);
EXPECT_EQ(write(fd, cmd, len), -1);
EXPECT_EQ(errno, EINVAL);
close(fd);
EXPECT_FALSE(entry_shows("test_interp_bind",
"bpf-interpreter nul " BIND_FIRST));
}
/* An entry binds at most BIND_MAX interpreters. */
TEST_F(bound_interp, capped_bindings)
{
char name[16];
int i;
/* The fixture bound "first" and "second" already. */
for (i = 2; i < BIND_MAX; i++) {
snprintf(name, sizeof(name), "n%d", i);
ASSERT_EQ(entry_bind("test_interp_bind", name, BIND_FIRST), 0);
}
EXPECT_EQ(entry_bind("test_interp_bind", "over", BIND_FIRST), -ENOSPC);
}
/* A binding pins a file: it is charged, and refused once the budget is out. */
TEST_F(bound_interp, bindings_are_charged)
{
int err = bind_out_of_budget("test_interp_bind", "third", BIND_FIRST);
if (err < 0)
SKIP(return, "no user namespaces or no " INTERP_LIMIT);
/* The charge follows the writer, not the entry file it writes to. */
EXPECT_EQ(err, ENOSPC);
/* The budget was the only thing in the way. */
EXPECT_EQ(entry_bind("test_interp_bind", "third", BIND_FIRST), 0);
}
TEST_HARNESS_MAIN

View File

@@ -0,0 +1,315 @@
/* 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);
}
}
/* Write @line to @entry's file, reporting the errno it was refused with. */
static inline int entry_command(const char *entry, const char *line)
{
char path[PATH_MAX];
int fd, retval = 0;
size_t len = strlen(line);
snprintf(path, sizeof(path), BINFMT_DIR "/%s", entry);
fd = open(path, O_WRONLY | O_CLOEXEC);
if (fd < 0)
return -errno;
if (write(fd, line, len) != (ssize_t)len)
retval = -errno;
close(fd);
return retval;
}
/* Does @entry's file report @line? */
static inline bool entry_shows(const char *entry, const char *line)
{
char path[PATH_MAX], buf[PATH_MAX];
bool found = false;
FILE *fp;
snprintf(path, sizeof(path), BINFMT_DIR "/%s", entry);
fp = fopen(path, "r");
if (!fp)
return false;
while (fgets(buf, sizeof(buf), fp)) {
buf[strcspn(buf, "\n")] = '\0';
if (!strcmp(buf, line)) {
found = true;
break;
}
}
fclose(fp);
return found;
}
/* Mount binfmt_misc unless it already is, and report whether it is usable. */
static inline bool binfmt_misc_available(void)
{
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 bool binfmt_flag_supported(char flag)
{
char rule[64];
snprintf(rule, sizeof(rule), ":bm_flag_probe:E::bmprobe::/bin/true:%c",
flag);
if (write_reg(rule))
return false;
unregister("bm_flag_probe");
return true;
}
/*
* 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,172 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Test the 'D' (register disabled) flag of binfmt_misc. An entry
* registered with it exists but cannot be matched until userspace enables
* it, which splits a registration into create and activate.
*
* Needs root for the registration; no bpf toolchain involved.
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include "binfmt_misc_common.h"
#include "kselftest_harness.h"
#define MAGIC "#DISABLED-SELFTEST#"
#define TARGET_PATH "/tmp/binfmt_disabled_target"
#define INTERP_PATH "/tmp/binfmt_disabled_interp.sh"
#define ENTRY "test_disabled"
#define RULE(flags) ":" ENTRY ":M:0:" MAGIC "::" INTERP_PATH ":" flags
/* The interpreter exits with a code the harness can recognise. */
#define EXIT_INTERP 7
/* The target only has to carry the magic; it is never actually loaded. */
static int create_target(void)
{
char buf[128] = MAGIC "\n";
int fd;
unlink(TARGET_PATH);
fd = open(TARGET_PATH, O_WRONLY | O_CREAT | O_EXCL, 0755);
if (fd < 0)
return -1;
if (write(fd, buf, sizeof(buf)) != (ssize_t)sizeof(buf)) {
close(fd);
return -1;
}
close(fd);
return 0;
}
static int create_interp(void)
{
char buf[64];
int fd;
unlink(INTERP_PATH);
fd = open(INTERP_PATH, O_WRONLY | O_CREAT | O_EXCL, 0755);
if (fd < 0)
return -1;
snprintf(buf, sizeof(buf), "#!/bin/sh\nexit %d\n", EXIT_INTERP);
if (write(fd, buf, strlen(buf)) != (ssize_t)strlen(buf)) {
close(fd);
return -1;
}
return close(fd);
}
FIXTURE(disabled) {
};
FIXTURE_SETUP(disabled)
{
if (getuid() != 0)
SKIP(return, "test must be run as root");
if (!binfmt_misc_available())
SKIP(return, "no binfmt_misc");
/* Skip the whole suite on a kernel that does not know 'D'. */
if (!binfmt_flag_supported('D')) {
ASSERT_EQ(errno, EINVAL);
SKIP(return, "kernel without the 'D' flag");
}
ASSERT_EQ(create_interp(), 0);
ASSERT_EQ(create_target(), 0);
}
FIXTURE_TEARDOWN(disabled)
{
unregister(ENTRY);
unlink(TARGET_PATH);
unlink(INTERP_PATH);
}
/* The entry exists but does not dispatch until it is enabled. */
TEST_F(disabled, inert_until_enabled)
{
ASSERT_EQ(write_reg(RULE("D")), 0);
EXPECT_TRUE(entry_shows(ENTRY, "disabled"));
/* Nothing matches it, so no binary format claims the target. */
EXPECT_EQ(run_payload(TARGET_PATH), RUN_ENOEXEC);
ASSERT_EQ(entry_command(ENTRY, "1\n"), 0);
EXPECT_TRUE(entry_shows(ENTRY, "enabled"));
EXPECT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);
}
/* Without 'D' an entry is matchable the moment it is registered. */
TEST_F(disabled, enabled_without_the_flag)
{
ASSERT_EQ(write_reg(RULE("")), 0);
EXPECT_TRUE(entry_shows(ENTRY, "enabled"));
EXPECT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);
}
/* 'D' is spent on the registration: the entry does not report it back. */
TEST_F(disabled, flag_not_reported)
{
ASSERT_EQ(write_reg(RULE("D")), 0);
EXPECT_FALSE(entry_shows(ENTRY, "flags: D"));
EXPECT_TRUE(entry_shows(ENTRY, "flags: "));
}
/* A disabled entry can be disabled and enabled like any other. */
TEST_F(disabled, toggles_like_any_entry)
{
ASSERT_EQ(write_reg(RULE("D")), 0);
ASSERT_EQ(entry_command(ENTRY, "1\n"), 0);
ASSERT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);
ASSERT_EQ(entry_command(ENTRY, "0\n"), 0);
EXPECT_EQ(run_payload(TARGET_PATH), RUN_ENOEXEC);
ASSERT_EQ(entry_command(ENTRY, "1\n"), 0);
EXPECT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);
}
/* 'D' composes with the invocation flags a static entry can carry. */
TEST_F(disabled, composes_with_invocation_flags)
{
ASSERT_EQ(write_reg(RULE("PD")), 0);
EXPECT_TRUE(entry_shows(ENTRY, "disabled"));
EXPECT_TRUE(entry_shows(ENTRY, "flags: P"));
}
/* '-1' to the status file sweeps a staged entry with everything else. */
TEST_F(disabled, removed_by_remove_all)
{
int fd;
ASSERT_EQ(write_reg(RULE("D")), 0);
EXPECT_TRUE(entry_shows(ENTRY, "disabled"));
fd = open(BINFMT_DIR "/status", O_WRONLY | O_CLOEXEC);
ASSERT_GE(fd, 0);
ASSERT_EQ(write(fd, "-1", 2), 2);
close(fd);
EXPECT_NE(access(BINFMT_DIR "/" ENTRY, F_OK), 0);
}
/* A file handle held across a removal cannot resurrect the entry. */
TEST_F(disabled, no_resurrection_after_remove)
{
int fd;
ASSERT_EQ(write_reg(RULE("D")), 0);
fd = open(BINFMT_DIR "/" ENTRY, O_WRONLY | O_CLOEXEC);
ASSERT_GE(fd, 0);
ASSERT_EQ(write(fd, "-1", 2), 2);
EXPECT_NE(access(BINFMT_DIR "/" ENTRY, F_OK), 0);
/* Accepted like any toggle of a removed entry, but publishes nothing. */
EXPECT_EQ(write(fd, "1", 1), 1);
EXPECT_EQ(run_payload(TARGET_PATH), RUN_ENOEXEC);
close(fd);
}
TEST_HARNESS_MAIN

View File

@@ -0,0 +1,232 @@
// SPDX-License-Identifier: GPL-2.0
/*
* A pre-opened interpreter - what 'F' gives a static entry and what a 'B'
* entry binds - keeps a file open for as long as the entry lives, so it pins
* the mount it came from. It costs no file descriptor, and binfmt_misc is
* FS_USERNS_MOUNT, so an unprivileged user namespace can create them without
* bound. Check that UCOUNT_BINFMT_MISC_INTERPRETERS bounds it, that an entry
* that pre-opens nothing is not charged, that removing an entry gives the
* charge back, and that nesting a user namespace does not evade it.
*
* Runs unprivileged in a user namespace.
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <unistd.h>
#include "../filesystems/utils.h"
#include "kselftest_harness.h"
#define MNT "/tmp/binfmt_interplimit"
#define NESTED_MNT "/tmp/binfmt_interplimit_nested"
#define LIMIT_SYSCTL "/proc/sys/user/max_binfmt_misc_interpreters"
#define MAGIC "\\xde\\xad"
/* Not on the instance, and unlike /bin/true it always exists. */
#define INTERP "/proc/self/exe"
/* Small enough to fill by hand, big enough that a refund is visible. */
#define LIMIT 4
/* What UCOUNT_ENTRY() lets a namespace raise its own limit to. */
#define LIMIT_MAX "2147483647"
static int ensure_dir(const char *path)
{
if (mkdir(path, 0755) && errno != EEXIST)
return -1;
return 0;
}
/* Write @val to @path, preserving write(2)'s errno for the caller. */
static int write_keep_errno(const char *path, const char *val)
{
int fd, saved;
ssize_t n;
fd = open(path, O_WRONLY | O_CLOEXEC);
if (fd < 0)
return -1;
n = write(fd, val, strlen(val));
saved = errno;
close(fd);
errno = saved;
return n < 0 ? -1 : 0;
}
static int set_limit(const char *val)
{
return write_keep_errno(LIMIT_SYSCTL, val);
}
static int register_at(const char *mnt, const char *rule)
{
char path[PATH_MAX];
snprintf(path, sizeof(path), "%s/register", mnt);
return write_keep_errno(path, rule);
}
/* An 'F' entry: one interpreter pre-opened at registration, one charge. */
static int register_fixed(const char *mnt, const char *name)
{
char rule[PATH_MAX];
snprintf(rule, sizeof(rule), ":%s:M::" MAGIC "::" INTERP ":F", name);
return register_at(mnt, rule);
}
/* The same entry without 'F': the interpreter is opened per exec instead. */
static int register_plain(const char *mnt, const char *name)
{
char rule[PATH_MAX];
snprintf(rule, sizeof(rule), ":%s:M::" MAGIC "::" INTERP ":", name);
return register_at(mnt, rule);
}
static int remove_entry(const char *mnt, const char *name)
{
char path[PATH_MAX];
snprintf(path, sizeof(path), "%s/%s", mnt, name);
return write_keep_errno(path, "-1\n");
}
static bool entry_exists(const char *mnt, const char *name)
{
char path[PATH_MAX];
snprintf(path, sizeof(path), "%s/%s", mnt, name);
return access(path, F_OK) == 0;
}
/* Register @n 'F' entries, each with a name of its own. */
static int fill_budget(const char *mnt, unsigned int n)
{
char name[32];
unsigned int i;
for (i = 0; i < n; i++) {
snprintf(name, sizeof(name), "fixed%u", i);
if (register_fixed(mnt, name))
return -1;
}
return 0;
}
FIXTURE(interp_limit) {
};
FIXTURE_SETUP(interp_limit)
{
/* setup_userns() exits rather than returns if this is not there. */
if (access("/proc/self/ns/user", F_OK))
SKIP(return, "kernel without user namespaces");
ASSERT_EQ(setup_userns(), 0);
/* CAP_SYS_RESOURCE in this namespace is what makes it writable. */
if (set_limit(LIMIT_MAX)) {
if (errno == ENOENT)
SKIP(return, "kernel without " LIMIT_SYSCTL);
SKIP(return, "cannot set the limit: %s", strerror(errno));
}
ASSERT_EQ(ensure_dir(MNT), 0);
if (mount("binfmt_misc", MNT, "binfmt_misc", 0, NULL)) {
int saved = errno;
/* Teardown doesn't run when setup skips, so clean up here. */
rmdir(MNT);
SKIP(return, "no binfmt_misc: %s", strerror(saved));
}
}
FIXTURE_TEARDOWN(interp_limit)
{
/* The namespaces go with the process; just don't litter /tmp. */
umount2(NESTED_MNT, MNT_DETACH);
umount2(MNT, MNT_DETACH);
rmdir(NESTED_MNT);
rmdir(MNT);
}
/* Every pre-opened interpreter is charged, and the budget is a hard stop. */
TEST_F(interp_limit, fixed_interpreters_are_charged)
{
char buf[32];
snprintf(buf, sizeof(buf), "%u", LIMIT);
ASSERT_EQ(set_limit(buf), 0);
ASSERT_EQ(fill_budget(MNT, LIMIT), 0);
EXPECT_NE(register_fixed(MNT, "over"), 0);
EXPECT_EQ(errno, ENOSPC);
/* A refused registration leaves nothing behind. */
EXPECT_FALSE(entry_exists(MNT, "over"));
}
/* An entry that pre-opens nothing pins nothing, so it is not charged. */
TEST_F(interp_limit, plain_entries_are_not_charged)
{
ASSERT_EQ(set_limit("0"), 0);
EXPECT_EQ(register_plain(MNT, "plain"), 0);
EXPECT_TRUE(entry_exists(MNT, "plain"));
/* ... while the same entry with 'F' has nothing to spend. */
EXPECT_NE(register_fixed(MNT, "fixed"), 0);
EXPECT_EQ(errno, ENOSPC);
}
/* Removing an entry closes its interpreters and gives the charge back. */
TEST_F(interp_limit, removal_refunds_the_charge)
{
char buf[32];
snprintf(buf, sizeof(buf), "%u", LIMIT);
ASSERT_EQ(set_limit(buf), 0);
ASSERT_EQ(fill_budget(MNT, LIMIT), 0);
ASSERT_NE(register_fixed(MNT, "over"), 0);
ASSERT_EQ(remove_entry(MNT, "fixed0"), 0);
EXPECT_EQ(register_fixed(MNT, "over"), 0);
}
/*
* The charge walks the ancestors, so a namespace cannot buy itself budget by
* nesting: it may raise only its own limit, and the parent it was created
* from is charged for every binding made below it.
*/
TEST_F(interp_limit, nesting_does_not_evade_it)
{
char buf[32];
snprintf(buf, sizeof(buf), "%u", LIMIT);
ASSERT_EQ(set_limit(buf), 0);
ASSERT_EQ(fill_budget(MNT, LIMIT), 0);
ASSERT_EQ(setup_userns(), 0);
ASSERT_EQ(set_limit(LIMIT_MAX), 0);
ASSERT_EQ(ensure_dir(NESTED_MNT), 0);
ASSERT_EQ(mount("binfmt_misc", NESTED_MNT, "binfmt_misc", 0, NULL), 0);
/* A fresh instance with an unlimited budget of its own, and yet: */
EXPECT_NE(register_fixed(NESTED_MNT, "nested"), 0);
EXPECT_EQ(errno, ENOSPC);
/* The nested instance works for anything that pins no file. */
EXPECT_EQ(register_plain(NESTED_MNT, "nested_plain"), 0);
}
TEST_HARNESS_MAIN

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,158 @@
// SPDX-License-Identifier: GPL-2.0
/*
* An 'F' entry keeps its interpreter open for as long as the entry exists,
* and the entry only goes away when the binfmt_misc superblock is destroyed.
* An interpreter that lives on a mount which in turn keeps that superblock
* alive therefore pins the instance that owns it, and nothing can break the
* cycle. Check the two ways userspace could arrange for that: an interpreter
* on the binfmt_misc instance itself, and one on a filesystem stacked on it.
*
* Runs unprivileged in a user namespace; binfmt_misc is FS_USERNS_MOUNT.
*/
#define _GNU_SOURCE
#include <fcntl.h>
#include <limits.h>
#include <sched.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include "../filesystems/utils.h"
#include "kselftest_harness.h"
#define MNT "/tmp/binfmt_selfpin"
#define BACKING "/tmp/binfmt_selfpin_back"
#define LOWER BACKING "/lower"
#define MERGED "/tmp/binfmt_selfpin_merged"
#define MAGIC "\\xde\\xad"
#define RULE(interp) ":selfpin:M::" MAGIC "::" interp ":F"
/* Not on the instance, and unlike /bin/true it always exists. */
#define INTERP "/proc/self/exe"
#define OPTS_MAX (3 * PATH_MAX + 64)
static int ensure_dir(const char *path)
{
if (mkdir(path, 0755) && errno != EEXIST)
return -1;
return 0;
}
/* Write @rule to this instance's register file, preserving write(2)'s errno. */
static int register_at(struct __test_metadata *_metadata, const char *rule)
{
int fd, saved;
ssize_t n;
fd = open(MNT "/register", O_WRONLY);
ASSERT_GE(fd, 0);
n = write(fd, rule, strlen(rule));
saved = errno;
close(fd);
errno = saved;
return n < 0 ? -1 : 0;
}
/*
* Mount an overlay over @lower using a private upper/work pair, so the two
* mounts this test performs cannot interfere with each other and neither
* overlaps the lower layer.
*/
static int mount_overlay(const char *lower, int nr)
{
char opts[OPTS_MAX], upper[PATH_MAX], work[PATH_MAX];
snprintf(upper, sizeof(upper), "%s/upper%d", BACKING, nr);
snprintf(work, sizeof(work), "%s/work%d", BACKING, nr);
if (mkdir(upper, 0755) || mkdir(work, 0755))
return -1;
snprintf(opts, sizeof(opts), "lowerdir=%s,upperdir=%s,workdir=%s",
lower, upper, work);
return mount("ovl", MERGED, "overlay", 0, opts);
}
FIXTURE(selfpin) {
};
FIXTURE_SETUP(selfpin)
{
/* setup_userns() exits rather than returns if this is not there. */
if (access("/proc/self/ns/user", F_OK))
SKIP(return, "kernel without user namespaces");
ASSERT_EQ(setup_userns(), 0);
ASSERT_EQ(ensure_dir(MNT), 0);
if (mount("binfmt_misc", MNT, "binfmt_misc", 0, NULL)) {
int saved = errno;
/* Teardown doesn't run when setup skips, so clean up here. */
rmdir(MNT);
SKIP(return, "no binfmt_misc: %s", strerror(saved));
}
}
FIXTURE_TEARDOWN(selfpin)
{
/* The namespaces go with the process; just don't litter /tmp. */
umount2(MERGED, MNT_DETACH);
umount2(BACKING, MNT_DETACH);
umount2(MNT, MNT_DETACH);
rmdir(MERGED);
rmdir(BACKING);
rmdir(MNT);
}
/*
* The instance's own files are regular files the mounter owns, so they can be
* made executable. Opening one for exec still has to fail, otherwise the entry
* pins the very superblock it lives in.
*/
TEST_F(selfpin, interpreter_on_the_instance)
{
ASSERT_EQ(chmod(MNT "/status", 0755), 0);
ASSERT_NE(register_at(_metadata, RULE(MNT "/status")), 0);
EXPECT_EQ(errno, EACCES);
}
/* Same for an entry file rather than one of the control files. */
TEST_F(selfpin, interpreter_on_an_entry)
{
ASSERT_EQ(register_at(_metadata, ":victim:M::" MAGIC "::" INTERP ":"), 0);
ASSERT_EQ(chmod(MNT "/victim", 0755), 0);
ASSERT_NE(register_at(_metadata, RULE(MNT "/victim")), 0);
EXPECT_EQ(errno, EACCES);
}
/*
* A stacking filesystem holds a private clone of each layer for its whole
* lifetime, so an instance used as a layer can be pinned by an interpreter
* that does not live on it at all. Refuse to be a layer.
*/
TEST_F(selfpin, refuses_to_be_stacked_on)
{
ASSERT_EQ(ensure_dir(BACKING), 0);
ASSERT_EQ(mount("tmpfs", BACKING, "tmpfs", 0, NULL), 0);
ASSERT_EQ(mkdir(LOWER, 0755), 0);
ASSERT_EQ(ensure_dir(MERGED), 0);
/* Nothing to prove unless overlayfs works here at all. */
if (mount_overlay(LOWER, 1)) {
if (errno == ENODEV || errno == EPERM)
SKIP(return, "no unprivileged overlayfs");
SKIP(return, "overlayfs unusable here: %s", strerror(errno));
}
ASSERT_EQ(umount(MERGED), 0);
EXPECT_NE(mount_overlay(MNT, 2), 0);
}
/* An ordinary interpreter still registers with 'F'. */
TEST_F(selfpin, ordinary_interpreter_still_works)
{
EXPECT_EQ(register_at(_metadata, RULE(INTERP)), 0);
}
TEST_HARNESS_MAIN

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

@@ -0,0 +1,61 @@
// SPDX-License-Identifier: GPL-2.0
/*
* binfmt_misc_ops handler for the selftest's fixed-interpreter case: match a
* 64-bit aarch64 ELF header from the prefetched buffer and route it to a fixed
* interpreter chosen by the program. This is the portable, self-contained
* equivalent of routing a foreign binary to an emulator: it matches
* programmatically and computes the interpreter, but points at a test binary
* the harness installs rather than a system emulator.
*/
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
char _license[] SEC("license") = "GPL";
#define EI_CLASS 4
#define ELFCLASS64 2
#define EM_AARCH64 183
extern int bpf_binprm_set_interp(struct linux_binprm *bprm, const char *path,
size_t path__sz) __ksym;
/*
* A magic-style decision needs nothing beyond the prefetched bprm->buf,
* even though the match program could read the file.
*/
SEC("struct_ops.s/match")
bool BPF_PROG(bpf_interp_match, struct linux_binprm *bprm)
{
__u16 machine;
if (bprm->buf[0] != 0x7f || bprm->buf[1] != 'E' ||
bprm->buf[2] != 'L' || bprm->buf[3] != 'F' ||
bprm->buf[EI_CLASS] != ELFCLASS64)
return false;
/* e_machine is a 16-bit little-endian field at offset 18. */
machine = (__u8)bprm->buf[18] | ((__u16)(__u8)bprm->buf[19] << 8);
return machine == EM_AARCH64;
}
SEC("struct_ops.s/load")
int BPF_PROG(bpf_interp_load, struct linux_binprm *bprm)
{
/*
* Keep the path on the (writable) stack: bpf_binprm_set_interp() takes
* a sized memory arg and the verifier rejects a read-only .rodata
* buffer for it. The harness installs the interpreter at this path.
*/
char interp[] = "/tmp/binfmt_bpf_interp";
/* @path__sz includes the terminating NUL; 0 commits the selection. */
return bpf_binprm_set_interp(bprm, interp, sizeof(interp));
}
SEC(".struct_ops.link")
struct binfmt_misc_ops bpf_interp = {
.match = (void *)bpf_interp_match,
.load = (void *)bpf_interp_load,
.name = "bpf_interp",
};

View File

@@ -1,2 +1,12 @@
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
CONFIG_OVERLAY_FS=y
CONFIG_TMPFS=y
CONFIG_USER_NS=y

View File

@@ -0,0 +1,76 @@
// SPDX-License-Identifier: GPL-2.0
/*
* binfmt_misc_ops handler for the selftest's bound-interpreter case: one
* handler, one entry, an interpreter per guest architecture - each bound to
* a file when the entry was registered rather than to a path resolved at
* exec time. The load program names the one it wants; a name the entry did
* not bind fails the exec, which the harness checks too.
*/
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
char _license[] SEC("license") = "GPL";
#define EI_CLASS 4
#define ELFCLASS64 2
#define E_MACHINE_OFF 18
#define EM_ARM 40
#define EM_AARCH64 183
#define EM_RISCV 243
extern int bpf_binprm_select_interp(struct linux_binprm *bprm,
const char *name, size_t name__sz) __ksym;
/* The guest architecture of a 64-bit ELF, or zero if it is not one. */
static __u16 elf_machine(struct linux_binprm *bprm)
{
if (bprm->buf[0] != 0x7f || bprm->buf[1] != 'E' ||
bprm->buf[2] != 'L' || bprm->buf[3] != 'F' ||
bprm->buf[EI_CLASS] != ELFCLASS64)
return 0;
/* Little-endian 16-bit field, read byte-wise for the verifier. */
return (__u8)bprm->buf[E_MACHINE_OFF] |
((__u16)(__u8)bprm->buf[E_MACHINE_OFF + 1] << 8);
}
SEC("struct_ops.s/match")
bool BPF_PROG(interp_bind_match, struct linux_binprm *bprm)
{
__u16 machine = elf_machine(bprm);
return machine == EM_AARCH64 || machine == EM_RISCV ||
machine == EM_ARM;
}
SEC("struct_ops.s/load")
int BPF_PROG(interp_bind_load, struct linux_binprm *bprm)
{
/*
* Names, not paths: each one selects a file the entry pre-opened, so
* nothing is resolved here or later, in any namespace. The buffers
* are on the stack because the verifier rejects .rodata for a sized
* memory argument.
*/
char first[] = "first";
char second[] = "second";
char unbound[] = "unbound";
switch (elf_machine(bprm)) {
case EM_AARCH64:
return bpf_binprm_select_interp(bprm, first, sizeof(first));
case EM_RISCV:
return bpf_binprm_select_interp(bprm, second, sizeof(second));
}
/* The entry bound nothing under this name: -ENOENT fails the exec. */
return bpf_binprm_select_interp(bprm, unbound, sizeof(unbound));
}
SEC(".struct_ops.link")
struct binfmt_misc_ops interp_bind = {
.match = (void *)interp_bind_match,
.load = (void *)interp_bind_load,
.name = "interp_bind",
};

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,224 @@
// SPDX-License-Identifier: GPL-2.0
/*
* nix_origin.bpf.c - $ORIGIN-relative PT_INTERP resolution
*
* A binfmt_misc_ops handler that makes relocatable (Nix-style) ELF
* binaries work: if PT_INTERP starts with "$ORIGIN/", the loader is
* resolved relative to the directory of the binary being executed and
* selected via bpf_binprm_set_interp(). The match program reads the
* program headers itself, so anything else never commits to this
* handler and passes through untouched.
*
* Activate with:
* bpftool struct_ops register nix_origin.bpf.o /sys/fs/bpf
* echo ':nix-origin:B::::nix_origin:' > /proc/sys/fs/binfmt_misc/register
*/
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
char _license[] SEC("license") = "GPL";
#define PATH_MAX 4096
#define EI_CLASS 4
#define ELFCLASSXX 2 /* ELFCLASS64; flip to 1 for 32-bit */
#define PT_INTERP 3
#define MAX_PHDRS 64
#define ORIGIN "$ORIGIN"
#define ORIGIN_LEN (sizeof(ORIGIN) - 1)
#define ENOENT 2
#define ENOEXEC 8
#define ENAMETOOLONG 36
extern int bpf_dynptr_from_file(struct file *file, __u32 flags,
struct bpf_dynptr *ptr__uninit) __ksym;
extern int bpf_dynptr_file_discard(struct bpf_dynptr *dynptr) __ksym;
extern int bpf_path_d_path(const struct path *path, char *buf,
size_t buf__sz) __ksym;
extern int bpf_binprm_set_interp(struct linux_binprm *bprm, const char *path,
size_t path__sz) __ksym;
struct scratch {
char interp[PATH_MAX]; /* PT_INTERP as embedded in the binary */
char path[PATH_MAX]; /* d_path of the binary, becomes the result */
};
/* Keyed by pid: execs run concurrently and the programs can sleep. */
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 512);
__type(key, __u64);
__type(value, struct scratch);
} scratch_map SEC(".maps");
static const struct scratch zero_scratch;
/* An ELF64 binary per the prefetched header? */
static bool is_elf64(struct linux_binprm *bprm)
{
return bprm->buf[0] == 0x7f && bprm->buf[1] == 'E' &&
bprm->buf[2] == 'L' && bprm->buf[3] == 'F' &&
bprm->buf[EI_CLASS] == ELFCLASSXX;
}
/* Locate PT_INTERP; false if the file has none or looks malformed. */
static bool find_pt_interp(struct bpf_dynptr *dp, struct elf64_phdr *phdr)
{
struct elf64_hdr ehdr;
bool found = false;
int i;
if (bpf_dynptr_read(&ehdr, sizeof(ehdr), dp, 0, 0))
return false;
if (ehdr.e_phentsize != sizeof(struct elf64_phdr))
return false;
bpf_for(i, 0, ehdr.e_phnum) {
if (i >= MAX_PHDRS)
break;
if (bpf_dynptr_read(phdr, sizeof(*phdr), dp,
ehdr.e_phoff + i * sizeof(*phdr), 0))
return false;
if (phdr->p_type == PT_INTERP) {
found = true;
break;
}
}
return found;
}
/*
* An ELF64 binary whose PT_INTERP starts with "$ORIGIN/" is ours. The
* match can sleep and read the file, so the decision is made here and
* regular binaries never commit to this handler: later binfmt_misc
* entries and binfmt_elf see them as if we did not exist.
*/
SEC("struct_ops.s/match")
bool BPF_PROG(nix_origin_match, struct linux_binprm *bprm)
{
char prefix[ORIGIN_LEN + 1] = {};
struct elf64_phdr phdr;
struct bpf_dynptr dp;
bool ours = false;
if (!is_elf64(bprm))
return false;
/* The dynptr must be discarded on every path once requested. */
if (bpf_dynptr_from_file(bprm->file, 0, &dp))
goto out;
if (find_pt_interp(&dp, &phdr) &&
phdr.p_filesz > ORIGIN_LEN + 1 &&
!bpf_dynptr_read(prefix, sizeof(prefix), &dp, phdr.p_offset, 0))
ours = !bpf_strncmp(prefix, sizeof(prefix), ORIGIN "/");
out:
bpf_dynptr_file_discard(&dp);
return ours;
}
/*
* The match is committed and already vetted the "$ORIGIN/" prefix, so
* everything here reads the file again from scratch: -ENOEXEC only
* covers a binary that changed under us and stopped being ours.
*/
SEC("struct_ops.s/load")
int BPF_PROG(nix_origin_load, struct linux_binprm *bprm)
{
__u32 isz, sfx, rsz, slash;
struct elf64_phdr phdr;
struct bpf_dynptr dp;
struct scratch *sc;
__u64 id;
int ret = -ENOEXEC, len, i;
if (bpf_dynptr_from_file(bprm->file, 0, &dp))
goto out;
if (!find_pt_interp(&dp, &phdr))
goto out;
isz = phdr.p_filesz;
if (isz <= ORIGIN_LEN + 1 || isz >= sizeof(sc->interp))
goto out;
/*
* The range check above compiles to a test on a zero-extended copy of
* the u64 p_filesz, so the verifier does not carry the bound to the
* dynptr_read() length below ("unbounded memory access"). Mask isz to
* the buffer size (a power of two) and force the masked value to be
* materialized with a barrier so the read uses the bounded register.
*/
isz &= sizeof(sc->interp) - 1;
barrier_var(isz);
id = bpf_get_current_pid_tgid();
if (bpf_map_update_elem(&scratch_map, &id, &zero_scratch, BPF_ANY))
goto out;
sc = bpf_map_lookup_elem(&scratch_map, &id);
if (!sc)
goto out_del;
if (bpf_dynptr_read(sc->interp, isz, &dp, phdr.p_offset, 0))
goto out_del;
if (sc->interp[isz - 1] != '\0')
goto out_del;
/* Not "$ORIGIN/..." anymore? Then it is not ours anymore either. */
if (sc->interp[0] != '$' || sc->interp[1] != 'O' ||
sc->interp[2] != 'R' || sc->interp[3] != 'I' ||
sc->interp[4] != 'G' || sc->interp[5] != 'I' ||
sc->interp[6] != 'N' || sc->interp[7] != '/')
goto out_del;
/*
* From here on resolution failures fail the exec instead of falling
* back to binfmt_elf, which would resolve the literal "$ORIGIN/..."
* relative to the caller's cwd.
*/
ret = -ENOENT;
len = bpf_path_d_path(&bprm->file->f_path, sc->path, sizeof(sc->path));
if (len <= 0 || len > sizeof(sc->path))
goto out_del;
/* Unreachable or unlinked ("... (deleted)") binaries can't resolve. */
if (sc->path[0] != '/')
goto out_del;
/* $ORIGIN = dirname of the binary. */
slash = 0;
bpf_for(i, 1, len - 1) {
if (i >= sizeof(sc->path))
break;
if (sc->path[i] == '/')
slash = i;
}
/* Splice the suffix (leading '/' and NUL included) onto the dir. */
sfx = isz - ORIGIN_LEN;
rsz = slash + sfx;
if (rsz > sizeof(sc->path)) {
ret = -ENAMETOOLONG;
goto out_del;
}
bpf_for(i, 0, sfx) {
__u32 s = ORIGIN_LEN + i, d = slash + i;
if (s >= sizeof(sc->interp) || d >= sizeof(sc->path))
break;
sc->path[d] = sc->interp[s];
}
ret = bpf_binprm_set_interp(bprm, sc->path, rsz);
out_del:
bpf_map_delete_elem(&scratch_map, &id);
out:
bpf_dynptr_file_discard(&dp);
return ret;
}
SEC(".struct_ops.link")
struct binfmt_misc_ops nix_origin = {
.match = (void *)nix_origin_match,
.load = (void *)nix_origin_load,
.name = "nix_origin",
};

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",
};