Linux Trace Kernel
 help / color / mirror / Atom feed
* [PATCH v2] tracing: ring_buffer: serialize shrink with NMI readers
@ 2026-11-10 12:26 Haoli Zhang
  2026-11-10 12:45 ` sashiko-bot
  0 siblings, 1 reply; 2+ messages in thread
From: Haoli Zhang @ 2026-11-10 12:26 UTC (permalink / raw)
  To: rostedt, mhiramat
  Cc: mathieu.desnoyers, linux-trace-kernel, linux-kernel,
	syzbot+37e1793bdb0061ea3a47, Haoli Zhang, Jinning Zhang, Bin Yu

The syzbot report shows a use-after-free in rb_is_reader_page(), reached
from rb_move_tail(), with the freed page released by rb_remove_pages().
Source inspection suggests that a detached reader page can retain a link
into the range removed during a shrink. This patch addresses that possible
stale-link path and coordinates NMI readers with page removal.

When an NMI reader cannot acquire reader_lock, rb_reader_lock() falls back
to reading the ring buffer without the lock. Gate lockless NMI readers
while rb_remove_pages() removes pages. Track readers that entered before
the gate and drain them before changing the page list; new NMI readers that
fail reader_lock while the gate is closed skip that CPU. The gate and
reader-count protocol uses ordering barriers and release/acquire accesses.
Readers that successfully acquire reader_lock remain serialized by it.

The NMI fallback increment of record_disabled is intentionally retained.
The pre-existing fallback comment says an unlocked NMI read may corrupt the
buffer and that recording must remain disabled afterward; the existing
unlock path does not undo that disable. The new nmi_readers counter is
separate: it tracks only the temporary lifetime of the lockless NMI read
and is decremented when that read exits.

During shrink, redirect reader_page->list.prev if it points to a page being
removed. Detect a stale reader_page->list.next as well, and after reconnecting
the ring redirect it to the surviving successor link, including the
transferred head flag. Treat skipped empty-buffer queries as non-empty, and
clear the timestamp output when peek or consume skips the read.

Reported-by: syzbot+37e1793bdb0061ea3a47@syzkaller.appspotmail.com
Closes: https://syzkaller.appspotmail.com/bug?extid=37e1793bdb0061ea3a47
Co-developed-by: Jinning Zhang <25031212117@stu.xidian.edu.cn>
Signed-off-by: Jinning Zhang <25031212117@stu.xidian.edu.cn>
Co-developed-by: Bin Yu <byu@xidian.edu.cn>
Signed-off-by: Bin Yu <byu@xidian.edu.cn>
Signed-off-by: Haoli Zhang <26031212468@stu.xidian.edu.cn>

---
v2:
  - Add NMI-reader gate and ordered reader-count drain.
  - Repair both detached reader-page links when they enter the shrink range.
  - Treat skipped empty queries as non-empty and clear skipped peek/consume timestamps.
diff --git a/kernel/trace/ring_buffer.c b/kernel/trace/ring_buffer.c
index 04bb94c..f07160e 100644
--- a/kernel/trace/ring_buffer.c
+++ b/kernel/trace/ring_buffer.c
@@ -522,6 +522,8 @@ struct ring_buffer_per_cpu {
 	int				cpu;
 	atomic_t			record_disabled;
 	atomic_t			resize_disabled;
+	atomic_t			resize_in_progress;
+	atomic_t			nmi_readers;
 	struct trace_buffer		*buffer;
 	raw_spinlock_t			reader_lock;	/* serialize readers */
 	arch_spinlock_t			lock;
@@ -2578,6 +2580,8 @@ rb_allocate_cpu_buffer(struct trace_buffer *buffer, unsigned long nr_pages, int
 
 	cpu_buffer->cpu = cpu;
 	cpu_buffer->buffer = buffer;
+	atomic_set(&cpu_buffer->resize_in_progress, 0);
+	atomic_set(&cpu_buffer->nmi_readers, 0);
 	raw_spin_lock_init(&cpu_buffer->reader_lock);
 	lockdep_set_class(&cpu_buffer->reader_lock, buffer->reader_lock_key);
 	cpu_buffer->lock = (arch_spinlock_t)__ARCH_SPIN_LOCK_UNLOCKED;
@@ -3074,10 +3078,18 @@ rb_remove_pages(struct ring_buffer_per_cpu *cpu_buffer, unsigned long nr_pages)
 	struct buffer_page *last_page, *first_page;
 	unsigned long nr_removed;
 	unsigned long head_bit;
+	bool reader_page_next_removed;
 	int page_entries;
 
 	head_bit = 0;
+	reader_page_next_removed = false;
 
+	/* Stop lockless NMI readers before unlinking pages. */
+	atomic_set_release(&cpu_buffer->resize_in_progress, 1);
+	/* Pair with the NMI reader's count increment and gate check. */
+	smp_mb();
+	while (atomic_read_acquire(&cpu_buffer->nmi_readers))
+		cpu_relax();
 	raw_spin_lock_irq(&cpu_buffer->reader_lock);
 	atomic_inc(&cpu_buffer->record_disabled);
 	/*
@@ -3106,6 +3118,19 @@ rb_remove_pages(struct ring_buffer_per_cpu *cpu_buffer, unsigned long nr_pages)
 	for (nr_removed = 0; nr_removed < nr_pages; nr_removed++) {
 		to_remove = rb_list_head(to_remove)->next;
 		head_bit |= (unsigned long)to_remove & RB_PAGE_HEAD;
+
+		/*
+		 * The reader page is not part of the ring buffer list, so its
+		 * previous pointer may still point to a page being removed.
+		 * rb_is_reader_page() follows this pointer, so redirect it to a
+		 * page that will remain in the ring buffer.
+		 */
+		if (cpu_buffer->reader_page->list.prev == rb_list_head(to_remove))
+			cpu_buffer->reader_page->list.prev = tail_page;
+		/* The detached reader page can also retain a stale forward link. */
+		if (rb_list_head(cpu_buffer->reader_page->list.next) ==
+		    rb_list_head(to_remove))
+			reader_page_next_removed = true;
 	}
 	/* Read iterators need to reset themselves when some pages removed */
 	cpu_buffer->pages_removed += nr_removed;
@@ -3119,6 +3144,9 @@ rb_remove_pages(struct ring_buffer_per_cpu *cpu_buffer, unsigned long nr_pages)
 	 */
 	tail_page->next = (struct list_head *)((unsigned long)next_page |
 						head_bit);
+	/* Keep the detached reader page's forward link off removed pages too. */
+	if (reader_page_next_removed)
+		cpu_buffer->reader_page->list.next = tail_page->next;
 	next_page = rb_list_head(next_page);
 	next_page->prev = tail_page;
 
@@ -3134,6 +3162,7 @@ rb_remove_pages(struct ring_buffer_per_cpu *cpu_buffer, unsigned long nr_pages)
 	/* pages are removed, resume tracing and then free the pages */
 	atomic_dec(&cpu_buffer->record_disabled);
 	raw_spin_unlock_irq(&cpu_buffer->reader_lock);
+	atomic_set_release(&cpu_buffer->resize_in_progress, 0);
 
 	RB_WARN_ON(cpu_buffer, list_empty(cpu_buffer->pages));
 
@@ -6293,11 +6322,18 @@ rb_iter_peek(struct ring_buffer_iter *iter, u64 *ts)
 }
 EXPORT_SYMBOL_GPL(ring_buffer_iter_peek);
 
-static inline bool rb_reader_lock(struct ring_buffer_per_cpu *cpu_buffer)
+enum rb_reader_lock_state {
+	RB_READER_LOCKED,
+	RB_READER_NMI,
+	RB_READER_SKIP,
+};
+
+static inline enum rb_reader_lock_state
+rb_reader_lock(struct ring_buffer_per_cpu *cpu_buffer)
 {
 	if (likely(!in_nmi())) {
 		raw_spin_lock(&cpu_buffer->reader_lock);
-		return true;
+		return RB_READER_LOCKED;
 	}
 
 	/*
@@ -6310,18 +6346,35 @@ static inline bool rb_reader_lock(struct ring_buffer_per_cpu *cpu_buffer)
 	 * Reading from NMI is a oneshot deal.
 	 */
 	if (raw_spin_trylock(&cpu_buffer->reader_lock))
-		return true;
+		return RB_READER_LOCKED;
+
+	/*
+	 * A resize cannot wait for an NMI that interrupted the CPU doing the
+	 * resize. Track lockless NMI readers so page removal can stop new ones
+	 * and wait for existing readers before changing the page list.
+	 */
+	atomic_inc(&cpu_buffer->nmi_readers);
+	/* Pair with the resize gate before deciding to read without the lock. */
+	smp_mb__after_atomic();
+	if (atomic_read_acquire(&cpu_buffer->resize_in_progress)) {
+		atomic_dec_return_release(&cpu_buffer->nmi_readers);
+		return RB_READER_SKIP;
+	}
 
 	/* Continue without locking, but disable the ring buffer */
 	atomic_inc(&cpu_buffer->record_disabled);
-	return false;
+
+	return RB_READER_NMI;
 }
 
 static inline void
-rb_reader_unlock(struct ring_buffer_per_cpu *cpu_buffer, bool locked)
+rb_reader_unlock(struct ring_buffer_per_cpu *cpu_buffer,
+		 enum rb_reader_lock_state state)
 {
-	if (likely(locked))
+	if (likely(state == RB_READER_LOCKED))
 		raw_spin_unlock(&cpu_buffer->reader_lock);
+	else if (state == RB_READER_NMI)
+		atomic_dec_return_release(&cpu_buffer->nmi_readers);
 }
 
 /**
@@ -6341,18 +6394,24 @@ ring_buffer_peek(struct trace_buffer *buffer, int cpu, u64 *ts,
 	struct ring_buffer_per_cpu *cpu_buffer = buffer->buffers[cpu];
 	struct ring_buffer_event *event;
 	unsigned long flags;
-	bool dolock;
+	enum rb_reader_lock_state state;
 
 	if (!cpumask_test_cpu(cpu, buffer->cpumask))
 		return NULL;
 
  again:
 	local_irq_save(flags);
-	dolock = rb_reader_lock(cpu_buffer);
-	event = rb_buffer_peek(cpu_buffer, ts, lost_events);
+	state = rb_reader_lock(cpu_buffer);
+	if (state == RB_READER_SKIP) {
+		if (ts)
+			*ts = 0;
+		event = NULL;
+	} else {
+		event = rb_buffer_peek(cpu_buffer, ts, lost_events);
+	}
 	if (event && event->type_len == RINGBUF_TYPE_PADDING)
 		rb_advance_reader(cpu_buffer);
-	rb_reader_unlock(cpu_buffer, dolock);
+	rb_reader_unlock(cpu_buffer, state);
 	local_irq_restore(flags);
 
 	if (event && event->type_len == RINGBUF_TYPE_PADDING)
@@ -6416,7 +6475,7 @@ ring_buffer_consume(struct trace_buffer *buffer, int cpu, u64 *ts,
 	struct ring_buffer_per_cpu *cpu_buffer;
 	struct ring_buffer_event *event = NULL;
 	unsigned long flags;
-	bool dolock;
+	enum rb_reader_lock_state state;
 
  again:
 	/* might be called in atomic */
@@ -6427,15 +6486,18 @@ ring_buffer_consume(struct trace_buffer *buffer, int cpu, u64 *ts,
 
 	cpu_buffer = buffer->buffers[cpu];
 	local_irq_save(flags);
-	dolock = rb_reader_lock(cpu_buffer);
+	state = rb_reader_lock(cpu_buffer);
 
-	event = rb_buffer_peek(cpu_buffer, ts, lost_events);
+	if (state != RB_READER_SKIP)
+		event = rb_buffer_peek(cpu_buffer, ts, lost_events);
+	else if (ts)
+		*ts = 0;
 	if (event) {
 		cpu_buffer->lost_events = 0;
 		rb_advance_reader(cpu_buffer);
 	}
 
-	rb_reader_unlock(cpu_buffer, dolock);
+	rb_reader_unlock(cpu_buffer, state);
 	local_irq_restore(flags);
 
  out:
@@ -6843,7 +6905,7 @@ bool ring_buffer_empty(struct trace_buffer *buffer)
 {
 	struct ring_buffer_per_cpu *cpu_buffer;
 	unsigned long flags;
-	bool dolock;
+	enum rb_reader_lock_state state;
 	bool ret;
 	int cpu;
 
@@ -6851,9 +6913,10 @@ bool ring_buffer_empty(struct trace_buffer *buffer)
 	for_each_buffer_cpu(buffer, cpu) {
 		cpu_buffer = buffer->buffers[cpu];
 		local_irq_save(flags);
-		dolock = rb_reader_lock(cpu_buffer);
-		ret = rb_per_cpu_empty(cpu_buffer);
-		rb_reader_unlock(cpu_buffer, dolock);
+		state = rb_reader_lock(cpu_buffer);
+		ret = state == RB_READER_SKIP ? false :
+			rb_per_cpu_empty(cpu_buffer);
+		rb_reader_unlock(cpu_buffer, state);
 		local_irq_restore(flags);
 
 		if (!ret)
@@ -6873,7 +6936,7 @@ bool ring_buffer_empty_cpu(struct trace_buffer *buffer, int cpu)
 {
 	struct ring_buffer_per_cpu *cpu_buffer;
 	unsigned long flags;
-	bool dolock;
+	enum rb_reader_lock_state state;
 	bool ret;
 
 	if (!cpumask_test_cpu(cpu, buffer->cpumask))
@@ -6881,9 +6944,10 @@ bool ring_buffer_empty_cpu(struct trace_buffer *buffer, int cpu)
 
 	cpu_buffer = buffer->buffers[cpu];
 	local_irq_save(flags);
-	dolock = rb_reader_lock(cpu_buffer);
-	ret = rb_per_cpu_empty(cpu_buffer);
-	rb_reader_unlock(cpu_buffer, dolock);
+	state = rb_reader_lock(cpu_buffer);
+	ret = state == RB_READER_SKIP ? false :
+		rb_per_cpu_empty(cpu_buffer);
+	rb_reader_unlock(cpu_buffer, state);
 	local_irq_restore(flags);
 
 	return ret;


^ permalink raw reply related	[flat|nested] 2+ messages in thread

* Re: [PATCH v2] tracing: ring_buffer: serialize shrink with NMI readers
  2026-11-10 12:26 [PATCH v2] tracing: ring_buffer: serialize shrink with NMI readers Haoli Zhang
@ 2026-11-10 12:45 ` sashiko-bot
  0 siblings, 0 replies; 2+ messages in thread
