[PATCH] perf/core: Fix refcount bug and potential UAF in perf_mmap

Haocheng Yu posted 1 patch 5 days, 7 hours ago
There is a newer version of this series
kernel/events/core.c | 38 +++++++++++++++++++-------------------
1 file changed, 19 insertions(+), 19 deletions(-)
[PATCH] perf/core: Fix refcount bug and potential UAF in perf_mmap
Posted by Haocheng Yu 5 days, 7 hours ago
Syzkaller reported a refcount_t: addition on 0; use-after-free warning
in perf_mmap.

The issue is caused by a race condition between mmap() and event
teardown. In perf_mmap(), the ring_buffer (rb) is accessed via
map_range() after the mmap_mutex is released. If another thread
closes the event or detaches the buffer during this window, the
reference count of rb can drop to zero, leading to a UAF or
refcount saturation when map_range() or subsequent logic attempts
to use it.

Fix this by extending the scope of mmap_mutex to cover the entire
setup process, including map_range(), ensuring the buffer remains
valid until the mapping is complete.

Reported-by: kernel test robot <lkp@intel.com>
Closes: https://lore.kernel.org/oe-kbuild-all/202602020208.m7KIjdzW-lkp@intel.com/
Signed-off-by: Haocheng Yu <yuhaocheng035@gmail.com>
---
 kernel/events/core.c | 38 +++++++++++++++++++-------------------
 1 file changed, 19 insertions(+), 19 deletions(-)

diff --git a/kernel/events/core.c b/kernel/events/core.c
index 2c35acc2722b..abefd1213582 100644
--- a/kernel/events/core.c
+++ b/kernel/events/core.c
@@ -7167,28 +7167,28 @@ static int perf_mmap(struct file *file, struct vm_area_struct *vma)
 			ret = perf_mmap_aux(vma, event, nr_pages);
 		if (ret)
 			return ret;
-	}
 
-	/*
-	 * Since pinned accounting is per vm we cannot allow fork() to copy our
-	 * vma.
-	 */
-	vm_flags_set(vma, VM_DONTCOPY | VM_DONTEXPAND | VM_DONTDUMP);
-	vma->vm_ops = &perf_mmap_vmops;
+		/*
+		 * Since pinned accounting is per vm we cannot allow fork() to copy our
+		 * vma.
+		 */
+		vm_flags_set(vma, VM_DONTCOPY | VM_DONTEXPAND | VM_DONTDUMP);
+		vma->vm_ops = &perf_mmap_vmops;
 
-	mapped = get_mapped(event, event_mapped);
-	if (mapped)
-		mapped(event, vma->vm_mm);
+		mapped = get_mapped(event, event_mapped);
+		if (mapped)
+			mapped(event, vma->vm_mm);
 
-	/*
-	 * Try to map it into the page table. On fail, invoke
-	 * perf_mmap_close() to undo the above, as the callsite expects
-	 * full cleanup in this case and therefore does not invoke
-	 * vmops::close().
-	 */
-	ret = map_range(event->rb, vma);
-	if (ret)
-		perf_mmap_close(vma);
+		/*
+		 * Try to map it into the page table. On fail, invoke
+		 * perf_mmap_close() to undo the above, as the callsite expects
+		 * full cleanup in this case and therefore does not invoke
+		 * vmops::close().
+		 */
+		ret = map_range(event->rb, vma);
+		if (ret)
+			perf_mmap_close(vma);
+	}
 
 	return ret;
 }

base-commit: 7d0a66e4bb9081d75c82ec4957c50034cb0ea449
-- 
2.51.0
Re: [PATCH] perf/core: Fix refcount bug and potential UAF in perf_mmap
Posted by Peter Zijlstra 5 days, 1 hour ago
On Mon, Feb 02, 2026 at 03:44:35PM +0800, Haocheng Yu wrote:
> Syzkaller reported a refcount_t: addition on 0; use-after-free warning
> in perf_mmap.
> 
> The issue is caused by a race condition between mmap() and event
> teardown. In perf_mmap(), the ring_buffer (rb) is accessed via
> map_range() after the mmap_mutex is released. If another thread
> closes the event or detaches the buffer during this window, the
> reference count of rb can drop to zero, leading to a UAF or
> refcount saturation when map_range() or subsequent logic attempts
> to use it.

