xref: /linux/tools/testing/selftests/arm64/abi/seccomp_ptrace_x0_bypass.c (revision 85cdaca6970028bf6f544c355c90035586836ddf)
1 // SPDX-License-Identifier: GPL-2.0
2 /*
3  * Test that seccomp, tracepoints and audit observe the correct syscall
4  * arguments after a ptracer has modified them at syscall-enter-stop.
5  *
6  * On arm64, both the first argument and the return value of a syscall
7  * are passed in register x0.  The original x0 is saved in
8  * pt_regs::orig_x0 during syscall entry and returned as the first
9  * argument by syscall_get_arguments().  Because ptrace modifications
10  * to x0 are not automatically reflected in orig_x0, seccomp, tracepoints
11  * and audit may see a stale value unless orig_x0 is explicitly
12  * re-synchronised after a ptrace stop.
13  *
14  * This test sets up a seccomp filter that allows write(2, ...) but kills
15  * the task for any other fd.  A ptracer changes the fd argument from 2
16  * to 1 at the syscall-enter stop.  If the orig_x0 re-sync works, seccomp
17  * sees the modified argument (fd=1) and kills the child with SIGSYS
18  * (test passes).  If orig_x0 is not re-synced, seccomp sees the original
19  * fd=2, the write succeeds and the child exits normally (test fails,
20  * vulnerability present).
21  */
22 #include <errno.h>
23 #include <stdbool.h>
24 #include <stddef.h>
25 #include <stdio.h>
26 #include <stdlib.h>
27 #include <string.h>
28 #include <unistd.h>
29 #include <sys/prctl.h>
30 #include <sys/ptrace.h>
31 #include <sys/uio.h>
32 #include <sys/wait.h>
33 #include <asm/ptrace.h>
34 #include <linux/elf.h>
35 #include <linux/filter.h>
36 #include <linux/seccomp.h>
37 #include <asm/unistd.h>
38 
39 #include "kselftest.h"
40 
41 #define EXPECTED_TESTS 1
42 
43 #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
44 #define ARG0_OFFSET	(offsetof(struct seccomp_data, args))
45 #else
46 #define ARG0_OFFSET	(offsetof(struct seccomp_data, args) + 4)
47 #endif
48 
49 static int do_child(void)
50 {
51 	if (ptrace(PTRACE_TRACEME, 0, NULL, NULL))
52 		ksft_exit_fail_perror("PTRACE_TRACEME");
53 
54 	if (raise(SIGSTOP))
55 		ksft_exit_fail_perror("raise(SIGSTOP)");
56 
57 	/*
58 	 * Seccomp filter:
59 	 *    If syscall is not write -> ALLOW
60 	 *    If syscall is write:
61 	 *	- If args[0] (fd) == 2 -> ALLOW
62 	 *	- Otherwise -> KILL
63 	 */
64 	struct sock_filter filter[] = {
65 		BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),	/* nr */
66 		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_write, 0, 3),
67 		BPF_STMT(BPF_LD | BPF_W | BPF_ABS, ARG0_OFFSET),	/* args[0] */
68 		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 2, 1, 0),
69 		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
70 		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
71 	};
72 	struct sock_fprog prog = {
73 		.len = ARRAY_SIZE(filter),
74 		.filter = filter,
75 	};
76 
77 	if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
78 		ksft_exit_fail_perror("prctl NO_NEW_PRIVS");
79 
80 	if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog))
81 		ksft_exit_fail_perror("prctl SECCOMP");
82 
83 	/*
84 	 * Invoke write(2, ...) while the tracer will change the first
85 	 * argument (fd) from 2 to 1 at syscall entry.
86 	 */
87 	syscall(__NR_write, 2, NULL, 0);
88 	_exit(0);
89 }
90 
91 static int do_parent(pid_t child)
92 {
93 	bool bypass = false;
94 	int status;
95 
96 	/* Wait for the initial SIGSTOP */
97 	if (waitpid(child, &status, 0) != child)
98 		ksft_exit_fail_msg("waitpid failed");
99 
100 	if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGSTOP)
101 		ksft_exit_fail_msg("unexpected stop status");
102 
103 	if (ptrace(PTRACE_SETOPTIONS, child, 0, PTRACE_O_TRACESYSGOOD | PTRACE_O_EXITKILL))
104 		ksft_exit_fail_perror("PTRACE_SETOPTIONS");
105 
106 	if (ptrace(PTRACE_SYSCALL, child, 0, 0))
107 		ksft_exit_fail_perror("PTRACE_SYSCALL");
108 
109 	while (1) {
110 		int sig;
111 
112 		if (waitpid(child, &status, 0) != child)
113 			ksft_exit_fail_msg("waitpid lost child");
114 
115 		if (WIFEXITED(status)) {
116 			/* Child exited normally – bypass succeeded */
117 			bypass = true;
118 			break;
119 		}
120 
121 		if (WIFSIGNALED(status)) {
122 			sig = WTERMSIG(status);
123 			if (sig == SIGSYS)
124 				break;
125 			ksft_exit_fail_msg("child died unexpectedly from signal %d (%s)",
126 					   sig, strsignal(sig));
127 		}
128 
129 		if (!WIFSTOPPED(status))
130 			ksft_exit_fail_msg("unexpected wait status");
131 
132 		sig = WSTOPSIG(status);
133 
134 		if (sig == (SIGTRAP | 0x80)) {
135 			struct user_regs_struct regs;
136 			struct iovec iov = {
137 				.iov_base = &regs,
138 				.iov_len = sizeof(regs),
139 			};
140 
141 			if (ptrace(PTRACE_GETREGSET, child, NT_PRSTATUS, &iov))
142 				ksft_exit_fail_perror("PTRACE_GETREGSET");
143 
144 			unsigned long syscall_nr = regs.regs[8];
145 			unsigned long x0 = regs.regs[0];
146 
147 			/* Modify fd from 2 to 1 at write entry */
148 			if (syscall_nr == __NR_write && x0 == 2) {
149 				regs.regs[0] = 1;
150 				if (ptrace(PTRACE_SETREGSET, child, NT_PRSTATUS, &iov))
151 					ksft_exit_fail_perror("PTRACE_SETREGSET");
152 			}
153 
154 			if (ptrace(PTRACE_SYSCALL, child, 0, 0))
155 				ksft_exit_fail_perror("PTRACE_SYSCALL");
156 		} else {
157 			/* Forward other signals */
158 			if (ptrace(PTRACE_SYSCALL, child, 0, sig))
159 				ksft_exit_fail_perror("PTRACE_SYSCALL");
160 		}
161 	}
162 
163 	/* bypass == true means vulnerability exists -> test fails */
164 	return bypass ? EXIT_FAILURE : EXIT_SUCCESS;
165 }
166 
167 int main(void)
168 {
169 	pid_t child;
170 
171 	ksft_print_header();
172 	ksft_set_plan(EXPECTED_TESTS);
173 
174 	child = fork();
175 	if (child < 0)
176 		ksft_exit_fail_msg("fork failed: %s", strerror(errno));
177 
178 	if (!child)
179 		return do_child();
180 
181 	/*
182 	 * do_parent() returns EXIT_SUCCESS if the child was killed by
183 	 * SIGSYS (i.e. seccomp correctly saw the modified argument),
184 	 * and EXIT_FAILURE if the child exited normally (bypass).
185 	 */
186 	int result = do_parent(child);
187 
188 	ksft_test_result(result == EXIT_SUCCESS, "seccomp_ptrace_x0_bypass\n");
189 
190 	ksft_print_cnts();
191 	return result;
192 }
193