mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-08-27 19:53:15 -04:00
selftests/exec: add binfmt_misc bpf-backed handler test
Exercise the bpf-backed ('B') binfmt_misc handlers end to end. A handler
is a struct binfmt_misc_ops struct_ops map; the test loads and attaches
it (which publishes it by name), activates it with a 'B' entry, and
checks that a matched binary is routed to the interpreter the program
selected via bpf_binprm_set_interp().
Two self-contained cases are covered:
- bpf_interp: the match program matches a synthetic aarch64 ELF header
from the prefetched bprm->buf and the load program routes it to a
fixed interpreter of its choosing.
- nix_origin: the match program parses the program headers to commit
only to a "$ORIGIN/..."-relative PT_INTERP and the load program
resolves it to an interpreter co-located with the binary -- the
relocatable-loader case the kernel ELF loader cannot express. The
relocatable binary is linked with PT_INTERP set to the literal
"$ORIGIN/binfmt_bpf_interp" (-Wl,--dynamic-linker), which the kernel
cannot resolve on its own.
Both route to a small test interpreter that prints a marker, proving the
program-selected interpreter actually ran.
The bpf objects are compiled against the running kernel's BTF: the
Makefile generates vmlinux.h with bpftool and the harness links libbpf.
Override CLANG/BPFTOOL/VMLINUX_BTF/LIBBPF_CFLAGS/LIBBPF_LDLIBS as needed.
The bpf pieces are only built when clang, bpftool, the vmlinux BTF and
libbpf are all present (HAVE_BPF_TOOLCHAIN=y forces them) so the other
exec selftests keep building without a bpf toolchain.
Christian Brauner (Amutable) <brauner@kernel.org> says:
Adapted to the two-op contract: 'B' entries carry the handler name in
the interpreter field, both programs are sleepable, the match programs
decide. nix_origin reads PT_INTERP from the match program and load
returns zero on success. Skip on kernels without binfmt_misc_ops in BTF.
Build the bpf pieces only when the toolchain is present and gitignore
the generated artifacts.
Signed-off-by: Farid Zakaria <farid.m.zakaria@gmail.com>
Link: https://patch.msgid.link/20260714-work-bpf-binfmt_misc-v2-9-57b7529c002c@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
This commit is contained in:
committed by
Christian Brauner
parent
186aaff0d1
commit
277d787feb
5
tools/testing/selftests/exec/.gitignore
vendored
5
tools/testing/selftests/exec/.gitignore
vendored
@@ -19,3 +19,8 @@ null-argv
|
||||
xxxxxxxx*
|
||||
pipe
|
||||
S_I*.test
|
||||
binfmt_misc_bpf
|
||||
binfmt_bpf_interp
|
||||
binfmt_bpf_app
|
||||
*.bpf.o
|
||||
vmlinux.h
|
||||
|
||||
@@ -21,6 +21,26 @@ TEST_GEN_PROGS += recursion-depth
|
||||
TEST_GEN_PROGS += null-argv
|
||||
TEST_GEN_PROGS += check-exec
|
||||
|
||||
# binfmt_misc bpf-backed ('B') handler test: a libbpf harness plus its
|
||||
# struct_ops objects and the test interpreter/app it routes between. Only
|
||||
# built when clang, bpftool, the vmlinux BTF and libbpf are all present
|
||||
# (HAVE_BPF_TOOLCHAIN=y forces it) so the other exec selftests don't grow
|
||||
# a bpf toolchain dependency.
|
||||
CLANG ?= clang
|
||||
BPFTOOL ?= bpftool
|
||||
VMLINUX_BTF ?= /sys/kernel/btf/vmlinux
|
||||
HAVE_BPF_TOOLCHAIN ?= $(shell command -v $(CLANG) >/dev/null 2>&1 && \
|
||||
command -v $(BPFTOOL) >/dev/null 2>&1 && \
|
||||
test -r $(VMLINUX_BTF) && \
|
||||
pkg-config --exists libbpf 2>/dev/null && echo y)
|
||||
ifeq ($(HAVE_BPF_TOOLCHAIN),y)
|
||||
TEST_GEN_PROGS += binfmt_misc_bpf
|
||||
TEST_GEN_FILES += bpf_interp.bpf.o nix_origin.bpf.o
|
||||
TEST_GEN_FILES += binfmt_bpf_interp binfmt_bpf_app
|
||||
else
|
||||
$(info exec selftests: skipping binfmt_misc_bpf, needs clang, bpftool, vmlinux BTF and libbpf)
|
||||
endif
|
||||
|
||||
EXTRA_CLEAN := $(OUTPUT)/subdir.moved $(OUTPUT)/execveat.moved $(OUTPUT)/xxxxx* \
|
||||
$(OUTPUT)/S_I*.test
|
||||
|
||||
@@ -55,3 +75,31 @@ $(OUTPUT)/script-exec.inc: $(CHECK_EXEC_SAMPLES)/script-exec.inc
|
||||
cp $< $@
|
||||
$(OUTPUT)/script-noexec.inc: $(CHECK_EXEC_SAMPLES)/script-noexec.inc
|
||||
cp $< $@
|
||||
|
||||
# --- binfmt_misc bpf ('B') handler test ---------------------------------
|
||||
# The struct_ops bpf objects are compiled against the running kernel's BTF.
|
||||
# CLANG/BPFTOOL/VMLINUX_BTF are set above next to the toolchain check;
|
||||
# override LIBBPF_CFLAGS/LDLIBS to point at a libbpf install.
|
||||
BPF_CFLAGS ?= -I$(OUTPUT)
|
||||
LIBBPF_CFLAGS ?=
|
||||
LIBBPF_LDLIBS ?= -lbpf -lelf -lz
|
||||
|
||||
$(OUTPUT)/vmlinux.h:
|
||||
$(BPFTOOL) btf dump file $(VMLINUX_BTF) format c > $@
|
||||
sed -i '/__ksym;$$/d' $@
|
||||
|
||||
$(OUTPUT)/%.bpf.o: %.bpf.c $(OUTPUT)/vmlinux.h
|
||||
$(CLANG) -g -O2 -target bpf -mcpu=v3 $(BPF_CFLAGS) $(LIBBPF_CFLAGS) -c $< -o $@
|
||||
|
||||
$(OUTPUT)/binfmt_misc_bpf: binfmt_misc_bpf.c
|
||||
$(CC) $(CFLAGS) $(LIBBPF_CFLAGS) $(LDFLAGS) $< $(LIBBPF_LDLIBS) -o $@
|
||||
|
||||
$(OUTPUT)/binfmt_bpf_interp: binfmt_bpf_interp.c
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) $< -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
|
||||
|
||||
12
tools/testing/selftests/exec/binfmt_bpf_app.c
Normal file
12
tools/testing/selftests/exec/binfmt_bpf_app.c
Normal file
@@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* A relocatable binary for the binfmt_misc_bpf $ORIGIN case. The Makefile
|
||||
* links it with PT_INTERP set to the literal "$ORIGIN/binfmt_bpf_interp"
|
||||
* (-Wl,--dynamic-linker), which the kernel ELF loader cannot resolve. The
|
||||
* nix_origin bpf handler resolves it relative to this binary's directory and
|
||||
* routes execution to the co-located interpreter.
|
||||
*/
|
||||
int main(void)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
15
tools/testing/selftests/exec/binfmt_bpf_interp.c
Normal file
15
tools/testing/selftests/exec/binfmt_bpf_interp.c
Normal file
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* Test interpreter for the binfmt_misc_bpf selftest. A bpf-backed 'B' handler
|
||||
* routes a matched binary here; printing this marker proves the program's
|
||||
* chosen interpreter actually ran.
|
||||
*/
|
||||
#include <unistd.h>
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
write(1, "BPF_INTERP_RAN\n", 15);
|
||||
return 0;
|
||||
}
|
||||
277
tools/testing/selftests/exec/binfmt_misc_bpf.c
Normal file
277
tools/testing/selftests/exec/binfmt_misc_bpf.c
Normal file
@@ -0,0 +1,277 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* Selftest for binfmt_misc bpf-backed ('B') handlers.
|
||||
*
|
||||
* A handler is a struct binfmt_misc_ops struct_ops map with a sleepable match
|
||||
* and a sleepable load program. Attaching it publishes it by name in the
|
||||
* caller's user namespace; a 'B' entry referencing it by name in the
|
||||
* interpreter field activates it:
|
||||
*
|
||||
* echo ':name:B::::<handler>:' > /proc/sys/fs/binfmt_misc/register
|
||||
*
|
||||
* Two self-contained cases are exercised:
|
||||
*
|
||||
* 1. bpf_interp: the match program matches a synthetic aarch64 ELF header
|
||||
* from the prefetched bprm->buf and the load program routes it to a
|
||||
* fixed interpreter of its choosing.
|
||||
* 2. nix_origin: the match program reads the binary's program headers to
|
||||
* commit only to a "$ORIGIN/..."-relative PT_INTERP and the load program
|
||||
* resolves it to an interpreter co-located with the binary (the
|
||||
* relocatable-loader case the kernel ELF loader cannot express).
|
||||
*
|
||||
* Both route to a test interpreter that prints BPF_INTERP_RAN, proving the
|
||||
* program's chosen interpreter actually ran.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#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>
|
||||
|
||||
#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 EXPECT "BPF_INTERP_RAN"
|
||||
|
||||
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)
|
||||
{
|
||||
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 */
|
||||
|
||||
fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0755);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
if (write(fd, hdr, sizeof(hdr)) != (ssize_t)sizeof(hdr)) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int register_entry(const char *name, const char *handler)
|
||||
{
|
||||
char rule[128];
|
||||
int fd;
|
||||
ssize_t n;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
static int check_output(const char *cmd, const char *expected)
|
||||
{
|
||||
char buf[128];
|
||||
FILE *fp;
|
||||
|
||||
fp = popen(cmd, "r");
|
||||
if (!fp)
|
||||
return -1;
|
||||
if (!fgets(buf, sizeof(buf), fp)) {
|
||||
pclose(fp);
|
||||
return -1;
|
||||
}
|
||||
pclose(fp);
|
||||
return strncmp(buf, expected, strlen(expected)) ? -1 : 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
static int run_case(const char *objfile, const char *handler,
|
||||
const char *entry, const char *target, const char *expect)
|
||||
{
|
||||
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)) {
|
||||
fprintf(stderr, "open %s failed\n", objfile);
|
||||
return -1;
|
||||
}
|
||||
if (bpf_object__load(obj)) {
|
||||
fprintf(stderr, "load %s failed (check dmesg for the verifier log)\n",
|
||||
objfile);
|
||||
goto close;
|
||||
}
|
||||
map = bpf_object__find_map_by_name(obj, handler);
|
||||
if (!map) {
|
||||
fprintf(stderr, "no struct_ops map '%s' in %s\n", handler, objfile);
|
||||
goto close;
|
||||
}
|
||||
link = bpf_map__attach_struct_ops(map);
|
||||
if (!link || libbpf_get_error(link)) {
|
||||
fprintf(stderr, "attach struct_ops '%s' failed\n", handler);
|
||||
goto close;
|
||||
}
|
||||
if (register_entry(entry, handler)) {
|
||||
fprintf(stderr, "register 'B' entry '%s' failed\n", entry);
|
||||
goto detach;
|
||||
}
|
||||
ret = check_output(target, expect);
|
||||
unregister_entry(entry);
|
||||
detach:
|
||||
bpf_link__destroy(link);
|
||||
close:
|
||||
bpf_object__close(obj);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char src[600], obj[600], appdst[600], interpdst[600];
|
||||
char exe[512];
|
||||
ssize_t n;
|
||||
int fail = 0;
|
||||
struct stat st;
|
||||
struct btf *btf;
|
||||
|
||||
if (getuid() != 0) {
|
||||
fprintf(stderr, "Skipping: test must be run as root\n");
|
||||
return 4; /* KSFT_SKIP */
|
||||
}
|
||||
|
||||
/* 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 */
|
||||
}
|
||||
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);
|
||||
|
||||
/* 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;
|
||||
}
|
||||
61
tools/testing/selftests/exec/bpf_interp.bpf.c
Normal file
61
tools/testing/selftests/exec/bpf_interp.bpf.c
Normal file
@@ -0,0 +1,61 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* binfmt_misc_ops handler for the selftest's fixed-interpreter case: match a
|
||||
* 64-bit aarch64 ELF header from the prefetched buffer and route it to a fixed
|
||||
* interpreter chosen by the program. This is the portable, self-contained
|
||||
* equivalent of routing a foreign binary to an emulator: it matches
|
||||
* programmatically and computes the interpreter, but points at a test binary
|
||||
* the harness installs rather than a system emulator.
|
||||
*/
|
||||
#include "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_tracing.h>
|
||||
|
||||
char _license[] SEC("license") = "GPL";
|
||||
|
||||
#define EI_CLASS 4
|
||||
#define ELFCLASS64 2
|
||||
#define EM_AARCH64 183
|
||||
|
||||
extern int bpf_binprm_set_interp(struct linux_binprm *bprm, const char *path,
|
||||
size_t path__sz) __ksym;
|
||||
|
||||
/*
|
||||
* A magic-style decision needs nothing beyond the prefetched bprm->buf,
|
||||
* even though the match program could read the file.
|
||||
*/
|
||||
SEC("struct_ops.s/match")
|
||||
bool BPF_PROG(bpf_interp_match, struct linux_binprm *bprm)
|
||||
{
|
||||
__u16 machine;
|
||||
|
||||
if (bprm->buf[0] != 0x7f || bprm->buf[1] != 'E' ||
|
||||
bprm->buf[2] != 'L' || bprm->buf[3] != 'F' ||
|
||||
bprm->buf[EI_CLASS] != ELFCLASS64)
|
||||
return false;
|
||||
|
||||
/* e_machine is a 16-bit little-endian field at offset 18. */
|
||||
machine = (__u8)bprm->buf[18] | ((__u16)(__u8)bprm->buf[19] << 8);
|
||||
return machine == EM_AARCH64;
|
||||
}
|
||||
|
||||
SEC("struct_ops.s/load")
|
||||
int BPF_PROG(bpf_interp_load, struct linux_binprm *bprm)
|
||||
{
|
||||
/*
|
||||
* Keep the path on the (writable) stack: bpf_binprm_set_interp() takes
|
||||
* a sized memory arg and the verifier rejects a read-only .rodata
|
||||
* buffer for it. The harness installs the interpreter at this path.
|
||||
*/
|
||||
char interp[] = "/tmp/binfmt_bpf_interp";
|
||||
|
||||
/* @path__sz includes the terminating NUL; 0 commits the selection. */
|
||||
return bpf_binprm_set_interp(bprm, interp, sizeof(interp));
|
||||
}
|
||||
|
||||
SEC(".struct_ops.link")
|
||||
struct binfmt_misc_ops bpf_interp = {
|
||||
.match = (void *)bpf_interp_match,
|
||||
.load = (void *)bpf_interp_load,
|
||||
.name = "bpf_interp",
|
||||
};
|
||||
224
tools/testing/selftests/exec/nix_origin.bpf.c
Normal file
224
tools/testing/selftests/exec/nix_origin.bpf.c
Normal file
@@ -0,0 +1,224 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* nix_origin.bpf.c - $ORIGIN-relative PT_INTERP resolution
|
||||
*
|
||||
* A binfmt_misc_ops handler that makes relocatable (Nix-style) ELF
|
||||
* binaries work: if PT_INTERP starts with "$ORIGIN/", the loader is
|
||||
* resolved relative to the directory of the binary being executed and
|
||||
* selected via bpf_binprm_set_interp(). The match program reads the
|
||||
* program headers itself, so anything else never commits to this
|
||||
* handler and passes through untouched.
|
||||
*
|
||||
* Activate with:
|
||||
* bpftool struct_ops register nix_origin.bpf.o /sys/fs/bpf
|
||||
* echo ':nix-origin:B::::nix_origin:' > /proc/sys/fs/binfmt_misc/register
|
||||
*/
|
||||
#include "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_tracing.h>
|
||||
|
||||
char _license[] SEC("license") = "GPL";
|
||||
|
||||
#define PATH_MAX 4096
|
||||
#define EI_CLASS 4
|
||||
#define ELFCLASSXX 2 /* ELFCLASS64; flip to 1 for 32-bit */
|
||||
#define PT_INTERP 3
|
||||
#define MAX_PHDRS 64
|
||||
|
||||
#define ORIGIN "$ORIGIN"
|
||||
#define ORIGIN_LEN (sizeof(ORIGIN) - 1)
|
||||
|
||||
#define ENOENT 2
|
||||
#define ENOEXEC 8
|
||||
#define ENAMETOOLONG 36
|
||||
|
||||
extern int bpf_dynptr_from_file(struct file *file, __u32 flags,
|
||||
struct bpf_dynptr *ptr__uninit) __ksym;
|
||||
extern int bpf_dynptr_file_discard(struct bpf_dynptr *dynptr) __ksym;
|
||||
extern int bpf_path_d_path(const struct path *path, char *buf,
|
||||
size_t buf__sz) __ksym;
|
||||
extern int bpf_binprm_set_interp(struct linux_binprm *bprm, const char *path,
|
||||
size_t path__sz) __ksym;
|
||||
|
||||
struct scratch {
|
||||
char interp[PATH_MAX]; /* PT_INTERP as embedded in the binary */
|
||||
char path[PATH_MAX]; /* d_path of the binary, becomes the result */
|
||||
};
|
||||
|
||||
/* Keyed by pid: execs run concurrently and the programs can sleep. */
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_HASH);
|
||||
__uint(max_entries, 512);
|
||||
__type(key, __u64);
|
||||
__type(value, struct scratch);
|
||||
} scratch_map SEC(".maps");
|
||||
|
||||
static const struct scratch zero_scratch;
|
||||
|
||||
/* An ELF64 binary per the prefetched header? */
|
||||
static bool is_elf64(struct linux_binprm *bprm)
|
||||
{
|
||||
return bprm->buf[0] == 0x7f && bprm->buf[1] == 'E' &&
|
||||
bprm->buf[2] == 'L' && bprm->buf[3] == 'F' &&
|
||||
bprm->buf[EI_CLASS] == ELFCLASSXX;
|
||||
}
|
||||
|
||||
/* Locate PT_INTERP; false if the file has none or looks malformed. */
|
||||
static bool find_pt_interp(struct bpf_dynptr *dp, struct elf64_phdr *phdr)
|
||||
{
|
||||
struct elf64_hdr ehdr;
|
||||
bool found = false;
|
||||
int i;
|
||||
|
||||
if (bpf_dynptr_read(&ehdr, sizeof(ehdr), dp, 0, 0))
|
||||
return false;
|
||||
if (ehdr.e_phentsize != sizeof(struct elf64_phdr))
|
||||
return false;
|
||||
|
||||
bpf_for(i, 0, ehdr.e_phnum) {
|
||||
if (i >= MAX_PHDRS)
|
||||
break;
|
||||
if (bpf_dynptr_read(phdr, sizeof(*phdr), dp,
|
||||
ehdr.e_phoff + i * sizeof(*phdr), 0))
|
||||
return false;
|
||||
if (phdr->p_type == PT_INTERP) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
/*
|
||||
* An ELF64 binary whose PT_INTERP starts with "$ORIGIN/" is ours. The
|
||||
* match can sleep and read the file, so the decision is made here and
|
||||
* regular binaries never commit to this handler: later binfmt_misc
|
||||
* entries and binfmt_elf see them as if we did not exist.
|
||||
*/
|
||||
SEC("struct_ops.s/match")
|
||||
bool BPF_PROG(nix_origin_match, struct linux_binprm *bprm)
|
||||
{
|
||||
char prefix[ORIGIN_LEN + 1] = {};
|
||||
struct elf64_phdr phdr;
|
||||
struct bpf_dynptr dp;
|
||||
bool ours = false;
|
||||
|
||||
if (!is_elf64(bprm))
|
||||
return false;
|
||||
|
||||
/* The dynptr must be discarded on every path once requested. */
|
||||
if (bpf_dynptr_from_file(bprm->file, 0, &dp))
|
||||
goto out;
|
||||
if (find_pt_interp(&dp, &phdr) &&
|
||||
phdr.p_filesz > ORIGIN_LEN + 1 &&
|
||||
!bpf_dynptr_read(prefix, sizeof(prefix), &dp, phdr.p_offset, 0))
|
||||
ours = !bpf_strncmp(prefix, sizeof(prefix), ORIGIN "/");
|
||||
out:
|
||||
bpf_dynptr_file_discard(&dp);
|
||||
return ours;
|
||||
}
|
||||
|
||||
/*
|
||||
* The match is committed and already vetted the "$ORIGIN/" prefix, so
|
||||
* everything here reads the file again from scratch: -ENOEXEC only
|
||||
* covers a binary that changed under us and stopped being ours.
|
||||
*/
|
||||
SEC("struct_ops.s/load")
|
||||
int BPF_PROG(nix_origin_load, struct linux_binprm *bprm)
|
||||
{
|
||||
__u32 isz, sfx, rsz, slash;
|
||||
struct elf64_phdr phdr;
|
||||
struct bpf_dynptr dp;
|
||||
struct scratch *sc;
|
||||
__u64 id;
|
||||
int ret = -ENOEXEC, len, i;
|
||||
|
||||
if (bpf_dynptr_from_file(bprm->file, 0, &dp))
|
||||
goto out;
|
||||
|
||||
if (!find_pt_interp(&dp, &phdr))
|
||||
goto out;
|
||||
|
||||
isz = phdr.p_filesz;
|
||||
if (isz <= ORIGIN_LEN + 1 || isz >= sizeof(sc->interp))
|
||||
goto out;
|
||||
/*
|
||||
* The range check above compiles to a test on a zero-extended copy of
|
||||
* the u64 p_filesz, so the verifier does not carry the bound to the
|
||||
* dynptr_read() length below ("unbounded memory access"). Mask isz to
|
||||
* the buffer size (a power of two) and force the masked value to be
|
||||
* materialized with a barrier so the read uses the bounded register.
|
||||
*/
|
||||
isz &= sizeof(sc->interp) - 1;
|
||||
barrier_var(isz);
|
||||
|
||||
id = bpf_get_current_pid_tgid();
|
||||
if (bpf_map_update_elem(&scratch_map, &id, &zero_scratch, BPF_ANY))
|
||||
goto out;
|
||||
sc = bpf_map_lookup_elem(&scratch_map, &id);
|
||||
if (!sc)
|
||||
goto out_del;
|
||||
|
||||
if (bpf_dynptr_read(sc->interp, isz, &dp, phdr.p_offset, 0))
|
||||
goto out_del;
|
||||
if (sc->interp[isz - 1] != '\0')
|
||||
goto out_del;
|
||||
|
||||
/* Not "$ORIGIN/..." anymore? Then it is not ours anymore either. */
|
||||
if (sc->interp[0] != '$' || sc->interp[1] != 'O' ||
|
||||
sc->interp[2] != 'R' || sc->interp[3] != 'I' ||
|
||||
sc->interp[4] != 'G' || sc->interp[5] != 'I' ||
|
||||
sc->interp[6] != 'N' || sc->interp[7] != '/')
|
||||
goto out_del;
|
||||
|
||||
/*
|
||||
* From here on resolution failures fail the exec instead of falling
|
||||
* back to binfmt_elf, which would resolve the literal "$ORIGIN/..."
|
||||
* relative to the caller's cwd.
|
||||
*/
|
||||
ret = -ENOENT;
|
||||
len = bpf_path_d_path(&bprm->file->f_path, sc->path, sizeof(sc->path));
|
||||
if (len <= 0 || len > sizeof(sc->path))
|
||||
goto out_del;
|
||||
/* Unreachable or unlinked ("... (deleted)") binaries can't resolve. */
|
||||
if (sc->path[0] != '/')
|
||||
goto out_del;
|
||||
|
||||
/* $ORIGIN = dirname of the binary. */
|
||||
slash = 0;
|
||||
bpf_for(i, 1, len - 1) {
|
||||
if (i >= sizeof(sc->path))
|
||||
break;
|
||||
if (sc->path[i] == '/')
|
||||
slash = i;
|
||||
}
|
||||
|
||||
/* Splice the suffix (leading '/' and NUL included) onto the dir. */
|
||||
sfx = isz - ORIGIN_LEN;
|
||||
rsz = slash + sfx;
|
||||
if (rsz > sizeof(sc->path)) {
|
||||
ret = -ENAMETOOLONG;
|
||||
goto out_del;
|
||||
}
|
||||
bpf_for(i, 0, sfx) {
|
||||
__u32 s = ORIGIN_LEN + i, d = slash + i;
|
||||
|
||||
if (s >= sizeof(sc->interp) || d >= sizeof(sc->path))
|
||||
break;
|
||||
sc->path[d] = sc->interp[s];
|
||||
}
|
||||
|
||||
ret = bpf_binprm_set_interp(bprm, sc->path, rsz);
|
||||
out_del:
|
||||
bpf_map_delete_elem(&scratch_map, &id);
|
||||
out:
|
||||
bpf_dynptr_file_discard(&dp);
|
||||
return ret;
|
||||
}
|
||||
|
||||
SEC(".struct_ops.link")
|
||||
struct binfmt_misc_ops nix_origin = {
|
||||
.match = (void *)nix_origin_match,
|
||||
.load = (void *)nix_origin_load,
|
||||
.name = "nix_origin",
|
||||
};
|
||||
Reference in New Issue
Block a user