Linux-ARM-Kernel Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v3 0/2] kselftest/arm64: Add two arm64 kselftests for orig_x0 issue
@ 2026-07-28  2:11 Jinjie Ruan
  2026-07-28  2:11 ` [PATCH v3 1/2] kselftest/arm64: Add seccomp ptrace x0 bypass test Jinjie Ruan
                   ` (2 more replies)
  0 siblings, 3 replies; 7+ messages in thread
From: Jinjie Ruan @ 2026-07-28  2:11 UTC (permalink / raw)
  To: catalin.marinas, will, shuah, linux-kernel, linux-arm-kernel,
	linux-kselftest
  Cc: ruanjinjie

Recently, a security issue related to orig_x0 was discovered and fixed
in arm64, but there are still no test cases covering it. Seccomp,
tracepoints, and audit can observe a stale value for x0 after a ptracer
or SECCOMP_RET_TRACE modifies x0.

As Kees suggested, this series adds two kselftests for arm64 that validate
the orig_x0 re-synchronisation fix in ptrace, the first one is for ptrace
update, the second one is for SECCOMP_RET_TRACE update.

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/

Changes in v3:
- Fix Big Endian issues.
- Add missing fork error check.
- Fix error BPF branch offset.
- Tighten exit status checks to ensure the child was terminated
  precisely by the expected SIGSYS signal.

Link: https://sashiko.dev/#/patchset/20260720085145.247690-1-ruanjinjie%40huawei.com

Changes in v2:
- Update the 2st testcase to cover the corner case pointed out
  by Sashiko[1].

[1]: https://sashiko.dev/#/patchset/20260716120640.6590-1-will@kernel.org

Jinjie Ruan (2):
  kselftest/arm64: Add seccomp ptrace x0 bypass test
  kselftest/arm64: Add testcase for SECCOMP_RET_TRACE orig_x0 bypass

 tools/testing/selftests/arm64/abi/.gitignore  |   2 +
 tools/testing/selftests/arm64/abi/Makefile    |   2 +-
 .../arm64/abi/seccomp_ptrace_x0_bypass.c      | 195 +++++++++++++++++
 .../arm64/abi/seccomp_ret_trace_x0_bypass.c   | 204 ++++++++++++++++++
 4 files changed, 402 insertions(+), 1 deletion(-)
 create mode 100644 tools/testing/selftests/arm64/abi/seccomp_ptrace_x0_bypass.c
 create mode 100644 tools/testing/selftests/arm64/abi/seccomp_ret_trace_x0_bypass.c

-- 
2.34.1



^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH v3 1/2] kselftest/arm64: Add seccomp ptrace x0 bypass test
  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 ` Jinjie Ruan
  2026-08-05 15:04   ` Mark Brown
  2026-07-28  2:11 ` [PATCH v3 2/2] kselftest/arm64: Add testcase for SECCOMP_RET_TRACE orig_x0 bypass Jinjie Ruan
  2026-08-02 12:12 ` [PATCH v3 0/2] kselftest/arm64: Add two arm64 kselftests for orig_x0 issue Will Deacon
  2 siblings, 1 reply; 7+ messages in thread
From: Jinjie Ruan @ 2026-07-28  2:11 UTC (permalink / raw)
  To: catalin.marinas, will, shuah, linux-kernel, linux-arm-kernel,
	linux-kselftest
  Cc: ruanjinjie

As Kees suggested, add a test that verifies that seccomp observes the
correct first argument after a ptracer modifies x0 at a syscall-enter-stop
on arm64.

The first syscall argument and the return value share register x0.
The original value is saved in orig_x0 on entry and used by
syscall_get_arguments(), but ptrace changes to x0 were not
automatically reflected there.  This test checks the kernel re-syncs
orig_x0 after a ptrace stop so that seccomp sees the modified
argument.

