diff --git a/tools/testing/selftests/exec/.gitignore b/tools/testing/selftests/exec/.gitignore index 8b93b405c424..94b9ab4eb46c 100644 --- a/tools/testing/selftests/exec/.gitignore +++ b/tools/testing/selftests/exec/.gitignore @@ -22,5 +22,7 @@ S_I*.test binfmt_misc_bpf binfmt_bpf_interp binfmt_bpf_app +binfmt_misc_transparent +binfmt_transparent_interp *.bpf.o vmlinux.h diff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile index d2a5a58f9432..978b8bb572fe 100644 --- a/tools/testing/selftests/exec/Makefile +++ b/tools/testing/selftests/exec/Makefile @@ -21,6 +21,11 @@ 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 + # 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 +40,7 @@ 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 += binfmt_bpf_interp binfmt_bpf_app else $(info exec selftests: skipping binfmt_misc_bpf, needs clang, bpftool, vmlinux BTF and libbpf) diff --git a/tools/testing/selftests/exec/binfmt_misc_bpf.c b/tools/testing/selftests/exec/binfmt_misc_bpf.c index c41fb80f2a72..31bc7dded585 100644 --- a/tools/testing/selftests/exec/binfmt_misc_bpf.c +++ b/tools/testing/selftests/exec/binfmt_misc_bpf.c @@ -9,7 +9,7 @@ * * echo ':name:B:::::' > /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,9 +18,13 @@ * 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. * - * 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 @@ -40,7 +44,10 @@ #define INTERP_PATH "/tmp/binfmt_bpf_interp" #define AARCH64_PATH "/tmp/binfmt_bpf_aarch64" #define RELOC_TEMPLATE "/tmp/binfmt_relocXXXXXX" +#define TRANS_INTERP "/tmp/binfmt_transparent_interp" +#define TRANS_PATH "/tmp/binfmt_bpf_riscv" #define EXPECT "BPF_INTERP_RAN" +#define TRANS_EXPECT "TRANSPARENT_OK" /* A minimal 64-bit little-endian ELF header, padded to the read size. */ static int create_fake_elf(const char *path, unsigned short machine) @@ -208,4 +215,28 @@ TEST_F(bpf_handler, origin_relative_interpreter) 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); +} + TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/exec/binfmt_misc_common.h b/tools/testing/selftests/exec/binfmt_misc_common.h index 70ae66082e40..0bd37e92421b 100644 --- a/tools/testing/selftests/exec/binfmt_misc_common.h +++ b/tools/testing/selftests/exec/binfmt_misc_common.h @@ -9,13 +9,27 @@ #include #include #include +#include #include #include +#include +#include #include #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" + +/* 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]; @@ -97,4 +111,83 @@ static inline int artifact_path(char *out, size_t sz, const char *name) 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; +} + #endif /* __SELFTESTS_EXEC_BINFMT_MISC_COMMON_H */ diff --git a/tools/testing/selftests/exec/binfmt_misc_transparent.c b/tools/testing/selftests/exec/binfmt_misc_transparent.c new file mode 100644 index 000000000000..d0cb845df1d3 --- /dev/null +++ b/tools/testing/selftests/exec/binfmt_misc_transparent.c @@ -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 +#include + +#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 diff --git a/tools/testing/selftests/exec/binfmt_transparent_interp.c b/tools/testing/selftests/exec/binfmt_transparent_interp.c new file mode 100644 index 000000000000..d4c4a538c9aa --- /dev/null +++ b/tools/testing/selftests/exec/binfmt_transparent_interp.c @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +#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; +} diff --git a/tools/testing/selftests/exec/transparent.bpf.c b/tools/testing/selftests/exec/transparent.bpf.c new file mode 100644 index 000000000000..7632019ebe69 --- /dev/null +++ b/tools/testing/selftests/exec/transparent.bpf.c @@ -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 +#include + +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", +};