mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Jinjie Ruan <ruanjinjie@huawei.com>
To: <tglx@kernel.org>, <peterz@infradead.org>, <luto@kernel.org>,
	<kees@kernel.org>, <linux-kernel@vger.kernel.org>
Subject: Re: [PATCH v2] entry: Fix seccomp bypass after ptrace with TSYNC
Date: Tue, 14 Jul 2026 15:36:36 +0800	[thread overview]
Message-ID: <157aa098-a263-4f4f-b0fc-108701480303@huawei.com> (raw)
In-Reply-To: <20260713025712.416366-1-ruanjinjie@huawei.com>



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. */


  reply	other threads:[~2026-07-14  7:36 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-13  2:57 Jinjie Ruan
2026-07-14  7:36 ` Jinjie Ruan [this message]
2026-07-14 14:40 ` [tip: core/entry] " tip-bot2 for Jinjie Ruan
2026-10-14  9:21 ` [PATCH v2] " Oleg Nesterov

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=157aa098-a263-4f4f-b0fc-108701480303@huawei.com \
    --to=ruanjinjie@huawei.com \
    --cc=kees@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=luto@kernel.org \
    --cc=peterz@infradead.org \
    --cc=tglx@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

all inboxes | Powered by JetHome®