Re: [PATCH v2] arm64: syscall: Ensure saved x0 is kept in-sync with tracer updates

From: Jinjie Ruan

Date: Sun Jul 19 2026 - 23:49:04 EST




On 7/18/2026 1:54 AM, Will Deacon wrote:
> On Thu, Jul 16, 2026 at 05:48:01PM +0100, Will Deacon wrote:
>> On Thu, 16 Jul 2026 13:06:39 +0100, Will Deacon wrote:
>>> When seccomp support was originally added to arm64 in a1ae65b21941
>>> ("arm64: add seccomp support"), seccomp was erroneously called _before_
>>> the ptrace syscall-enter-stop and therefore the tracer could trivially
>>> manipulate the syscall register state after the seccomp check had
>>> passed. This was subsequently fixed in a5cd110cb836 ("arm64/ptrace: run
>>> seccomp after ptrace") by moving the seccomp check after the tracer has
>>> run. Unfortunately, a decade later, that fix has been reported to be
>>> incomplete.
>>>
>>> [...]
>>
>> Applied to arm64 (for-next/fixes), thanks!
>>
>> [1/1] arm64: syscall: Ensure saved x0 is kept in-sync with tracer updates
>> https://git.kernel.org/arm64/c/e057b9477232
>
> Bah, I've had to revert this. I think Sashiko makes a good point here
> that the seccomp interaction is still broken when the filter is
> re-evaluated after the tracer stop, because that all happens inside
> secure_computing() so we don't get a chance to update 'orig_x0':
>
> https://sashiko.dev/#/patchset/20260716120640.6590-1-will@xxxxxxxxxx
>

Hi Will,

Yes, I also think the point raised by Sashiko is meaningful.

After reviewing the relevant code, I believe that the issue Sashiko
pointed out regarding the compat task also exists.

My confusion is that on arm64 compat mode, both audit and trace use
orig_x0, but the first parameter used for executing system calls is
regs->reg[0]. If x0 is modified at the system call entry point in ptrace
but orig_x0 is not modified, or if orig_x0 is modified but x0 is not,
then the first parameter for audit and the actual system call being
executed will be out of sync. Could there be any issues here? Is it the
responsibility of ptrace to ensure that orig_x0 and x0 are synchronized
at the system call entry point on arm64 compat mode?

Whether it is ptrace or the kernel, ensuring that orig_x0 and x0 are
synchronized at the entry point of a system call, I believe it is
reasonable to use orig_x0 as the first parameter of the system call
execution and pre-set an error code in advance, as this way orig_x0
always retains the latest value of x0.

diff --git a/arch/arm64/include/asm/syscall_wrapper.h
b/arch/arm64/include/asm/syscall_wrapper.h
index abb57bc54305..6b13d7c8ad95 100644
--- a/arch/arm64/include/asm/syscall_wrapper.h
+++ b/arch/arm64/include/asm/syscall_wrapper.h
@@ -12,7 +12,7 @@

#define SC_ARM64_REGS_TO_ARGS(x, ...) \
__MAP(x,__SC_ARGS \
- ,,regs->regs[0],,regs->regs[1],,regs->regs[2] \
+ ,,regs->orig_x0,,regs->regs[1],,regs->regs[2] \
,,regs->regs[3],,regs->regs[4],,regs->regs[5])

#ifdef CONFIG_COMPAT
diff --git a/arch/arm64/kernel/syscall.c b/arch/arm64/kernel/syscall.c
index 2535cae9413d..a978bab59924 100644
--- a/arch/arm64/kernel/syscall.c
+++ b/arch/arm64/kernel/syscall.c
@@ -62,6 +62,7 @@ static __always_inline void el0_svc_common(struct
pt_regs *regs, int scno, int s
unsigned long work;

regs->orig_x0 = regs->regs[0];
+ syscall_set_return_value(current, regs, -ENOSYS, 0);
regs->syscallno = scno;

/*
@@ -94,23 +95,6 @@ static __always_inline void el0_svc_common(struct
pt_regs *regs, int scno, int s

work = READ_ONCE(current_thread_info()->syscall_work);
if (unlikely(work & SYSCALL_WORK_ENTER)) {
- /*
- * The de-facto standard way to skip a system call using
ptrace
- * is to set the system call to -1 (NO_SYSCALL) and set
x0 to a
- * suitable error code for consumption by userspace.
However,
- * this cannot be distinguished from a user-issued
syscall(-1)
- * and so we must set x0 to -ENOSYS here in case the
tracer doesn't
- * issue the skip and we fall into trace_exit with x0
preserved.
- *
- * This is slightly odd because it also means that if a
tracer
- * sets the system call number to -1 but does not
initialise x0,
- * then x0 will be preserved for all system calls apart
from a
- * user-issued syscall(-1). However, requesting a skip
and not
- * setting the return value is unlikely to do anything
sensible
- * anyway.
- */
- if (scno == NO_SYSCALL)
- syscall_set_return_value(current, regs, -ENOSYS, 0);
if (!syscall_trace_enter(regs, work, scno))
goto trace_exit;


> I've got a v3 that takes a different approach, so I'll send that out
> shortly. Jinjie, thanks for sending the selftests, but maybe we can
> extend them to cover the loophole above as wel?

I will update the test cases to cover the corner case it pointed out.

Best regards,
Jinjie

>
> Will