Something went wrong. Try again.
(I'm so sorry!) Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373// 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