From: sashiko-bot @ 2026-11-10 12:45 UTC (permalink / raw)
  To: Haoli Zhang; +Cc: linux-trace-kernel

Thank you for your contribution! Sashiko AI review found 3 potential
issues to consider:
- [High] tracing: ring_buffer: infinite NMI loop when empty checks
  return false on skip
- [Medium] tracing: ring_buffer: stale event pointer in
  ring_buffer_consume() on skip
- [Low] tracing: ring_buffer: missing Fixes trailer on bug-fix commit
--

commit baa028f41be3b1534303fc3f33ecfebadca9debf
Author: Haoli Zhang <26031212468@stu.xidian.edu.cn>

> tracing: ring_buffer: serialize shrink with NMI readers
>
> The syzbot report shows a use-after-free in rb_is_reader_page(), reached
> from rb_move_tail(), with the freed page released by rb_remove_pages().
> Source inspection suggests that a detached reader page can retain a link
> into the range removed during a shrink. This patch addresses that possible
> stale-link path and coordinates NMI readers with page removal.
>
> When an NMI reader cannot acquire reader_lock, rb_reader_lock() falls back
> to reading the ring buffer without the lock. Gate lockless NMI readers
> while rb_remove_pages() removes pages. Track readers that entered before
> the gate and drain them before changing the page list; new NMI readers that
> fail reader_lock while the gate is closed skip that CPU. The gate and
> reader-count protocol uses ordering barriers and release/acquire accesses.
> Readers that successfully acquire reader_lock remain serialized by it.
>
> The NMI fallback increment of record_disabled is intentionally retained.
> The pre-existing fallback comment says an unlocked NMI read may corrupt the
> buffer and that recording must remain disabled afterward; the existing
> unlock path does not undo that disable. The new nmi_readers counter is
> separate: it tracks only the temporary lifetime of the lockless NMI read
> and is decremented when that read exits.
>
> During shrink, redirect reader_page->list.prev if it points to a page being
> removed. Detect a stale reader_page->list.next as well, and after reconnecting
> the ring redirect it to the surviving successor link, including the
> transferred head flag. Treat skipped empty-buffer queries as non-empty, and
> clear the timestamp output when peek or consume skips the read.
>
> Reported-by: syzbot+37e1793bdb0061ea3a47@syzkaller.appspotmail.com
> Closes: https://syzkaller.appspotmail.com/bug?extid=37e1793bdb0061ea3a47
> Co-developed-by: Jinning Zhang <25031212117@stu.xidian.edu.cn>
> Signed-off-by: Jinning Zhang <25031212117@stu.xidian.edu.cn>
> Co-developed-by: Bin Yu <byu@xidian.edu.cn>
> Signed-off-by: Bin Yu <byu@xidian.edu.cn>
> Signed-off-by: Haoli Zhang <26031212468@stu.xidian.edu.cn>

