mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-08-31 03:35:32 -04:00
selftests/exec: test interpreters bound to a 'B' entry
One handler, one entry registered disabled, an interpreter per guest architecture bound to a file one write at a time. The load program picks one by name per exec: - an aarch64 binary runs the interpreter bound as "first" and a riscv one the interpreter bound as "second", from a single entry and a single handler - unlinking a bound interpreter and putting a different binary in its place changes nothing, which is what the binding exists for - the entry reports what it bound, under the names it bound them as - a name the entry did not bind fails the exec with -ENOENT rather than falling back to anything - activating the entry refuses further binding with -EBUSY, a later disable does not undo that, and an entry registered without 'D' never accepted a '+' write to begin with - a name binds one interpreter, and control characters are refused - the command has to end at the write, bytes past an embedded nul are refused - an entry binds at most 100 interpreters, the next one is refused with -ENOSPC The test interpreter prints its argv[0], which is the path the kernel ran that copy under, so one binary installed at two paths tells the harness which of them the program picked. Link: https://patch.msgid.link/20260730-work-binfmt_misc-preopen-v1-8-4a0b0da71f16@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
This commit is contained in:
@@ -56,8 +56,8 @@ HAVE_BPF_TOOLCHAIN ?= $(shell command -v $(CLANG) >/dev/null 2>&1 && \
|
||||
ifeq ($(HAVE_BPF_TOOLCHAIN),y)
|
||||
TEST_GEN_PROGS += binfmt_misc_bpf
|
||||
TEST_GEN_FILES += bpf_interp.bpf.o nix_origin.bpf.o transparent.bpf.o
|
||||
TEST_GEN_FILES += loader.bpf.o
|
||||
TEST_GEN_FILES += binfmt_bpf_interp binfmt_bpf_app
|
||||
TEST_GEN_FILES += loader.bpf.o interp_bind.bpf.o
|
||||
TEST_GEN_FILES += binfmt_bpf_interp binfmt_bpf_app binfmt_bind_interp
|
||||
else
|
||||
$(info exec selftests: skipping binfmt_misc_bpf, needs clang, bpftool, vmlinux BTF and libbpf)
|
||||
endif
|
||||
@@ -127,6 +127,9 @@ $(OUTPUT)/binfmt_misc_bpf: binfmt_misc_bpf.c binfmt_misc_common.h
|
||||
$(OUTPUT)/binfmt_bpf_interp: binfmt_bpf_interp.c
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) $< -o $@
|
||||
|
||||
$(OUTPUT)/binfmt_bind_interp: binfmt_bind_interp.c
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) $< -o $@
|
||||
|
||||
$(OUTPUT)/binfmt_loader_payload: binfmt_loader_payload.c binfmt_misc_common.h
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -fPIE -pie $< -o $@
|
||||
|
||||
|
||||
14
tools/testing/selftests/exec/binfmt_bind_interp.c
Normal file
14
tools/testing/selftests/exec/binfmt_bind_interp.c
Normal file
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* Test interpreter for the bound-interpreter case of the binfmt_misc_bpf
|
||||
* selftest. Two copies are installed at different paths and bound to one
|
||||
* entry under different names; printing argv[0] - the path the kernel ran
|
||||
* this copy under - tells the harness which of them the load program picked.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
printf("BIND_RAN %s\n", argc > 0 ? argv[0] : "");
|
||||
return 0;
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
*
|
||||
* echo ':name:B::::<handler>:' > /proc/sys/fs/binfmt_misc/register
|
||||
*
|
||||
* Three self-contained cases are exercised:
|
||||
* Five self-contained cases are exercised:
|
||||
*
|
||||
* 1. bpf_interp: the match program matches a synthetic aarch64 ELF header
|
||||
* from the prefetched bprm->buf and the load program routes it to a
|
||||
@@ -26,6 +26,11 @@
|
||||
* (binfmt_loader_payload) runs as the main image with the selected
|
||||
* interpreter substituted for its PT_INTERP and asserts the native
|
||||
* identity from inside.
|
||||
* 5. interp_bind: an entry registered disabled with 'D' is given its
|
||||
* interpreters one write at a time, and the load program picks one by
|
||||
* name per exec. Replacing what the path holds afterwards changes
|
||||
* nothing, which is the point of binding a file rather than resolving
|
||||
* a name at exec time. Enabling the entry seals it.
|
||||
*
|
||||
* The first two route to a test interpreter that prints BPF_INTERP_RAN,
|
||||
* proving the program's chosen interpreter actually ran.
|
||||
@@ -54,6 +59,12 @@
|
||||
#define TRANS_EXPECT "TRANSPARENT_OK"
|
||||
#define LOADER_INTERP "/tmp/binfmt_loader_interp"
|
||||
#define LOADER_PATH "/tmp/binfmt_bpf_loader.ldrtest"
|
||||
#define BIND_FIRST "/tmp/binfmt_bind_first"
|
||||
#define BIND_SECOND "/tmp/binfmt_bind_second"
|
||||
#define BIND_ARM_PATH "/tmp/binfmt_bind_arm"
|
||||
#define BIND_RISCV_PATH "/tmp/binfmt_bind_riscv"
|
||||
#define BIND_EXPECT "BIND_RAN "
|
||||
#define BIND_MAX 100
|
||||
|
||||
/* A minimal 64-bit little-endian ELF header, padded to the read size. */
|
||||
static int create_fake_elf(const char *path, unsigned short machine)
|
||||
@@ -82,11 +93,17 @@ static int create_fake_elf(const char *path, unsigned short machine)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int register_entry(const char *name, const char *handler)
|
||||
/*
|
||||
* Register a 'B' entry for @handler. With @flags "D" the entry is created
|
||||
* disabled, which is what leaves it open to being given interpreters.
|
||||
*/
|
||||
static int register_entry(const char *name, const char *handler,
|
||||
const char *flags)
|
||||
{
|
||||
char rule[PATH_MAX];
|
||||
|
||||
snprintf(rule, sizeof(rule), ":%s:B::::%s:", name, handler);
|
||||
snprintf(rule, sizeof(rule), ":%s:B::::%s:%s", name, handler,
|
||||
flags ? flags : "");
|
||||
return write_reg(rule);
|
||||
}
|
||||
|
||||
@@ -139,10 +156,12 @@ struct bpf_case {
|
||||
|
||||
/*
|
||||
* Load @objfile, attach its struct_ops map @handler (which publishes the
|
||||
* handler) and activate a 'B' entry named @entry that references it.
|
||||
* handler) and register a 'B' entry named @entry that references it, with
|
||||
* @flags as the entry's register-string flags.
|
||||
*/
|
||||
static int bpf_case_start(struct bpf_case *c, const char *objfile,
|
||||
const char *handler, const char *entry)
|
||||
static int bpf_case_start_flags(struct bpf_case *c, const char *objfile,
|
||||
const char *handler, const char *entry,
|
||||
const char *flags)
|
||||
{
|
||||
struct bpf_map *map;
|
||||
|
||||
@@ -172,7 +191,7 @@ static int bpf_case_start(struct bpf_case *c, const char *objfile,
|
||||
c->link = NULL;
|
||||
goto fail;
|
||||
}
|
||||
if (register_entry(entry, handler)) {
|
||||
if (register_entry(entry, handler, flags)) {
|
||||
fprintf(stderr, "register 'B' entry '%s' failed\n", entry);
|
||||
goto fail;
|
||||
}
|
||||
@@ -186,6 +205,12 @@ static int bpf_case_start(struct bpf_case *c, const char *objfile,
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int bpf_case_start(struct bpf_case *c, const char *objfile,
|
||||
const char *handler, const char *entry)
|
||||
{
|
||||
return bpf_case_start_flags(c, objfile, handler, entry, NULL);
|
||||
}
|
||||
|
||||
static void bpf_case_stop(struct bpf_case *c)
|
||||
{
|
||||
unregister(c->entry);
|
||||
@@ -318,4 +343,226 @@ TEST_F(bpf_handler, loader_substitution)
|
||||
unlink(LOADER_INTERP);
|
||||
}
|
||||
|
||||
/* The errno an exec of @path fails with, 0 if it succeeded. */
|
||||
static int exec_errno(const char *path)
|
||||
{
|
||||
int status;
|
||||
pid_t pid;
|
||||
|
||||
pid = fork();
|
||||
if (pid == 0) {
|
||||
execl(path, path, (char *)NULL);
|
||||
_exit(errno);
|
||||
}
|
||||
if (pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
|
||||
return -1;
|
||||
return WEXITSTATUS(status);
|
||||
}
|
||||
|
||||
/* Install a copy of the bound-interpreter test binary at @path. */
|
||||
static int install_interp(const char *path)
|
||||
{
|
||||
char src[PATH_MAX];
|
||||
|
||||
if (artifact_path(src, sizeof(src), "binfmt_bind_interp"))
|
||||
return -1;
|
||||
return copy_file(src, path);
|
||||
}
|
||||
|
||||
/* Bind @path to @entry under @name, the '+' command of a disabled entry. */
|
||||
static int entry_bind(const char *entry, const char *name, const char *path)
|
||||
{
|
||||
char cmd[PATH_MAX];
|
||||
|
||||
snprintf(cmd, sizeof(cmd), "+%s %s\n", name, path);
|
||||
return entry_command(entry, cmd);
|
||||
}
|
||||
|
||||
FIXTURE(bound_interp) {
|
||||
char obj[PATH_MAX];
|
||||
struct bpf_case c;
|
||||
bool started;
|
||||
};
|
||||
|
||||
FIXTURE_SETUP(bound_interp)
|
||||
{
|
||||
const char *why = bpf_handler_unsupported();
|
||||
|
||||
if (why)
|
||||
SKIP(return, "%s", why);
|
||||
if (!binfmt_flag_supported('D')) {
|
||||
ASSERT_EQ(errno, EINVAL);
|
||||
SKIP(return, "kernel without the 'D' flag");
|
||||
}
|
||||
|
||||
ASSERT_EQ(install_interp(BIND_FIRST), 0);
|
||||
ASSERT_EQ(install_interp(BIND_SECOND), 0);
|
||||
|
||||
ASSERT_EQ(artifact_path(self->obj, sizeof(self->obj),
|
||||
"interp_bind.bpf.o"), 0);
|
||||
|
||||
/*
|
||||
* Registered disabled, so it cannot be matched yet and can still be
|
||||
* given interpreters. Each path is resolved once, by its write(2);
|
||||
* from here on the entry holds the files themselves.
|
||||
*/
|
||||
ASSERT_EQ(bpf_case_start_flags(&self->c, self->obj, "interp_bind",
|
||||
"test_interp_bind", "D"), 0);
|
||||
self->started = true;
|
||||
|
||||
ASSERT_EQ(entry_bind("test_interp_bind", "first", BIND_FIRST), 0);
|
||||
ASSERT_EQ(entry_bind("test_interp_bind", "second", BIND_SECOND), 0);
|
||||
}
|
||||
|
||||
FIXTURE_TEARDOWN(bound_interp)
|
||||
{
|
||||
if (self->started)
|
||||
bpf_case_stop(&self->c);
|
||||
unlink(BIND_FIRST);
|
||||
unlink(BIND_SECOND);
|
||||
unlink(AARCH64_PATH);
|
||||
unlink(BIND_RISCV_PATH);
|
||||
unlink(BIND_ARM_PATH);
|
||||
}
|
||||
|
||||
/* Enabling is what makes the configured entry matchable. */
|
||||
static int activate(const char *entry)
|
||||
{
|
||||
return entry_command(entry, "1\n");
|
||||
}
|
||||
|
||||
/* One entry, one interpreter per guest architecture, picked per exec. */
|
||||
TEST_F(bound_interp, selects_by_name)
|
||||
{
|
||||
ASSERT_EQ(create_fake_elf(AARCH64_PATH, EM_AARCH64), 0);
|
||||
ASSERT_EQ(create_fake_elf(BIND_RISCV_PATH, EM_RISCV), 0);
|
||||
|
||||
/* Disabled, so it does not match and no format claims the binary. */
|
||||
EXPECT_EQ(exec_errno(AARCH64_PATH), ENOEXEC);
|
||||
|
||||
ASSERT_EQ(activate("test_interp_bind"), 0);
|
||||
EXPECT_EQ(check_output(AARCH64_PATH, BIND_EXPECT BIND_FIRST), 0);
|
||||
EXPECT_EQ(check_output(BIND_RISCV_PATH, BIND_EXPECT BIND_SECOND), 0);
|
||||
}
|
||||
|
||||
/* What was bound is what runs, whatever the path holds afterwards. */
|
||||
TEST_F(bound_interp, path_no_longer_decides)
|
||||
{
|
||||
char other[PATH_MAX];
|
||||
|
||||
ASSERT_EQ(create_fake_elf(AARCH64_PATH, EM_AARCH64), 0);
|
||||
ASSERT_EQ(activate("test_interp_bind"), 0);
|
||||
|
||||
/* Bound interpreters are pinned against writes, exactly like 'F'. */
|
||||
EXPECT_TRUE(write_denied(BIND_FIRST));
|
||||
|
||||
/* Replace the path with a different binary: a new file, new inode. */
|
||||
ASSERT_EQ(artifact_path(other, sizeof(other), "binfmt_bpf_interp"), 0);
|
||||
ASSERT_EQ(unlink(BIND_FIRST), 0);
|
||||
ASSERT_EQ(copy_file(other, BIND_FIRST), 0);
|
||||
|
||||
EXPECT_EQ(check_output(AARCH64_PATH, BIND_EXPECT BIND_FIRST), 0);
|
||||
}
|
||||
|
||||
/* The entry reports what it bound, under the names it bound them as. */
|
||||
TEST_F(bound_interp, entry_reports_bindings)
|
||||
{
|
||||
EXPECT_TRUE(entry_shows("test_interp_bind",
|
||||
"bpf-interpreter first " BIND_FIRST));
|
||||
EXPECT_TRUE(entry_shows("test_interp_bind",
|
||||
"bpf-interpreter second " BIND_SECOND));
|
||||
}
|
||||
|
||||
/* Selecting a name the entry did not bind fails the exec. */
|
||||
TEST_F(bound_interp, unbound_name_fails)
|
||||
{
|
||||
ASSERT_EQ(create_fake_elf(BIND_ARM_PATH, EM_ARM), 0);
|
||||
ASSERT_EQ(activate("test_interp_bind"), 0);
|
||||
|
||||
EXPECT_EQ(exec_errno(BIND_ARM_PATH), ENOENT);
|
||||
}
|
||||
|
||||
/* Activating seals it: what can be matched cannot be changed. */
|
||||
TEST_F(bound_interp, sealed_once_active)
|
||||
{
|
||||
ASSERT_EQ(activate("test_interp_bind"), 0);
|
||||
|
||||
EXPECT_EQ(entry_bind("test_interp_bind", "third", BIND_SECOND), -EBUSY);
|
||||
EXPECT_FALSE(entry_shows("test_interp_bind",
|
||||
"bpf-interpreter third " BIND_SECOND));
|
||||
}
|
||||
|
||||
/* The seal is for good: disabling the entry again reopens nothing. */
|
||||
TEST_F(bound_interp, disable_does_not_unseal)
|
||||
{
|
||||
ASSERT_EQ(activate("test_interp_bind"), 0);
|
||||
ASSERT_EQ(entry_command("test_interp_bind", "0\n"), 0);
|
||||
|
||||
EXPECT_EQ(entry_bind("test_interp_bind", "third", BIND_SECOND), -EBUSY);
|
||||
}
|
||||
|
||||
/* An entry registered without 'D' is sealed from the start. */
|
||||
TEST_F(bound_interp, born_sealed)
|
||||
{
|
||||
/* A second entry for the handler the fixture already published. */
|
||||
ASSERT_EQ(register_entry("test_born_sealed", "interp_bind", NULL), 0);
|
||||
|
||||
EXPECT_EQ(entry_bind("test_born_sealed", "first", BIND_FIRST), -EBUSY);
|
||||
unregister("test_born_sealed");
|
||||
}
|
||||
|
||||
/* A name is bound once; a second use of it is refused. */
|
||||
TEST_F(bound_interp, duplicate_name_refused)
|
||||
{
|
||||
EXPECT_EQ(entry_bind("test_interp_bind", "first", BIND_SECOND), -EEXIST);
|
||||
}
|
||||
|
||||
/* A name is a printable word: the entry file reports 'name path' lines. */
|
||||
TEST_F(bound_interp, name_must_be_printable)
|
||||
{
|
||||
/* A control character would forge a line into the entry file. */
|
||||
EXPECT_EQ(entry_bind("test_interp_bind", "a\tb", BIND_FIRST), -EINVAL);
|
||||
EXPECT_EQ(entry_bind("test_interp_bind", "a\nb", BIND_FIRST), -EINVAL);
|
||||
|
||||
/* A space cannot even be spelled: the path starts after the first one. */
|
||||
EXPECT_EQ(entry_bind("test_interp_bind", "a b", BIND_FIRST), -EINVAL);
|
||||
}
|
||||
|
||||
/* The command ends at the write: bytes past an embedded nul are refused. */
|
||||
TEST_F(bound_interp, trailing_bytes_refused)
|
||||
{
|
||||
char cmd[PATH_MAX];
|
||||
size_t len;
|
||||
int fd;
|
||||
|
||||
/* entry_command() cannot spell a nul, so write the buffer raw. */
|
||||
snprintf(cmd, sizeof(cmd), "+nul %s", BIND_FIRST);
|
||||
len = strlen(cmd) + 1;
|
||||
memcpy(cmd + len, "junk", sizeof("junk"));
|
||||
len += sizeof("junk");
|
||||
|
||||
fd = open(BINFMT_DIR "/test_interp_bind", O_WRONLY | O_CLOEXEC);
|
||||
ASSERT_GE(fd, 0);
|
||||
EXPECT_EQ(write(fd, cmd, len), -1);
|
||||
EXPECT_EQ(errno, EINVAL);
|
||||
close(fd);
|
||||
|
||||
EXPECT_FALSE(entry_shows("test_interp_bind",
|
||||
"bpf-interpreter nul " BIND_FIRST));
|
||||
}
|
||||
|
||||
/* An entry binds at most BIND_MAX interpreters. */
|
||||
TEST_F(bound_interp, capped_bindings)
|
||||
{
|
||||
char name[16];
|
||||
int i;
|
||||
|
||||
/* The fixture bound "first" and "second" already. */
|
||||
for (i = 2; i < BIND_MAX; i++) {
|
||||
snprintf(name, sizeof(name), "n%d", i);
|
||||
ASSERT_EQ(entry_bind("test_interp_bind", name, BIND_FIRST), 0);
|
||||
}
|
||||
EXPECT_EQ(entry_bind("test_interp_bind", "over", BIND_FIRST), -ENOSPC);
|
||||
}
|
||||
|
||||
TEST_HARNESS_MAIN
|
||||
|
||||
76
tools/testing/selftests/exec/interp_bind.bpf.c
Normal file
76
tools/testing/selftests/exec/interp_bind.bpf.c
Normal file
@@ -0,0 +1,76 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* binfmt_misc_ops handler for the selftest's bound-interpreter case: one
|
||||
* handler, one entry, an interpreter per guest architecture - each bound to
|
||||
* a file when the entry was registered rather than to a path resolved at
|
||||
* exec time. The load program names the one it wants; a name the entry did
|
||||
* not bind fails the exec, which the harness checks too.
|
||||
*/
|
||||
#include "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_tracing.h>
|
||||
|
||||
char _license[] SEC("license") = "GPL";
|
||||
|
||||
#define EI_CLASS 4
|
||||
#define ELFCLASS64 2
|
||||
#define E_MACHINE_OFF 18
|
||||
#define EM_ARM 40
|
||||
#define EM_AARCH64 183
|
||||
#define EM_RISCV 243
|
||||
|
||||
extern int bpf_binprm_select_interp(struct linux_binprm *bprm,
|
||||
const char *name, size_t name__sz) __ksym;
|
||||
|
||||
/* The guest architecture of a 64-bit ELF, or zero if it is not one. */
|
||||
static __u16 elf_machine(struct linux_binprm *bprm)
|
||||
{
|
||||
if (bprm->buf[0] != 0x7f || bprm->buf[1] != 'E' ||
|
||||
bprm->buf[2] != 'L' || bprm->buf[3] != 'F' ||
|
||||
bprm->buf[EI_CLASS] != ELFCLASS64)
|
||||
return 0;
|
||||
|
||||
/* Little-endian 16-bit field, read byte-wise for the verifier. */
|
||||
return (__u8)bprm->buf[E_MACHINE_OFF] |
|
||||
((__u16)(__u8)bprm->buf[E_MACHINE_OFF + 1] << 8);
|
||||
}
|
||||
|
||||
SEC("struct_ops.s/match")
|
||||
bool BPF_PROG(interp_bind_match, struct linux_binprm *bprm)
|
||||
{
|
||||
__u16 machine = elf_machine(bprm);
|
||||
|
||||
return machine == EM_AARCH64 || machine == EM_RISCV ||
|
||||
machine == EM_ARM;
|
||||
}
|
||||
|
||||
SEC("struct_ops.s/load")
|
||||
int BPF_PROG(interp_bind_load, struct linux_binprm *bprm)
|
||||
{
|
||||
/*
|
||||
* Names, not paths: each one selects a file the entry pre-opened, so
|
||||
* nothing is resolved here or later, in any namespace. The buffers
|
||||
* are on the stack because the verifier rejects .rodata for a sized
|
||||
* memory argument.
|
||||
*/
|
||||
char first[] = "first";
|
||||
char second[] = "second";
|
||||
char unbound[] = "unbound";
|
||||
|
||||
switch (elf_machine(bprm)) {
|
||||
case EM_AARCH64:
|
||||
return bpf_binprm_select_interp(bprm, first, sizeof(first));
|
||||
case EM_RISCV:
|
||||
return bpf_binprm_select_interp(bprm, second, sizeof(second));
|
||||
}
|
||||
|
||||
/* The entry bound nothing under this name: -ENOENT fails the exec. */
|
||||
return bpf_binprm_select_interp(bprm, unbound, sizeof(unbound));
|
||||
}
|
||||
|
||||
SEC(".struct_ops.link")
|
||||
struct binfmt_misc_ops interp_bind = {
|
||||
.match = (void *)interp_bind_match,
|
||||
.load = (void *)interp_bind_load,
|
||||
.name = "interp_bind",
|
||||
};
|
||||
Reference in New Issue
Block a user