projects/linux·blob master · refs

372 lines · 9.8 KiB

1// SPDX-License-Identifier: GPL-2.0
2/*
3 * Test the 'L' (loader substitution) flag of binfmt_misc. A matched
4 * binary runs as the MAIN image - a fully native exec - with the
5 * registered interpreter substituted for its PT_INTERP. The payload
6 * (binfmt_loader_payload) asserts the native identity from inside.
7 *
8 * The substitute is a copy of the system loader found via our own
9 * PT_INTERP; magic matching pokes a marker into the ELF header's
10 * e_ident padding, which kernel and loader ignore.
11 *
12 * Needs root for the registration; no bpf toolchain involved.
13 */
14#define _GNU_SOURCE
15#include <elf.h>
16#include <link.h>
17#include <signal.h>
18#include <stddef.h>
19#include <stdio.h>
20#include <stdlib.h>
21#include <sys/mman.h>
22#include <sys/ptrace.h>
23#include <sys/syscall.h>
24#include <sys/wait.h>
25
26#include "binfmt_misc_common.h"
27#include "kselftest_harness.h"
28
29#define ENTRY "test_loader"
30#define INTERP_PATH "/tmp/binfmt_loader_interp"
31#define MOVED_PATH INTERP_PATH ".moved"
32#define TARGET_PATH "/tmp/binfmt_loader_target.ldrtest"
33#define STATIC_PATH "/tmp/binfmt_loader_static.ldrtest"
34#define FOREIGN_PATH "/tmp/binfmt_loader_foreign.ldrtest"
35#define SCRIPT_PATH "/tmp/binfmt_loader_script.ldrtest"
36#define M_RULE ":" ENTRY ":M:9:" LOADER_MARKER "::" INTERP_PATH ":L"
37#define E_RULE ":" ENTRY ":E::ldrtest::" INTERP_PATH ":L"
38#define FL_RULE ":" ENTRY ":E::ldrtest::" INTERP_PATH ":FL"
39
40/* Execute the binary from an inaccessible O_CLOEXEC memfd. */
41static int run_memfd(const char *path)
42{
43 int status;
44 pid_t pid;
45
46 pid = fork();
47 if (pid == 0) {
48 char *argv[] = { PAYLOAD_ARGV0, PAYLOAD_ARG1, PAYLOAD_ARG2, NULL };
49 char buf[4096];
50 int in, mfd;
51 ssize_t n;
52
53 mfd = memfd_create("loader-test", MFD_CLOEXEC);
54 in = open(path, O_RDONLY);
55 if (mfd < 0 || in < 0)
56 _exit(125);
57 while ((n = read(in, buf, sizeof(buf))) > 0)
58 if (write(mfd, buf, n) != n)
59 _exit(125);
60 close(in);
61 setenv("BINFMT_TEST_MEMFD", "1", 1);
62 unsetenv("BINFMT_TEST_BINARY");
63 syscall(SYS_execveat, mfd, "", argv, environ, AT_EMPTY_PATH);
64 _exit(126);
65 }
66 if (pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
67 return -1;
68 return WEXITSTATUS(status);
69}
70
71/*
72 * The differentiator against the transparent mode: at PTRACE_EVENT_EXEC
73 * the identity is already complete - exe, auxv and the stat code markers
74 * are mutually consistent with no window a debugger could observe.
75 */
76static int ptrace_probe(const char *target)
77{
78 unsigned long auxv[2 * 64], base = 0, entry = 0, at_flags = 0;
79 unsigned long start_code = 0, end_code = 0;
80 int status, fd, execfd_seen = 0, failed = 0;
81 char path[64], buf[PATH_MAX];
82 ssize_t n;
83 pid_t pid;
84 int i;
85
86 pid = fork();
87 if (pid == 0) {
88 ptrace(PTRACE_TRACEME, 0, NULL, NULL);
89 raise(SIGSTOP);
90 execl(target, PAYLOAD_ARGV0, PAYLOAD_ARG1, PAYLOAD_ARG2, (char *)NULL);
91 _exit(126);
92 }
93 if (pid < 0)
94 return -1;
95 if (waitpid(pid, &status, 0) != pid || !WIFSTOPPED(status))
96 goto fail_kill;
97 if (ptrace(PTRACE_SETOPTIONS, pid, NULL, (void *)PTRACE_O_TRACEEXEC))
98 goto fail_kill;
99 if (ptrace(PTRACE_CONT, pid, NULL, NULL))
100 goto fail_kill;
101 if (waitpid(pid, &status, 0) != pid || !WIFSTOPPED(status) ||
102 status >> 8 != (SIGTRAP | (PTRACE_EVENT_EXEC << 8))) {
103 fprintf(stderr, "no exec stop (status %#x)\n", status);
104 goto fail_kill;
105 }
106
107 snprintf(path, sizeof(path), "/proc/%d/exe", pid);
108 n = readlink(path, buf, sizeof(buf) - 1);
109 if (n <= 0) {
110 failed = 1;
111 } else {
112 buf[n] = '\0';
113 if (strcmp(buf, target)) {
114 fprintf(stderr, "exe at exec stop: %s\n", buf);
115 failed = 1;
116 }
117 }
118
119 snprintf(path, sizeof(path), "/proc/%d/auxv", pid);
120 fd = open(path, O_RDONLY);
121 if (fd < 0) {
122 n = -1;
123 } else {
124 n = read(fd, auxv, sizeof(auxv));
125 close(fd);
126 }
127 if (n <= 0) {
128 failed = 1;
129 n = 0;
130 }
131 for (i = 0; i + 1 < (int)(n / sizeof(unsigned long)); i += 2) {
132 switch (auxv[i]) {
133 case AT_BASE:
134 base = auxv[i + 1];
135 break;
136 case AT_ENTRY:
137 entry = auxv[i + 1];
138 break;
139 case AT_FLAGS:
140 at_flags = auxv[i + 1];
141 break;
142 case AT_EXECFD:
143 execfd_seen = 1;
144 break;
145 }
146 }
147
148 if (stat_codes(pid, &start_code, &end_code))
149 failed = 1;
150
151 if (!base || execfd_seen || at_flags) {
152 fprintf(stderr, "auxv at exec stop not native\n");
153 failed = 1;
154 }
155 if (!start_code || entry < start_code || entry >= end_code) {
156 fprintf(stderr, "auxv/stat inconsistent at exec stop\n");
157 failed = 1;
158 }
159
160 if (ptrace(PTRACE_CONT, pid, NULL, NULL))
161 goto fail_kill;
162 if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status) ||
163 WEXITSTATUS(status))
164 failed = 1;
165 return failed ? -1 : 0;
166
167fail_kill:
168 kill(pid, SIGKILL);
169 waitpid(pid, &status, 0);
170 return -1;
171}
172
173FIXTURE(loader) {
174 bool have_static;
175};
176
177FIXTURE_SETUP(loader)
178{
179 unsigned short foreign_machine = 0xdead;
180 char src[PATH_MAX], loader[PATH_MAX];
181
182 if (getuid() != 0)
183 SKIP(return, "test must be run as root");
184 if (!binfmt_misc_available())
185 SKIP(return, "no binfmt_misc");
186 if (find_loader(loader, sizeof(loader)))
187 SKIP(return, "cannot determine own PT_INTERP");
188
189 ASSERT_EQ(copy_file(loader, INTERP_PATH), 0);
190
191 ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_loader_payload"), 0);
192 ASSERT_EQ(copy_file(src, TARGET_PATH), 0);
193 ASSERT_EQ(patch_file(TARGET_PATH, EI_PAD, LOADER_MARKER,
194 strlen(LOADER_MARKER)), 0);
195
196 /* The same payload with a machine type this kernel cannot load. */
197 ASSERT_EQ(copy_file(src, FOREIGN_PATH), 0);
198 ASSERT_EQ(patch_file(FOREIGN_PATH, EI_PAD, LOADER_MARKER,
199 strlen(LOADER_MARKER)), 0);
200 ASSERT_EQ(patch_file(FOREIGN_PATH, offsetof(ElfW(Ehdr), e_machine),
201 &foreign_machine, sizeof(foreign_machine)), 0);
202
203 self->have_static =
204 artifact_path(src, sizeof(src), "binfmt_loader_payload_static") == 0 &&
205 copy_file(src, STATIC_PATH) == 0;
206
207 setenv("BINFMT_TEST_BINARY", TARGET_PATH, 1);
208 setenv("BINFMT_TEST_INTERP", INTERP_PATH, 1);
209
210 /* Everything below needs the flag; find out once. */
211 if (write_reg(E_RULE)) {
212 ASSERT_EQ(errno, EINVAL);
213 SKIP(return, "kernel without the 'L' flag");
214 }
215 unregister(ENTRY);
216}
217
218FIXTURE_TEARDOWN(loader)
219{
220 unregister(ENTRY);
221 if (access(MOVED_PATH, F_OK) == 0)
222 rename(MOVED_PATH, INTERP_PATH);
223 unlink(TARGET_PATH);
224 unlink(STATIC_PATH);
225 unlink(FOREIGN_PATH);
226 unlink(SCRIPT_PATH);
227 unlink(INTERP_PATH);
228}
229
230/* Grammar sanity check: the same entry without 'L' has to register. */
231TEST_F(loader, plain_entry_registers)
232{
233 ASSERT_EQ(write_reg(":" ENTRY ":E::ldrtest::" INTERP_PATH ":"), 0);
234}
235
236/* 'L' is a native exec: every classic-dispatch flag is rejected. */
237TEST_F(loader, rejects_classic_flags)
238{
239 static const char * const combos[] = { "LT", "LP", "LC", "LO" };
240 char rule[PATH_MAX];
241 unsigned int i;
242
243 for (i = 0; i < ARRAY_SIZE(combos); i++) {
244 int rc;
245
246 snprintf(rule, sizeof(rule),
247 ":" ENTRY ":E::ldrtest::" INTERP_PATH ":%s", combos[i]);
248 rc = write_reg(rule);
249 EXPECT_EQ(rc, -1)
250 TH_LOG("'%s' was not rejected", combos[i]);
251 if (rc == 0) {
252 unregister(ENTRY);
253 continue;
254 }
255 EXPECT_EQ(errno, EINVAL);
256 }
257}
258
259/*
260 * Without 'F' the interpreter is opened when the binary is executed, so a
261 * relative path would be resolved against the caller's working directory.
262 */
263TEST_F(loader, rejects_relative_interpreter)
264{
265 static const char * const flags[] = { "L", "C" };
266 char rule[PATH_MAX];
267 unsigned int i;
268
269 for (i = 0; i < ARRAY_SIZE(flags); i++) {
270 int rc;
271
272 snprintf(rule, sizeof(rule),
273 ":" ENTRY ":E::ldrtest::binfmt_loader_interp:%s",
274 flags[i]);
275 rc = write_reg(rule);
276 EXPECT_EQ(rc, -1)
277 TH_LOG("'%s' accepted a relative interpreter", flags[i]);
278 if (rc == 0) {
279 unregister(ENTRY);
280 continue;
281 }
282 EXPECT_EQ(errno, EINVAL);
283 }
284}
285
286TEST_F(loader, extension_matched)
287{
288 ASSERT_EQ(write_reg(E_RULE), 0);
289 EXPECT_EQ(run_payload(TARGET_PATH), 0);
290}
291
292TEST_F(loader, magic_matched)
293{
294 ASSERT_EQ(write_reg(M_RULE), 0);
295 EXPECT_EQ(run_payload(TARGET_PATH), 0);
296}
297
298/*
299 * The differentiator against the transparent mode: at PTRACE_EVENT_EXEC the
300 * identity is already complete, with no window a debugger could observe.
301 */
302TEST_F(loader, exec_stop_consistency)
303{
304 ASSERT_EQ(write_reg(E_RULE), 0);
305 EXPECT_EQ(ptrace_probe(TARGET_PATH), 0);
306}
307
308/* A binary without PT_INTERP drops the override and runs natively. */
309TEST_F(loader, static_binary_runs_natively)
310{
311 if (!self->have_static)
312 SKIP(return, "no static payload built");
313
314 ASSERT_EQ(write_reg(E_RULE), 0);
315 setenv("BINFMT_TEST_BINARY", STATIC_PATH, 1);
316 setenv("BINFMT_TEST_STATIC", "1", 1);
317 EXPECT_EQ(run_payload(STATIC_PATH), 0);
318 unsetenv("BINFMT_TEST_STATIC");
319 setenv("BINFMT_TEST_BINARY", TARGET_PATH, 1);
320}
321
322/*
323 * A '#!' file that matched an 'L' entry is claimed by binfmt_script, which
324 * sits ahead of binfmt_elf. The substitute the entry staged has to be
325 * released when the interpreter replaces the file, not leaked.
326 */
327TEST_F(loader, script_claims_the_file)
328{
329 static const char script[] = "#!/bin/sh\nexit 0\n";
330 int fd;
331
332 unlink(SCRIPT_PATH);
333 fd = open(SCRIPT_PATH, O_WRONLY | O_CREAT | O_EXCL, 0755);
334 ASSERT_GE(fd, 0);
335 ASSERT_EQ(write(fd, script, sizeof(script) - 1),
336 (ssize_t)sizeof(script) - 1);
337 ASSERT_EQ(close(fd), 0);
338
339 ASSERT_EQ(write_reg(E_RULE), 0);
340 EXPECT_EQ(run_payload(SCRIPT_PATH), 0);
341
342 /* A leaked substitute keeps its write denial on the loader. */
343 fd = open(INTERP_PATH, O_WRONLY);
344 EXPECT_GE(fd, 0)
345 TH_LOG("loader still write denied (errno %d)", errno);
346 if (fd >= 0)
347 close(fd);
348}
349
350/* Nothing needs the binary's path, so an inaccessible fd works. */
351TEST_F(loader, inaccessible_memfd)
352{
353 ASSERT_EQ(write_reg(M_RULE), 0);
354 EXPECT_EQ(run_memfd(TARGET_PATH), 0);
355}
356
357/* The whole exec of a wrong-arch binary fails as if unhandled. */
358TEST_F(loader, foreign_arch_enoexec)
359{
360 ASSERT_EQ(write_reg(M_RULE), 0);
361 EXPECT_EQ(run_payload(FOREIGN_PATH), RUN_ENOEXEC);
362}
363
364/* 'F' pre-opens the substitute, so it survives losing its path. */
365TEST_F(loader, fixed_interpreter_survives_rename)
366{
367 ASSERT_EQ(write_reg(FL_RULE), 0);
368 ASSERT_EQ(rename(INTERP_PATH, MOVED_PATH), 0);
369 EXPECT_EQ(run_payload(TARGET_PATH), 0);
370}
371
372TEST_HARNESS_MAIN