A seccomp filter allows write(2,...) and kills the task for any other
fd.  The tracer changes fd from 2 to 1 at entry.  If orig_x0 remains
stale, the child exits normally (bypass, test fails).  If orig_x0 is
correctly updated, the child is killed by SIGSYS (test passes).

Before the fix:
	 ./seccomp_ptrace_x0_bypass
	TAP version 13
	1..1
	not ok 1 seccomp_ptrace_x0_bypass
	# Totals: pass:0 fail:1 xfail:0 xpass:0 skip:0 error:0

After the fix:
	# ./seccomp_ptrace_x0_bypass
	TAP version 13
	1..1
	[   19.475951] audit: type=1326 audit(1784254846.284:2): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=227 comm="seccomp_ptrace_" exe="/mnt/seccomp0
	[   19.477852] audit: type=1701 audit(1784254846.284:3): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=227 comm="seccomp_ptrace_" exe="/mnt/seccomp1
	ok 1 seccomp_ptrace_x0_bypass
	# 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/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_ptrace_x0_bypass.c      | 195 ++++++++++++++++++
 3 files changed, 197 insertions(+), 1 deletion(-)
 create mode 100644 tools/testing/selftests/arm64/abi/seccomp_ptrace_x0_bypass.c

diff --git a/tools/testing/selftests/arm64/abi/.gitignore b/tools/testing/selftests/arm64/abi/.gitignore
index 44f8b80f37e3..39129a9907c7 100644
--- a/tools/testing/selftests/arm64/abi/.gitignore
+++ b/tools/testing/selftests/arm64/abi/.gitignore
@@ -1,4 +1,5 @@
 hwcap
 ptrace
+seccomp_ptrace_x0_bypass
 syscall-abi
 tpidr2
diff --git a/tools/testing/selftests/arm64/abi/Makefile b/tools/testing/selftests/arm64/abi/Makefile
index 483488f8c2ad..5a16db379bd4 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
+TEST_GEN_PROGS := hwcap ptrace syscall-abi tpidr2 seccomp_ptrace_x0_bypass
 
 include ../../lib.mk
 
