mirror of
https://github.com/torvalds/linux.git
synced 2026-09-22 12:44:03 +02:00
kselftest/arm64: Add testcase for SECCOMP_RET_TRACE orig_x0 bypass
Add a selftest that verifies the kernel re-evaluates a seccomp filter with the correct (ptrace-modified) first argument after a SECCOMP_RET_TRACE stop. On arm64, syscall_get_arguments() reads the first argument from orig_x0, which may be stale if the tracer modified regs->regs[0] but orig_x0 was not synced. This can cause the filter to see an old argument and incorrectly allow a syscall that it should have rejected. The child installs a filter that: - TRACEs write() when fd == 2 - returns ERRNO(EPERM) when fd == 1 The parent catches the SECCOMP event, changes x0 (fd) from 2 to 1, and resumes the child. If the seccomp re-evaluation sees the stale orig_x0 (fd=2) the filter returns TRACE again and the kernel (with recheck_after_trace=true) allows the syscall to proceed – write succeeds and the child exits 0. If the seccomp re-evaluation sees the new value (fd=1) the filter returns ERRNO(EPERM), write fails and the child exits non-zero. The test passes only when the write fails (child exit != 0). Before the fix: # ./seccomp_ret_trace_x0_bypass TAP version 13 1..1 not ok 1 write succeeded, orig_x0 bypass likely # Totals: pass:0 fail:1 xfail:0 xpass:0 skip:0 error:0 After the fix: # ./seccomp_ret_trace_x0_bypass TAP version 13 1..1 ok 1 seccomp correctly denied modified syscall # Totals: pass:1 fail:0 xfail:0 xpass:0 skip:0 error:0 Cc: Kees Cook <kees@kernel.org> Cc: Will Deacon <will@kernel.org> Cc: Catalin Marinas <catalin.marinas@arm.com> Cc: Mark Rutland <mark.rutland@arm.com> Link: https://lore.kernel.org/all/20260717182758.17111-1-will@kernel.org/ Link: https://lore.kernel.org/all/20260716120640.6590-1-will@kernel.org/ Link: https://lore.kernel.org/all/202607152004.DEA95D63@keescook/ Suggested-by: Kees Cook <kees@kernel.org> Signed-off-by: Jinjie Ruan <ruanjinjie@huawei.com> Signed-off-by: Will Deacon <will@kernel.org>
This commit is contained in:
parent
2fcbc4adf9
commit
21e37da120
1
tools/testing/selftests/arm64/abi/.gitignore
vendored
1
tools/testing/selftests/arm64/abi/.gitignore
vendored
|
|
@ -1,5 +1,6 @@
|
||||||
hwcap
|
hwcap
|
||||||
ptrace
|
ptrace
|
||||||
seccomp_ptrace_x0_bypass
|
seccomp_ptrace_x0_bypass
|
||||||
|
seccomp_ret_trace_x0_bypass
|
||||||
syscall-abi
|
syscall-abi
|
||||||
tpidr2
|
tpidr2
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# SPDX-License-Identifier: GPL-2.0
|
# SPDX-License-Identifier: GPL-2.0
|
||||||
# Copyright (C) 2021 ARM Limited
|
# Copyright (C) 2021 ARM Limited
|
||||||
|
|
||||||
TEST_GEN_PROGS := hwcap ptrace syscall-abi tpidr2 seccomp_ptrace_x0_bypass
|
TEST_GEN_PROGS := hwcap ptrace syscall-abi tpidr2 seccomp_ptrace_x0_bypass seccomp_ret_trace_x0_bypass
|
||||||
|
|
||||||
include ../../lib.mk
|
include ../../lib.mk
|
||||||
|
|
||||||
|
|
|
||||||
204
tools/testing/selftests/arm64/abi/seccomp_ret_trace_x0_bypass.c
Normal file
204
tools/testing/selftests/arm64/abi/seccomp_ret_trace_x0_bypass.c
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
// SPDX-License-Identifier: GPL-2.0
|
||||||
|
/*
|
||||||
|
* Test for SECCOMP_RET_TRACE argument modification bypass
|
||||||
|
* via stale orig_x0 during filter re-evaluation.
|
||||||
|
*
|
||||||
|
* On arm64, syscall_get_arguments() reads the first argument from
|
||||||
|
* regs->orig_x0. When a seccomp filter returns SECCOMP_RET_TRACE,
|
||||||
|
* ptrace may modify regs->regs[0] while orig_x0 remains unchanged.
|
||||||
|
* The kernel then re-evaluates the filter; if it sees the stale
|
||||||
|
* orig_x0, it may incorrectly allow a syscall that the tracer intended
|
||||||
|
* to block.
|
||||||
|
*
|
||||||
|
* This test installs a filter that:
|
||||||
|
* - TRACEs write() when fd == 2
|
||||||
|
* - returns ERRNO(EPERM) when fd == 1
|
||||||
|
* - allows all other syscalls
|
||||||
|
*
|
||||||
|
* The child calls write(2, ...). The parent catches the SECCOMP stop,
|
||||||
|
* changes x0 (fd) from 2 to 1, and resumes the child.
|
||||||
|
*
|
||||||
|
* If re-evaluation sees the old fd=2 (stale orig_x0), the filter
|
||||||
|
* returns TRACE again; because recheck_after_trace is true, the kernel
|
||||||
|
* allows the syscall to proceed. write(1, ...) succeeds, child exits 0.
|
||||||
|
* -> test FAIL (bypass detected).
|
||||||
|
*
|
||||||
|
* If re-evaluation sees the new fd=1 (synced orig_x0), the filter
|
||||||
|
* returns ERRNO(EPERM), write fails, child exits 1.
|
||||||
|
* -> test PASS (no bypass).
|
||||||
|
*
|
||||||
|
* No special privileges required beyond CAP_SYS_PTRACE.
|
||||||
|
*/
|
||||||
|
#include <errno.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <sys/prctl.h>
|
||||||
|
#include <sys/ptrace.h>
|
||||||
|
#include <sys/uio.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <linux/elf.h>
|
||||||
|
#include <linux/filter.h>
|
||||||
|
#include <linux/seccomp.h>
|
||||||
|
#include <linux/ptrace.h>
|
||||||
|
|
||||||
|
#include "kselftest.h"
|
||||||
|
|
||||||
|
#ifndef __NR_write
|
||||||
|
#define __NR_write 64
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define PTRACE_EVENT_MASK(status) ((status) >> 16)
|
||||||
|
|
||||||
|
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||||
|
#define ARG0_OFFSET (offsetof(struct seccomp_data, args))
|
||||||
|
#else
|
||||||
|
#define ARG0_OFFSET (offsetof(struct seccomp_data, args) + 4)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static int do_child(void)
|
||||||
|
{
|
||||||
|
long ret;
|
||||||
|
|
||||||
|
if (ptrace(PTRACE_TRACEME, 0, NULL, NULL))
|
||||||
|
_exit(2);
|
||||||
|
|
||||||
|
raise(SIGSTOP); /* synchronize with parent */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Filter:
|
||||||
|
* if syscall == write:
|
||||||
|
* if fd == 2 -> TRACE
|
||||||
|
* if fd == 1 -> ERRNO(EPERM)
|
||||||
|
* else -> ALLOW
|
||||||
|
* else -> ALLOW
|
||||||
|
*/
|
||||||
|
struct sock_filter filter[] = {
|
||||||
|
/* Load syscall number */
|
||||||
|
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
|
||||||
|
/* If not write, allow */
|
||||||
|
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_write, 0, 5),
|
||||||
|
/* Load first argument (fd) */
|
||||||
|
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, ARG0_OFFSET),
|
||||||
|
/* fd == 2 ? */
|
||||||
|
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 2, 0, 1),
|
||||||
|
/* Yes: TRACE */
|
||||||
|
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_TRACE),
|
||||||
|
/* fd == 1 ? */
|
||||||
|
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 1, 0, 1),
|
||||||
|
/* Yes: ERRNO(EPERM) */
|
||||||
|
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),
|
||||||
|
/* Other fd: ALLOW */
|
||||||
|
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
|
||||||
|
};
|
||||||
|
|
||||||
|
struct sock_fprog prog = {
|
||||||
|
.len = ARRAY_SIZE(filter),
|
||||||
|
.filter = filter,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
|
||||||
|
_exit(3);
|
||||||
|
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog))
|
||||||
|
_exit(4);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* write(2, ...) triggers TRACE, parent changes fd to 1.
|
||||||
|
* If re-eval sees fd=1 -> ERRNO -> write fails, ret = -EPERM.
|
||||||
|
* If re-eval sees fd=2 -> TRACE again -> allowed -> write succeeds.
|
||||||
|
*/
|
||||||
|
ret = syscall(__NR_write, 2, "", 0);
|
||||||
|
_exit(ret == 0 ? 0 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
struct user_pt_regs regs;
|
||||||
|
struct iovec iov = { .iov_base = ®s, .iov_len = sizeof(regs) };
|
||||||
|
pid_t child;
|
||||||
|
int status;
|
||||||
|
|
||||||
|
ksft_print_header();
|
||||||
|
ksft_set_plan(1);
|
||||||
|
|
||||||
|
child = fork();
|
||||||
|
if (child < 0)
|
||||||
|
ksft_exit_fail_msg("fork failed: %s", strerror(errno));
|
||||||
|
|
||||||
|
if (!child)
|
||||||
|
return do_child();
|
||||||
|
|
||||||
|
/* 1. Wait for initial SIGSTOP */
|
||||||
|
if (waitpid(child, &status, 0) != child)
|
||||||
|
ksft_exit_fail_msg("waitpid SIGSTOP");
|
||||||
|
if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGSTOP)
|
||||||
|
ksft_exit_fail_msg("unexpected initial stop");
|
||||||
|
|
||||||
|
/* 2. Enable SECCOMP ptrace events */
|
||||||
|
if (ptrace(PTRACE_SETOPTIONS, child, 0, PTRACE_O_TRACESECCOMP))
|
||||||
|
ksft_exit_fail_msg("PTRACE_SETOPTIONS");
|
||||||
|
|
||||||
|
/* 3. Continue child to hit SECCOMP stop */
|
||||||
|
if (ptrace(PTRACE_CONT, child, 0, 0))
|
||||||
|
ksft_exit_fail_msg("PTRACE_CONT");
|
||||||
|
|
||||||
|
/* 4. Wait for SECCOMP stop */
|
||||||
|
while (1) {
|
||||||
|
if (waitpid(child, &status, 0) != child)
|
||||||
|
ksft_exit_fail_msg("waitpid SECCOMP");
|
||||||
|
if (WIFEXITED(status)) {
|
||||||
|
ksft_test_result_fail("child exited before SECCOMP stop\n");
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
if (WIFSIGNALED(status)) {
|
||||||
|
ksft_test_result_fail("child killed unexpectedly\n");
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
if (WIFSTOPPED(status) &&
|
||||||
|
WSTOPSIG(status) == SIGTRAP &&
|
||||||
|
PTRACE_EVENT_MASK(status) == PTRACE_EVENT_SECCOMP)
|
||||||
|
break;
|
||||||
|
ptrace(PTRACE_CONT, child, 0, WSTOPSIG(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 5. Modify x0 (fd) from 2 to 1 */
|
||||||
|
if (ptrace(PTRACE_GETREGSET, child, NT_PRSTATUS, &iov))
|
||||||
|
ksft_exit_fail_perror("GETREGSET");
|
||||||
|
if (regs.regs[8] != __NR_write || regs.regs[0] != 2) {
|
||||||
|
ksft_test_result_fail("unexpected regs: syscall=%llu, x0=%llu\n",
|
||||||
|
regs.regs[8], regs.regs[0]);
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
regs.regs[0] = 1;
|
||||||
|
if (ptrace(PTRACE_SETREGSET, child, NT_PRSTATUS, &iov))
|
||||||
|
ksft_exit_fail_perror("SETREGSET");
|
||||||
|
|
||||||
|
/* 6. Resume child */
|
||||||
|
if (ptrace(PTRACE_CONT, child, 0, 0))
|
||||||
|
ksft_exit_fail_perror("PTRACE_CONT");
|
||||||
|
|
||||||
|
/* 7. Reap child – must exit normally */
|
||||||
|
if (waitpid(child, &status, 0) != child)
|
||||||
|
ksft_exit_fail_msg("final waitpid");
|
||||||
|
|
||||||
|
if (!WIFEXITED(status)) {
|
||||||
|
ksft_test_result_fail("child did not exit normally\n");
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WEXITSTATUS(status) != 0)
|
||||||
|
ksft_test_result_pass("seccomp correctly denied modified syscall\n");
|
||||||
|
else
|
||||||
|
ksft_test_result_fail("write succeeded, orig_x0 bypass likely\n");
|
||||||
|
|
||||||
|
out:
|
||||||
|
if (child > 0) {
|
||||||
|
kill(child, SIGKILL);
|
||||||
|
waitpid(child, NULL, 0);
|
||||||
|
}
|
||||||
|
ksft_print_cnts();
|
||||||
|
return ksft_get_fail_cnt() ? EXIT_FAILURE : EXIT_SUCCESS;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user