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 | /* Copied from security/yama/yama_lsm.c */ |
23 | #define YAMA_SCOPE_DISABLED 0 |
24 | #define YAMA_SCOPE_RELATIONAL 1 |
25 | #define YAMA_SCOPE_CAPABILITY 2 |
26 | #define YAMA_SCOPE_NO_ATTACH 3 |
27 | |
28 | static void create_domain(struct __test_metadata *const _metadata) |
29 | { |
30 | int ruleset_fd; |
31 | struct landlock_ruleset_attr ruleset_attr = { |
32 | .handled_access_fs = LANDLOCK_ACCESS_FS_MAKE_BLOCK, |
33 | }; |
34 | |
35 | ruleset_fd = |
36 | landlock_create_ruleset(attr: &ruleset_attr, size: sizeof(ruleset_attr), flags: 0); |
37 | EXPECT_LE(0, ruleset_fd) |
38 | { |
39 | TH_LOG("Failed to create a ruleset: %s" , strerror(errno)); |
40 | } |
41 | EXPECT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); |
42 | EXPECT_EQ(0, landlock_restrict_self(ruleset_fd, 0)); |
43 | EXPECT_EQ(0, close(ruleset_fd)); |
44 | } |
45 | |
46 | static int test_ptrace_read(const pid_t pid) |
47 | { |
48 | static const char path_template[] = "/proc/%d/environ" ; |
49 | char procenv_path[sizeof(path_template) + 10]; |
50 | int procenv_path_size, fd; |
51 | |
52 | procenv_path_size = snprintf(procenv_path, sizeof(procenv_path), |
53 | path_template, pid); |
54 | if (procenv_path_size >= sizeof(procenv_path)) |
55 | return E2BIG; |
56 | |
57 | fd = open(procenv_path, O_RDONLY | O_CLOEXEC); |
58 | if (fd < 0) |
59 | return errno; |
60 | /* |
61 | * Mixing error codes from close(2) and open(2) should not lead to any |
62 | * (access type) confusion for this test. |
63 | */ |
64 | if (close(fd) != 0) |
65 | return errno; |
66 | return 0; |
67 | } |
68 | |
69 | static int get_yama_ptrace_scope(void) |
70 | { |
71 | int ret; |
72 | char buf[2] = {}; |
73 | const int fd = open("/proc/sys/kernel/yama/ptrace_scope" , O_RDONLY); |
74 | |
75 | if (fd < 0) |
76 | return 0; |
77 | |
78 | if (read(fd, buf, 1) < 0) { |
79 | close(fd); |
80 | return -1; |
81 | } |
82 | |
83 | ret = atoi(buf); |
84 | close(fd); |
85 | return ret; |
86 | } |
87 | |
88 | /* clang-format off */ |
89 | FIXTURE(hierarchy) {}; |
90 | /* clang-format on */ |
91 | |
92 | FIXTURE_VARIANT(hierarchy) |
93 | { |
94 | const bool domain_both; |
95 | const bool domain_parent; |
96 | const bool domain_child; |
97 | }; |
98 | |
99 | /* |
100 | * Test multiple tracing combinations between a parent process P1 and a child |
101 | * process P2. |
102 | * |
103 | * Yama's scoped ptrace is presumed disabled. If enabled, this optional |
104 | * restriction is enforced in addition to any Landlock check, which means that |
105 | * all P2 requests to trace P1 would be denied. |
106 | */ |
107 | |
108 | /* |
109 | * No domain |
110 | * |
111 | * P1-. P1 -> P2 : allow |
112 | * \ P2 -> P1 : allow |
113 | * 'P2 |
114 | */ |
115 | /* clang-format off */ |
116 | FIXTURE_VARIANT_ADD(hierarchy, allow_without_domain) { |
117 | /* clang-format on */ |
118 | .domain_both = false, |
119 | .domain_parent = false, |
120 | .domain_child = false, |
121 | }; |
122 | |
123 | /* |
124 | * Child domain |
125 | * |
126 | * P1--. P1 -> P2 : allow |
127 | * \ P2 -> P1 : deny |
128 | * .'-----. |
129 | * | P2 | |
130 | * '------' |
131 | */ |
132 | /* clang-format off */ |
133 | FIXTURE_VARIANT_ADD(hierarchy, allow_with_one_domain) { |
134 | /* clang-format on */ |
135 | .domain_both = false, |
136 | .domain_parent = false, |
137 | .domain_child = true, |
138 | }; |
139 | |
140 | /* |
141 | * Parent domain |
142 | * .------. |
143 | * | P1 --. P1 -> P2 : deny |
144 | * '------' \ P2 -> P1 : allow |
145 | * ' |
146 | * P2 |
147 | */ |
148 | /* clang-format off */ |
149 | FIXTURE_VARIANT_ADD(hierarchy, deny_with_parent_domain) { |
150 | /* clang-format on */ |
151 | .domain_both = false, |
152 | .domain_parent = true, |
153 | .domain_child = false, |
154 | }; |
155 | |
156 | /* |
157 | * Parent + child domain (siblings) |
158 | * .------. |
159 | * | P1 ---. P1 -> P2 : deny |
160 | * '------' \ P2 -> P1 : deny |
161 | * .---'--. |
162 | * | P2 | |
163 | * '------' |
164 | */ |
165 | /* clang-format off */ |
166 | FIXTURE_VARIANT_ADD(hierarchy, deny_with_sibling_domain) { |
167 | /* clang-format on */ |
168 | .domain_both = false, |
169 | .domain_parent = true, |
170 | .domain_child = true, |
171 | }; |
172 | |
173 | /* |
174 | * Same domain (inherited) |
175 | * .-------------. |
176 | * | P1----. | P1 -> P2 : allow |
177 | * | \ | P2 -> P1 : allow |
178 | * | ' | |
179 | * | P2 | |
180 | * '-------------' |
181 | */ |
182 | /* clang-format off */ |
183 | FIXTURE_VARIANT_ADD(hierarchy, allow_sibling_domain) { |
184 | /* clang-format on */ |
185 | .domain_both = true, |
186 | .domain_parent = false, |
187 | .domain_child = false, |
188 | }; |
189 | |
190 | /* |
191 | * Inherited + child domain |
192 | * .-----------------. |
193 | * | P1----. | P1 -> P2 : allow |
194 | * | \ | P2 -> P1 : deny |
195 | * | .-'----. | |
196 | * | | P2 | | |
197 | * | '------' | |
198 | * '-----------------' |
199 | */ |
200 | /* clang-format off */ |
201 | FIXTURE_VARIANT_ADD(hierarchy, allow_with_nested_domain) { |
202 | /* clang-format on */ |
203 | .domain_both = true, |
204 | .domain_parent = false, |
205 | .domain_child = true, |
206 | }; |
207 | |
208 | /* |
209 | * Inherited + parent domain |
210 | * .-----------------. |
211 | * |.------. | P1 -> P2 : deny |
212 | * || P1 ----. | P2 -> P1 : allow |
213 | * |'------' \ | |
214 | * | ' | |
215 | * | P2 | |
216 | * '-----------------' |
217 | */ |
218 | /* clang-format off */ |
219 | FIXTURE_VARIANT_ADD(hierarchy, deny_with_nested_and_parent_domain) { |
220 | /* clang-format on */ |
221 | .domain_both = true, |
222 | .domain_parent = true, |
223 | .domain_child = false, |
224 | }; |
225 | |
226 | /* |
227 | * Inherited + parent and child domain (siblings) |
228 | * .-----------------. |
229 | * | .------. | P1 -> P2 : deny |
230 | * | | P1 . | P2 -> P1 : deny |
231 | * | '------'\ | |
232 | * | \ | |
233 | * | .--'---. | |
234 | * | | P2 | | |
235 | * | '------' | |
236 | * '-----------------' |
237 | */ |
238 | /* clang-format off */ |
239 | FIXTURE_VARIANT_ADD(hierarchy, deny_with_forked_domain) { |
240 | /* clang-format on */ |
241 | .domain_both = true, |
242 | .domain_parent = true, |
243 | .domain_child = true, |
244 | }; |
245 | |
246 | FIXTURE_SETUP(hierarchy) |
247 | { |
248 | } |
249 | |
250 | FIXTURE_TEARDOWN(hierarchy) |
251 | { |
252 | } |
253 | |
254 | /* Test PTRACE_TRACEME and PTRACE_ATTACH for parent and child. */ |
255 | TEST_F(hierarchy, trace) |
256 | { |
257 | pid_t child, parent; |
258 | int status, err_proc_read; |
259 | int pipe_child[2], pipe_parent[2]; |
260 | int yama_ptrace_scope; |
261 | char buf_parent; |
262 | long ret; |
263 | bool can_read_child, can_trace_child, can_read_parent, can_trace_parent; |
264 | |
265 | yama_ptrace_scope = get_yama_ptrace_scope(); |
266 | ASSERT_LE(0, yama_ptrace_scope); |
267 | |
268 | if (yama_ptrace_scope > YAMA_SCOPE_DISABLED) |
269 | TH_LOG("Incomplete tests due to Yama restrictions (scope %d)" , |
270 | yama_ptrace_scope); |
271 | |
272 | /* |
273 | * can_read_child is true if a parent process can read its child |
274 | * process, which is only the case when the parent process is not |
275 | * isolated from the child with a dedicated Landlock domain. |
276 | */ |
277 | can_read_child = !variant->domain_parent; |
278 | |
279 | /* |
280 | * can_trace_child is true if a parent process can trace its child |
281 | * process. This depends on two conditions: |
282 | * - The parent process is not isolated from the child with a dedicated |
283 | * Landlock domain. |
284 | * - Yama allows tracing children (up to YAMA_SCOPE_RELATIONAL). |
285 | */ |
286 | can_trace_child = can_read_child && |
287 | yama_ptrace_scope <= YAMA_SCOPE_RELATIONAL; |
288 | |
289 | /* |
290 | * can_read_parent is true if a child process can read its parent |
291 | * process, which is only the case when the child process is not |
292 | * isolated from the parent with a dedicated Landlock domain. |
293 | */ |
294 | can_read_parent = !variant->domain_child; |
295 | |
296 | /* |
297 | * can_trace_parent is true if a child process can trace its parent |
298 | * process. This depends on two conditions: |
299 | * - The child process is not isolated from the parent with a dedicated |
300 | * Landlock domain. |
301 | * - Yama is disabled (YAMA_SCOPE_DISABLED). |
302 | */ |
303 | can_trace_parent = can_read_parent && |
304 | yama_ptrace_scope <= YAMA_SCOPE_DISABLED; |
305 | |
306 | /* |
307 | * Removes all effective and permitted capabilities to not interfere |
308 | * with cap_ptrace_access_check() in case of PTRACE_MODE_FSCREDS. |
309 | */ |
310 | drop_caps(_metadata); |
311 | |
312 | parent = getpid(); |
313 | ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC)); |
314 | ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); |
315 | if (variant->domain_both) { |
316 | create_domain(_metadata); |
317 | if (!__test_passed(metadata: _metadata)) |
318 | /* Aborts before forking. */ |
319 | return; |
320 | } |
321 | |
322 | child = fork(); |
323 | ASSERT_LE(0, child); |
324 | if (child == 0) { |
325 | char buf_child; |
326 | |
327 | ASSERT_EQ(0, close(pipe_parent[1])); |
328 | ASSERT_EQ(0, close(pipe_child[0])); |
329 | if (variant->domain_child) |
330 | create_domain(_metadata); |
331 | |
332 | /* Waits for the parent to be in a domain, if any. */ |
333 | ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1)); |
334 | |
335 | /* Tests PTRACE_MODE_READ on the parent. */ |
336 | err_proc_read = test_ptrace_read(pid: parent); |
337 | if (can_read_parent) { |
338 | EXPECT_EQ(0, err_proc_read); |
339 | } else { |
340 | EXPECT_EQ(EACCES, err_proc_read); |
341 | } |
342 | |
343 | /* Tests PTRACE_ATTACH on the parent. */ |
344 | ret = ptrace(PTRACE_ATTACH, parent, NULL, 0); |
345 | if (can_trace_parent) { |
346 | EXPECT_EQ(0, ret); |
347 | } else { |
348 | EXPECT_EQ(-1, ret); |
349 | EXPECT_EQ(EPERM, errno); |
350 | } |
351 | if (ret == 0) { |
352 | ASSERT_EQ(parent, waitpid(parent, &status, 0)); |
353 | ASSERT_EQ(1, WIFSTOPPED(status)); |
354 | ASSERT_EQ(0, ptrace(PTRACE_DETACH, parent, NULL, 0)); |
355 | } |
356 | |
357 | /* Tests child PTRACE_TRACEME. */ |
358 | ret = ptrace(PTRACE_TRACEME); |
359 | if (can_trace_child) { |
360 | EXPECT_EQ(0, ret); |
361 | } else { |
362 | EXPECT_EQ(-1, ret); |
363 | EXPECT_EQ(EPERM, errno); |
364 | } |
365 | |
366 | /* |
367 | * Signals that the PTRACE_ATTACH test is done and the |
368 | * PTRACE_TRACEME test is ongoing. |
369 | */ |
370 | ASSERT_EQ(1, write(pipe_child[1], "." , 1)); |
371 | |
372 | if (can_trace_child) { |
373 | ASSERT_EQ(0, raise(SIGSTOP)); |
374 | } |
375 | |
376 | /* Waits for the parent PTRACE_ATTACH test. */ |
377 | ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1)); |
378 | _exit(_metadata->exit_code); |
379 | return; |
380 | } |
381 | |
382 | ASSERT_EQ(0, close(pipe_child[1])); |
383 | ASSERT_EQ(0, close(pipe_parent[0])); |
384 | if (variant->domain_parent) |
385 | create_domain(_metadata); |
386 | |
387 | /* Signals that the parent is in a domain, if any. */ |
388 | ASSERT_EQ(1, write(pipe_parent[1], "." , 1)); |
389 | |
390 | /* |
391 | * Waits for the child to test PTRACE_ATTACH on the parent and start |
392 | * testing PTRACE_TRACEME. |
393 | */ |
394 | ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1)); |
395 | |
396 | /* Tests child PTRACE_TRACEME. */ |
397 | if (can_trace_child) { |
398 | ASSERT_EQ(child, waitpid(child, &status, 0)); |
399 | ASSERT_EQ(1, WIFSTOPPED(status)); |
400 | ASSERT_EQ(0, ptrace(PTRACE_DETACH, child, NULL, 0)); |
401 | } else { |
402 | /* The child should not be traced by the parent. */ |
403 | EXPECT_EQ(-1, ptrace(PTRACE_DETACH, child, NULL, 0)); |
404 | EXPECT_EQ(ESRCH, errno); |
405 | } |
406 | |
407 | /* Tests PTRACE_MODE_READ on the child. */ |
408 | err_proc_read = test_ptrace_read(pid: child); |
409 | if (can_read_child) { |
410 | EXPECT_EQ(0, err_proc_read); |
411 | } else { |
412 | EXPECT_EQ(EACCES, err_proc_read); |
413 | } |
414 | |
415 | /* Tests PTRACE_ATTACH on the child. */ |
416 | ret = ptrace(PTRACE_ATTACH, child, NULL, 0); |
417 | if (can_trace_child) { |
418 | EXPECT_EQ(0, ret); |
419 | } else { |
420 | EXPECT_EQ(-1, ret); |
421 | EXPECT_EQ(EPERM, errno); |
422 | } |
423 | |
424 | if (ret == 0) { |
425 | ASSERT_EQ(child, waitpid(child, &status, 0)); |
426 | ASSERT_EQ(1, WIFSTOPPED(status)); |
427 | ASSERT_EQ(0, ptrace(PTRACE_DETACH, child, NULL, 0)); |
428 | } |
429 | |
430 | /* Signals that the parent PTRACE_ATTACH test is done. */ |
431 | ASSERT_EQ(1, write(pipe_parent[1], "." , 1)); |
432 | ASSERT_EQ(child, waitpid(child, &status, 0)); |
433 | |
434 | if (WIFSIGNALED(status) || !WIFEXITED(status) || |
435 | WEXITSTATUS(status) != EXIT_SUCCESS) |
436 | _metadata->exit_code = KSFT_FAIL; |
437 | } |
438 | |
439 | TEST_HARNESS_MAIN |
440 | |