1 // SPDX-License-Identifier: GPL-2.0
2 /*
3  * Landlock tests - Ptrace
4  *
5  * Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net>
6  * Copyright © 2019-2020 ANSSI
7  */
8 
9 #define _GNU_SOURCE
10 #include <errno.h>
11 #include <fcntl.h>
12 #include <linux/landlock.h>
13 #include <signal.h>
14 #include <sys/prctl.h>
15 #include <sys/ptrace.h>
16 #include <sys/types.h>
17 #include <sys/wait.h>
18 #include <unistd.h>
19 
20 #include "common.h"
21 
22 static void create_domain(struct __test_metadata *const _metadata)
23 {
24 	int ruleset_fd;
25 	struct landlock_ruleset_attr ruleset_attr = {
26 		.handled_access_fs = LANDLOCK_ACCESS_FS_MAKE_BLOCK,
27 	};
28 
29 	ruleset_fd =
30 		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
31 	EXPECT_LE(0, ruleset_fd)
32 	{
33 		TH_LOG("Failed to create a ruleset: %s", strerror(errno));
34 	}
35 	EXPECT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
36 	EXPECT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
37 	EXPECT_EQ(0, close(ruleset_fd));
38 }
39 
40 static int test_ptrace_read(const pid_t pid)
41 {
42 	static const char path_template[] = "/proc/%d/environ";
43 	char procenv_path[sizeof(path_template) + 10];
44 	int procenv_path_size, fd;
45 
46 	procenv_path_size = snprintf(procenv_path, sizeof(procenv_path),
47 				     path_template, pid);
48 	if (procenv_path_size >= sizeof(procenv_path))
49 		return E2BIG;
50 
51 	fd = open(procenv_path, O_RDONLY | O_CLOEXEC);
52 	if (fd < 0)
53 		return errno;
54 	/*
55 	 * Mixing error codes from close(2) and open(2) should not lead to any
56 	 * (access type) confusion for this test.
57 	 */
58 	if (close(fd) != 0)
59 		return errno;
60 	return 0;
61 }
62 
63 /* clang-format off */
64 FIXTURE(hierarchy) {};
65 /* clang-format on */
66 
67 FIXTURE_VARIANT(hierarchy)
68 {
69 	const bool domain_both;
70 	const bool domain_parent;
71 	const bool domain_child;
72 };
73 
74 /*
75  * Test multiple tracing combinations between a parent process P1 and a child
76  * process P2.
77  *
78  * Yama's scoped ptrace is presumed disabled.  If enabled, this optional
79  * restriction is enforced in addition to any Landlock check, which means that
80  * all P2 requests to trace P1 would be denied.
81  */
82 
83 /*
84  *        No domain
85  *
86  *   P1-.               P1 -> P2 : allow
87  *       \              P2 -> P1 : allow
88  *        'P2
89  */
90 /* clang-format off */
91 FIXTURE_VARIANT_ADD(hierarchy, allow_without_domain) {
92 	/* clang-format on */
93 	.domain_both = false,
94 	.domain_parent = false,
95 	.domain_child = false,
96 };
97 
98 /*
99  *        Child domain
100  *
101  *   P1--.              P1 -> P2 : allow
102  *        \             P2 -> P1 : deny
103  *        .'-----.
104  *        |  P2  |
105  *        '------'
106  */
107 /* clang-format off */
108 FIXTURE_VARIANT_ADD(hierarchy, allow_with_one_domain) {
109 	/* clang-format on */
110 	.domain_both = false,
111 	.domain_parent = false,
112 	.domain_child = true,
113 };
114 
115 /*
116  *        Parent domain
117  * .------.
118  * |  P1  --.           P1 -> P2 : deny
119  * '------'  \          P2 -> P1 : allow
120  *            '
121  *            P2
122  */
123 /* clang-format off */
124 FIXTURE_VARIANT_ADD(hierarchy, deny_with_parent_domain) {
125 	/* clang-format on */
126 	.domain_both = false,
127 	.domain_parent = true,
128 	.domain_child = false,
129 };
130 
131 /*
132  *        Parent + child domain (siblings)
133  * .------.
134  * |  P1  ---.          P1 -> P2 : deny
135  * '------'   \         P2 -> P1 : deny
136  *         .---'--.
137  *         |  P2  |
138  *         '------'
139  */
140 /* clang-format off */
141 FIXTURE_VARIANT_ADD(hierarchy, deny_with_sibling_domain) {
142 	/* clang-format on */
143 	.domain_both = false,
144 	.domain_parent = true,
145 	.domain_child = true,
146 };
147 
148 /*
149  *         Same domain (inherited)
150  * .-------------.
151  * | P1----.     |      P1 -> P2 : allow
152  * |        \    |      P2 -> P1 : allow
153  * |         '   |
154  * |         P2  |
155  * '-------------'
156  */
157 /* clang-format off */
158 FIXTURE_VARIANT_ADD(hierarchy, allow_sibling_domain) {
159 	/* clang-format on */
160 	.domain_both = true,
161 	.domain_parent = false,
162 	.domain_child = false,
163 };
164 
165 /*
166  *         Inherited + child domain
167  * .-----------------.
168  * |  P1----.        |  P1 -> P2 : allow
169  * |         \       |  P2 -> P1 : deny
170  * |        .-'----. |
171  * |        |  P2  | |
172  * |        '------' |
173  * '-----------------'
174  */
175 /* clang-format off */
176 FIXTURE_VARIANT_ADD(hierarchy, allow_with_nested_domain) {
177 	/* clang-format on */
178 	.domain_both = true,
179 	.domain_parent = false,
180 	.domain_child = true,
181 };
182 
183 /*
184  *         Inherited + parent domain
185  * .-----------------.
186  * |.------.         |  P1 -> P2 : deny
187  * ||  P1  ----.     |  P2 -> P1 : allow
188  * |'------'    \    |
189  * |             '   |
190  * |             P2  |
191  * '-----------------'
192  */
193 /* clang-format off */
194 FIXTURE_VARIANT_ADD(hierarchy, deny_with_nested_and_parent_domain) {
195 	/* clang-format on */
196 	.domain_both = true,
197 	.domain_parent = true,
198 	.domain_child = false,
199 };
200 
201 /*
202  *         Inherited + parent and child domain (siblings)
203  * .-----------------.
204  * | .------.        |  P1 -> P2 : deny
205  * | |  P1  .        |  P2 -> P1 : deny
206  * | '------'\       |
207  * |          \      |
208  * |        .--'---. |
209  * |        |  P2  | |
210  * |        '------' |
211  * '-----------------'
212  */
213 /* clang-format off */
214 FIXTURE_VARIANT_ADD(hierarchy, deny_with_forked_domain) {
215 	/* clang-format on */
216 	.domain_both = true,
217 	.domain_parent = true,
218 	.domain_child = true,
219 };
220 
221 FIXTURE_SETUP(hierarchy)
222 {
223 }
224 
225 FIXTURE_TEARDOWN(hierarchy)
226 {
227 }
228 
229 /* Test PTRACE_TRACEME and PTRACE_ATTACH for parent and child. */
230 TEST_F(hierarchy, trace)
231 {
232 	pid_t child, parent;
233 	int status, err_proc_read;
234 	int pipe_child[2], pipe_parent[2];
235 	char buf_parent;
236 	long ret;
237 
238 	/*
239 	 * Removes all effective and permitted capabilities to not interfere
240 	 * with cap_ptrace_access_check() in case of PTRACE_MODE_FSCREDS.
241 	 */
242 	drop_caps(_metadata);
243 
244 	parent = getpid();
245 	ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
246 	ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
247 	if (variant->domain_both) {
248 		create_domain(_metadata);
249 		if (!_metadata->passed)
250 			/* Aborts before forking. */
251 			return;
252 	}
253 
254 	child = fork();
255 	ASSERT_LE(0, child);
256 	if (child == 0) {
257 		char buf_child;
258 
259 		ASSERT_EQ(0, close(pipe_parent[1]));
260 		ASSERT_EQ(0, close(pipe_child[0]));
261 		if (variant->domain_child)
262 			create_domain(_metadata);
263 
264 		/* Waits for the parent to be in a domain, if any. */
265 		ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
266 
267 		/* Tests PTRACE_ATTACH and PTRACE_MODE_READ on the parent. */
268 		err_proc_read = test_ptrace_read(parent);
269 		ret = ptrace(PTRACE_ATTACH, parent, NULL, 0);
270 		if (variant->domain_child) {
271 			EXPECT_EQ(-1, ret);
272 			EXPECT_EQ(EPERM, errno);
273 			EXPECT_EQ(EACCES, err_proc_read);
274 		} else {
275 			EXPECT_EQ(0, ret);
276 			EXPECT_EQ(0, err_proc_read);
277 		}
278 		if (ret == 0) {
279 			ASSERT_EQ(parent, waitpid(parent, &status, 0));
280 			ASSERT_EQ(1, WIFSTOPPED(status));
281 			ASSERT_EQ(0, ptrace(PTRACE_DETACH, parent, NULL, 0));
282 		}
283 
284 		/* Tests child PTRACE_TRACEME. */
285 		ret = ptrace(PTRACE_TRACEME);
286 		if (variant->domain_parent) {
287 			EXPECT_EQ(-1, ret);
288 			EXPECT_EQ(EPERM, errno);
289 		} else {
290 			EXPECT_EQ(0, ret);
291 		}
292 
293 		/*
294 		 * Signals that the PTRACE_ATTACH test is done and the
295 		 * PTRACE_TRACEME test is ongoing.
296 		 */
297 		ASSERT_EQ(1, write(pipe_child[1], ".", 1));
298 
299 		if (!variant->domain_parent) {
300 			ASSERT_EQ(0, raise(SIGSTOP));
301 		}
302 
303 		/* Waits for the parent PTRACE_ATTACH test. */
304 		ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
305 		_exit(_metadata->passed ? EXIT_SUCCESS : EXIT_FAILURE);
306 		return;
307 	}
308 
309 	ASSERT_EQ(0, close(pipe_child[1]));
310 	ASSERT_EQ(0, close(pipe_parent[0]));
311 	if (variant->domain_parent)
312 		create_domain(_metadata);
313 
314 	/* Signals that the parent is in a domain, if any. */
315 	ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
316 
317 	/*
318 	 * Waits for the child to test PTRACE_ATTACH on the parent and start
319 	 * testing PTRACE_TRACEME.
320 	 */
321 	ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1));
322 
323 	/* Tests child PTRACE_TRACEME. */
324 	if (!variant->domain_parent) {
325 		ASSERT_EQ(child, waitpid(child, &status, 0));
326 		ASSERT_EQ(1, WIFSTOPPED(status));
327 		ASSERT_EQ(0, ptrace(PTRACE_DETACH, child, NULL, 0));
328 	} else {
329 		/* The child should not be traced by the parent. */
330 		EXPECT_EQ(-1, ptrace(PTRACE_DETACH, child, NULL, 0));
331 		EXPECT_EQ(ESRCH, errno);
332 	}
333 
334 	/* Tests PTRACE_ATTACH and PTRACE_MODE_READ on the child. */
335 	err_proc_read = test_ptrace_read(child);
336 	ret = ptrace(PTRACE_ATTACH, child, NULL, 0);
337 	if (variant->domain_parent) {
338 		EXPECT_EQ(-1, ret);
339 		EXPECT_EQ(EPERM, errno);
340 		EXPECT_EQ(EACCES, err_proc_read);
341 	} else {
342 		EXPECT_EQ(0, ret);
343 		EXPECT_EQ(0, err_proc_read);
344 	}
345 	if (ret == 0) {
346 		ASSERT_EQ(child, waitpid(child, &status, 0));
347 		ASSERT_EQ(1, WIFSTOPPED(status));
348 		ASSERT_EQ(0, ptrace(PTRACE_DETACH, child, NULL, 0));
349 	}
350 
351 	/* Signals that the parent PTRACE_ATTACH test is done. */
352 	ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
353 	ASSERT_EQ(child, waitpid(child, &status, 0));
354 	if (WIFSIGNALED(status) || !WIFEXITED(status) ||
355 	    WEXITSTATUS(status) != EXIT_SUCCESS)
356 		_metadata->passed = 0;
357 }
358 
359 TEST_HARNESS_MAIN
360