372 lines · 9.8 KB · C
RawDownload
// 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