summaryrefslogtreecommitdiffstats
path: root/tools/testing/selftests/exec
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2026-10-02 12:17:24 -0700
committerLinus Torvalds <torvalds@linux-foundation.org>2026-10-02 12:17:24 -0700
commit3f1fe48a36b0b6722dc3fd421d93512bac138e9a (patch)
treeb767d7f6e26bc64334d3f14f8422d187239dc45d /tools/testing/selftests/exec
downloadlinux-stable-3f1fe48a36b0b6722dc3fd421d93512bac138e9a.tar.gz
linux-stable-3f1fe48a36b0b6722dc3fd421d93512bac138e9a.zip
Merge tag 'io_uring-7.3-20261002' of git://git.kernel.org/pub/scm/linux/kernel/git/axboe/linuxgrafted
Pull io_uring fixes from Jens Axboe: - Fix a task_work add use-after-free with SQPOLL. The sqpoll thread could pop and complete the last request while io_req_normal_work_add() was still looking at them after the mpscq push. Use the same approach as DEFER_TASKRUN to protect from that, holding an RCU read lock across the add, and have exit wait for an RCU grace period for SQPOLL rings as well. - CQE32 ring fixes: correct the free entry check for 32b CQEs, zero the big_cqe for aux CQEs, and only post the dummy skip CQE on CQE_MIXED rings - Mark the source filter table as COW when cloning bpf filters, so registering another filter on the source doesn't modify the shared table in place - Initialize the task context before running the BPF loop - Requeue zcrx multishot receives stopped by a local resource - End a TX_TIMESTAMP multishot cmd when the CQ is full (lollipopkit) * tag 'io_uring-7.3-20261002' of git://git.kernel.org/pub/scm/linux/kernel/git/axboe/linux: io_uring: fix task_work add use-after-free with SQPOLL io_uring/cmd_net: end TX_TIMESTAMP multishot when the CQ is full io_uring/zcrx: requeue multishot receives stopped by a local resource io_uring: initialize task context before running the BPF loop io_uring: zero big_cqe for aux CQEs on CQE32 rings io_uring: fix free entry check for 32b CQEs on CQE32 rings io_uring: only post the dummy skip CQE on CQE_MIXED rings io_uring/bpf_filter: mark source as COW when cloning filters
Diffstat (limited to 'tools/testing/selftests/exec')
-rw-r--r--tools/testing/selftests/exec/.gitignore32
-rw-r--r--tools/testing/selftests/exec/Makefile150
-rw-r--r--tools/testing/selftests/exec/binfmt_bind_interp.c14
-rw-r--r--tools/testing/selftests/exec/binfmt_bpf_app.c12
-rw-r--r--tools/testing/selftests/exec/binfmt_bpf_interp.c15
-rw-r--r--tools/testing/selftests/exec/binfmt_loader_payload.c146
-rw-r--r--tools/testing/selftests/exec/binfmt_misc_bpf.c638
-rw-r--r--tools/testing/selftests/exec/binfmt_misc_common.h315
-rw-r--r--tools/testing/selftests/exec/binfmt_misc_disabled.c172
-rw-r--r--tools/testing/selftests/exec/binfmt_misc_interplimit.c232
-rw-r--r--tools/testing/selftests/exec/binfmt_misc_loader.c372
-rw-r--r--tools/testing/selftests/exec/binfmt_misc_selfpin.c158
-rw-r--r--tools/testing/selftests/exec/binfmt_misc_transparent.c95
-rwxr-xr-xtools/testing/selftests/exec/binfmt_script.py179
-rw-r--r--tools/testing/selftests/exec/binfmt_transparent_interp.c112
-rw-r--r--tools/testing/selftests/exec/bpf_interp.bpf.c61
-rwxr-xr-xtools/testing/selftests/exec/check-exec-tests.sh205
-rw-r--r--tools/testing/selftests/exec/check-exec.c463
-rw-r--r--tools/testing/selftests/exec/config12
-rw-r--r--tools/testing/selftests/exec/execveat.c520
-rw-r--r--tools/testing/selftests/exec/false.c5
-rw-r--r--tools/testing/selftests/exec/interp_bind.bpf.c76
-rw-r--r--tools/testing/selftests/exec/load_address.c105
-rw-r--r--tools/testing/selftests/exec/loader.bpf.c56
-rw-r--r--tools/testing/selftests/exec/nix_origin.bpf.c224
-rw-r--r--tools/testing/selftests/exec/non-regular.c196
-rw-r--r--tools/testing/selftests/exec/null-argv.c78
-rw-r--r--tools/testing/selftests/exec/recursion-depth.c66
-rw-r--r--tools/testing/selftests/exec/transparent.bpf.c57
29 files changed, 4766 insertions, 0 deletions
diff --git a/tools/testing/selftests/exec/.gitignore b/tools/testing/selftests/exec/.gitignore
new file mode 100644
index 000000000..e42ecd4c9
--- /dev/null
+++ b/tools/testing/selftests/exec/.gitignore
@@ -0,0 +1,32 @@
+# SPDX-License-Identifier: GPL-2.0-only
+subdir*
+script*
+execveat
+execveat.symlink
+execveat.moved
+execveat.path.ephemeral
+execveat.ephemeral
+execveat.denatured
+non-regular
+null-argv
+/check-exec
+/false
+/inc
+/load_address.*
+!load_address.c
+/recursion-depth
+/set-exec
+xxxxxxxx*
+pipe
+S_I*.test
+binfmt_misc_bpf
+binfmt_misc_interplimit
+binfmt_bpf_interp
+binfmt_bpf_app
+binfmt_misc_transparent
+binfmt_transparent_interp
+binfmt_misc_loader
+binfmt_loader_payload
+binfmt_loader_payload_static
+*.bpf.o
+vmlinux.h
diff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile
new file mode 100644
index 000000000..b640af8f0
--- /dev/null
+++ b/tools/testing/selftests/exec/Makefile
@@ -0,0 +1,150 @@
+# SPDX-License-Identifier: GPL-2.0
+CFLAGS = -Wall
+CFLAGS += -Wno-nonnull
+CFLAGS += $(KHDR_INCLUDES)
+
+LDLIBS += -lcap
+
+ALIGNS := 0x1000 0x200000 0x1000000
+ALIGN_PIES := $(patsubst %,load_address.%,$(ALIGNS))
+ALIGN_STATIC_PIES := $(patsubst %,load_address.static.%,$(ALIGNS))
+ALIGNMENT_TESTS := $(ALIGN_PIES) $(ALIGN_STATIC_PIES)
+
+TEST_PROGS := binfmt_script.py check-exec-tests.sh
+TEST_GEN_PROGS := execveat non-regular $(ALIGNMENT_TESTS)
+TEST_GEN_PROGS_EXTENDED := false inc set-exec script-exec.inc script-noexec.inc
+TEST_GEN_FILES := execveat.symlink execveat.denatured script subdir
+# Makefile is a run-time dependency, since it's accessed by the execveat test
+TEST_FILES := Makefile
+
+TEST_GEN_PROGS += recursion-depth
+TEST_GEN_PROGS += null-argv
+TEST_GEN_PROGS += check-exec
+
+# binfmt_misc must not be reachable as an exec source or as a stacking layer,
+# or an 'F' entry can pin the instance that owns it. Unprivileged, no bpf.
+TEST_GEN_PROGS += binfmt_misc_selfpin
+
+# The interpreters an 'F' or 'B' entry pre-opens are charged against
+# UCOUNT_BINFMT_MISC_INTERPRETERS. Unprivileged, no bpf.
+TEST_GEN_PROGS += binfmt_misc_interplimit
+
+# 'D' (register disabled) binfmt_misc test: an entry that exists but does
+# not dispatch until it is enabled. Static magic entry, no bpf toolchain.
+TEST_GEN_PROGS += binfmt_misc_disabled
+
+# Static ('T' flag) transparent binfmt_misc test; the asserting interpreter
+# is shared with the bpf harness's transparent case. No bpf toolchain needed.
+TEST_GEN_PROGS += binfmt_misc_transparent
+TEST_GEN_FILES += binfmt_transparent_interp
+
+# 'L' (loader substitution) binfmt_misc test: the payload runs as the main
+# image with a copy of the system loader substituted for its PT_INTERP and
+# asserts the native identity from inside; the static build proves the
+# override is dropped for a binary without PT_INTERP.
+TEST_GEN_PROGS += binfmt_misc_loader
+TEST_GEN_FILES += binfmt_loader_payload binfmt_loader_payload_static
+
+# binfmt_misc bpf-backed ('B') handler test: a libbpf harness plus its
+# struct_ops objects and the test interpreter/app it routes between. Only
+# built when clang, bpftool, the vmlinux BTF and libbpf are all present
+# (HAVE_BPF_TOOLCHAIN=y forces it) so the other exec selftests don't grow
+# a bpf toolchain dependency.
+CLANG ?= clang
+BPFTOOL ?= bpftool
+VMLINUX_BTF ?= /sys/kernel/btf/vmlinux
+HAVE_BPF_TOOLCHAIN ?= $(shell command -v $(CLANG) >/dev/null 2>&1 && \
+ command -v $(BPFTOOL) >/dev/null 2>&1 && \
+ test -r $(VMLINUX_BTF) && \
+ pkg-config --exists libbpf 2>/dev/null && echo y)
+ifeq ($(HAVE_BPF_TOOLCHAIN),y)
+TEST_GEN_PROGS += binfmt_misc_bpf
+TEST_GEN_FILES += bpf_interp.bpf.o nix_origin.bpf.o transparent.bpf.o
+TEST_GEN_FILES += loader.bpf.o interp_bind.bpf.o
+TEST_GEN_FILES += binfmt_bpf_interp binfmt_bpf_app binfmt_bind_interp
+else
+$(info exec selftests: skipping binfmt_misc_bpf, needs clang, bpftool, vmlinux BTF and libbpf)
+endif
+
+EXTRA_CLEAN := $(OUTPUT)/subdir.moved $(OUTPUT)/execveat.moved $(OUTPUT)/xxxxx* \
+ $(OUTPUT)/S_I*.test
+
+LOCAL_HDRS += binfmt_misc_common.h
+
+include ../lib.mk
+
+CHECK_EXEC_SAMPLES := $(top_srcdir)/samples/check-exec
+
+$(OUTPUT)/subdir:
+ mkdir -p $@
+$(OUTPUT)/script: Makefile
+ echo '#!/bin/bash' > $@
+ echo 'exit $$*' >> $@
+ chmod +x $@
+$(OUTPUT)/execveat.symlink: $(OUTPUT)/execveat
+ cd $(OUTPUT) && ln -s -f $(shell basename $<) $(shell basename $@)
+$(OUTPUT)/execveat.denatured: $(OUTPUT)/execveat
+ cp $< $@
+ chmod -x $@
+$(OUTPUT)/load_address.0x%: load_address.c
+ $(CC) $(CFLAGS) $(LDFLAGS) -Wl,-z,max-page-size=$(lastword $(subst ., ,$@)) \
+ -fPIE -pie $< -o $@
+$(OUTPUT)/load_address.static.0x%: load_address.c
+ $(CC) $(CFLAGS) $(LDFLAGS) -Wl,-z,max-page-size=$(lastword $(subst ., ,$@)) \
+ -fPIE -static-pie $< -o $@
+$(OUTPUT)/false: false.c
+ $(CC) $(CFLAGS) $(LDFLAGS) -static $< -o $@
+$(OUTPUT)/inc: $(CHECK_EXEC_SAMPLES)/inc.c
+ $(CC) $(CFLAGS) $(LDFLAGS) $< -o $@
+$(OUTPUT)/set-exec: $(CHECK_EXEC_SAMPLES)/set-exec.c
+ $(CC) $(CFLAGS) $(LDFLAGS) $< -o $@
+$(OUTPUT)/script-exec.inc: $(CHECK_EXEC_SAMPLES)/script-exec.inc
+ cp $< $@
+$(OUTPUT)/script-noexec.inc: $(CHECK_EXEC_SAMPLES)/script-noexec.inc
+ cp $< $@
+
+# Reuses setup_userns()/write_file() from the filesystems selftests. Their
+# wrappers.h wants the uapi headers, so ask for them here rather than widening
+# CFLAGS for every program in this directory.
+$(OUTPUT)/binfmt_misc_selfpin: CFLAGS += $(TOOLS_INCLUDES)
+$(OUTPUT)/binfmt_misc_selfpin: ../filesystems/utils.c
+$(OUTPUT)/binfmt_misc_interplimit: CFLAGS += $(TOOLS_INCLUDES)
+$(OUTPUT)/binfmt_misc_interplimit: ../filesystems/utils.c
+
+# --- binfmt_misc bpf ('B') handler test ---------------------------------
+# The struct_ops bpf objects are compiled against the running kernel's BTF.
+# CLANG/BPFTOOL/VMLINUX_BTF are set above next to the toolchain check;
+# override LIBBPF_CFLAGS/LDLIBS to point at a libbpf install.
+BPF_CFLAGS ?= -I$(OUTPUT)
+LIBBPF_CFLAGS ?=
+LIBBPF_LDLIBS ?= -lbpf -lelf -lz
+
+$(OUTPUT)/vmlinux.h:
+ $(BPFTOOL) btf dump file $(VMLINUX_BTF) format c > $@
+
+# BPF_NO_KFUNC_PROTOTYPES: the programs declare the kfuncs they use themselves.
+$(OUTPUT)/%.bpf.o: %.bpf.c $(OUTPUT)/vmlinux.h
+ $(CLANG) -g -O2 -target bpf -mcpu=v3 -DBPF_NO_KFUNC_PROTOTYPES \
+ $(BPF_CFLAGS) $(LIBBPF_CFLAGS) -c $< -o $@
+
+$(OUTPUT)/binfmt_misc_bpf: binfmt_misc_bpf.c binfmt_misc_common.h
+ $(CC) $(CFLAGS) $(LIBBPF_CFLAGS) $(LDFLAGS) $< $(LIBBPF_LDLIBS) -o $@
+
+$(OUTPUT)/binfmt_bpf_interp: binfmt_bpf_interp.c
+ $(CC) $(CFLAGS) $(LDFLAGS) $< -o $@
+
+$(OUTPUT)/binfmt_bind_interp: binfmt_bind_interp.c
+ $(CC) $(CFLAGS) $(LDFLAGS) $< -o $@
+
+$(OUTPUT)/binfmt_loader_payload: binfmt_loader_payload.c binfmt_misc_common.h
+ $(CC) $(CFLAGS) $(LDFLAGS) -fPIE -pie $< -o $@
+
+$(OUTPUT)/binfmt_loader_payload_static: binfmt_loader_payload.c binfmt_misc_common.h
+ $(CC) $(CFLAGS) $(LDFLAGS) -static $< -o $@
+
+# PT_INTERP is set to the literal "$ORIGIN/binfmt_bpf_interp"; the nix_origin
+# handler resolves it relative to the binary at run time.
+$(OUTPUT)/binfmt_bpf_app: binfmt_bpf_app.c
+ $(CC) $(CFLAGS) $(LDFLAGS) -Wl,--dynamic-linker,'$$ORIGIN/binfmt_bpf_interp' $< -o $@
+
+EXTRA_CLEAN += $(OUTPUT)/vmlinux.h $(OUTPUT)/*.bpf.o
diff --git a/tools/testing/selftests/exec/binfmt_bind_interp.c b/tools/testing/selftests/exec/binfmt_bind_interp.c
new file mode 100644
index 000000000..06d650628
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_bind_interp.c
@@ -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;
+}
diff --git a/tools/testing/selftests/exec/binfmt_bpf_app.c b/tools/testing/selftests/exec/binfmt_bpf_app.c
new file mode 100644
index 000000000..472270f14
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_bpf_app.c
@@ -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;
+}
diff --git a/tools/testing/selftests/exec/binfmt_bpf_interp.c b/tools/testing/selftests/exec/binfmt_bpf_interp.c
new file mode 100644
index 000000000..2db205f09
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_bpf_interp.c
@@ -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;
+}
diff --git a/tools/testing/selftests/exec/binfmt_loader_payload.c b/tools/testing/selftests/exec/binfmt_loader_payload.c
new file mode 100644
index 000000000..272db8efb
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_loader_payload.c
@@ -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;
+}
diff --git a/tools/testing/selftests/exec/binfmt_misc_bpf.c b/tools/testing/selftests/exec/binfmt_misc_bpf.c
new file mode 100644
index 000000000..b2a451890
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_misc_bpf.c
@@ -0,0 +1,638 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Selftest for binfmt_misc bpf-backed ('B') handlers.
+ *
+ * A handler is a struct binfmt_misc_ops struct_ops map with a sleepable match
+ * and a sleepable load program. Attaching it publishes it by name in the
+ * caller's user namespace; a 'B' entry referencing it by name in the
+ * interpreter field activates it:
+ *
+ * echo ':name:B::::<handler>:' > /proc/sys/fs/binfmt_misc/register
+ *
+ * Five self-contained cases are exercised:
+ *
+ * 1. bpf_interp: the match program matches a synthetic aarch64 ELF header
+ * from the prefetched bprm->buf and the load program routes it to a
+ * fixed interpreter of its choosing.
+ * 2. nix_origin: the match program reads the binary's program headers to
+ * commit only to a "$ORIGIN/..."-relative PT_INTERP and the load program
+ * resolves it to an interpreter co-located with the binary (the
+ * relocatable-loader case the kernel ELF loader cannot express).
+ * 3. transparent: the load program sets BPF_BINPRM_TRANSPARENT; the
+ * asserting interpreter (binfmt_transparent_interp) verifies the
+ * identity the kernel constructed (exe link, argv, cmdline, comm,
+ * AT_EXECFD, write denial) from inside the process.
+ * 4. loader: the load program sets BPF_BINPRM_LOADER; the payload
+ * (binfmt_loader_payload) runs as the main image with the selected
+ * interpreter substituted for its PT_INTERP and asserts the native
+ * identity from inside.
+ * 5. interp_bind: an entry registered disabled with 'D' is given its
+ * interpreters one write at a time, and the load program picks one by
+ * name per exec. Replacing what the path holds afterwards changes
+ * nothing, which is the point of binding a file rather than resolving
+ * a name at exec time. Enabling the entry seals it.
+ *
+ * The first two route to a test interpreter that prints BPF_INTERP_RAN,
+ * proving the program's chosen interpreter actually ran.
+ */
+#define _GNU_SOURCE
+#include <elf.h>
+#include <limits.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#include <bpf/btf.h>
+#include <bpf/libbpf.h>
+
+#include "binfmt_misc_common.h"
+#include "kselftest_harness.h"
+
+#define INTERP_PATH "/tmp/binfmt_bpf_interp"
+#define AARCH64_PATH "/tmp/binfmt_bpf_aarch64"
+#define RELOC_TEMPLATE "/tmp/binfmt_relocXXXXXX"
+#define TRANS_INTERP "/tmp/binfmt_transparent_interp"
+#define TRANS_PATH "/tmp/binfmt_bpf_riscv"
+#define EXPECT "BPF_INTERP_RAN"
+#define TRANS_EXPECT "TRANSPARENT_OK"
+#define LOADER_INTERP "/tmp/binfmt_loader_interp"
+#define LOADER_PATH "/tmp/binfmt_bpf_loader.ldrtest"
+#define BIND_FIRST "/tmp/binfmt_bind_first"
+#define BIND_SECOND "/tmp/binfmt_bind_second"
+#define BIND_ARM_PATH "/tmp/binfmt_bind_arm"
+#define BIND_RISCV_PATH "/tmp/binfmt_bind_riscv"
+#define BIND_EXPECT "BIND_RAN "
+#define BIND_MAX 100
+#define INTERP_LIMIT "/proc/sys/user/max_binfmt_misc_interpreters"
+/* Exit status of the binding child when it cannot set up a budget of its own. */
+#define BIND_NO_BUDGET 200
+
+/* A minimal 64-bit little-endian ELF header, padded to the read size. */
+static int create_fake_elf(const char *path, unsigned short machine)
+{
+ unsigned char hdr[256] = {0};
+ int fd;
+
+ hdr[0] = 0x7f; hdr[1] = 'E'; hdr[2] = 'L'; hdr[3] = 'F';
+ hdr[4] = ELFCLASS64;
+ hdr[5] = ELFDATA2LSB;
+ hdr[6] = EV_CURRENT;
+ hdr[16] = ET_EXEC;
+ hdr[18] = machine & 0xff; /* e_machine, little-endian */
+ hdr[19] = machine >> 8;
+ hdr[20] = EV_CURRENT;
+
+ unlink(path);
+ fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0755);
+ if (fd < 0)
+ return -1;
+ if (write(fd, hdr, sizeof(hdr)) != (ssize_t)sizeof(hdr)) {
+ close(fd);
+ return -1;
+ }
+ close(fd);
+ return 0;
+}
+
+/*
+ * Register a 'B' entry for @handler. With @flags "D" the entry is created
+ * disabled, which is what leaves it open to being given interpreters.
+ */
+static int register_entry(const char *name, const char *handler,
+ const char *flags)
+{
+ char rule[PATH_MAX];
+
+ snprintf(rule, sizeof(rule), ":%s:B::::%s:%s", name, handler,
+ flags ? flags : "");
+ return write_reg(rule);
+}
+
+static int check_output(const char *cmd, const char *expected)
+{
+ char buf[128];
+ FILE *fp;
+
+ fp = popen(cmd, "r");
+ if (!fp)
+ return -1;
+ if (!fgets(buf, sizeof(buf), fp)) {
+ pclose(fp);
+ return -1;
+ }
+ pclose(fp);
+ return strncmp(buf, expected, strlen(expected)) ? -1 : 0;
+}
+
+/* Does the kernel BTF know struct binfmt_misc_ops (CONFIG_BINFMT_MISC_BPF)? */
+static bool have_binfmt_misc_ops(void)
+{
+ struct btf *btf = btf__load_vmlinux_btf();
+ bool have;
+
+ have = btf && btf__find_by_name_kind(btf, "binfmt_misc_ops",
+ BTF_KIND_STRUCT) >= 0;
+ btf__free(btf);
+ return have;
+}
+
+/* The reason bpf handler cases cannot run here, NULL if they can. */
+static const char *bpf_handler_unsupported(void)
+{
+ if (getuid() != 0)
+ return "test must be run as root";
+ if (!have_binfmt_misc_ops())
+ return "no struct binfmt_misc_ops in the kernel BTF (CONFIG_BINFMT_MISC_BPF)";
+ if (!binfmt_misc_available())
+ return "no binfmt_misc";
+ return NULL;
+}
+
+/* An attached handler with its 'B' entry activated. */
+struct bpf_case {
+ struct bpf_object *obj;
+ struct bpf_link *link;
+ const char *entry;
+};
+
+/*
+ * Load @objfile, attach its struct_ops map @handler (which publishes the
+ * handler) and register a 'B' entry named @entry that references it, with
+ * @flags as the entry's register-string flags.
+ */
+static int bpf_case_start_flags(struct bpf_case *c, const char *objfile,
+ const char *handler, const char *entry,
+ const char *flags)
+{
+ struct bpf_map *map;
+
+ c->obj = NULL;
+ c->link = NULL;
+ c->entry = entry;
+
+ c->obj = bpf_object__open_file(objfile, NULL);
+ if (!c->obj || libbpf_get_error(c->obj)) {
+ fprintf(stderr, "open %s failed\n", objfile);
+ c->obj = NULL;
+ return -1;
+ }
+ if (bpf_object__load(c->obj)) {
+ fprintf(stderr, "load %s failed (check dmesg for the verifier log)\n",
+ objfile);
+ goto fail;
+ }
+ map = bpf_object__find_map_by_name(c->obj, handler);
+ if (!map) {
+ fprintf(stderr, "no struct_ops map '%s' in %s\n", handler, objfile);
+ goto fail;
+ }
+ c->link = bpf_map__attach_struct_ops(map);
+ if (!c->link || libbpf_get_error(c->link)) {
+ fprintf(stderr, "attach struct_ops '%s' failed\n", handler);
+ c->link = NULL;
+ goto fail;
+ }
+ if (register_entry(entry, handler, flags)) {
+ fprintf(stderr, "register 'B' entry '%s' failed\n", entry);
+ goto fail;
+ }
+ return 0;
+
+fail:
+ bpf_link__destroy(c->link);
+ bpf_object__close(c->obj);
+ c->obj = NULL;
+ c->link = NULL;
+ return -1;
+}
+
+static int bpf_case_start(struct bpf_case *c, const char *objfile,
+ const char *handler, const char *entry)
+{
+ return bpf_case_start_flags(c, objfile, handler, entry, NULL);
+}
+
+static void bpf_case_stop(struct bpf_case *c)
+{
+ unregister(c->entry);
+ bpf_link__destroy(c->link);
+ bpf_object__close(c->obj);
+}
+
+/* Activate @handler, run @target and check it produced @expect. */
+static int run_case(const char *objfile, const char *handler,
+ const char *entry, const char *target, const char *expect)
+{
+ struct bpf_case c;
+ int ret;
+
+ if (bpf_case_start(&c, objfile, handler, entry))
+ return -1;
+ ret = check_output(target, expect);
+ bpf_case_stop(&c);
+ return ret;
+}
+
+FIXTURE(bpf_handler) {
+ char obj[PATH_MAX]; /* struct_ops object of the case under test */
+};
+
+FIXTURE_SETUP(bpf_handler)
+{
+ char src[PATH_MAX];
+ const char *why = bpf_handler_unsupported();
+
+ if (why)
+ SKIP(return, "%s", why);
+
+ /* Shared test interpreter. */
+ ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_bpf_interp"), 0);
+ ASSERT_EQ(copy_file(src, INTERP_PATH), 0);
+}
+
+FIXTURE_TEARDOWN(bpf_handler)
+{
+ unlink(INTERP_PATH);
+}
+
+/* The match program matches a synthetic header, the load program routes it. */
+TEST_F(bpf_handler, fixed_interpreter)
+{
+ ASSERT_EQ(create_fake_elf(AARCH64_PATH, EM_AARCH64), 0);
+ ASSERT_EQ(artifact_path(self->obj, sizeof(self->obj),
+ "bpf_interp.bpf.o"), 0);
+ EXPECT_EQ(run_case(self->obj, "bpf_interp", "test_bpf_interp",
+ AARCH64_PATH, EXPECT), 0);
+ unlink(AARCH64_PATH);
+}
+
+/* A "$ORIGIN/..." PT_INTERP resolved to an interpreter next to the binary. */
+TEST_F(bpf_handler, origin_relative_interpreter)
+{
+ char src[PATH_MAX], app[PATH_MAX], interp[PATH_MAX];
+ char dir[] = RELOC_TEMPLATE;
+
+ ASSERT_NE(mkdtemp(dir), NULL);
+ snprintf(app, sizeof(app), "%s/app", dir);
+ snprintf(interp, sizeof(interp), "%s/binfmt_bpf_interp", dir);
+ ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_bpf_app"), 0);
+ ASSERT_EQ(copy_file(src, app), 0);
+ ASSERT_EQ(copy_file(INTERP_PATH, interp), 0);
+
+ ASSERT_EQ(artifact_path(self->obj, sizeof(self->obj),
+ "nix_origin.bpf.o"), 0);
+ EXPECT_EQ(run_case(self->obj, "nix_origin", "test_bpf_origin",
+ app, EXPECT), 0);
+
+ unlink(app);
+ unlink(interp);
+ rmdir(dir);
+}
+
+/* A transparent dispatch: the process presents as the binary, not the interp. */
+TEST_F(bpf_handler, transparent_dispatch)
+{
+ char src[PATH_MAX], cmd[PATH_MAX + 16];
+
+ /* Probe for transparent-mode support via its static counterpart. */
+ if (!binfmt_flag_supported('T'))
+ SKIP(return, "kernel without transparent mode");
+
+ ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_transparent_interp"), 0);
+ ASSERT_EQ(copy_file(src, TRANS_INTERP), 0);
+ ASSERT_EQ(create_fake_elf(TRANS_PATH, EM_RISCV), 0);
+
+ setenv("BINFMT_TEST_BINARY", TRANS_PATH, 1);
+ snprintf(cmd, sizeof(cmd), "%s argone argtwo", TRANS_PATH);
+ ASSERT_EQ(artifact_path(self->obj, sizeof(self->obj),
+ "transparent.bpf.o"), 0);
+ EXPECT_EQ(run_case(self->obj, "transparent", "test_bpf_transparent",
+ cmd, TRANS_EXPECT), 0);
+
+ unlink(TRANS_PATH);
+ unlink(TRANS_INTERP);
+}
+
+/* A per-exec loader substitution: the payload runs as a native exec. */
+TEST_F(bpf_handler, loader_substitution)
+{
+ char src[PATH_MAX], loader[PATH_MAX];
+ struct bpf_case c;
+ int status;
+
+ if (find_loader(loader, sizeof(loader)))
+ SKIP(return, "cannot determine own PT_INTERP");
+
+ ASSERT_EQ(copy_file(loader, LOADER_INTERP), 0);
+ ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_loader_payload"), 0);
+ ASSERT_EQ(copy_file(src, LOADER_PATH), 0);
+ ASSERT_EQ(patch_file(LOADER_PATH, EI_PAD, LOADER_MARKER,
+ strlen(LOADER_MARKER)), 0);
+ ASSERT_EQ(artifact_path(self->obj, sizeof(self->obj),
+ "loader.bpf.o"), 0);
+
+ setenv("BINFMT_TEST_BINARY", LOADER_PATH, 1);
+ setenv("BINFMT_TEST_INTERP", LOADER_INTERP, 1);
+
+ ASSERT_EQ(bpf_case_start(&c, self->obj, "loader", "test_bpf_loader"), 0);
+ status = run_payload(LOADER_PATH);
+ bpf_case_stop(&c);
+ EXPECT_EQ(status, 0);
+
+ unsetenv("BINFMT_TEST_INTERP");
+ unlink(LOADER_PATH);
+ unlink(LOADER_INTERP);
+}
+
+/* The errno an exec of @path fails with, 0 if it succeeded. */
+static int exec_errno(const char *path)
+{
+ int status;
+ pid_t pid;
+
+ pid = fork();
+ if (pid == 0) {
+ execl(path, path, (char *)NULL);
+ _exit(errno);
+ }
+ if (pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
+ return -1;
+ return WEXITSTATUS(status);
+}
+
+/* Install a copy of the bound-interpreter test binary at @path. */
+static int install_interp(const char *path)
+{
+ char src[PATH_MAX];
+
+ if (artifact_path(src, sizeof(src), "binfmt_bind_interp"))
+ return -1;
+ return copy_file(src, path);
+}
+
+/* Bind @path to @entry under @name, the '+' command of a disabled entry. */
+static int entry_bind(const char *entry, const char *name, const char *path)
+{
+ char cmd[PATH_MAX];
+
+ snprintf(cmd, sizeof(cmd), "+%s %s\n", name, path);
+ return entry_command(entry, cmd);
+}
+
+/* Set the interpreter budget of this namespace. */
+static int write_interp_limit(const char *val)
+{
+ ssize_t n;
+ int fd;
+
+ fd = open(INTERP_LIMIT, O_WRONLY | O_CLOEXEC);
+ if (fd < 0)
+ return -1;
+ n = write(fd, val, strlen(val));
+ close(fd);
+ return n < 0 ? -1 : 0;
+}
+
+/*
+ * The errno a bind is refused with when the writer is a child that has spent
+ * the budget of a user namespace of its own, 0 if it succeeded and -1 if the
+ * child could not set itself up. The fd is opened here and inherited, so the
+ * interpreter is still opened with this process's credentials.
+ */
+static int bind_out_of_budget(const char *entry, const char *name,
+ const char *path)
+{
+ char cmd[PATH_MAX], file[PATH_MAX];
+ int fd, status, retval;
+ pid_t pid;
+
+ snprintf(file, sizeof(file), BINFMT_DIR "/%s", entry);
+ snprintf(cmd, sizeof(cmd), "+%s %s\n", name, path);
+
+ fd = open(file, O_WRONLY | O_CLOEXEC);
+ if (fd < 0)
+ return -1;
+
+ pid = fork();
+ if (pid == 0) {
+ ssize_t n;
+
+ /* A namespace of its own, with nothing left in it to spend. */
+ if (unshare(CLONE_NEWUSER) || write_interp_limit("0"))
+ _exit(BIND_NO_BUDGET);
+ n = write(fd, cmd, strlen(cmd));
+ _exit(n < 0 ? errno : 0);
+ }
+ close(fd);
+ if (pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
+ return -1;
+ retval = WEXITSTATUS(status);
+ return retval == BIND_NO_BUDGET ? -1 : retval;
+}
+
+FIXTURE(bound_interp) {
+ char obj[PATH_MAX];
+ struct bpf_case c;
+ bool started;
+};
+
+FIXTURE_SETUP(bound_interp)
+{
+ const char *why = bpf_handler_unsupported();
+
+ if (why)
+ SKIP(return, "%s", why);
+ if (!binfmt_flag_supported('D')) {
+ ASSERT_EQ(errno, EINVAL);
+ SKIP(return, "kernel without the 'D' flag");
+ }
+
+ ASSERT_EQ(install_interp(BIND_FIRST), 0);
+ ASSERT_EQ(install_interp(BIND_SECOND), 0);
+
+ ASSERT_EQ(artifact_path(self->obj, sizeof(self->obj),
+ "interp_bind.bpf.o"), 0);
+
+ /*
+ * Registered disabled, so it cannot be matched yet and can still be
+ * given interpreters. Each path is resolved once, by its write(2);
+ * from here on the entry holds the files themselves.
+ */
+ ASSERT_EQ(bpf_case_start_flags(&self->c, self->obj, "interp_bind",
+ "test_interp_bind", "D"), 0);
+ self->started = true;
+
+ ASSERT_EQ(entry_bind("test_interp_bind", "first", BIND_FIRST), 0);
+ ASSERT_EQ(entry_bind("test_interp_bind", "second", BIND_SECOND), 0);
+}
+
+FIXTURE_TEARDOWN(bound_interp)
+{
+ if (self->started)
+ bpf_case_stop(&self->c);
+ unlink(BIND_FIRST);
+ unlink(BIND_SECOND);
+ unlink(AARCH64_PATH);
+ unlink(BIND_RISCV_PATH);
+ unlink(BIND_ARM_PATH);
+}
+
+/* Enabling is what makes the configured entry matchable. */
+static int activate(const char *entry)
+{
+ return entry_command(entry, "1\n");
+}
+
+/* One entry, one interpreter per guest architecture, picked per exec. */
+TEST_F(bound_interp, selects_by_name)
+{
+ ASSERT_EQ(create_fake_elf(AARCH64_PATH, EM_AARCH64), 0);
+ ASSERT_EQ(create_fake_elf(BIND_RISCV_PATH, EM_RISCV), 0);
+
+ /* Disabled, so it does not match and no format claims the binary. */
+ EXPECT_EQ(exec_errno(AARCH64_PATH), ENOEXEC);
+
+ ASSERT_EQ(activate("test_interp_bind"), 0);
+ EXPECT_EQ(check_output(AARCH64_PATH, BIND_EXPECT BIND_FIRST), 0);
+ EXPECT_EQ(check_output(BIND_RISCV_PATH, BIND_EXPECT BIND_SECOND), 0);
+}
+
+/* What was bound is what runs, whatever the path holds afterwards. */
+TEST_F(bound_interp, path_no_longer_decides)
+{
+ char other[PATH_MAX];
+
+ ASSERT_EQ(create_fake_elf(AARCH64_PATH, EM_AARCH64), 0);
+ ASSERT_EQ(activate("test_interp_bind"), 0);
+
+ /* Bound interpreters are pinned against writes, exactly like 'F'. */
+ EXPECT_TRUE(write_denied(BIND_FIRST));
+
+ /* Replace the path with a different binary: a new file, new inode. */
+ ASSERT_EQ(artifact_path(other, sizeof(other), "binfmt_bpf_interp"), 0);
+ ASSERT_EQ(unlink(BIND_FIRST), 0);
+ ASSERT_EQ(copy_file(other, BIND_FIRST), 0);
+
+ EXPECT_EQ(check_output(AARCH64_PATH, BIND_EXPECT BIND_FIRST), 0);
+}
+
+/* The entry reports what it bound, under the names it bound them as. */
+TEST_F(bound_interp, entry_reports_bindings)
+{
+ EXPECT_TRUE(entry_shows("test_interp_bind",
+ "bpf-interpreter first " BIND_FIRST));
+ EXPECT_TRUE(entry_shows("test_interp_bind",
+ "bpf-interpreter second " BIND_SECOND));
+}
+
+/* Selecting a name the entry did not bind fails the exec. */
+TEST_F(bound_interp, unbound_name_fails)
+{
+ ASSERT_EQ(create_fake_elf(BIND_ARM_PATH, EM_ARM), 0);
+ ASSERT_EQ(activate("test_interp_bind"), 0);
+
+ EXPECT_EQ(exec_errno(BIND_ARM_PATH), ENOENT);
+}
+
+/* Activating seals it: what can be matched cannot be changed. */
+TEST_F(bound_interp, sealed_once_active)
+{
+ ASSERT_EQ(activate("test_interp_bind"), 0);
+
+ EXPECT_EQ(entry_bind("test_interp_bind", "third", BIND_SECOND), -EBUSY);
+ EXPECT_FALSE(entry_shows("test_interp_bind",
+ "bpf-interpreter third " BIND_SECOND));
+}
+
+/* The seal is for good: disabling the entry again reopens nothing. */
+TEST_F(bound_interp, disable_does_not_unseal)
+{
+ ASSERT_EQ(activate("test_interp_bind"), 0);
+ ASSERT_EQ(entry_command("test_interp_bind", "0\n"), 0);
+
+ EXPECT_EQ(entry_bind("test_interp_bind", "third", BIND_SECOND), -EBUSY);
+}
+
+/* An entry registered without 'D' is sealed from the start. */
+TEST_F(bound_interp, born_sealed)
+{
+ /* A second entry for the handler the fixture already published. */
+ ASSERT_EQ(register_entry("test_born_sealed", "interp_bind", NULL), 0);
+
+ EXPECT_EQ(entry_bind("test_born_sealed", "first", BIND_FIRST), -EBUSY);
+ unregister("test_born_sealed");
+}
+
+/* A name is bound once; a second use of it is refused. */
+TEST_F(bound_interp, duplicate_name_refused)
+{
+ EXPECT_EQ(entry_bind("test_interp_bind", "first", BIND_SECOND), -EEXIST);
+}
+
+/* A name is a printable word: the entry file reports 'name path' lines. */
+TEST_F(bound_interp, name_must_be_printable)
+{
+ /* A control character would forge a line into the entry file. */
+ EXPECT_EQ(entry_bind("test_interp_bind", "a\tb", BIND_FIRST), -EINVAL);
+ EXPECT_EQ(entry_bind("test_interp_bind", "a\nb", BIND_FIRST), -EINVAL);
+
+ /* A space cannot even be spelled: the path starts after the first one. */
+ EXPECT_EQ(entry_bind("test_interp_bind", "a b", BIND_FIRST), -EINVAL);
+}
+
+/* The command ends at the write: bytes past an embedded nul are refused. */
+TEST_F(bound_interp, trailing_bytes_refused)
+{
+ char cmd[PATH_MAX];
+ size_t len;
+ int fd;
+
+ /* entry_command() cannot spell a nul, so write the buffer raw. */
+ snprintf(cmd, sizeof(cmd), "+nul %s", BIND_FIRST);
+ len = strlen(cmd) + 1;
+ memcpy(cmd + len, "junk", sizeof("junk"));
+ len += sizeof("junk");
+
+ fd = open(BINFMT_DIR "/test_interp_bind", O_WRONLY | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+ EXPECT_EQ(write(fd, cmd, len), -1);
+ EXPECT_EQ(errno, EINVAL);
+ close(fd);
+
+ EXPECT_FALSE(entry_shows("test_interp_bind",
+ "bpf-interpreter nul " BIND_FIRST));
+}
+
+/* An entry binds at most BIND_MAX interpreters. */
+TEST_F(bound_interp, capped_bindings)
+{
+ char name[16];
+ int i;
+
+ /* The fixture bound "first" and "second" already. */
+ for (i = 2; i < BIND_MAX; i++) {
+ snprintf(name, sizeof(name), "n%d", i);
+ ASSERT_EQ(entry_bind("test_interp_bind", name, BIND_FIRST), 0);
+ }
+ EXPECT_EQ(entry_bind("test_interp_bind", "over", BIND_FIRST), -ENOSPC);
+}
+
+/* A binding pins a file: it is charged, and refused once the budget is out. */
+TEST_F(bound_interp, bindings_are_charged)
+{
+ int err = bind_out_of_budget("test_interp_bind", "third", BIND_FIRST);
+
+ if (err < 0)
+ SKIP(return, "no user namespaces or no " INTERP_LIMIT);
+
+ /* The charge follows the writer, not the entry file it writes to. */
+ EXPECT_EQ(err, ENOSPC);
+
+ /* The budget was the only thing in the way. */
+ EXPECT_EQ(entry_bind("test_interp_bind", "third", BIND_FIRST), 0);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/exec/binfmt_misc_common.h b/tools/testing/selftests/exec/binfmt_misc_common.h
new file mode 100644
index 000000000..745aff84d
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_misc_common.h
@@ -0,0 +1,315 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Helpers shared by the binfmt_misc selftests. */
+#ifndef __SELFTESTS_EXEC_BINFMT_MISC_COMMON_H
+#define __SELFTESTS_EXEC_BINFMT_MISC_COMMON_H
+
+#include <elf.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <libgen.h>
+#include <limits.h>
+#include <link.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#define BINFMT_DIR "/proc/sys/fs/binfmt_misc"
+#define BINFMT_REG BINFMT_DIR "/register"
+
+/* comm holds 15 usable chars; a read of /proc/self/comm appends a newline. */
+#define TASK_COMM_LEN 16
+
+/* The canonical payload argv: run_payload() passes it, the payloads assert it. */
+#define PAYLOAD_ARGV0 "payload-argv0"
+#define PAYLOAD_ARG1 "argone"
+#define PAYLOAD_ARG2 "argtwo"
+
+/* Marker the loader tests poke into the payload's e_ident padding. */
+#define LOADER_MARKER "LDRTST"
+
+/* Exit status run_payload() reports when the exec was refused as unhandled. */
+#define RUN_ENOEXEC 42
+
+static inline int copy_file(const char *src, const char *dst)
+{
+ char buf[4096];
+ int in, out;
+ ssize_t n;
+
+ in = open(src, O_RDONLY);
+ if (in < 0)
+ return -1;
+ /* The tests share /tmp, so never write through a name they don't own. */
+ unlink(dst);
+ out = open(dst, O_WRONLY | O_CREAT | O_EXCL, 0755);
+ if (out < 0) {
+ close(in);
+ return -1;
+ }
+ while ((n = read(in, buf, sizeof(buf))) > 0) {
+ if (write(out, buf, n) != n) {
+ close(in);
+ close(out);
+ return -1;
+ }
+ }
+ close(in);
+ close(out);
+ return n < 0 ? -1 : 0;
+}
+
+/* Write @rule to the register file, preserving the write's errno. */
+static inline int write_reg(const char *rule)
+{
+ int fd, saved;
+ ssize_t n;
+
+ fd = open(BINFMT_REG, O_WRONLY);
+ if (fd < 0)
+ return -1;
+ n = write(fd, rule, strlen(rule));
+ saved = errno;
+ close(fd);
+ errno = saved;
+ return n < 0 ? -1 : 0;
+}
+
+static inline void unregister(const char *name)
+{
+ char path[PATH_MAX];
+ int fd;
+
+ snprintf(path, sizeof(path), BINFMT_DIR "/%s", name);
+ fd = open(path, O_WRONLY);
+ if (fd >= 0) {
+ if (write(fd, "-1", 2) < 0)
+ ; /* best effort */
+ close(fd);
+ }
+}
+
+/* Write @line to @entry's file, reporting the errno it was refused with. */
+static inline int entry_command(const char *entry, const char *line)
+{
+ char path[PATH_MAX];
+ int fd, retval = 0;
+ size_t len = strlen(line);
+
+ snprintf(path, sizeof(path), BINFMT_DIR "/%s", entry);
+ fd = open(path, O_WRONLY | O_CLOEXEC);
+ if (fd < 0)
+ return -errno;
+ if (write(fd, line, len) != (ssize_t)len)
+ retval = -errno;
+ close(fd);
+ return retval;
+}
+
+/* Does @entry's file report @line? */
+static inline bool entry_shows(const char *entry, const char *line)
+{
+ char path[PATH_MAX], buf[PATH_MAX];
+ bool found = false;
+ FILE *fp;
+
+ snprintf(path, sizeof(path), BINFMT_DIR "/%s", entry);
+ fp = fopen(path, "r");
+ if (!fp)
+ return false;
+ while (fgets(buf, sizeof(buf), fp)) {
+ buf[strcspn(buf, "\n")] = '\0';
+ if (!strcmp(buf, line)) {
+ found = true;
+ break;
+ }
+ }
+ fclose(fp);
+ return found;
+}
+
+/* Mount binfmt_misc unless it already is, and report whether it is usable. */
+static inline bool binfmt_misc_available(void)
+{
+ if (access(BINFMT_REG, F_OK) < 0)
+ mount("binfmt_misc", BINFMT_DIR, "binfmt_misc", 0, NULL);
+ return access(BINFMT_REG, F_OK) == 0;
+}
+
+/* Absolute path of @name in the directory this test was built into. */
+static inline int artifact_path(char *out, size_t sz, const char *name)
+{
+ char exe[PATH_MAX];
+ ssize_t n;
+
+ n = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
+ if (n < 0)
+ return -1;
+ exe[n] = '\0';
+ if ((size_t)snprintf(out, sz, "%s/%s", dirname(exe), name) >= sz)
+ return -1;
+ return 0;
+}
+
+/* Probe kernel support for a registration flag with a throwaway entry. */
+static inline bool binfmt_flag_supported(char flag)
+{
+ char rule[64];
+
+ snprintf(rule, sizeof(rule), ":bm_flag_probe:E::bmprobe::/bin/true:%c",
+ flag);
+ if (write_reg(rule))
+ return false;
+ unregister("bm_flag_probe");
+ return true;
+}
+
+/*
+ * Run @path with the canonical payload argv and return its exit status, or
+ * RUN_ENOEXEC when the exec itself was refused as unhandled.
+ */
+static inline int run_payload(const char *path)
+{
+ int status;
+ pid_t pid;
+
+ pid = fork();
+ if (pid == 0) {
+ execl(path, PAYLOAD_ARGV0, PAYLOAD_ARG1, PAYLOAD_ARG2,
+ (char *)NULL);
+ _exit(errno == ENOEXEC ? RUN_ENOEXEC : 126);
+ }
+ if (pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
+ return -1;
+ return WEXITSTATUS(status);
+}
+
+/* Does the exe link name @path? */
+static inline bool exe_is(const char *path)
+{
+ char exe[PATH_MAX], real[PATH_MAX];
+ ssize_t n;
+
+ n = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
+ if (n <= 0 || !realpath(path, real))
+ return false;
+ exe[n] = '\0';
+ return !strcmp(exe, real);
+}
+
+/* Is comm @name truncated to what a comm can hold? */
+static inline bool comm_is(const char *name)
+{
+ char comm[TASK_COMM_LEN + 2], expect[TASK_COMM_LEN];
+ ssize_t n;
+ int fd;
+
+ fd = open("/proc/self/comm", O_RDONLY);
+ if (fd < 0)
+ return false;
+ n = read(fd, comm, sizeof(comm) - 1);
+ close(fd);
+ if (n <= 0)
+ return false;
+ if (comm[n - 1] == '\n')
+ n--;
+ comm[n] = '\0';
+ snprintf(expect, sizeof(expect), "%s", name);
+ return !strcmp(comm, expect);
+}
+
+/* Opening @path for writing has to fail with ETXTBSY. */
+static inline bool write_denied(const char *path)
+{
+ int fd = open(path, O_WRONLY);
+
+ if (fd >= 0) {
+ close(fd);
+ return false;
+ }
+ return errno == ETXTBSY;
+}
+
+static inline int patch_file(const char *path, off_t off, const void *data, size_t len)
+{
+ ssize_t n;
+ int fd;
+
+ fd = open(path, O_WRONLY);
+ if (fd < 0)
+ return -1;
+ n = pwrite(fd, data, len, off);
+ close(fd);
+ return n == (ssize_t)len ? 0 : -1;
+}
+
+/* start_code and end_code are the 26th and 27th fields of /proc/pid/stat. */
+static inline int stat_codes(pid_t pid, unsigned long *start_code,
+ unsigned long *end_code)
+{
+ char buf[4096], path[64], *p;
+ ssize_t n;
+ int fd, i;
+
+ snprintf(path, sizeof(path), "/proc/%d/stat", pid);
+ fd = open(path, O_RDONLY);
+ if (fd < 0)
+ return -1;
+ n = read(fd, buf, sizeof(buf) - 1);
+ close(fd);
+ if (n <= 0)
+ return -1;
+ buf[n] = '\0';
+
+ /* Skip "pid (comm)", then start_code is the 24th field after it. */
+ p = strrchr(buf, ')');
+ if (!p)
+ return -1;
+ p++;
+ for (i = 0; i < 23; i++) {
+ p = strchr(p + 1, ' ');
+ if (!p)
+ return -1;
+ }
+ if (sscanf(p, " %lu %lu", start_code, end_code) != 2)
+ return -1;
+ return 0;
+}
+
+/* Find the system loader through our own PT_INTERP. */
+static inline int find_loader(char *out, size_t sz)
+{
+ ElfW(Ehdr) eh;
+ ElfW(Phdr) ph;
+ int fd, i, ret = -1;
+
+ fd = open("/proc/self/exe", O_RDONLY);
+ if (fd < 0)
+ return -1;
+ if (pread(fd, &eh, sizeof(eh), 0) != sizeof(eh))
+ goto out;
+ for (i = 0; i < eh.e_phnum; i++) {
+ if (pread(fd, &ph, sizeof(ph),
+ eh.e_phoff + i * eh.e_phentsize) != sizeof(ph))
+ goto out;
+ if (ph.p_type != PT_INTERP)
+ continue;
+ if (!ph.p_filesz || ph.p_filesz > sz)
+ goto out;
+ if (pread(fd, out, ph.p_filesz, ph.p_offset) !=
+ (ssize_t)ph.p_filesz)
+ goto out;
+ out[ph.p_filesz - 1] = '\0';
+ ret = 0;
+ break;
+ }
+out:
+ close(fd);
+ return ret;
+}
+
+#endif /* __SELFTESTS_EXEC_BINFMT_MISC_COMMON_H */
diff --git a/tools/testing/selftests/exec/binfmt_misc_disabled.c b/tools/testing/selftests/exec/binfmt_misc_disabled.c
new file mode 100644
index 000000000..47c9e8a4e
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_misc_disabled.c
@@ -0,0 +1,172 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Test the 'D' (register disabled) flag of binfmt_misc. An entry
+ * registered with it exists but cannot be matched until userspace enables
+ * it, which splits a registration into create and activate.
+ *
+ * Needs root for the registration; no bpf toolchain involved.
+ */
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "binfmt_misc_common.h"
+#include "kselftest_harness.h"
+
+#define MAGIC "#DISABLED-SELFTEST#"
+#define TARGET_PATH "/tmp/binfmt_disabled_target"
+#define INTERP_PATH "/tmp/binfmt_disabled_interp.sh"
+#define ENTRY "test_disabled"
+#define RULE(flags) ":" ENTRY ":M:0:" MAGIC "::" INTERP_PATH ":" flags
+
+/* The interpreter exits with a code the harness can recognise. */
+#define EXIT_INTERP 7
+
+/* The target only has to carry the magic; it is never actually loaded. */
+static int create_target(void)
+{
+ char buf[128] = MAGIC "\n";
+ int fd;
+
+ unlink(TARGET_PATH);
+ fd = open(TARGET_PATH, O_WRONLY | O_CREAT | O_EXCL, 0755);
+ if (fd < 0)
+ return -1;
+ if (write(fd, buf, sizeof(buf)) != (ssize_t)sizeof(buf)) {
+ close(fd);
+ return -1;
+ }
+ close(fd);
+ return 0;
+}
+
+static int create_interp(void)
+{
+ char buf[64];
+ int fd;
+
+ unlink(INTERP_PATH);
+ fd = open(INTERP_PATH, O_WRONLY | O_CREAT | O_EXCL, 0755);
+ if (fd < 0)
+ return -1;
+ snprintf(buf, sizeof(buf), "#!/bin/sh\nexit %d\n", EXIT_INTERP);
+ if (write(fd, buf, strlen(buf)) != (ssize_t)strlen(buf)) {
+ close(fd);
+ return -1;
+ }
+ return close(fd);
+}
+
+FIXTURE(disabled) {
+};
+
+FIXTURE_SETUP(disabled)
+{
+ if (getuid() != 0)
+ SKIP(return, "test must be run as root");
+ if (!binfmt_misc_available())
+ SKIP(return, "no binfmt_misc");
+
+ /* Skip the whole suite on a kernel that does not know 'D'. */
+ if (!binfmt_flag_supported('D')) {
+ ASSERT_EQ(errno, EINVAL);
+ SKIP(return, "kernel without the 'D' flag");
+ }
+
+ ASSERT_EQ(create_interp(), 0);
+ ASSERT_EQ(create_target(), 0);
+}
+
+FIXTURE_TEARDOWN(disabled)
+{
+ unregister(ENTRY);
+ unlink(TARGET_PATH);
+ unlink(INTERP_PATH);
+}
+
+/* The entry exists but does not dispatch until it is enabled. */
+TEST_F(disabled, inert_until_enabled)
+{
+ ASSERT_EQ(write_reg(RULE("D")), 0);
+ EXPECT_TRUE(entry_shows(ENTRY, "disabled"));
+
+ /* Nothing matches it, so no binary format claims the target. */
+ EXPECT_EQ(run_payload(TARGET_PATH), RUN_ENOEXEC);
+
+ ASSERT_EQ(entry_command(ENTRY, "1\n"), 0);
+ EXPECT_TRUE(entry_shows(ENTRY, "enabled"));
+ EXPECT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);
+}
+
+/* Without 'D' an entry is matchable the moment it is registered. */
+TEST_F(disabled, enabled_without_the_flag)
+{
+ ASSERT_EQ(write_reg(RULE("")), 0);
+ EXPECT_TRUE(entry_shows(ENTRY, "enabled"));
+ EXPECT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);
+}
+
+/* 'D' is spent on the registration: the entry does not report it back. */
+TEST_F(disabled, flag_not_reported)
+{
+ ASSERT_EQ(write_reg(RULE("D")), 0);
+ EXPECT_FALSE(entry_shows(ENTRY, "flags: D"));
+ EXPECT_TRUE(entry_shows(ENTRY, "flags: "));
+}
+
+/* A disabled entry can be disabled and enabled like any other. */
+TEST_F(disabled, toggles_like_any_entry)
+{
+ ASSERT_EQ(write_reg(RULE("D")), 0);
+
+ ASSERT_EQ(entry_command(ENTRY, "1\n"), 0);
+ ASSERT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);
+ ASSERT_EQ(entry_command(ENTRY, "0\n"), 0);
+ EXPECT_EQ(run_payload(TARGET_PATH), RUN_ENOEXEC);
+ ASSERT_EQ(entry_command(ENTRY, "1\n"), 0);
+ EXPECT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);
+}
+
+/* 'D' composes with the invocation flags a static entry can carry. */
+TEST_F(disabled, composes_with_invocation_flags)
+{
+ ASSERT_EQ(write_reg(RULE("PD")), 0);
+ EXPECT_TRUE(entry_shows(ENTRY, "disabled"));
+ EXPECT_TRUE(entry_shows(ENTRY, "flags: P"));
+}
+
+/* '-1' to the status file sweeps a staged entry with everything else. */
+TEST_F(disabled, removed_by_remove_all)
+{
+ int fd;
+
+ ASSERT_EQ(write_reg(RULE("D")), 0);
+ EXPECT_TRUE(entry_shows(ENTRY, "disabled"));
+
+ fd = open(BINFMT_DIR "/status", O_WRONLY | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(write(fd, "-1", 2), 2);
+ close(fd);
+
+ EXPECT_NE(access(BINFMT_DIR "/" ENTRY, F_OK), 0);
+}
+
+/* A file handle held across a removal cannot resurrect the entry. */
+TEST_F(disabled, no_resurrection_after_remove)
+{
+ int fd;
+
+ ASSERT_EQ(write_reg(RULE("D")), 0);
+ fd = open(BINFMT_DIR "/" ENTRY, O_WRONLY | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+
+ ASSERT_EQ(write(fd, "-1", 2), 2);
+ EXPECT_NE(access(BINFMT_DIR "/" ENTRY, F_OK), 0);
+
+ /* Accepted like any toggle of a removed entry, but publishes nothing. */
+ EXPECT_EQ(write(fd, "1", 1), 1);
+ EXPECT_EQ(run_payload(TARGET_PATH), RUN_ENOEXEC);
+ close(fd);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/exec/binfmt_misc_interplimit.c b/tools/testing/selftests/exec/binfmt_misc_interplimit.c
new file mode 100644
index 000000000..bf611c551
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_misc_interplimit.c
@@ -0,0 +1,232 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * A pre-opened interpreter - what 'F' gives a static entry and what a 'B'
+ * entry binds - keeps a file open for as long as the entry lives, so it pins
+ * the mount it came from. It costs no file descriptor, and binfmt_misc is
+ * FS_USERNS_MOUNT, so an unprivileged user namespace can create them without
+ * bound. Check that UCOUNT_BINFMT_MISC_INTERPRETERS bounds it, that an entry
+ * that pre-opens nothing is not charged, that removing an entry gives the
+ * charge back, and that nesting a user namespace does not evade it.
+ *
+ * Runs unprivileged in a user namespace.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "../filesystems/utils.h"
+#include "kselftest_harness.h"
+
+#define MNT "/tmp/binfmt_interplimit"
+#define NESTED_MNT "/tmp/binfmt_interplimit_nested"
+#define LIMIT_SYSCTL "/proc/sys/user/max_binfmt_misc_interpreters"
+
+#define MAGIC "\\xde\\xad"
+/* Not on the instance, and unlike /bin/true it always exists. */
+#define INTERP "/proc/self/exe"
+
+/* Small enough to fill by hand, big enough that a refund is visible. */
+#define LIMIT 4
+
+/* What UCOUNT_ENTRY() lets a namespace raise its own limit to. */
+#define LIMIT_MAX "2147483647"
+
+static int ensure_dir(const char *path)
+{
+ if (mkdir(path, 0755) && errno != EEXIST)
+ return -1;
+ return 0;
+}
+
+/* Write @val to @path, preserving write(2)'s errno for the caller. */
+static int write_keep_errno(const char *path, const char *val)
+{
+ int fd, saved;
+ ssize_t n;
+
+ fd = open(path, O_WRONLY | O_CLOEXEC);
+ if (fd < 0)
+ return -1;
+ n = write(fd, val, strlen(val));
+ saved = errno;
+ close(fd);
+ errno = saved;
+ return n < 0 ? -1 : 0;
+}
+
+static int set_limit(const char *val)
+{
+ return write_keep_errno(LIMIT_SYSCTL, val);
+}
+
+static int register_at(const char *mnt, const char *rule)
+{
+ char path[PATH_MAX];
+
+ snprintf(path, sizeof(path), "%s/register", mnt);
+ return write_keep_errno(path, rule);
+}
+
+/* An 'F' entry: one interpreter pre-opened at registration, one charge. */
+static int register_fixed(const char *mnt, const char *name)
+{
+ char rule[PATH_MAX];
+
+ snprintf(rule, sizeof(rule), ":%s:M::" MAGIC "::" INTERP ":F", name);
+ return register_at(mnt, rule);
+}
+
+/* The same entry without 'F': the interpreter is opened per exec instead. */
+static int register_plain(const char *mnt, const char *name)
+{
+ char rule[PATH_MAX];
+
+ snprintf(rule, sizeof(rule), ":%s:M::" MAGIC "::" INTERP ":", name);
+ return register_at(mnt, rule);
+}
+
+static int remove_entry(const char *mnt, const char *name)
+{
+ char path[PATH_MAX];
+
+ snprintf(path, sizeof(path), "%s/%s", mnt, name);
+ return write_keep_errno(path, "-1\n");
+}
+
+static bool entry_exists(const char *mnt, const char *name)
+{
+ char path[PATH_MAX];
+
+ snprintf(path, sizeof(path), "%s/%s", mnt, name);
+ return access(path, F_OK) == 0;
+}
+
+/* Register @n 'F' entries, each with a name of its own. */
+static int fill_budget(const char *mnt, unsigned int n)
+{
+ char name[32];
+ unsigned int i;
+
+ for (i = 0; i < n; i++) {
+ snprintf(name, sizeof(name), "fixed%u", i);
+ if (register_fixed(mnt, name))
+ return -1;
+ }
+ return 0;
+}
+
+FIXTURE(interp_limit) {
+};
+
+FIXTURE_SETUP(interp_limit)
+{
+ /* setup_userns() exits rather than returns if this is not there. */
+ if (access("/proc/self/ns/user", F_OK))
+ SKIP(return, "kernel without user namespaces");
+ ASSERT_EQ(setup_userns(), 0);
+
+ /* CAP_SYS_RESOURCE in this namespace is what makes it writable. */
+ if (set_limit(LIMIT_MAX)) {
+ if (errno == ENOENT)
+ SKIP(return, "kernel without " LIMIT_SYSCTL);
+ SKIP(return, "cannot set the limit: %s", strerror(errno));
+ }
+
+ ASSERT_EQ(ensure_dir(MNT), 0);
+ if (mount("binfmt_misc", MNT, "binfmt_misc", 0, NULL)) {
+ int saved = errno;
+
+ /* Teardown doesn't run when setup skips, so clean up here. */
+ rmdir(MNT);
+ SKIP(return, "no binfmt_misc: %s", strerror(saved));
+ }
+}
+
+FIXTURE_TEARDOWN(interp_limit)
+{
+ /* The namespaces go with the process; just don't litter /tmp. */
+ umount2(NESTED_MNT, MNT_DETACH);
+ umount2(MNT, MNT_DETACH);
+ rmdir(NESTED_MNT);
+ rmdir(MNT);
+}
+
+/* Every pre-opened interpreter is charged, and the budget is a hard stop. */
+TEST_F(interp_limit, fixed_interpreters_are_charged)
+{
+ char buf[32];
+
+ snprintf(buf, sizeof(buf), "%u", LIMIT);
+ ASSERT_EQ(set_limit(buf), 0);
+
+ ASSERT_EQ(fill_budget(MNT, LIMIT), 0);
+
+ EXPECT_NE(register_fixed(MNT, "over"), 0);
+ EXPECT_EQ(errno, ENOSPC);
+
+ /* A refused registration leaves nothing behind. */
+ EXPECT_FALSE(entry_exists(MNT, "over"));
+}
+
+/* An entry that pre-opens nothing pins nothing, so it is not charged. */
+TEST_F(interp_limit, plain_entries_are_not_charged)
+{
+ ASSERT_EQ(set_limit("0"), 0);
+
+ EXPECT_EQ(register_plain(MNT, "plain"), 0);
+ EXPECT_TRUE(entry_exists(MNT, "plain"));
+
+ /* ... while the same entry with 'F' has nothing to spend. */
+ EXPECT_NE(register_fixed(MNT, "fixed"), 0);
+ EXPECT_EQ(errno, ENOSPC);
+}
+
+/* Removing an entry closes its interpreters and gives the charge back. */
+TEST_F(interp_limit, removal_refunds_the_charge)
+{
+ char buf[32];
+
+ snprintf(buf, sizeof(buf), "%u", LIMIT);
+ ASSERT_EQ(set_limit(buf), 0);
+
+ ASSERT_EQ(fill_budget(MNT, LIMIT), 0);
+ ASSERT_NE(register_fixed(MNT, "over"), 0);
+
+ ASSERT_EQ(remove_entry(MNT, "fixed0"), 0);
+ EXPECT_EQ(register_fixed(MNT, "over"), 0);
+}
+
+/*
+ * The charge walks the ancestors, so a namespace cannot buy itself budget by
+ * nesting: it may raise only its own limit, and the parent it was created
+ * from is charged for every binding made below it.
+ */
+TEST_F(interp_limit, nesting_does_not_evade_it)
+{
+ char buf[32];
+
+ snprintf(buf, sizeof(buf), "%u", LIMIT);
+ ASSERT_EQ(set_limit(buf), 0);
+ ASSERT_EQ(fill_budget(MNT, LIMIT), 0);
+
+ ASSERT_EQ(setup_userns(), 0);
+ ASSERT_EQ(set_limit(LIMIT_MAX), 0);
+
+ ASSERT_EQ(ensure_dir(NESTED_MNT), 0);
+ ASSERT_EQ(mount("binfmt_misc", NESTED_MNT, "binfmt_misc", 0, NULL), 0);
+
+ /* A fresh instance with an unlimited budget of its own, and yet: */
+ EXPECT_NE(register_fixed(NESTED_MNT, "nested"), 0);
+ EXPECT_EQ(errno, ENOSPC);
+
+ /* The nested instance works for anything that pins no file. */
+ EXPECT_EQ(register_plain(NESTED_MNT, "nested_plain"), 0);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/exec/binfmt_misc_loader.c b/tools/testing/selftests/exec/binfmt_misc_loader.c
new file mode 100644
index 000000000..1e14dcd27
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_misc_loader.c
@@ -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
diff --git a/tools/testing/selftests/exec/binfmt_misc_selfpin.c b/tools/testing/selftests/exec/binfmt_misc_selfpin.c
new file mode 100644
index 000000000..5286b0604
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_misc_selfpin.c
@@ -0,0 +1,158 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * An 'F' entry keeps its interpreter open for as long as the entry exists,
+ * and the entry only goes away when the binfmt_misc superblock is destroyed.
+ * An interpreter that lives on a mount which in turn keeps that superblock
+ * alive therefore pins the instance that owns it, and nothing can break the
+ * cycle. Check the two ways userspace could arrange for that: an interpreter
+ * on the binfmt_misc instance itself, and one on a filesystem stacked on it.
+ *
+ * Runs unprivileged in a user namespace; binfmt_misc is FS_USERNS_MOUNT.
+ */
+#define _GNU_SOURCE
+#include <fcntl.h>
+#include <limits.h>
+#include <sched.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+
+#include "../filesystems/utils.h"
+#include "kselftest_harness.h"
+
+#define MNT "/tmp/binfmt_selfpin"
+#define BACKING "/tmp/binfmt_selfpin_back"
+#define LOWER BACKING "/lower"
+#define MERGED "/tmp/binfmt_selfpin_merged"
+
+#define MAGIC "\\xde\\xad"
+#define RULE(interp) ":selfpin:M::" MAGIC "::" interp ":F"
+/* Not on the instance, and unlike /bin/true it always exists. */
+#define INTERP "/proc/self/exe"
+
+#define OPTS_MAX (3 * PATH_MAX + 64)
+
+static int ensure_dir(const char *path)
+{
+ if (mkdir(path, 0755) && errno != EEXIST)
+ return -1;
+ return 0;
+}
+
+/* Write @rule to this instance's register file, preserving write(2)'s errno. */
+static int register_at(struct __test_metadata *_metadata, const char *rule)
+{
+ int fd, saved;
+ ssize_t n;
+
+ fd = open(MNT "/register", O_WRONLY);
+ ASSERT_GE(fd, 0);
+ n = write(fd, rule, strlen(rule));
+ saved = errno;
+ close(fd);
+ errno = saved;
+ return n < 0 ? -1 : 0;
+}
+
+/*
+ * Mount an overlay over @lower using a private upper/work pair, so the two
+ * mounts this test performs cannot interfere with each other and neither
+ * overlaps the lower layer.
+ */
+static int mount_overlay(const char *lower, int nr)
+{
+ char opts[OPTS_MAX], upper[PATH_MAX], work[PATH_MAX];
+
+ snprintf(upper, sizeof(upper), "%s/upper%d", BACKING, nr);
+ snprintf(work, sizeof(work), "%s/work%d", BACKING, nr);
+ if (mkdir(upper, 0755) || mkdir(work, 0755))
+ return -1;
+
+ snprintf(opts, sizeof(opts), "lowerdir=%s,upperdir=%s,workdir=%s",
+ lower, upper, work);
+ return mount("ovl", MERGED, "overlay", 0, opts);
+}
+
+FIXTURE(selfpin) {
+};
+
+FIXTURE_SETUP(selfpin)
+{
+ /* setup_userns() exits rather than returns if this is not there. */
+ if (access("/proc/self/ns/user", F_OK))
+ SKIP(return, "kernel without user namespaces");
+ ASSERT_EQ(setup_userns(), 0);
+
+ ASSERT_EQ(ensure_dir(MNT), 0);
+ if (mount("binfmt_misc", MNT, "binfmt_misc", 0, NULL)) {
+ int saved = errno;
+
+ /* Teardown doesn't run when setup skips, so clean up here. */
+ rmdir(MNT);
+ SKIP(return, "no binfmt_misc: %s", strerror(saved));
+ }
+}
+
+FIXTURE_TEARDOWN(selfpin)
+{
+ /* The namespaces go with the process; just don't litter /tmp. */
+ umount2(MERGED, MNT_DETACH);
+ umount2(BACKING, MNT_DETACH);
+ umount2(MNT, MNT_DETACH);
+ rmdir(MERGED);
+ rmdir(BACKING);
+ rmdir(MNT);
+}
+
+/*
+ * The instance's own files are regular files the mounter owns, so they can be
+ * made executable. Opening one for exec still has to fail, otherwise the entry
+ * pins the very superblock it lives in.
+ */
+TEST_F(selfpin, interpreter_on_the_instance)
+{
+ ASSERT_EQ(chmod(MNT "/status", 0755), 0);
+
+ ASSERT_NE(register_at(_metadata, RULE(MNT "/status")), 0);
+ EXPECT_EQ(errno, EACCES);
+}
+
+/* Same for an entry file rather than one of the control files. */
+TEST_F(selfpin, interpreter_on_an_entry)
+{
+ ASSERT_EQ(register_at(_metadata, ":victim:M::" MAGIC "::" INTERP ":"), 0);
+ ASSERT_EQ(chmod(MNT "/victim", 0755), 0);
+
+ ASSERT_NE(register_at(_metadata, RULE(MNT "/victim")), 0);
+ EXPECT_EQ(errno, EACCES);
+}
+
+/*
+ * A stacking filesystem holds a private clone of each layer for its whole
+ * lifetime, so an instance used as a layer can be pinned by an interpreter
+ * that does not live on it at all. Refuse to be a layer.
+ */
+TEST_F(selfpin, refuses_to_be_stacked_on)
+{
+ ASSERT_EQ(ensure_dir(BACKING), 0);
+ ASSERT_EQ(mount("tmpfs", BACKING, "tmpfs", 0, NULL), 0);
+ ASSERT_EQ(mkdir(LOWER, 0755), 0);
+ ASSERT_EQ(ensure_dir(MERGED), 0);
+
+ /* Nothing to prove unless overlayfs works here at all. */
+ if (mount_overlay(LOWER, 1)) {
+ if (errno == ENODEV || errno == EPERM)
+ SKIP(return, "no unprivileged overlayfs");
+ SKIP(return, "overlayfs unusable here: %s", strerror(errno));
+ }
+ ASSERT_EQ(umount(MERGED), 0);
+
+ EXPECT_NE(mount_overlay(MNT, 2), 0);
+}
+
+/* An ordinary interpreter still registers with 'F'. */
+TEST_F(selfpin, ordinary_interpreter_still_works)
+{
+ EXPECT_EQ(register_at(_metadata, RULE(INTERP)), 0);
+}
+
+TEST_HARNESS_MAIN
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 000000000..2ebf73de8
--- /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 <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
diff --git a/tools/testing/selftests/exec/binfmt_script.py b/tools/testing/selftests/exec/binfmt_script.py
new file mode 100755
index 000000000..2c575a2c0
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_script.py
@@ -0,0 +1,179 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# Test that truncation of bprm->buf doesn't cause unexpected execs paths, along
+# with various other pathological cases.
+import os, subprocess
+
+# Relevant commits
+#
+# b5372fe5dc84 ("exec: load_script: Do not exec truncated interpreter path")
+# 6eb3c3d0a52d ("exec: increase BINPRM_BUF_SIZE to 256")
+
+# BINPRM_BUF_SIZE
+SIZE=256
+
+NAME_MAX=int(subprocess.check_output(["getconf", "NAME_MAX", "."]))
+
+test_num=0
+pass_num=0
+fail_num=0
+
+code='''#!/usr/bin/perl
+print "Executed interpreter! Args:\n";
+print "0 : '$0'\n";
+$counter = 1;
+foreach my $a (@ARGV) {
+ print "$counter : '$a'\n";
+ $counter++;
+}
+'''
+
+##
+# test - produce a binfmt_script hashbang line for testing
+#
+# @size: bytes for bprm->buf line, including hashbang but not newline
+# @good: whether this script is expected to execute correctly
+# @hashbang: the special 2 bytes for running binfmt_script
+# @leading: any leading whitespace before the executable path
+# @root: start of executable pathname
+# @target: end of executable pathname
+# @arg: bytes following the executable pathname
+# @fill: character to fill between @root and @target to reach @size bytes
+# @newline: character to use as newline, not counted towards @size
+# ...
+def test(name, size, good=True, leading="", root="./", target="/perl",
+ fill="A", arg="", newline="\n", hashbang="#!"):
+ global test_num, pass_num, fail_num, tests, NAME_MAX
+ test_num += 1
+ if test_num > tests:
+ raise ValueError("more binfmt_script tests than expected! (want %d, expected %d)"
+ % (test_num, tests))
+
+ middle = ""
+ remaining = size - len(hashbang) - len(leading) - len(root) - len(target) - len(arg)
+ # The middle of the pathname must not exceed NAME_MAX
+ while remaining >= NAME_MAX:
+ middle += fill * (NAME_MAX - 1)
+ middle += '/'
+ remaining -= NAME_MAX
+ middle += fill * remaining
+
+ dirpath = root + middle
+ binary = dirpath + target
+ if len(target):
+ os.makedirs(dirpath, mode=0o755, exist_ok=True)
+ open(binary, "w").write(code)
+ os.chmod(binary, 0o755)
+
+ buf=hashbang + leading + root + middle + target + arg + newline
+ if len(newline) > 0:
+ buf += 'echo this is not really perl\n'
+
+ script = "binfmt_script-%s" % (name)
+ open(script, "w").write(buf)
+ os.chmod(script, 0o755)
+
+ proc = subprocess.Popen(["./%s" % (script)], shell=True,
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+ stdout = proc.communicate()[0]
+
+ if proc.returncode == 0 and b'Executed interpreter' in stdout:
+ if good:
+ print("ok %d - binfmt_script %s (successful good exec)"
+ % (test_num, name))
+ pass_num += 1
+ else:
+ print("not ok %d - binfmt_script %s succeeded when it should have failed"
+ % (test_num, name))
+ fail_num = 1
+ else:
+ if good:
+ print("not ok %d - binfmt_script %s failed when it should have succeeded (rc:%d)"
+ % (test_num, name, proc.returncode))
+ fail_num = 1
+ else:
+ print("ok %d - binfmt_script %s (correctly failed bad exec)"
+ % (test_num, name))
+ pass_num += 1
+
+ # Clean up crazy binaries
+ os.unlink(script)
+ if len(target):
+ elements = binary.split('/')
+ os.unlink(binary)
+ elements.pop()
+ while len(elements) > 1:
+ os.rmdir("/".join(elements))
+ elements.pop()
+
+tests=27
+print("TAP version 1.3")
+print("1..%d" % (tests))
+
+### FAIL (8 tests)
+
+# Entire path is well past the BINFMT_BUF_SIZE.
+test(name="too-big", size=SIZE+80, good=False)
+# Path is right at max size, making it impossible to tell if it was truncated.
+test(name="exact", size=SIZE, good=False)
+# Same as above, but with leading whitespace.
+test(name="exact-space", size=SIZE, good=False, leading=" ")
+# Huge buffer of only whitespace.
+test(name="whitespace-too-big", size=SIZE+71, good=False, root="",
+ fill=" ", target="")
+# A good path, but it gets truncated due to leading whitespace.
+test(name="truncated", size=SIZE+17, good=False, leading=" " * 19)
+# Entirely empty except for #!
+test(name="empty", size=2, good=False, root="",
+ fill="", target="", newline="")
+# Within size, but entirely spaces
+test(name="spaces", size=SIZE-1, good=False, root="", fill=" ",
+ target="", newline="")
+# Newline before binary.
+test(name="newline-prefix", size=SIZE-1, good=False, leading="\n",
+ root="", fill=" ", target="")
+
+### ok (19 tests)
+
+# The original test case that was broken by commit:
+# 8099b047ecc4 ("exec: load_script: don't blindly truncate shebang string")
+test(name="test.pl", size=439, leading=" ",
+ root="./nix/store/bwav8kz8b3y471wjsybgzw84mrh4js9-perl-5.28.1/bin",
+ arg=" -I/nix/store/x6yyav38jgr924nkna62q3pkp0dgmzlx-perl5.28.1-File-Slurp-9999.25/lib/perl5/site_perl -I/nix/store/ha8v67sl8dac92r9z07vzr4gv1y9nwqz-perl5.28.1-Net-DBus-1.1.0/lib/perl5/site_perl -I/nix/store/dcrkvnjmwh69ljsvpbdjjdnqgwx90a9d-perl5.28.1-XML-Parser-2.44/lib/perl5/site_perl -I/nix/store/rmji88k2zz7h4zg97385bygcydrf2q8h-perl5.28.1-XML-Twig-3.52/lib/perl5/site_perl")
+# One byte under size, leaving newline visible.
+test(name="one-under", size=SIZE-1)
+# Two bytes under size, leaving newline visible.
+test(name="two-under", size=SIZE-2)
+# Exact size, but trailing whitespace visible instead of newline
+test(name="exact-trunc-whitespace", size=SIZE, arg=" ")
+# Exact size, but trailing space and first arg char visible instead of newline.
+test(name="exact-trunc-arg", size=SIZE, arg=" f")
+# One bute under, with confirmed non-truncated arg since newline now visible.
+test(name="one-under-full-arg", size=SIZE-1, arg=" f")
+# Short read buffer by one byte.
+test(name="one-under-no-nl", size=SIZE-1, newline="")
+# Short read buffer by half buffer size.
+test(name="half-under-no-nl", size=int(SIZE/2), newline="")
+# One byte under with whitespace arg. leaving wenline visible.
+test(name="one-under-trunc-arg", size=SIZE-1, arg=" ")
+# One byte under with whitespace leading. leaving wenline visible.
+test(name="one-under-leading", size=SIZE-1, leading=" ")
+# One byte under with whitespace leading and as arg. leaving newline visible.
+test(name="one-under-leading-trunc-arg", size=SIZE-1, leading=" ", arg=" ")
+# Same as above, but with 2 bytes under
+test(name="two-under-no-nl", size=SIZE-2, newline="")
+test(name="two-under-trunc-arg", size=SIZE-2, arg=" ")
+test(name="two-under-leading", size=SIZE-2, leading=" ")
+test(name="two-under-leading-trunc-arg", size=SIZE-2, leading=" ", arg=" ")
+# Same as above, but with buffer half filled
+test(name="two-under-no-nl", size=int(SIZE/2), newline="")
+test(name="two-under-trunc-arg", size=int(SIZE/2), arg=" ")
+test(name="two-under-leading", size=int(SIZE/2), leading=" ")
+test(name="two-under-lead-trunc-arg", size=int(SIZE/2), leading=" ", arg=" ")
+
+print("# Totals: pass:%d fail:%d xfail:0 xpass:0 skip:0 error:0" % (pass_num, fail_num))
+
+if test_num != tests:
+ raise ValueError("fewer binfmt_script tests than expected! (ran %d, expected %d"
+ % (test_num, tests))
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 000000000..d4c4a538c
--- /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 <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;
+}
diff --git a/tools/testing/selftests/exec/bpf_interp.bpf.c b/tools/testing/selftests/exec/bpf_interp.bpf.c
new file mode 100644
index 000000000..8df2d2d01
--- /dev/null
+++ b/tools/testing/selftests/exec/bpf_interp.bpf.c
@@ -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",
+};
diff --git a/tools/testing/selftests/exec/check-exec-tests.sh b/tools/testing/selftests/exec/check-exec-tests.sh
new file mode 100755
index 000000000..87102906a
--- /dev/null
+++ b/tools/testing/selftests/exec/check-exec-tests.sh
@@ -0,0 +1,205 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Test the "inc" interpreter.
+#
+# See include/uapi/linux/securebits.h, include/uapi/linux/fcntl.h and
+# samples/check-exec/inc.c
+#
+# Copyright © 2024 Microsoft Corporation
+
+set -u -e -o pipefail
+
+EXPECTED_OUTPUT="1"
+exec 2>/dev/null
+
+DIR="$(dirname $(readlink -f "$0"))"
+source "${DIR}"/../kselftest/ktap_helpers.sh
+
+exec_direct() {
+ local expect="$1"
+ local script="$2"
+ shift 2
+ local ret=0
+ local out
+
+ # Updates PATH for `env` to execute the `inc` interpreter.
+ out="$(PATH="." "$@" "${script}")" || ret=$?
+
+ if [[ ${ret} -ne ${expect} ]]; then
+ echo "ERROR: Wrong expectation for direct file execution: ${ret}"
+ return 1
+ fi
+ if [[ ${ret} -eq 0 && "${out}" != "${EXPECTED_OUTPUT}" ]]; then
+ echo "ERROR: Wrong output for direct file execution: ${out}"
+ return 1
+ fi
+}
+
+exec_indirect() {
+ local expect="$1"
+ local script="$2"
+ shift 2
+ local ret=0
+ local out
+
+ # Script passed as argument.
+ out="$("$@" ./inc "${script}")" || ret=$?
+
+ if [[ ${ret} -ne ${expect} ]]; then
+ echo "ERROR: Wrong expectation for indirect file execution: ${ret}"
+ return 1
+ fi
+ if [[ ${ret} -eq 0 && "${out}" != "${EXPECTED_OUTPUT}" ]]; then
+ echo "ERROR: Wrong output for indirect file execution: ${out}"
+ return 1
+ fi
+}
+
+exec_stdin_reg() {
+ local expect="$1"
+ local script="$2"
+ shift 2
+ local ret=0
+ local out
+
+ # Executing stdin must be allowed if the related file is executable.
+ out="$("$@" ./inc -i < "${script}")" || ret=$?
+
+ if [[ ${ret} -ne ${expect} ]]; then
+ echo "ERROR: Wrong expectation for stdin regular file execution: ${ret}"
+ return 1
+ fi
+ if [[ ${ret} -eq 0 && "${out}" != "${EXPECTED_OUTPUT}" ]]; then
+ echo "ERROR: Wrong output for stdin regular file execution: ${out}"
+ return 1
+ fi
+}
+
+exec_stdin_pipe() {
+ local expect="$1"
+ shift
+ local ret=0
+ local out
+
+ # A pipe is not executable.
+ out="$(cat script-exec.inc | "$@" ./inc -i)" || ret=$?
+
+ if [[ ${ret} -ne ${expect} ]]; then
+ echo "ERROR: Wrong expectation for stdin pipe execution: ${ret}"
+ return 1
+ fi
+}
+
+exec_argument() {
+ local expect="$1"
+ local ret=0
+ shift
+ local out
+
+ # Script not coming from a file must not be executed.
+ out="$("$@" ./inc -c "$(< script-exec.inc)")" || ret=$?
+
+ if [[ ${ret} -ne ${expect} ]]; then
+ echo "ERROR: Wrong expectation for arbitrary argument execution: ${ret}"
+ return 1
+ fi
+ if [[ ${ret} -eq 0 && "${out}" != "${EXPECTED_OUTPUT}" ]]; then
+ echo "ERROR: Wrong output for arbitrary argument execution: ${out}"
+ return 1
+ fi
+}
+
+exec_interactive() {
+ exec_stdin_pipe "$@"
+ exec_argument "$@"
+}
+
+ktap_test() {
+ ktap_test_result "$*" "$@"
+}
+
+ktap_print_header
+ktap_set_plan 28
+
+# Without secbit configuration, nothing is changed.
+
+ktap_print_msg "By default, executable scripts are allowed to be interpreted and executed."
+ktap_test exec_direct 0 script-exec.inc
+ktap_test exec_indirect 0 script-exec.inc
+
+ktap_print_msg "By default, executable stdin is allowed to be interpreted."
+ktap_test exec_stdin_reg 0 script-exec.inc
+
+ktap_print_msg "By default, non-executable scripts are allowed to be interpreted, but not directly executed."
+# We get 126 because of direct execution by Bash.
+ktap_test exec_direct 126 script-noexec.inc
+ktap_test exec_indirect 0 script-noexec.inc
+
+ktap_print_msg "By default, non-executable stdin is allowed to be interpreted."
+ktap_test exec_stdin_reg 0 script-noexec.inc
+
+ktap_print_msg "By default, interactive commands are allowed to be interpreted."
+ktap_test exec_interactive 0
+
+# With only file restriction: protect non-malicious users from inadvertent errors (e.g. python ~/Downloads/*.py).
+
+ktap_print_msg "With -f, executable scripts are allowed to be interpreted and executed."
+ktap_test exec_direct 0 script-exec.inc ./set-exec -f --
+ktap_test exec_indirect 0 script-exec.inc ./set-exec -f --
+
+ktap_print_msg "With -f, executable stdin is allowed to be interpreted."
+ktap_test exec_stdin_reg 0 script-exec.inc ./set-exec -f --
+
+ktap_print_msg "With -f, non-executable scripts are not allowed to be executed nor interpreted."
+# Direct execution of non-executable script is alwayse denied by the kernel.
+ktap_test exec_direct 1 script-noexec.inc ./set-exec -f --
+ktap_test exec_indirect 1 script-noexec.inc ./set-exec -f --
+
+ktap_print_msg "With -f, non-executable stdin is allowed to be interpreted."
+ktap_test exec_stdin_reg 0 script-noexec.inc ./set-exec -f --
+
+ktap_print_msg "With -f, interactive commands are allowed to be interpreted."
+ktap_test exec_interactive 0 ./set-exec -f --
+
+# With only denied interactive commands: check or monitor script content (e.g. with LSM).
+
+ktap_print_msg "With -i, executable scripts are allowed to be interpreted and executed."
+ktap_test exec_direct 0 script-exec.inc ./set-exec -i --
+ktap_test exec_indirect 0 script-exec.inc ./set-exec -i --
+
+ktap_print_msg "With -i, executable stdin is allowed to be interpreted."
+ktap_test exec_stdin_reg 0 script-exec.inc ./set-exec -i --
+
+ktap_print_msg "With -i, non-executable scripts are allowed to be interpreted, but not directly executed."
+# Direct execution of non-executable script is alwayse denied by the kernel.
+ktap_test exec_direct 1 script-noexec.inc ./set-exec -i --
+ktap_test exec_indirect 0 script-noexec.inc ./set-exec -i --
+
+ktap_print_msg "With -i, non-executable stdin is not allowed to be interpreted."
+ktap_test exec_stdin_reg 1 script-noexec.inc ./set-exec -i --
+
+ktap_print_msg "With -i, interactive commands are not allowed to be interpreted."
+ktap_test exec_interactive 1 ./set-exec -i --
+
+# With both file restriction and denied interactive commands: only allow executable scripts.
+
+ktap_print_msg "With -fi, executable scripts are allowed to be interpreted and executed."
+ktap_test exec_direct 0 script-exec.inc ./set-exec -fi --
+ktap_test exec_indirect 0 script-exec.inc ./set-exec -fi --
+
+ktap_print_msg "With -fi, executable stdin is allowed to be interpreted."
+ktap_test exec_stdin_reg 0 script-exec.inc ./set-exec -fi --
+
+ktap_print_msg "With -fi, non-executable scripts are not allowed to be interpreted nor executed."
+# Direct execution of non-executable script is alwayse denied by the kernel.
+ktap_test exec_direct 1 script-noexec.inc ./set-exec -fi --
+ktap_test exec_indirect 1 script-noexec.inc ./set-exec -fi --
+
+ktap_print_msg "With -fi, non-executable stdin is not allowed to be interpreted."
+ktap_test exec_stdin_reg 1 script-noexec.inc ./set-exec -fi --
+
+ktap_print_msg "With -fi, interactive commands are not allowed to be interpreted."
+ktap_test exec_interactive 1 ./set-exec -fi --
+
+ktap_finished
diff --git a/tools/testing/selftests/exec/check-exec.c b/tools/testing/selftests/exec/check-exec.c
new file mode 100644
index 000000000..f2397e75a
--- /dev/null
+++ b/tools/testing/selftests/exec/check-exec.c
@@ -0,0 +1,463 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Test execveat(2) with AT_EXECVE_CHECK, and prctl(2) with
+ * SECBIT_EXEC_RESTRICT_FILE, SECBIT_EXEC_DENY_INTERACTIVE, and their locked
+ * counterparts.
+ *
+ * Copyright © 2018-2020 ANSSI
+ * Copyright © 2024 Microsoft Corporation
+ *
+ * Author: Mickaël Salaün <mic@digikod.net>
+ */
+
+#include <asm-generic/unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/prctl.h>
+#include <linux/securebits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/capability.h>
+#include <sys/mount.h>
+#include <sys/prctl.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/sysmacros.h>
+#include <unistd.h>
+
+/* Defines AT_EXECVE_CHECK without type conflicts. */
+#define _ASM_GENERIC_FCNTL_H
+#include <linux/fcntl.h>
+
+#include "kselftest_harness.h"
+
+static int sys_execveat(int dirfd, const char *pathname, char *const argv[],
+ char *const envp[], int flags)
+{
+ return syscall(__NR_execveat, dirfd, pathname, argv, envp, flags);
+}
+
+static void drop_privileges(struct __test_metadata *const _metadata)
+{
+ const unsigned int noroot = SECBIT_NOROOT | SECBIT_NOROOT_LOCKED;
+ cap_t cap_p;
+
+ if ((cap_get_secbits() & noroot) != noroot)
+ EXPECT_EQ(0, cap_set_secbits(noroot));
+
+ cap_p = cap_get_proc();
+ EXPECT_NE(NULL, cap_p);
+ EXPECT_NE(-1, cap_clear(cap_p));
+
+ /*
+ * Drops everything, especially CAP_SETPCAP, CAP_DAC_OVERRIDE, and
+ * CAP_DAC_READ_SEARCH.
+ */
+ EXPECT_NE(-1, cap_set_proc(cap_p));
+ EXPECT_NE(-1, cap_free(cap_p));
+}
+
+static int test_secbits_set(const unsigned int secbits)
+{
+ int err;
+
+ err = prctl(PR_SET_SECUREBITS, secbits);
+ if (err)
+ return errno;
+ return 0;
+}
+
+FIXTURE(access)
+{
+ int memfd, pipefd;
+ int pipe_fds[2], socket_fds[2];
+};
+
+FIXTURE_VARIANT(access)
+{
+ const bool mount_exec;
+ const bool file_exec;
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(access, mount_exec_file_exec) {
+ /* clang-format on */
+ .mount_exec = true,
+ .file_exec = true,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(access, mount_exec_file_noexec) {
+ /* clang-format on */
+ .mount_exec = true,
+ .file_exec = false,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(access, mount_noexec_file_exec) {
+ /* clang-format on */
+ .mount_exec = false,
+ .file_exec = true,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(access, mount_noexec_file_noexec) {
+ /* clang-format on */
+ .mount_exec = false,
+ .file_exec = false,
+};
+
+static const char binary_path[] = "./false";
+static const char workdir_path[] = "./test-mount";
+static const char reg_file_path[] = "./test-mount/regular_file";
+static const char dir_path[] = "./test-mount/directory";
+static const char block_dev_path[] = "./test-mount/block_device";
+static const char char_dev_path[] = "./test-mount/character_device";
+static const char fifo_path[] = "./test-mount/fifo";
+
+FIXTURE_SETUP(access)
+{
+ int procfd_path_size;
+ static const char path_template[] = "/proc/self/fd/%d";
+ char procfd_path[sizeof(path_template) + 10];
+
+ /* Makes sure we are not already restricted nor locked. */
+ EXPECT_EQ(0, test_secbits_set(0));
+
+ /*
+ * Cleans previous workspace if any error previously happened (don't
+ * check errors).
+ */
+ umount(workdir_path);
+ rmdir(workdir_path);
+
+ /* Creates a clean mount point. */
+ ASSERT_EQ(0, mkdir(workdir_path, 00700));
+ ASSERT_EQ(0, mount("test", workdir_path, "tmpfs",
+ MS_MGC_VAL | (variant->mount_exec ? 0 : MS_NOEXEC),
+ "mode=0700,size=9m"));
+
+ /* Creates a regular file. */
+ ASSERT_EQ(0, mknod(reg_file_path,
+ S_IFREG | (variant->file_exec ? 0700 : 0600), 0));
+ /* Creates a directory. */
+ ASSERT_EQ(0, mkdir(dir_path, variant->file_exec ? 0700 : 0600));
+ /* Creates a character device: /dev/null. */
+ ASSERT_EQ(0, mknod(char_dev_path, S_IFCHR | 0400, makedev(1, 3)));
+ /* Creates a block device: /dev/loop0 */
+ ASSERT_EQ(0, mknod(block_dev_path, S_IFBLK | 0400, makedev(7, 0)));
+ /* Creates a fifo. */
+ ASSERT_EQ(0, mknod(fifo_path, S_IFIFO | 0600, 0));
+
+ /* Creates a regular file without user mount point. */
+ self->memfd = memfd_create("test-exec-probe", MFD_CLOEXEC);
+ ASSERT_LE(0, self->memfd);
+ /* Sets mode, which must be ignored by the exec check. */
+ ASSERT_EQ(0, fchmod(self->memfd, variant->file_exec ? 0700 : 0600));
+
+ /* Creates a pipefs file descriptor. */
+ ASSERT_EQ(0, pipe(self->pipe_fds));
+ procfd_path_size = snprintf(procfd_path, sizeof(procfd_path),
+ path_template, self->pipe_fds[0]);
+ ASSERT_LT(procfd_path_size, sizeof(procfd_path));
+ self->pipefd = open(procfd_path, O_RDWR | O_CLOEXEC);
+ ASSERT_LE(0, self->pipefd);
+ ASSERT_EQ(0, fchmod(self->pipefd, variant->file_exec ? 0700 : 0600));
+
+ /* Creates a socket file descriptor. */
+ ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0,
+ self->socket_fds));
+}
+
+FIXTURE_TEARDOWN_PARENT(access)
+{
+ /* There is no need to unlink the test files. */
+ EXPECT_EQ(0, umount(workdir_path));
+ EXPECT_EQ(0, rmdir(workdir_path));
+}
+
+static void fill_exec_fd(struct __test_metadata *_metadata, const int fd_out)
+{
+ char buf[1024];
+ size_t len;
+ int fd_in;
+
+ fd_in = open(binary_path, O_CLOEXEC | O_RDONLY);
+ ASSERT_LE(0, fd_in);
+ /* Cannot use copy_file_range(2) because of EXDEV. */
+ len = read(fd_in, buf, sizeof(buf));
+ EXPECT_LE(0, len);
+ while (len > 0) {
+ EXPECT_EQ(len, write(fd_out, buf, len))
+ {
+ TH_LOG("Failed to write: %s (%d)", strerror(errno),
+ errno);
+ }
+ len = read(fd_in, buf, sizeof(buf));
+ EXPECT_LE(0, len);
+ }
+ EXPECT_EQ(0, close(fd_in));
+}
+
+static void fill_exec_path(struct __test_metadata *_metadata,
+ const char *const path)
+{
+ int fd_out;
+
+ fd_out = open(path, O_CLOEXEC | O_WRONLY);
+ ASSERT_LE(0, fd_out)
+ {
+ TH_LOG("Failed to open %s: %s", path, strerror(errno));
+ }
+ fill_exec_fd(_metadata, fd_out);
+ EXPECT_EQ(0, close(fd_out));
+}
+
+static void test_exec_fd(struct __test_metadata *_metadata, const int fd,
+ const int err_code)
+{
+ char *const argv[] = { "", NULL };
+ int access_ret, access_errno;
+
+ /*
+ * If we really execute fd, filled with the "false" binary, the current
+ * thread will exits with an error, which will be interpreted by the
+ * test framework as an error. With AT_EXECVE_CHECK, we only check a
+ * potential successful execution.
+ */
+ access_ret = sys_execveat(fd, "", argv, NULL,
+ AT_EMPTY_PATH | AT_EXECVE_CHECK);
+ access_errno = errno;
+ if (err_code) {
+ EXPECT_EQ(-1, access_ret);
+ EXPECT_EQ(err_code, access_errno)
+ {
+ TH_LOG("Wrong error for execveat(2): %s (%d)",
+ strerror(access_errno), errno);
+ }
+ } else {
+ EXPECT_EQ(0, access_ret)
+ {
+ TH_LOG("Access denied: %s", strerror(access_errno));
+ }
+ }
+}
+
+static void test_exec_path(struct __test_metadata *_metadata,
+ const char *const path, const int err_code)
+{
+ int flags = O_CLOEXEC;
+ int fd;
+
+ /* Do not block on pipes. */
+ if (path == fifo_path)
+ flags |= O_NONBLOCK;
+
+ fd = open(path, flags | O_RDONLY);
+ ASSERT_LE(0, fd)
+ {
+ TH_LOG("Failed to open %s: %s", path, strerror(errno));
+ }
+ test_exec_fd(_metadata, fd, err_code);
+ EXPECT_EQ(0, close(fd));
+}
+
+/* Tests that we don't get ENOEXEC. */
+TEST_F(access, regular_file_empty)
+{
+ const int exec = variant->mount_exec && variant->file_exec;
+
+ test_exec_path(_metadata, reg_file_path, exec ? 0 : EACCES);
+
+ drop_privileges(_metadata);
+ test_exec_path(_metadata, reg_file_path, exec ? 0 : EACCES);
+}
+
+TEST_F(access, regular_file_elf)
+{
+ const int exec = variant->mount_exec && variant->file_exec;
+
+ fill_exec_path(_metadata, reg_file_path);
+
+ test_exec_path(_metadata, reg_file_path, exec ? 0 : EACCES);
+
+ drop_privileges(_metadata);
+ test_exec_path(_metadata, reg_file_path, exec ? 0 : EACCES);
+}
+
+/* Tests that we don't get ENOEXEC. */
+TEST_F(access, memfd_empty)
+{
+ const int exec = variant->file_exec;
+
+ test_exec_fd(_metadata, self->memfd, exec ? 0 : EACCES);
+
+ drop_privileges(_metadata);
+ test_exec_fd(_metadata, self->memfd, exec ? 0 : EACCES);
+}
+
+TEST_F(access, memfd_elf)
+{
+ const int exec = variant->file_exec;
+
+ fill_exec_fd(_metadata, self->memfd);
+
+ test_exec_fd(_metadata, self->memfd, exec ? 0 : EACCES);
+
+ drop_privileges(_metadata);
+ test_exec_fd(_metadata, self->memfd, exec ? 0 : EACCES);
+}
+
+TEST_F(access, non_regular_files)
+{
+ test_exec_path(_metadata, dir_path, EACCES);
+ test_exec_path(_metadata, block_dev_path, EACCES);
+ test_exec_path(_metadata, char_dev_path, EACCES);
+ test_exec_path(_metadata, fifo_path, EACCES);
+ test_exec_fd(_metadata, self->socket_fds[0], EACCES);
+ test_exec_fd(_metadata, self->pipefd, EACCES);
+}
+
+/* clang-format off */
+FIXTURE(secbits) {};
+/* clang-format on */
+
+FIXTURE_VARIANT(secbits)
+{
+ const bool is_privileged;
+ const int error;
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(secbits, priv) {
+ /* clang-format on */
+ .is_privileged = true,
+ .error = 0,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(secbits, unpriv) {
+ /* clang-format on */
+ .is_privileged = false,
+ .error = EPERM,
+};
+
+FIXTURE_SETUP(secbits)
+{
+ /* Makes sure no exec bits are set. */
+ EXPECT_EQ(0, test_secbits_set(0));
+ EXPECT_EQ(0, prctl(PR_GET_SECUREBITS));
+
+ if (!variant->is_privileged)
+ drop_privileges(_metadata);
+}
+
+FIXTURE_TEARDOWN(secbits)
+{
+}
+
+TEST_F(secbits, legacy)
+{
+ EXPECT_EQ(variant->error, test_secbits_set(0));
+}
+
+#define CHILD(...) \
+ do { \
+ pid_t child = vfork(); \
+ EXPECT_LE(0, child); \
+ if (child == 0) { \
+ __VA_ARGS__; \
+ _exit(0); \
+ } \
+ } while (0)
+
+TEST_F(secbits, exec)
+{
+ unsigned int secbits = prctl(PR_GET_SECUREBITS);
+
+ secbits |= SECBIT_EXEC_RESTRICT_FILE;
+ EXPECT_EQ(0, test_secbits_set(secbits));
+ EXPECT_EQ(secbits, prctl(PR_GET_SECUREBITS));
+ CHILD(EXPECT_EQ(secbits, prctl(PR_GET_SECUREBITS)));
+
+ secbits |= SECBIT_EXEC_DENY_INTERACTIVE;
+ EXPECT_EQ(0, test_secbits_set(secbits));
+ EXPECT_EQ(secbits, prctl(PR_GET_SECUREBITS));
+ CHILD(EXPECT_EQ(secbits, prctl(PR_GET_SECUREBITS)));
+
+ secbits &= ~(SECBIT_EXEC_RESTRICT_FILE | SECBIT_EXEC_DENY_INTERACTIVE);
+ EXPECT_EQ(0, test_secbits_set(secbits));
+ EXPECT_EQ(secbits, prctl(PR_GET_SECUREBITS));
+ CHILD(EXPECT_EQ(secbits, prctl(PR_GET_SECUREBITS)));
+}
+
+TEST_F(secbits, check_locked_set)
+{
+ unsigned int secbits = prctl(PR_GET_SECUREBITS);
+
+ secbits |= SECBIT_EXEC_RESTRICT_FILE;
+ EXPECT_EQ(0, test_secbits_set(secbits));
+ secbits |= SECBIT_EXEC_RESTRICT_FILE_LOCKED;
+ EXPECT_EQ(0, test_secbits_set(secbits));
+
+ /* Checks lock set but unchanged. */
+ EXPECT_EQ(variant->error, test_secbits_set(secbits));
+ CHILD(EXPECT_EQ(variant->error, test_secbits_set(secbits)));
+
+ secbits &= ~SECBIT_EXEC_RESTRICT_FILE;
+ EXPECT_EQ(EPERM, test_secbits_set(0));
+ CHILD(EXPECT_EQ(EPERM, test_secbits_set(0)));
+}
+
+TEST_F(secbits, check_locked_unset)
+{
+ unsigned int secbits = prctl(PR_GET_SECUREBITS);
+
+ secbits |= SECBIT_EXEC_RESTRICT_FILE_LOCKED;
+ EXPECT_EQ(0, test_secbits_set(secbits));
+
+ /* Checks lock unset but unchanged. */
+ EXPECT_EQ(variant->error, test_secbits_set(secbits));
+ CHILD(EXPECT_EQ(variant->error, test_secbits_set(secbits)));
+
+ secbits &= ~SECBIT_EXEC_RESTRICT_FILE;
+ EXPECT_EQ(EPERM, test_secbits_set(0));
+ CHILD(EXPECT_EQ(EPERM, test_secbits_set(0)));
+}
+
+TEST_F(secbits, restrict_locked_set)
+{
+ unsigned int secbits = prctl(PR_GET_SECUREBITS);
+
+ secbits |= SECBIT_EXEC_DENY_INTERACTIVE;
+ EXPECT_EQ(0, test_secbits_set(secbits));
+ secbits |= SECBIT_EXEC_DENY_INTERACTIVE_LOCKED;
+ EXPECT_EQ(0, test_secbits_set(secbits));
+
+ /* Checks lock set but unchanged. */
+ EXPECT_EQ(variant->error, test_secbits_set(secbits));
+ CHILD(EXPECT_EQ(variant->error, test_secbits_set(secbits)));
+
+ secbits &= ~SECBIT_EXEC_DENY_INTERACTIVE;
+ EXPECT_EQ(EPERM, test_secbits_set(0));
+ CHILD(EXPECT_EQ(EPERM, test_secbits_set(0)));
+}
+
+TEST_F(secbits, restrict_locked_unset)
+{
+ unsigned int secbits = prctl(PR_GET_SECUREBITS);
+
+ secbits |= SECBIT_EXEC_DENY_INTERACTIVE_LOCKED;
+ EXPECT_EQ(0, test_secbits_set(secbits));
+
+ /* Checks lock unset but unchanged. */
+ EXPECT_EQ(variant->error, test_secbits_set(secbits));
+ CHILD(EXPECT_EQ(variant->error, test_secbits_set(secbits)));
+
+ secbits &= ~SECBIT_EXEC_DENY_INTERACTIVE;
+ EXPECT_EQ(EPERM, test_secbits_set(0));
+ CHILD(EXPECT_EQ(EPERM, test_secbits_set(0)));
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/exec/config b/tools/testing/selftests/exec/config
new file mode 100644
index 000000000..ea359a929
--- /dev/null
+++ b/tools/testing/selftests/exec/config
@@ -0,0 +1,12 @@
+CONFIG_BLK_DEV=y
+CONFIG_BLK_DEV_LOOP=y
+CONFIG_BINFMT_MISC=y
+CONFIG_BINFMT_MISC_BPF=y
+CONFIG_BPF_JIT=y
+CONFIG_BPF_SYSCALL=y
+CONFIG_DEBUG_INFO=y
+CONFIG_DEBUG_INFO_BTF=y
+CONFIG_DEBUG_INFO_DWARF4=y
+CONFIG_OVERLAY_FS=y
+CONFIG_TMPFS=y
+CONFIG_USER_NS=y
diff --git a/tools/testing/selftests/exec/execveat.c b/tools/testing/selftests/exec/execveat.c
new file mode 100644
index 000000000..d37c068ed
--- /dev/null
+++ b/tools/testing/selftests/exec/execveat.c
@@ -0,0 +1,520 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2014 Google, Inc.
+ *
+ * Selftests for execveat(2).
+ */
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE /* to get O_PATH, AT_EMPTY_PATH */
+#endif
+#include <sys/sendfile.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "kselftest.h"
+
+#define TESTS_EXPECTED 54
+#define TEST_NAME_LEN (PATH_MAX * 4)
+
+#define CHECK_COMM "CHECK_COMM"
+
+static char longpath[2 * PATH_MAX] = "";
+static char *envp[] = { "IN_TEST=yes", NULL, NULL };
+static char *argv[] = { "execveat", "99", NULL };
+
+static int execveat_(int fd, const char *path, char **argv, char **envp,
+ int flags)
+{
+#ifdef __NR_execveat
+ return syscall(__NR_execveat, fd, path, argv, envp, flags);
+#else
+ errno = ENOSYS;
+ return -1;
+#endif
+}
+
+#define check_execveat_fail(fd, path, flags, errno) \
+ _check_execveat_fail(fd, path, flags, errno, #errno)
+static int _check_execveat_fail(int fd, const char *path, int flags,
+ int expected_errno, const char *errno_str)
+{
+ char test_name[TEST_NAME_LEN];
+ int rc;
+
+ errno = 0;
+ snprintf(test_name, sizeof(test_name),
+ "Check failure of execveat(%d, '%s', %d) with %s",
+ fd, path?:"(null)", flags, errno_str);
+ rc = execveat_(fd, path, argv, envp, flags);
+
+ if (rc > 0) {
+ ksft_print_msg("unexpected success from execveat(2)\n");
+ ksft_test_result_fail("%s\n", test_name);
+ return 1;
+ }
+ if (errno != expected_errno) {
+ ksft_print_msg("expected errno %d (%s) not %d (%s)\n",
+ expected_errno, strerror(expected_errno),
+ errno, strerror(errno));
+ ksft_test_result_fail("%s\n", test_name);
+ return 1;
+ }
+ ksft_test_result_pass("%s\n", test_name);
+ return 0;
+}
+
+static int check_execveat_invoked_rc(int fd, const char *path, int flags,
+ int expected_rc, int expected_rc2)
+{
+ char test_name[TEST_NAME_LEN];
+ int status;
+ int rc;
+ pid_t child;
+ int pathlen = path ? strlen(path) : 0;
+
+ if (pathlen > 40)
+ snprintf(test_name, sizeof(test_name),
+ "Check success of execveat(%d, '%.20s...%s', %d)... ",
+ fd, path, (path + pathlen - 20), flags);
+ else
+ snprintf(test_name, sizeof(test_name),
+ "Check success of execveat(%d, '%s', %d)... ",
+ fd, path?:"(null)", flags);
+
+ child = fork();
+ if (child < 0) {
+ ksft_perror("fork() failed");
+ ksft_test_result_fail("%s\n", test_name);
+ return 1;
+ }
+ if (child == 0) {
+ /* Child: do execveat(). */
+ rc = execveat_(fd, path, argv, envp, flags);
+ ksft_print_msg("child execveat() failed, rc=%d errno=%d (%s)\n",
+ rc, errno, strerror(errno));
+ exit(errno);
+ }
+ /* Parent: wait for & check child's exit status. */
+ rc = waitpid(child, &status, 0);
+ if (rc != child) {
+ ksft_print_msg("waitpid(%d,...) returned %d\n", child, rc);
+ ksft_test_result_fail("%s\n", test_name);
+ return 1;
+ }
+ if (!WIFEXITED(status)) {
+ ksft_print_msg("child %d did not exit cleanly, status=%08x\n",
+ child, status);
+ ksft_test_result_fail("%s\n", test_name);
+ return 1;
+ }
+ if ((WEXITSTATUS(status) != expected_rc) &&
+ (WEXITSTATUS(status) != expected_rc2)) {
+ ksft_print_msg("child %d exited with %d neither %d nor %d\n",
+ child, WEXITSTATUS(status), expected_rc,
+ expected_rc2);
+ ksft_test_result_fail("%s\n", test_name);
+ return 1;
+ }
+ ksft_test_result_pass("%s\n", test_name);
+ return 0;
+}
+
+static int check_execveat(int fd, const char *path, int flags)
+{
+ return check_execveat_invoked_rc(fd, path, flags, 99, 99);
+}
+
+static char *concat(const char *left, const char *right)
+{
+ char *result = malloc(strlen(left) + strlen(right) + 1);
+
+ strcpy(result, left);
+ strcat(result, right);
+ return result;
+}
+
+static int open_or_die(const char *filename, int flags)
+{
+ int fd = open(filename, flags);
+
+ if (fd < 0)
+ ksft_exit_fail_msg("Failed to open '%s'; "
+ "check prerequisites are available\n", filename);
+ return fd;
+}
+
+static void exe_cp(const char *src, const char *dest)
+{
+ int in_fd = open_or_die(src, O_RDONLY);
+ int out_fd = open(dest, O_RDWR|O_CREAT|O_TRUNC, 0755);
+ struct stat info;
+
+ fstat(in_fd, &info);
+ sendfile(out_fd, in_fd, NULL, info.st_size);
+ close(in_fd);
+ close(out_fd);
+}
+
+#define XX_DIR_LEN 200
+static int check_execveat_pathmax(int root_dfd, const char *src, int is_script)
+{
+ int fail = 0;
+ int ii, count, len;
+ char longname[XX_DIR_LEN + 1];
+ int fd;
+
+ if (*longpath == '\0') {
+ /* Create a filename close to PATH_MAX in length */
+ char *cwd = getcwd(NULL, 0);
+
+ if (!cwd) {
+ ksft_perror("Failed to getcwd()");
+ return 2;
+ }
+ strcpy(longpath, cwd);
+ strcat(longpath, "/");
+ memset(longname, 'x', XX_DIR_LEN - 1);
+ longname[XX_DIR_LEN - 1] = '/';
+ longname[XX_DIR_LEN] = '\0';
+ count = (PATH_MAX - 3 - strlen(cwd)) / XX_DIR_LEN;
+ for (ii = 0; ii < count; ii++) {
+ strcat(longpath, longname);
+ mkdir(longpath, 0755);
+ }
+ len = (PATH_MAX - 3 - strlen(cwd)) - (count * XX_DIR_LEN);
+ if (len <= 0)
+ len = 1;
+ memset(longname, 'y', len);
+ longname[len] = '\0';
+ strcat(longpath, longname);
+ free(cwd);
+ }
+ exe_cp(src, longpath);
+
+ /*
+ * Execute as a pre-opened file descriptor, which works whether this is
+ * a script or not (because the interpreter sees a filename like
+ * "/dev/fd/20").
+ */
+ fd = open(longpath, O_RDONLY);
+ if (fd > 0) {
+ ksft_print_msg("Invoke copy of '%s' via filename of length %zu:\n",
+ src, strlen(longpath));
+ fail += check_execveat(fd, "", AT_EMPTY_PATH);
+ } else {
+ ksft_print_msg("Failed to open length %zu filename, errno=%d (%s)\n",
+ strlen(longpath), errno, strerror(errno));
+ fail++;
+ }
+
+ /*
+ * Execute as a long pathname relative to "/". If this is a script,
+ * the interpreter will launch but fail to open the script because its
+ * name ("/dev/fd/5/xxx....") is bigger than PATH_MAX.
+ *
+ * The failure code is usually 127 (POSIX: "If a command is not found,
+ * the exit status shall be 127."), but some systems give 126 (POSIX:
+ * "If the command name is found, but it is not an executable utility,
+ * the exit status shall be 126."), so allow either.
+ */
+ if (is_script) {
+ ksft_print_msg("Invoke script via root_dfd and relative filename\n");
+ fail += check_execveat_invoked_rc(root_dfd, longpath + 1, 0,
+ 127, 126);
+ } else {
+ ksft_print_msg("Invoke exec via root_dfd and relative filename\n");
+ fail += check_execveat(root_dfd, longpath + 1, 0);
+ }
+
+ return fail;
+}
+
+static int check_execveat_comm(int fd, char *argv0, char *expected)
+{
+ char buf[128], *old_env, *old_argv0;
+ int ret;
+
+ snprintf(buf, sizeof(buf), CHECK_COMM "=%s", expected);
+
+ old_env = envp[1];
+ envp[1] = buf;
+
+ old_argv0 = argv[0];
+ argv[0] = argv0;
+
+ ksft_print_msg("Check execveat(AT_EMPTY_PATH)'s comm is %s\n",
+ expected);
+ ret = check_execveat_invoked_rc(fd, "", AT_EMPTY_PATH, 0, 0);
+
+ envp[1] = old_env;
+ argv[0] = old_argv0;
+
+ return ret;
+}
+
+static int run_tests(void)
+{
+ int fail = 0;
+ char *fullname = realpath("execveat", NULL);
+ char *fullname_script = realpath("script", NULL);
+ char *fullname_symlink = concat(fullname, ".symlink");
+ int subdir_dfd = open_or_die("subdir", O_DIRECTORY|O_RDONLY);
+ int subdir_dfd_ephemeral = open_or_die("subdir.ephemeral",
+ O_DIRECTORY|O_RDONLY);
+ int dot_dfd = open_or_die(".", O_DIRECTORY|O_RDONLY);
+ int root_dfd = open_or_die("/", O_DIRECTORY|O_RDONLY);
+ int dot_dfd_path = open_or_die(".", O_DIRECTORY|O_RDONLY|O_PATH);
+ int dot_dfd_cloexec = open_or_die(".", O_DIRECTORY|O_RDONLY|O_CLOEXEC);
+ int fd = open_or_die("execveat", O_RDONLY);
+ int fd_path = open_or_die("execveat", O_RDONLY|O_PATH);
+ int fd_symlink = open_or_die("execveat.symlink", O_RDONLY);
+ int fd_denatured = open_or_die("execveat.denatured", O_RDONLY);
+ int fd_denatured_path = open_or_die("execveat.denatured",
+ O_RDONLY|O_PATH);
+ int fd_script = open_or_die("script", O_RDONLY);
+ int fd_ephemeral = open_or_die("execveat.ephemeral", O_RDONLY);
+ int fd_ephemeral_path = open_or_die("execveat.path.ephemeral",
+ O_RDONLY|O_PATH);
+ int fd_script_ephemeral = open_or_die("script.ephemeral", O_RDONLY);
+ int fd_cloexec = open_or_die("execveat", O_RDONLY|O_CLOEXEC);
+ int fd_script_cloexec = open_or_die("script", O_RDONLY|O_CLOEXEC);
+
+ /* Check if we have execveat at all, and bail early if not */
+ errno = 0;
+ execveat_(-1, NULL, NULL, NULL, 0);
+ if (errno == ENOSYS) {
+ ksft_exit_skip(
+ "ENOSYS calling execveat - no kernel support?\n");
+ }
+
+ /* Change file position to confirm it doesn't affect anything */
+ lseek(fd, 10, SEEK_SET);
+
+ /* Normal executable file: */
+ /* dfd + path */
+ fail += check_execveat(subdir_dfd, "../execveat", 0);
+ fail += check_execveat(dot_dfd, "execveat", 0);
+ fail += check_execveat(dot_dfd_path, "execveat", 0);
+ /* absolute path */
+ fail += check_execveat(AT_FDCWD, fullname, 0);
+ /* absolute path with nonsense dfd */
+ fail += check_execveat(99, fullname, 0);
+ /* fd + no path */
+ fail += check_execveat(fd, "", AT_EMPTY_PATH);
+ /* O_CLOEXEC fd + no path */
+ fail += check_execveat(fd_cloexec, "", AT_EMPTY_PATH);
+ /* O_PATH fd */
+ fail += check_execveat(fd_path, "", AT_EMPTY_PATH);
+
+ /* Mess with executable file that's already open: */
+ /* fd + no path to a file that's been renamed */
+ rename("execveat.ephemeral", "execveat.moved");
+ fail += check_execveat(fd_ephemeral, "", AT_EMPTY_PATH);
+ /* fd + no path to a file that's been deleted */
+ unlink("execveat.moved"); /* remove the file now fd open */
+ fail += check_execveat(fd_ephemeral, "", AT_EMPTY_PATH);
+
+ /* Mess with executable file that's already open with O_PATH */
+ /* fd + no path to a file that's been deleted */
+ unlink("execveat.path.ephemeral");
+ fail += check_execveat(fd_ephemeral_path, "", AT_EMPTY_PATH);
+
+ /* Invalid argument failures */
+ fail += check_execveat_fail(fd, "", 0, ENOENT);
+ fail += check_execveat_fail(fd, NULL, AT_EMPTY_PATH, EFAULT);
+
+ /* Symlink to executable file: */
+ /* dfd + path */
+ fail += check_execveat(dot_dfd, "execveat.symlink", 0);
+ fail += check_execveat(dot_dfd_path, "execveat.symlink", 0);
+ /* absolute path */
+ fail += check_execveat(AT_FDCWD, fullname_symlink, 0);
+ /* fd + no path, even with AT_SYMLINK_NOFOLLOW (already followed) */
+ fail += check_execveat(fd_symlink, "", AT_EMPTY_PATH);
+ fail += check_execveat(fd_symlink, "",
+ AT_EMPTY_PATH|AT_SYMLINK_NOFOLLOW);
+
+ /* Symlink fails when AT_SYMLINK_NOFOLLOW set: */
+ /* dfd + path */
+ fail += check_execveat_fail(dot_dfd, "execveat.symlink",
+ AT_SYMLINK_NOFOLLOW, ELOOP);
+ fail += check_execveat_fail(dot_dfd_path, "execveat.symlink",
+ AT_SYMLINK_NOFOLLOW, ELOOP);
+ /* absolute path */
+ fail += check_execveat_fail(AT_FDCWD, fullname_symlink,
+ AT_SYMLINK_NOFOLLOW, ELOOP);
+
+ /* Non-regular file failure */
+ fail += check_execveat_fail(dot_dfd, "pipe", 0, EACCES);
+ unlink("pipe");
+
+ /* Shell script wrapping executable file: */
+ /* dfd + path */
+ fail += check_execveat(subdir_dfd, "../script", 0);
+ fail += check_execveat(dot_dfd, "script", 0);
+ fail += check_execveat(dot_dfd_path, "script", 0);
+ /* absolute path */
+ fail += check_execveat(AT_FDCWD, fullname_script, 0);
+ /* fd + no path */
+ fail += check_execveat(fd_script, "", AT_EMPTY_PATH);
+ fail += check_execveat(fd_script, "",
+ AT_EMPTY_PATH|AT_SYMLINK_NOFOLLOW);
+ /* O_CLOEXEC fd fails for a script (as script file inaccessible) */
+ fail += check_execveat_fail(fd_script_cloexec, "", AT_EMPTY_PATH,
+ ENOENT);
+ fail += check_execveat_fail(dot_dfd_cloexec, "script", 0, ENOENT);
+
+ /* Mess with script file that's already open: */
+ /* fd + no path to a file that's been renamed */
+ rename("script.ephemeral", "script.moved");
+ fail += check_execveat(fd_script_ephemeral, "", AT_EMPTY_PATH);
+ /* fd + no path to a file that's been deleted */
+ unlink("script.moved"); /* remove the file while fd open */
+ fail += check_execveat(fd_script_ephemeral, "", AT_EMPTY_PATH);
+
+ /* Rename a subdirectory in the path: */
+ rename("subdir.ephemeral", "subdir.moved");
+ fail += check_execveat(subdir_dfd_ephemeral, "../script", 0);
+ fail += check_execveat(subdir_dfd_ephemeral, "script", 0);
+ /* Remove the subdir and its contents */
+ unlink("subdir.moved/script");
+ unlink("subdir.moved");
+ /* Shell loads via deleted subdir OK because name starts with .. */
+ fail += check_execveat(subdir_dfd_ephemeral, "../script", 0);
+ fail += check_execveat_fail(subdir_dfd_ephemeral, "script", 0, ENOENT);
+
+ /* Flag values other than AT_SYMLINK_NOFOLLOW => EINVAL */
+ fail += check_execveat_fail(dot_dfd, "execveat", 0xFFFF, EINVAL);
+ /* Invalid path => ENOENT */
+ fail += check_execveat_fail(dot_dfd, "no-such-file", 0, ENOENT);
+ fail += check_execveat_fail(dot_dfd_path, "no-such-file", 0, ENOENT);
+ fail += check_execveat_fail(AT_FDCWD, "no-such-file", 0, ENOENT);
+ /* Attempt to execute directory => EACCES */
+ fail += check_execveat_fail(dot_dfd, "", AT_EMPTY_PATH, EACCES);
+ /* Attempt to execute non-executable => EACCES */
+ fail += check_execveat_fail(dot_dfd, "Makefile", 0, EACCES);
+ fail += check_execveat_fail(fd_denatured, "", AT_EMPTY_PATH, EACCES);
+ fail += check_execveat_fail(fd_denatured_path, "", AT_EMPTY_PATH,
+ EACCES);
+ /* Attempt to execute nonsense FD => EBADF */
+ fail += check_execveat_fail(99, "", AT_EMPTY_PATH, EBADF);
+ fail += check_execveat_fail(99, "execveat", 0, EBADF);
+ /* Attempt to execute relative to non-directory => ENOTDIR */
+ fail += check_execveat_fail(fd, "execveat", 0, ENOTDIR);
+
+ fail += check_execveat_pathmax(root_dfd, "execveat", 0);
+ fail += check_execveat_pathmax(root_dfd, "script", 1);
+
+ /* /proc/pid/comm gives filename by default */
+ fail += check_execveat_comm(fd, "sentinel", "execveat");
+ /* /proc/pid/comm gives argv[0] when invoked via link */
+ fail += check_execveat_comm(fd_symlink, "sentinel", "execveat");
+ /* /proc/pid/comm gives filename if NULL is passed */
+ fail += check_execveat_comm(fd, NULL, "execveat");
+
+ return fail;
+}
+
+static void prerequisites(void)
+{
+ int fd;
+ const char *script = "#!/bin/bash\nexit $*\n";
+
+ /* Create ephemeral copies of files */
+ exe_cp("execveat", "execveat.ephemeral");
+ exe_cp("execveat", "execveat.path.ephemeral");
+ exe_cp("script", "script.ephemeral");
+ mkdir("subdir.ephemeral", 0755);
+
+ fd = open("subdir.ephemeral/script", O_RDWR|O_CREAT|O_TRUNC, 0755);
+ write(fd, script, strlen(script));
+ close(fd);
+
+ mkfifo("pipe", 0755);
+}
+
+int main(int argc, char **argv)
+{
+ int ii;
+ int rc;
+ const char *verbose = getenv("VERBOSE");
+ const char *check_comm = getenv(CHECK_COMM);
+
+ if (argc >= 2 || check_comm) {
+ /*
+ * If we are invoked with an argument, or no arguments but a
+ * command to check, don't run tests.
+ */
+ const char *in_test = getenv("IN_TEST");
+
+ if (verbose) {
+ ksft_print_msg("invoked with:\n");
+ for (ii = 0; ii < argc; ii++)
+ ksft_print_msg("\t[%d]='%s\n'", ii, argv[ii]);
+ }
+
+ /* If the tests wanted us to check the command, do so. */
+ if (check_comm) {
+ /* TASK_COMM_LEN == 16 */
+ char buf[32];
+ int fd, ret;
+
+ fd = open("/proc/self/comm", O_RDONLY);
+ if (fd < 0) {
+ ksft_perror("open() comm failed");
+ exit(1);
+ }
+
+ ret = read(fd, buf, sizeof(buf));
+ if (ret < 0) {
+ ksft_perror("read() comm failed");
+ close(fd);
+ exit(1);
+ }
+ close(fd);
+
+ // trim off the \n
+ buf[ret-1] = 0;
+
+ if (strcmp(buf, check_comm)) {
+ ksft_print_msg("bad comm, got: %s expected: %s\n",
+ buf, check_comm);
+ exit(1);
+ }
+
+ exit(0);
+ }
+
+ /* Check expected environment transferred. */
+ if (!in_test || strcmp(in_test, "yes") != 0) {
+ ksft_print_msg("no IN_TEST=yes in env\n");
+ return 1;
+ }
+
+ /* Use the final argument as an exit code. */
+ rc = atoi(argv[argc - 1]);
+ exit(rc);
+ } else {
+ ksft_print_header();
+ ksft_set_plan(TESTS_EXPECTED);
+ prerequisites();
+ if (verbose)
+ envp[1] = "VERBOSE=1";
+ rc = run_tests();
+ if (rc > 0)
+ printf("%d tests failed\n", rc);
+ ksft_finished();
+ }
+
+ return rc;
+}
diff --git a/tools/testing/selftests/exec/false.c b/tools/testing/selftests/exec/false.c
new file mode 100644
index 000000000..104383ec3
--- /dev/null
+++ b/tools/testing/selftests/exec/false.c
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: GPL-2.0
+int main(void)
+{
+ return 1;
+}
diff --git a/tools/testing/selftests/exec/interp_bind.bpf.c b/tools/testing/selftests/exec/interp_bind.bpf.c
new file mode 100644
index 000000000..1ce45cca2
--- /dev/null
+++ b/tools/testing/selftests/exec/interp_bind.bpf.c
@@ -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",
+};
diff --git a/tools/testing/selftests/exec/load_address.c b/tools/testing/selftests/exec/load_address.c
new file mode 100644
index 000000000..55fd3732f
--- /dev/null
+++ b/tools/testing/selftests/exec/load_address.c
@@ -0,0 +1,105 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+#include <link.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include "kselftest.h"
+
+struct Statistics {
+ unsigned long long load_address;
+ unsigned long long alignment;
+ bool interp;
+};
+
+int ExtractStatistics(struct dl_phdr_info *info, size_t size, void *data)
+{
+ struct Statistics *stats = (struct Statistics *) data;
+ int i;
+
+ if (info->dlpi_name != NULL && info->dlpi_name[0] != '\0') {
+ // Ignore headers from other than the executable.
+ return 2;
+ }
+
+ stats->load_address = (unsigned long long) info->dlpi_addr;
+ stats->alignment = 0;
+
+ for (i = 0; i < info->dlpi_phnum; i++) {
+ unsigned long long align;
+
+ if (info->dlpi_phdr[i].p_type == PT_INTERP) {
+ stats->interp = true;
+ continue;
+ }
+
+ if (info->dlpi_phdr[i].p_type != PT_LOAD)
+ continue;
+
+ align = info->dlpi_phdr[i].p_align;
+
+ if (align > stats->alignment)
+ stats->alignment = align;
+ }
+
+ return 1; // Terminate dl_iterate_phdr.
+}
+
+int main(int argc, char **argv)
+{
+ struct Statistics extracted = { };
+ unsigned long long misalign, pow2;
+ bool interp_needed;
+ char buf[1024];
+ FILE *maps;
+ int ret;
+
+ ksft_print_header();
+ ksft_set_plan(4);
+
+ /* Dump maps file for debugging reference. */
+ maps = fopen("/proc/self/maps", "r");
+ if (!maps)
+ ksft_exit_fail_msg("FAILED: /proc/self/maps: %s\n", strerror(errno));
+ while (fgets(buf, sizeof(buf), maps)) {
+ ksft_print_msg("%s", buf);
+ }
+ fclose(maps);
+
+ /* Walk the program headers. */
+ ret = dl_iterate_phdr(ExtractStatistics, &extracted);
+ if (ret != 1)
+ ksft_exit_fail_msg("FAILED: dl_iterate_phdr\n");
+
+ /* Report our findings. */
+ ksft_print_msg("load_address=%#llx alignment=%#llx\n",
+ extracted.load_address, extracted.alignment);
+
+ /* If we're named with ".static." we expect no INTERP. */
+ interp_needed = strstr(argv[0], ".static.") == NULL;
+
+ /* Were we built as expected? */
+ ksft_test_result(interp_needed == extracted.interp,
+ "%s INTERP program header %s\n",
+ interp_needed ? "Wanted" : "Unwanted",
+ extracted.interp ? "seen" : "missing");
+
+ /* Did we find an alignment? */
+ ksft_test_result(extracted.alignment != 0,
+ "Alignment%s found\n", extracted.alignment ? "" : " NOT");
+
+ /* Is the alignment sane? */
+ pow2 = extracted.alignment & (extracted.alignment - 1);
+ ksft_test_result(pow2 == 0,
+ "Alignment is%s a power of 2: %#llx\n",
+ pow2 == 0 ? "" : " NOT", extracted.alignment);
+
+ /* Is the load address aligned? */
+ misalign = extracted.load_address & (extracted.alignment - 1);
+ ksft_test_result(misalign == 0, "Load Address is %saligned (%#llx)\n",
+ misalign ? "MIS" : "", misalign);
+
+ ksft_finished();
+}
diff --git a/tools/testing/selftests/exec/loader.bpf.c b/tools/testing/selftests/exec/loader.bpf.c
new file mode 100644
index 000000000..108e51dd4
--- /dev/null
+++ b/tools/testing/selftests/exec/loader.bpf.c
@@ -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",
+};
diff --git a/tools/testing/selftests/exec/nix_origin.bpf.c b/tools/testing/selftests/exec/nix_origin.bpf.c
new file mode 100644
index 000000000..378e22a4c
--- /dev/null
+++ b/tools/testing/selftests/exec/nix_origin.bpf.c
@@ -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",
+};
diff --git a/tools/testing/selftests/exec/non-regular.c b/tools/testing/selftests/exec/non-regular.c
new file mode 100644
index 000000000..14ac36487
--- /dev/null
+++ b/tools/testing/selftests/exec/non-regular.c
@@ -0,0 +1,196 @@
+// SPDX-License-Identifier: GPL-2.0+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/sysmacros.h>
+#include <sys/types.h>
+
+#include "kselftest_harness.h"
+
+/* Remove a file, ignoring the result if it didn't exist. */
+void rm(struct __test_metadata *_metadata, const char *pathname,
+ int is_dir)
+{
+ int rc;
+
+ if (is_dir)
+ rc = rmdir(pathname);
+ else
+ rc = unlink(pathname);
+
+ if (rc < 0) {
+ ASSERT_EQ(errno, ENOENT) {
+ TH_LOG("Not ENOENT: %s", pathname);
+ }
+ } else {
+ ASSERT_EQ(rc, 0) {
+ TH_LOG("Failed to remove: %s", pathname);
+ }
+ }
+}
+
+FIXTURE(file) {
+ char *pathname;
+ int is_dir;
+};
+
+FIXTURE_VARIANT(file)
+{
+ const char *name;
+ int expected;
+ int is_dir;
+ void (*setup)(struct __test_metadata *_metadata,
+ FIXTURE_DATA(file) *self,
+ const FIXTURE_VARIANT(file) *variant);
+ int major, minor, mode; /* for mknod() */
+};
+
+void setup_link(struct __test_metadata *_metadata,
+ FIXTURE_DATA(file) *self,
+ const FIXTURE_VARIANT(file) *variant)
+{
+ const char * const paths[] = {
+ "/bin/true",
+ "/usr/bin/true",
+ };
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(paths); i++) {
+ if (access(paths[i], X_OK) == 0) {
+ ASSERT_EQ(symlink(paths[i], self->pathname), 0);
+ return;
+ }
+ }
+ ASSERT_EQ(1, 0) {
+ TH_LOG("Could not find viable 'true' binary");
+ }
+}
+
+FIXTURE_VARIANT_ADD(file, S_IFLNK)
+{
+ .name = "S_IFLNK",
+ .expected = ELOOP,
+ .setup = setup_link,
+};
+
+void setup_dir(struct __test_metadata *_metadata,
+ FIXTURE_DATA(file) *self,
+ const FIXTURE_VARIANT(file) *variant)
+{
+ ASSERT_EQ(mkdir(self->pathname, 0755), 0);
+}
+
+FIXTURE_VARIANT_ADD(file, S_IFDIR)
+{
+ .name = "S_IFDIR",
+ .is_dir = 1,
+ .expected = EACCES,
+ .setup = setup_dir,
+};
+
+void setup_node(struct __test_metadata *_metadata,
+ FIXTURE_DATA(file) *self,
+ const FIXTURE_VARIANT(file) *variant)
+{
+ dev_t dev;
+ int rc;
+
+ dev = makedev(variant->major, variant->minor);
+ rc = mknod(self->pathname, 0755 | variant->mode, dev);
+ ASSERT_EQ(rc, 0) {
+ if (errno == EPERM)
+ SKIP(return, "Please run as root; cannot mknod(%s)",
+ variant->name);
+ }
+}
+
+FIXTURE_VARIANT_ADD(file, S_IFBLK)
+{
+ .name = "S_IFBLK",
+ .expected = EACCES,
+ .setup = setup_node,
+ /* /dev/loop0 */
+ .major = 7,
+ .minor = 0,
+ .mode = S_IFBLK,
+};
+
+FIXTURE_VARIANT_ADD(file, S_IFCHR)
+{
+ .name = "S_IFCHR",
+ .expected = EACCES,
+ .setup = setup_node,
+ /* /dev/zero */
+ .major = 1,
+ .minor = 5,
+ .mode = S_IFCHR,
+};
+
+void setup_fifo(struct __test_metadata *_metadata,
+ FIXTURE_DATA(file) *self,
+ const FIXTURE_VARIANT(file) *variant)
+{
+ ASSERT_EQ(mkfifo(self->pathname, 0755), 0);
+}
+
+FIXTURE_VARIANT_ADD(file, S_IFIFO)
+{
+ .name = "S_IFIFO",
+ .expected = EACCES,
+ .setup = setup_fifo,
+};
+
+FIXTURE_SETUP(file)
+{
+ ASSERT_GT(asprintf(&self->pathname, "%s.test", variant->name), 6);
+ self->is_dir = variant->is_dir;
+
+ rm(_metadata, self->pathname, variant->is_dir);
+ variant->setup(_metadata, self, variant);
+}
+
+FIXTURE_TEARDOWN(file)
+{
+ rm(_metadata, self->pathname, self->is_dir);
+}
+
+TEST_F(file, exec_errno)
+{
+ char * const argv[2] = { (char * const)self->pathname, NULL };
+
+ EXPECT_LT(execv(argv[0], argv), 0);
+ EXPECT_EQ(errno, variant->expected);
+}
+
+/* S_IFSOCK */
+FIXTURE(sock)
+{
+ int fd;
+};
+
+FIXTURE_SETUP(sock)
+{
+ self->fd = socket(AF_INET, SOCK_STREAM, 0);
+ ASSERT_GE(self->fd, 0);
+}
+
+FIXTURE_TEARDOWN(sock)
+{
+ if (self->fd >= 0)
+ ASSERT_EQ(close(self->fd), 0);
+}
+
+TEST_F(sock, exec_errno)
+{
+ char * const argv[2] = { " magic socket ", NULL };
+ char * const envp[1] = { NULL };
+
+ EXPECT_LT(fexecve(self->fd, argv, envp), 0);
+ EXPECT_EQ(errno, EACCES);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/exec/null-argv.c b/tools/testing/selftests/exec/null-argv.c
new file mode 100644
index 000000000..4940aee5b
--- /dev/null
+++ b/tools/testing/selftests/exec/null-argv.c
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Test that empty argvs are swapped out for a single empty string. */
+#include <stdio.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include "kselftest.h"
+
+#define FORK(exec) \
+do { \
+ pid = fork(); \
+ if (pid == 0) { \
+ /* Child */ \
+ exec; /* Some kind of exec */ \
+ perror("# " #exec); \
+ return 1; \
+ } \
+ check_result(pid, #exec); \
+} while (0)
+
+void check_result(pid_t pid, const char *msg)
+{
+ int wstatus;
+
+ if (pid == (pid_t)-1) {
+ perror("# fork");
+ ksft_test_result_fail("fork failed: %s\n", msg);
+ return;
+ }
+ if (waitpid(pid, &wstatus, 0) < 0) {
+ perror("# waitpid");
+ ksft_test_result_fail("waitpid failed: %s\n", msg);
+ return;
+ }
+ if (!WIFEXITED(wstatus)) {
+ ksft_test_result_fail("child did not exit: %s\n", msg);
+ return;
+ }
+ if (WEXITSTATUS(wstatus) != 0) {
+ ksft_test_result_fail("non-zero exit: %s\n", msg);
+ return;
+ }
+ ksft_test_result_pass("%s\n", msg);
+}
+
+int main(int argc, char *argv[], char *envp[])
+{
+ pid_t pid;
+ static char * const args[] = { NULL };
+ static char * const str[] = { "", NULL };
+
+ /* argc counting checks */
+ if (argc < 1) {
+ fprintf(stderr, "# FAIL: saw argc == 0 (old kernel?)\n");
+ return 1;
+ }
+ if (argc != 1) {
+ fprintf(stderr, "# FAIL: unknown argc (%d)\n", argc);
+ return 1;
+ }
+ if (argv[0][0] == '\0') {
+ /* Good, we found a NULL terminated string at argv[0]! */
+ return 0;
+ }
+
+ /* Test runner. */
+ ksft_print_header();
+ ksft_set_plan(5);
+
+ FORK(execve(argv[0], str, NULL));
+ FORK(execve(argv[0], NULL, NULL));
+ FORK(execve(argv[0], NULL, envp));
+ FORK(execve(argv[0], args, NULL));
+ FORK(execve(argv[0], args, envp));
+
+ ksft_exit(ksft_cnt.ksft_pass == ksft_plan);
+}
diff --git a/tools/testing/selftests/exec/recursion-depth.c b/tools/testing/selftests/exec/recursion-depth.c
new file mode 100644
index 000000000..7b5c4f6d1
--- /dev/null
+++ b/tools/testing/selftests/exec/recursion-depth.c
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2019 Alexey Dobriyan <adobriyan@gmail.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+/* Test that pointing #! script interpreter to self doesn't recurse. */
+#include <errno.h>
+#include <sched.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <sys/mount.h>
+#include <unistd.h>
+#include "kselftest.h"
+
+int main(void)
+{
+ int fd, rv;
+
+ ksft_print_header();
+ ksft_set_plan(1);
+
+ if (unshare(CLONE_NEWNS) == -1) {
+ if (errno == ENOSYS || errno == EPERM) {
+ ksft_test_result_skip("error: unshare, errno %d\n", errno);
+ ksft_finished();
+ }
+ ksft_exit_fail_perror("error: unshare");
+ }
+
+ if (mount(NULL, "/", NULL, MS_PRIVATE | MS_REC, NULL) == -1)
+ ksft_exit_fail_perror("error: mount '/'");
+
+ /* Require "exec" filesystem. */
+ if (mount(NULL, "/tmp", "ramfs", 0, NULL) == -1)
+ ksft_exit_fail_perror("error: mount ramfs");
+
+#define FILENAME "/tmp/1"
+
+ fd = creat(FILENAME, 0700);
+ if (fd == -1)
+ ksft_exit_fail_perror("error: creat");
+
+#define S "#!" FILENAME "\n"
+ if (write(fd, S, strlen(S)) != strlen(S))
+ ksft_exit_fail_perror("error: write");
+
+ close(fd);
+
+ rv = execve(FILENAME, NULL, NULL);
+ ksft_test_result(rv == -1 && errno == ELOOP,
+ "execve failed as expected (ret %d, errno %d)\n", rv, errno);
+ ksft_finished();
+}
diff --git a/tools/testing/selftests/exec/transparent.bpf.c b/tools/testing/selftests/exec/transparent.bpf.c
new file mode 100644
index 000000000..7632019eb
--- /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 <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",
+};