mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-08-31 11:41:29 -04:00
Merge patch series "binfmt_misc: transparent interpreters and PT_INTERP loader substitution"
Christian Brauner <brauner@kernel.org> says: binfmt_misc has exactly one execution model where the registered interpreter becomes the executed program and the matched binary is handed to it as an argument. For wine or qemu-user that is the point. For a per-binary loader it is backwards. The interpreter is an implementation detail of running the binary, yet it owns the entire process identity: - argv[0] and /proc/pid/cmdline show the interpreter invocation, not what the caller executed. - /proc/self/exe names the interpreter. Relocatable programs commonly locate themselves through it and find the dynamic linker instead. - A binary passed to execveat() as an inaccessible O_CLOEXEC fd cannot run at all as the interpreter has no path to open it by. - gdb cross-validates AT_ENTRY/AT_PHDR against the exe file and discards the load displacement on mismatch leaving PIE symbols unrelocated. This series adds two dispatch modes that close the gap from opposite ends: (1) transparent dispatch Registered with the 'T' flag or chosen per exec with BPF_BINPRM_TRANSPARENT. The binary is sent to the interpreter through AT_EXECFD, the argument vector stays exactly as the caller built it, and the kernel labels mm->exe_file and comm with the binary. A new AT_FLAGS_TRANSPARENT_INTERP aux vector bit is raised indicating that nothing was spliced, argv belongs to the program, and to load it from the descriptor. The interpreter keeps control of mapping the binary, so the mode covers foreign architectures and non-ELF payloads. The exe label is not a new privilege. It names precisely the file the caller passed to execve(), not a file of the process's choosing. That file is permission-checked, write-denied while the process runs and recorded by audit. Credential derivation does not change exactly as today. (2) loader substitution The kernel executes the matched binary natively as the main image and substitutes the registered interpreter for the binary's PT_INTERP. binfmt_misc functions as a PT_INTERP override. There is no contract and no identity to reconstruct. So a stock dynamic loader works unchanged. Hence, 'L' is for native-arch ELF with PT_INTERP. The two modes compose. A bpf handler reads the ELF header from bprm->buf and grades per binary, picking 'L' where it applies and 'T' or classic dispatch for the rest. If userspace control over relocation is wanted 'T' is the way to go. * patches from https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-0-e57866e4ae0f@kernel.org: (21 commits) binfmt_misc: document loader substitution selftests/exec: test binfmt_misc loader substitution binfmt_misc: let a bpf handler request loader substitution binfmt_misc: add the 'L' loader substitution flag binfmt_elf_fdpic: consume a stashed PT_INTERP substitute binfmt_elf: consume a stashed PT_INTERP substitute exec: carry a PT_INTERP substitute in struct linux_binprm binfmt_misc: document the transparent identity contract selftests/exec: test the transparent binfmt_misc mode binfmt_misc: let a bpf handler run the interpreter transparently binfmt_misc: add a static transparent flag 'T' binfmt_misc: add transparent interpreter dispatch exec: label mm->exe_file with the binary for a transparent dispatch exec: add AT_FLAGS_TRANSPARENT_INTERP selftests/exec: convert the binfmt_misc bpf test to the kselftest harness exec: release the replaced file with do_close_execat() binfmt_misc: split out entry_open_interpreter() and build_interp_argv() binfmt_misc: normalize the per-exec invocation flags binfmt_misc: table-drive the register string flags docs, binfmt_misc: keep general usage out of the handler sections ... Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-0-e57866e4ae0f@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
This commit is contained in:
@@ -90,6 +90,23 @@ Here is what the fields mean:
|
||||
emulation is installed and uses the opened image to spawn the
|
||||
emulator, meaning it is always available once installed,
|
||||
regardless of how the environment changes.
|
||||
``T`` - transparent
|
||||
Run the interpreter transparently. The binary is handed to
|
||||
the interpreter through ``AT_EXECFD`` (``T`` implies ``O``),
|
||||
the argument vector is left exactly as the caller built it
|
||||
and the kernel labels ``/proc/pid/exe`` with the binary
|
||||
instead of the interpreter. The interpreter has to load the
|
||||
binary from ``AT_EXECFD`` and follow the
|
||||
``AT_FLAGS_TRANSPARENT_INTERP`` contract. Combining ``T``
|
||||
with ``P`` is rejected: transparency preserves the whole
|
||||
argument vector, argv[0] included.
|
||||
``L`` - loader substitution
|
||||
Do not run the interpreter on the binary at all: load the
|
||||
binary itself as a fully native exec and substitute the
|
||||
interpreter for the loader named in the binary's
|
||||
``PT_INTERP``. See the "Loader substitution" section
|
||||
below. ``L`` rejects ``T``, ``P``, ``O`` and ``C``;
|
||||
``F`` composes.
|
||||
|
||||
|
||||
There are some restrictions:
|
||||
@@ -98,66 +115,12 @@ There are some restrictions:
|
||||
- the magic must reside in the first 128 bytes of the file, i.e.
|
||||
offset+size(magic) has to be less than 128
|
||||
- the interpreter string may not exceed 127 characters
|
||||
- an interpreter used with ``C`` or ``L`` but without ``F`` has to be
|
||||
named by an absolute path. It is opened when the binary is executed, so
|
||||
a relative one would be resolved against the working directory of
|
||||
whoever runs the binary
|
||||
|
||||
|
||||
bpf-backed handlers
|
||||
-------------------
|
||||
|
||||
With ``CONFIG_BINFMT_MISC_BPF`` both the matching and the interpreter
|
||||
selection can be delegated to bpf programs. A handler is an instance of the
|
||||
``binfmt_misc_ops`` struct_ops with a ``match`` and a ``load`` program and a
|
||||
``name``. Once the struct_ops map is registered the handler can be activated
|
||||
with a ``B`` entry that references it by name in the ``interpreter`` field
|
||||
and carries neither offset, magic, nor mask::
|
||||
|
||||
echo ':qemu:B::::my_handler:' > register
|
||||
|
||||
Both programs receive the ``linux_binprm`` of the binary and both can
|
||||
sleep. The ``match`` program decides whether the handler applies: it is
|
||||
consulted during the entry walk exactly like magic and extension matching,
|
||||
in the same registration order with the same first-match-wins semantics.
|
||||
Unlike static matching it is not limited to the prefetched first bytes of
|
||||
the file in ``bprm->buf``: it can read the file, e.g. to parse ELF program
|
||||
headers whose data sits at arbitrary offsets. It only decides, though: the
|
||||
selection kfuncs below are rejected in it. The ``load`` program of the
|
||||
matched handler then selects the interpreter: it can equally read the file
|
||||
and derive the interpreter from the binary's location. It selects the
|
||||
interpreter by calling the ``bpf_binprm_set_interp()`` kfunc with an
|
||||
absolute path and returning ``0``. A match is committed: a failing
|
||||
``load`` fails the exec with its error instead of falling through to later
|
||||
entries; ``-ENOEXEC`` lets the remaining binary formats have a go. The
|
||||
interpreter is opened with the credentials of the task doing the exec,
|
||||
exactly as a statically registered interpreter would be.
|
||||
|
||||
The ``load`` program can also pass a single argument to the interpreter with
|
||||
the ``bpf_binprm_set_interp_arg()`` kfunc. It is inserted between the
|
||||
interpreter and the binary, exactly like the optional argument of a ``#!``
|
||||
interpreter line, e.g. for a handler that resolves ``$ORIGIN`` in a script's
|
||||
``#!`` path and needs to preserve the argument that followed it.
|
||||
|
||||
The invocation flags a static entry fixes at registration - ``P``, ``C``
|
||||
and ``O`` - are per-exec choices for a bpf handler, made by the ``load``
|
||||
program with the ``bpf_binprm_set_flags()`` kfunc, so a single handler can
|
||||
decide them differently for each binary it handles:
|
||||
|
||||
- ``BPF_BINPRM_PRESERVE_ARGV0`` keeps the caller's ``argv[0]`` (the ``P``
|
||||
flag).
|
||||
- ``BPF_BINPRM_CREDENTIALS`` computes credentials from the binary (the ``C``
|
||||
flag), bounded to user namespaces that map the binary's owner just like
|
||||
any other setuid exec.
|
||||
- ``BPF_BINPRM_EXECFD`` opens the binary on the interpreter's behalf and
|
||||
passes it through the ``AT_EXECFD`` aux vector entry (the ``O`` flag), so
|
||||
the interpreter can run binaries it could not open by path.
|
||||
|
||||
Because these are program choices, a ``B`` entry carries no flags in the
|
||||
register string; ``F`` (pre-open a fixed interpreter) has no meaning for it.
|
||||
|
||||
A handler is looked up only in the user namespace the struct_ops map was
|
||||
registered in. Handlers are not inherited, so an entry can only reference a
|
||||
handler registered in the same user namespace as its binfmt_misc instance.
|
||||
The entry keeps the handler alive; deleting the struct_ops map only prevents
|
||||
new activations.
|
||||
|
||||
To use binfmt_misc you have to mount it first. You can mount it with
|
||||
``mount -t binfmt_misc none /proc/sys/fs/binfmt_misc`` command, or you can add
|
||||
a line ``none /proc/sys/fs/binfmt_misc binfmt_misc defaults 0 0`` to your
|
||||
@@ -198,6 +161,152 @@ or ``/proc/sys/fs/binfmt_misc/status``. A single entry can also be removed
|
||||
by simply unlinking (``rm``) ``/proc/.../the_name``.
|
||||
|
||||
|
||||
bpf-backed handlers
|
||||
-------------------
|
||||
|
||||
With ``CONFIG_BINFMT_MISC_BPF`` both the matching and the interpreter
|
||||
selection can be delegated to bpf programs. A handler is an instance of the
|
||||
``binfmt_misc_ops`` struct_ops with a ``match`` and a ``load`` program and a
|
||||
``name``. Once the struct_ops map is registered the handler can be activated
|
||||
with a ``B`` entry that references it by name in the ``interpreter`` field
|
||||
and carries neither offset, magic, nor mask::
|
||||
|
||||
echo ':qemu:B::::my_handler:' > register
|
||||
|
||||
Both programs receive the ``linux_binprm`` of the binary and both can
|
||||
sleep. The ``match`` program decides whether the handler applies: it is
|
||||
consulted during the entry walk exactly like magic and extension matching,
|
||||
in the same registration order with the same first-match-wins semantics.
|
||||
Unlike static matching it is not limited to the prefetched first bytes of
|
||||
the file in ``bprm->buf``: it can read the file, e.g. to parse ELF program
|
||||
headers whose data sits at arbitrary offsets. It only decides, though: the
|
||||
selection kfuncs below are rejected in it. The ``load`` program of the
|
||||
matched handler then selects the interpreter: it can equally read the file
|
||||
and derive the interpreter from the binary's location. It selects the
|
||||
interpreter by calling the ``bpf_binprm_set_interp()`` kfunc with an
|
||||
absolute path and returning ``0``. A match is committed: a failing
|
||||
``load`` fails the exec with its error instead of falling through to later
|
||||
entries; ``-ENOEXEC`` lets the remaining binary formats have a go. The
|
||||
interpreter is opened with the credentials of the task doing the exec,
|
||||
exactly as a statically registered interpreter would be.
|
||||
|
||||
The ``load`` program can also pass a single argument to the interpreter with
|
||||
the ``bpf_binprm_set_interp_arg()`` kfunc. It is inserted between the
|
||||
interpreter and the binary, exactly like the optional argument of a ``#!``
|
||||
interpreter line, e.g. for a handler that resolves ``$ORIGIN`` in a script's
|
||||
``#!`` path and needs to preserve the argument that followed it.
|
||||
|
||||
The invocation flags a static entry fixes at registration - ``P``, ``C``,
|
||||
``O``, ``T`` and ``L`` - are per-exec choices for a bpf handler, made by the
|
||||
``load`` program with the ``bpf_binprm_set_flags()`` kfunc, so a single
|
||||
handler can decide them differently for each binary it handles:
|
||||
|
||||
- ``BPF_BINPRM_PRESERVE_ARGV0`` keeps the caller's ``argv[0]`` (the ``P``
|
||||
flag).
|
||||
- ``BPF_BINPRM_CREDENTIALS`` computes credentials from the binary (the ``C``
|
||||
flag), bounded to user namespaces that map the binary's owner just like
|
||||
any other setuid exec.
|
||||
- ``BPF_BINPRM_EXECFD`` opens the binary on the interpreter's behalf and
|
||||
passes it through the ``AT_EXECFD`` aux vector entry (the ``O`` flag), so
|
||||
the interpreter can run binaries it could not open by path.
|
||||
- ``BPF_BINPRM_TRANSPARENT`` runs the interpreter transparently (the ``T``
|
||||
flag): the binary is handed over through ``AT_EXECFD`` as
|
||||
with ``BPF_BINPRM_EXECFD``, but the argument vector is also left as the
|
||||
caller passed it. An interpreter that loads the binary from ``AT_EXECFD``
|
||||
then appears in ``argv[0]`` and ``/proc/pid/cmdline`` as a direct
|
||||
execution of the binary. ``BPF_BINPRM_PRESERVE_ARGV0`` and a staged
|
||||
interpreter argument are rejected in combination with it, just as ``P``
|
||||
is with ``T``. It also lets a handler
|
||||
run a binary passed as an inaccessible ``O_CLOEXEC`` file descriptor to
|
||||
``execveat()``, which a path-splicing dispatch cannot: the interpreter
|
||||
has no path by which to open it.
|
||||
- ``BPF_BINPRM_LOADER`` substitutes the interpreter for the binary's
|
||||
``PT_INTERP`` and runs the binary as a fully native exec (the ``L``
|
||||
flag). It excludes the other flags and a staged interpreter argument.
|
||||
|
||||
Because these are program choices, a ``B`` entry carries no flags in the
|
||||
register string; ``F`` (pre-open a fixed interpreter) has no meaning for it.
|
||||
|
||||
A handler is looked up only in the user namespace the struct_ops map was
|
||||
registered in. Handlers are not inherited, so an entry can only reference a
|
||||
handler registered in the same user namespace as its binfmt_misc instance.
|
||||
The entry keeps the handler alive; deleting the struct_ops map only prevents
|
||||
new activations.
|
||||
|
||||
|
||||
Transparent interpreters
|
||||
------------------------
|
||||
|
||||
With the ``T`` flag or ``BPF_BINPRM_TRANSPARENT`` the dispatch is invisible
|
||||
to the resulting process. The argument vector is left exactly as the caller
|
||||
built it. The binary is passed through ``AT_EXECFD``. The kernel also labels
|
||||
``/proc/pid/exe`` correctly. The binary's file is write-denied while the
|
||||
process runs and the interpreter's is not, exactly as if the binary had been
|
||||
executed directly. A transparent entry does not change how credentials are
|
||||
derived. As
|
||||
with any other entry, set*id bits of the binary are only honored with ``C`` (or
|
||||
``BPF_BINPRM_CREDENTIALS``).
|
||||
|
||||
The interpreter has to be built for this contract. The kernel announces it
|
||||
with ``AT_FLAGS_TRANSPARENT_INTERP`` in the ``AT_FLAGS`` aux vector entry
|
||||
next to ``AT_EXECFD``. The argument vector belongs entirely to the program,
|
||||
nothing was spliced in, so the interpreter doesn't consume arguments and
|
||||
simply loads the program from the descriptor. The bit is also the loader's
|
||||
license to finish the identity. After mapping the program it may retarget the
|
||||
``AT_PHDR``/``AT_ENTRY``/``AT_BASE`` entries of ``/proc/pid/auxv`` and the
|
||||
code/data statistics markers via one ``PR_SET_MM_MAP`` which completes
|
||||
what attaching debuggers observe. What remains visibly different from a
|
||||
direct execution is the address space layout. The interpreter occupies
|
||||
the main-image position and the program lives in the mmap region.
|
||||
|
||||
|
||||
Loader substitution
|
||||
-------------------
|
||||
|
||||
The ``L`` flag turns the execution model around. Instead of running the
|
||||
registered interpreter with the binary as its payload the kernel loads
|
||||
the matched binary itself as the main image and substitutes the registered
|
||||
interpreter for the loader named in the binary's ``PT_INTERP``.
|
||||
|
||||
Because the exec is native, there is no dispatch identity to
|
||||
reconstruct and no contract the substitute has to implement. A stock
|
||||
dynamic loader works unchanged. The argument vector is untouched,
|
||||
credentials and ``AT_SECURE`` derive from the binary, there is no
|
||||
``AT_EXECFD`` and no marker in the aux vector, the binary sits in the
|
||||
main-image slot with the native brk placement so ``/proc/pid/maps``,
|
||||
core dumps and perf mmap records have the native shape, and the
|
||||
identity is already complete when ``PTRACE_EVENT_EXEC`` stops the
|
||||
tracee. So launching under a debugger works, not just attaching. ``L``
|
||||
entries are for ELF binaries of a native architecture. Foreign-arch
|
||||
emulation and non-ELF payloads remain the domain of the classic and
|
||||
transparent modes.
|
||||
|
||||
The override applies when the format that finally claims the file is
|
||||
ELF with a ``PT_INTERP``. A matched binary without one or an
|
||||
interpreter-less ``ET_DYN`` drops the override and runs natively. A file
|
||||
claimed by another format - a ``#!`` script, say - is handled by that
|
||||
format as if the entry had not matched. ``L`` is therefore not an
|
||||
enforcement mechanism: it decides how a binary that asks for a loader is
|
||||
run, it does not guarantee that everything matching the entry runs under
|
||||
the substitute. A format that cannot consume the override at all instead
|
||||
refuses the exec with ``ENOEXEC`` before the point of no return.
|
||||
|
||||
A wrong-architecture ELF fails the whole exec with ``ENOEXEC`` exactly
|
||||
as if no entry had matched. A substitute that is not ELF of the right
|
||||
architecture fails with ``ELIBBAD``. The usual ``PT_INTERP`` sanity
|
||||
checks on the binary still apply. But the segment's content is otherwise
|
||||
irrelevant.
|
||||
|
||||
``L`` rejects the classic-dispatch flags ``T``, ``P``, ``O`` and ``C``
|
||||
at registration. ``F`` composes and is valuable: with it the substitute
|
||||
is opened at registration time, so later mount namespace or path changes
|
||||
cannot redirect it. Without it the substitute is opened when the binary
|
||||
is executed, and the path is resolved in the mount namespace and root of
|
||||
whoever runs the binary, which is why it has to be absolute. As with
|
||||
``C``, register only trusted interpreters. The substituted loader runs
|
||||
with credentials derived from the binary.
|
||||
|
||||
|
||||
Hints
|
||||
-----
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
316
fs/binfmt_misc.c
316
fs/binfmt_misc.c
@@ -10,6 +10,7 @@
|
||||
|
||||
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
|
||||
|
||||
#include <linux/array_size.h>
|
||||
#include <linux/binfmt_misc.h>
|
||||
#include <linux/binfmts.h>
|
||||
#include <linux/bitops.h>
|
||||
@@ -49,8 +50,42 @@ enum binfmt_misc_entry_flags {
|
||||
MISC_FMT_OPEN_BINARY = (1U << 30),
|
||||
MISC_FMT_CREDENTIALS = (1U << 29),
|
||||
MISC_FMT_OPEN_FILE = (1U << 28),
|
||||
MISC_FMT_TRANSPARENT = (1U << 27),
|
||||
MISC_FMT_LOADER = (1U << 26),
|
||||
};
|
||||
|
||||
/**
|
||||
* struct binfmt_misc_flag - a flag character of the register string
|
||||
* @c: the character userspace writes and reads back
|
||||
* @flag: the entry flag it sets
|
||||
* @implies: entry flags it turns on in addition
|
||||
* @desc: what it does, for the registration debug output
|
||||
*/
|
||||
struct binfmt_misc_flag {
|
||||
char c;
|
||||
unsigned long flag;
|
||||
unsigned long implies;
|
||||
const char *desc;
|
||||
};
|
||||
|
||||
static const struct binfmt_misc_flag misc_flags[] = {
|
||||
{ 'P', MISC_FMT_PRESERVE_ARGV0, 0, "preserve argv0" },
|
||||
{ 'O', MISC_FMT_OPEN_BINARY, 0, "open binary" },
|
||||
{ 'C', MISC_FMT_CREDENTIALS, MISC_FMT_OPEN_BINARY, "credentials from the binary" },
|
||||
{ 'F', MISC_FMT_OPEN_FILE, 0, "open interpreter file now" },
|
||||
{ 'T', MISC_FMT_TRANSPARENT, MISC_FMT_OPEN_BINARY, "transparent" },
|
||||
{ 'L', MISC_FMT_LOADER, 0, "loader substitution" },
|
||||
};
|
||||
|
||||
/* Look up a flag character, NULL if @c is not one. */
|
||||
static const struct binfmt_misc_flag *misc_flag_by_char(const char c)
|
||||
{
|
||||
for (int i = 0; i < ARRAY_SIZE(misc_flags); i++)
|
||||
if (misc_flags[i].c == c)
|
||||
return &misc_flags[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct binfmt_misc_entry {
|
||||
struct hlist_node node;
|
||||
unsigned long flags; /* type, status, etc. */
|
||||
@@ -288,56 +323,97 @@ static const char *entry_select_interpreter(const struct binfmt_misc_entry *e,
|
||||
return ERR_PTR(retval);
|
||||
}
|
||||
|
||||
/*
|
||||
* the loader itself
|
||||
/**
|
||||
* entry_invocation_flags - the invocation flags in effect for this exec
|
||||
* @e: matched binary type handler
|
||||
* @bprm: binary that is being executed
|
||||
*
|
||||
* A static entry fixes its flags at registration, a 'B' entry's load program
|
||||
* picks them per exec with bpf_binprm_set_flags(). Translate the latter into
|
||||
* the former, implications included, so the dispatch has one set to act on.
|
||||
*
|
||||
* Return: the invocation flags for this exec
|
||||
*/
|
||||
static int load_misc_binary(struct linux_binprm *bprm)
|
||||
static unsigned long entry_invocation_flags(const struct binfmt_misc_entry *e,
|
||||
struct linux_binprm *bprm)
|
||||
{
|
||||
struct binfmt_misc_entry *fmt __free(put_binfmt_handler) = NULL;
|
||||
const char *interpreter;
|
||||
struct file *interp_file;
|
||||
struct binfmt_misc *misc;
|
||||
bool preserve_argv0, want_execfd, want_creds;
|
||||
unsigned long flags = 0;
|
||||
u64 bpf_flags;
|
||||
|
||||
if (!test_bit(MISC_FMT_BPF_BIT, &e->flags))
|
||||
return e->flags;
|
||||
|
||||
bpf_flags = bprm->bpf_flags;
|
||||
/* Clear so they can't accumulate into a nested interpreter level. */
|
||||
bprm->bpf_flags = 0;
|
||||
|
||||
if (bpf_flags & BPF_BINPRM_PRESERVE_ARGV0)
|
||||
flags |= MISC_FMT_PRESERVE_ARGV0;
|
||||
if (bpf_flags & BPF_BINPRM_EXECFD)
|
||||
flags |= MISC_FMT_OPEN_BINARY;
|
||||
if (bpf_flags & BPF_BINPRM_CREDENTIALS)
|
||||
flags |= MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_BINARY;
|
||||
if (bpf_flags & BPF_BINPRM_TRANSPARENT)
|
||||
flags |= MISC_FMT_TRANSPARENT | MISC_FMT_OPEN_BINARY;
|
||||
if (bpf_flags & BPF_BINPRM_LOADER)
|
||||
flags |= MISC_FMT_LOADER;
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* entry_open_interpreter - open the entry's interpreter for execution
|
||||
* @e: matched binary type handler
|
||||
* @interpreter: the interpreter selected for this exec
|
||||
*
|
||||
* An 'F' entry hands out a clone of the file it pre-opened at registration,
|
||||
* any other entry opens the selected path.
|
||||
*
|
||||
* Return: the opened interpreter on success, an ERR_PTR on failure
|
||||
*/
|
||||
static struct file *entry_open_interpreter(const struct binfmt_misc_entry *e,
|
||||
const char *interpreter)
|
||||
{
|
||||
struct file *interp_file __free(fput) = NULL;
|
||||
int retval;
|
||||
|
||||
misc = current_binfmt_misc();
|
||||
if (!READ_ONCE(misc->enabled))
|
||||
return -ENOEXEC;
|
||||
if (!(e->flags & MISC_FMT_OPEN_FILE))
|
||||
return open_exec(interpreter);
|
||||
|
||||
fmt = get_binfmt_handler(misc, bprm);
|
||||
if (!fmt)
|
||||
return -ENOEXEC;
|
||||
interp_file = file_clone_open(e->interp_file);
|
||||
if (IS_ERR(interp_file))
|
||||
return interp_file;
|
||||
|
||||
/* Need to be able to load the file after exec */
|
||||
retval = exe_file_deny_write_access(interp_file);
|
||||
if (retval)
|
||||
return ERR_PTR(retval);
|
||||
|
||||
return no_free_ptr(interp_file);
|
||||
}
|
||||
|
||||
/**
|
||||
* build_interp_argv - splice the interpreter invocation into the argv
|
||||
* @bprm: binary that is being executed
|
||||
* @interpreter: the interpreter selected for this exec
|
||||
* @flags: invocation flags in effect for this exec
|
||||
*
|
||||
* The interpreter becomes argv[0] and the binary its last argument, with an
|
||||
* optional staged argument in between. The caller's argv[0] is dropped
|
||||
* unless 'P' keeps it.
|
||||
*
|
||||
* Return: 0 on success, a negative error code on failure
|
||||
*/
|
||||
static int build_interp_argv(struct linux_binprm *bprm, const char *interpreter,
|
||||
unsigned long flags)
|
||||
{
|
||||
int retval;
|
||||
|
||||
/* The interpreter has to be able to load the binary by path. */
|
||||
if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE)
|
||||
return -ENOENT;
|
||||
|
||||
interpreter = entry_select_interpreter(fmt, bprm);
|
||||
if (IS_ERR(interpreter))
|
||||
return PTR_ERR(interpreter);
|
||||
|
||||
/*
|
||||
* The invocation flags are fixed at registration for a static handler
|
||||
* and chosen per exec by the load program, via bpf_binprm_set_flags(),
|
||||
* for a bpf one.
|
||||
*/
|
||||
if (test_bit(MISC_FMT_BPF_BIT, &fmt->flags)) {
|
||||
u64 f = bprm->bpf_flags;
|
||||
|
||||
/* Clear so it can't accumulate into a nested interpreter level. */
|
||||
bprm->bpf_flags = 0;
|
||||
|
||||
preserve_argv0 = f & BPF_BINPRM_PRESERVE_ARGV0;
|
||||
want_creds = f & BPF_BINPRM_CREDENTIALS;
|
||||
want_execfd = f & (BPF_BINPRM_CREDENTIALS | BPF_BINPRM_EXECFD);
|
||||
} else {
|
||||
preserve_argv0 = fmt->flags & MISC_FMT_PRESERVE_ARGV0;
|
||||
want_creds = fmt->flags & MISC_FMT_CREDENTIALS;
|
||||
want_execfd = fmt->flags & MISC_FMT_OPEN_BINARY;
|
||||
}
|
||||
|
||||
/* The entry's own choice - not one accumulated from an earlier level. */
|
||||
if (preserve_argv0) {
|
||||
if (flags & MISC_FMT_PRESERVE_ARGV0) {
|
||||
bprm->interp_flags |= BINPRM_FLAGS_PRESERVE_ARGV0;
|
||||
} else {
|
||||
retval = remove_arg_zero(bprm);
|
||||
@@ -371,31 +447,83 @@ static int load_misc_binary(struct linux_binprm *bprm)
|
||||
return retval;
|
||||
bprm->argc++;
|
||||
|
||||
/* Update interp in case binfmt_script needs it. */
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* the loader itself
|
||||
*/
|
||||
static int load_misc_binary(struct linux_binprm *bprm)
|
||||
{
|
||||
struct binfmt_misc_entry *fmt __free(put_binfmt_handler) = NULL;
|
||||
const char *interpreter;
|
||||
struct file *interp_file;
|
||||
struct binfmt_misc *misc;
|
||||
unsigned long flags;
|
||||
int retval;
|
||||
|
||||
/* Only binfmt_misc stages one and exec_binprm() clears it per round. */
|
||||
WARN_ON_ONCE(bprm->loader);
|
||||
|
||||
misc = current_binfmt_misc();
|
||||
if (!READ_ONCE(misc->enabled))
|
||||
return -ENOEXEC;
|
||||
|
||||
fmt = get_binfmt_handler(misc, bprm);
|
||||
if (!fmt)
|
||||
return -ENOEXEC;
|
||||
|
||||
interpreter = entry_select_interpreter(fmt, bprm);
|
||||
if (IS_ERR(interpreter))
|
||||
return PTR_ERR(interpreter);
|
||||
|
||||
flags = entry_invocation_flags(fmt, bprm);
|
||||
|
||||
/* No argv is built for a staged argument to land in. */
|
||||
if ((flags & (MISC_FMT_LOADER | MISC_FMT_TRANSPARENT)) &&
|
||||
bprm->bpf_interp_arg)
|
||||
return -EINVAL;
|
||||
|
||||
/*
|
||||
* Stash the interpreter for binfmt_elf to consume in place of the
|
||||
* binary's PT_INTERP and decline the match, so the search continues
|
||||
* to the real format in the same round.
|
||||
*/
|
||||
if (flags & MISC_FMT_LOADER) {
|
||||
interp_file = entry_open_interpreter(fmt, interpreter);
|
||||
if (IS_ERR(interp_file)) {
|
||||
retval = PTR_ERR(interp_file);
|
||||
/* Declining here would run the binary's own PT_INTERP. */
|
||||
return retval == -ENOEXEC ? -EACCES : retval;
|
||||
}
|
||||
|
||||
bprm->loader = interp_file;
|
||||
return -ENOEXEC;
|
||||
}
|
||||
|
||||
if (!(flags & MISC_FMT_TRANSPARENT)) {
|
||||
retval = build_interp_argv(bprm, interpreter, flags);
|
||||
if (retval)
|
||||
return retval;
|
||||
}
|
||||
|
||||
/* Update interp for the next round; sched_prepare_exec reports it. */
|
||||
retval = bprm_change_interp(interpreter, bprm);
|
||||
if (retval < 0)
|
||||
return retval;
|
||||
|
||||
if (fmt->flags & MISC_FMT_OPEN_FILE) {
|
||||
interp_file = file_clone_open(fmt->interp_file);
|
||||
if (!IS_ERR(interp_file)) {
|
||||
int err = exe_file_deny_write_access(interp_file);
|
||||
|
||||
if (err) {
|
||||
fput(interp_file);
|
||||
interp_file = ERR_PTR(err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
interp_file = open_exec(interpreter);
|
||||
}
|
||||
interp_file = entry_open_interpreter(fmt, interpreter);
|
||||
if (IS_ERR(interp_file))
|
||||
return PTR_ERR(interp_file);
|
||||
|
||||
/* Raise only past the last failure, or an -ENOEXEC decline leaks it. */
|
||||
if (flags & MISC_FMT_TRANSPARENT)
|
||||
bprm->interp_flags |= BINPRM_FLAGS_TRANSPARENT_INTERP;
|
||||
|
||||
bprm->interpreter = interp_file;
|
||||
if (want_execfd)
|
||||
if (flags & MISC_FMT_OPEN_BINARY)
|
||||
bprm->have_execfd = 1;
|
||||
if (want_creds)
|
||||
if (flags & MISC_FMT_CREDENTIALS)
|
||||
bprm->execfd_creds = 1;
|
||||
return 0;
|
||||
}
|
||||
@@ -424,30 +552,16 @@ static char *scanarg(char *s, char del)
|
||||
return s;
|
||||
}
|
||||
|
||||
/* Parse the 'flags' field, stopping at the first character that is not one. */
|
||||
static char *check_special_flags(char *p, struct binfmt_misc_entry *e)
|
||||
{
|
||||
for (;; p++) {
|
||||
switch (*p) {
|
||||
case 'P':
|
||||
pr_debug("register: flag: P (preserve argv0)\n");
|
||||
e->flags |= MISC_FMT_PRESERVE_ARGV0;
|
||||
break;
|
||||
case 'O':
|
||||
pr_debug("register: flag: O (open binary)\n");
|
||||
e->flags |= MISC_FMT_OPEN_BINARY;
|
||||
break;
|
||||
case 'C':
|
||||
pr_debug("register: flag: C (preserve creds)\n");
|
||||
/* C implies O */
|
||||
e->flags |= MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_BINARY;
|
||||
break;
|
||||
case 'F':
|
||||
pr_debug("register: flag: F: open interpreter file now\n");
|
||||
e->flags |= MISC_FMT_OPEN_FILE;
|
||||
break;
|
||||
default:
|
||||
const struct binfmt_misc_flag *f = misc_flag_by_char(*p);
|
||||
|
||||
if (!f)
|
||||
return p;
|
||||
}
|
||||
pr_debug("register: flag: %c (%s)\n", f->c, f->desc);
|
||||
e->flags |= f->flag | f->implies;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -570,7 +684,7 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,
|
||||
size_t count)
|
||||
{
|
||||
struct binfmt_misc_entry *e __free(kfree) = NULL;
|
||||
char *buf, *p;
|
||||
char *buf, *p, *flags;
|
||||
char del;
|
||||
|
||||
pr_debug("register: received %zu bytes\n", count);
|
||||
@@ -595,7 +709,7 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,
|
||||
pr_debug("register: delim: %#x {%c}\n", del, del);
|
||||
|
||||
/* A flag-char delimiter runs the flag scan off the buffer. */
|
||||
if (del == 'P' || del == 'O' || del == 'C' || del == 'F')
|
||||
if (misc_flag_by_char(del))
|
||||
return ERR_PTR(-EINVAL);
|
||||
|
||||
/* Pad the buffer with the delim to simplify parsing below. */
|
||||
@@ -666,21 +780,38 @@ static struct binfmt_misc_entry *create_entry(const char __user *buffer,
|
||||
}
|
||||
|
||||
/* Parse the 'flags' field. */
|
||||
flags = p;
|
||||
p = check_special_flags(p, e);
|
||||
|
||||
/*
|
||||
* A bpf handler decides the invocation flags per exec with
|
||||
* bpf_binprm_set_flags() rather than fixing them at registration, and
|
||||
* 'F' (pre-open a fixed interpreter) is meaningless for it, so a 'B'
|
||||
* entry's flags field has to be empty.
|
||||
*/
|
||||
if (test_bit(MISC_FMT_BPF_BIT, &e->flags) && p != flags)
|
||||
return ERR_PTR(-EINVAL);
|
||||
|
||||
/* Transparency preserves the whole argv, argv[0] included. */
|
||||
if ((e->flags & MISC_FMT_TRANSPARENT) &&
|
||||
(e->flags & MISC_FMT_PRESERVE_ARGV0))
|
||||
return ERR_PTR(-EINVAL);
|
||||
|
||||
/* A native exec splices no argv, passes no execfd and needs no creds. */
|
||||
if ((e->flags & MISC_FMT_LOADER) &&
|
||||
(e->flags & (MISC_FMT_TRANSPARENT | MISC_FMT_PRESERVE_ARGV0 |
|
||||
MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_BINARY)))
|
||||
return ERR_PTR(-EINVAL);
|
||||
|
||||
if (*p == '\n')
|
||||
p++;
|
||||
if (p != buf + count)
|
||||
return ERR_PTR(-EINVAL);
|
||||
|
||||
/*
|
||||
* A bpf handler decides the invocation flags per exec with
|
||||
* bpf_binprm_set_flags() rather than fixing them at registration, so a
|
||||
* 'B' entry carries no flags: 'P', 'C' and 'O' become per-exec choices
|
||||
* and 'F' (pre-open a fixed interpreter) is meaningless for it.
|
||||
*/
|
||||
if (test_bit(MISC_FMT_BPF_BIT, &e->flags) &&
|
||||
(e->flags & (MISC_FMT_PRESERVE_ARGV0 | MISC_FMT_OPEN_BINARY |
|
||||
MISC_FMT_CREDENTIALS | MISC_FMT_OPEN_FILE)))
|
||||
/* Non-F opens the interp at exec against the caller's cwd; require absolute. */
|
||||
if ((e->flags & (MISC_FMT_LOADER | MISC_FMT_CREDENTIALS)) &&
|
||||
!(e->flags & MISC_FMT_OPEN_FILE) &&
|
||||
e->interpreter[0] != '/')
|
||||
return ERR_PTR(-EINVAL);
|
||||
|
||||
return no_free_ptr(e);
|
||||
@@ -743,14 +874,9 @@ static int bm_entry_show(struct seq_file *m, void *unused)
|
||||
|
||||
/* print the special flags */
|
||||
seq_puts(m, "flags: ");
|
||||
if (e->flags & MISC_FMT_PRESERVE_ARGV0)
|
||||
seq_putc(m, 'P');
|
||||
if (e->flags & MISC_FMT_OPEN_BINARY)
|
||||
seq_putc(m, 'O');
|
||||
if (e->flags & MISC_FMT_CREDENTIALS)
|
||||
seq_putc(m, 'C');
|
||||
if (e->flags & MISC_FMT_OPEN_FILE)
|
||||
seq_putc(m, 'F');
|
||||
for (int i = 0; i < ARRAY_SIZE(misc_flags); i++)
|
||||
if (e->flags & misc_flags[i].flag)
|
||||
seq_putc(m, misc_flags[i].c);
|
||||
seq_putc(m, '\n');
|
||||
|
||||
if (test_bit(MISC_FMT_BPF_BIT, &e->flags)) {
|
||||
|
||||
@@ -171,19 +171,33 @@ __bpf_kfunc int bpf_binprm_set_interp_arg(struct linux_binprm *bprm,
|
||||
* @flags: an OR of enum bpf_binprm_flags values
|
||||
*
|
||||
* To be called from the load program of a struct binfmt_misc_ops handler. It
|
||||
* decides per exec what a static entry fixes at registration with the P, C and
|
||||
* O flags: BPF_BINPRM_PRESERVE_ARGV0 keeps the caller's argv[0],
|
||||
* decides per exec what a static entry fixes at registration with the P, C,
|
||||
* O, T and L flags: BPF_BINPRM_PRESERVE_ARGV0 keeps the caller's argv[0],
|
||||
* BPF_BINPRM_CREDENTIALS computes credentials from the binary, and
|
||||
* BPF_BINPRM_EXECFD hands the binary to the interpreter through AT_EXECFD.
|
||||
* BPF_BINPRM_TRANSPARENT additionally leaves the argument vector untouched,
|
||||
* making the exec look like a direct execution of the binary.
|
||||
* BPF_BINPRM_LOADER substitutes the interpreter for the binary's PT_INTERP
|
||||
* and runs the binary as a native exec; it excludes every other flag.
|
||||
* Calling it again replaces the flags, passing zero clears them again.
|
||||
*
|
||||
* Return: 0 on success, -EINVAL if @flags contains an unknown bit
|
||||
* Return: 0 on success, -EINVAL if @flags contains an unknown bit or an
|
||||
* invalid combination
|
||||
*/
|
||||
__bpf_kfunc int bpf_binprm_set_flags(struct linux_binprm *bprm,
|
||||
enum bpf_binprm_flags flags)
|
||||
{
|
||||
if (flags & ~(BPF_BINPRM_PRESERVE_ARGV0 | BPF_BINPRM_CREDENTIALS |
|
||||
BPF_BINPRM_EXECFD))
|
||||
BPF_BINPRM_EXECFD | BPF_BINPRM_TRANSPARENT |
|
||||
BPF_BINPRM_LOADER))
|
||||
return -EINVAL;
|
||||
|
||||
/* Loader substitution is a native exec: no splice, execfd or creds work. */
|
||||
if ((flags & BPF_BINPRM_LOADER) && (flags & ~BPF_BINPRM_LOADER))
|
||||
return -EINVAL;
|
||||
|
||||
/* Transparency preserves the whole argv, argv[0] included. */
|
||||
if ((flags & BPF_BINPRM_TRANSPARENT) && (flags & BPF_BINPRM_PRESERVE_ARGV0))
|
||||
return -EINVAL;
|
||||
|
||||
bprm->bpf_flags = flags;
|
||||
|
||||
84
fs/exec.c
84
fs/exec.c
@@ -1101,6 +1101,17 @@ void __set_task_comm(struct task_struct *tsk, const char *buf, bool exec)
|
||||
perf_event_comm(tsk, exec);
|
||||
}
|
||||
|
||||
/*
|
||||
* The file the process presents as: its exe link and comm. A transparent
|
||||
* dispatch presents as the binary, which is bprm->executable.
|
||||
*/
|
||||
static struct file *bprm_identity_file(const struct linux_binprm *bprm)
|
||||
{
|
||||
if (bprm->interp_flags & BINPRM_FLAGS_TRANSPARENT_INTERP)
|
||||
return bprm->executable;
|
||||
return bprm->file;
|
||||
}
|
||||
|
||||
/*
|
||||
* Calling this is the point of no return. None of the failures will be
|
||||
* seen by userspace since either the process is already taking a fatal
|
||||
@@ -1112,6 +1123,10 @@ int begin_new_exec(struct linux_binprm * bprm)
|
||||
struct task_struct *me = current;
|
||||
int retval;
|
||||
|
||||
/* A pending PT_INTERP substitution this format cannot consume. */
|
||||
if (bprm->loader)
|
||||
return -ENOEXEC;
|
||||
|
||||
/* Once we are committed compute the creds */
|
||||
retval = bprm_creds_from_file(bprm);
|
||||
if (retval)
|
||||
@@ -1151,7 +1166,7 @@ int begin_new_exec(struct linux_binprm * bprm)
|
||||
* not visible until then. Doing it here also ensures
|
||||
* we don't race against replace_mm_exe_file().
|
||||
*/
|
||||
retval = set_mm_exe_file(bprm->mm, bprm->file);
|
||||
retval = set_mm_exe_file(bprm->mm, bprm_identity_file(bprm));
|
||||
if (retval)
|
||||
goto out;
|
||||
|
||||
@@ -1241,6 +1256,8 @@ int begin_new_exec(struct linux_binprm * bprm)
|
||||
* Let's fix it up to be something reasonable.
|
||||
*/
|
||||
if (bprm->comm_from_dentry) {
|
||||
struct file *comm_file = bprm_identity_file(bprm);
|
||||
|
||||
/*
|
||||
* Hold RCU lock to keep the name from being freed behind our back.
|
||||
* Use acquire semantics to make sure the terminating NUL from
|
||||
@@ -1250,7 +1267,7 @@ int begin_new_exec(struct linux_binprm * bprm)
|
||||
* detecting a concurrent rename and just want a terminated name.
|
||||
*/
|
||||
rcu_read_lock();
|
||||
__set_task_comm(me, smp_load_acquire(&bprm->file->f_path.dentry->d_name.name),
|
||||
__set_task_comm(me, smp_load_acquire(&comm_file->f_path.dentry->d_name.name),
|
||||
true);
|
||||
rcu_read_unlock();
|
||||
} else {
|
||||
@@ -1291,10 +1308,17 @@ int begin_new_exec(struct linux_binprm * bprm)
|
||||
|
||||
/* Pass the opened binary to the interpreter. */
|
||||
if (bprm->have_execfd) {
|
||||
retval = FD_ADD(0, bprm->executable);
|
||||
if (retval < 0)
|
||||
goto out_unlock;
|
||||
struct file *executable = bprm->executable;
|
||||
|
||||
/* mm->exe_file carries its own write denial now so drop it. */
|
||||
exe_file_allow_write_access(executable);
|
||||
bprm->executable = NULL;
|
||||
retval = FD_ADD(0, executable);
|
||||
if (retval < 0) {
|
||||
/* The reference was not consumed. */
|
||||
fput(executable);
|
||||
goto out_unlock;
|
||||
}
|
||||
bprm->execfd = retval;
|
||||
}
|
||||
return 0;
|
||||
@@ -1394,6 +1418,39 @@ static void do_close_execat(struct file *file)
|
||||
fput(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* bprm_open_interpreter - open the interpreter the binary asks for
|
||||
* @bprm: binary that is being executed
|
||||
* @path: the interpreter path named in the binary's PT_INTERP
|
||||
*
|
||||
* A binfmt_misc loader entry substitutes for the interpreter the binary
|
||||
* names. Hand out the stashed substitute if there is one and open @path
|
||||
* if there is not. The caller owns the reference either way and releases
|
||||
* it like any other open_exec() one.
|
||||
*
|
||||
* Return: the interpreter on success, an ERR_PTR on failure
|
||||
*/
|
||||
struct file *bprm_open_interpreter(struct linux_binprm *bprm, const char *path)
|
||||
{
|
||||
if (bprm->loader)
|
||||
return no_free_ptr(bprm->loader);
|
||||
return open_exec(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* bprm_drop_loader - discard a PT_INTERP substitute that does not apply
|
||||
* @bprm: binary that is being executed
|
||||
*
|
||||
* A binary without PT_INTERP has nothing to substitute for, so drop the
|
||||
* override and let the binary load natively rather than have
|
||||
* begin_new_exec() refuse it. A no-op once bprm_open_interpreter() took
|
||||
* the substitute.
|
||||
*/
|
||||
void bprm_drop_loader(struct linux_binprm *bprm)
|
||||
{
|
||||
do_close_execat(no_free_ptr(bprm->loader));
|
||||
}
|
||||
|
||||
static void free_bprm(struct linux_binprm *bprm)
|
||||
{
|
||||
if (bprm->mm) {
|
||||
@@ -1413,8 +1470,9 @@ static void free_bprm(struct linux_binprm *bprm)
|
||||
if (bprm->old_mm)
|
||||
exec_mm_put_old(bprm->old_mm);
|
||||
do_close_execat(bprm->file);
|
||||
if (bprm->executable)
|
||||
fput(bprm->executable);
|
||||
/* An unconsumed PT_INTERP substitute from a binfmt_misc loader entry. */
|
||||
bprm_drop_loader(bprm);
|
||||
do_close_execat(bprm->executable);
|
||||
/* If a binfmt changed the interp, free it. */
|
||||
if (bprm->interp != bprm->filename)
|
||||
kfree(bprm->interp);
|
||||
@@ -1731,19 +1789,23 @@ static int exec_binprm(struct linux_binprm *bprm)
|
||||
if (!bprm->interpreter)
|
||||
break;
|
||||
|
||||
/* A stashed PT_INTERP substitute belonged to the replaced file. */
|
||||
bprm_drop_loader(bprm);
|
||||
|
||||
exec = bprm->file;
|
||||
bprm->file = bprm->interpreter;
|
||||
bprm->interpreter = NULL;
|
||||
|
||||
exe_file_allow_write_access(exec);
|
||||
if (unlikely(bprm->have_execfd)) {
|
||||
if (bprm->executable) {
|
||||
fput(exec);
|
||||
do_close_execat(exec);
|
||||
return -ENOEXEC;
|
||||
}
|
||||
/* Kept for AT_EXECFD; the write denial rides along until hand-over. */
|
||||
bprm->executable = exec;
|
||||
} else
|
||||
fput(exec);
|
||||
} else {
|
||||
do_close_execat(exec);
|
||||
}
|
||||
}
|
||||
|
||||
audit_bprm(bprm);
|
||||
|
||||
@@ -16,6 +16,12 @@ struct user_namespace;
|
||||
* @BPF_BINPRM_CREDENTIALS: compute credentials from the binary; implies execfd
|
||||
* (like the 'C' flag)
|
||||
* @BPF_BINPRM_EXECFD: pass the binary via AT_EXECFD (like the 'O' flag)
|
||||
* @BPF_BINPRM_TRANSPARENT: leave argv untouched, the interpreter takes the
|
||||
* binary from AT_EXECFD (like the 'T' flag); implies
|
||||
* execfd, excludes preserve-argv0
|
||||
* @BPF_BINPRM_LOADER: substitute the interpreter for the binary's PT_INTERP
|
||||
* and run the binary as a native exec (like the 'L'
|
||||
* flag); excludes every other flag
|
||||
*
|
||||
* Set from a load program with bpf_binprm_set_flags(). Unlike a static entry,
|
||||
* a bpf handler chooses these per exec rather than once at registration.
|
||||
@@ -24,6 +30,8 @@ enum bpf_binprm_flags {
|
||||
BPF_BINPRM_PRESERVE_ARGV0 = (1ULL << 0),
|
||||
BPF_BINPRM_CREDENTIALS = (1ULL << 1),
|
||||
BPF_BINPRM_EXECFD = (1ULL << 2),
|
||||
BPF_BINPRM_TRANSPARENT = (1ULL << 3),
|
||||
BPF_BINPRM_LOADER = (1ULL << 4),
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -62,6 +62,7 @@ struct linux_binprm {
|
||||
is_check:1;
|
||||
struct file *executable; /* Executable to pass to the interpreter */
|
||||
struct file *interpreter;
|
||||
struct file *loader;
|
||||
struct file *file;
|
||||
struct cred *cred; /* new credentials */
|
||||
int unsafe; /* how unsafe this exec is (mask of LSM_UNSAFE_*) */
|
||||
@@ -93,6 +94,28 @@ struct linux_binprm {
|
||||
#define BINPRM_FLAGS_PRESERVE_ARGV0_BIT 3
|
||||
#define BINPRM_FLAGS_PRESERVE_ARGV0 (1 << BINPRM_FLAGS_PRESERVE_ARGV0_BIT)
|
||||
|
||||
/* binfmt_misc dispatched to the interpreter transparently */
|
||||
#define BINPRM_FLAGS_TRANSPARENT_INTERP_BIT 4
|
||||
#define BINPRM_FLAGS_TRANSPARENT_INTERP (1 << BINPRM_FLAGS_TRANSPARENT_INTERP_BIT)
|
||||
|
||||
/**
|
||||
* bprm_at_flags - the AT_FLAGS this invocation implies
|
||||
* @bprm: binary that is being executed
|
||||
*
|
||||
* Tell the program on the receiving end which dispatch contract it got.
|
||||
*
|
||||
* Return: the AT_FLAGS value for this exec
|
||||
*/
|
||||
static inline unsigned long bprm_at_flags(const struct linux_binprm *bprm)
|
||||
{
|
||||
/* Transparency preserves the whole argv, argv[0] included. */
|
||||
if (bprm->interp_flags & BINPRM_FLAGS_TRANSPARENT_INTERP)
|
||||
return AT_FLAGS_TRANSPARENT_INTERP;
|
||||
if (bprm->interp_flags & BINPRM_FLAGS_PRESERVE_ARGV0)
|
||||
return AT_FLAGS_PRESERVE_ARGV0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* This structure defines the functions that are used to load the binary formats that
|
||||
* linux accepts.
|
||||
@@ -137,6 +160,8 @@ extern int begin_new_exec(struct linux_binprm * bprm);
|
||||
extern void setup_new_exec(struct linux_binprm * bprm);
|
||||
extern void finalize_exec(struct linux_binprm *bprm);
|
||||
extern void would_dump(struct linux_binprm *, struct file *);
|
||||
struct file *bprm_open_interpreter(struct linux_binprm *bprm, const char *path);
|
||||
void bprm_drop_loader(struct linux_binprm *bprm);
|
||||
|
||||
extern int suid_dumpable;
|
||||
|
||||
|
||||
@@ -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 */
|
||||
|
||||
5
tools/testing/selftests/exec/.gitignore
vendored
5
tools/testing/selftests/exec/.gitignore
vendored
@@ -22,5 +22,10 @@ S_I*.test
|
||||
binfmt_misc_bpf
|
||||
binfmt_bpf_interp
|
||||
binfmt_bpf_app
|
||||
binfmt_misc_transparent
|
||||
binfmt_transparent_interp
|
||||
binfmt_misc_loader
|
||||
binfmt_loader_payload
|
||||
binfmt_loader_payload_static
|
||||
*.bpf.o
|
||||
vmlinux.h
|
||||
|
||||
@@ -21,6 +21,18 @@ TEST_GEN_PROGS += recursion-depth
|
||||
TEST_GEN_PROGS += null-argv
|
||||
TEST_GEN_PROGS += check-exec
|
||||
|
||||
# Static ('T' flag) transparent binfmt_misc test; the asserting interpreter
|
||||
# is shared with the bpf harness's transparent case. No bpf toolchain needed.
|
||||
TEST_GEN_PROGS += binfmt_misc_transparent
|
||||
TEST_GEN_FILES += binfmt_transparent_interp
|
||||
|
||||
# 'L' (loader substitution) binfmt_misc test: the payload runs as the main
|
||||
# image with a copy of the system loader substituted for its PT_INTERP and
|
||||
# asserts the native identity from inside; the static build proves the
|
||||
# override is dropped for a binary without PT_INTERP.
|
||||
TEST_GEN_PROGS += binfmt_misc_loader
|
||||
TEST_GEN_FILES += binfmt_loader_payload binfmt_loader_payload_static
|
||||
|
||||
# binfmt_misc bpf-backed ('B') handler test: a libbpf harness plus its
|
||||
# struct_ops objects and the test interpreter/app it routes between. Only
|
||||
# built when clang, bpftool, the vmlinux BTF and libbpf are all present
|
||||
@@ -35,7 +47,8 @@ HAVE_BPF_TOOLCHAIN ?= $(shell command -v $(CLANG) >/dev/null 2>&1 && \
|
||||
pkg-config --exists libbpf 2>/dev/null && echo y)
|
||||
ifeq ($(HAVE_BPF_TOOLCHAIN),y)
|
||||
TEST_GEN_PROGS += binfmt_misc_bpf
|
||||
TEST_GEN_FILES += bpf_interp.bpf.o nix_origin.bpf.o
|
||||
TEST_GEN_FILES += bpf_interp.bpf.o nix_origin.bpf.o transparent.bpf.o
|
||||
TEST_GEN_FILES += loader.bpf.o
|
||||
TEST_GEN_FILES += binfmt_bpf_interp binfmt_bpf_app
|
||||
else
|
||||
$(info exec selftests: skipping binfmt_misc_bpf, needs clang, bpftool, vmlinux BTF and libbpf)
|
||||
@@ -44,6 +57,8 @@ endif
|
||||
EXTRA_CLEAN := $(OUTPUT)/subdir.moved $(OUTPUT)/execveat.moved $(OUTPUT)/xxxxx* \
|
||||
$(OUTPUT)/S_I*.test
|
||||
|
||||
LOCAL_HDRS += binfmt_misc_common.h
|
||||
|
||||
include ../lib.mk
|
||||
|
||||
CHECK_EXEC_SAMPLES := $(top_srcdir)/samples/check-exec
|
||||
@@ -86,20 +101,27 @@ LIBBPF_LDLIBS ?= -lbpf -lelf -lz
|
||||
|
||||
$(OUTPUT)/vmlinux.h:
|
||||
$(BPFTOOL) btf dump file $(VMLINUX_BTF) format c > $@
|
||||
sed -i '/__ksym;$$/d' $@
|
||||
|
||||
# BPF_NO_KFUNC_PROTOTYPES: the programs declare the kfuncs they use themselves.
|
||||
$(OUTPUT)/%.bpf.o: %.bpf.c $(OUTPUT)/vmlinux.h
|
||||
$(CLANG) -g -O2 -target bpf -mcpu=v3 $(BPF_CFLAGS) $(LIBBPF_CFLAGS) -c $< -o $@
|
||||
$(CLANG) -g -O2 -target bpf -mcpu=v3 -DBPF_NO_KFUNC_PROTOTYPES \
|
||||
$(BPF_CFLAGS) $(LIBBPF_CFLAGS) -c $< -o $@
|
||||
|
||||
$(OUTPUT)/binfmt_misc_bpf: binfmt_misc_bpf.c
|
||||
$(OUTPUT)/binfmt_misc_bpf: binfmt_misc_bpf.c binfmt_misc_common.h
|
||||
$(CC) $(CFLAGS) $(LIBBPF_CFLAGS) $(LDFLAGS) $< $(LIBBPF_LDLIBS) -o $@
|
||||
|
||||
$(OUTPUT)/binfmt_bpf_interp: binfmt_bpf_interp.c
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) $< -o $@
|
||||
|
||||
$(OUTPUT)/binfmt_loader_payload: binfmt_loader_payload.c binfmt_misc_common.h
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -fPIE -pie $< -o $@
|
||||
|
||||
$(OUTPUT)/binfmt_loader_payload_static: binfmt_loader_payload.c binfmt_misc_common.h
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -static $< -o $@
|
||||
|
||||
# PT_INTERP is set to the literal "$ORIGIN/binfmt_bpf_interp"; the nix_origin
|
||||
# handler resolves it relative to the binary at run time.
|
||||
$(OUTPUT)/binfmt_bpf_app: binfmt_bpf_app.c
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -Wl,--dynamic-linker,'$$ORIGIN/binfmt_bpf_interp' $< -o $@
|
||||
|
||||
EXTRA_CLEAN += $(OUTPUT)/vmlinux.h $(OUTPUT)/bpf_interp.bpf.o $(OUTPUT)/nix_origin.bpf.o
|
||||
EXTRA_CLEAN += $(OUTPUT)/vmlinux.h $(OUTPUT)/*.bpf.o
|
||||
|
||||
146
tools/testing/selftests/exec/binfmt_loader_payload.c
Normal file
146
tools/testing/selftests/exec/binfmt_loader_payload.c
Normal 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;
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
*
|
||||
* echo ':name:B::::<handler>:' > /proc/sys/fs/binfmt_misc/register
|
||||
*
|
||||
* Two self-contained cases are exercised:
|
||||
* Three self-contained cases are exercised:
|
||||
*
|
||||
* 1. bpf_interp: the match program matches a synthetic aarch64 ELF header
|
||||
* from the prefetched bprm->buf and the load program routes it to a
|
||||
@@ -18,73 +18,60 @@
|
||||
* commit only to a "$ORIGIN/..."-relative PT_INTERP and the load program
|
||||
* resolves it to an interpreter co-located with the binary (the
|
||||
* relocatable-loader case the kernel ELF loader cannot express).
|
||||
* 3. transparent: the load program sets BPF_BINPRM_TRANSPARENT; the
|
||||
* asserting interpreter (binfmt_transparent_interp) verifies the
|
||||
* identity the kernel constructed (exe link, argv, cmdline, comm,
|
||||
* AT_EXECFD, write denial) from inside the process.
|
||||
* 4. loader: the load program sets BPF_BINPRM_LOADER; the payload
|
||||
* (binfmt_loader_payload) runs as the main image with the selected
|
||||
* interpreter substituted for its PT_INTERP and asserts the native
|
||||
* identity from inside.
|
||||
*
|
||||
* Both route to a test interpreter that prints BPF_INTERP_RAN, proving the
|
||||
* program's chosen interpreter actually ran.
|
||||
* The first two route to a test interpreter that prints BPF_INTERP_RAN,
|
||||
* proving the program's chosen interpreter actually ran.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <elf.h>
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <libgen.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <bpf/btf.h>
|
||||
#include <bpf/libbpf.h>
|
||||
|
||||
#include "binfmt_misc_common.h"
|
||||
#include "kselftest_harness.h"
|
||||
|
||||
#define INTERP_PATH "/tmp/binfmt_bpf_interp"
|
||||
#define AARCH64_PATH "/tmp/binfmt_bpf_aarch64"
|
||||
#define RELOC_DIR "/tmp/binfmt_reloc"
|
||||
#define BINFMT_REG "/proc/sys/fs/binfmt_misc/register"
|
||||
#define RELOC_TEMPLATE "/tmp/binfmt_relocXXXXXX"
|
||||
#define TRANS_INTERP "/tmp/binfmt_transparent_interp"
|
||||
#define TRANS_PATH "/tmp/binfmt_bpf_riscv"
|
||||
#define EXPECT "BPF_INTERP_RAN"
|
||||
#define TRANS_EXPECT "TRANSPARENT_OK"
|
||||
#define LOADER_INTERP "/tmp/binfmt_loader_interp"
|
||||
#define LOADER_PATH "/tmp/binfmt_bpf_loader.ldrtest"
|
||||
|
||||
static char testdir[512]; /* directory holding this test's built artifacts */
|
||||
|
||||
static int copy_file(const char *src, const char *dst)
|
||||
{
|
||||
char buf[4096];
|
||||
int in, out;
|
||||
ssize_t n;
|
||||
|
||||
in = open(src, O_RDONLY);
|
||||
if (in < 0)
|
||||
return -1;
|
||||
out = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0755);
|
||||
if (out < 0) {
|
||||
close(in);
|
||||
return -1;
|
||||
}
|
||||
while ((n = read(in, buf, sizeof(buf))) > 0) {
|
||||
if (write(out, buf, n) != n) {
|
||||
close(in);
|
||||
close(out);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
close(in);
|
||||
close(out);
|
||||
return n < 0 ? -1 : 0;
|
||||
}
|
||||
|
||||
/* A minimal 64-bit little-endian aarch64 ELF header, padded to the read size. */
|
||||
static int create_fake_aarch64(const char *path)
|
||||
/* A minimal 64-bit little-endian ELF header, padded to the read size. */
|
||||
static int create_fake_elf(const char *path, unsigned short machine)
|
||||
{
|
||||
unsigned char hdr[256] = {0};
|
||||
int fd;
|
||||
|
||||
hdr[0] = 0x7f; hdr[1] = 'E'; hdr[2] = 'L'; hdr[3] = 'F';
|
||||
hdr[4] = 2; /* ELFCLASS64 */
|
||||
hdr[5] = 1; /* ELFDATA2LSB */
|
||||
hdr[6] = 1; /* EV_CURRENT */
|
||||
hdr[16] = 2; /* e_type = ET_EXEC */
|
||||
hdr[18] = 183 & 0xff; /* e_machine = EM_AARCH64 */
|
||||
hdr[19] = (183 >> 8) & 0xff;
|
||||
hdr[20] = 1; /* e_version */
|
||||
hdr[4] = ELFCLASS64;
|
||||
hdr[5] = ELFDATA2LSB;
|
||||
hdr[6] = EV_CURRENT;
|
||||
hdr[16] = ET_EXEC;
|
||||
hdr[18] = machine & 0xff; /* e_machine, little-endian */
|
||||
hdr[19] = machine >> 8;
|
||||
hdr[20] = EV_CURRENT;
|
||||
|
||||
fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0755);
|
||||
unlink(path);
|
||||
fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0755);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
if (write(fd, hdr, sizeof(hdr)) != (ssize_t)sizeof(hdr)) {
|
||||
@@ -97,31 +84,10 @@ static int create_fake_aarch64(const char *path)
|
||||
|
||||
static int register_entry(const char *name, const char *handler)
|
||||
{
|
||||
char rule[128];
|
||||
int fd;
|
||||
ssize_t n;
|
||||
char rule[PATH_MAX];
|
||||
|
||||
snprintf(rule, sizeof(rule), ":%s:B::::%s:", name, handler);
|
||||
fd = open(BINFMT_REG, O_WRONLY);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
n = write(fd, rule, strlen(rule));
|
||||
close(fd);
|
||||
return n < 0 ? -1 : 0;
|
||||
}
|
||||
|
||||
static void unregister_entry(const char *name)
|
||||
{
|
||||
char path[128];
|
||||
int fd;
|
||||
|
||||
snprintf(path, sizeof(path), "/proc/sys/fs/binfmt_misc/%s", name);
|
||||
fd = open(path, O_WRONLY);
|
||||
if (fd >= 0) {
|
||||
if (write(fd, "-1", 2) < 0)
|
||||
; /* best effort */
|
||||
close(fd);
|
||||
}
|
||||
return write_reg(rule);
|
||||
}
|
||||
|
||||
static int check_output(const char *cmd, const char *expected)
|
||||
@@ -140,138 +106,205 @@ static int check_output(const char *cmd, const char *expected)
|
||||
return strncmp(buf, expected, strlen(expected)) ? -1 : 0;
|
||||
}
|
||||
|
||||
/* An attached handler with its 'B' entry activated. */
|
||||
struct bpf_case {
|
||||
struct bpf_object *obj;
|
||||
struct bpf_link *link;
|
||||
const char *entry;
|
||||
};
|
||||
|
||||
/*
|
||||
* Load @objfile, attach its struct_ops map @handler (which publishes the
|
||||
* handler), activate a 'B' entry named @entry that references it, run @target
|
||||
* and check it produced @expect.
|
||||
* handler) and activate a 'B' entry named @entry that references it.
|
||||
*/
|
||||
static int run_case(const char *objfile, const char *handler,
|
||||
const char *entry, const char *target, const char *expect)
|
||||
static int bpf_case_start(struct bpf_case *c, const char *objfile,
|
||||
const char *handler, const char *entry)
|
||||
{
|
||||
struct bpf_object *obj;
|
||||
struct bpf_map *map;
|
||||
struct bpf_link *link;
|
||||
int ret = -1;
|
||||
|
||||
obj = bpf_object__open_file(objfile, NULL);
|
||||
if (!obj || libbpf_get_error(obj)) {
|
||||
c->obj = NULL;
|
||||
c->link = NULL;
|
||||
c->entry = entry;
|
||||
|
||||
c->obj = bpf_object__open_file(objfile, NULL);
|
||||
if (!c->obj || libbpf_get_error(c->obj)) {
|
||||
fprintf(stderr, "open %s failed\n", objfile);
|
||||
c->obj = NULL;
|
||||
return -1;
|
||||
}
|
||||
if (bpf_object__load(obj)) {
|
||||
if (bpf_object__load(c->obj)) {
|
||||
fprintf(stderr, "load %s failed (check dmesg for the verifier log)\n",
|
||||
objfile);
|
||||
goto close;
|
||||
goto fail;
|
||||
}
|
||||
map = bpf_object__find_map_by_name(obj, handler);
|
||||
map = bpf_object__find_map_by_name(c->obj, handler);
|
||||
if (!map) {
|
||||
fprintf(stderr, "no struct_ops map '%s' in %s\n", handler, objfile);
|
||||
goto close;
|
||||
goto fail;
|
||||
}
|
||||
link = bpf_map__attach_struct_ops(map);
|
||||
if (!link || libbpf_get_error(link)) {
|
||||
c->link = bpf_map__attach_struct_ops(map);
|
||||
if (!c->link || libbpf_get_error(c->link)) {
|
||||
fprintf(stderr, "attach struct_ops '%s' failed\n", handler);
|
||||
goto close;
|
||||
c->link = NULL;
|
||||
goto fail;
|
||||
}
|
||||
if (register_entry(entry, handler)) {
|
||||
fprintf(stderr, "register 'B' entry '%s' failed\n", entry);
|
||||
goto detach;
|
||||
goto fail;
|
||||
}
|
||||
return 0;
|
||||
|
||||
fail:
|
||||
bpf_link__destroy(c->link);
|
||||
bpf_object__close(c->obj);
|
||||
c->obj = NULL;
|
||||
c->link = NULL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void bpf_case_stop(struct bpf_case *c)
|
||||
{
|
||||
unregister(c->entry);
|
||||
bpf_link__destroy(c->link);
|
||||
bpf_object__close(c->obj);
|
||||
}
|
||||
|
||||
/* Activate @handler, run @target and check it produced @expect. */
|
||||
static int run_case(const char *objfile, const char *handler,
|
||||
const char *entry, const char *target, const char *expect)
|
||||
{
|
||||
struct bpf_case c;
|
||||
int ret;
|
||||
|
||||
if (bpf_case_start(&c, objfile, handler, entry))
|
||||
return -1;
|
||||
ret = check_output(target, expect);
|
||||
unregister_entry(entry);
|
||||
detach:
|
||||
bpf_link__destroy(link);
|
||||
close:
|
||||
bpf_object__close(obj);
|
||||
bpf_case_stop(&c);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
FIXTURE(bpf_handler) {
|
||||
char obj[PATH_MAX]; /* struct_ops object of the case under test */
|
||||
};
|
||||
|
||||
FIXTURE_SETUP(bpf_handler)
|
||||
{
|
||||
char src[600], obj[600], appdst[600], interpdst[600];
|
||||
char exe[512];
|
||||
ssize_t n;
|
||||
int fail = 0;
|
||||
struct stat st;
|
||||
char src[PATH_MAX];
|
||||
struct btf *btf;
|
||||
|
||||
if (getuid() != 0) {
|
||||
fprintf(stderr, "Skipping: test must be run as root\n");
|
||||
return 4; /* KSFT_SKIP */
|
||||
}
|
||||
if (getuid() != 0)
|
||||
SKIP(return, "test must be run as root");
|
||||
|
||||
/* The kernel must know struct binfmt_misc_ops (CONFIG_BINFMT_MISC_BPF). */
|
||||
btf = btf__load_vmlinux_btf();
|
||||
if (!btf || btf__find_by_name_kind(btf, "binfmt_misc_ops",
|
||||
BTF_KIND_STRUCT) < 0) {
|
||||
fprintf(stderr,
|
||||
"Skipping: no struct binfmt_misc_ops in the kernel BTF (CONFIG_BINFMT_MISC_BPF)\n");
|
||||
btf__free(btf);
|
||||
return 4; /* KSFT_SKIP */
|
||||
SKIP(return,
|
||||
"no struct binfmt_misc_ops in the kernel BTF (CONFIG_BINFMT_MISC_BPF)");
|
||||
}
|
||||
btf__free(btf);
|
||||
|
||||
n = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
|
||||
if (n < 0) {
|
||||
perror("readlink");
|
||||
return 1;
|
||||
}
|
||||
exe[n] = '\0';
|
||||
snprintf(testdir, sizeof(testdir), "%s", dirname(exe));
|
||||
|
||||
if (stat("/sys/fs/bpf", &st) < 0)
|
||||
mkdir("/sys/fs/bpf", 0755);
|
||||
mount("bpf", "/sys/fs/bpf", "bpf", 0, NULL);
|
||||
if (access(BINFMT_REG, F_OK) < 0)
|
||||
mount("binfmt_misc", "/proc/sys/fs/binfmt_misc", "binfmt_misc", 0, NULL);
|
||||
if (!binfmt_misc_available())
|
||||
SKIP(return, "no binfmt_misc");
|
||||
|
||||
/* Shared test interpreter. */
|
||||
snprintf(src, sizeof(src), "%s/binfmt_bpf_interp", testdir);
|
||||
if (copy_file(src, INTERP_PATH)) {
|
||||
fprintf(stderr, "cannot install %s\n", INTERP_PATH);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Case 1: match a synthetic aarch64 header -> fixed interpreter. */
|
||||
printf("[*] case 1: match aarch64 header -> program-chosen interpreter\n");
|
||||
if (create_fake_aarch64(AARCH64_PATH)) {
|
||||
fprintf(stderr, "cannot create %s\n", AARCH64_PATH);
|
||||
return 1;
|
||||
}
|
||||
snprintf(obj, sizeof(obj), "%s/bpf_interp.bpf.o", testdir);
|
||||
if (run_case(obj, "bpf_interp", "test_bpf_interp", AARCH64_PATH, EXPECT) == 0)
|
||||
printf("[+] case 1 passed\n");
|
||||
else {
|
||||
printf("[-] case 1 FAILED\n");
|
||||
fail = 1;
|
||||
}
|
||||
unlink(AARCH64_PATH);
|
||||
|
||||
/* Case 2: $ORIGIN-relative PT_INTERP -> co-located interpreter. */
|
||||
printf("[*] case 2: $ORIGIN interpreter resolved relative to the binary\n");
|
||||
mkdir(RELOC_DIR, 0755);
|
||||
snprintf(appdst, sizeof(appdst), "%s/app", RELOC_DIR);
|
||||
snprintf(interpdst, sizeof(interpdst), "%s/binfmt_bpf_interp", RELOC_DIR);
|
||||
snprintf(src, sizeof(src), "%s/binfmt_bpf_app", testdir);
|
||||
if (copy_file(src, appdst) ||
|
||||
copy_file(INTERP_PATH, interpdst)) {
|
||||
fprintf(stderr, "cannot set up %s\n", RELOC_DIR);
|
||||
fail = 1;
|
||||
} else {
|
||||
snprintf(obj, sizeof(obj), "%s/nix_origin.bpf.o", testdir);
|
||||
if (run_case(obj, "nix_origin", "test_bpf_origin", appdst, EXPECT) == 0)
|
||||
printf("[+] case 2 passed\n");
|
||||
else {
|
||||
printf("[-] case 2 FAILED\n");
|
||||
fail = 1;
|
||||
}
|
||||
}
|
||||
unlink(appdst);
|
||||
unlink(interpdst);
|
||||
rmdir(RELOC_DIR);
|
||||
unlink(INTERP_PATH);
|
||||
|
||||
if (!fail)
|
||||
printf("[*] all binfmt_misc bpf cases passed\n");
|
||||
return fail;
|
||||
ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_bpf_interp"), 0);
|
||||
ASSERT_EQ(copy_file(src, INTERP_PATH), 0);
|
||||
}
|
||||
|
||||
FIXTURE_TEARDOWN(bpf_handler)
|
||||
{
|
||||
unlink(INTERP_PATH);
|
||||
}
|
||||
|
||||
/* The match program matches a synthetic header, the load program routes it. */
|
||||
TEST_F(bpf_handler, fixed_interpreter)
|
||||
{
|
||||
ASSERT_EQ(create_fake_elf(AARCH64_PATH, EM_AARCH64), 0);
|
||||
ASSERT_EQ(artifact_path(self->obj, sizeof(self->obj),
|
||||
"bpf_interp.bpf.o"), 0);
|
||||
EXPECT_EQ(run_case(self->obj, "bpf_interp", "test_bpf_interp",
|
||||
AARCH64_PATH, EXPECT), 0);
|
||||
unlink(AARCH64_PATH);
|
||||
}
|
||||
|
||||
/* A "$ORIGIN/..." PT_INTERP resolved to an interpreter next to the binary. */
|
||||
TEST_F(bpf_handler, origin_relative_interpreter)
|
||||
{
|
||||
char src[PATH_MAX], app[PATH_MAX], interp[PATH_MAX];
|
||||
char dir[] = RELOC_TEMPLATE;
|
||||
|
||||
ASSERT_NE(mkdtemp(dir), NULL);
|
||||
snprintf(app, sizeof(app), "%s/app", dir);
|
||||
snprintf(interp, sizeof(interp), "%s/binfmt_bpf_interp", dir);
|
||||
ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_bpf_app"), 0);
|
||||
ASSERT_EQ(copy_file(src, app), 0);
|
||||
ASSERT_EQ(copy_file(INTERP_PATH, interp), 0);
|
||||
|
||||
ASSERT_EQ(artifact_path(self->obj, sizeof(self->obj),
|
||||
"nix_origin.bpf.o"), 0);
|
||||
EXPECT_EQ(run_case(self->obj, "nix_origin", "test_bpf_origin",
|
||||
app, EXPECT), 0);
|
||||
|
||||
unlink(app);
|
||||
unlink(interp);
|
||||
rmdir(dir);
|
||||
}
|
||||
|
||||
/* A transparent dispatch: the process presents as the binary, not the interp. */
|
||||
TEST_F(bpf_handler, transparent_dispatch)
|
||||
{
|
||||
char src[PATH_MAX], cmd[PATH_MAX + 16];
|
||||
|
||||
/* Probe for transparent-mode support via its static counterpart. */
|
||||
if (binfmt_flag_supported('T'))
|
||||
SKIP(return, "kernel without transparent mode");
|
||||
|
||||
ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_transparent_interp"), 0);
|
||||
ASSERT_EQ(copy_file(src, TRANS_INTERP), 0);
|
||||
ASSERT_EQ(create_fake_elf(TRANS_PATH, EM_RISCV), 0);
|
||||
|
||||
setenv("BINFMT_TEST_BINARY", TRANS_PATH, 1);
|
||||
snprintf(cmd, sizeof(cmd), "%s argone argtwo", TRANS_PATH);
|
||||
ASSERT_EQ(artifact_path(self->obj, sizeof(self->obj),
|
||||
"transparent.bpf.o"), 0);
|
||||
EXPECT_EQ(run_case(self->obj, "transparent", "test_bpf_transparent",
|
||||
cmd, TRANS_EXPECT), 0);
|
||||
|
||||
unlink(TRANS_PATH);
|
||||
unlink(TRANS_INTERP);
|
||||
}
|
||||
|
||||
/* A per-exec loader substitution: the payload runs as a native exec. */
|
||||
TEST_F(bpf_handler, loader_substitution)
|
||||
{
|
||||
char src[PATH_MAX], loader[PATH_MAX];
|
||||
struct bpf_case c;
|
||||
int status;
|
||||
|
||||
if (find_loader(loader, sizeof(loader)))
|
||||
SKIP(return, "cannot determine own PT_INTERP");
|
||||
|
||||
ASSERT_EQ(copy_file(loader, LOADER_INTERP), 0);
|
||||
ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_loader_payload"), 0);
|
||||
ASSERT_EQ(copy_file(src, LOADER_PATH), 0);
|
||||
ASSERT_EQ(patch_file(LOADER_PATH, EI_PAD, LOADER_MARKER,
|
||||
strlen(LOADER_MARKER)), 0);
|
||||
ASSERT_EQ(artifact_path(self->obj, sizeof(self->obj),
|
||||
"loader.bpf.o"), 0);
|
||||
|
||||
setenv("BINFMT_TEST_BINARY", LOADER_PATH, 1);
|
||||
setenv("BINFMT_TEST_INTERP", LOADER_INTERP, 1);
|
||||
|
||||
ASSERT_EQ(bpf_case_start(&c, self->obj, "loader", "test_bpf_loader"), 0);
|
||||
status = run_payload(LOADER_PATH);
|
||||
bpf_case_stop(&c);
|
||||
EXPECT_EQ(status, 0);
|
||||
|
||||
unsetenv("BINFMT_TEST_INTERP");
|
||||
unlink(LOADER_PATH);
|
||||
unlink(LOADER_INTERP);
|
||||
}
|
||||
|
||||
TEST_HARNESS_MAIN
|
||||
|
||||
276
tools/testing/selftests/exec/binfmt_misc_common.h
Normal file
276
tools/testing/selftests/exec/binfmt_misc_common.h
Normal file
@@ -0,0 +1,276 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0 */
|
||||
/* Helpers shared by the binfmt_misc selftests. */
|
||||
#ifndef __SELFTESTS_EXEC_BINFMT_MISC_COMMON_H
|
||||
#define __SELFTESTS_EXEC_BINFMT_MISC_COMMON_H
|
||||
|
||||
#include <elf.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <libgen.h>
|
||||
#include <limits.h>
|
||||
#include <link.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define BINFMT_DIR "/proc/sys/fs/binfmt_misc"
|
||||
#define BINFMT_REG BINFMT_DIR "/register"
|
||||
|
||||
/* comm holds 15 usable chars; a read of /proc/self/comm appends a newline. */
|
||||
#define TASK_COMM_LEN 16
|
||||
|
||||
/* The canonical payload argv: run_payload() passes it, the payloads assert it. */
|
||||
#define PAYLOAD_ARGV0 "payload-argv0"
|
||||
#define PAYLOAD_ARG1 "argone"
|
||||
#define PAYLOAD_ARG2 "argtwo"
|
||||
|
||||
/* Marker the loader tests poke into the payload's e_ident padding. */
|
||||
#define LOADER_MARKER "LDRTST"
|
||||
|
||||
/* Exit status run_payload() reports when the exec was refused as unhandled. */
|
||||
#define RUN_ENOEXEC 42
|
||||
|
||||
static inline int copy_file(const char *src, const char *dst)
|
||||
{
|
||||
char buf[4096];
|
||||
int in, out;
|
||||
ssize_t n;
|
||||
|
||||
in = open(src, O_RDONLY);
|
||||
if (in < 0)
|
||||
return -1;
|
||||
/* The tests share /tmp, so never write through a name they don't own. */
|
||||
unlink(dst);
|
||||
out = open(dst, O_WRONLY | O_CREAT | O_EXCL, 0755);
|
||||
if (out < 0) {
|
||||
close(in);
|
||||
return -1;
|
||||
}
|
||||
while ((n = read(in, buf, sizeof(buf))) > 0) {
|
||||
if (write(out, buf, n) != n) {
|
||||
close(in);
|
||||
close(out);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
close(in);
|
||||
close(out);
|
||||
return n < 0 ? -1 : 0;
|
||||
}
|
||||
|
||||
/* Write @rule to the register file, preserving the write's errno. */
|
||||
static inline int write_reg(const char *rule)
|
||||
{
|
||||
int fd, saved;
|
||||
ssize_t n;
|
||||
|
||||
fd = open(BINFMT_REG, O_WRONLY);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
n = write(fd, rule, strlen(rule));
|
||||
saved = errno;
|
||||
close(fd);
|
||||
errno = saved;
|
||||
return n < 0 ? -1 : 0;
|
||||
}
|
||||
|
||||
static inline void unregister(const char *name)
|
||||
{
|
||||
char path[PATH_MAX];
|
||||
int fd;
|
||||
|
||||
snprintf(path, sizeof(path), BINFMT_DIR "/%s", name);
|
||||
fd = open(path, O_WRONLY);
|
||||
if (fd >= 0) {
|
||||
if (write(fd, "-1", 2) < 0)
|
||||
; /* best effort */
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
/* Mount binfmt_misc unless it already is, and report whether it is usable. */
|
||||
static inline bool binfmt_misc_available(void)
|
||||
{
|
||||
if (access(BINFMT_REG, F_OK) < 0)
|
||||
mount("binfmt_misc", BINFMT_DIR, "binfmt_misc", 0, NULL);
|
||||
return access(BINFMT_REG, F_OK) == 0;
|
||||
}
|
||||
|
||||
/* Absolute path of @name in the directory this test was built into. */
|
||||
static inline int artifact_path(char *out, size_t sz, const char *name)
|
||||
{
|
||||
char exe[PATH_MAX];
|
||||
ssize_t n;
|
||||
|
||||
n = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
|
||||
if (n < 0)
|
||||
return -1;
|
||||
exe[n] = '\0';
|
||||
if ((size_t)snprintf(out, sz, "%s/%s", dirname(exe), name) >= sz)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Probe kernel support for a registration flag with a throwaway entry. */
|
||||
static inline int binfmt_flag_supported(char flag)
|
||||
{
|
||||
char rule[64];
|
||||
|
||||
snprintf(rule, sizeof(rule), ":bm_flag_probe:E::bmprobe::/bin/true:%c",
|
||||
flag);
|
||||
if (write_reg(rule))
|
||||
return -1;
|
||||
unregister("bm_flag_probe");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Run @path with the canonical payload argv and return its exit status, or
|
||||
* RUN_ENOEXEC when the exec itself was refused as unhandled.
|
||||
*/
|
||||
static inline int run_payload(const char *path)
|
||||
{
|
||||
int status;
|
||||
pid_t pid;
|
||||
|
||||
pid = fork();
|
||||
if (pid == 0) {
|
||||
execl(path, PAYLOAD_ARGV0, PAYLOAD_ARG1, PAYLOAD_ARG2,
|
||||
(char *)NULL);
|
||||
_exit(errno == ENOEXEC ? RUN_ENOEXEC : 126);
|
||||
}
|
||||
if (pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
|
||||
return -1;
|
||||
return WEXITSTATUS(status);
|
||||
}
|
||||
|
||||
/* Does the exe link name @path? */
|
||||
static inline bool exe_is(const char *path)
|
||||
{
|
||||
char exe[PATH_MAX], real[PATH_MAX];
|
||||
ssize_t n;
|
||||
|
||||
n = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
|
||||
if (n <= 0 || !realpath(path, real))
|
||||
return false;
|
||||
exe[n] = '\0';
|
||||
return !strcmp(exe, real);
|
||||
}
|
||||
|
||||
/* Is comm @name truncated to what a comm can hold? */
|
||||
static inline bool comm_is(const char *name)
|
||||
{
|
||||
char comm[TASK_COMM_LEN + 2], expect[TASK_COMM_LEN];
|
||||
ssize_t n;
|
||||
int fd;
|
||||
|
||||
fd = open("/proc/self/comm", O_RDONLY);
|
||||
if (fd < 0)
|
||||
return false;
|
||||
n = read(fd, comm, sizeof(comm) - 1);
|
||||
close(fd);
|
||||
if (n <= 0)
|
||||
return false;
|
||||
if (comm[n - 1] == '\n')
|
||||
n--;
|
||||
comm[n] = '\0';
|
||||
snprintf(expect, sizeof(expect), "%s", name);
|
||||
return !strcmp(comm, expect);
|
||||
}
|
||||
|
||||
/* Opening @path for writing has to fail with ETXTBSY. */
|
||||
static inline bool write_denied(const char *path)
|
||||
{
|
||||
int fd = open(path, O_WRONLY);
|
||||
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
return false;
|
||||
}
|
||||
return errno == ETXTBSY;
|
||||
}
|
||||
|
||||
static inline int patch_file(const char *path, off_t off, const void *data, size_t len)
|
||||
{
|
||||
ssize_t n;
|
||||
int fd;
|
||||
|
||||
fd = open(path, O_WRONLY);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
n = pwrite(fd, data, len, off);
|
||||
close(fd);
|
||||
return n == (ssize_t)len ? 0 : -1;
|
||||
}
|
||||
|
||||
/* start_code and end_code are the 26th and 27th fields of /proc/pid/stat. */
|
||||
static inline int stat_codes(pid_t pid, unsigned long *start_code,
|
||||
unsigned long *end_code)
|
||||
{
|
||||
char buf[4096], path[64], *p;
|
||||
ssize_t n;
|
||||
int fd, i;
|
||||
|
||||
snprintf(path, sizeof(path), "/proc/%d/stat", pid);
|
||||
fd = open(path, O_RDONLY);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
n = read(fd, buf, sizeof(buf) - 1);
|
||||
close(fd);
|
||||
if (n <= 0)
|
||||
return -1;
|
||||
buf[n] = '\0';
|
||||
|
||||
/* Skip "pid (comm)", then start_code is the 24th field after it. */
|
||||
p = strrchr(buf, ')');
|
||||
if (!p)
|
||||
return -1;
|
||||
p++;
|
||||
for (i = 0; i < 23; i++) {
|
||||
p = strchr(p + 1, ' ');
|
||||
if (!p)
|
||||
return -1;
|
||||
}
|
||||
if (sscanf(p, " %lu %lu", start_code, end_code) != 2)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Find the system loader through our own PT_INTERP. */
|
||||
static inline int find_loader(char *out, size_t sz)
|
||||
{
|
||||
ElfW(Ehdr) eh;
|
||||
ElfW(Phdr) ph;
|
||||
int fd, i, ret = -1;
|
||||
|
||||
fd = open("/proc/self/exe", O_RDONLY);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
if (pread(fd, &eh, sizeof(eh), 0) != sizeof(eh))
|
||||
goto out;
|
||||
for (i = 0; i < eh.e_phnum; i++) {
|
||||
if (pread(fd, &ph, sizeof(ph),
|
||||
eh.e_phoff + i * eh.e_phentsize) != sizeof(ph))
|
||||
goto out;
|
||||
if (ph.p_type != PT_INTERP)
|
||||
continue;
|
||||
if (!ph.p_filesz || ph.p_filesz > sz)
|
||||
goto out;
|
||||
if (pread(fd, out, ph.p_filesz, ph.p_offset) !=
|
||||
(ssize_t)ph.p_filesz)
|
||||
goto out;
|
||||
out[ph.p_filesz - 1] = '\0';
|
||||
ret = 0;
|
||||
break;
|
||||
}
|
||||
out:
|
||||
close(fd);
|
||||
return ret;
|
||||
}
|
||||
|
||||
#endif /* __SELFTESTS_EXEC_BINFMT_MISC_COMMON_H */
|
||||
372
tools/testing/selftests/exec/binfmt_misc_loader.c
Normal file
372
tools/testing/selftests/exec/binfmt_misc_loader.c
Normal 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
|
||||
95
tools/testing/selftests/exec/binfmt_misc_transparent.c
Normal file
95
tools/testing/selftests/exec/binfmt_misc_transparent.c
Normal 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
|
||||
112
tools/testing/selftests/exec/binfmt_transparent_interp.c
Normal file
112
tools/testing/selftests/exec/binfmt_transparent_interp.c
Normal 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;
|
||||
}
|
||||
@@ -1,2 +1,9 @@
|
||||
CONFIG_BLK_DEV=y
|
||||
CONFIG_BLK_DEV_LOOP=y
|
||||
CONFIG_BINFMT_MISC=y
|
||||
CONFIG_BINFMT_MISC_BPF=y
|
||||
CONFIG_BPF_JIT=y
|
||||
CONFIG_BPF_SYSCALL=y
|
||||
CONFIG_DEBUG_INFO=y
|
||||
CONFIG_DEBUG_INFO_BTF=y
|
||||
CONFIG_DEBUG_INFO_DWARF4=y
|
||||
|
||||
56
tools/testing/selftests/exec/loader.bpf.c
Normal file
56
tools/testing/selftests/exec/loader.bpf.c
Normal 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",
|
||||
};
|
||||
57
tools/testing/selftests/exec/transparent.bpf.c
Normal file
57
tools/testing/selftests/exec/transparent.bpf.c
Normal 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",
|
||||
};
|
||||
Reference in New Issue
Block a user