1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /*
3  * Author: Aleksa Sarai <cyphar@cyphar.com>
4  * Copyright (C) 2018-2019 SUSE LLC.
5  */
6 
7 #define _GNU_SOURCE
8 #include <errno.h>
9 #include <fcntl.h>
10 #include <sched.h>
11 #include <sys/stat.h>
12 #include <sys/types.h>
13 #include <sys/mount.h>
14 #include <sys/mman.h>
15 #include <sys/prctl.h>
16 #include <signal.h>
17 #include <stdio.h>
18 #include <stdlib.h>
19 #include <stdbool.h>
20 #include <string.h>
21 #include <syscall.h>
22 #include <limits.h>
23 #include <unistd.h>
24 
25 #include "../kselftest.h"
26 #include "helpers.h"
27 
28 /* Construct a test directory with the following structure:
29  *
30  * root/
31  * |-- a/
32  * |   `-- c/
33  * `-- b/
34  */
setup_testdir(void)35 int setup_testdir(void)
36 {
37 	int dfd;
38 	char dirname[] = "/tmp/ksft-openat2-rename-attack.XXXXXX";
39 
40 	/* Make the top-level directory. */
41 	if (!mkdtemp(dirname))
42 		ksft_exit_fail_msg("setup_testdir: failed to create tmpdir\n");
43 	dfd = open(dirname, O_PATH | O_DIRECTORY);
44 	if (dfd < 0)
45 		ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n");
46 
47 	E_mkdirat(dfd, "a", 0755);
48 	E_mkdirat(dfd, "b", 0755);
49 	E_mkdirat(dfd, "a/c", 0755);
50 
51 	return dfd;
52 }
53 
54 /* Swap @dirfd/@a and @dirfd/@b constantly. Parent must kill this process. */
spawn_attack(int dirfd,char * a,char * b)55 pid_t spawn_attack(int dirfd, char *a, char *b)
56 {
57 	pid_t child = fork();
58 	if (child != 0)
59 		return child;
60 
61 	/* If the parent (the test process) dies, kill ourselves too. */
62 	E_prctl(PR_SET_PDEATHSIG, SIGKILL);
63 
64 	/* Swap @a and @b. */
65 	for (;;)
66 		renameat2(dirfd, a, dirfd, b, RENAME_EXCHANGE);
67 	exit(1);
68 }
69 
70 #define NUM_RENAME_TESTS 2
71 #define ROUNDS 400000
72 
flagname(int resolve)73 const char *flagname(int resolve)
74 {
75 	switch (resolve) {
76 	case RESOLVE_IN_ROOT:
77 		return "RESOLVE_IN_ROOT";
78 	case RESOLVE_BENEATH:
79 		return "RESOLVE_BENEATH";
80 	}
81 	return "(unknown)";
82 }
83 
test_rename_attack(int resolve)84 void test_rename_attack(int resolve)
85 {
86 	int dfd, afd;
87 	pid_t child;
88 	void (*resultfn)(const char *msg, ...) = ksft_test_result_pass;
89 	int escapes = 0, other_errs = 0, exdevs = 0, eagains = 0, successes = 0;
90 
91 	struct open_how how = {
92 		.flags = O_PATH,
93 		.resolve = resolve,
94 	};
95 
96 	if (!openat2_supported) {
97 		how.resolve = 0;
98 		ksft_print_msg("openat2(2) unsupported -- using openat(2) instead\n");
99 	}
100 
101 	dfd = setup_testdir();
102 	afd = openat(dfd, "a", O_PATH);
103 	if (afd < 0)
104 		ksft_exit_fail_msg("test_rename_attack: failed to open 'a'\n");
105 
106 	child = spawn_attack(dfd, "a/c", "b");
107 
108 	for (int i = 0; i < ROUNDS; i++) {
109 		int fd;
110 		char *victim_path = "c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../..";
111 
112 		if (openat2_supported)
113 			fd = sys_openat2(afd, victim_path, &how);
114 		else
115 			fd = sys_openat(afd, victim_path, &how);
116 
117 		if (fd < 0) {
118 			if (fd == -EAGAIN)
119 				eagains++;
120 			else if (fd == -EXDEV)
121 				exdevs++;
122 			else if (fd == -ENOENT)
123 				escapes++; /* escaped outside and got ENOENT... */
124 			else
125 				other_errs++; /* unexpected error */
126 		} else {
127 			if (fdequal(fd, afd, NULL))
128 				successes++;
129 			else
130 				escapes++; /* we got an unexpected fd */
131 		}
132 		close(fd);
133 	}
134 
135 	if (escapes > 0)
136 		resultfn = ksft_test_result_fail;
137 	ksft_print_msg("non-escapes: EAGAIN=%d EXDEV=%d E<other>=%d success=%d\n",
138 		       eagains, exdevs, other_errs, successes);
139 	resultfn("rename attack with %s (%d runs, got %d escapes)\n",
140 		 flagname(resolve), ROUNDS, escapes);
141 
142 	/* Should be killed anyway, but might as well make sure. */
143 	E_kill(child, SIGKILL);
144 }
145 
146 #define NUM_TESTS NUM_RENAME_TESTS
147 
main(int argc,char ** argv)148 int main(int argc, char **argv)
149 {
150 	ksft_print_header();
151 	ksft_set_plan(NUM_TESTS);
152 
153 	test_rename_attack(RESOLVE_BENEATH);
154 	test_rename_attack(RESOLVE_IN_ROOT);
155 
156 	if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0)
157 		ksft_exit_fail();
158 	else
159 		ksft_exit_pass();
160 }
161