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
