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

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.

> 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.

[ ... ]
> @@ -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.

[ ... ]
> @@ -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

Reply via email to