diff --git a/tools/testing/selftests/arm64/abi/seccomp_ptrace_x0_bypass.c b/tools/testing/selftests/arm64/abi/seccomp_ptrace_x0_bypass.c
new file mode 100644
index 000000000000..a00621a8949c
--- /dev/null
+++ b/tools/testing/selftests/arm64/abi/seccomp_ptrace_x0_bypass.c
@@ -0,0 +1,195 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Test that seccomp, tracepoints and audit observe the correct syscall
+ * arguments after a ptracer has modified them at syscall-enter-stop.
+ *
+ * On arm64, both the first argument and the return value of a syscall
+ * are passed in register x0.  The original x0 is saved in
+ * pt_regs::orig_x0 during syscall entry and returned as the first
+ * argument by syscall_get_arguments().  Because ptrace modifications
+ * to x0 are not automatically reflected in orig_x0, seccomp, tracepoints
+ * and audit may see a stale value unless orig_x0 is explicitly
+ * re-synchronised after a ptrace stop.
+ *
+ * This test sets up a seccomp filter that allows write(2, ...) but kills
+ * the task for any other fd.  A ptracer changes the fd argument from 2
+ * to 1 at the syscall-enter stop.  If the orig_x0 re-sync works, seccomp
+ * sees the modified argument (fd=1) and kills the child with SIGSYS
+ * (test passes).  If orig_x0 is not re-synced, seccomp sees the original
+ * fd=2, the write succeeds and the child exits normally (test fails,
+ * vulnerability present).
+ */
+#include <errno.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/prctl.h>
+#include <sys/ptrace.h>
+#include <sys/uio.h>
+#include <sys/wait.h>
+#include <asm/ptrace.h>
+#include <linux/elf.h>
+#include <linux/filter.h>
+#include <linux/seccomp.h>
+
+#include "kselftest.h"
+
+#ifndef __NR_write
+#define __NR_write 64
+#endif
+
+#define EXPECTED_TESTS 1
+
+#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)
+{
+	if (ptrace(PTRACE_TRACEME, 0, NULL, NULL))
+		ksft_exit_fail_perror("PTRACE_TRACEME");
+
+	if (raise(SIGSTOP))
+		ksft_exit_fail_perror("raise(SIGSTOP)");
+
+	/*
+	 * Seccomp filter:
+	 *    If syscall is not write -> ALLOW
+	 *    If syscall is write:
+	 *	- If args[0] (fd) == 2 -> ALLOW
+	 *	- Otherwise -> KILL
+	 */
+	struct sock_filter filter[] = {
+		BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),	/* nr */
+		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_write, 0, 3),
+		BPF_STMT(BPF_LD | BPF_W | BPF_ABS, ARG0_OFFSET),	/* args[0] */
+		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 2, 1, 0),
+		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
+		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))
+		ksft_exit_fail_perror("prctl NO_NEW_PRIVS");
+
+	if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog))
+		ksft_exit_fail_perror("prctl SECCOMP");
+
+	/*
+	 * Invoke write(2, ...) while the tracer will change the first
+	 * argument (fd) from 2 to 1 at syscall entry.
+	 */
+	syscall(__NR_write, 2, NULL, 0);
+	_exit(0);
+}
+
+static int do_parent(pid_t child)
+{
+	bool bypass = false;
+	int status;
+
+	/* Wait for the initial SIGSTOP */
+	if (waitpid(child, &status, 0) != child)
+		ksft_exit_fail_msg("waitpid failed");
+
+	if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGSTOP)
+		ksft_exit_fail_msg("unexpected stop status");
+
+	if (ptrace(PTRACE_SETOPTIONS, child, 0, PTRACE_O_TRACESYSGOOD | PTRACE_O_EXITKILL))
+		ksft_exit_fail_perror("PTRACE_SETOPTIONS");
+
+	if (ptrace(PTRACE_SYSCALL, child, 0, 0))
+		ksft_exit_fail_perror("PTRACE_SYSCALL");
+
+	while (1) {
+		int sig;
+
+		if (waitpid(child, &status, 0) != child)
+			ksft_exit_fail_msg("waitpid lost child");
+
+		if (WIFEXITED(status)) {
+			/* Child exited normally – bypass succeeded */
+			bypass = true;
+			break;
+		}
+
+		if (WIFSIGNALED(status)) {
+			sig = WTERMSIG(status);
+			if (sig == SIGSYS)
+				break;
+			ksft_exit_fail_msg("child died unexpectedly from signal %d (%s)",
+					   sig, strsignal(sig));
+		}
+
+		if (!WIFSTOPPED(status))
+			ksft_exit_fail_msg("unexpected wait status");
+
+		sig = WSTOPSIG(status);
+
+		if (sig == (SIGTRAP | 0x80)) {
+			struct user_regs_struct regs;
+			struct iovec iov = {
+				.iov_base = &regs,
+				.iov_len = sizeof(regs),
+			};
+
+			if (ptrace(PTRACE_GETREGSET, child, NT_PRSTATUS, &iov))
+				ksft_exit_fail_perror("PTRACE_GETREGSET");
+
+			unsigned long syscall_nr = regs.regs[8];
+			unsigned long x0 = regs.regs[0];
+
+			/* Modify fd from 2 to 1 at write entry */
+			if (syscall_nr == __NR_write && x0 == 2) {
+				regs.regs[0] = 1;
+				if (ptrace(PTRACE_SETREGSET, child, NT_PRSTATUS, &iov))
+					ksft_exit_fail_perror("PTRACE_SETREGSET");
+			}
+
+			if (ptrace(PTRACE_SYSCALL, child, 0, 0))
+				ksft_exit_fail_perror("PTRACE_SYSCALL");
+		} else {
+			/* Forward other signals */
+			if (ptrace(PTRACE_SYSCALL, child, 0, sig))
+				ksft_exit_fail_perror("PTRACE_SYSCALL");
+		}
+	}
+
+	/* bypass == true means vulnerability exists -> test fails */
+	return bypass ? EXIT_FAILURE : EXIT_SUCCESS;
+}
+
+int main(void)
+{
+	pid_t child;
+
+	ksft_print_header();
+	ksft_set_plan(EXPECTED_TESTS);
+
+	child = fork();
+	if (child < 0)
+		ksft_exit_fail_msg("fork failed: %s", strerror(errno));
+
+	if (!child)
+		return do_child();
+
+	/*
+	 * do_parent() returns EXIT_SUCCESS if the child was killed by
+	 * SIGSYS (i.e. seccomp correctly saw the modified argument),
+	 * and EXIT_FAILURE if the child exited normally (bypass).
+	 */
+	int result = do_parent(child);
+
+	ksft_test_result(result == EXIT_SUCCESS, "seccomp_ptrace_x0_bypass\n");
+
+	ksft_print_cnts();
+	return result;
+}
-- 
2.34.1



