include/linux/entry-common.h | 3 +++ 1 file changed, 3 insertions(+)
Sashiko review pointed out the following issue.
If a thread is stopped in syscall_trace_enter() for ptrace, another
thread can install a seccomp filter with SECCOMP_FILTER_FLAG_TSYNC
(e.g., via seccomp_attach_filter()). This will successfully set
SYSCALL_WORK_SECCOMP on the stopped thread, but syscall_trace_enter()
evaluates a cached 'work' variable sampled on entry. Consequently,
the subsequent check for SYSCALL_WORK_SECCOMP misses the newly
assigned flag, and the filter is silently bypassed.
This race condition could allow an unprivileged process to execute
a prohibited system call (e.g., execve) that the newly installed filter
was intended to block, especially since the tracer might have modified
the system call number during the ptrace stop.
Fix this by re-reading the syscall_work flags after ptrace handling,
so that any new SYSCALL_WORK_SECCOMP flag set by another thread via
TSYNC during the ptrace stop is observed before the subsequent
seccomp check.
Cc: Kees Cook <kees@kernel.org>
Cc: Thomas Gleixner <tglx@kernel.org>
Cc: Andy Lutomirski <luto@amacapital.net>
Cc: Will Drewry <wad@chromium.org>
Cc: stable@vger.kernel.org
Link: https://lore.kernel.org/all/20260629132914.1135C1F000E9@smtp.kernel.org/
Fixes: 142781e108b1 ("entry: Provide generic syscall entry functionality")
Signed-off-by: Jinjie Ruan <ruanjinjie@huawei.com>
---
v2:
- Adopt lighter-weight approach suggested by Thomas: re-read syscall_work
after ptrace instead of calling seccomp_permit_syscall() unconditionally.
- Update the commit message to reflect the actual implementation.
---
include/linux/entry-common.h | 3 +++
1 file changed, 3 insertions(+)
diff --git a/include/linux/entry-common.h b/include/linux/entry-common.h
index 07d97def7dcb..299f13c78a6f 100644
--- a/include/linux/entry-common.h
+++ b/include/linux/entry-common.h
@@ -97,6 +97,9 @@ static __always_inline long syscall_trace_enter(struct pt_regs *regs, unsigned l
if (!arch_ptrace_report_syscall_permit_entry(regs) ||
(work & SYSCALL_WORK_SYSCALL_EMU))
return -1L;
+
+ /* ptrace might have changed work flags */
+ work = READ_ONCE(current_thread_info()->syscall_work);
}
/* Do seccomp after ptrace, to catch any tracer changes. */
--
2.34.1
On 7/13/2026 10:57 AM, Jinjie Ruan wrote:
> Sashiko review pointed out the following issue.
>
> If a thread is stopped in syscall_trace_enter() for ptrace, another
> thread can install a seccomp filter with SECCOMP_FILTER_FLAG_TSYNC
> (e.g., via seccomp_attach_filter()). This will successfully set
> SYSCALL_WORK_SECCOMP on the stopped thread, but syscall_trace_enter()
> evaluates a cached 'work' variable sampled on entry. Consequently,
> the subsequent check for SYSCALL_WORK_SECCOMP misses the newly
> assigned flag, and the filter is silently bypassed.
>
> This race condition could allow an unprivileged process to execute
> a prohibited system call (e.g., execve) that the newly installed filter
> was intended to block, especially since the tracer might have modified
> the system call number during the ptrace stop.
>
> Fix this by re-reading the syscall_work flags after ptrace handling,
> so that any new SYSCALL_WORK_SECCOMP flag set by another thread via
> TSYNC during the ptrace stop is observed before the subsequent
> seccomp check.
>
> Cc: Kees Cook <kees@kernel.org>
> Cc: Thomas Gleixner <tglx@kernel.org>
> Cc: Andy Lutomirski <luto@amacapital.net>
> Cc: Will Drewry <wad@chromium.org>
> Cc: stable@vger.kernel.org
> Link: https://lore.kernel.org/all/20260629132914.1135C1F000E9@smtp.kernel.org/
> Fixes: 142781e108b1 ("entry: Provide generic syscall entry functionality")
> Signed-off-by: Jinjie Ruan <ruanjinjie@huawei.com>
> ---
> v2:
> - Adopt lighter-weight approach suggested by Thomas: re-read syscall_work
> after ptrace instead of calling seccomp_permit_syscall() unconditionally.
> - Update the commit message to reflect the actual implementation.
> ---
> include/linux/entry-common.h | 3 +++
> 1 file changed, 3 insertions(+)
>
> diff --git a/include/linux/entry-common.h b/include/linux/entry-common.h
> index 07d97def7dcb..299f13c78a6f 100644
> --- a/include/linux/entry-common.h
> +++ b/include/linux/entry-common.h
> @@ -97,6 +97,9 @@ static __always_inline long syscall_trace_enter(struct pt_regs *regs, unsigned l
> if (!arch_ptrace_report_syscall_permit_entry(regs) ||
> (work & SYSCALL_WORK_SYSCALL_EMU))
> return -1L;
> +
> + /* ptrace might have changed work flags */
> + work = READ_ONCE(current_thread_info()->syscall_work);
Hi, Thomas,
This issue can be reproduced itself on x86 on v7.2-rc1 and this patch
can fix the issue.
I used DeepSeek to help write the following test method and script,
which can successfully reproduce the issue.
# Test Approach
## How the test works:
A child process is created and traced with ptrace(PTRACE_SYSCALL).
The child raises SIGSTOP to synchronise with the tracer, then calls getpid.
When the child stops at the syscall‑enter tracepoint, the tracer signals
a helper thread (via a pipe) to install a seccomp filter using
SECCOMP_FILTER_FLAG_TSYNC.
The filter kills any getpid invocation, while the child’s getpid is
still paused inside syscall_trace_enter().
After a short sleep to guarantee installation, the tracer resumes the child.
## Expected results:
Without the fix: The cached syscall_work flag does not see the newly
added SYSCALL_WORK_SECCOMP; the getpid call executes normally, the child
exits cleanly, and the test prints [tracer] child exited normally -> BYPASS.
With the fix: The re‑read of syscall_work after the ptrace stop picks up
the SYSCALL_WORK_SECCOMP flag, __secure_computing() is invoked, the
filter kills the process, and the test prints [tracer] child killed by
SIGSYS -> bug fixed.
# kernel debug patch
--- a/include/linux/entry-common.h
+++ b/include/linux/entry-common.h
@@ -96,6 +96,10 @@ static __always_inline long
syscall_trace_enter(struct pt_regs *regs, unsigned l
ret = arch_ptrace_report_syscall_entry(regs);
if (ret || (work & SYSCALL_WORK_SYSCALL_EMU))
return -1L;
+
+ pr_info("[DEBUG] After ptrace, scno: %d, SECCOMP:
0x%lx\n", syscall_get_nr(current, regs), work & SYSCALL_WORK_SECCOMP);
+ work = READ_ONCE(current_thread_info()->syscall_work);
+ pr_info("[DEBUG] After update, scno: %d, SECCOMP:
0x%lx\n", syscall_get_nr(current, regs), work & SYSCALL_WORK_SECCOMP);
}
# Test results
## baseline (Comment out "work refresh")
# ./tsync_seccomp_bypass_test
[tracer] signaling filter thread
[filter thread] seccomp filter installed
[ 10.497627] [DEBUG] After ptrace, scno: 39, SECCOMP: 0x0
[ 10.498086] [DEBUG] After update, scno: 39, SECCOMP: 0x0
[ 10.498593] [DEBUG] After ptrace, scno: 231, SECCOMP: 0x1
[ 10.499060] [DEBUG] After update, scno: 231, SECCOMP: 0x1
[tracer] child exited normally -> BYPASS
## After refresh work with the patch
# ./tsync_seccomp_bypass_test
[tracer] signaling filter thread
[filter thread] seccomp filter installed
[ 7.590580] [DEBUG] After ptrace, scno: 39, SECCOMP: 0x0
[ 7.591063] [DEBUG] After update, scno: 39, SECCOMP: 0x1
[ 7.591734] audit: type=1326 audit(1784013262.411:2):
auid=4294967295 uid=0 gid=0 ses=4294967295 subj=kernel pid=589
comm="tsync_seccomp_b" exe="/root/tsync0
[tracer] child killed by SIGSYS -> bug fixed
# Test program
cat tsync_seccomp_bypass_test.c
```
/*
* TSYNC seccomp bypass test for x86-64, using only getpid (syscall 39)
*
* Reproduces the race fixed by:
* "entry: Fix seccomp bypass after ptrace with TSYNC"
*
* Expected behavior:
* Without fix: getpid executes and child exits normally (bypass).
* With fix: child is killed by SIGSYS because filter blocks getpid.
*
* Build: gcc -o tsync_seccomp_bypass_test tsync_seccomp_bypass_test.c
* Run: ./tsync_seccomp_bypass_test
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <errno.h>
static int pipefd[2];
/* Thread 2: install TSYNC filter that kills any getpid() call */
static void *install_filter(void *arg)
{
char ch;
if (read(pipefd[0], &ch, 1) != 1) {
perror("pipe read");
pthread_exit(NULL);
}
struct sock_filter filter[] = {
/* Load syscall number */
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, 0),
/* If it is getpid (39 on x86-64), kill */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_getpid, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
/* All other syscalls allowed */
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {
.len = sizeof(filter) / sizeof(filter[0]),
.filter = filter,
};
if (syscall(__NR_seccomp, SECCOMP_SET_MODE_FILTER,
SECCOMP_FILTER_FLAG_TSYNC, &prog) == -1) {
perror("seccomp TSYNC");
} else {
fprintf(stderr, "[filter thread] seccomp filter installed\n");
}
pthread_exit(NULL);
}
int main(void)
{
if (pipe(pipefd) == -1) {
perror("pipe");
exit(1);
}
pid_t child = fork();
if (child < 0) {
perror("fork");
exit(1);
}
if (child == 0) {
/* ========== Child process ========== */
pthread_t tid;
close(pipefd[1]);
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) {
perror("prctl NO_NEW_PRIVS");
exit(1);
}
if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) == -1) {
perror("ptrace TRACEME");
exit(1);
}
if (pthread_create(&tid, NULL, install_filter, NULL) != 0) {
perror("pthread_create");
exit(1);
}
raise(SIGSTOP); /* initial sync with tracer */
/*
* Only call getpid. It will be stopped by ptrace at entry,
* the filter thread will install TSYNC during that stop.
*/
syscall(__NR_getpid);
_exit(0);
}
/* ========== Parent process (tracer) ========== */
close(pipefd[0]);
int status;
if (waitpid(child, &status, 0) == -1) {
perror("waitpid");
return 1;
}
if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGSTOP) {
fprintf(stderr, "expected initial SIGSTOP\n");
return 1;
}
ptrace(PTRACE_SETOPTIONS, child, 0, PTRACE_O_TRACESYSGOOD);
ptrace(PTRACE_SYSCALL, child, 0, 0);
int bypass = 0;
int triggered = 0;
while (1) {
if (waitpid(child, &status, 0) == -1) {
perror("waitpid");
break;
}
if (WIFEXITED(status)) {
fprintf(stderr, "[tracer] child exited normally -> BYPASS\n");
bypass = 1;
break;
}
if (WIFSIGNALED(status)) {
if (WTERMSIG(status) == SIGSYS)
fprintf(stderr, "[tracer] child killed by SIGSYS -> bug
fixed\n");
else
fprintf(stderr, "[tracer] child killed by signal %d\n",
WTERMSIG(status));
break;
}
if (WIFSTOPPED(status)) {
int sig = WSTOPSIG(status);
if (sig == (SIGTRAP | 0x80)) { /* syscall stop */
if (!triggered) {
fprintf(stderr, "[tracer] signaling filter thread\n");
char ch = 'x';
if (write(pipefd[1], &ch, 1) != 1)
perror("pipe write");
usleep(100000); /* wait for TSYNC to complete */
triggered = 1;
}
ptrace(PTRACE_SYSCALL, child, 0, 0);
} else {
ptrace(PTRACE_SYSCALL, child, 0, sig);
}
}
}
close(pipefd[1]);
return bypass ? 1 : 0;
}
```
> }
>
> /* Do seccomp after ptrace, to catch any tracer changes. */
The following commit has been merged into the core/entry branch of tip:
Commit-ID: 4a3591287fb7f808e209b4974ed337f609a2006b
Gitweb: https://git.kernel.org/tip/4a3591287fb7f808e209b4974ed337f609a2006b
Author: Jinjie Ruan <ruanjinjie@huawei.com>
AuthorDate: Mon, 13 Jul 2026 10:57:12 +08:00
Committer: Thomas Gleixner <tglx@kernel.org>
CommitterDate: Tue, 14 Jul 2026 16:34:07 +02:00
entry: Fix seccomp bypass after ptrace with TSYNC
Sashiko review pointed out the following issue.
If a thread is stopped in syscall_trace_enter() for ptrace, another
thread can install a seccomp filter with SECCOMP_FILTER_FLAG_TSYNC
(e.g., via seccomp_attach_filter()). This will successfully set
SYSCALL_WORK_SECCOMP on the stopped thread, but syscall_trace_enter()
evaluates a cached 'work' variable sampled on entry. Consequently,
the subsequent check for SYSCALL_WORK_SECCOMP misses the newly
assigned flag, and the filter is silently bypassed.
This race condition could allow an unprivileged process to execute
a prohibited system call (e.g., execve) that the newly installed filter
was intended to block, especially since the tracer might have modified
the system call number during the ptrace stop.
Fix this by re-reading the syscall_work flags after ptrace handling,
so that any new SYSCALL_WORK_SECCOMP flag set by another thread via
TSYNC during the ptrace stop is observed before the subsequent
seccomp check.
Fixes: 142781e108b1 ("entry: Provide generic syscall entry functionality")
Signed-off-by: Jinjie Ruan <ruanjinjie@huawei.com>
Signed-off-by: Thomas Gleixner <tglx@kernel.org>
Cc: stable@vger.kernel.org
Link: https://lore.kernel.org/all/20260629132914.1135C1F000E9@smtp.kernel.org/
Link: https://patch.msgid.link/20260713025712.416366-1-ruanjinjie@huawei.com
---
include/linux/entry-common.h | 3 +++
1 file changed, 3 insertions(+)
diff --git a/include/linux/entry-common.h b/include/linux/entry-common.h
index 07d97de..299f13c 100644
--- a/include/linux/entry-common.h
+++ b/include/linux/entry-common.h
@@ -97,6 +97,9 @@ static __always_inline long syscall_trace_enter(struct pt_regs *regs, unsigned l
if (!arch_ptrace_report_syscall_permit_entry(regs) ||
(work & SYSCALL_WORK_SYSCALL_EMU))
return -1L;
+
+ /* ptrace might have changed work flags */
+ work = READ_ONCE(current_thread_info()->syscall_work);
}
/* Do seccomp after ptrace, to catch any tracer changes. */
© 2016 - 2026 Red Hat, Inc.