Thread (7 messages) flat view 7 messages, 4 authors, 3d ago

Re: [PATCH] ring-buffer: Fix race between ring_buffer_subbuf_order_set() and readers

From: sashiko-bot@kernel.org
Date: 2026-08-13 16:30:11

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:

Pre-existing issues:
- [Critical] Structural TOCTOU race: global order and size are updated before replacing per-CPU buffer pages, leading to Heap Buffer Overflow.
- [High] Incomplete TOCTOU fix: alloc_cpu_data() evaluates buffer->subbuf_order locklessly, causing bpage->order mismatch.
- [High] Race window between pre-flush loop and buffer->subbuf_order update allows caching undersized pages.
- [High] Post-update flush loop unconditionally frees newly cached pages using old_order.
- [High] Error path fails to flush cached pages when rolling back to old_order.
--
--- Patch [1]: [PATCH] ring-buffer: Fix race between ring_buffer_subbuf_order_set() and readers ---
commit d1c3f71c852aab912bda60e5894b681e43da7c7b
Author: Masami Hiramatsu (Google) [off-list ref]

ring-buffer: Fix race between ring_buffer_subbuf_order_set() and readers

This commit attempts to fix a TOCTOU race condition in the ring buffer code
by ensuring that the per-CPU free_page cache is flushed and protected by
the cpu_buffer lock during order changes. It also updates reader functions
to validate the page order under the same lock to prevent using mismatched
page sizes.
quoted hunk ↗ jump to hunk
diff --git a/kernel/trace/ring_buffer.c b/kernel/trace/ring_buffer.c
index c2c57fbf6f448..9a11c663cff5b 100644
--- a/kernel/trace/ring_buffer.c
+++ b/kernel/trace/ring_buffer.c
@@ -6957,11 +6957,11 @@ ring_buffer_alloc_read_page(struct trace_buffer *buffer, int cpu)
 	if (!bpage)
 		return ERR_PTR(-ENOMEM);
 
-	bpage->order = buffer->subbuf_order;
 	cpu_buffer = buffer->buffers[cpu];
 	local_irq_save(flags);
 	arch_spin_lock(&cpu_buffer->lock);
 
+	bpage->order = buffer->subbuf_order;
 	if (cpu_buffer->free_page) {
 		bpage->data = cpu_buffer->free_page;
 		cpu_buffer->free_page = NULL;
 	}
[Severity: High]
This isn't a bug introduced by this patch, but does this locklessly evaluate
buffer->subbuf_order after dropping the lock?

Looking at the code just below this hunk in ring_buffer_alloc_read_page():

	arch_spin_unlock(&cpu_buffer->lock);
	local_irq_restore(flags);

	if (bpage->data) {
		rb_init_data_page(bpage->data);
	} else {
		bpage->data = alloc_cpu_data(cpu, cpu_buffer->buffer->subbuf_order);
		...

If a concurrent writer changes the global order after the lock is dropped but
before alloc_cpu_data() evaluates the argument, the allocated page size will
mismatch the stored bpage->order.

When ring_buffer_free_read_page() later frees this page using bpage->order, it
could free a mismatched page size and corrupt the buddy allocator.

[ ... ]
quoted hunk ↗ jump to hunk
@@ -7350,6 +7350,27 @@ int ring_buffer_subbuf_order_set(struct trace_buffer *buffer, int order)
 	/* Make sure all commits have finished */
 	synchronize_rcu();
 
+	/* Flush any cached free_page allocated with old_order */
+	for_each_buffer_cpu(buffer, cpu) {
+		struct buffer_data_page *old_free;
+		unsigned long flags;
+
+		if (!cpumask_test_cpu(cpu, buffer->cpumask))
+			continue;
+
+		cpu_buffer = buffer->buffers[cpu];
+
+		local_irq_save(flags);
+		arch_spin_lock(&cpu_buffer->lock);
+		old_free = cpu_buffer->free_page;
+		cpu_buffer->free_page = NULL;
+		arch_spin_unlock(&cpu_buffer->lock);
+		local_irq_restore(flags);
+
+		if (old_free)
+			free_pages((unsigned long)old_free, old_order);
+	}
+
 	buffer->subbuf_order = order;
 	buffer->subbuf_size = psize - BUF_PAGE_HDR_SIZE;
[Severity: High]
This isn't a bug introduced by this patch, but does updating the global order
after the flush loop leave a race window?

During the unprotected window between the flush loop completing and updating
buffer->subbuf_order, the global order is still old_order.

A concurrent reader could call ring_buffer_free_read_page(), see that
data_page->order matches buffer->subbuf_order (both old), and cache an
old_order page back into cpu_buffer->free_page.

Once the order is updated below, subsequent readers might extract this cached
old_order page, tag it with the new order, and eventually free it with the new
order, causing buddy allocator corruption.

[Severity: Critical]
This isn't a bug introduced by this patch, but does updating the global order
and size here, before replacing per-CPU buffer pages, create a structural
race?

If a concurrent reader calls ring_buffer_read_page(), it validates its
new-order data_page against the new global order and swaps it into the ring
buffer. It extracts the old, unreplaced reader->page into dpage.

If the buffer is expanding, dpage is an undersized old-order page.

However, the reader executes memset() using the prematurely updated global
buffer->subbuf_size:

ring_buffer_read_page():
	if (size < buffer->subbuf_size)
		memset(&dpage->data[size], 0, buffer->subbuf_size - size);

This would blindly write zeroes far beyond the bounds of the extracted
old-order dpage, resulting in a heap buffer overflow reachable via
unprivileged tracefs reads.

[ ... ]
quoted hunk ↗ jump to hunk
@@ -7431,8 +7452,10 @@ int ring_buffer_subbuf_order_set(struct trace_buffer *buffer, int order)
 		cpu_buffer->nr_pages = cpu_buffer->nr_pages_to_update;
 		cpu_buffer->nr_pages_to_update = 0;
 
+		arch_spin_lock(&cpu_buffer->lock);
 		old_free_data_page = cpu_buffer->free_page;
 		cpu_buffer->free_page = NULL;
+		arch_spin_unlock(&cpu_buffer->lock);
 
 		rb_head_page_activate(cpu_buffer);
[Severity: High]
This isn't a bug introduced by this patch, but does this second flush loop
unconditionally free potentially new-order pages with old_order?

After the global order is set to new_order, this function performs a sleepable
allocation loop (__rb_allocate_pages). During this time, concurrent readers
can allocate and free new_order pages, successfully caching them into
cpu_buffer->free_page.

When this code extracts cpu_buffer->free_page and subsequently frees it down
below:

	free_pages((unsigned long)old_free_data_page, old_order);

It blindly forces what could be a new_order page to be freed with old_order,
corrupting the buddy allocator.

[Severity: High]
This isn't a bug introduced by this patch, but does the error path fail to
flush cached pages when rolling back?

If __rb_allocate_pages() fails, the code jumps to the error label and restores
buffer->subbuf_order = old_order:

ring_buffer_subbuf_order_set():
error:
	buffer->subbuf_order = old_order;
	buffer->subbuf_size = old_size;

During the window where it was new_order, concurrent readers may have cached
new_order pages into cpu_buffer->free_page. The error path returns without
flushing cpu_buffer->free_page.

Subsequent readers would extract these new_order pages, tag them as old_order,
and free them with old_order, causing a memory leak (if expanding) or allocator
corruption (if shrinking).

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/178663777320.475864.4716637934003507750.stgit@devnote2?part=1
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help