So you're saying this is something like:

	Thread-1		Thread-2

	mmap(fd)
				close(fd) / ioctl(fd, IOC_SET_OUTPUT)


I don't think close() is possible, because mmap() should have a
reference on the struct file from fget(), no?

That leaves the ioctl(), let me go have a peek.
Re: [PATCH] perf/core: Fix refcount bug and potential UAF in perf_mmap
Posted by Peter Zijlstra 5 days ago
On Mon, Feb 02, 2026 at 02:58:59PM +0100, Peter Zijlstra wrote:
> On Mon, Feb 02, 2026 at 03:44:35PM +0800, Haocheng Yu wrote:
> > Syzkaller reported a refcount_t: addition on 0; use-after-free warning
> > in perf_mmap.
> > 
> > The issue is caused by a race condition between mmap() and event
> > teardown. In perf_mmap(), the ring_buffer (rb) is accessed via
> > map_range() after the mmap_mutex is released. If another thread
> > closes the event or detaches the buffer during this window, the
> > reference count of rb can drop to zero, leading to a UAF or
> > refcount saturation when map_range() or subsequent logic attempts
> > to use it.
> 
> So you're saying this is something like:
> 
> 	Thread-1		Thread-2
> 
> 	mmap(fd)
> 				close(fd) / ioctl(fd, IOC_SET_OUTPUT)
> 
> 
> I don't think close() is possible, because mmap() should have a
> reference on the struct file from fget(), no?
> 
> That leaves the ioctl(), let me go have a peek.

I'm not seeing it; once perf_mmap_rb() completes, we should have
event->mmap_count != 0, and this the IOC_SET_OUTPUT will fail.

Please provide a better explanation.
Re: [PATCH] perf/core: Fix refcount bug and potential UAF in perf_mmap
Posted by 余昊铖 4 days, 23 hours ago
Hi Peter,

Thanks for the review. You are right, my previous explanation was
inaccurate. The actual race condition occurs between a failing
mmap() on one event and a concurrent mmap() on a second event
that shares the ring buffer (e.g., via output redirection).

Detailed scenario is as follows, for example:
1. Thread A calls mmap(event_A). It allocates the ring buffer, sets
event_A->rb, and initializes refcount to 1. It then drops mmap_mutex.
2. Thread A calls map_range(). Suppose this fails. Thread A then
proceeds to the error path and calls perf_mmap_close().
3. Thread B concurrently calls mmap(event_B), where event_B is
configured to share event_A's buffer. Thread B acquires
event_A->mmap_mutex and sees the valid event_A->rb pointer.
4. The race triggers here: If Thread A's perf_mmap_close() logic
decrements the ring buffer's refcount to 0 (releasing it) but the pointer
event_A->rb is still visible to Thread B (or was read by Thread B before
it was cleared), Thread B triggers the "refcount_t: addition on 0" warning
when it attempts to increment the refcount in perf_mmap_rb().

The fix extends the scope of mmap_mutex to cover map_range() and the
potential error handling path. This ensures that event->rb is only exposed
to other threads after it is fully successfully mapped, or it is cleaned up
atomically inside the lock if mapping fails.

I have updated the commit message accordingly.

Thanks,
Haocheng
[PATCH v2] perf/core: Fix refcount bug and potential UAF in perf_mmap
Posted by yuhaocheng035@gmail.com 4 days, 22 hours ago
From: Haocheng Yu <yuhaocheng035@gmail.com>

Syzkaller reported a refcount_t: addition on 0; use-after-free warning
in perf_mmap.