^ permalink raw reply related	[flat|nested] 7+ messages in thread

* [PATCH v3 2/2] kselftest/arm64: Add testcase for SECCOMP_RET_TRACE orig_x0 bypass
  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-07-28  2:11 ` Jinjie Ruan
  2026-08-02 12:12 ` [PATCH v3 0/2] kselftest/arm64: Add two arm64 kselftests for orig_x0 issue Will Deacon
  2 siblings, 0 replies; 7+ messages in thread
From: Jinjie Ruan @ 2026-07-28  2:11 UTC (permalink / raw)
  To: catalin.marinas, will, shuah, linux-kernel, linux-arm-kernel,
	linux-kselftest
  Cc: ruanjinjie

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



^ permalink raw reply related	[flat|nested] 7+ messages in thread

* Re: [PATCH v3 0/2] kselftest/arm64: Add two arm64 kselftests for orig_x0 issue
  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-07-28  2:11 ` [PATCH v3 2/2] kselftest/arm64: Add testcase for SECCOMP_RET_TRACE orig_x0 bypass Jinjie Ruan
@ 2026-08-02 12:12 ` Will Deacon
  2 siblings, 0 replies; 7+ messages in thread
From: Will Deacon @ 2026-08-02 12:12 UTC (permalink / raw)
  To: catalin.marinas, shuah, linux-kernel, linux-arm-kernel,
	linux-kselftest, Jinjie Ruan
  Cc: kernel-team, Will Deacon

On Tue, 28 Jul 2026 10:11:20 +0800, Jinjie Ruan wrote:
> Recently, a security issue related to orig_x0 was discovered and fixed
> in arm64, but there are still no test cases covering it. Seccomp,
> tracepoints, and audit can observe a stale value for x0 after a ptracer
> or SECCOMP_RET_TRACE modifies x0.
> 
> As Kees suggested, this series adds two kselftests for arm64 that validate
> the orig_x0 re-synchronisation fix in ptrace, the first one is for ptrace
> update, the second one is for SECCOMP_RET_TRACE update.
> 
> [...]

Applied to arm64 (for-next/selftests), thanks!

[1/2] kselftest/arm64: Add seccomp ptrace x0 bypass test
      https://git.kernel.org/arm64/c/2fcbc4adf997
[2/2] kselftest/arm64: Add testcase for SECCOMP_RET_TRACE orig_x0 bypass
      https://git.kernel.org/arm64/c/21e37da12071

Cheers,
-- 
Will

https://fixes.arm64.dev
https://next.arm64.dev
https://will.arm64.dev


^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH v3 1/2] kselftest/arm64: Add seccomp ptrace x0 bypass test
  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
  0 siblings, 1 reply; 7+ messages in thread
