Files
laptops-kernel/tools/testing/selftests/exec/binfmt_loader_payload.c
Christian Brauner 707466845c selftests/exec: test binfmt_misc loader substitution
Exercise the 'L' flag end to end. 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:

- argv exactly as the caller built it
- no AT_EXECFD
- AT_FLAGS clear
- AT_BASE set but outside its own image
- AT_PHDR/AT_ENTRY inside it
- /proc/self/{exe,comm,stat} and AT_EXECFN all describing the binary
- ETXTBSY on the running binary
- the substituted loader visible in /proc/self/maps under its real path

Magic matching pokes a marker into the ELF header's e_ident padding
(EI_PAD, offset 9), which sits inside the match window and is ignored by
kernel and loader alike. the same binary is also matched by extension.

Two cases cover the paths where the substitution does not happen. A '#!'
file that matched an 'L' entry is claimed by binfmt_script rather than by
binfmt_elf, so the staged substitute has to be released when the
interpreter replaces the file; the test opens the loader for writing
afterwards, which fails with ETXTBSY if the write denial was leaked
instead. A relative interpreter path is rejected at registration for both
'L' and 'C', neither of which may resolve one against the working
directory of whoever runs the binary.

The bpf-side BPF_BINPRM_LOADER path shares all machinery past the flag
mapping. A harness case for it can join the bpf runtime coverage of
the transparent series.

Link: https://patch.msgid.link/20260721-work-bpf-binfmt_misc-ptinterp-v2-20-e57866e4ae0f@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-07-28 16:00:11 +02:00

147 lines
4.4 KiB
C

// 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;
}