The issue is caused by a race condition between a failing mmap() setup
and a concurrent mmap() on a dependent event (e.g., using output
redirection).

In perf_mmap(), the ring_buffer (rb) is allocated and assigned to
event->rb with the mmap_mutex held. The mutex is then released to
perform map_range().

If map_range() fails, perf_mmap_close() is called to clean up.
However, since the mutex was dropped, another thread attaching to
this event (via inherited events or output redirection) can acquire
the mutex, observe the valid event->rb pointer, and attempt to
increment its reference count. If the cleanup path has already
dropped the reference count to zero, this results in a
use-after-free or refcount saturation warning.

Fix this by extending the scope of mmap_mutex to cover the
map_range() call. This ensures that the ring buffer initialization
and mapping (or cleanup on failure) happens atomically effectively,
preventing other threads from accessing a half-initialized or
dying ring buffer.

Reported-by: kernel test robot <lkp@intel.com>
Closes: https://lore.kernel.org/oe-kbuild-all/202602020208.m7KIjdzW-lkp@intel.com/
Signed-off-by: Haocheng Yu <yuhaocheng035@gmail.com>
---
 kernel/events/core.c | 38 +++++++++++++++++++-------------------
 1 file changed, 19 insertions(+), 19 deletions(-)

diff --git a/kernel/events/core.c b/kernel/events/core.c
index 2c35acc2722b..abefd1213582 100644
--- a/kernel/events/core.c
+++ b/kernel/events/core.c
@@ -7167,28 +7167,28 @@ static int perf_mmap(struct file *file, struct vm_area_struct *vma)
 			ret = perf_mmap_aux(vma, event, nr_pages);
 		if (ret)
 			return ret;
-	}
 
-	/*
-	 * Since pinned accounting is per vm we cannot allow fork() to copy our
-	 * vma.
-	 */
-	vm_flags_set(vma, VM_DONTCOPY | VM_DONTEXPAND | VM_DONTDUMP);
-	vma->vm_ops = &perf_mmap_vmops;
+		/*
+		 * Since pinned accounting is per vm we cannot allow fork() to copy our
+		 * vma.
+		 */
+		vm_flags_set(vma, VM_DONTCOPY | VM_DONTEXPAND | VM_DONTDUMP);
+		vma->vm_ops = &perf_mmap_vmops;
 
-	mapped = get_mapped(event, event_mapped);
-	if (mapped)
-		mapped(event, vma->vm_mm);
+		mapped = get_mapped(event, event_mapped);
+		if (mapped)
+			mapped(event, vma->vm_mm);
 
-	/*
-	 * Try to map it into the page table. On fail, invoke
-	 * perf_mmap_close() to undo the above, as the callsite expects
-	 * full cleanup in this case and therefore does not invoke
-	 * vmops::close().
-	 */
-	ret = map_range(event->rb, vma);
-	if (ret)
-		perf_mmap_close(vma);
+		/*
+		 * Try to map it into the page table. On fail, invoke
+		 * perf_mmap_close() to undo the above, as the callsite expects
+		 * full cleanup in this case and therefore does not invoke
+		 * vmops::close().
+		 */
+		ret = map_range(event->rb, vma);
+		if (ret)
+			perf_mmap_close(vma);
+	}
 
 	return ret;
 }

base-commit: 7d0a66e4bb9081d75c82ec4957c50034cb0ea449
-- 
2.51.0
Re: [PATCH v2] perf/core: Fix refcount bug and potential UAF in perf_mmap
Posted by Peter Zijlstra 1 day, 6 hours ago
On Tue, Feb 03, 2026 at 12:20:56AM +0800, yuhaocheng035@gmail.com wrote:
> From: Haocheng Yu <yuhaocheng035@gmail.com>
> 
> Syzkaller reported a refcount_t: addition on 0; use-after-free warning
> in perf_mmap.
> 
> The issue is caused by a race condition between a failing mmap() setup
> and a concurrent mmap() on a dependent event (e.g., using output
> redirection).
> 
> In perf_mmap(), the ring_buffer (rb) is allocated and assigned to
> event->rb with the mmap_mutex held. The mutex is then released to
> perform map_range().
> 
> If map_range() fails, perf_mmap_close() is called to clean up.
> However, since the mutex was dropped, another thread attaching to
> this event (via inherited events or output redirection) can acquire
> the mutex, observe the valid event->rb pointer, and attempt to
> increment its reference count. If the cleanup path has already
> dropped the reference count to zero, this results in a
> use-after-free or refcount saturation warning.
> 
> Fix this by extending the scope of mmap_mutex to cover the
> map_range() call. This ensures that the ring buffer initialization
> and mapping (or cleanup on failure) happens atomically effectively,
> preventing other threads from accessing a half-initialized or
> dying ring buffer.

And you're sure this time? To me it feels bit like talking to an LLM.

I suppose there is nothing wrong with having an LLM process syzkaller
output and even have it propose patches, but before you send it out an
actual human should get involved and apply critical thinking skills.

Just throwing stuff at a maintainer and hoping he does the thinking for
you is not appreciated.

> Reported-by: kernel test robot <lkp@intel.com>
> Closes: https://lore.kernel.org/oe-kbuild-all/202602020208.m7KIjdzW-lkp@intel.com/
> Signed-off-by: Haocheng Yu <yuhaocheng035@gmail.com>
> ---
>  kernel/events/core.c | 38 +++++++++++++++++++-------------------
>  1 file changed, 19 insertions(+), 19 deletions(-)
> 
> diff --git a/kernel/events/core.c b/kernel/events/core.c
> index 2c35acc2722b..abefd1213582 100644
> --- a/kernel/events/core.c
> +++ b/kernel/events/core.c
> @@ -7167,28 +7167,28 @@ static int perf_mmap(struct file *file, struct vm_area_struct *vma)
>  			ret = perf_mmap_aux(vma, event, nr_pages);
>  		if (ret)
>  			return ret;
> -	}
>  
> -	/*
> -	 * Since pinned accounting is per vm we cannot allow fork() to copy our
> -	 * vma.
> -	 */
> -	vm_flags_set(vma, VM_DONTCOPY | VM_DONTEXPAND | VM_DONTDUMP);
> -	vma->vm_ops = &perf_mmap_vmops;
> +		/*
> +		 * Since pinned accounting is per vm we cannot allow fork() to copy our
> +		 * vma.
> +		 */
> +		vm_flags_set(vma, VM_DONTCOPY | VM_DONTEXPAND | VM_DONTDUMP);
> +		vma->vm_ops = &perf_mmap_vmops;
>  
> -	mapped = get_mapped(event, event_mapped);
> -	if (mapped)
> -		mapped(event, vma->vm_mm);
> +		mapped = get_mapped(event, event_mapped);
> +		if (mapped)
> +			mapped(event, vma->vm_mm);
>  
> -	/*
> -	 * Try to map it into the page table. On fail, invoke
> -	 * perf_mmap_close() to undo the above, as the callsite expects
> -	 * full cleanup in this case and therefore does not invoke
> -	 * vmops::close().
> -	 */
> -	ret = map_range(event->rb, vma);
> -	if (ret)
> -		perf_mmap_close(vma);
> +		/*
> +		 * Try to map it into the page table. On fail, invoke
> +		 * perf_mmap_close() to undo the above, as the callsite expects
> +		 * full cleanup in this case and therefore does not invoke
> +		 * vmops::close().
> +		 */
> +		ret = map_range(event->rb, vma);
> +		if (ret)
> +			perf_mmap_close(vma);
> +	}
>  
>  	return ret;
>  }
> 
> base-commit: 7d0a66e4bb9081d75c82ec4957c50034cb0ea449
> -- 
> 2.51.0
>