Linux-ARM-Kernel Archive on lore.kernel.org
 help / color / mirror / Atom feed
From: Jinjie Ruan <ruanjinjie@huawei.com>
To: <catalin.marinas@arm.com>, <will@kernel.org>, <shuah@kernel.org>,
	<linux-kernel@vger.kernel.org>,
	<linux-arm-kernel@lists.infradead.org>,
	<linux-kselftest@vger.kernel.org>
Cc: <ruanjinjie@huawei.com>
Subject: [PATCH v3 2/2] kselftest/arm64: Add testcase for SECCOMP_RET_TRACE orig_x0 bypass
Date: Tue, 28 Jul 2026 10:11:22 +0800	[thread overview]
Message-ID: <20260728021122.2640104-3-ruanjinjie@huawei.com> (raw)
In-Reply-To: <20260728021122.2640104-1-ruanjinjie@huawei.com>

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>
---
 tools/testing/selftests/arm64/abi/.gitignore  |   1 +
 tools/testing/selftests/arm64/abi/Makefile    |   2 +-
 .../arm64/abi/seccomp_ret_trace_x0_bypass.c   | 204 ++++++++++++++++++
 3 files changed, 206 insertions(+), 1 deletion(-)
 create mode 100644 tools/testing/selftests/arm64/abi/seccomp_ret_trace_x0_bypass.c

diff --git a/tools/testing/selftests/arm64/abi/.gitignore b/tools/testing/selftests/arm64/abi/.gitignore
index 39129a9907c7..491a80db9dff 100644
--- a/tools/testing/selftests/arm64/abi/.gitignore
+++ b/tools/testing/selftests/arm64/abi/.gitignore
@@ -1,5 +1,6 @@
 hwcap
 ptrace
 seccomp_ptrace_x0_bypass
+seccomp_ret_trace_x0_bypass
 syscall-abi
 tpidr2
diff --git a/tools/testing/selftests/arm64/abi/Makefile b/tools/testing/selftests/arm64/abi/Makefile
index 5a16db379bd4..a01d3806eba8 100644
--- a/tools/testing/selftests/arm64/abi/Makefile
+++ b/tools/testing/selftests/arm64/abi/Makefile
@@ -1,7 +1,7 @@
 # SPDX-License-Identifier: GPL-2.0
 # 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
 
diff --git a/tools/testing/selftests/arm64/abi/seccomp_ret_trace_x0_bypass.c b/tools/testing/selftests/arm64/abi/seccomp_ret_trace_x0_bypass.c
new file mode 100644
index 000000000000..3a69f53f4f88
--- /dev/null
+++ b/tools/testing/selftests/arm64/abi/seccomp_ret_trace_x0_bypass.c
@@ -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 = &regs, .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;
+}
-- 
2.34.1



  parent reply	other threads:[~2026-07-28  2:11 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-28  2:11 [PATCH v3 0/2] kselftest/arm64: Add two arm64 kselftests for orig_x0 issue Jinjie Ruan
2026-07-28  2:11 ` [PATCH v3 1/2] kselftest/arm64: Add seccomp ptrace x0 bypass test Jinjie Ruan
2026-08-05 15:04   ` Mark Brown
2026-08-06  8:22     ` Jinjie Ruan
2026-08-06 11:58       ` Mark Brown
2026-07-28  2:11 ` Jinjie Ruan [this message]
2026-08-02 12:12 ` [PATCH v3 0/2] kselftest/arm64: Add two arm64 kselftests for orig_x0 issue Will Deacon

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260728021122.2640104-3-ruanjinjie@huawei.com \
    --to=ruanjinjie@huawei.com \
    --cc=catalin.marinas@arm.com \
    --cc=linux-arm-kernel@lists.infradead.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=shuah@kernel.org \
    --cc=will@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox