xref: /linux/tools/testing/selftests/arm64/abi/seccomp_ret_trace_x0_bypass.c (revision 67f8bc848ee31831336bd478e57d2f993551902e)
1 // SPDX-License-Identifier: GPL-2.0
2 /*
3  * Test for SECCOMP_RET_TRACE argument modification bypass
4  * via stale orig_x0 during filter re-evaluation.
5  *
6  * On arm64, syscall_get_arguments() reads the first argument from
7  * regs->orig_x0.  When a seccomp filter returns SECCOMP_RET_TRACE,
8  * ptrace may modify regs->regs[0] while orig_x0 remains unchanged.
9  * The kernel then re-evaluates the filter; if it sees the stale
10  * orig_x0, it may incorrectly allow a syscall that the tracer intended
11  * to block.
12  *
13  * This test installs a filter that:
14  *   - TRACEs write() when fd == 2
15  *   - returns ERRNO(EPERM) when fd == 1
16  *   - allows all other syscalls
17  *
18  * The child calls write(2, ...).  The parent catches the SECCOMP stop,
19  * changes x0 (fd) from 2 to 1, and resumes the child.
20  *
21  * If re-evaluation sees the old fd=2 (stale orig_x0), the filter
22  * returns TRACE again; because recheck_after_trace is true, the kernel
23  * allows the syscall to proceed.  write(1, ...) succeeds, child exits 0.
24  * -> test FAIL (bypass detected).
25  *
26  * If re-evaluation sees the new fd=1 (synced orig_x0), the filter
27  * returns ERRNO(EPERM), write fails, child exits 1.
28  * -> test PASS (no bypass).
29  *
30  * No special privileges required beyond CAP_SYS_PTRACE.
31  */
32 #include <errno.h>
33 #include <signal.h>
34 #include <stddef.h>
35 #include <stdio.h>
36 #include <stdlib.h>
37 #include <unistd.h>
38 #include <errno.h>
39 #include <sys/prctl.h>
40 #include <sys/ptrace.h>
41 #include <sys/uio.h>
42 #include <sys/wait.h>
43 #include <linux/elf.h>
44 #include <linux/filter.h>
45 #include <linux/seccomp.h>
46 #include <linux/ptrace.h>
47 #include <asm/unistd.h>
48 
49 #include "kselftest.h"
50 
51 #define PTRACE_EVENT_MASK(status) ((status) >> 16)
52 
53 #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
54 #define ARG0_OFFSET	(offsetof(struct seccomp_data, args))
55 #else
56 #define ARG0_OFFSET	(offsetof(struct seccomp_data, args) + 4)
57 #endif
58 
59 static int do_child(void)
60 {
61 	long ret;
62 
63 	if (ptrace(PTRACE_TRACEME, 0, NULL, NULL))
64 		_exit(2);
65 
66 	raise(SIGSTOP);	/* synchronize with parent */
67 
68 	/*
69 	 * Filter:
70 	 *   if syscall == write:
71 	 *     if fd == 2 -> TRACE
72 	 *     if fd == 1 -> ERRNO(EPERM)
73 	 *     else -> ALLOW
74 	 *   else -> ALLOW
75 	 */
76 	struct sock_filter filter[] = {
77 		/* Load syscall number */
78 		BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
79 		/* If not write, allow */
80 		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_write, 0, 5),
81 		/* Load first argument (fd) */
82 		BPF_STMT(BPF_LD | BPF_W | BPF_ABS, ARG0_OFFSET),
83 		/* fd == 2 ? */
84 		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 2, 0, 1),
85 		/* Yes: TRACE */
86 		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_TRACE),
87 		/* fd == 1 ? */
88 		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 1, 0, 1),
89 		/* Yes: ERRNO(EPERM) */
90 		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),
91 		/* Other fd: ALLOW */
92 		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
93 	};
94 
95 	struct sock_fprog prog = {
96 		.len = ARRAY_SIZE(filter),
97 		.filter = filter,
98 	};
99 
100 	if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
101 		_exit(3);
102 	if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog))
103 		_exit(4);
104 
105 	/*
106 	 * write(2, ...) triggers TRACE, parent changes fd to 1.
107 	 * If re-eval sees fd=1 -> ERRNO -> write fails, ret = -EPERM.
108 	 * If re-eval sees fd=2 -> TRACE again -> allowed -> write succeeds.
109 	 */
110 	ret = syscall(__NR_write, 2, "", 0);
111 	_exit(ret == 0 ? 0 : 1);
112 }
113 
114 int main(void)
115 {
116 	struct user_pt_regs regs;
117 	struct iovec iov = { .iov_base = &regs, .iov_len = sizeof(regs) };
118 	pid_t child;
119 	int status;
120 
121 	ksft_print_header();
122 	ksft_set_plan(1);
123 
124 	child = fork();
125 	if (child < 0)
126 		ksft_exit_fail_msg("fork failed: %s", strerror(errno));
127 
128 	if (!child)
129 		return do_child();
130 
131 	/* 1. Wait for initial SIGSTOP */
132 	if (waitpid(child, &status, 0) != child)
133 		ksft_exit_fail_msg("waitpid SIGSTOP");
134 	if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGSTOP)
135 		ksft_exit_fail_msg("unexpected initial stop");
136 
137 	/* 2. Enable SECCOMP ptrace events */
138 	if (ptrace(PTRACE_SETOPTIONS, child, 0, PTRACE_O_TRACESECCOMP))
139 		ksft_exit_fail_msg("PTRACE_SETOPTIONS");
140 
141 	/* 3. Continue child to hit SECCOMP stop */
142 	if (ptrace(PTRACE_CONT, child, 0, 0))
143 		ksft_exit_fail_msg("PTRACE_CONT");
144 
145 	/* 4. Wait for SECCOMP stop */
146 	while (1) {
147 		if (waitpid(child, &status, 0) != child)
148 			ksft_exit_fail_msg("waitpid SECCOMP");
149 		if (WIFEXITED(status)) {
150 			ksft_test_result_fail("child exited before SECCOMP stop\n");
151 			goto out;
152 		}
153 		if (WIFSIGNALED(status)) {
154 			ksft_test_result_fail("child killed unexpectedly\n");
155 			goto out;
156 		}
157 		if (WIFSTOPPED(status) &&
158 		    WSTOPSIG(status) == SIGTRAP &&
159 		    PTRACE_EVENT_MASK(status) == PTRACE_EVENT_SECCOMP)
160 			break;
161 		ptrace(PTRACE_CONT, child, 0, WSTOPSIG(status));
162 	}
163 
164 	/* 5. Modify x0 (fd) from 2 to 1 */
165 	if (ptrace(PTRACE_GETREGSET, child, NT_PRSTATUS, &iov))
166 		ksft_exit_fail_perror("GETREGSET");
167 	if (regs.regs[8] != __NR_write || regs.regs[0] != 2) {
168 		ksft_test_result_fail("unexpected regs: syscall=%llu, x0=%llu\n",
169 				      regs.regs[8], regs.regs[0]);
170 		goto out;
171 	}
172 	regs.regs[0] = 1;
173 	if (ptrace(PTRACE_SETREGSET, child, NT_PRSTATUS, &iov))
174 		ksft_exit_fail_perror("SETREGSET");
175 
176 	/* 6. Resume child */
177 	if (ptrace(PTRACE_CONT, child, 0, 0))
178 		ksft_exit_fail_perror("PTRACE_CONT");
179 
180 	/* 7. Reap child – must exit normally */
181 	if (waitpid(child, &status, 0) != child)
182 		ksft_exit_fail_msg("final waitpid");
183 
184 	if (!WIFEXITED(status)) {
185 		ksft_test_result_fail("child did not exit normally\n");
186 		goto out;
187 	}
188 
189 	if (WEXITSTATUS(status) != 0)
190 		ksft_test_result_pass("seccomp correctly denied modified syscall\n");
191 	else
192 		ksft_test_result_fail("write succeeded, orig_x0 bypass likely\n");
193 
194 out:
195 	if (child > 0) {
196 		kill(child, SIGKILL);
197 		waitpid(child, NULL, 0);
198 	}
199 	ksft_print_cnts();
200 	return ksft_get_fail_cnt() ? EXIT_FAILURE : EXIT_SUCCESS;
201 }
202