| 0/0 |
2026/08/09 00:29 |
flow |
assessment-security |
48m
Results: map[DenialOfService:true Explanation:The bug is a race condition in the ftrace ring buffer between resizing the
sub-buffer size (`ring_buffer_subbuf_order_set`) and reading from the ring
buffer (`ring_buffer_read_page`).
### Root Cause Analysis
When a user writes to `/sys/kernel/tracing/buffer_subbuf_size_kb`, the kernel
calls `ring_buffer_subbuf_order_set` to resize the ring buffer pages. This
function updates `buffer->subbuf_order` and `buffer->subbuf_size` early on, but
then calls `synchronize_rcu()` before taking the `reader_lock` to actually
replace the old pages with new ones.
Concurrently, a reader thread reading from `trace_pipe_raw` calls
`tracing_buffers_read`, which allocates a new read page using the newly updated
`buffer->subbuf_order`. It then proceeds to `ring_buffer_read_page`, takes the
`reader_lock`, and swaps this newly allocated page with the current reader page
in the ring buffer.
Because `ring_buffer_subbuf_order_set` hasn't replaced the pages yet, the reader
page in the ring buffer is still of the **old** order. The swap results in a
mismatch:
1. The ring buffer's `buffer_page` metadata now points to a data page of the
**new** order, but its `order` field still reflects the **old** order.
2. The reader's `data_page` metadata now points to a data page of the **old**
order, but its `order` field reflects the **new** order.
When these pages are eventually freed via `free_pages()`, the kernel uses the
mismatched order fields. This leads to severe memory corruption: freeing a small
page as a large page (corrupting adjacent memory) or freeing a large page as a
small page (memory leak).
The `WARNING` reported by syzbot is a side effect of this race. In
`__rb_get_reader_page`, the code reads the new (smaller) `subbuf_size` and
compares it against the write counter of the old (larger) page. If the old page
contains more data than the new size, the kernel incorrectly assumes a writer is
writing out-of-bounds, waits for 1 second, and triggers the `WARNING`. If the
old page contains less data, the WARNING is bypassed, and the memory corruption
silently occurs.
### Security Impact
* **Exploitable / Denial of Service:** Yes. The bug leads to freeing pages with
the wrong order, which is a powerful memory corruption primitive that can be
exploited to corrupt adjacent kernel memory or crash the system.
* **Unprivileged / User Namespaces:** No. Triggering the bug requires writing to
`buffer_subbuf_size_kb` and reading from `trace_pipe_raw`. These files are
located in `tracefs`, which is restricted to `root` (0640 permissions) and
cannot be mounted inside unprivileged user namespaces.
* **Other Triggers:** No. This is strictly a local privilege issue tied to the
tracing subsystem. Exploitable:true FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:false]
|
| 1/1 |
2026/08/09 00:29 |
action |
syz-repro-to-c-repro |
0m
Results: map[SimplifiedCRepro:// Copyright 2026 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
// IMPORTANT: Do not copy the macros or definitions below directly into your reproducer.
// Instead, add the following line to your reproducer:
// #include "race_toolkit.h"
// --- Race Condition Toolkit ---
// Macros and snippets for CPU pinning, memory barriers, and userfaultfd.
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/futex.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <pthread.h>
#include <sched.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <time.h>
#include <unistd.h>
// Unbuffered I/O: Ensure logs are written immediately.
#define SETUP_UNBUFFERED_IO() setvbuf(stdout, NULL, _IONBF, 0)
// CPU Pinning: Pin the current thread to a specific CPU core.
#define PIN_TO_CPU(cpu) \
do { \
cpu_set_t mask; \
CPU_ZERO(&mask); \
CPU_SET(cpu, &mask); \
if (sched_setaffinity(0, sizeof(mask), &mask) == -1) { \
perror("sched_setaffinity"); \
} \
} while (0)
// Memory Barrier: Ensure memory ordering.
#define MB() __atomic_thread_fence(__ATOMIC_SEQ_CST)
// Spin-wait Barrier: Wait until a memory location has a specific value.
// Best for tight race windows (low latency, no context switches).
#define WAIT_ON(addr, val) \
do { \
while (__atomic_load_n(addr, __ATOMIC_ACQUIRE) != (val)) \
; \
} while (0)
// Signal: Set a memory location to a specific value to release a WAIT_ON.
#define SIGNAL(addr, val) __atomic_store_n(addr, val, __ATOMIC_RELEASE)
// --- Timing Primitives ---
// Robust timing loops in VM environments (using CLOCK_MONOTONIC to avoid time(NULL) jumps).
static inline double timer_elapsed_sec(struct timespec* start)
{
struct timespec now;
if (clock_gettime(CLOCK_MONOTONIC, &now) == -1) {
perror("clock_gettime(CLOCK_MONOTONIC) elapsed");
exit(1);
}
return (double)(now.tv_sec - start->tv_sec) + (double)(now.tv_nsec - start->tv_nsec) / 1e9;
}
// Initialize a monotonic timer variable.
#define TIMER_START(t) \
struct timespec t; \
if (clock_gettime(CLOCK_MONOTONIC, &t) == -1) { \
perror("clock_gettime(CLOCK_MONOTONIC) start"); \
exit(1); \
}
// Check if the elapsed time since 't' is less than 'sec' seconds.
#define TIMER_NOT_EXPIRED(t, sec) (timer_elapsed_sec(&(t)) < (double)(sec))
// Futex-based Event: Shared with syzkaller executor.
// Best for general synchronization or longer waits to save CPU.
typedef struct {
int state;
} event_t;
static void event_init(event_t* ev)
{
ev->state = 0;
}
static void event_reset(event_t* ev)
{
ev->state = 0;
}
static void event_set(event_t* ev)
{
if (__atomic_load_n(&ev->state, __ATOMIC_ACQUIRE)) {
fprintf(stderr, "event already set\n");
exit(1);
}
__atomic_store_n(&ev->state, 1, __ATOMIC_RELEASE);
syscall(SYS_futex, &ev->state, FUTEX_WAKE | FUTEX_PRIVATE_FLAG, 1000000);
}
static void event_wait(event_t* ev)
{
while (!__atomic_load_n(&ev->state, __ATOMIC_ACQUIRE))
syscall(SYS_futex, &ev->state, FUTEX_WAIT | FUTEX_PRIVATE_FLAG, 0, 0);
}
// userfaultfd setup: Register a memory range for page fault handling.
static int setup_uffd(void* addr, size_t len)
{
int uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1)
return -1;
struct uffdio_api api = {.api = UFFD_API, .features = 0};
if (ioctl(uffd, UFFDIO_API, &api) == -1) {
close(uffd);
return -1;
}
struct uffdio_register reg = {
.range = {.start = (uintptr_t)addr, .len = len},
.mode = UFFDIO_REGISTER_MODE_MISSING};
if (ioctl(uffd, UFFDIO_REGISTER, ®) == -1) {
close(uffd);
return -1;
}
return uffd;
}
// --- Guidance on Usage ---
// 1. Use WAIT_ON/SIGNAL for tight race conditions to avoid scheduling overhead.
// 2. Use event_t (futexes) for general coordination or when waiting for longer periods.
// 3. Always use PIN_TO_CPU to increase race probability on multi-core systems.
// 4. Use setup_uffd to register a memory range for page fault handling. This allows you to
// pause a thread accessing that memory until you handle the fault, creating a reliable
// and controllable race window.
// 5. Call SETUP_UNBUFFERED_IO() at the start of main() to ensure that logs are printed
// immediately. This is essential for understanding the exact interleaving of events
// when debugging race conditions.
// 6. For timing-based loops (e.g., running a race for 10 seconds), do NOT use time(NULL)
// or loops relying on real-time clocks, as VM clocks are highly unreliable and can fail or drift.
// Instead, use the robust monotonic timing primitives TIMER_START and TIMER_NOT_EXPIRED:
// TIMER_START(start);
// while (TIMER_NOT_EXPIRED(start, 10.0)) {
// // Your race logic here
// }
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>
#define NUM_CPUS 4
int trace_fds[NUM_CPUS];
int marker_fd;
int size_fd;
void *reader_thread(void *arg) {
long cpu = (long)arg;
PIN_TO_CPU(cpu);
char buf[16384];
while (1) {
int res = read(trace_fds[cpu], buf, sizeof(buf));
if (res < 0 && errno != EAGAIN) {
// Ignore other errors
}
}
return NULL;
}
void *writer_thread(void *arg) {
long cpu = (long)arg;
PIN_TO_CPU(cpu);
while (1) {
int res = write(marker_fd, "A", 1);
if (res < 0) {
// Ignore errors
}
}
return NULL;
}
void *sizer_thread(void *arg) {
while (1) {
pwrite(size_fd, "16\n", 3, 0);
pwrite(size_fd, "4\n", 2, 0);
}
return NULL;
}
int main() {
SETUP_UNBUFFERED_IO();
size_fd = open("/sys/kernel/tracing/buffer_subbuf_size_kb", O_WRONLY);
if (size_fd < 0) {
size_fd = open("/sys/kernel/debug/tracing/buffer_subbuf_size_kb", O_WRONLY);
if (size_fd < 0) {
printf("[-] Failed to open buffer_subbuf_size_kb: %s\n", strerror(errno));
exit(1);
}
}
printf("[+] Opened buffer_subbuf_size_kb.\n");
marker_fd = open("/sys/kernel/tracing/trace_marker", O_WRONLY);
if (marker_fd < 0) {
marker_fd = open("/sys/kernel/debug/tracing/trace_marker", O_WRONLY);
if (marker_fd < 0) {
printf("[-] Failed to open trace_marker: %s\n", strerror(errno));
exit(1);
}
}
printf("[+] Opened trace_marker.\n");
for (long i = 0; i < NUM_CPUS; i++) {
char path[256];
snprintf(path, sizeof(path), "/sys/kernel/tracing/per_cpu/cpu%ld/trace_pipe_raw", i);
trace_fds[i] = open(path, O_RDONLY | O_NONBLOCK);
if (trace_fds[i] < 0) {
snprintf(path, sizeof(path), "/sys/kernel/debug/tracing/per_cpu/cpu%ld/trace_pipe_raw", i);
trace_fds[i] = open(path, O_RDONLY | O_NONBLOCK);
if (trace_fds[i] < 0) {
printf("[-] Failed to open trace_pipe_raw for cpu %ld: %s\n", i, strerror(errno));
continue;
}
}
printf("[+] Opened trace_pipe_raw for cpu %ld.\n", i);
pthread_t t1, t2;
pthread_create(&t1, NULL, reader_thread, (void *)i);
pthread_create(&t2, NULL, writer_thread, (void *)i);
}
pthread_t t3;
pthread_create(&t3, NULL, sizer_thread, NULL);
printf("[+] Threads started, waiting for crash...\n");
TIMER_START(start);
while (TIMER_NOT_EXPIRED(start, 10.0)) {
sleep(1);
}
printf("[+] Done.\n");
return 0;
}]
|
| 2/1 |
2026/08/09 00:29 |
action |
kernel-checkouter |
0m
Results: map[KernelSrc:/app/workdir/cache/src/2c647d04ba64204b0de04a5458fe5db9d3049774]
|
| 3/1 |
2026/08/09 00:29 |
action |
kernel-builder |
24m
Results: map[KernelObj:/app/workdir/cache/build/f0f4fe1b759816dcab04eb4d0389fd46f10e2dd5]
|
| 4/1 |
2026/08/09 00:54 |
action |
codesearch-prepare |
17m
Results: map[Index:codesearch-index]
|
| 5/1 |
2026/08/09 01:12 |
agent |
expert |
6m
Model:
Results: map[DenialOfService:true Exploitable:true FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:false]
Instruction: You are an experienced Linux kernel security engineer. Your task is to analyze given kernel bug report
and determine its security impact based on the following dimensions.
Use the provided tools to examine the source code, check for capability checks (e.g., capable(), ns_capable()),
and understand the nature of the bug. Analyze the given kernel build and configuration.
You can check the kernel config by grepping ".config" file; you can check kernel cmdline by grepping
".config" file for "CONFIG_CMDLINE=". Assume sysctl parameters have default values.
But analyze for the corresponding production build w/o debugging tools enabled (like KASAN, KMSAN, UBSAN).
Try different strategies when analyzing the bug:
- think of ways in which the vulnerable code is unreachable
- or the other way around: try to come up with different ideas of how an unprivileged user can reach the bug
If still unsure err on the side of the bug being non-exploitable/not-accessible.
In the final reply, provide a reasoning for your assessment.
Analysis dimensions:
* Exploitable:
Determine if the bug can result in memory corruption, elevated privileges, or an information leak.
Memory safety issues are almost always exploitable (KASAN or UBSAN reports for use-after-free, out-of-bounds;
refcounting issues, corrupted lists, etc). When kernel is crashing on a completely wild pointer access
(e.g. user-space address, or non-canonical address, but not on NULL or address corresponding to KASAN shadow
for NULL address), including both data accesses and control transfers, that also usually implies possibility
of exploitation. Such reports usually say "unable to handle kernel paging request".
Uses of uninitialized values detected by KMSAN may be exploitable b/c attacker frequently can affect uninit
values with spraying techniques. However, for these exploitability depends on how exactly the uninit value
is used in the code, and what it affects.
Information leaks are exploitable on their own and should be classified as such. A bug that copies kernel
memory contents to userspace (e.g. an out-of-bounds read whose result is returned to the caller, or
uninitialized stack/heap bytes written to a user buffer) is exploitable: it can reveal kernel pointer
values and defeat KASLR, expose sensitive data such as cryptographic keys or other processes' memory, and
serves as a necessary building block in most modern kernel privilege-escalation exploit chains. Do not classify
an information leak as non-exploitable solely because it does not directly cause a memory write or control-flow
hijack; the leak itself is the exploit primitive.
Think of what happens after the bug is triggered. Some bugs cause kernel panic and halt execution,
they are harder to exploit. For example, BUG reports halts the kernel. However, WARNING reports don't halt
execution in production builds. Debug bug detection tools (like KASAN, KMSAN, KCSAN, UBSAN) are also not enabled
in production builds, so attacker can freely exploit these bugs w/o being detected by these tools.
If you see an integer overflow, think how the overflowed value used later (if it's used as allocation size,
or an array index). If you see an out-of-bounds read, think if it's followed by an out-of-bounds write as well.
Some KCSAN data-races may be exploitable by skilled attackers as well. Think what data structures got corrupted
as the result of data races and how. However, note that kernel has lots of "benign" data races that don't lead
to any runtime misbehavior at all.
* Denial Of Service:
Determine if the bug can result in denial-of-service. Most bugs can, since they cause system crash,
hangs, deadlocks, or resource leaks. This is mostly applicable to WARNING bugs that won't cause system crash
in production. For these think what will be consequences of the violation of the kernel assumptions flagged
by the WARNING. In some cases the unexpected condition is also properly handled by the normal control flow
(e.g. with "if (WARN_ON(...))"), these won't cause denial-of-service. If the condition is not handled,
then it may or may not cause denial-of-service.
* Accessible From Unprivileged Processes:
Determine if the bug can be reached from a typical (non-root) user process that does NOT have any special capabilities
(like CAP_SYS_ADMIN, CAP_NET_ADMIN, CAP_NET_RAW, CAP_PERFMON) or access to device nodes restricted to root.
Assume that unprivileged_bpf_disabled=1, that is eBPF loading is not accessible. However, cBPF (classical BPF)
is still accessible to non-root processes.
Assume that user namespaces are not accessible, that is, the process cannot get the mentioned capabilities even
within a new user namespace (checked by ns_capable() function in the kernel sources).
* Accessible From User Namespaces:
Determine if the bug can be reached within a user-namespace where the process has all capabilities
(including CAP_SYS_ADMIN, CAP_NET_ADMIN, CAP_NET_RAW, CAP_PERFMON). Such capabilities are checked with ns_capable()
function in the kernel sources.
* VM Guest Trigger:
Determine if the bug can be triggered from the context of a typical KVM guest (e.g., set up by a QEMU VMM).
Consider accesses to standard Linux host paravirtualized features (virtio-blk, virtio-net, etc.),
and handling of VM exits in the KVM code.
* VM Host Trigger in The Confidential Computing Context:
Determine if the bug can be triggered in a confidential computing guest kernel from the context of a KVM host.
Consider access to standard Linux guest paravirtualized features (virtio-blk, virtio-net, etc.).
* Ethernet Network Trigger:
Determine if the bug can be triggered by processing ingress network Ethernet traffic, either directly (network stack)
or via drivers exposed to network data.
* Other Remote Trigger:
Determine if the bug can be triggered by processing remote traffic other than Ethernet (Wifi, Bluetooth, NFC, etc).
* Peripheral Trigger:
Determine if the bug can be triggered via an untrusted peripheral device that can be physically plugged
into a system, such as a USB device or a niche hardware driver handling external hardware inputs.
This is particularly important for mobile and desktop environments where users can plug in unknown devices.
* Malicious Filesystem Trigger:
Determine if the bug can be triggered by the kernel mounting and parsing a malicious filesystem image.
This is highly critical for Desktop and Mobile environments where external media or downloaded images
might be auto-mounted.
Don't make assumptions about the kernel source code (it may be different from what you assume it is).
Extensively use the provided code access tools (codesearch-*, git-*, grepper, etc)
to examine the actual source code, and confirm any assumptions.
Prefer calling several tools at the same time to save round-trips.
Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.
Prompt:
The kernel bug report is:
------------[ cut here ]------------
1
WARNING: kernel/trace/ring_buffer.c:5957 at __rb_get_reader_page+0xe70/0x1450 kernel/trace/ring_buffer.c:5957, CPU#1: syz-executor422/6023
Modules linked in:
CPU: 1 UID: 0 PID: 6023 Comm: syz-executor422 Not tainted syzkaller #0 PREEMPT(full)
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 07/24/2026
RIP: 0010:__rb_get_reader_page+0xe70/0x1450 kernel/trace/ring_buffer.c:5957
Code: ff df 48 c1 ea 03 80 3c 02 00 0f 85 12 04 00 00 48 8b 43 10 be 04 00 00 00 48 8d 68 08 48 89 ef e8 75 ea 6b 00 f0 ff 45 00 90 <0f> 0b 90 e9 1c f9 ff ff 4c 8b 74 24 60 e8 1e 65 fc ff 48 8b 54 24
RSP: 0018:ffffc9000370fa20 EFLAGS: 00010006
RAX: 0000000000000001 RBX: ffff88801cade000 RCX: ffffffff820c7a2b
RDX: 0000000000000001 RSI: 0000000000000004 RDI: ffff88813fe62008
RBP: ffff88813fe62008 R08: 0000000000000001 R09: ffffed1027fcc401
R10: ffff88813fe6200b R11: ffff88801cade0a8 R12: ffff88801f75d980
R13: ffffed1003eebb32 R14: 0000000000000ff0 R15: ffff88801f75d990
FS: 00007effc71086c0(0000) GS:ffff888123fe4000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00007effc7106000 CR3: 0000000076748000 CR4: 00000000003526f0
Call Trace:
<TASK>
rb_get_reader_page kernel/trace/ring_buffer.c:5981 [inline]
ring_buffer_read_page+0x8ed/0x1710 kernel/trace/ring_buffer.c:7103
tracing_buffers_read+0x2bf/0xaf0 kernel/trace/trace.c:7125
vfs_read+0x1e4/0xb40 fs/read_write.c:572
ksys_read+0x12a/0x250 fs/read_write.c:716
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x10b/0x830 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7effc8166cde
Code: 08 0f 85 f5 e2 ff ff 49 89 fb 48 89 f0 48 89 d7 48 89 ce 4c 89 c2 4d 89 ca 4c 8b 44 24 08 4c 8b 4c 24 10 4c 89 5c 24 08 0f 05 <c3> 90 41 57 41 56 4d 89 c6 41 55 4d 89 cd 41 54 55 53 48 83 ec 08
RSP: 002b:00007effc7104198 EFLAGS: 00000246 ORIG_RAX: 0000000000000000
RAX: ffffffffffffffda RBX: 00007effc71086c0 RCX: 00007effc8166cde
RDX: 0000000000004000 RSI: 00007effc7104200 RDI: 0000000000000006
RBP: 00007effc7104200 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 00007effc81e0b20
R13: 0000000000000000 R14: 00007ffdeadf5850 R15: 00007ffdeadf5938
</TASK>
It is reproducible with the followint program.
Keep in mind that it may lack the precise threading, sandboxing, and some arguments of a working reproducer.
But it should give an idea of the involved syscalls.
// Copyright 2026 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
// IMPORTANT: Do not copy the macros or definitions below directly into your reproducer.
// Instead, add the following line to your reproducer:
// #include "race_toolkit.h"
// --- Race Condition Toolkit ---
// Macros and snippets for CPU pinning, memory barriers, and userfaultfd.
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/futex.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <pthread.h>
#include <sched.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <time.h>
#include <unistd.h>
// Unbuffered I/O: Ensure logs are written immediately.
#define SETUP_UNBUFFERED_IO() setvbuf(stdout, NULL, _IONBF, 0)
// CPU Pinning: Pin the current thread to a specific CPU core.
#define PIN_TO_CPU(cpu) \
do { \
cpu_set_t mask; \
CPU_ZERO(&mask); \
CPU_SET(cpu, &mask); \
if (sched_setaffinity(0, sizeof(mask), &mask) == -1) { \
perror("sched_setaffinity"); \
} \
} while (0)
// Memory Barrier: Ensure memory ordering.
#define MB() __atomic_thread_fence(__ATOMIC_SEQ_CST)
// Spin-wait Barrier: Wait until a memory location has a specific value.
// Best for tight race windows (low latency, no context switches).
#define WAIT_ON(addr, val) \
do { \
while (__atomic_load_n(addr, __ATOMIC_ACQUIRE) != (val)) \
; \
} while (0)
// Signal: Set a memory location to a specific value to release a WAIT_ON.
#define SIGNAL(addr, val) __atomic_store_n(addr, val, __ATOMIC_RELEASE)
// --- Timing Primitives ---
// Robust timing loops in VM environments (using CLOCK_MONOTONIC to avoid time(NULL) jumps).
static inline double timer_elapsed_sec(struct timespec* start)
{
struct timespec now;
if (clock_gettime(CLOCK_MONOTONIC, &now) == -1) {
perror("clock_gettime(CLOCK_MONOTONIC) elapsed");
exit(1);
}
return (double)(now.tv_sec - start->tv_sec) + (double)(now.tv_nsec - start->tv_nsec) / 1e9;
}
// Initialize a monotonic timer variable.
#define TIMER_START(t) \
struct timespec t; \
if (clock_gettime(CLOCK_MONOTONIC, &t) == -1) { \
perror("clock_gettime(CLOCK_MONOTONIC) start"); \
exit(1); \
}
// Check if the elapsed time since 't' is less than 'sec' seconds.
#define TIMER_NOT_EXPIRED(t, sec) (timer_elapsed_sec(&(t)) < (double)(sec))
// Futex-based Event: Shared with syzkaller executor.
// Best for general synchronization or longer waits to save CPU.
typedef struct {
int state;
} event_t;
static void event_init(event_t* ev)
{
ev->state = 0;
}
static void event_reset(event_t* ev)
{
ev->state = 0;
}
static void event_set(event_t* ev)
{
if (__atomic_load_n(&ev->state, __ATOMIC_ACQUIRE)) {
fprintf(stderr, "event already set\n");
exit(1);
}
__atomic_store_n(&ev->state, 1, __ATOMIC_RELEASE);
syscall(SYS_futex, &ev->state, FUTEX_WAKE | FUTEX_PRIVATE_FLAG, 1000000);
}
static void event_wait(event_t* ev)
{
while (!__atomic_load_n(&ev->state, __ATOMIC_ACQUIRE))
syscall(SYS_futex, &ev->state, FUTEX_WAIT | FUTEX_PRIVATE_FLAG, 0, 0);
}
// userfaultfd setup: Register a memory range for page fault handling.
static int setup_uffd(void* addr, size_t len)
{
int uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1)
return -1;
struct uffdio_api api = {.api = UFFD_API, .features = 0};
if (ioctl(uffd, UFFDIO_API, &api) == -1) {
close(uffd);
return -1;
}
struct uffdio_register reg = {
.range = {.start = (uintptr_t)addr, .len = len},
.mode = UFFDIO_REGISTER_MODE_MISSING};
if (ioctl(uffd, UFFDIO_REGISTER, ®) == -1) {
close(uffd);
return -1;
}
return uffd;
}
// --- Guidance on Usage ---
// 1. Use WAIT_ON/SIGNAL for tight race conditions to avoid scheduling overhead.
// 2. Use event_t (futexes) for general coordination or when waiting for longer periods.
// 3. Always use PIN_TO_CPU to increase race probability on multi-core systems.
// 4. Use setup_uffd to register a memory range for page fault handling. This allows you to
// pause a thread accessing that memory until you handle the fault, creating a reliable
// and controllable race window.
// 5. Call SETUP_UNBUFFERED_IO() at the start of main() to ensure that logs are printed
// immediately. This is essential for understanding the exact interleaving of events
// when debugging race conditions.
// 6. For timing-based loops (e.g., running a race for 10 seconds), do NOT use time(NULL)
// or loops relying on real-time clocks, as VM clocks are highly unreliable and can fail or drift.
// Instead, use the robust monotonic timing primitives TIMER_START and TIMER_NOT_EXPIRED:
// TIMER_START(start);
// while (TIMER_NOT_EXPIRED(start, 10.0)) {
// // Your race logic here
// }
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>
#define NUM_CPUS 4
int trace_fds[NUM_CPUS];
int marker_fd;
int size_fd;
void *reader_thread(void *arg) {
long cpu = (long)arg;
PIN_TO_CPU(cpu);
char buf[16384];
while (1) {
int res = read(trace_fds[cpu], buf, sizeof(buf));
if (res < 0 && errno != EAGAIN) {
// Ignore other errors
}
}
return NULL;
}
void *writer_thread(void *arg) {
long cpu = (long)arg;
PIN_TO_CPU(cpu);
while (1) {
int res = write(marker_fd, "A", 1);
if (res < 0) {
// Ignore errors
}
}
return NULL;
}
void *sizer_thread(void *arg) {
while (1) {
pwrite(size_fd, "16\n", 3, 0);
pwrite(size_fd, "4\n", 2, 0);
}
return NULL;
}
int main() {
SETUP_UNBUFFERED_IO();
size_fd = open("/sys/kernel/tracing/buffer_subbuf_size_kb", O_WRONLY);
if (size_fd < 0) {
size_fd = open("/sys/kernel/debug/tracing/buffer_subbuf_size_kb", O_WRONLY);
if (size_fd < 0) {
printf("[-] Failed to open buffer_subbuf_size_kb: %s\n", strerror(errno));
exit(1);
}
}
printf("[+] Opened buffer_subbuf_size_kb.\n");
marker_fd = open("/sys/kernel/tracing/trace_marker", O_WRONLY);
if (marker_fd < 0) {
marker_fd = open("/sys/kernel/debug/tracing/trace_marker", O_WRONLY);
if (marker_fd < 0) {
printf("[-] Failed to open trace_marker: %s\n", strerror(errno));
exit(1);
}
}
printf("[+] Opened trace_marker.\n");
for (long i = 0; i < NUM_CPUS; i++) {
char path[256];
snprintf(path, sizeof(path), "/sys/kernel/tracing/per_cpu/cpu%ld/trace_pipe_raw", i);
trace_fds[i] = open(path, O_RDONLY | O_NONBLOCK);
if (trace_fds[i] < 0) {
snprintf(path, sizeof(path), "/sys/kernel/debug/tracing/per_cpu/cpu%ld/trace_pipe_raw", i);
trace_fds[i] = open(path, O_RDONLY | O_NONBLOCK);
if (trace_fds[i] < 0) {
printf("[-] Failed to open trace_pipe_raw for cpu %ld: %s\n", i, strerror(errno));
continue;
}
}
printf("[+] Opened trace_pipe_raw for cpu %ld.\n", i);
pthread_t t1, t2;
pthread_create(&t1, NULL, reader_thread, (void *)i);
pthread_create(&t2, NULL, writer_thread, (void *)i);
}
pthread_t t3;
pthread_create(&t3, NULL, sizer_thread, NULL);
printf("[+] Threads started, waiting for crash...\n");
TIMER_START(start);
while (TIMER_NOT_EXPIRED(start, 10.0)) {
sleep(1);
}
printf("[+] Done.\n");
return 0;
}
Reply: The bug is a race condition in the ftrace ring buffer between resizing the sub-buffer size (`ring_buffer_subbuf_order_set`) and reading from the ring buffer (`ring_buffer_read_page`).
### Root Cause Analysis
When a user writes to `/sys/kernel/tracing/buffer_subbuf_size_kb`, the kernel calls `ring_buffer_subbuf_order_set` to resize the ring buffer pages. This function updates `buffer->subbuf_order` and `buffer->subbuf_size` early on, but then calls `synchronize_rcu()` before taking the `reader_lock` to actually replace the old pages with new ones.
Concurrently, a reader thread reading from `trace_pipe_raw` calls `tracing_buffers_read`, which allocates a new read page using the newly updated `buffer->subbuf_order`. It then proceeds to `ring_buffer_read_page`, takes the `reader_lock`, and swaps this newly allocated page with the current reader page in the ring buffer.
Because `ring_buffer_subbuf_order_set` hasn't replaced the pages yet, the reader page in the ring buffer is still of the **old** order. The swap results in a mismatch:
1. The ring buffer's `buffer_page` metadata now points to a data page of the **new** order, but its `order` field still reflects the **old** order.
2. The reader's `data_page` metadata now points to a data page of the **old** order, but its `order` field reflects the **new** order.
When these pages are eventually freed via `free_pages()`, the kernel uses the mismatched order fields. This leads to severe memory corruption: freeing a small page as a large page (corrupting adjacent memory) or freeing a large page as a small page (memory leak).
The `WARNING` reported by syzbot is a side effect of this race. In `__rb_get_reader_page`, the code reads the new (smaller) `subbuf_size` and compares it against the write counter of the old (larger) page. If the old page contains more data than the new size, the kernel incorrectly assumes a writer is writing out-of-bounds, waits for 1 second, and triggers the `WARNING`. If the old page contains less data, the WARNING is bypassed, and the memory corruption silently occurs.
### Security Impact
* **Exploitable / Denial of Service:** Yes. The bug leads to freeing pages with the wrong order, which is a powerful memory corruption primitive that can be exploited to corrupt adjacent kernel memory or crash the system.
* **Unprivileged / User Namespaces:** No. Triggering the bug requires writing to `buffer_subbuf_size_kb` and reading from `trace_pipe_raw`. These files are located in `tracefs`, which is restricted to `root` (0640 permissions) and cannot be mounted inside unprivileged user namespaces.
* **Other Triggers:** No. This is strictly a local privilege issue tied to the tracing subsystem.
|
| 6/2 |
2026/08/09 01:12 |
llm |
expert |
0m
Model:
Tokens:
input: 10034
output: 29
thoughts: 155
Thoughts: **Analyzing a Kernel Trace Buffer Warning**
Okay, so I've got a warning flagged in `kernel/trace/ring_buffer.c:5957` during a routine I'm familiar with, specifically, `__rb_get_reader_page`. The reproducer setup is interesting: it's hitting several key trace points. Itβs working with `/sys/kernel/tracing/buffer_subbuf_size_kb`, `/sys/kernel/tracing/trace_marker`, and these `trace_pipe_raw` files, per CPU. The code spawns threads that read from the `trace_pipe_raw`, and simultaneously write to both the `trace_marker` and `buffer_subbuf_size_kb`. The write to `buffer_subbuf_size_kb` is changing the size between 16 and 4 - that's where things get interesting and where I'll expect to see the race conditions. I need to dive into `kernel/trace/ring_buffer.c:5957` now and see what the function does and what might be going wrong given this context. This looks like a classic race condition scenario that I need to pinpoint.
|
| 7/2 |
2026/08/09 01:12 |
tool |
git-log |
0m
Args: map[Count:1 PathPrefix:kernel/trace/ring_buffer.c]
Results: map[Output:260b20d9b78b ring-buffer: Fix subbuf_ids memory leak in rb_allocate_cpu_buffer() error path
]
|
| 8/2 |
2026/08/09 01:12 |
llm |
expert |
0m
Model:
Tokens:
input: 10520
output: 38
thoughts: 25
Thoughts: Here's my analysis of the provided thought process, tailored for an expert audience:
**Analyzing `kernel/trace/ring_buffer.c` - Potential Issue Around Line 5957**
Okay, so I'm thinking we need to zero in on a specific area within `kernel/trace/ring_buffer.c`. My initial hunch tells me we should be scrutinizing the code in the vicinity of line 5957. That line number usually points to a significant block of code. I'm immediately trying to remember what specific functionality that section of the ring buffer implementation typically covers - could be allocation, write operations, read operations or error handling - something pivotal to the overall trace functionality. This is where I start to form potential hypotheses about what problem we might be facing. Is it a potential race condition? A memory leak? Perhaps an incorrect offset calculation? Or is this connected to our current tracing performance anomalies? Time to dive into the code and start looking for the root cause. I need to refresh my memory on the surrounding functions and data structures.
|
| 9/2 |
2026/08/09 01:12 |
tool |
read-file |
0m
Args: map[File:kernel/trace/ring_buffer.c FirstLine:5940 LineCount:50]
Results: map[Contents:5940: /*
5941: * The writer has preempt disable, wait for it. But not forever
5942: * Although, 1 second is pretty much "forever"
5943: */
5944: #define USECS_WAIT 1000000
5945: for (nr_loops = 0; nr_loops < USECS_WAIT; nr_loops++) {
5946: /* If the write is past the end of page, a writer is still updating it */
5947: if (likely(!reader || rb_page_write(reader) <= bsize))
5948: break;
5949:
5950: udelay(1);
5951:
5952: /* Get the latest version of the reader write value */
5953: smp_rmb();
5954: }
5955:
5956: /* The writer is not moving forward? Something is wrong */
5957: if (RB_WARN_ON(cpu_buffer, nr_loops == USECS_WAIT))
5958: reader = NULL;
5959:
5960: /*
5961: * Make sure we see any padding after the write update
5962: * (see rb_reset_tail()).
5963: *
5964: * In addition, a writer may be writing on the reader page
5965: * if the page has not been fully filled, so the read barrier
5966: * is also needed to make sure we see the content of what is
5967: * committed by the writer (see rb_set_commit_to_write()).
5968: */
5969: smp_rmb();
5970:
5971: if (!cpu_buffer->lost_events)
5972: cpu_buffer->lost_events = missed_events;
5973:
5974: return reader;
5975: }
5976:
5977: static struct buffer_page *
5978: rb_get_reader_page(struct ring_buffer_per_cpu *cpu_buffer)
5979: {
5980: return cpu_buffer->remote ? __rb_get_reader_page_from_remote(cpu_buffer) :
5981: __rb_get_reader_page(cpu_buffer);
5982: }
5983:
5984: static void rb_advance_reader(struct ring_buffer_per_cpu *cpu_buffer)
5985: {
5986: struct ring_buffer_event *event;
5987: struct buffer_page *reader;
5988: unsigned length;
5989:
]
|
| 10/2 |
2026/08/09 01:12 |
llm |
expert |
0m
Model:
Tokens:
input: 11553
output: 20
thoughts: 233
Thoughts: **Analyzing a Potential Kernel Tracing Issue**
Okay, so I'm looking at this code snippet, and it's throwing up a red flag. This `RB_WARN_ON` check is a timeout mechanism, specifically designed to catch situations where the writer in this ring buffer isn't progressing. It's essentially waiting for the writer to finish writing to a reader page, and it's set to time out after a second of `udelay(1)` loops, which is defined by `USECS_WAIT`. The condition here suggests either the writer has stalled, or, something worse, there's corruption happening with the value being written to the reader page.
The reproducer apparently involves concurrent modifications to the `buffer_subbuf_size_kb` while reads and writes are in progress. That's immediately suspect. The problem *likely* lies there, which is being written to concurrently.
Aha, and then the penny drops. Tracing functionality like this is almost always root-restricted, right? The very nature of accessing and controlling kernel internals means it needs privileged access. So, the question is, *why* can this repro even happen? Let me check the permissions on those critical files: `/sys/kernel/tracing/buffer_subbuf_size_kb` and, more generally, `/sys/kernel/tracing/trace_marker`. Iβll also need to verify the permissions on the tracefs mount point itself. Given that `/sys/kernel/tracing` is typically mounted as tracefs, which I expect is properly secured, the issue may lie with a configuration problem or a more subtle race condition, such as the caller not using locking correctly, and thus inadvertently granting unpriviledged access.
|
| 11/2 |
2026/08/09 01:12 |
tool |
grepper |
0m
Args: map[Expression:tracefs_create_file]
Results: map[Output:arch/arm64/kvm/hyp_trace.c=384=static int hyp_trace_init_tracefs(struct dentry *d, void *priv)
arch/arm64/kvm/hyp_trace.c-385-{
arch/arm64/kvm/hyp_trace.c:386: if (!tracefs_create_file("write_event", 0200, d, NULL, &hyp_trace_write_event_fops))
arch/arm64/kvm/hyp_trace.c-387- return -ENOMEM;
arch/arm64/kvm/hyp_trace.c-388-
arch/arm64/kvm/hyp_trace.c:389: return tracefs_create_file("trace_clock", 0440, d, NULL, &hyp_trace_clock_fops) ?
arch/arm64/kvm/hyp_trace.c-390- 0 : -ENOMEM;
--
fs/tracefs/inode.c=567=static struct inode *instance_inode(struct dentry *parent, struct inode *inode)
--
fs/tracefs/inode.c-586-/**
fs/tracefs/inode.c:587: * tracefs_create_file - create a file in the tracefs filesystem
fs/tracefs/inode.c-588- * @name: a pointer to a string containing the name of the file to create.
--
fs/tracefs/inode.c-611- */
fs/tracefs/inode.c:612:struct dentry *tracefs_create_file(const char *name, umode_t mode,
fs/tracefs/inode.c-613- struct dentry *parent, void *data,
--
fs/tracefs/inode.c-647-}
fs/tracefs/inode.c:648:EXPORT_SYMBOL_GPL(tracefs_create_file);
fs/tracefs/inode.c-649-
--
fs/tracefs/inode.c=745=static void remove_one(struct dentry *victim)
--
fs/tracefs/inode.c-755- * was previously created with a call to another tracefs function
fs/tracefs/inode.c:756: * (like tracefs_create_file() or variants thereof.)
fs/tracefs/inode.c-757- */
--
include/linux/tracefs.h=91=void eventfs_remove_dir(struct eventfs_inode *ei);
include/linux/tracefs.h-92-
include/linux/tracefs.h:93:struct dentry *tracefs_create_file(const char *name, umode_t mode,
include/linux/tracefs.h-94- struct dentry *parent, void *data,
--
kernel/trace/remote_test.c=235=static int remote_test_init_tracefs(struct dentry *d, void *unused)
kernel/trace/remote_test.c-236-{
kernel/trace/remote_test.c:237: return tracefs_create_file("write_event", 0200, d, NULL, &write_event_fops) ?
kernel/trace/remote_test.c-238- 0 : -ENOMEM;
--
kernel/trace/rv/rv.h=4=struct rv_interface {
--
kernel/trace/rv/rv.h-16-#define rv_create_dir tracefs_create_dir
kernel/trace/rv/rv.h:17:#define rv_create_file tracefs_create_file
kernel/trace/rv/rv.h-18-#define rv_remove tracefs_remove
--
kernel/trace/trace.c=7850=struct dentry *trace_create_file(const char *name,
--
kernel/trace/trace.c-7857-
kernel/trace/trace.c:7858: ret = tracefs_create_file(name, mode, parent, data, fops);
kernel/trace/trace.c-7859- if (!ret)
--
kernel/trace/trace_events_synth.c=2411=static __init int trace_events_synth_init(void)
--
kernel/trace/trace_events_synth.c-2418-
kernel/trace/trace_events_synth.c:2419: entry = tracefs_create_file("synthetic_events", TRACE_MODE_WRITE,
kernel/trace/trace_events_synth.c-2420- NULL, NULL, &synth_events_fops);
--
kernel/trace/trace_events_user.c=2874=static int create_user_tracefs(void)
--
kernel/trace/trace_events_user.c-2877-
kernel/trace/trace_events_user.c:2878: edata = tracefs_create_file("user_events_data", TRACE_MODE_WRITE,
kernel/trace/trace_events_user.c-2879- NULL, NULL, &user_data_fops);
--
kernel/trace/trace_events_user.c-2885-
kernel/trace/trace_events_user.c:2886: emmap = tracefs_create_file("user_events_status", TRACE_MODE_READ,
kernel/trace/trace_events_user.c-2887- NULL, NULL, &user_status_fops);
--
kernel/trace/trace_hwlat.c=765=static int init_tracefs(void)
--
kernel/trace/trace_hwlat.c-777-
kernel/trace/trace_hwlat.c:778: hwlat_sample_window = tracefs_create_file("window", TRACE_MODE_WRITE,
kernel/trace/trace_hwlat.c-779- top_dir,
--
kernel/trace/trace_hwlat.c-784-
kernel/trace/trace_hwlat.c:785: hwlat_sample_width = tracefs_create_file("width", TRACE_MODE_WRITE,
kernel/trace/trace_hwlat.c-786- top_dir,
--
kernel/trace/trace_osnoise.c=2739=static int init_timerlat_stack_tracefs(struct dentry *top_dir)
--
kernel/trace/trace_osnoise.c-2742-
kernel/trace/trace_osnoise.c:2743: tmp = tracefs_create_file("print_stack", TRACE_MODE_WRITE, top_dir,
kernel/trace/trace_osnoise.c-2744- &osnoise_print_stack, &trace_min_max_fops);
--
kernel/trace/trace_osnoise.c=2800=static int init_timerlat_tracefs(struct dentry *top_dir)
--
kernel/trace/trace_osnoise.c-2804-
kernel/trace/trace_osnoise.c:2805: tmp = tracefs_create_file("timerlat_period_us", TRACE_MODE_WRITE, top_dir,
kernel/trace/trace_osnoise.c-2806- &timerlat_period, &trace_min_max_fops);
--
kernel/trace/trace_osnoise.c-2809-
kernel/trace/trace_osnoise.c:2810: tmp = tracefs_create_file("timerlat_align_us", TRACE_MODE_WRITE, top_dir,
kernel/trace/trace_osnoise.c-2811- &timerlat_align_us, &trace_min_max_fops);
--
kernel/trace/trace_osnoise.c=2835=static int init_tracefs(void)
--
kernel/trace/trace_osnoise.c-2848-
kernel/trace/trace_osnoise.c:2849: tmp = tracefs_create_file("period_us", TRACE_MODE_WRITE, top_dir,
kernel/trace/trace_osnoise.c-2850- &osnoise_period, &trace_min_max_fops);
--
kernel/trace/trace_osnoise.c-2853-
kernel/trace/trace_osnoise.c:2854: tmp = tracefs_create_file("runtime_us", TRACE_MODE_WRITE, top_dir,
kernel/trace/trace_osnoise.c-2855- &osnoise_runtime, &trace_min_max_fops);
--
kernel/trace/trace_osnoise.c-2858-
kernel/trace/trace_osnoise.c:2859: tmp = tracefs_create_file("stop_tracing_us", TRACE_MODE_WRITE, top_dir,
kernel/trace/trace_osnoise.c-2860- &osnoise_stop_tracing_in, &trace_min_max_fops);
--
kernel/trace/trace_osnoise.c-2863-
kernel/trace/trace_osnoise.c:2864: tmp = tracefs_create_file("stop_tracing_total_us", TRACE_MODE_WRITE, top_dir,
kernel/trace/trace_osnoise.c-2865- &osnoise_stop_tracing_total, &trace_min_max_fops);
--
kernel/trace/trace_stat.c=290=static int init_stat_file(struct stat_session *session)
--
kernel/trace/trace_stat.c-296-
kernel/trace/trace_stat.c:297: session->file = tracefs_create_file(session->ts->name, TRACE_MODE_WRITE,
kernel/trace/trace_stat.c-298- stat_dir, session,
]
|
| 12/2 |
2026/08/09 01:12 |
llm |
expert |
0m
Model:
Tokens:
input: 14642
output: 22
thoughts: 36
Thoughts: Okay, here's my interpretation of that thought process, tailored for an expert audience:
**Examining Tracefs File Creation Modes**
Right, so we're dealing with `tracefs` files, and the key thing to note is that they're instantiated with either `TRACE_MODE_WRITE` or `TRACE_MODE_READ`. My immediate thought is, let's drill down into what that `TRACE_MODE_WRITE` flag actually represents. I need a clear understanding of its semantics to fully grasp how these tracepoints are being handled. We should check the source, or the documentation to find out.
|
| 13/2 |
2026/08/09 01:12 |
tool |
grepper |
0m
Args: map[Expression:#define TRACE_MODE_WRITE]
Results: map[Output:kernel/trace/trace.h-33-
kernel/trace/trace.h:34:#define TRACE_MODE_WRITE 0640
kernel/trace/trace.h-35-#define TRACE_MODE_READ 0440
]
|
| 14/2 |
2026/08/09 01:12 |
llm |
expert |
0m
Model:
Tokens:
input: 14901
output: 26
thoughts: 62
Thoughts: **Analyzing Tracefs Security**
Okay, so I'm looking at these files, and the permissions are either 0640 or 0440 β standard stuff, nothing alarming there. The owner is likely root, as is typical. But the real question burning in my mind is, can an unprivileged user mount tracefs? Tracefs is usually mounted in `/sys/kernel/tracing`, right? And the next big question: how does it behave within user namespaces? Can someone with limited privileges somehow leverage that to see or modify things they shouldn't? This is where the interesting security considerations come into play, and I need to investigate further.
|
| 15/2 |
2026/08/09 01:12 |
tool |
grepper |
0m
Error:
Args: map[Expression:fs_context_operations trace_fs_context_ops]
Results:
|
| 16/2 |
2026/08/09 01:12 |
llm |
expert |
0m
Model:
Tokens:
input: 15140
output: 22
thoughts: 22
Thoughts: Here's a summary of my thought process:
**Diving into the Tracefs Inode Implementation**
Okay, so I need to understand the `fs_context_operations` within `tracefs`. My initial instinct is to dig directly into the source code, specifically `fs/tracefs/inode.c`. That file is likely where the core inode-related operations for the `tracefs` filesystem are defined, and that's the place to start looking for how the filesystem is set up. Let's see what we can find in `inode.c`.
|
| 17/2 |
2026/08/09 01:12 |
tool |
grepper |
0m
Args: map[Expression:tracefs_init_fs_context]
Results: map[Output:fs/tracefs/inode.c=495=static const struct fs_context_operations tracefs_context_ops = {
--
fs/tracefs/inode.c-501-
fs/tracefs/inode.c:502:static int tracefs_init_fs_context(struct fs_context *fc)
fs/tracefs/inode.c-503-{
--
fs/tracefs/inode.c=517=static struct file_system_type trace_fs_type = {
--
fs/tracefs/inode.c-519- .name = "tracefs",
fs/tracefs/inode.c:520: .init_fs_context = tracefs_init_fs_context,
fs/tracefs/inode.c-521- .parameters = tracefs_param_specs,
]
|
| 18/2 |
2026/08/09 01:12 |
llm |
expert |
0m
Model:
Tokens:
input: 15519
output: 36
thoughts: 22
Thoughts: Here's a summary of my thought process:
**Diving into the Filesystem Tracing Code**
Okay, so I need to understand how this tracing functionality is working within the filesystem layer. Let's start by looking at `trace_fs_type` defined within `fs/tracefs/inode.c`. That's the logical starting point β the file and function name suggest it's dealing with the core type definition, which is exactly where I'd begin my investigation. It's likely involved in setting up the tracefs filesystem's inode structure and linking it to the tracing infrastructure, or it could be involved in some kind of internal representation of tracefs. Time to see what that function is actually doing. This is where the real work begins.
|
| 19/2 |
2026/08/09 01:12 |
tool |
read-file |
0m
Args: map[File:fs/tracefs/inode.c FirstLine:510 LineCount:20]
Results: map[Contents: 510: fsi->mode = TRACEFS_DEFAULT_MODE;
511:
512: fc->s_fs_info = fsi;
513: fc->ops = &tracefs_context_ops;
514: return 0;
515: }
516:
517: static struct file_system_type trace_fs_type = {
518: .owner = THIS_MODULE,
519: .name = "tracefs",
520: .init_fs_context = tracefs_init_fs_context,
521: .parameters = tracefs_param_specs,
522: .kill_sb = kill_anon_super,
523: };
524: MODULE_ALIAS_FS("tracefs");
525:
526: struct dentry *tracefs_start_creating(const char *name, struct dentry *parent)
527: {
528: struct dentry *dentry;
529: int error;
]
|
| 20/2 |
2026/08/09 01:12 |
llm |
expert |
0m
Model:
Tokens:
input: 16043
output: 31
thoughts: 306
Thoughts: **Analyzing the Tracefs Security Boundary**
Okay, so it looks like `trace_fs_type` doesn't support `FS_USERNS_MOUNT`. That immediately limits the scope of this bug. Since I can't mount tracefs within a user namespace, it's pretty safe to say this vulnerability isn't exploitable by unprivileged processes or within user namespaces. That's a good first step in assessing the impact. This means it requires root, or at least CAP_SYS_ADMIN in the initial namespace, which narrows down the potential attack vectors.
Now, I need to figure out if there are any *other* ways to trigger this. The bug is specifically in `__rb_get_reader_page`, which is called through a series of functions: `rb_get_reader_page`, then `ring_buffer_read_page`, and finally, accessed via `tracing_buffers_read` in `kernel/trace/trace.c`. That, in turn, is the read operation for `trace_pipe_raw`. My understanding of the reproducer is that it's opening a `/sys/kernel/tracing/per_cpu/cpuX/trace_pipe_raw` file, which, of course, resides within tracefs.
But is there another path to the ring buffer? I know Perf uses ring buffers, but they have their own implementation in `kernel/events/ring_buffer.c`, so that's not it. Ftrace *does* use `kernel/trace/ring_buffer.c`, so that is a possible vector. I need to make sure there are no other consumers of `kernel/trace/ring_buffer.c`. I'm going to have to do a quick code search to verify.
|
| 21/2 |
2026/08/09 01:13 |
tool |
codesearch-find-references |
0m
Args: map[IncludeSnippetLines:0 Name:ring_buffer_read_page]
Results: map[References:[map[ReferenceKind:calls ReferencingEntityKind:function ReferencingEntityName:tracing_buffers_read SourceFile:kernel/trace/trace.c SourceLine:7125] map[ReferenceKind:calls ReferencingEntityKind:function ReferencingEntityName:tracing_buffers_splice_read SourceFile:kernel/trace/trace.c SourceLine:7316] map[ReferenceKind:takes-address-of ReferencingEntityKind:global_variable ReferencingEntityName:__UNIQUE_ID_addressable_ring_buffer_read_page_1262 SourceFile:kernel/trace/ring_buffer.c SourceLine:7253] map[ReferenceKind:takes-address-of ReferencingEntityKind:global_variable ReferencingEntityName:__gendwarfksyms_ptr_ring_buffer_read_page SourceFile:kernel/trace/ring_buffer.c SourceLine:7253] map[ReferenceKind:takes-address-of ReferencingEntityKind:global_variable ReferencingEntityName:__gendwarfksyms_ptr_ring_buffer_read_page SourceFile:kernel/trace/ring_buffer.c SourceLine:7253]] TruncatedOutput:false]
|
| 22/2 |
2026/08/09 01:13 |
llm |
expert |
0m
Model:
Tokens:
input: 17084
output: 38
thoughts: 299
Thoughts: **Analyzing the Tracefs Vulnerability**
Okay, so I'm looking at this code, and it seems the issue is strictly contained within `kernel/trace/trace.c`, specifically the `tracing_buffers_read` and `tracing_buffers_splice_read` functions. These functions operate on `trace_pipe_raw`. This immediately flags it as a `tracefs` problem.
Given the nature of `tracefs`, it's generally only accessible to root, or potentially users in the `tracing` group, assuming that group has been configured. But even then, thatβs an administrative decision, and ultimately privileged access. By default, the `tracefs` files are owned by root, and the permissions are set to either 0640 or 0440.
Hold on, there's the `tracefs` mount options like `gid=` and `mode=`. However, any unprivileged user cannot mount it. So, that's not a concern here. It's also not accessible from user namespaces. This reduces the attack surface significantly.
Now, what about the potential impact of this? I see it's a `WARNING` in `kernel/trace/ring_buffer.c:5957`. The code says something like this:
```c
/* The writer is not moving forward? Something is wrong */
if (RB_WARN_ON(cpu_buffer, nr_loops == USECS_WAIT))
reader = NULL;
```
A `WARNING` in a production build isn't going to halt the system unless `panic_on_warn` is set, which is not the default. The system will continue to run. If the condition is met, the `reader` pointer is set to NULL. I need to dig into the surrounding code to determine the implications of a null `reader` pointer.
|
| 23/2 |
2026/08/09 01:13 |
tool |
read-file |
0m
Args: map[File:kernel/trace/ring_buffer.c FirstLine:5960 LineCount:20]
Results: map[Contents:5960: /*
5961: * Make sure we see any padding after the write update
5962: * (see rb_reset_tail()).
5963: *
5964: * In addition, a writer may be writing on the reader page
5965: * if the page has not been fully filled, so the read barrier
5966: * is also needed to make sure we see the content of what is
5967: * committed by the writer (see rb_set_commit_to_write()).
5968: */
5969: smp_rmb();
5970:
5971: if (!cpu_buffer->lost_events)
5972: cpu_buffer->lost_events = missed_events;
5973:
5974: return reader;
5975: }
5976:
5977: static struct buffer_page *
5978: rb_get_reader_page(struct ring_buffer_per_cpu *cpu_buffer)
5979: {
]
|
| 24/2 |
2026/08/09 01:13 |
llm |
expert |
0m
Model:
Tokens:
input: 18111
output: 38
thoughts: 37
Thoughts: Okay, here's my interpretation of those thoughts, tailored for an expert audience, presented in the first person:
**Investigating a Null Reader**
Right, so the function is returning `reader` as `NULL`. That's a classic sign of something going awry in the reader setup or the read operation itself. Let's trace this back. The immediate call stack suggests `rb_get_reader_page` is the culprit, which then calls `ring_buffer_read_page`. I need to understand why `ring_buffer_read_page` is returning a null reader object. The failure may be due to improper ring buffer configuration, uninitialized variables, an invalid read request (out-of-bounds, or at an impossible offset given the current state), or maybe even a race condition leading to an unexpected state within the ring buffer. I'll need to scrutinize those functions and their parameters, paying close attention to the buffer state and locking mechanisms, if any, within `ring_buffer_read_page`, and any error paths it might expose. Let's see how the reader is used in the calling function as well, in case something isn't getting correctly initialized.
|
| 25/2 |
2026/08/09 01:13 |
tool |
read-file |
0m
Args: map[File:kernel/trace/ring_buffer.c FirstLine:7090 LineCount:30]
Results: map[Contents:7090:
7091: if (!data_page || !data_page->data)
7092: return -1;
7093:
7094: if (data_page->order != buffer->subbuf_order)
7095: return -1;
7096:
7097: dpage = data_page->data;
7098: if (!dpage)
7099: return -1;
7100:
7101: guard(raw_spinlock_irqsave)(&cpu_buffer->reader_lock);
7102:
7103: reader = rb_get_reader_page(cpu_buffer);
7104: if (!reader)
7105: return -1;
7106:
7107: event = rb_reader_event(cpu_buffer);
7108:
7109: read = reader->read;
7110: commit = rb_page_commit(reader);
7111: size = rb_page_size(reader);
7112:
7113: /* Check if any events were dropped */
7114: missed_events = cpu_buffer->lost_events;
7115:
7116: force_memcpy = cpu_buffer->mapped || cpu_buffer->remote;
7117:
7118: /*
7119: * If this page has been partially read or
]
|
| 26/2 |
2026/08/09 01:13 |
llm |
expert |
0m
Model:
Tokens:
input: 18837
output: 36
thoughts: 43
Thoughts: Okay, here's my interpretation of this scenario, formatted as requested:
**Interpreting a Potential Null Pointer in Ring Buffer Reading**
Alright, so I'm thinking about how the `ring_buffer_read_page` function is designed to handle potential issues. Specifically, what happens if the `reader` parameter passed to it is a null pointer? The documentation states that in this case, the function gracefully returns `-1`. This is a crucial design decision, and I can see the reasoning behind it.
The good thing is that the function's failure is handled by `tracing_buffers_read` which is the one calling `ring_buffer_read_page`. It looks like that particular function will correctly interpret a return value of `-1` (or any value less than 0) as an error condition. This implies a level of robustness in the system because, even if a null pointer is encountered at a lower level, the higher-level caller knows how to deal with the problem rather than crashing. I would expect some sort of error handling, logging, or alternative action to be triggered at that point, ensuring data integrity isn't compromised. It's a clean separation of concerns, where `ring_buffer_read_page` focuses on a particular, expected, state, and the caller is responsible for orchestrating the overall flow.
|
| 27/2 |
2026/08/09 01:13 |
tool |
read-file |
0m
Args: map[File:kernel/trace/trace.c FirstLine:7115 LineCount:30]
Results: map[Contents:7115: }
7116: if (!info->spare)
7117: return ret;
7118:
7119: /* Do we have previous read data to read? */
7120: if (info->read < page_size)
7121: goto read;
7122:
7123: again:
7124: trace_access_lock(iter->cpu_file);
7125: ret = ring_buffer_read_page(iter->array_buffer->buffer,
7126: info->spare,
7127: count,
7128: iter->cpu_file, 0);
7129: trace_access_unlock(iter->cpu_file);
7130:
7131: if (ret < 0) {
7132: if (trace_empty(iter) && !iter->closed) {
7133: if (update_last_data_if_empty(iter->tr))
7134: return 0;
7135:
7136: if ((filp->f_flags & O_NONBLOCK))
7137: return -EAGAIN;
7138:
7139: ret = wait_on_pipe(iter, 0);
7140: if (ret)
7141: return ret;
7142:
7143: goto again;
7144: }
]
|
| 28/2 |
2026/08/09 01:13 |
llm |
expert |
0m
Model:
Tokens:
input: 19602
output: 29
thoughts: 266
Thoughts: **Analyzing the Warning: A Deep Dive**
Okay, so I'm looking at this error condition, and the immediate thought is the return value of `ret < 0`. It could signal a wait, an `-EAGAIN`, or even a `0`. Good, returning `-1` is handled properly; that's not the root cause. The real issue is the *warning* itself. It doesn't trigger a crash, a deadlock, or memory corruption, which is a relief. It's just a heads-up that either the writer is blocked, or the reader's page is corrupted.
Since it's a WARNING, a denial of service in a production environment is unlikely, unless `panic_on_warn` is enabled. But... wait a minute. If the writer is stuck, does that potentially indicate a system deadlock? That's what the warning "The writer is not moving forward? Something is wrong" suggests. The loop is clearly waiting for the writer to complete its operation on the reader page.
The writer's operating on a separate CPU, with preemption off. So if it *is* stuck, the problem could be a deadlock within the writer itself, a writer taking an unreasonable amount of time, or perhaps a corrupted state related to a subbuf size change. We need to determine if the writer is actually stalled, or if there's been state corruption. The reproducer mentions concurrent subbuf size changes... which immediately triggers a red flag.
The reproducer's changing subbuf sizes concurrently, which forces a ring buffer resize. Okay, ring buffer resize... that operation would almost certainly involve resetting or replacing pages. Time to dig into `ring_buffer_resize`. Let's see what happens there.
|
| 29/2 |
2026/08/09 01:13 |
tool |
codesearch-find-references |
0m
Args: map[IncludeSnippetLines:0 Name:ring_buffer_resize]
Results: map[References:[map[ReferenceKind:calls ReferencingEntityKind:function ReferencingEntityName:__tracing_resize_ring_buffer SourceFile:kernel/trace/trace.c SourceLine:4738] map[ReferenceKind:takes-address-of ReferencingEntityKind:global_variable ReferencingEntityName:__UNIQUE_ID_addressable_ring_buffer_resize_1130 SourceFile:kernel/trace/ring_buffer.c SourceLine:3418] map[ReferenceKind:takes-address-of ReferencingEntityKind:global_variable ReferencingEntityName:__gendwarfksyms_ptr_ring_buffer_resize SourceFile:kernel/trace/ring_buffer.c SourceLine:3418] map[ReferenceKind:takes-address-of ReferencingEntityKind:global_variable ReferencingEntityName:__gendwarfksyms_ptr_ring_buffer_resize SourceFile:kernel/trace/ring_buffer.c SourceLine:3418]] TruncatedOutput:false]
|
| 30/2 |
2026/08/09 01:13 |
llm |
expert |
0m
Model:
Tokens:
input: 20539
output: 22
thoughts: 34
Thoughts: Okay, here's my interpretation of those thoughts, framed as if they are my own:
**Investigating a Reproducer**
Hold on a second... this reproducer is tweaking `buffer_subbuf_size_kb`. That's interesting, because when that value changes, the system is supposed to call `tracing_buffers_subbuf_size_write` or something very similar to it. This suggests a potential area to investigate further.
|
| 31/2 |
2026/08/09 01:13 |
tool |
grepper |
0m
Args: map[Expression:buffer_subbuf_size_kb]
Results: map[Output:Documentation/trace/ftrace.rst=90=of ftrace. Here is a list of some of the key files:
--
Documentation/trace/ftrace.rst-220-
Documentation/trace/ftrace.rst:221: buffer_subbuf_size_kb:
Documentation/trace/ftrace.rst-222-
--
Documentation/trace/ftrace.rst-230-
Documentation/trace/ftrace.rst:231: Note, the buffer_subbuf_size_kb is a way for the user to specify the
Documentation/trace/ftrace.rst-232- minimum size of the subbuffer. The kernel may make it bigger due to the
--
kernel/trace/trace.c=8876=init_tracer_tracefs(struct trace_array *tr, struct dentry *d_tracer)
--
kernel/trace/trace.c-8916-
kernel/trace/trace.c:8917: trace_create_file("buffer_subbuf_size_kb", writable_mode, d_tracer,
kernel/trace/trace.c-8918- tr, &buffer_subbuf_size_fops);
--
tools/testing/selftests/ftrace/test.d/00basic/ringbuffer_subbuf_size.tc-3-# description: Change the ringbuffer sub-buffer size
tools/testing/selftests/ftrace/test.d/00basic/ringbuffer_subbuf_size.tc:4:# requires: buffer_subbuf_size_kb
tools/testing/selftests/ftrace/test.d/00basic/ringbuffer_subbuf_size.tc-5-# flags: instance
--
tools/testing/selftests/ftrace/test.d/00basic/ringbuffer_subbuf_size.tc=54=test_buffer() {
--
tools/testing/selftests/ftrace/test.d/00basic/ringbuffer_subbuf_size.tc-84-
tools/testing/selftests/ftrace/test.d/00basic/ringbuffer_subbuf_size.tc:85:ORIG=`cat buffer_subbuf_size_kb`
tools/testing/selftests/ftrace/test.d/00basic/ringbuffer_subbuf_size.tc-86-
--
tools/testing/selftests/ftrace/test.d/00basic/ringbuffer_subbuf_size.tc=89=for a in 4 8 16 32 ; do
tools/testing/selftests/ftrace/test.d/00basic/ringbuffer_subbuf_size.tc:90: echo $a > buffer_subbuf_size_kb
tools/testing/selftests/ftrace/test.d/00basic/ringbuffer_subbuf_size.tc-91- test_buffer $a
tools/testing/selftests/ftrace/test.d/00basic/ringbuffer_subbuf_size.tc=92=done
tools/testing/selftests/ftrace/test.d/00basic/ringbuffer_subbuf_size.tc-93-
tools/testing/selftests/ftrace/test.d/00basic/ringbuffer_subbuf_size.tc:94:echo $ORIG > buffer_subbuf_size_kb
tools/testing/selftests/ftrace/test.d/00basic/ringbuffer_subbuf_size.tc-95-
--
tools/testing/selftests/ring-buffer/map_test.c=137=FIXTURE_SETUP(map)
--
tools/testing/selftests/ring-buffer/map_test.c-160-
tools/testing/selftests/ring-buffer/map_test.c:161: tracefs_write_int(TRACEFS_ROOT"/buffer_subbuf_size_kb", variant->subbuf_size);
tools/testing/selftests/ring-buffer/map_test.c-162-
]
|
| 32/2 |
2026/08/09 01:13 |
llm |
expert |
0m
Model:
Tokens:
input: 21673
output: 40
thoughts: 25
Thoughts: Here's a summary of my thought process:
**Diving into Trace Buffering**
Okay, I need to understand how these trace buffers are working internally. The most likely place to start is the `kernel/trace/trace.c` file. Specifically, I should look at `buffer_subbuf_size_fops` β that name suggests it's handling the size or allocation strategy related to sub-buffers within the trace infrastructure. That's probably where the meat of the memory management and I/O operations related to trace events are implemented. Let's see what's going on there.
|
| 33/2 |
2026/08/09 01:13 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:kernel/trace/trace.c Name:buffer_subbuf_size_fops]
Results: map[SourceCode:8262: static const struct file_operations buffer_subbuf_size_fops = {
8263: .open = tracing_open_generic_tr,
8264: .read = buffer_subbuf_size_read,
8265: .write = buffer_subbuf_size_write,
8266: .release = tracing_release_generic_tr,
8267: .llseek = default_llseek,
8268: };
SourceFile:kernel/trace/trace.c]
|
| 34/2 |
2026/08/09 01:13 |
llm |
expert |
0m
Model:
Tokens:
input: 22023
output: 39
thoughts: 14
Thoughts: Okay, here's my interpretation of that thought process, assuming I'm deep in the weeds of some technical problem and have a good grasp of the relevant concepts:
**Deep Dive: Checking `buffer_subbuf_size_write`**
Alright, time to get granular. Let's not dance around the issue. I need to zero in on a potential culprit in this performance bottleneck or bug, and my gut feeling, based on everything else I've seen, is pulling me towards the buffer management. Specifically, I need to check how we're handling writes to sub-buffers. The variable `buffer_subbuf_size_write` is key here. I'm thinking: is it configured correctly? Is it being updated as expected? Are we hitting any edge cases with that size? A quick diagnostic or trace of its values could reveal a lot. It's a fundamental parameter, and if it's off, it'll cascade into all sorts of problems. Let's see what the code is doing with it.
|
| 35/2 |
2026/08/09 01:13 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:kernel/trace/trace.c Name:buffer_subbuf_size_write]
Results: map[SourceCode:8190: static ssize_t
8191: buffer_subbuf_size_write(struct file *filp, const char __user *ubuf,
8192: size_t cnt, loff_t *ppos)
8193: {
8194: struct trace_array *tr = filp->private_data;
8195: unsigned long val;
8196: int old_order;
8197: int order;
8198: int pages;
8199: int ret;
8200:
8201: ret = kstrtoul_from_user(ubuf, cnt, 10, &val);
8202: if (ret)
8203: return ret;
8204:
8205: val *= 1024; /* value passed in is in KB */
8206:
8207: pages = DIV_ROUND_UP(val, PAGE_SIZE);
8208: order = fls(pages - 1);
8209:
8210: /* limit between 1 and 128 system pages */
8211: if (order < 0 || order > 7)
8212: return -EINVAL;
8213:
8214: /* Do not allow tracing while changing the order of the ring buffer */
8215: tracing_stop_tr(tr);
8216:
8217: old_order = ring_buffer_subbuf_order_get(tr->array_buffer.buffer);
8218: if (old_order == order)
8219: goto out;
8220:
8221: ret = ring_buffer_subbuf_order_set(tr->array_buffer.buffer, order);
8222: if (ret)
8223: goto out;
8224:
8225: #ifdef CONFIG_TRACER_SNAPSHOT
8226:
8227: if (!tr->allocated_snapshot)
8228: goto out_max;
8229:
8230: ret = ring_buffer_subbuf_order_set(tr->snapshot_buffer.buffer, order);
8231: if (ret) {
8232: /* Put back the old order */
8233: cnt = ring_buffer_subbuf_order_set(tr->array_buffer.buffer, old_order);
8234: if (WARN_ON_ONCE(cnt)) {
8235: /*
8236: * AARGH! We are left with different orders!
8237: * The max buffer is our "snapshot" buffer.
8238: * When a tracer needs a snapshot (one of the
8239: * latency tracers), it swaps the max buffer
8240: * with the saved snap shot. We succeeded to
8241: * update the order of the main buffer, but failed to
8242: * update the order of the max buffer. But when we tried
8243: * to reset the main buffer to the original size, we
8244: * failed there too. This is very unlikely to
8245: * happen, but if it does, warn and kill all
8246: * tracing.
8247: */
8248: tracing_disabled = 1;
8249: }
8250: goto out;
8251: }
8252: out_max:
8253: #endif
8254: (*ppos)++;
8255: out:
8256: if (ret)
8257: cnt = ret;
8258: tracing_start_tr(tr);
8259: return cnt;
8260: }
SourceFile:kernel/trace/trace.c]
|
| 36/2 |
2026/08/09 01:13 |
llm |
expert |
0m
Model:
Tokens:
input: 23333
output: 43
thoughts: 64
Thoughts: Okay, let's break this down.
**Analyzing `buffer_subbuf_size_write`**
Right, so `buffer_subbuf_size_write` is doing some interesting things. I see it's calling `tracing_stop_tr(tr)` and then immediately `tracing_start_tr(tr)`. That's a bit unusual; I need to understand why it's toggling tracing like that. Are we trying to isolate a specific section of code, or is there a subtle race condition concern? It could also be related to a specific hardware trace buffer implementation.
Furthermore, it's also making a call to `ring_buffer_subbuf_order_set`. This is what piques my interest the most, specifically related to the ring buffer implementation. `ring_buffer_subbuf_order_set` likely controls some aspect of how the sub-buffers are managed and ordered within the larger ring buffer. This impacts data placement, likely influencing performance. Let's dig deeper into the functionality of `ring_buffer_subbuf_order_set` to determine what it's doing with sub-buffer ordering.
|
| 37/2 |
2026/08/09 01:14 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:kernel/trace/ring_buffer.c Name:ring_buffer_subbuf_order_set]
Results: map[SourceCode:7300: /**
7301: * ring_buffer_subbuf_order_set - set the size of ring buffer sub page.
7302: * @buffer: The ring_buffer to set the new page size.
7303: * @order: Order of the system pages in one sub buffer page
7304: *
7305: * By default, one ring buffer pages equals to one system page. This API can be
7306: * used to set new size of the ring buffer page. The size must be order of
7307: * system page size, that's why the input parameter @order is the order of
7308: * system pages that are allocated for one ring buffer page:
7309: * 0 - 1 system page
7310: * 1 - 2 system pages
7311: * 3 - 4 system pages
7312: * ...
7313: *
7314: * Returns 0 on success or < 0 in case of an error.
7315: */
7316: int ring_buffer_subbuf_order_set(struct trace_buffer *buffer, int order)
7317: {
7318: struct ring_buffer_per_cpu *cpu_buffer;
7319: struct buffer_page *bpage, *tmp;
7320: int old_order, old_size;
7321: int nr_pages;
7322: int psize;
7323: int err;
7324: int cpu;
7325:
7326: if (!buffer || order < 0)
7327: return -EINVAL;
7328:
7329: if (buffer->subbuf_order == order)
7330: return 0;
7331:
7332: psize = (1 << order) * PAGE_SIZE;
7333: if (psize <= BUF_PAGE_HDR_SIZE)
7334: return -EINVAL;
7335:
7336: /* Size of a subbuf cannot be greater than the write counter */
7337: if (psize > RB_WRITE_MASK + 1)
7338: return -EINVAL;
7339:
7340: old_order = buffer->subbuf_order;
7341: old_size = buffer->subbuf_size;
7342:
7343: /* prevent another thread from changing buffer sizes */
7344: guard(mutex)(&buffer->mutex);
7345: atomic_inc(&buffer->record_disabled);
7346:
7347: /* Make sure all commits have finished */
7348: synchronize_rcu();
7349:
7350: buffer->subbuf_order = order;
7351: buffer->subbuf_size = psize - BUF_PAGE_HDR_SIZE;
7352:
7353: /* Make sure all new buffers are allocated, before deleting the old ones */
7354: for_each_buffer_cpu(buffer, cpu) {
7355:
7356: if (!cpumask_test_cpu(cpu, buffer->cpumask))
7357: continue;
7358:
7359: cpu_buffer = buffer->buffers[cpu];
7360:
7361: if (cpu_buffer->mapped) {
7362: err = -EBUSY;
7363: goto error;
7364: }
7365:
7366: /* Update the number of pages to match the new size */
7367: nr_pages = old_size * buffer->buffers[cpu]->nr_pages;
7368: nr_pages = DIV_ROUND_UP(nr_pages, buffer->subbuf_size);
7369:
7370: /* we need a minimum of two pages */
7371: if (nr_pages < 2)
7372: nr_pages = 2;
7373:
7374: cpu_buffer->nr_pages_to_update = nr_pages;
7375:
7376: /* Include the reader page */
7377: nr_pages++;
7378:
7379: /* Allocate the new size buffer */
7380: INIT_LIST_HEAD(&cpu_buffer->new_pages);
7381: if (__rb_allocate_pages(cpu_buffer, nr_pages,
7382: &cpu_buffer->new_pages)) {
7383: /* not enough memory for new pages */
7384: err = -ENOMEM;
7385: goto error;
7386: }
7387: }
7388:
7389: for_each_buffer_cpu(buffer, cpu) {
7390: struct buffer_data_page *old_free_data_page;
7391: struct list_head old_pages;
7392: unsigned long flags;
7393:
7394: if (!cpumask_test_cpu(cpu, buffer->cpumask))
7395: continue;
7396:
7397: cpu_buffer = buffer->buffers[cpu];
7398:
7399: raw_spin_lock_irqsave(&cpu_buffer->reader_lock, flags);
7400:
7401: /* Clear the head bit to make the link list normal to read */
7402: rb_head_page_deactivate(cpu_buffer);
7403:
7404: /*
7405: * Collect buffers from the cpu_buffer pages list and the
7406: * reader_page on old_pages, so they can be freed later when not
7407: * under a spinlock. The pages list is a linked list with no
7408: * head, adding old_pages turns it into a regular list with
7409: * old_pages being the head.
7410: */
7411: list_add(&old_pages, cpu_buffer->pages);
7412: list_add(&cpu_buffer->reader_page->list, &old_pages);
7413:
7414: /* One page was allocated for the reader page */
7415: cpu_buffer->reader_page = list_entry(cpu_buffer->new_pages.next,
7416: struct buffer_page, list);
7417: list_del_init(&cpu_buffer->reader_page->list);
7418:
7419: /* Install the new pages, remove the head from the list */
7420: cpu_buffer->pages = cpu_buffer->new_pages.next;
7421: list_del_init(&cpu_buffer->new_pages);
7422: cpu_buffer->cnt++;
7423:
7424: cpu_buffer->head_page
7425: = list_entry(cpu_buffer->pages, struct buffer_page, list);
7426: cpu_buffer->tail_page = cpu_buffer->commit_page = cpu_buffer->head_page;
7427:
7428: cpu_buffer->nr_pages = cpu_buffer->nr_pages_to_update;
7429: cpu_buffer->nr_pages_to_update = 0;
7430:
7431: old_free_data_page = cpu_buffer->free_page;
7432: cpu_buffer->free_page = NULL;
7433:
7434: rb_head_page_activate(cpu_buffer);
7435:
7436: raw_spin_unlock_irqrestore(&cpu_buffer->reader_lock, flags);
7437:
7438: /* Free old sub buffers */
7439: list_for_each_entry_safe(bpage, tmp, &old_pages, list) {
7440: list_del_init(&bpage->list);
7441: free_buffer_page(bpage);
7442: }
7443: free_pages((unsigned long)old_free_data_page, old_order);
7444:
7445: rb_check_pages(cpu_buffer);
7446: }
7447:
7448: atomic_dec(&buffer->record_disabled);
7449:
7450: return 0;
7451:
7452: error:
7453: buffer->subbuf_order = old_order;
7454: buffer->subbuf_size = old_size;
7455:
7456: atomic_dec(&buffer->record_disabled);
7457:
7458: for_each_buffer_cpu(buffer, cpu) {
7459: cpu_buffer = buffer->buffers[cpu];
7460:
7461: if (!cpu_buffer->nr_pages_to_update)
7462: continue;
7463:
7464: list_for_each_entry_safe(bpage, tmp, &cpu_buffer->new_pages, list) {
7465: list_del_init(&bpage->list);
7466: free_buffer_page(bpage);
7467: }
7468: }
7469:
7470: return err;
7471: }
SourceFile:kernel/trace/ring_buffer.c]
|
| 38/2 |
2026/08/09 01:14 |
llm |
expert |
0m
Model:
Tokens:
input: 26269
output: 36
thoughts: 307
Thoughts: **Analyzing Ring Buffer Synchronization**
Okay, let's break this down. I'm looking at `ring_buffer_subbuf_order_set`. It's allocating new pages, acquiring the `reader_lock`, swapping out the buffers, and then freeing the old pages. That makes sense, standard allocation and swap. But hang on, `ring_buffer_read_page` *also* takes the `reader_lock`!
So, `ring_buffer_subbuf_order_set` and `ring_buffer_read_page` are mutually exclusive due to the `reader_lock`. That's a clean separation, one for readers, the other reconfigures the buffers.
But what about the writer? Where is the write path? Ah, the writer is initiated by a `trace_marker` write. Following the call chain, `trace_marker_write` -> `tracing_mark_write` -> `trace_buffer_lock_reserve` -> `ring_buffer_lock_reserve`. Does the writer contend on `reader_lock`? No, it uses lockless operations in the ring buffer. That's good; this avoids unnecessary contention.
However, `ring_buffer_subbuf_order_set` does call `atomic_inc(&buffer->record_disabled);` and `synchronize_rcu()`. Hmmm, is there a potential race here? Let's consider: `tracing_stop_tr(tr)` in `buffer_subbuf_size_write` disables tracing. Does `trace_marker` respect this disable flag? Maybe `trace_marker` doesn't check it? I need to check `trace_marker_write` to be sure.
|
| 39/2 |
2026/08/09 01:14 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:kernel/trace/trace.c Name:tracing_mark_write]
Results: map[SourceCode:6285: static ssize_t
6286: tracing_mark_write(struct file *filp, const char __user *ubuf,
6287: size_t cnt, loff_t *fpos)
6288: {
6289: struct trace_array *tr = filp->private_data;
6290: ssize_t written = -ENODEV;
6291: unsigned long ip;
6292: char *buf;
6293:
6294: if (unlikely(tracing_disabled))
6295: return -EINVAL;
6296:
6297: if (!(tr->trace_flags & TRACE_ITER(MARKERS)))
6298: return -EINVAL;
6299:
6300: if ((ssize_t)cnt < 0)
6301: return -EINVAL;
6302:
6303: if (cnt > TRACE_MARKER_MAX_SIZE)
6304: cnt = TRACE_MARKER_MAX_SIZE;
6305:
6306: /* Must have preemption disabled while having access to the buffer */
6307: guard(preempt_notrace)();
6308:
6309: buf = trace_user_fault_read(trace_user_buffer, ubuf, cnt, NULL, NULL);
6310: if (!buf)
6311: return -EFAULT;
6312:
6313: /* The selftests expect this function to be the IP address */
6314: ip = _THIS_IP_;
6315:
6316: /* The global trace_marker can go to multiple instances */
6317: if (tr == &global_trace) {
6318: guard(rcu)();
6319: list_for_each_entry_rcu(tr, &marker_copies, marker_list) {
6320: written = write_marker_to_buffer(tr, buf, cnt, ip);
6321: if (written < 0)
6322: break;
6323: }
6324: } else {
6325: written = write_marker_to_buffer(tr, buf, cnt, ip);
6326: }
6327:
6328: return written;
6329: }
SourceFile:kernel/trace/trace.c]
|
| 40/2 |
2026/08/09 01:14 |
llm |
expert |
0m
Model:
Tokens:
input: 27652
output: 39
thoughts: 85
Thoughts: **Tracing Buffer Lock Dive**
Okay, so I'm looking at how `write_marker_to_buffer` interacts with the tracing buffer, and it looks like it calls `__trace_buffer_lock_reserve`. My first thought is to check what's going on with `buffer->record_disabled` in there. Ah, right, that flag! `ring_buffer_subbuf_order_set` increases that. But, wait, that doesn't fully explain all the disable scenarios. I remember `tracing_stop_tr(tr)` also disables tracing. So, the question remains. Let's dig deeper into how `__trace_buffer_lock_reserve` actually behaves in all these scenarios. Need to confirm that the disabling logic is consistent and covers all the bases.
|
| 41/2 |
2026/08/09 01:14 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:kernel/trace/trace.c Name:__trace_buffer_lock_reserve]
Results: map[SourceCode:1668: static __always_inline struct ring_buffer_event *
1669: __trace_buffer_lock_reserve(struct trace_buffer *buffer,
1670: int type,
1671: unsigned long len,
1672: unsigned int trace_ctx)
1673: {
1674: struct ring_buffer_event *event;
1675:
1676: event = ring_buffer_lock_reserve(buffer, len);
1677: if (event != NULL)
1678: trace_event_setup(event, type, trace_ctx);
1679:
1680: return event;
1681: }
SourceFile:kernel/trace/trace.h]
|
| 42/2 |
2026/08/09 01:14 |
llm |
expert |
0m
Model:
Tokens:
input: 28163
output: 40
thoughts: 13
Thoughts: Here's my attempt at summarizing the thought process, tailored for an expert audience and written in the first person:
**Diving into `ring_buffer_lock_reserve`**
Okay, time to circle back and examine `ring_buffer_lock_reserve`. My initial thought process now is: Let's check this function, likely integral to the lock's operation, for performance implications. I'm especially keen to see how it interacts with the underlying ring buffer data structure. Are we encountering contention issues here? Are there any potential bottlenecks in how reservations are handled? This function holds the key to concurrency control within the buffer; It's important to understand the locking protocol and any potential lock convoys or starvation scenarios it might introduce. I'll need to look at whether it's optimized for the expected workload, and the implications of using this particular locking primitive. What are the read-write semantics? Are there any unexpected side effects related to this method of making space in the buffer? I need to analyze its behavior under different concurrency levels and buffer utilization rates. So, `ring_buffer_lock_reserve`, it is. Let's see what's in there.
|
| 43/2 |
2026/08/09 01:14 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:kernel/trace/ring_buffer.c Name:ring_buffer_lock_reserve]
Results: map[SourceCode:4940: /**
4941: * ring_buffer_lock_reserve - reserve a part of the buffer
4942: * @buffer: the ring buffer to reserve from
4943: * @length: the length of the data to reserve (excluding event header)
4944: *
4945: * Returns a reserved event on the ring buffer to copy directly to.
4946: * The user of this interface will need to get the body to write into
4947: * and can use the ring_buffer_event_data() interface.
4948: *
4949: * The length is the length of the data needed, not the event length
4950: * which also includes the event header.
4951: *
4952: * Must be paired with ring_buffer_unlock_commit, unless NULL is returned.
4953: * If NULL is returned, then nothing has been allocated or locked.
4954: */
4955: struct ring_buffer_event *
4956: ring_buffer_lock_reserve(struct trace_buffer *buffer, unsigned long length)
4957: {
4958: struct ring_buffer_per_cpu *cpu_buffer;
4959: struct ring_buffer_event *event;
4960: int cpu;
4961:
4962: /* If we are tracing schedule, we don't want to recurse */
4963: preempt_disable_notrace();
4964:
4965: if (unlikely(atomic_read(&buffer->record_disabled)))
4966: goto out;
4967:
4968: cpu = raw_smp_processor_id();
4969:
4970: if (unlikely(!cpumask_test_cpu(cpu, buffer->cpumask)))
4971: goto out;
4972:
4973: cpu_buffer = buffer->buffers[cpu];
4974:
4975: if (unlikely(atomic_read(&cpu_buffer->record_disabled)))
4976: goto out;
4977:
4978: if (unlikely(length > buffer->max_data_size))
4979: goto out;
4980:
4981: if (unlikely(trace_recursive_lock(cpu_buffer)))
4982: goto out;
4983:
4984: event = rb_reserve_next_event(buffer, cpu_buffer, length);
4985: if (!event)
4986: goto out_unlock;
4987:
4988: return event;
4989:
4990: out_unlock:
4991: trace_recursive_unlock(cpu_buffer);
4992: out:
4993: preempt_enable_notrace();
4994: return NULL;
4995: }
SourceFile:kernel/trace/ring_buffer.c]
|
| 44/2 |
2026/08/09 01:14 |
llm |
expert |
0m
Model:
Tokens:
input: 29283
output: 41
thoughts: 1138
Thoughts: **Analyzing a Potential Race Condition in the Ring Buffer Subbuffer Size Change**
Okay, let's break this down. My initial thought was that `ring_buffer_subbuf_order_set` was correctly preventing concurrent access by setting `buffer->record_disabled` and using `synchronize_rcu()`. The idea was that writers would be blocked, and readers would either see the disabled state or complete before the size change.
The warning in `__rb_get_reader_page` threw a wrench in that assumption. This code is designed to wait if a writer is actively writing to a page. The `udelay(1)` loop suggests it's waiting for a write operation to complete. I considered that `ring_buffer_subbuf_order_set` and `ring_buffer_read_page` *could* run concurrently, but that's impossible because they both take the `cpu_buffer->reader_lock`. So, that can't be it. They are mutually exclusive.
The critical piece seems to be the order of operations in `ring_buffer_subbuf_order_set`. It modifies `buffer->subbuf_size` *before* taking the `reader_lock`. This is where the issue lies. `ring_buffer_read_page` is where `__rb_get_reader_page` is called. It only takes the `cpu_buffer->reader_lock`, not `buffer->mutex`. This means a reader thread could be running concurrently with the size change, *up to the point* that `ring_buffer_subbuf_order_set` acquires `reader_lock`.
If `ring_buffer_subbuf_order_set` changes the size *smaller* (say, 16KB to 4KB), then the reader can get the *new* size (`bsize`), and the *old* (larger) reader page. The warning triggers because the `rb_page_write(reader)` counter (representing the current write position on the *old* 16KB page) could be greater than the `bsize` (now the *new* 4KB size). The loop in `__rb_get_reader_page` fails the `likely(!reader || rb_page_write(reader) <= bsize)` test and ultimately triggers the warning, because it sees a write position beyond the new subbuffer's bounds. This is a classic race condition scenario, where the reader thread sees an inconsistent view of the buffer's state. It is caused by the unprotected and untimely change of `buffer->subbuf_size`.
|
| 45/2 |
2026/08/09 01:14 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:kernel/trace/ring_buffer.c Name:__rb_get_reader_page]
Results: map[SourceCode:5795: static struct buffer_page *
5796: __rb_get_reader_page(struct ring_buffer_per_cpu *cpu_buffer)
5797: {
5798: int max_loops = cpu_buffer->ring_meta ? cpu_buffer->nr_pages : 3;
5799: unsigned long bsize = READ_ONCE(cpu_buffer->buffer->subbuf_size);
5800: struct buffer_page *reader = NULL;
5801: unsigned long overwrite;
5802: unsigned long flags;
5803: int missed_events = 0;
5804: int nr_loops = 0;
5805: bool ret;
5806:
5807: local_irq_save(flags);
5808: arch_spin_lock(&cpu_buffer->lock);
5809:
5810: again:
5811: /*
5812: * This should normally only loop twice. But because the
5813: * start of the reader inserts an empty page, it causes a
5814: * case where we will loop three times. There should be no
5815: * reason to loop four times unless the ring buffer is a
5816: * recovered persistent ring buffer. For persistent ring buffers,
5817: * invalid pages are reset during recovery, so there may be more
5818: * than 3 contiguous pages can be empty, but less than nr_pages.
5819: */
5820: if (RB_WARN_ON(cpu_buffer, ++nr_loops > max_loops)) {
5821: reader = NULL;
5822: goto out;
5823: }
5824:
5825: reader = cpu_buffer->reader_page;
5826:
5827: /* If there's more to read, return this page */
5828: if (cpu_buffer->reader_page->read < rb_page_size(reader))
5829: goto out;
5830:
5831: /* Never should we have an index greater than the size */
5832: if (RB_WARN_ON(cpu_buffer,
5833: cpu_buffer->reader_page->read > rb_page_size(reader)))
5834: goto out;
5835:
5836: /* check if we caught up to the tail */
5837: reader = NULL;
5838: if (cpu_buffer->commit_page == cpu_buffer->reader_page)
5839: goto out;
5840:
5841: /* Don't bother swapping if the ring buffer is empty */
5842: if (rb_num_of_entries(cpu_buffer) == 0)
5843: goto out;
5844:
5845: /*
5846: * Reset the reader page to size zero.
5847: */
5848: local_set(&cpu_buffer->reader_page->write, 0);
5849: local_set(&cpu_buffer->reader_page->entries, 0);
5850: rb_init_data_page(cpu_buffer->reader_page->page);
5851: cpu_buffer->reader_page->real_end = 0;
5852:
5853: spin:
5854: /*
5855: * Splice the empty reader page into the list around the head.
5856: */
5857: reader = rb_set_head_page(cpu_buffer);
5858: if (!reader)
5859: goto out;
5860: cpu_buffer->reader_page->list.next = rb_list_head(reader->list.next);
5861: cpu_buffer->reader_page->list.prev = reader->list.prev;
5862:
5863: /*
5864: * cpu_buffer->pages just needs to point to the buffer, it
5865: * has no specific buffer page to point to. Lets move it out
5866: * of our way so we don't accidentally swap it.
5867: */
5868: cpu_buffer->pages = reader->list.prev;
5869:
5870: /* The reader page will be pointing to the new head */
5871: rb_set_list_to_head(&cpu_buffer->reader_page->list);
5872:
5873: /*
5874: * We want to make sure we read the overruns after we set up our
5875: * pointers to the next object. The writer side does a
5876: * cmpxchg to cross pages which acts as the mb on the writer
5877: * side. Note, the reader will constantly fail the swap
5878: * while the writer is updating the pointers, so this
5879: * guarantees that the overwrite recorded here is the one we
5880: * want to compare with the last_overrun.
5881: */
5882: smp_mb();
5883: overwrite = local_read(&(cpu_buffer->overrun));
5884:
5885: /*
5886: * Here's the tricky part.
5887: *
5888: * We need to move the pointer past the header page.
5889: * But we can only do that if a writer is not currently
5890: * moving it. The page before the header page has the
5891: * flag bit '1' set if it is pointing to the page we want.
5892: * but if the writer is in the process of moving it
5893: * then it will be '2' or already moved '0'.
5894: */
5895:
5896: ret = rb_head_page_replace(reader, cpu_buffer->reader_page);
5897:
5898: /*
5899: * If we did not convert it, then we must try again.
5900: */
5901: if (!ret)
5902: goto spin;
5903:
5904: if (rb_page_commit(reader) & RB_MISSED_EVENTS)
5905: missed_events = -1;
5906:
5907: if (cpu_buffer->ring_meta)
5908: rb_update_meta_reader(cpu_buffer, reader);
5909:
5910: /*
5911: * Yay! We succeeded in replacing the page.
5912: *
5913: * Now make the new head point back to the reader page.
5914: */
5915: rb_list_head(reader->list.next)->prev = &cpu_buffer->reader_page->list;
5916: rb_inc_page(&cpu_buffer->head_page);
5917:
5918: cpu_buffer->cnt++;
5919: local_inc(&cpu_buffer->pages_read);
5920:
5921: /* Finally update the reader page to the new head */
5922: cpu_buffer->reader_page = reader;
5923: cpu_buffer->reader_page->read = 0;
5924:
5925: if (overwrite != cpu_buffer->last_overrun) {
5926: cpu_buffer->lost_events = overwrite - cpu_buffer->last_overrun;
5927: cpu_buffer->last_overrun = overwrite;
5928: }
5929:
5930: goto again;
5931:
5932: out:
5933: /* Update the read_stamp on the first event */
5934: if (reader && reader->read == 0)
5935: cpu_buffer->read_stamp = reader->page->time_stamp;
5936:
5937: arch_spin_unlock(&cpu_buffer->lock);
5938: local_irq_restore(flags);
5939:
5940: /*
5941: * The writer has preempt disable, wait for it. But not forever
5942: * Although, 1 second is pretty much "forever"
5943: */
5944: #define USECS_WAIT 1000000
5945: for (nr_loops = 0; nr_loops < USECS_WAIT; nr_loops++) {
5946: /* If the write is past the end of page, a writer is still updating it */
5947: if (likely(!reader || rb_page_write(reader) <= bsize))
5948: break;
5949:
5950: udelay(1);
5951:
5952: /* Get the latest version of the reader write value */
5953: smp_rmb();
5954: }
5955:
5956: /* The writer is not moving forward? Something is wrong */
5957: if (RB_WARN_ON(cpu_buffer, nr_loops == USECS_WAIT))
5958: reader = NULL;
5959:
5960: /*
5961: * Make sure we see any padding after the write update
5962: * (see rb_reset_tail()).
5963: *
5964: * In addition, a writer may be writing on the reader page
5965: * if the page has not been fully filled, so the read barrier
5966: * is also needed to make sure we see the content of what is
5967: * committed by the writer (see rb_set_commit_to_write()).
5968: */
5969: smp_rmb();
5970:
5971: if (!cpu_buffer->lost_events)
5972: cpu_buffer->lost_events = missed_events;
5973:
5974: return reader;
5975: }
SourceFile:kernel/trace/ring_buffer.c]
|
| 46/2 |
2026/08/09 01:14 |
llm |
expert |
0m
Model:
Tokens:
input: 33771
output: 34
thoughts: 1822
Thoughts: **Race Condition in Ring Buffer Subbuffer Size Change**
Okay, so here's what's happening. I've been looking at a race condition related to changing the subbuffer size within the ring buffer. It all stems from how `__rb_get_reader_page` handles updates to `bsize` when called concurrently with `ring_buffer_subbuf_order_set`.
Here's the problem: `__rb_get_reader_page` reads `bsize` (the subbuffer size) *before* it takes the `cpu_buffer->lock`, *before* the reader page swap, but *after* the `reader_lock` is taken. Meanwhile, `ring_buffer_subbuf_order_set` *first* grabs the `buffer->mutex`, changes `buffer->subbuf_size`, then tries to take the `cpu_buffer->reader_lock`.
So, the race is this: if `ring_buffer_subbuf_order_set` changes `buffer->subbuf_size` *before* `__rb_get_reader_page` gets to read it, then the reader thread using `__rb_get_reader_page` will read the *old* value of `bsize`. And then the code goes on with an old page size, which is not what's expected.
Specifically, if `ring_buffer_subbuf_order_set` sets `buffer->subbuf_size` to, say, 4KB, while the underlying pages are still 16KB, `__rb_get_reader_page` could read this old subbuf_size, swap out a 16KB page, and then compare the amount of data written to that 16KB page against the *smaller* 4KB `bsize`. Since the page is actually 16KB, it'll trigger the warning because `rb_page_write(reader) <= bsize` will be false.
So, this results in a false positive warning. The ring buffer *thinks* the writer is writing past the end of the page, but that's not the case. It's just a miscomparison between the *old* `subbuf_size` and the *new* amount of written data, due to an out-of-order `reader_lock` acquisition.
The warning appears, the reader page gets set to `NULL`, `ring_buffer_read_page` returns `-1`, and the caller gracefully handles the error. I checked - the ring buffer doesn't leak or corrupt memory, and there is no crash or hang other than a 1-second delay.
The reader page that gets swapped out is not lost. The next time a read happens, the new reader page is swapped out. The old one is freed after.
The `rb_page_write` is just reading a counter, so there are no out-of-bounds accesses there.
One final note: this race condition is only accessible to users with root privileges (or those with explicit tracefs access). Even though access can be explicitly enabled, by default, this is not something unprivileged users can trigger. And it can't be triggered in a user namespace.
|
| 47/2 |
2026/08/09 01:14 |
tool |
codesearch-find-references |
0m
Args: map[IncludeSnippetLines:0 Name:ring_buffer_subbuf_order_set]
Results: map[References:[map[ReferenceKind:calls ReferencingEntityKind:function ReferencingEntityName:buffer_subbuf_size_write SourceFile:kernel/trace/trace.c SourceLine:8221] map[ReferenceKind:takes-address-of ReferencingEntityKind:global_variable ReferencingEntityName:__UNIQUE_ID_addressable_ring_buffer_subbuf_order_set_1268 SourceFile:kernel/trace/ring_buffer.c SourceLine:7472] map[ReferenceKind:takes-address-of ReferencingEntityKind:global_variable ReferencingEntityName:__gendwarfksyms_ptr_ring_buffer_subbuf_order_set SourceFile:kernel/trace/ring_buffer.c SourceLine:7472] map[ReferenceKind:takes-address-of ReferencingEntityKind:global_variable ReferencingEntityName:__gendwarfksyms_ptr_ring_buffer_subbuf_order_set SourceFile:kernel/trace/ring_buffer.c SourceLine:7472]] TruncatedOutput:false]
|
| 48/2 |
2026/08/09 01:14 |
llm |
expert |
0m
Model:
Tokens:
input: 36604
output: 40
thoughts: 920
Thoughts: **Analysis of Potential Tracefs Vulnerability**
Okay, so I've been looking at this code path, specifically the warning that's being generated. It looks like this warning originates solely from `buffer_subbuf_size_write`, which is the write handler for `buffer_subbuf_size_kb`. That means we're dealing with a Tracefs issue, and nothing else.
First, I need to verify if unprivileged users can actually trigger this. The `buffer_subbuf_size_kb` file is created with `TRACE_MODE_WRITE` (0640). The owner is root, and that means unprivileged users can't write to it. That's a critical piece of information.
Therefore, my initial assessment is:
* **Exploitable:** No. This is just a WARNING. It's the result of a race condition and a stale size comparison against a valid write counter, but it doesn't lead to anything dangerous.
* **Denial of Service:** No. The warning causes a small (1-second) delay in the reader thread. That's not a denial of service.
* **Accessible From Unprivileged Processes:** No. Tracefs requires root privileges.
* **Accessible From User Namespaces:** No. Tracefs can't be mounted in userns.
* **Trigger Types:** No. (Vulnerable to VM guest/host, Network, Perhipheral, or malicious filesystem triggers)
Now, the important question β could there be a memory corruption risk lurking in here? Let's consider the scenario where `ring_buffer_subbuf_order_set` changes `buffer->subbuf_size` to a larger value, let's say from 4KB to 16KB. Let's see what happens...
`bsize` in `__rb_get_reader_page` becomes 16KB. The page size is 4KB. `rb_page_write(reader)` will be at most 4KB. The comparison, `rb_page_write(reader) <= bsize` becomes `4KB <= 16KB`, which is true, and the loop breaks. We get a `reader` page returned, which goes into `ring_buffer_read_page`.
Inside `ring_buffer_read_page`...
```c
read = reader->read;
commit = rb_page_commit(reader);
size = rb_page_size(reader);
```
Since we're using a 4KB page, `commit` will be at most 4KB and therefore, `size` will be at most 4KB. The code then copies data up to `size`, so the copy is safe within the 4KB page. No out-of-bounds read there. Good.
What if `ring_buffer_subbuf_order_set` frees the old pages while `ring_buffer_read_page` is using them? That would be bad, very bad.
But wait, `ring_buffer_subbuf_order_set` *does* take `reader_lock` before freeing the old pages!
```c
raw_spin_lock_irqsave(&cpu_buffer->reader_lock, flags);
...
list_add(&old_pages, cpu_buffer->pages);
list_add(&cpu_buffer->reader_page->list, &old_pages);
...
cpu_buffer->reader_page = list_entry(cpu_buffer->new_pages.next, ...);
...
raw_spin_unlock_irqrestore(&cpu_buffer->reader_lock, flags);
/* Free old sub buffers */
list_for_each_entry_safe(bpage, tmp, &old_pages, list) {
list_del_init(&bpage->list);
free_buffer_page(bpage);
}
```
So `ring_buffer_subbuf_order_set` waits for the reader thread to release `reader_lock`. Once `ring_buffer_read_page` has released it, the `reader` page is no longer in use, as it has either copied the data or otherwise finished it's operation. Now, *that* assumes that `ring_buffer_read_page` releases the lock *before* passing the data to user space. It should be safe... but I need to double-check that `ring_buffer_read_page` doesn't hold `reader_lock` while copying to user space. I need to look closer.
|
| 49/2 |
2026/08/09 01:15 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:kernel/trace/ring_buffer.c Name:ring_buffer_read_page]
Results: map[SourceCode:7030: /**
7031: * ring_buffer_read_page - extract a page from the ring buffer
7032: * @buffer: buffer to extract from
7033: * @data_page: the page to use allocated from ring_buffer_alloc_read_page
7034: * @len: amount to extract
7035: * @cpu: the cpu of the buffer to extract
7036: * @full: should the extraction only happen when the page is full.
7037: *
7038: * This function will pull out a page from the ring buffer and consume it.
7039: * @data_page must be the address of the variable that was returned
7040: * from ring_buffer_alloc_read_page. This is because the page might be used
7041: * to swap with a page in the ring buffer.
7042: *
7043: * for example:
7044: * rpage = ring_buffer_alloc_read_page(buffer, cpu);
7045: * if (IS_ERR(rpage))
7046: * return PTR_ERR(rpage);
7047: * ret = ring_buffer_read_page(buffer, rpage, len, cpu, 0);
7048: * if (ret >= 0)
7049: * process_page(ring_buffer_read_page_data(rpage), ret);
7050: * ring_buffer_free_read_page(buffer, cpu, rpage);
7051: *
7052: * When @full is set, the function will not return true unless
7053: * the writer is off the reader page.
7054: *
7055: * Note: it is up to the calling functions to handle sleeps and wakeups.
7056: * The ring buffer can be used anywhere in the kernel and can not
7057: * blindly call wake_up. The layer that uses the ring buffer must be
7058: * responsible for that.
7059: *
7060: * Returns:
7061: * >=0 if data has been transferred, returns the offset of consumed data.
7062: * <0 if no data has been transferred.
7063: */
7064: int ring_buffer_read_page(struct trace_buffer *buffer,
7065: struct buffer_data_read_page *data_page,
7066: size_t len, int cpu, int full)
7067: {
7068: struct ring_buffer_per_cpu *cpu_buffer = buffer->buffers[cpu];
7069: struct ring_buffer_event *event;
7070: struct buffer_data_page *dpage;
7071: struct buffer_page *reader;
7072: long missed_events;
7073: unsigned int commit;
7074: unsigned int size;
7075: unsigned int read;
7076: u64 save_timestamp;
7077: bool force_memcpy;
7078:
7079: if (!cpumask_test_cpu(cpu, buffer->cpumask))
7080: return -1;
7081:
7082: /*
7083: * If len is not big enough to hold the page header, then
7084: * we can not copy anything.
7085: */
7086: if (len <= BUF_PAGE_HDR_SIZE)
7087: return -1;
7088:
7089: len -= BUF_PAGE_HDR_SIZE;
7090:
7091: if (!data_page || !data_page->data)
7092: return -1;
7093:
7094: if (data_page->order != buffer->subbuf_order)
7095: return -1;
7096:
7097: dpage = data_page->data;
7098: if (!dpage)
7099: return -1;
7100:
7101: guard(raw_spinlock_irqsave)(&cpu_buffer->reader_lock);
7102:
7103: reader = rb_get_reader_page(cpu_buffer);
7104: if (!reader)
7105: return -1;
7106:
7107: event = rb_reader_event(cpu_buffer);
7108:
7109: read = reader->read;
7110: commit = rb_page_commit(reader);
7111: size = rb_page_size(reader);
7112:
7113: /* Check if any events were dropped */
7114: missed_events = cpu_buffer->lost_events;
7115:
7116: force_memcpy = cpu_buffer->mapped || cpu_buffer->remote;
7117:
7118: /*
7119: * If this page has been partially read or
7120: * if len is not big enough to read the rest of the page or
7121: * a writer is still on the page, then
7122: * we must copy the data from the page to the buffer.
7123: * Otherwise, we can simply swap the page with the one passed in.
7124: */
7125: if (read || (len < (size - read)) ||
7126: cpu_buffer->reader_page == cpu_buffer->commit_page ||
7127: force_memcpy) {
7128: struct buffer_data_page *rpage = cpu_buffer->reader_page->page;
7129: unsigned int rpos = read;
7130: unsigned int pos = 0;
7131: unsigned int event_size;
7132: unsigned int flags = 0;
7133:
7134: /*
7135: * If a full page is expected, this can still be returned
7136: * if there's been a previous partial read and the
7137: * rest of the page can be read and the commit page is off
7138: * the reader page.
7139: */
7140: if (full &&
7141: (!read || (len < (size - read)) ||
7142: cpu_buffer->reader_page == cpu_buffer->commit_page))
7143: return -1;
7144:
7145: if (len > (size - read))
7146: len = (size - read);
7147:
7148: /* Always keep the time extend and data together */
7149: event_size = rb_event_ts_length(event);
7150:
7151: if (len < event_size)
7152: return -1;
7153:
7154: if (commit & RB_MISSED_EVENTS)
7155: flags = RB_MISSED_EVENTS;
7156:
7157: /* save the current timestamp, since the user will need it */
7158: save_timestamp = cpu_buffer->read_stamp;
7159:
7160: /* Need to copy one event at a time */
7161: do {
7162: /* We need the size of one event, because
7163: * rb_advance_reader only advances by one event,
7164: * whereas rb_event_ts_length may include the size of
7165: * one or two events.
7166: * We have already ensured there's enough space if this
7167: * is a time extend. */
7168: event_size = rb_event_length(event);
7169: memcpy(dpage->data + pos, rpage->data + rpos, event_size);
7170:
7171: len -= event_size;
7172:
7173: rb_advance_reader(cpu_buffer);
7174: rpos = reader->read;
7175: pos += event_size;
7176:
7177: if (rpos >= size)
7178: break;
7179:
7180: event = rb_reader_event(cpu_buffer);
7181: /* Always keep the time extend and data together */
7182: event_size = rb_event_ts_length(event);
7183: } while (len >= event_size);
7184:
7185: /* update dpage */
7186: local_set(&dpage->commit, pos | flags);
7187: dpage->time_stamp = save_timestamp;
7188:
7189: /* we copied everything to the beginning */
7190: read = 0;
7191: } else {
7192: /* update the entry counter */
7193: cpu_buffer->read += rb_page_entries(reader);
7194: cpu_buffer->read_bytes += rb_page_size(reader);
7195:
7196: /* swap the pages */
7197: rb_init_data_page(dpage);
7198: dpage = reader->page;
7199: reader->page = data_page->data;
7200: local_set(&reader->write, 0);
7201: local_set(&reader->entries, 0);
7202: reader->read = 0;
7203: data_page->data = dpage;
7204: if (!missed_events && rb_data_page_commit(dpage) & RB_MISSED_EVENTS)
7205: missed_events = -1;
7206:
7207: /*
7208: * Use the real_end for the data size,
7209: * This gives us a chance to store the lost events
7210: * on the page.
7211: */
7212: if (reader->real_end)
7213: local_set(&dpage->commit, reader->real_end);
7214: }
7215:
7216: cpu_buffer->lost_events = 0;
7217:
7218: size = rb_data_page_size(dpage);
7219: /*
7220: * Set a flag in the commit field if we lost events
7221: */
7222: if (missed_events) {
7223: /*
7224: * If there is room at the end of the page to save the
7225: * missed events, then record it there.
7226: */
7227: if (missed_events > 0 &&
7228: buffer->subbuf_size - size >= sizeof(missed_events)) {
7229: memcpy(&dpage->data[size], &missed_events,
7230: sizeof(missed_events));
7231: local_add(RB_MISSED_STORED, &dpage->commit);
7232: size += sizeof(missed_events);
7233: }
7234: /*
7235: * Note, for the persistent ring buffer, the RB_MISSED_EVENTS
7236: * may have been set in the main buffer via the verification code.
7237: * But here, dpage is a copy of that page and has not yet had
7238: * the RB_MISSED_EVENTS set. As for the normal buffers,
7239: * the main write buffer does not set these bits and it needs
7240: * to be set here.
7241: */
7242: local_add(RB_MISSED_EVENTS, &dpage->commit);
7243: }
7244:
7245: /*
7246: * This page may be off to user land. Zero it out here.
7247: */
7248: if (size < buffer->subbuf_size)
7249: memset(&dpage->data[size], 0, buffer->subbuf_size - size);
7250:
7251: return read;
7252: }
SourceFile:kernel/trace/ring_buffer.c]
|
| 50/2 |
2026/08/09 01:15 |
llm |
expert |
0m
Model:
Tokens:
input: 41950
output: 38
thoughts: 1490
Thoughts: **Analyzing Ring Buffer Page Management during Order Changes**
Okay, so I'm looking at how `ring_buffer_read_page` interacts with page order changes in this ring buffer implementation. My first thought is that this function, which copies data, holds the `reader_lock` for its entire duration, whether it's copying data directly into `data_page->data` or swapping pages. That's a critical point for synchronization. The caller, `tracing_buffers_read`, later copies from `data_page` to user space, so `ring_buffer_read_page` is not directly involved in the user-space copy.
The `reader_lock` is key here. Because `ring_buffer_read_page` holds the lock while it's using a page, `ring_buffer_subbuf_order_set` *can't* free those pages until the read operation is complete. Now, the code swaps pages: `reader->page` ends up holding the page from `data_page->data`, and the old `reader->page` is what's returned. `ring_buffer_subbuf_order_set` will then free that old page.
Here's the problem: The free function, `free_buffer_page`, calls `free_page` on `bpage->page`. If the swap happened, `bpage->page` would hold a page from `data_page->data`. This raises a red flag. The thing is allocated via `ring_buffer_alloc_read_page`, which allocates a page of the *new* order if `buffer->subbuf_order` changes.
However, there's a safety check: `ring_buffer_read_page` checks `data_page->order` against `buffer->subbuf_order`. If `buffer->subbuf_order` is changed *before* `ring_buffer_read_page` grabs the `reader_lock`, a mismatch is detected, and `-1` is returned. Now, `data_page` is allocated within `tracing_buffers_read`, so if `buffer->subbuf_order` has changed, the orders might not match! This means that `ring_buffer_read_page` returns an error (`-EAGAIN` or it will wait). And because it errors out it does not swap the page, and the old order page is freed. This is fine.
However, what if `data_page->order` *does* match the *new* order? The function proceeds, eventually swapping the `reader_page` with a page from the ring buffer. Crucially, the ring buffer at this point still contains the old order pages because `ring_buffer_subbuf_order_set` hasn't done its thing. `rb_get_reader_page` swaps the `reader_page` with the *old* page, then `ring_buffer_read_page` does the swap.
Here's the problem: `reader->page` now points to the *new* order page, and the old page is returned in `dpage`. `ring_buffer_subbuf_order_set` *then* takes the `reader_lock`, replaces the pages, and frees the *old* `buffer_page` via `free_buffer_page`. However, `bpage->page` is the new order page, but `free_buffer_page` calls `free_page` on it, which will free a single page instead of the higher-order page. This *is* a problem. I'll need to double check the code around `free_buffer_page` to be sure.
|
| 51/2 |
2026/08/09 01:15 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:kernel/trace/ring_buffer.c Name:free_buffer_page]
Results: map[SourceCode: 394: static void free_buffer_page(struct buffer_page *bpage)
395: {
396: /* Range pages are not to be freed */
397: if (!bpage->range)
398: free_pages((unsigned long)bpage->page, bpage->order);
399: kfree(bpage);
400: }
SourceFile:kernel/trace/ring_buffer.c]
|
| 52/2 |
2026/08/09 01:15 |
llm |
expert |
0m
Model:
Tokens:
input: 44396
output: 36
thoughts: 766
Thoughts: **Memory Order Mismatch in Ring Buffer Page Handling**
Okay, so I'm digging into this `free_buffer_page` function, and the alarm bells are starting to ring. It uses `bpage->order`, which is fine, but I'm looking at how `ring_buffer_read_page` swaps pages and I'm seeing a potential problem. It seems to swap `reader->page` with `data_page->data` without updating `reader->order`.
Let me look at that `ring_buffer_read_page` function again. Yep, there it is: swapping the pages. But the order isn't updated! Now, `reader` is a `struct buffer_page`. If `reader->order` is still the *old* order and `reader->page` is *now* the data page's data (which is the *new* order), then `free_buffer_page` will be freeing that new order page using the *old* order! This is not good.
Is this even possible? Let's trace it. `ring_buffer_subbuf_order_set` sets the buffer's sub buffer order to the new order, okay. Then `tracing_buffers_read` calls `ring_buffer_alloc_read_page`, which allocates a `data_page` with the new order. Makes sense. Then `tracing_buffers_read` calls `ring_buffer_read_page`. Inside, we see the check matching the page and sub buffer order - both are new orders. Now `ring_buffer_read_page` takes the reader lock. Then `rb_get_reader_page` is called, which swaps `cpu_buffer->reader_page` with the head of the ring buffer, which still has the old pages, because `reader_lock` wasn't taken before.
And *then* that's where the magic happens and things get messy: the swap occurs between `reader->page` and `data_page->data`. `reader->page` *was* an old order page. Now it's a new order page. And `data_page->data` went the other way. The function returns. Next, `ring_buffer_subbuf_order_set` takes the reader lock, replaces the pages, and frees the old buffer pages. Now comes `free_buffer_page(bpage)`. `bpage->order` is *still* the old order! But `bpage->page` now contains the *new* order page. So it's calling `free_pages(bpage->page, bpage->order)`.
If the new order is greater than the old, it's freeing a large page as a small page - potential memory leak. If the new order is smaller than the old, it's freeing a small page as a large page, causing memory corruption because it might not own all the pages.
So, I need to know if `bpage->order` is actually being used. Time to check `struct buffer_page`. This seems like a critical potential bug.
|
| 53/2 |
2026/08/09 01:15 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:kernel/trace/ring_buffer.c Name:buffer_page]
Results: map[SourceCode: 333: /*
334: * Note, the buffer_page list must be first. The buffer pages
335: * are allocated in cache lines, which means that each buffer
336: * page will be at the beginning of a cache line, and thus
337: * the least significant bits will be zero. We use this to
338: * add flags in the list struct pointers, to make the ring buffer
339: * lockless.
340: */
341: struct buffer_page {
342: struct list_head list; /* list of buffer pages */
343: local_t write; /* index for next write */
344: unsigned read; /* index for next read */
345: local_t entries; /* entries on this page */
346: unsigned long real_end; /* real end of data */
347: unsigned order; /* order of the page */
348: u32 id:30; /* ID for external mapping */
349: u32 range:1; /* Mapped via a range */
350: struct buffer_data_page *page; /* Actual data page */
351: };
SourceFile:kernel/trace/ring_buffer.c]
|
| 54/2 |
2026/08/09 01:15 |
llm |
expert |
0m
Model:
Tokens:
input: 46224
output: 42
thoughts: 269
Thoughts: **Analyzing Page Swapping in `ring_buffer_read_page`**
Okay, so I'm focusing on how `order` is handled within `struct buffer_page`. I remember that `order` *is* a field in that struct. Now, when `ring_buffer_read_page` does its page swap involving `reader->page` and `data_page->data`, does the `order` field get swapped along with the page data? I need to clarify this.
Let me quickly review the code snippet for `ring_buffer_read_page` again... *scans code*... Okay, here's the relevant part: `/* swap the pages */ rb_init_data_page(dpage); dpage = reader->page; reader->page = data_page->data; local_set(&reader->write, 0); local_set(&reader->entries, 0); reader->read = 0; data_page->data = dpage;`
Aha! It's clear from this that *`reader->order` is NOT swapped!* That's a crucial point. Now, what about the `data_page->order` field? Does the code update that? No, it doesn't.
So, after the swap, `data_page->data` (the old data) now refers to a page of the *old* order. However, `data_page->order` *still* holds the *new* order value! This means there's a potential mismatch between the data and the perceived order. And eventually, `tracing_buffers_read` will trigger `ring_buffer_free_read_page`, which will free `data_page`. This needs further investigation.
|
| 55/2 |
2026/08/09 01:15 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:kernel/trace/ring_buffer.c Name:ring_buffer_free_read_page]
Results: map[SourceCode:6984: /**
6985: * ring_buffer_free_read_page - free an allocated read page
6986: * @buffer: the buffer the page was allocate for
6987: * @cpu: the cpu buffer the page came from
6988: * @data_page: the page to free
6989: *
6990: * Free a page allocated from ring_buffer_alloc_read_page.
6991: */
6992: void ring_buffer_free_read_page(struct trace_buffer *buffer, int cpu,
6993: struct buffer_data_read_page *data_page)
6994: {
6995: struct ring_buffer_per_cpu *cpu_buffer;
6996: struct buffer_data_page *dpage = data_page->data;
6997: struct page *page = virt_to_page(dpage);
6998: unsigned long flags;
6999:
7000: if (!buffer || !buffer->buffers || !buffer->buffers[cpu])
7001: return;
7002:
7003: cpu_buffer = buffer->buffers[cpu];
7004:
7005: /*
7006: * If the page is still in use someplace else, or order of the page
7007: * is different from the subbuffer order of the buffer -
7008: * we can't reuse it
7009: */
7010: if (page_ref_count(page) > 1 || data_page->order != buffer->subbuf_order)
7011: goto out;
7012:
7013: local_irq_save(flags);
7014: arch_spin_lock(&cpu_buffer->lock);
7015:
7016: if (!cpu_buffer->free_page) {
7017: cpu_buffer->free_page = dpage;
7018: dpage = NULL;
7019: }
7020:
7021: arch_spin_unlock(&cpu_buffer->lock);
7022: local_irq_restore(flags);
7023:
7024: out:
7025: free_pages((unsigned long)dpage, data_page->order);
7026: kfree(data_page);
7027: }
SourceFile:kernel/trace/ring_buffer.c]
|
| 56/2 |
2026/08/09 01:15 |
llm |
expert |
0m
Model:
Tokens:
input: 47587
output: 42
thoughts: 800
Thoughts: **Memory Corruption in Ring Buffer Page Management**
Okay, here's the breakdown of this potential issue as I see it, from start to finish. It looks like we've got a serious problem. The `ring_buffer_free_read_page` function calls `free_pages` with an `unsigned long` for the page to be freed and a page order. If `data_page->data` is still representing an OLD order page, but `data_page->order` is the NEW order, we're going to call `free_pages` on the OLD order with the NEW order, and that's where the trouble begins.
The core vulnerability, as I'm tracing it, hinges on a race condition between threads and the `subbuf_order` update.
Here's the sequence I'm seeing:
1. `buffer->subbuf_order` is initially set to OLD\_ORDER.
2. Then, Thread 1 comes along and calls `ring_buffer_subbuf_order_set` to change it to NEW\_ORDER.
3. Critically, Thread 1 takes `buffer->mutex`.
4. Thread 1 updates `buffer->subbuf_order = NEW_ORDER`.
5. Now, Thread 2, in the meantime, calls `tracing_buffers_read`.
6. Thread 2 then calls `ring_buffer_alloc_read_page`. It snags the current `buffer->subbuf_order`, which is **now** the new `NEW_ORDER` after Thread 1 got there first. It allocates `data_page` based on this `NEW_ORDER`.
7. Thread 2 calls `ring_buffer_read_page`. It checks the orders, so everything checks out to the code. `data_page->order` and `buffer->subbuf_order` are both NEW\_ORDER. It takes `reader_lock`, swaps the reader page, grabs the pages, so the ring buffer *still* contains OLD\_ORDER pages at this point. Thus, reader is an OLD\_ORDER `buffer_page`. After the swap, `reader->page` is now NEW\_ORDER, but `data_page->data` is OLD\_ORDER.
8. Thread 1 takes `reader_lock` *after* Thread 2 has read. It then replaces the old pages (freeing pages) and calls `free_buffer_page(reader)`. Here's where it goes sideways. `reader->order` is still OLD\_ORDER, but `reader->page` is NEW\_ORDER. It calls `free_pages(reader->page, OLD_ORDER)`. If `NEW_ORDER` is greater than `OLD_ORDER`, it will free a small page as a large page causing a memory leak.
9. Thread 2 now calls `ring_buffer_free_read_page`. And we are back at our first problem. We call `free_pages(data_page->data, data_page->order)`. `data_page->data` is OLD\_ORDER, `data_page->order` is NEW\_ORDER. If `NEW_ORDER` is *greater* than `OLD_ORDER`, it will free a small page as a large page which causes memory corruption.
10. If `NEW_ORDER < OLD_ORDER`, it is *freeing* a large page as a small page which would be a memory leak.
So, yes, it's definitely a memory corruption, and a race condition is driving it. It looks like there are two specific cases to worry about.
* If `NEW_ORDER > OLD_ORDER`, `ring_buffer_free_read_page` is freeing a small page as a large page.
* If `NEW_ORDER < OLD_ORDER`, then `free_buffer_page` is freeing a small page as a large page.
This is all bad news because either way, we have a significant memory corruption.
Wait, now that I have outlined the process, I need to go back and check the order of operations in `ring_buffer_alloc_read_page`. Is that function reading the `buffer->subbuf_order` value directly? We need to make sure our assumptions are correct to fully diagnose.
|
| 57/2 |
2026/08/09 01:16 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:kernel/trace/ring_buffer.c Name:ring_buffer_alloc_read_page]
Results: map[SourceCode:6927: /**
6928: * ring_buffer_alloc_read_page - allocate a page to read from buffer
6929: * @buffer: the buffer to allocate for.
6930: * @cpu: the cpu buffer to allocate.
6931: *
6932: * This function is used in conjunction with ring_buffer_read_page.
6933: * When reading a full page from the ring buffer, these functions
6934: * can be used to speed up the process. The calling function should
6935: * allocate a few pages first with this function. Then when it
6936: * needs to get pages from the ring buffer, it passes the result
6937: * of this function into ring_buffer_read_page, which will swap
6938: * the page that was allocated, with the read page of the buffer.
6939: *
6940: * Returns:
6941: * The page allocated, or ERR_PTR
6942: */
6943: struct buffer_data_read_page *
6944: ring_buffer_alloc_read_page(struct trace_buffer *buffer, int cpu)
6945: {
6946: struct ring_buffer_per_cpu *cpu_buffer;
6947: struct buffer_data_read_page *bpage = NULL;
6948: unsigned long flags;
6949:
6950: if (!cpumask_test_cpu(cpu, buffer->cpumask))
6951: return ERR_PTR(-ENODEV);
6952:
6953: bpage = kzalloc_obj(*bpage);
6954: if (!bpage)
6955: return ERR_PTR(-ENOMEM);
6956:
6957: bpage->order = buffer->subbuf_order;
6958: cpu_buffer = buffer->buffers[cpu];
6959: local_irq_save(flags);
6960: arch_spin_lock(&cpu_buffer->lock);
6961:
6962: if (cpu_buffer->free_page) {
6963: bpage->data = cpu_buffer->free_page;
6964: cpu_buffer->free_page = NULL;
6965: }
6966:
6967: arch_spin_unlock(&cpu_buffer->lock);
6968: local_irq_restore(flags);
6969:
6970: if (bpage->data) {
6971: rb_init_data_page(bpage->data);
6972: } else {
6973: bpage->data = alloc_cpu_data(cpu, cpu_buffer->buffer->subbuf_order);
6974: if (!bpage->data) {
6975: kfree(bpage);
6976: return ERR_PTR(-ENOMEM);
6977: }
6978: }
6979:
6980: return bpage;
6981: }
SourceFile:kernel/trace/ring_buffer.c]
|
| 58/2 |
2026/08/09 01:16 |
llm |
expert |
0m
Model:
Tokens:
input: 50209
output: 34
thoughts: 833
Thoughts: **Analyzing a Potential Memory Corruption Vulnerability in the Trace Ring Buffer**
Okay, so I've been digging into this code and I think I've found something nasty. It starts with this allocation: `bpage->order = buffer->subbuf_order;` and then `alloc_cpu_data(cpu, cpu_buffer->buffer->subbuf_order);` β which allocates a page with `NEW_ORDER`. But here's the kicker: what if we're using `cpu_buffer->free_page`? If we are, that's an `OLD_ORDER` page, because it was set *before* the `ring_buffer_subbuf_order_set` function replaced the pages.
Now, `ring_buffer_subbuf_order_set` *does* clear `cpu_buffer->free_page`: `cpu_buffer->free_page = NULL;`. But it only does that *after* taking the `reader_lock`. That means *before* the lock is taken, `cpu_buffer->free_page` is still the old `OLD_ORDER` page! This is where the race happens. `ring_buffer_alloc_read_page` might grab that old `cpu_buffer->free_page`, which is `OLD_ORDER`, but then set `bpage->order = NEW_ORDER`!
So, `bpage->data` ends up being an `OLD_ORDER` page, but `bpage->order` is `NEW_ORDER`. Then, `ring_buffer_read_page` swaps it. Finally, `ring_buffer_free_read_page` frees this page using the *wrong* `bpage->order` ( `NEW_ORDER`), resulting in freeing an `OLD_ORDER` page as a `NEW_ORDER` page! Boom! This is textbook memory corruption!
The bug is exploitable for memory corruption, but it's only exploitable by root as tracefs is restricted to root and cannot be mounted in a user namespace.
Now, the important question is: can an unprivileged user trigger this somehow? Let's rule out the likely suspects: `perf` uses a different ring buffer implementation. `ftrace` does use `kernel/trace/ring_buffer.c`, but it's root-only, so that's a dead end. BPF uses its own ring buffer as well. However, BPF tracepoints can read from the trace ring buffer, but they don't change the subbuf size. `user_events` allows unprivileged users to write to trace events, but changing the `buffer_subbuf_size_kb` is root-only with mode `0640`! So, an unprivileged user *cannot* change the subbuf size.
Okay, let's just double check that, just to be absolutely sure. Yep, the `trace_create_file` function sets `writable_mode` to `0640`, and the owner is `root`.
Okay, it looks like only root can change this variable, which means this vulnerability is not accessible by unprivileged users. I should check all the callers of `ring_buffer_subbuf_order_set` just to be sure.
|
| 59/2 |
2026/08/09 01:16 |
tool |
codesearch-find-references |
0m
Args: map[IncludeSnippetLines:0 Name:ring_buffer_subbuf_order_set]
Results: map[References:[map[ReferenceKind:calls ReferencingEntityKind:function ReferencingEntityName:buffer_subbuf_size_write SourceFile:kernel/trace/trace.c SourceLine:8221] map[ReferenceKind:takes-address-of ReferencingEntityKind:global_variable ReferencingEntityName:__UNIQUE_ID_addressable_ring_buffer_subbuf_order_set_1268 SourceFile:kernel/trace/ring_buffer.c SourceLine:7472] map[ReferenceKind:takes-address-of ReferencingEntityKind:global_variable ReferencingEntityName:__gendwarfksyms_ptr_ring_buffer_subbuf_order_set SourceFile:kernel/trace/ring_buffer.c SourceLine:7472] map[ReferenceKind:takes-address-of ReferencingEntityKind:global_variable ReferencingEntityName:__gendwarfksyms_ptr_ring_buffer_subbuf_order_set SourceFile:kernel/trace/ring_buffer.c SourceLine:7472]] TruncatedOutput:false]
|
| 60/2 |
2026/08/09 01:16 |
llm |
expert |
0m
Model:
Tokens:
input: 52059
output: 39
thoughts: 706
Thoughts: **My Assessment of the Tracepoint Bug**
Okay, so I've dug into this, and here's the breakdown of my thinking. My primary focus here is exploitability and security implications.
First, this is strictly a root-only bug, and only concerns `buffer_subbuf_size_write`. That's important. Since itβs root-only, itβs not something a standard unprivileged process or user namespace can touch. It's also not triggered over the network, by a peripheral, through a filesystem interaction, or within a virtual machine (either guest or host).
The core of the issue boils down to a race condition between `ring_buffer_subbuf_order_set` and `ring_buffer_read_page`. We're talking about a `WARNING` and potential memory corruption. If those pages are swapped or freed in the wrong order, boom, that's exploitable memory corruption. But, the catch, and a big one, is that root privileges are required for writes to `buffer_subbuf_size_kb` and reads from `trace_pipe_raw`. This keeps it out of reach from unprivileged users and user namespaces.
I tried to verify if there's any other access path to `buffer_subbuf_size_write`. I checked if it was reachable through some `ioctl` or system call, but no, it's a file operation on a `tracefs` file. Now, the question is, can an unprivileged user mount `tracefs` in any way? I reviewed `trace_fs_type.fs_flags` and found it does *not* have `FS_USERNS_MOUNT`. So, no, not through user namespaces. Could an unprivileged user access it if the admin mounts it with permissive permissions? Sure, but I'm assuming the default configuration. By default, it's mounted at `/sys/kernel/tracing` with `0700` permissions. Even if the directory *was* accessible, the actual file, `buffer_subbuf_size_kb`, is `0640` root:root.
So, here's the exploitability matrix:
* Exploitable: Yes (memory corruption leading to a crash).
* Denial of Service: Yes (memory corruption leads to crash).
* Accessible From Unprivileged Processes: No.
* Accessible From User Namespaces: No.
* VM Guest Trigger: No.
* VM Host Trigger: No.
* Ethernet Network Trigger: No.
* Other Remote Trigger: No.
* Peripheral Trigger: No.
* Malicious Filesystem Trigger: No.
But here's the critical question Iβm circling: If root already has control, is it *really* a vulnerability? Generally, bugs that require root are still bugs, but they often have lower security impact unless they bypass lockdown or secure boot. Let's see if this bypasses lockdown.
My analysis of the `buffer_subbuf_size_write` function reveals that there is no `security_locked_down()` check.
```c
static ssize_t
buffer_subbuf_size_write(struct file *filp, const char __user *ubuf,
size_t cnt, loff_t *ppos)
```
**Bingo!** Because there's no lockdown check, this bug lets root corrupt memory and potentially execute arbitrary code *even when lockdown is enabled*. That's the key. This is a legitimate lockdown bypass. This changes everything.
Therefore:
* Exploitable: Yes (lockdown bypass).
|
| 61/2 |
2026/08/09 01:16 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:kernel/trace/trace.c Name:buffer_subbuf_size_write]
Results: map[SourceCode:8190: static ssize_t
8191: buffer_subbuf_size_write(struct file *filp, const char __user *ubuf,
8192: size_t cnt, loff_t *ppos)
8193: {
8194: struct trace_array *tr = filp->private_data;
8195: unsigned long val;
8196: int old_order;
8197: int order;
8198: int pages;
8199: int ret;
8200:
8201: ret = kstrtoul_from_user(ubuf, cnt, 10, &val);
8202: if (ret)
8203: return ret;
8204:
8205: val *= 1024; /* value passed in is in KB */
8206:
8207: pages = DIV_ROUND_UP(val, PAGE_SIZE);
8208: order = fls(pages - 1);
8209:
8210: /* limit between 1 and 128 system pages */
8211: if (order < 0 || order > 7)
8212: return -EINVAL;
8213:
8214: /* Do not allow tracing while changing the order of the ring buffer */
8215: tracing_stop_tr(tr);
8216:
8217: old_order = ring_buffer_subbuf_order_get(tr->array_buffer.buffer);
8218: if (old_order == order)
8219: goto out;
8220:
8221: ret = ring_buffer_subbuf_order_set(tr->array_buffer.buffer, order);
8222: if (ret)
8223: goto out;
8224:
8225: #ifdef CONFIG_TRACER_SNAPSHOT
8226:
8227: if (!tr->allocated_snapshot)
8228: goto out_max;
8229:
8230: ret = ring_buffer_subbuf_order_set(tr->snapshot_buffer.buffer, order);
8231: if (ret) {
8232: /* Put back the old order */
8233: cnt = ring_buffer_subbuf_order_set(tr->array_buffer.buffer, old_order);
8234: if (WARN_ON_ONCE(cnt)) {
8235: /*
8236: * AARGH! We are left with different orders!
8237: * The max buffer is our "snapshot" buffer.
8238: * When a tracer needs a snapshot (one of the
8239: * latency tracers), it swaps the max buffer
8240: * with the saved snap shot. We succeeded to
8241: * update the order of the main buffer, but failed to
8242: * update the order of the max buffer. But when we tried
8243: * to reset the main buffer to the original size, we
8244: * failed there too. This is very unlikely to
8245: * happen, but if it does, warn and kill all
8246: * tracing.
8247: */
8248: tracing_disabled = 1;
8249: }
8250: goto out;
8251: }
8252: out_max:
8253: #endif
8254: (*ppos)++;
8255: out:
8256: if (ret)
8257: cnt = ret;
8258: tracing_start_tr(tr);
8259: return cnt;
8260: }
SourceFile:kernel/trace/trace.c]
|
| 62/2 |
2026/08/09 01:16 |
llm |
expert |
0m
Model:
Tokens:
input: 54621
output: 38
thoughts: 24
Thoughts: Okay, here's my interpretation of those thoughts, crafted with the understanding that I'm speaking to a fellow expert:
**Lockdown Check Analysis**
Right, no obvious lockdown checks in this particular spot. I need to be absolutely certain though, and my gut's telling me to verify that the absence is correct. But... hang on a minute. Could there be a check tucked away, perhaps nested deeper? *tracing_open_generic_tr*... that function title rings a bell. Hmm, I better double-check the internals of `tracing_open_generic_tr` β I wouldn't want to miss a subtle, potentially indirect lockdown mechanism hidden within. This requires a deeper dive.
|
| 63/2 |
2026/08/09 01:16 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:kernel/trace/trace.c Name:tracing_open_generic_tr]
Results: map[SourceCode:3486: /*
3487: * Open and update trace_array ref count.
3488: * Must have the current trace_array passed to it.
3489: */
3490: int tracing_open_generic_tr(struct inode *inode, struct file *filp)
3491: {
3492: struct trace_array *tr = inode->i_private;
3493: int ret;
3494:
3495: ret = tracing_check_open_get_tr(tr);
3496: if (ret)
3497: return ret;
3498:
3499: if ((filp->f_mode & FMODE_WRITE) && trace_array_is_readonly(tr)) {
3500: trace_array_put(tr);
3501: return -EACCES;
3502: }
3503:
3504: filp->private_data = inode->i_private;
3505:
3506: return 0;
3507: }
SourceFile:kernel/trace/trace.c]
|
| 64/2 |
2026/08/09 01:16 |
llm |
expert |
0m
Model:
Tokens:
input: 55155
output: 40
thoughts: 15
Thoughts: Okay, here's my interpretation of that thought process, assuming I'm an expert in this field, presented as a first-person summary:
**Analyzing `tracing_check_open_get_tr`**
Alright, time to dig in. The thought process immediately lands on `tracing_check_open_get_tr`. That function name is pretty specific, so it's a good starting point. I need to understand its role and what context it operates within. My mental checklist kicks in: What does "tracing" refer to here? Is it a specific logging or monitoring system? What's the scope of "check"? Is it validation, error handling, or something else entirely? The "open" suggests a file or resource handle. And "get_tr"? That probably refers to a trace, or transaction representation; perhaps a handle or ID associated with a transaction. So, I need to know why I am checking this, and what the transaction context is.
|
| 65/2 |
2026/08/09 01:17 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:kernel/trace/trace.c Name:tracing_check_open_get_tr]
Results: map[SourceCode: 669: int tracing_check_open_get_tr(struct trace_array *tr)
670: {
671: int ret;
672:
673: ret = security_locked_down(LOCKDOWN_TRACEFS);
674: if (ret)
675: return ret;
676:
677: if (tracing_disabled)
678: return -ENODEV;
679:
680: if (tr && trace_array_get(tr) < 0)
681: return -ENODEV;
682:
683: return 0;
684: }
SourceFile:kernel/trace/trace.c]
|
| 66/2 |
2026/08/09 01:17 |
llm |
expert |
0m
Model:
Tokens:
input: 55636
output: 36
thoughts: 1057
Thoughts: **Analysis of the Ring Buffer Memory Corruption Bug**
Okay, so I'm looking at this `tracing_check_open_get_tr` function and it's calling `security_locked_down(LOCKDOWN_TRACEFS)`. Right, that means if lockdown is enabled (like with `integrity` or `confidentiality`), I can't even *open* the tracefs file. Good. So, this doesn't bypass lockdown. Now, does this bug, which requires root, represent a *security* vulnerability? Normally, something root-only that doesn't bypass lockdown isn't considered a security problem, just a regular bug.
But, the prompt... it's asking me to classify the bug based on dimensions. And itβs explicitly warning that even an information leak can be an exploit. So, let's break it down:
* **Exploitable:** Yes, because it results in memory corruption.
* **Denial of Service:** Yes, because memory corruption will definitely cause a crash.
* **Unprivileged:** No. It needs root.
* **UserNamespace:** No.
Hmm, is there another way to trigger this without going through tracefs? Letβs think about `user_events`. Unprivileged users can write trace events through that, but can they change the `buffer_subbuf_size_kb`? No, they can't. So, it's tracefs-dependent.
Now, let's make sure `LOCKDOWN_TRACEFS` is the *only* protection. If lockdown *isn't* enabled, a root user can just load a kernel module to do whatever they want. So, this bug isn't giving root any *additional* privileges, but... it's still a memory corruption bug. And according to the prompt, we still classify it as "exploitable" because of the memory corruption.
Alright, let's *really* dig into this memory corruption. I need to be precise.
1. `ring_buffer_alloc_read_page` allocates `bpage->data` using `NEW_ORDER`.
2. `ring_buffer_read_page` swaps `reader->page` (OLD_ORDER) with `bpage->data` (NEW_ORDER).
3. `ring_buffer_subbuf_order_set` *frees* `reader` using `reader->order` (OLD_ORDER). It calls `free_pages(reader->page, OLD_ORDER)`. But `reader->page` is now `bpage->data` (NEW_ORDER). So... if `NEW_ORDER > OLD_ORDER`, it frees a large page as a small page. The rest of the large page is leaked, but that's not the critical problem. If `NEW_ORDER < OLD_ORDER`, it's freeing a small page as a *large* page. *That* means it's freeing pages it *doesn't own*! That's definite memory corruption!
4. `ring_buffer_free_read_page` frees `bpage->data` using `bpage->order` (NEW_ORDER). `bpage->data` is the OLD_ORDER page. If `NEW_ORDER > OLD_ORDER`, it frees a small page as a *large* page. Memory corruption! If `NEW_ORDER < OLD_ORDER`, it frees a large page as a small page, so, a memory leak.
So, *either* increasing or decreasing the subbuf size leads to memory corruption! If you *increase* the size, it's `ring_buffer_free_read_page` corrupting. If you *decrease* the size, it's `ring_buffer_subbuf_order_set` causing corruption.
So, yes, absolutely a memory corruption bug.
Now, can these functions run concurrently and trigger this? Does `ring_buffer_subbuf_order_set` and `ring_buffer_read_page` operate at the same time? `ring_buffer_subbuf_order_set` takes `buffer->mutex`. `tracing_buffers_read` *doesn't* take `buffer->mutex`! It only uses `trace_access_lock(iter->cpu_file)`. And that's specific to the file, not the buffer mutex. However, `buffer_subbuf_size_write` calls `tracing_stop_tr(tr)`. Does that stop readings? Let me check `tracing_stop_tr`.
|
| 67/2 |
2026/08/09 01:17 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:kernel/trace/trace.c Name:tracing_stop_tr]
Results: map[SourceCode:1657: static void tracing_stop_tr(struct trace_array *tr)
1658: {
1659: struct trace_buffer *buffer;
1660:
1661: guard(raw_spinlock_irqsave)(&tr->start_lock);
1662: if (tr->stop_count++)
1663: return;
1664:
1665: /* Prevent the buffers from switching */
1666: arch_spin_lock(&tr->max_lock);
1667:
1668: buffer = tr->array_buffer.buffer;
1669: if (buffer)
1670: ring_buffer_record_disable(buffer);
1671:
1672: #ifdef CONFIG_TRACER_SNAPSHOT
1673: buffer = tr->snapshot_buffer.buffer;
1674: if (buffer)
1675: ring_buffer_record_disable(buffer);
1676: #endif
1677:
1678: arch_spin_unlock(&tr->max_lock);
1679: }
SourceFile:kernel/trace/trace.c]
|
| 68/2 |
2026/08/09 01:17 |
llm |
expert |
0m
Model:
Tokens:
input: 58036
output: 66
thoughts: 2164
Thoughts: **Race Condition Analysis in Ring Buffer Subbuf Order Setting**
Okay, here's my breakdown of this ring buffer vulnerability. I've pinpointed a serious race condition. `tracing_stop_tr` disables writing, but reading can continue. The problem stems from how `buffer_subbuf_size_write` interacts with the reader thread. Specifically, I'm concerned about the `ring_buffer_read_page` function. It seems there's a window where the subbuf order (and size) can change *while* a read is in progress, potentially corrupting memory.
My first thought was, "Does `trace_access_lock` protect everything?" Nope. `buffer_subbuf_size_write` doesn't take it, meaning a race is possible, and memory corruption is the likely outcome.
I started digging deeper into `ring_buffer_read_page`, and now I understand that the key lies in the page swapping logic within the `ring_buffer_read_page` function. The function checks if the order of the newly allocated `data_page` matches the current `buffer->subbuf_order` *before* the swap. However, it's missing a check on the `reader` page order.
Here's the scenario:
1. `buffer_subbuf_size_write` is called, it changes the `buffer->subbuf_order`.
2. `ring_buffer_subbuf_order_set` is called, which changes the `buffer->subbuf_order`.
3. `ring_buffer_subbuf_order_set` then uses `synchronize_rcu()`, which takes time.
4. In the meantime, the reader is active. It reads the new `buffer->subbuf_order`, allocates `data_page` based on this *new* order.
5. `ring_buffer_read_page` is called. It checks the *new* `buffer->subbuf_order` and allocates a new page based on that.
6. The `reader_lock` is taken.
7. The swapping occurs. The pages within the ring buffer might still be based on the *old* `subbuf_order`. This means `reader->page` ends up with the *new* order, while `data_page` gets the *old* order.
8. `synchronize_rcu()` in `ring_buffer_subbuf_order_set` eventually takes the `reader_lock`, replaces the ring buffer pages and frees the old pages.
The crucial point is that when those old pages are freed, they're freed with the *incorrect* order. This is a textbook memory corruption bug. A smaller page is freed as a larger page, or vice versa. This can be easily triggered because of the delay within `synchronize_rcu()`.
What about other protections? `ring_buffer_read_page` checks `data_page->order`, but NOT the `reader`'s order. `rb_get_reader_page` doesn't have an order check either, confirming my suspicions.
So, the exploitation scenario is clear. It's:
* Exploitable: Memory corruption.
* Denial of Service: Crash due to memory corruption.
* Unprivileged: False. Root privileges are needed to change `buffer_subbuf_size_kb`.
* UserNamespace: False. `tracefs` can't be mounted in a user namespace.
Can this be triggered from a VM guest? No, `tracefs` is host-local. What about a malicious filesystem? Again, no. I confirmed this by checking the `trace_fs_type` in `fs/tracefs/inode.c`. There's no `FS_USERNS_MOUNT` flag. The same is true for `debugfs`.
Now, let's look at the WARNING message that syzbot hit. The WARNING indicates that `rb_page_write(reader) > bsize`, which is a side effect of this race condition.
If the pages still have the OLD_ORDER, and the NEW_ORDER has less data written to it, then the race can happen without triggering the warning, and it leads to the memory corruption anyway. The WARNING can sometimes prevent the swap from happening, but it doesn't always.
**In summary:** This is a race between order changes in the ring buffer subbuf settings (`ring_buffer_subbuf_order_set`) and `ring_buffer_read_page`, allowing memory corruption.
|
| 69/2 |
2026/08/09 01:18 |
tool |
set-results |
0m
Args: map[DenialOfService:true Exploitable:true FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:false]
Results: map[DenialOfService:true Exploitable:true FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:false]
|
| 70/2 |
2026/08/09 01:18 |
llm |
expert |
0m
Model:
Tokens:
input: 61313
output: 667
thoughts: 442
Thoughts: **Final Thoughts on the Ftrace Ring Buffer Bug**
Okay, the analysis is complete. Time to synthesize this into a final response. Let's see... the core issue is definitely that race condition in the ftrace ring buffer between resizing the sub-buffer and reading from it. I need to clearly articulate that the `ring_buffer_subbuf_order_set` function updates crucial variables like `buffer->subbuf_order` and `buffer->subbuf_size` *before* the `reader_lock` is acquired and the actual page replacement happens in the ring buffer.
The concurrent problem arises because `ring_buffer_read_page`, through `tracing_buffers_read`, might allocate a new read page using this *new*, updated `subbuf_order`. Crucially, it then swaps this freshly allocated page into the ring buffer, replacing an older one. The swap is the killer here because that "old" page, still holds the *original* size and order. This is where we get the size mismatch: the metadata (`bpage->order`) associated with the old page is now out of sync with its actual physical size.
The consequences of this are nasty. When those mismatched pages are freed later on, they're freed using the incorrect order. This inevitably leads to memory corruption, like freeing a small page as if it were a large page, or vice-versa. And that's all it takes for the corruption to occur.
Now, that syzbot WARNING... it's just a symptom. It's a side effect of the race. If the old page had *more* data written to it than the new `subbuf_size`, `__rb_get_reader_page` incorrectly assumes the writer has written beyond the page's boundaries and triggers that timeout warning. It's like the kernel is screaming, "Hey, something's not right here!" But if there's *less* data on the old page, the warning is bypassed, and the memory corruption happens silently. That makes it harder to diagnose!
Let's not forget the exploitability. To trigger this bug, we're talking about writing to `/sys/kernel/tracing/buffer_subbuf_size_kb` and reading from `trace_pipe_raw`. Both are restricted to root, which is good from a security perspective. Tracefs permissions (0640) and ownership (root) are the key. Furthermore, Tracefs can't be mounted in an unprivileged user namespace, which means that any exploit would require root access.
So, in the end, it is a memory corruption and denial of service vulnerability, but it is only exploitable by a privileged user (root). That should be the gist of it.
|
| 71/1 |
2026/08/09 01:18 |
action |
format-explanation |
0m
Results: map[Explanation:The bug is a race condition in the ftrace ring buffer between resizing the
sub-buffer size (`ring_buffer_subbuf_order_set`) and reading from the ring
buffer (`ring_buffer_read_page`).
### Root Cause Analysis
When a user writes to `/sys/kernel/tracing/buffer_subbuf_size_kb`, the kernel
calls `ring_buffer_subbuf_order_set` to resize the ring buffer pages. This
function updates `buffer->subbuf_order` and `buffer->subbuf_size` early on, but
then calls `synchronize_rcu()` before taking the `reader_lock` to actually
replace the old pages with new ones.
Concurrently, a reader thread reading from `trace_pipe_raw` calls
`tracing_buffers_read`, which allocates a new read page using the newly updated
`buffer->subbuf_order`. It then proceeds to `ring_buffer_read_page`, takes the
`reader_lock`, and swaps this newly allocated page with the current reader page
in the ring buffer.
Because `ring_buffer_subbuf_order_set` hasn't replaced the pages yet, the reader
page in the ring buffer is still of the **old** order. The swap results in a
mismatch:
1. The ring buffer's `buffer_page` metadata now points to a data page of the
**new** order, but its `order` field still reflects the **old** order.
2. The reader's `data_page` metadata now points to a data page of the **old**
order, but its `order` field reflects the **new** order.
When these pages are eventually freed via `free_pages()`, the kernel uses the
mismatched order fields. This leads to severe memory corruption: freeing a small
page as a large page (corrupting adjacent memory) or freeing a large page as a
small page (memory leak).
The `WARNING` reported by syzbot is a side effect of this race. In
`__rb_get_reader_page`, the code reads the new (smaller) `subbuf_size` and
compares it against the write counter of the old (larger) page. If the old page
contains more data than the new size, the kernel incorrectly assumes a writer is
writing out-of-bounds, waits for 1 second, and triggers the `WARNING`. If the
old page contains less data, the WARNING is bypassed, and the memory corruption
silently occurs.
### Security Impact
* **Exploitable / Denial of Service:** Yes. The bug leads to freeing pages with
the wrong order, which is a powerful memory corruption primitive that can be
exploited to corrupt adjacent kernel memory or crash the system.
* **Unprivileged / User Namespaces:** No. Triggering the bug requires writing to
`buffer_subbuf_size_kb` and reading from `trace_pipe_raw`. These files are
located in `tracefs`, which is restricted to `root` (0640 permissions) and
cannot be mounted inside unprivileged user namespaces.
* **Other Triggers:** No. This is strictly a local privilege issue tied to the
tracing subsystem.]
|