[Severity: Low]
Since this fixes live page unlinking in rb_remove_pages(), should this
include:

  Fixes: 83f40318dab0 ("ring-buffer: Make removal of ring buffer pages atomic")

> diff --git a/kernel/trace/ring_buffer.c b/kernel/trace/ring_buffer.c
> index e760d2fe386fb..e947ac629e67f 100644
> --- a/kernel/trace/ring_buffer.c
> +++ b/kernel/trace/ring_buffer.c

[ ... ]

> @@ -6416,7 +6475,7 @@ ring_buffer_consume(struct trace_buffer *buffer, int cpu, u64 *ts,
>  	struct ring_buffer_per_cpu *cpu_buffer;
>  	struct ring_buffer_event *event = NULL;
>  	unsigned long flags;
> -	bool dolock;
> +	enum rb_reader_lock_state state;
>  
>   again:
>  	/* might be called in atomic */
> @@ -6427,15 +6486,18 @@ ring_buffer_consume(struct trace_buffer *buffer, int cpu, u64 *ts,
>  
>  	cpu_buffer = buffer->buffers[cpu];
>  	local_irq_save(flags);
> -	dolock = rb_reader_lock(cpu_buffer);
> +	state = rb_reader_lock(cpu_buffer);
>  
> -	event = rb_buffer_peek(cpu_buffer, ts, lost_events);
> +	if (state != RB_READER_SKIP)
> +		event = rb_buffer_peek(cpu_buffer, ts, lost_events);
> +	else if (ts)
> +		*ts = 0;
>  	if (event) {
>  		cpu_buffer->lost_events = 0;
>  		rb_advance_reader(cpu_buffer);
>  	}
>  
> -	rb_reader_unlock(cpu_buffer, dolock);
> +	rb_reader_unlock(cpu_buffer, state);
>  	local_irq_restore(flags);
>  
>   out:

[Severity: Medium]
Can event retain a stale non-NULL pointer when ring_buffer_consume()
retries at again: and then hits RB_READER_SKIP?

event is initialized to NULL only before the again: label.  If the first
pass peeks a RINGBUF_TYPE_PADDING event and jumps back to again: at the
end of ring_buffer_consume(), and the retry sees state == RB_READER_SKIP,
event is not cleared to NULL (unlike in ring_buffer_peek()).  if (event)
then calls rb_advance_reader(cpu_buffer) without reader_lock or an
nmi_readers reference while rb_remove_pages() is unlinking and freeing
pages, and the trailing RINGBUF_TYPE_PADDING check loops back to again:.

[ ... ]

> @@ -6848,7 +6910,7 @@ bool ring_buffer_empty(struct trace_buffer *buffer)
>  {
>  	struct ring_buffer_per_cpu *cpu_buffer;
>  	unsigned long flags;
> -	bool dolock;
> +	enum rb_reader_lock_state state;
>  	bool ret;
>  	int cpu;
>  
> @@ -6856,9 +6918,10 @@ bool ring_buffer_empty(struct trace_buffer *buffer)
>  	for_each_buffer_cpu(buffer, cpu) {
>  		cpu_buffer = buffer->buffers[cpu];
>  		local_irq_save(flags);
> -		dolock = rb_reader_lock(cpu_buffer);
> -		ret = rb_per_cpu_empty(cpu_buffer);
> -		rb_reader_unlock(cpu_buffer, dolock);
> +		state = rb_reader_lock(cpu_buffer);
> +		ret = state == RB_READER_SKIP ? false :
> +			rb_per_cpu_empty(cpu_buffer);
> +		rb_reader_unlock(cpu_buffer, state);
>  		local_irq_restore(flags);
>  
>  		if (!ret)
> @@ -6878,7 +6941,7 @@ bool ring_buffer_empty_cpu(struct trace_buffer *buffer, int cpu)
>  {
>  	struct ring_buffer_per_cpu *cpu_buffer;
>  	unsigned long flags;
> -	bool dolock;
> +	enum rb_reader_lock_state state;
>  	bool ret;
>  
>  	if (!cpumask_test_cpu(cpu, buffer->cpumask))
> @@ -6886,9 +6949,10 @@ bool ring_buffer_empty_cpu(struct trace_buffer *buffer, int cpu)
>  
>  	cpu_buffer = buffer->buffers[cpu];
>  	local_irq_save(flags);
> -	dolock = rb_reader_lock(cpu_buffer);
> -	ret = rb_per_cpu_empty(cpu_buffer);
> -	rb_reader_unlock(cpu_buffer, dolock);
> +	state = rb_reader_lock(cpu_buffer);
> +	ret = state == RB_READER_SKIP ? false :
> +		rb_per_cpu_empty(cpu_buffer);
> +	rb_reader_unlock(cpu_buffer, state);
>  	local_irq_restore(flags);
>  
>  	return ret;

[Severity: High]
Does returning false from ring_buffer_empty() and ring_buffer_empty_cpu()
on RB_READER_SKIP cause ftrace_dump_one() to spin forever in NMI context?

rb_remove_pages()
  atomic_set_release(&cpu_buffer->resize_in_progress, 1)
  raw_spin_lock_irq(&cpu_buffer->reader_lock)
  <NMI>
    ftrace_dump_one()
      while (!trace_empty(&iter))
        ring_buffer_empty_cpu()  --> false (RB_READER_SKIP)
        trace_find_next_entry_inc(&iter)
          ...
            ring_buffer_peek()   --> NULL (RB_READER_SKIP)
        touch_nmi_watchdog()     <-- spins forever

rb_reader_lock() only returns RB_READER_SKIP in NMI context when
reader_lock is held and resize_in_progress is set.  If ftrace_dump_one()
interrupts rb_remove_pages() (or runs on another CPU during a panic after
stopping the resizing CPU), rb_remove_pages() cannot resume to release
reader_lock or clear resize_in_progress until the NMI dump finishes.
Because ring_buffer_empty_cpu() reports the skipped CPU buffer as
non-empty while ring_buffer_peek() returns NULL, trace_empty(&iter) stays
false and the while (!trace_empty(&iter)) loop in ftrace_dump_one() never
terminates.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261110122610.655756-1-26031212468@stu.xidian.edu.cn?part=1

^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-11-10 12:45 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-10 12:26 [PATCH v2] tracing: ring_buffer: serialize shrink with NMI readers Haoli Zhang
2026-11-10 12:45 ` sashiko-bot

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox