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

Masami Hiramatsu (Google) <[email protected]>
Newsgroups org.kernel.vger.linux-trace-kernel,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
On Fri, 14 Aug 2026 15:53:43 +0100
Vincent Donnefort <[email protected]> wrote:

> On Fri, Aug 14, 2026 at 10:42:08AM -0400, Steven Rostedt wrote:
> > On Fri, 14 Aug 2026 01:16:13 +0900
> > "Masami Hiramatsu (Google)" <[email protected]> wrote:
> > 
> > > From: Masami Hiramatsu (Google) <[email protected]>
> > > 
> > > When ring_buffer_subbuf_order_set() updates buffer->subbuf_order, it
> > > previously modified buffer->subbuf_order before clearing the cached
> > > per-CPU free_page entries. Furthermore, clearing cpu_buffer->free_page
> > > was done under cpu_buffer->reader_lock, whereas
> > > ring_buffer_alloc_read_page() protects cpu_buffer->free_page using
> > > arch_spin_lock(&cpu_buffer->lock).
> > > 
> > > Because ring_buffer_alloc_read_page(), ring_buffer_free_read_page(),
> > > and ring_buffer_read_page() checked buffer->subbuf_order locklessly
> > > before accessing reader resources, a TOCTOU race allowed a concurrent
> > > reader to obtain, cache, or swap a page allocated under an outdated order
> > > while tagging bpage->order with the new order. This allowed undersized
> > > pages to be swapped into the ring buffer, leading to heap buffer overflows,
> > > or caused free_pages() to be called with an invalid order.
> > > 
> > > Fix this by:
> > > 1. Flushing and freeing all per-CPU cached free_page entries under
> > >    arch_spin_lock(&cpu_buffer->lock) using old_order before modifying
> > >    buffer->subbuf_order.
> > > 2. Protecting bpage->order assignment under
> > >    arch_spin_lock(&cpu_buffer->lock) in ring_buffer_alloc_read_page().
> > > 3. Moving the buffer->subbuf_order validation inside
> > >    arch_spin_lock(&cpu_buffer->lock) in ring_buffer_free_read_page().
> > > 4. Re-validating buffer->subbuf_order inside reader_lock in
> > >    ring_buffer_read_page().
> > > 5. Protecting cpu_buffer->free_page extraction with
> > >    arch_spin_lock(&cpu_buffer->lock) in ring_buffer_subbuf_order_set().
> > > 
> > > Fixes: 2808e31ec12e ("ring-buffer: Add interface for configuring trace sub buffer size")
> > > Assisted-by: Antigravity:gemini-3.6-flash
> > > Signed-off-by: Masami Hiramatsu (Google) <[email protected]>
> > > ---
> > 
> > 
> > Please rebase on top of ring-buffer/for-next, as I added Vincent's patches to that.
> > 
> > >  kernel/trace/ring_buffer.c |   35 +++++++++++++++++++++++++++++------
> > >  1 file changed, 29 insertions(+), 6 deletions(-)
> > > 
> > > diff --git a/kernel/trace/ring_buffer.c b/kernel/trace/ring_buffer.c
> > > index 2667992f0aa2..6d180689ad59 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;
> > 
> > This is still needed.
> 
> I am not sure, the lock is per-cpu_buffer but subbuf_order. is global to
> trace_buffer?

Yeah, we don't need this.
I got new ring_buffer_read_page() ensures the data_page->order ==
cpu_buffer->reader_page->order. This is a bit tricky but it allows us to
asynchronously allocate reader_page in different order outside of locking.
(and if free_page exists, it is copied with its order, so it is safe.)

> 
> > 
> > >  	if (cpu_buffer->free_page) {
> > >  		bpage->data = cpu_buffer->free_page;
> > >  		cpu_buffer->free_page = NULL;
> > > @@ -7010,13 +7010,13 @@ void ring_buffer_free_read_page(struct trace_buffer *buffer, int cpu,
> > >  	 * is different from the subbuffer order of the buffer -
> > >  	 * we can't reuse it
> > >  	 */
> > > -	if (page_ref_count(page) > 1 || data_page->order != buffer->subbuf_order)
> > > +	if (page_ref_count(page) > 1)
> > >  		goto out;
> > >  
> > >  	local_irq_save(flags);
> > >  	arch_spin_lock(&cpu_buffer->lock);
> > >  
> > > -	if (!cpu_buffer->free_page) {
> > > +	if (data_page->order == buffer->subbuf_order && !cpu_buffer->free_page) {
> > 
> > Swap the order please. It has to check both to continue and if one fails it
> > will not continue. Checking for cpu_buffer->free_page to be NULL first is
> > the quicker check. And also the more likely one to fail.
> > 
> > >  		cpu_buffer->free_page = dpage;
> > >  		dpage = NULL;
> > >  	}
> > > @@ -7094,15 +7094,15 @@ int ring_buffer_read_page(struct trace_buffer *buffer,
> > >  	if (!data_page || !data_page->data)
> > >  		return -1;
> > >  
> > > -	if (data_page->order != buffer->subbuf_order)
> > > -		return -1;
> > > -
> > >  	dpage = data_page->data;
> > >  	if (!dpage)
> > >  		return -1;
> > >  
> > >  	guard(raw_spinlock_irqsave)(&cpu_buffer->reader_lock);
> > >  
> > > +	if (data_page->order != buffer->subbuf_order)
> > > +		return -1;
> > > +
> 
> I have modified this as part of 
> 
> tracing: Fix subbuf resize races with trace_pipe_raw readers 

Got it. Yours changes the data structure. Mine is for older kernels
as a minimal change. 

> 
> > >  	reader = rb_get_reader_page(cpu_buffer);
> > >  	if (!reader)
> > >  		return -1;
> > > @@ -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);
> > > +	}
> > 
> > Honestly, this should be a separate patch. The first part of this patch is
> > data races with adding and freeing, but this is about changes to the size.

Since Vincent's patch[1] handles data page pointer with its order, we don't
this part anymore.

[1] ring-buffer: Make cpu_buffer::free_page a buffer_data_read_page

So I confirmed Vincent's patches with Fixed tag are fixing the racing
problem in the different way.

Thank you,

-- 
Masami Hiramatsu (Google) <[email protected]>
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.