From: Mark Brown @ 2026-08-05 15:04 UTC (permalink / raw)
  To: Jinjie Ruan
  Cc: catalin.marinas, will, shuah, linux-kernel, linux-arm-kernel,
	linux-kselftest, Aishwarya.TCV

[-- Attachment #1: Type: text/plain, Size: 4097 bytes --]

On Tue, Jul 28, 2026 at 10:11:21AM +0800, Jinjie Ruan wrote:
> As Kees suggested, add a test that verifies that seccomp observes the
> correct first argument after a ptracer modifies x0 at a syscall-enter-stop
> on arm64.

This change is now in -next and is breaking the build on Arm's internal
KernelCI instance, the same thing can also be seen on at least upstream
KernelCI mixed in with extra issues due to what looks like a
missing/failed headers_install - I'll report the infra aspects of that
separately:

  CC       seccomp_ptrace_x0_bypass
seccomp_ptrace_x0_bypass.c: In function ‘do_child’:
seccomp_ptrace_x0_bypass.c:67:28: error: array type has incomplete element type ‘struct sock_filter’
   67 |         struct sock_filter filter[] = {
      |                            ^~~~~~
seccomp_ptrace_x0_bypass.c:68:17: warning: implicit declaration of function ‘BPF_STMT’; did you mean ‘BPF_STX’? [-Wimplicit-function-declaration]
   68 |                 BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),  /* nr */
      |                 ^~~~~~~~
      |                 BPF_STX
seccomp_ptrace_x0_bypass.c:69:17: warning: implicit declaration of function ‘BPF_JUMP’; did you mean ‘BPF_JMP’? [-Wimplicit-function-declaration]
   69 |                 BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_write, 0, 3),
      |                 ^~~~~~~~
      |                 BPF_JMP
seccomp_ptrace_x0_bypass.c:75:16: error: variable ‘prog’ has initializer but incomplete type
   75 |         struct sock_fprog prog = {
      |                ^~~~~~~~~~
seccomp_ptrace_x0_bypass.c:76:18: error: ‘struct sock_fprog’ has no member named ‘len’
   76 |                 .len = ARRAY_SIZE(filter),
      |                  ^~~
In file included from seccomp_ptrace_x0_bypass.c:38:
/tmp/kci/linux/tools/testing/selftests/kselftest.h:64:25: warning: excess elements in struct initializer
   64 | #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
      |                         ^
seccomp_ptrace_x0_bypass.c:76:24: note: in expansion of macro ‘ARRAY_SIZE’
   76 |                 .len = ARRAY_SIZE(filter),
      |                        ^~~~~~~~~~
/tmp/kci/linux/tools/testing/selftests/kselftest.h:64:25: note: (near initialization for ‘prog’)
   64 | #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
      |                         ^
seccomp_ptrace_x0_bypass.c:76:24: note: in expansion of macro ‘ARRAY_SIZE’
   76 |                 .len = ARRAY_SIZE(filter),
      |                        ^~~~~~~~~~
seccomp_ptrace_x0_bypass.c:77:18: error: ‘struct sock_fprog’ has no member named ‘filter’
   77 |                 .filter = filter,
      |                  ^~~~~~
seccomp_ptrace_x0_bypass.c:77:27: warning: excess elements in struct initializer
   77 |                 .filter = filter,
      |                           ^~~~~~
seccomp_ptrace_x0_bypass.c:77:27: note: (near initialization for ‘prog’)
seccomp_ptrace_x0_bypass.c:75:27: error: storage size of ‘prog’ isn’t known
   75 |         struct sock_fprog prog = {
      |                           ^~~~
seccomp_ptrace_x0_bypass.c:75:27: warning: unused variable ‘prog’ [-Wunused-variable]
seccomp_ptrace_x0_bypass.c:67:28: warning: unused variable ‘filter’ [-Wunused-variable]
   67 |         struct sock_filter filter[] = {


> --- /dev/null
> +++ b/tools/testing/selftests/arm64/abi/seccomp_ptrace_x0_bypass.c
> @@ -0,0 +1,195 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Test that seccomp, tracepoints and audit observe the correct syscall
> + * arguments after a ptracer has modified them at syscall-enter-stop.

> +#include <linux/filter.h>

The relevant definitions ought to be being picked up from here, there's
a copy of a current version in tools/include and has been for a while
(since at least v6.18) but there's no Makefile update to ensure that
gets picked up.

> +#ifndef __NR_write
> +#define __NR_write 64
> +#endif

That's...  interesting.

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 488 bytes --]

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH v3 1/2] kselftest/arm64: Add seccomp ptrace x0 bypass test
  2026-08-05 15:04   ` Mark Brown
@ 2026-08-06  8:22     ` Jinjie Ruan
  2026-08-06 11:58       ` Mark Brown
  0 siblings, 1 reply; 7+ messages in thread
From: Jinjie Ruan @ 2026-08-06  8:22 UTC (permalink / raw)
  To: Mark Brown
  Cc: catalin.marinas, will, shuah, linux-kernel, linux-arm-kernel,
	linux-kselftest, Aishwarya.TCV



在 2026/8/5 23:04, Mark Brown 写道:
> On Tue, Jul 28, 2026 at 10:11:21AM +0800, Jinjie Ruan wrote:
>> As Kees suggested, add a test that verifies that seccomp observes the
>> correct first argument after a ptracer modifies x0 at a syscall-enter-stop
>> on arm64.
> 
> This change is now in -next and is breaking the build on Arm's internal
> KernelCI instance, the same thing can also be seen on at least upstream
> KernelCI mixed in with extra issues due to what looks like a
> missing/failed headers_install - I'll report the infra aspects of that
> separately:

Thank you for the report.

> 
>   CC       seccomp_ptrace_x0_bypass
> seccomp_ptrace_x0_bypass.c: In function ‘do_child’:
> seccomp_ptrace_x0_bypass.c:67:28: error: array type has incomplete element type ‘struct sock_filter’
>    67 |         struct sock_filter filter[] = {
>       |                            ^~~~~~
> seccomp_ptrace_x0_bypass.c:68:17: warning: implicit declaration of function ‘BPF_STMT’; did you mean ‘BPF_STX’? [-Wimplicit-function-declaration]
>    68 |                 BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),  /* nr */
>       |                 ^~~~~~~~
>       |                 BPF_STX
> seccomp_ptrace_x0_bypass.c:69:17: warning: implicit declaration of function ‘BPF_JUMP’; did you mean ‘BPF_JMP’? [-Wimplicit-function-declaration]
>    69 |                 BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_write, 0, 3),
>       |                 ^~~~~~~~
>       |                 BPF_JMP
> seccomp_ptrace_x0_bypass.c:75:16: error: variable ‘prog’ has initializer but incomplete type
>    75 |         struct sock_fprog prog = {
>       |                ^~~~~~~~~~
> seccomp_ptrace_x0_bypass.c:76:18: error: ‘struct sock_fprog’ has no member named ‘len’
>    76 |                 .len = ARRAY_SIZE(filter),
>       |                  ^~~
> In file included from seccomp_ptrace_x0_bypass.c:38:
> /tmp/kci/linux/tools/testing/selftests/kselftest.h:64:25: warning: excess elements in struct initializer
>    64 | #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
>       |                         ^
> seccomp_ptrace_x0_bypass.c:76:24: note: in expansion of macro ‘ARRAY_SIZE’
>    76 |                 .len = ARRAY_SIZE(filter),
>       |                        ^~~~~~~~~~
> /tmp/kci/linux/tools/testing/selftests/kselftest.h:64:25: note: (near initialization for ‘prog’)
>    64 | #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
>       |                         ^
> seccomp_ptrace_x0_bypass.c:76:24: note: in expansion of macro ‘ARRAY_SIZE’
>    76 |                 .len = ARRAY_SIZE(filter),
>       |                        ^~~~~~~~~~
> seccomp_ptrace_x0_bypass.c:77:18: error: ‘struct sock_fprog’ has no member named ‘filter’
>    77 |                 .filter = filter,
>       |                  ^~~~~~
> seccomp_ptrace_x0_bypass.c:77:27: warning: excess elements in struct initializer
>    77 |                 .filter = filter,
>       |                           ^~~~~~
> seccomp_ptrace_x0_bypass.c:77:27: note: (near initialization for ‘prog’)
> seccomp_ptrace_x0_bypass.c:75:27: error: storage size of ‘prog’ isn’t known
>    75 |         struct sock_fprog prog = {
>       |                           ^~~~
> seccomp_ptrace_x0_bypass.c:75:27: warning: unused variable ‘prog’ [-Wunused-variable]
> seccomp_ptrace_x0_bypass.c:67:28: warning: unused variable ‘filter’ [-Wunused-variable]
>    67 |         struct sock_filter filter[] = {
> 
> 
>> --- /dev/null
>> +++ b/tools/testing/selftests/arm64/abi/seccomp_ptrace_x0_bypass.c
>> @@ -0,0 +1,195 @@
>> +// SPDX-License-Identifier: GPL-2.0
>> +/*
>> + * Test that seccomp, tracepoints and audit observe the correct syscall
>> + * arguments after a ptracer has modified them at syscall-enter-stop.
> 
>> +#include <linux/filter.h>
> 
> The relevant definitions ought to be being picked up from here, there's
> a copy of a current version in tools/include and has been for a while
> (since at least v6.18) but there's no Makefile update to ensure that
> gets picked up.

Will add -I$(top_srcdir)/tools/include to CFLAGS for this test.

> 
>> +#ifndef __NR_write
>> +#define __NR_write 64
>> +#endif
> 
> That's...  interesting.

I'll remove the hardcoded __NR_write.



^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH v3 1/2] kselftest/arm64: Add seccomp ptrace x0 bypass test
  2026-08-06  8:22     ` Jinjie Ruan
@ 2026-08-06 11:58       ` Mark Brown
  0 siblings, 0 replies; 7+ messages in thread
From: Mark Brown @ 2026-08-06 11:58 UTC (permalink / raw)
  To: Jinjie Ruan
  Cc: catalin.marinas, will, shuah, linux-kernel, linux-arm-kernel,
	linux-kselftest, Aishwarya.TCV

[-- Attachment #1: Type: text/plain, Size: 695 bytes --]

On Thu, Aug 06, 2026 at 04:22:31PM +0800, Jinjie Ruan wrote:
> 在 2026/8/5 23:04, Mark Brown 写道:
> > On Tue, Jul 28, 2026 at 10:11:21AM +0800, Jinjie Ruan wrote:

> > The relevant definitions ought to be being picked up from here, there's
> > a copy of a current version in tools/include and has been for a while
> > (since at least v6.18) but there's no Makefile update to ensure that
> > gets picked up.

> Will add -I$(top_srcdir)/tools/include to CFLAGS for this test.

It turns out that's actually getting pulled in already - the issue is
that linux/filter.h doesn't include uapi/linux/filter.h and the
definition is in the uapi header.  I sent a fix for this yesterday.

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 488 bytes --]

^ permalink raw reply	[flat|nested] 7+ messages in thread

end of thread, other threads:[~2026-08-06 11:58 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH v3 2/2] kselftest/arm64: Add testcase for SECCOMP_RET_TRACE orig_x0 bypass Jinjie Ruan
2026-08-02 12:12 ` [PATCH v3 0/2] kselftest/arm64: Add two arm64 kselftests for orig_x0 issue Will Deacon

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox