Skip to contentSkip to conversation

perf: Fix deadlock in perf_mmap()

PATCH In review Qing Wang 5 participants 16 messages Mar 9 – May 5 perf events

lore
Share & tools
lore

Qing Wang

author
#0 lore raw

PATCH 1/1 perf: Fix deadlock in perf_mmap()

There is a possible deadlock in perf_mmap() if mmap_range() fails then perf_mmap_close() is called while holding event->mmap_mutex. Since perf_mmap_close() also acquire the same mutex, this result in self-deadlock.

Fix this by moving the cleanup(perf_mmap_close()) outside the scope of scoped_guard(mutex, &event->mmap_mutex).

Fixes 77de62ad3de3 ("perf/core: Fix refcount bug and potential UAF in perf_mmap")
Reported-by [email protected]
---
 kernel/events/core.c | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)
kernel/events/core.c +4 -1
index 1f5699b339ec..e5ce03ce926d 100644
@@ -7485,9 +7485,12 @@ static int perf_mmap(struct file *file, struct vm_area_struct *vma)
 		 */
 		ret = map_range(event->rb, vma);
 		if (ret)
-			perf_mmap_close(vma);
+			goto out_close;
 	}
+	return 0;
 
+out_close:
+	perf_mmap_close(vma);
 	return ret;
 }
 
Signature · 1 line
2.34.1
reviewer
#1 lore raw
Quoted 5 lines · Qing Wang

There is a possible deadlock in perf_mmap() if mmap_range() fails then perf_mmap_close() is called while holding event->mmap_mutex. Since perf_mmap_close() also acquire the same mutex, this result in self-deadlock.

Quoted 7 lines

Fix this by moving the cleanup(perf_mmap_close()) outside the scope of scoped_guard(mutex, &event->mmap_mutex).

Fixes 77de62ad3de3 ("perf/core: Fix refcount bug and potential UAF in perf_mmap")
Reported-by [email protected]

Hi Qing, thank you for looking into this. I proposed a similar fix:
https://lore.kernel.org/lkml/CAP-5=fW-wHEv=TCULwk_HVOhWHdqRd8AZoESZsU_vnhLjghUBQ@mail.gmail.com/
but Haocheng noted it reintroduced the race condition the original fix was targeting. Haocheng has a larger fix in:
https://lore.kernel.org/lkml/[email protected]/
that should handle both the original race condition and the deadlock. I wonder that fix may be made smaller by passing a parameter like "holds_event_mmap_mutex" to perf_mmap_close, something like:

```
static void __perf_mmap_close(struct vm_area_struct *vma, struct
perf_event *event, bool holds_event_mmap_lock)
{
   ... // code from original perf_mmap_close
   if ((!holds_event_mmap_lock &&
!refcount_dec_and_mutex_lock(&event->mmap_count, &event->mmap_mutex))
||
       (holds_event_mmap_lock && !refcount_dec_and_test(&event->mmap_count)))
     ...

static void perf_mmap_close(struct vm_area_struct *vma)
{
       struct perf_event *event = vma->vm_file->private_data;
       __perf_mmap_close(vma, event, /*holds_event_mmap_lock=*/false);
}

static void perf_mmap_close_locked(struct vm_area_struct *vma, struct
perf_event *event)
{
       __perf_mmap_close(vma, event, /*holds_event_mmap_lock=*/true);
}
```

Thanks,
Ian

Quoted 25 lines
---
 kernel/events/core.c | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)
kernel/events/core.c +4 -2
index 1f5699b339ec..e5ce03ce926d 100644
@@ -7485,9 +7485,12 @@ static int perf_mmap(struct file *file, struct vm_area_struct *vma)
                 */
                ret = map_range(event->rb, vma);
                if (ret)
-                       perf_mmap_close(vma);
+                       goto out_close;
        }
+       return 0;
 
+out_close:
+       perf_mmap_close(vma);
        return ret;
 }
 
--

2.34.1

author
#2 lore raw
Quoted 6 lines · Ian Rogers

Hi Qing, thank you for looking into this. I proposed a similar fix:
https://lore.kernel.org/lkml/CAP-5=fW-wHEv=TCULwk_HVOhWHdqRd8AZoESZsU_vnhLjghUBQ@mail.gmail.com/
but Haocheng noted it reintroduced the race condition the original fix was targeting. Haocheng has a larger fix in:
https://lore.kernel.org/lkml/[email protected]/
that should handle both the original race condition and the deadlock.

Oh, this fix is too large.

Quoted 26 lines

I wonder that fix may be made smaller by passing a parameter like "holds_event_mmap_mutex" to perf_mmap_close, something like:

```
static void __perf_mmap_close(struct vm_area_struct *vma, struct
perf_event *event, bool holds_event_mmap_lock)
{
   ... // code from original perf_mmap_close
   if ((!holds_event_mmap_lock &&
!refcount_dec_and_mutex_lock(&event->mmap_count, &event->mmap_mutex))
||
       (holds_event_mmap_lock && !refcount_dec_and_test(&event->mmap_count)))
     ...

static void perf_mmap_close(struct vm_area_struct *vma)
{
       struct perf_event *event = vma->vm_file->private_data;
       __perf_mmap_close(vma, event, /*holds_event_mmap_lock=*/false);
}

static void perf_mmap_close_locked(struct vm_area_struct *vma, struct
perf_event *event)
{
       __perf_mmap_close(vma, event, /*holds_event_mmap_lock=*/true);
}
```
Quoted 2 lines

Thanks,
Ian

LGTM, your fix is better.

But, can you explain why there is still race issue after applying my patch?

Thanks,
Qing

reviewer
#3 lore raw
Quoted 37 lines · Qing Wang
Quoted 6 lines · Ian Rogers

Hi Qing, thank you for looking into this. I proposed a similar fix:
https://lore.kernel.org/lkml/CAP-5=fW-wHEv=TCULwk_HVOhWHdqRd8AZoESZsU_vnhLjghUBQ@mail.gmail.com/
but Haocheng noted it reintroduced the race condition the original fix was targeting. Haocheng has a larger fix in:
https://lore.kernel.org/lkml/[email protected]/
that should handle both the original race condition and the deadlock.

Oh, this fix is too large.

Quoted 26 lines

I wonder that fix may be made smaller by passing a parameter like "holds_event_mmap_mutex" to perf_mmap_close, something like:

```
static void __perf_mmap_close(struct vm_area_struct *vma, struct
perf_event *event, bool holds_event_mmap_lock)
{
   ... // code from original perf_mmap_close
   if ((!holds_event_mmap_lock &&
!refcount_dec_and_mutex_lock(&event->mmap_count, &event->mmap_mutex))
||
       (holds_event_mmap_lock && !refcount_dec_and_test(&event->mmap_count)))
     ...

static void perf_mmap_close(struct vm_area_struct *vma)
{
       struct perf_event *event = vma->vm_file->private_data;
       __perf_mmap_close(vma, event, /*holds_event_mmap_lock=*/false);
}

static void perf_mmap_close_locked(struct vm_area_struct *vma, struct
perf_event *event)
{
       __perf_mmap_close(vma, event, /*holds_event_mmap_lock=*/true);
}
```
Quoted 6 lines
Quoted 2 lines

Thanks,
Ian

LGTM, your fix is better.

But, can you explain why there is still race issue after applying my patch?

Hi Qing,

Haocheng has a complex explanation here:
https://lore.kernel.org/lkml/CAAoXzSoYPvE-AdhT28Rhik2BtFhFuntaM6uhrMqKLUaDgkyiNg@mail.gmail.com/
with the original cause, which I think stemmed from syzkaller fuzzing. I can follow the explanation there but I'm not familiar enough with the ring buffer code to know whether the race with open is benign or not. I'm guessing Peter Z thought not, hence the original patch landed.

Thanks,
Ian

Quoted 2 lines

Thanks,
Qing

reviewer
#4 lore raw
Quoted 48 lines · Ian Rogers
Quoted 37 lines · Qing Wang
Quoted 6 lines · Ian Rogers

Hi Qing, thank you for looking into this. I proposed a similar fix:
https://lore.kernel.org/lkml/CAP-5=fW-wHEv=TCULwk_HVOhWHdqRd8AZoESZsU_vnhLjghUBQ@mail.gmail.com/
but Haocheng noted it reintroduced the race condition the original fix was targeting. Haocheng has a larger fix in:
https://lore.kernel.org/lkml/[email protected]/
that should handle both the original race condition and the deadlock.

Oh, this fix is too large.

Quoted 26 lines

I wonder that fix may be made smaller by passing a parameter like "holds_event_mmap_mutex" to perf_mmap_close, something like:

```
static void __perf_mmap_close(struct vm_area_struct *vma, struct
perf_event *event, bool holds_event_mmap_lock)
{
   ... // code from original perf_mmap_close
   if ((!holds_event_mmap_lock &&
!refcount_dec_and_mutex_lock(&event->mmap_count, &event->mmap_mutex))
||
       (holds_event_mmap_lock && !refcount_dec_and_test(&event->mmap_count)))
     ...

static void perf_mmap_close(struct vm_area_struct *vma)
{
       struct perf_event *event = vma->vm_file->private_data;
       __perf_mmap_close(vma, event, /*holds_event_mmap_lock=*/false);
}

static void perf_mmap_close_locked(struct vm_area_struct *vma, struct
perf_event *event)
{
       __perf_mmap_close(vma, event, /*holds_event_mmap_lock=*/true);
}
```
Quoted 6 lines
Quoted 2 lines

Thanks,
Ian

LGTM, your fix is better.

But, can you explain why there is still race issue after applying my patch?

Hi Qing,

Quoted 7 lines

Haocheng has a complex explanation here:
https://lore.kernel.org/lkml/CAAoXzSoYPvE-AdhT28Rhik2BtFhFuntaM6uhrMqKLUaDgkyiNg@mail.gmail.com/
with the original cause, which I think stemmed from syzkaller fuzzing. I can follow the explanation there but I'm not familiar enough with the ring buffer code to know whether the race with open is benign or not. I'm guessing Peter Z thought not, hence the original patch landed.

I think no fix has landed yet and the original patch with the issue has appeared in backports. Haocheng, did you want to see if you could incorporate my feedback into your fix?

Thanks,
Ian

Quoted 5 lines

Thanks,
Ian

Quoted 2 lines

Thanks,
Qing

#5 lore raw

I apologize for the previous misunderstanding—I assumed you would submit a separate patch, since you came up with the whole idea. Thus I didn't update my patch. Your solution is indeed better. I'd be happy to incorporate your feedback into my fix if you wish.

Thanks,
Haocheng

Quoted 70 lines · Ian Rogers <[email protected]> 于2026年3月25日周三 02:38写道:
Quoted 48 lines · Ian Rogers
Quoted 37 lines · Qing Wang
Quoted 6 lines · Ian Rogers

Hi Qing, thank you for looking into this. I proposed a similar fix:
https://lore.kernel.org/lkml/CAP-5=fW-wHEv=TCULwk_HVOhWHdqRd8AZoESZsU_vnhLjghUBQ@mail.gmail.com/
but Haocheng noted it reintroduced the race condition the original fix was targeting. Haocheng has a larger fix in:
https://lore.kernel.org/lkml/[email protected]/
that should handle both the original race condition and the deadlock.

Oh, this fix is too large.

Quoted 26 lines

I wonder that fix may be made smaller by passing a parameter like "holds_event_mmap_mutex" to perf_mmap_close, something like:

```
static void __perf_mmap_close(struct vm_area_struct *vma, struct
perf_event *event, bool holds_event_mmap_lock)
{
   ... // code from original perf_mmap_close
   if ((!holds_event_mmap_lock &&
!refcount_dec_and_mutex_lock(&event->mmap_count, &event->mmap_mutex))
||
       (holds_event_mmap_lock && !refcount_dec_and_test(&event->mmap_count)))
     ...

static void perf_mmap_close(struct vm_area_struct *vma)
{
       struct perf_event *event = vma->vm_file->private_data;
       __perf_mmap_close(vma, event, /*holds_event_mmap_lock=*/false);
}

static void perf_mmap_close_locked(struct vm_area_struct *vma, struct
perf_event *event)
{
       __perf_mmap_close(vma, event, /*holds_event_mmap_lock=*/true);
}
```
Quoted 6 lines
Quoted 2 lines

Thanks,
Ian

LGTM, your fix is better.

But, can you explain why there is still race issue after applying my patch?

Hi Qing,

Quoted 7 lines

Haocheng has a complex explanation here:
https://lore.kernel.org/lkml/CAAoXzSoYPvE-AdhT28Rhik2BtFhFuntaM6uhrMqKLUaDgkyiNg@mail.gmail.com/
with the original cause, which I think stemmed from syzkaller fuzzing. I can follow the explanation there but I'm not familiar enough with the ring buffer code to know whether the race with open is benign or not. I'm guessing Peter Z thought not, hence the original patch landed.

I think no fix has landed yet and the original patch with the issue has appeared in backports. Haocheng, did you want to see if you could incorporate my feedback into your fix?

Thanks,
Ian

Quoted 5 lines

Thanks,
Ian

Quoted 2 lines

Thanks,
Qing

#6 lore raw

PATCH v3 1/1 perf/core: Fix refcount bug and potential UAF in perf_mmap

From: Haocheng Yu <[email protected]>

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.

v2:
Because expanding the guarded region would cause the event->mmap_mutex to be acquired repeatedly in the perf_mmap_close function, potentially leading to a self deadlock, the original logic of perf_mmap_close was retained, and the mutex-holding logic was modified to obtain the perf_mmap_close_locked function.

v3:
The fix is made smaller by passing the parameter "holds_event_mmap_mutex" to perf_mmap_close.

---
 kernel/events/core.c | 78 +++++++++++++++++++++++++++-----------------
 1 file changed, 48 insertions(+), 30 deletions(-)
kernel/events/core.c +48 -30
index 2c35acc2722b..a3228c587de1 100644
@@ -6730,9 +6730,10 @@ static void perf_pmu_output_stop(struct perf_event *event);
  * the buffer here, where we still have a VM context. This means we need
  * to detach all events redirecting to us.
  */
-static void perf_mmap_close(struct vm_area_struct *vma)
+static void __perf_mmap_close(struct vm_area_struct *vma, struct perf_event *event,
+			      bool holds_event_mmap_lock)
 {
-	struct perf_event *event = vma->vm_file->private_data;
+	struct perf_event *iter_event;
 	mapped_f unmapped = get_mapped(event, event_unmapped);
 	struct perf_buffer *rb = ring_buffer_get(event);
 	struct user_struct *mmap_user = rb->mmap_user;
@@ -6772,11 +6773,14 @@ static void perf_mmap_close(struct vm_area_struct *vma)
 	if (refcount_dec_and_test(&rb->mmap_count))
 		detach_rest = true;
 
-	if (!refcount_dec_and_mutex_lock(&event->mmap_count, &event->mmap_mutex))
+	if ((!holds_event_mmap_lock &&
+	     !refcount_dec_and_mutex_lock(&event->mmap_count, &event->mmap_mutex)) ||
+	    (holds_event_mmap_lock && !refcount_dec_and_test(&event->mmap_count)))
 		goto out_put;
 
 	ring_buffer_attach(event, NULL);
-	mutex_unlock(&event->mmap_mutex);
+	if (!holds_event_mmap_lock)
+		mutex_unlock(&event->mmap_mutex);
 
 	/* If there's still other mmap()s of this buffer, we're done. */
 	if (!detach_rest)
@@ -6789,8 +6793,8 @@ static void perf_mmap_close(struct vm_area_struct *vma)
 	 */
 again:
 	rcu_read_lock();
-	list_for_each_entry_rcu(event, &rb->event_list, rb_entry) {
-		if (!atomic_long_inc_not_zero(&event->refcount)) {
+	list_for_each_entry_rcu(iter_event, &rb->event_list, rb_entry) {
+		if (!atomic_long_inc_not_zero(&iter_event->refcount)) {
 			/*
 			 * This event is en-route to free_event() which will
 			 * detach it and remove it from the list.
@@ -6799,7 +6803,8 @@ static void perf_mmap_close(struct vm_area_struct *vma)
 		}
 		rcu_read_unlock();
 
-		mutex_lock(&event->mmap_mutex);
+		if (!holds_event_mmap_lock)
+			mutex_lock(&iter_event->mmap_mutex);
 		/*
 		 * Check we didn't race with perf_event_set_output() which can
 		 * swizzle the rb from under us while we were waiting to
@@ -6810,11 +6815,12 @@ static void perf_mmap_close(struct vm_area_struct *vma)
 		 * still restart the iteration to make sure we're not now
 		 * iterating the wrong list.
 		 */
-		if (event->rb == rb)
-			ring_buffer_attach(event, NULL);
+		if (iter_event->rb == rb)
+			ring_buffer_attach(iter_event, NULL);
 
-		mutex_unlock(&event->mmap_mutex);
-		put_event(event);
+		if (!holds_event_mmap_lock)
+			mutex_unlock(&iter_event->mmap_mutex);
+		put_event(iter_event);
 
 		/*
 		 * Restart the iteration; either we're on the wrong list or
@@ -6842,6 +6848,18 @@ static void perf_mmap_close(struct vm_area_struct *vma)
 	ring_buffer_put(rb); /* could be last */
 }
 
+static void perf_mmap_close(struct vm_area_struct *vma)
+{
+	struct perf_event *event = vma->vm_file->private_data;
+
+	__perf_mmap_close(vma, event, false);
+}
+
+static void perf_mmap_close_locked(struct vm_area_struct *vma, struct perf_event *event)
+{
+	__perf_mmap_close(vma, event, true);
+}
+
 static vm_fault_t perf_mmap_pfn_mkwrite(struct vm_fault *vmf)
 {
 	/* The first page is the user control page, others are read-only. */
@@ -7167,28 +7185,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_locked(vma, event);
+	}
 
 	return ret;
 }

base-commit: 7d0a66e4bb9081d75c82ec4957c50034cb0ea449

Signature · 1 line
2.51.0
reviewer Reviewed-by on v3 1/1
#7 lore raw

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

Quoted 35 lines · On Wed, Mar 25, 2026 at 3:21 AM <[email protected]> wrote:

From: Haocheng Yu <[email protected]>

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.

v2:
Because expanding the guarded region would cause the event->mmap_mutex to be acquired repeatedly in the perf_mmap_close function, potentially leading to a self deadlock, the original logic of perf_mmap_close was retained, and the mutex-holding logic was modified to obtain the perf_mmap_close_locked function.

Quoted 8 lines

v3:
The fix is made smaller by passing the parameter "holds_event_mmap_mutex" to perf_mmap_close.

Thanks,
Ian

Quoted 148 lines
---
 kernel/events/core.c | 78 +++++++++++++++++++++++++++-----------------
 1 file changed, 48 insertions(+), 30 deletions(-)
kernel/events/core.c +48 -30
index 2c35acc2722b..a3228c587de1 100644
@@ -6730,9 +6730,10 @@ static void perf_pmu_output_stop(struct perf_event *event);
  * the buffer here, where we still have a VM context. This means we need
  * to detach all events redirecting to us.
  */
-static void perf_mmap_close(struct vm_area_struct *vma)
+static void __perf_mmap_close(struct vm_area_struct *vma, struct perf_event *event,
+                             bool holds_event_mmap_lock)
 {
-       struct perf_event *event = vma->vm_file->private_data;
+       struct perf_event *iter_event;
        mapped_f unmapped = get_mapped(event, event_unmapped);
        struct perf_buffer *rb = ring_buffer_get(event);
        struct user_struct *mmap_user = rb->mmap_user;
@@ -6772,11 +6773,14 @@ static void perf_mmap_close(struct vm_area_struct *vma)
        if (refcount_dec_and_test(&rb->mmap_count))
                detach_rest = true;
 
-       if (!refcount_dec_and_mutex_lock(&event->mmap_count, &event->mmap_mutex))
+       if ((!holds_event_mmap_lock &&
+            !refcount_dec_and_mutex_lock(&event->mmap_count, &event->mmap_mutex)) ||
+           (holds_event_mmap_lock && !refcount_dec_and_test(&event->mmap_count)))
                goto out_put;
 
        ring_buffer_attach(event, NULL);
-       mutex_unlock(&event->mmap_mutex);
+       if (!holds_event_mmap_lock)
+               mutex_unlock(&event->mmap_mutex);
 
        /* If there's still other mmap()s of this buffer, we're done. */
        if (!detach_rest)
@@ -6789,8 +6793,8 @@ static void perf_mmap_close(struct vm_area_struct *vma)
         */
 again:
        rcu_read_lock();
-       list_for_each_entry_rcu(event, &rb->event_list, rb_entry) {
-               if (!atomic_long_inc_not_zero(&event->refcount)) {
+       list_for_each_entry_rcu(iter_event, &rb->event_list, rb_entry) {
+               if (!atomic_long_inc_not_zero(&iter_event->refcount)) {
                        /*
                         * This event is en-route to free_event() which will
                         * detach it and remove it from the list.
@@ -6799,7 +6803,8 @@ static void perf_mmap_close(struct vm_area_struct *vma)
                }
                rcu_read_unlock();
 
-               mutex_lock(&event->mmap_mutex);
+               if (!holds_event_mmap_lock)
+                       mutex_lock(&iter_event->mmap_mutex);
                /*
                 * Check we didn't race with perf_event_set_output() which can
                 * swizzle the rb from under us while we were waiting to
@@ -6810,11 +6815,12 @@ static void perf_mmap_close(struct vm_area_struct *vma)
                 * still restart the iteration to make sure we're not now
                 * iterating the wrong list.
                 */
-               if (event->rb == rb)
-                       ring_buffer_attach(event, NULL);
+               if (iter_event->rb == rb)
+                       ring_buffer_attach(iter_event, NULL);
 
-               mutex_unlock(&event->mmap_mutex);
-               put_event(event);
+               if (!holds_event_mmap_lock)
+                       mutex_unlock(&iter_event->mmap_mutex);
+               put_event(iter_event);
 
                /*
                 * Restart the iteration; either we're on the wrong list or
@@ -6842,6 +6848,18 @@ static void perf_mmap_close(struct vm_area_struct *vma)
        ring_buffer_put(rb); /* could be last */
 }
 
+static void perf_mmap_close(struct vm_area_struct *vma)
+{
+       struct perf_event *event = vma->vm_file->private_data;
+
+       __perf_mmap_close(vma, event, false);
+}
+
+static void perf_mmap_close_locked(struct vm_area_struct *vma, struct perf_event *event)
+{
+       __perf_mmap_close(vma, event, true);
+}
+
 static vm_fault_t perf_mmap_pfn_mkwrite(struct vm_fault *vmf)
 {
        /* The first page is the user control page, others are read-only. */
@@ -7167,28 +7185,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_locked(vma, event);
+       }
 
        return ret;
 }

base-commit: 7d0a66e4bb9081d75c82ec4957c50034cb0ea449 --
2.51.0

maintainer
#8 lore raw

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

In reply to [email protected]

Argh,. why is this hidden in this old thread :/

Quoted 18 lines · On Wed, Mar 25, 2026 at 06:20:53PM +0800, [email protected] wrote:
kernel/events/core.c +3 -2
index 2c35acc2722b..a3228c587de1 100644
@@ -6730,9 +6730,10 @@ static void perf_pmu_output_stop(struct perf_event *event);
  * the buffer here, where we still have a VM context. This means we need
  * to detach all events redirecting to us.
  */
-static void perf_mmap_close(struct vm_area_struct *vma)
+static void __perf_mmap_close(struct vm_area_struct *vma, struct perf_event *event,
+			      bool holds_event_mmap_lock)
 {
-	struct perf_event *event = vma->vm_file->private_data;
+	struct perf_event *iter_event;
 	mapped_f unmapped = get_mapped(event, event_unmapped);
 	struct perf_buffer *rb = ring_buffer_get(event);
 	struct user_struct *mmap_user = rb->mmap_user;

kernel/events/core.c static void perf_mmap_close(struct vm_area_struct *vma) · line 6779

Quoted 8 lines
kernel/events/core.c +3 -1
@@ -6772,11 +6773,14 @@ static void perf_mmap_close(struct vm_area_struct *vma)
 	if (refcount_dec_and_test(&rb->mmap_count))
 		detach_rest = true;
 
-	if (!refcount_dec_and_mutex_lock(&event->mmap_count, &event->mmap_mutex))
+	if ((!holds_event_mmap_lock &&
+	     !refcount_dec_and_mutex_lock(&event->mmap_count, &event->mmap_mutex)) ||
+	    (holds_event_mmap_lock && !refcount_dec_and_test(&event->mmap_count)))
 		goto out_put;

*groan*, this is horrible.

Let me have a poke to see if there isn't a saner variant around.

maintainer
#9 lore raw

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

Quoted 34 lines · Peter Zijlstra

Argh,. why is this hidden in this old thread :/

Quoted 18 lines · On Wed, Mar 25, 2026 at 06:20:53PM +0800, [email protected] wrote:
kernel/events/core.c +3 -2
index 2c35acc2722b..a3228c587de1 100644
@@ -6730,9 +6730,10 @@ static void perf_pmu_output_stop(struct perf_event *event);
  * the buffer here, where we still have a VM context. This means we need
  * to detach all events redirecting to us.
  */
-static void perf_mmap_close(struct vm_area_struct *vma)
+static void __perf_mmap_close(struct vm_area_struct *vma, struct perf_event *event,
+			      bool holds_event_mmap_lock)
 {
-	struct perf_event *event = vma->vm_file->private_data;
+	struct perf_event *iter_event;
 	mapped_f unmapped = get_mapped(event, event_unmapped);
 	struct perf_buffer *rb = ring_buffer_get(event);
 	struct user_struct *mmap_user = rb->mmap_user;

kernel/events/core.c static void perf_mmap_close(struct vm_area_struct *vma) · line 6779

Quoted 8 lines
kernel/events/core.c +3 -1
@@ -6772,11 +6773,14 @@ static void perf_mmap_close(struct vm_area_struct *vma)
 	if (refcount_dec_and_test(&rb->mmap_count))
 		detach_rest = true;
 
-	if (!refcount_dec_and_mutex_lock(&event->mmap_count, &event->mmap_mutex))
+	if ((!holds_event_mmap_lock &&
+	     !refcount_dec_and_mutex_lock(&event->mmap_count, &event->mmap_mutex)) ||
+	    (holds_event_mmap_lock && !refcount_dec_and_test(&event->mmap_count)))
 		goto out_put;

*groan*, this is horrible.

Quoted 1 line

Let me have a poke to see if there isn't a saner variant around.

Also, I just realized this patch doesn't even apply, it is against a tree without 77de62ad3de3 ("perf/core: Fix refcount bug and potential UAF in perf_mmap").

author
#10 lore raw

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

In reply to Peter Zijlstra
Quoted 33 lines · Peter Zijlstra

Argh,. why is this hidden in this old thread :/

Quoted 18 lines · On Wed, Mar 25, 2026 at 06:20:53PM +0800, [email protected] wrote:
kernel/events/core.c +3 -2
index 2c35acc2722b..a3228c587de1 100644
@@ -6730,9 +6730,10 @@ static void perf_pmu_output_stop(struct perf_event *event);
  * the buffer here, where we still have a VM context. This means we need
  * to detach all events redirecting to us.
  */
-static void perf_mmap_close(struct vm_area_struct *vma)
+static void __perf_mmap_close(struct vm_area_struct *vma, struct perf_event *event,
+			      bool holds_event_mmap_lock)
 {
-	struct perf_event *event = vma->vm_file->private_data;
+	struct perf_event *iter_event;
 	mapped_f unmapped = get_mapped(event, event_unmapped);
 	struct perf_buffer *rb = ring_buffer_get(event);
 	struct user_struct *mmap_user = rb->mmap_user;

kernel/events/core.c static void perf_mmap_close(struct vm_area_struct *vma) · line 6779

Quoted 8 lines
kernel/events/core.c +3 -1
@@ -6772,11 +6773,14 @@ static void perf_mmap_close(struct vm_area_struct *vma)
 	if (refcount_dec_and_test(&rb->mmap_count))
 		detach_rest = true;
 
-	if (!refcount_dec_and_mutex_lock(&event->mmap_count, &event->mmap_mutex))
+	if ((!holds_event_mmap_lock &&
+	     !refcount_dec_and_mutex_lock(&event->mmap_count, &event->mmap_mutex)) ||
+	    (holds_event_mmap_lock && !refcount_dec_and_test(&event->mmap_count)))
 		goto out_put;

*groan*, this is horrible.

Quoted 1 line

Let me have a poke to see if there isn't a saner variant around.

I think it's ok to move perf_mmap_close() outside the mutex lock, like this:

https://lore.kernel.org/all/[email protected]/T/#m0f82e8ecdfdfce4acd5121bcb799e864cf05ebf9

kernel/events/core.c +4 -1
index 1f5699b339ec..e5ce03ce926d 100644
@@ -7485,9 +7485,12 @@ static int perf_mmap(struct file *file, struct vm_area_struct *vma)
 		 */
 		ret = map_range(event->rb, vma);
 		if (ret)
-			perf_mmap_close(vma);
+			goto out_close;
 	}
+	return 0;
 
+out_close:
+	perf_mmap_close(vma);
 	return ret;
 }
 

How do you think?

--
Qing

maintainer
#11 lore raw

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

Quoted 58 lines · Qing Wang
Quoted 33 lines · Peter Zijlstra

Argh,. why is this hidden in this old thread :/

Quoted 18 lines · On Wed, Mar 25, 2026 at 06:20:53PM +0800, [email protected] wrote:
kernel/events/core.c +3 -2
index 2c35acc2722b..a3228c587de1 100644
@@ -6730,9 +6730,10 @@ static void perf_pmu_output_stop(struct perf_event *event);
  * the buffer here, where we still have a VM context. This means we need
  * to detach all events redirecting to us.
  */
-static void perf_mmap_close(struct vm_area_struct *vma)
+static void __perf_mmap_close(struct vm_area_struct *vma, struct perf_event *event,
+			      bool holds_event_mmap_lock)
 {
-	struct perf_event *event = vma->vm_file->private_data;
+	struct perf_event *iter_event;
 	mapped_f unmapped = get_mapped(event, event_unmapped);
 	struct perf_buffer *rb = ring_buffer_get(event);
 	struct user_struct *mmap_user = rb->mmap_user;

kernel/events/core.c static void perf_mmap_close(struct vm_area_struct *vma) · line 6779

Quoted 8 lines
kernel/events/core.c +3 -1
@@ -6772,11 +6773,14 @@ static void perf_mmap_close(struct vm_area_struct *vma)
 	if (refcount_dec_and_test(&rb->mmap_count))
 		detach_rest = true;
 
-	if (!refcount_dec_and_mutex_lock(&event->mmap_count, &event->mmap_mutex))
+	if ((!holds_event_mmap_lock &&
+	     !refcount_dec_and_mutex_lock(&event->mmap_count, &event->mmap_mutex)) ||
+	    (holds_event_mmap_lock && !refcount_dec_and_test(&event->mmap_count)))
 		goto out_put;

*groan*, this is horrible.

Quoted 1 line

Let me have a poke to see if there isn't a saner variant around.

I think it's ok to move perf_mmap_close() outside the mutex lock, like this:

https://lore.kernel.org/all/[email protected]/T/#m0f82e8ecdfdfce4acd5121bcb799e864cf05ebf9

kernel/events/core.c +4 -1
index 1f5699b339ec..e5ce03ce926d 100644
@@ -7485,9 +7485,12 @@ static int perf_mmap(struct file *file, struct vm_area_struct *vma)
 		 */
 		ret = map_range(event->rb, vma);
 		if (ret)
-			perf_mmap_close(vma);
+			goto out_close;
 	}
+	return 0;
 
+out_close:
+	perf_mmap_close(vma);
 	return ret;
 }
 
Quoted 1 line

How do you think?

Well, that will just re-introduce the original problem. As you were told there.

What about something like this?

kernel/events/core.c +46 -11
index 1f5699b339ec..0bb1d8b83bc9 100644
@@ -7010,6 +7010,7 @@ static void perf_mmap_open(struct vm_area_struct *vma)
 }
 
 static void perf_pmu_output_stop(struct perf_event *event);
+static void perf_mmap_unaccount(struct vm_area_struct *vma, struct perf_buffer *rb);
 
 /*
  * A buffer can be mmap()ed multiple times; either directly through the same
@@ -7025,8 +7026,6 @@ static void perf_mmap_close(struct vm_area_struct *vma)
 	mapped_f unmapped = get_mapped(event, event_unmapped);
 	struct perf_buffer *rb = ring_buffer_get(event);
 	struct user_struct *mmap_user = rb->mmap_user;
-	int mmap_locked = rb->mmap_locked;
-	unsigned long size = perf_data_size(rb);
 	bool detach_rest = false;
 
 	/* FIXIES vs perf_pmu_unregister() */
@@ -7121,11 +7120,7 @@ static void perf_mmap_close(struct vm_area_struct *vma)
 	 * Aside from that, this buffer is 'fully' detached and unmapped,
 	 * undo the VM accounting.
 	 */
-
-	atomic_long_sub((size >> PAGE_SHIFT) + 1 - mmap_locked,
-			&mmap_user->locked_vm);
-	atomic64_sub(mmap_locked, &vma->vm_mm->pinned_vm);
-	free_uid(mmap_user);
+	perf_mmap_unaccount(vma, rb);
 
 out_put:
 	ring_buffer_put(rb); /* could be last */
@@ -7265,6 +7260,15 @@ static void perf_mmap_account(struct vm_area_struct *vma, long user_extra, long
 	atomic64_add(extra, &vma->vm_mm->pinned_vm);
 }
 
+static void perf_mmap_unaccount(struct vm_area_struct *vma, struct perf_buffer *rb)
+{
+	struct user_struct *user = rb->mmap_user;
+
+	atomic_long_sub((perf_data_size(rb) >> PAGE_SHIFT) + 1 - rb->mmap_locked,
+			&user->locked_vm);
+	atomic64_sub(rb->mmap_locked, &vma->vm_mm->pinned_vm);
+}
+
 static int perf_mmap_rb(struct vm_area_struct *vma, struct perf_event *event,
 			unsigned long nr_pages)
 {
@@ -7327,8 +7331,6 @@ static int perf_mmap_rb(struct vm_area_struct *vma, struct perf_event *event,
 	if (!rb)
 		return -ENOMEM;
 
-	refcount_set(&rb->mmap_count, 1);
-	rb->mmap_user = get_current_user();
 	rb->mmap_locked = extra;
 
 	ring_buffer_attach(event, rb);
@@ -7484,10 +7486,43 @@ static int perf_mmap(struct file *file, struct vm_area_struct *vma)
 		 * vmops::close().
 		 */
 		ret = map_range(event->rb, vma);
-		if (ret)
-			perf_mmap_close(vma);
+		if (likely(!ret))
+			return 0;
+
+		/* Error path */
+
+		/*
+		 * If this is the first mmap(), then event->mmap_count should
+		 * be stable at 1. It is only modified by:
+		 * perf_mmap_{open,close}() and perf_mmap().
+		 *
+		 * The former are not possible because this mmap() hasn't been
+		 * successful yet, and the latter is serialized by
+		 * event->mmap_mutex which we still hold (note that mmap_lock
+		 * is not strictly sufficient here, because the event fd can
+		 * be passed to another process through trivial means like
+		 * fork(), leading to concurrent mmap() from different mm).
+		 *
+		 * Make sure to remove event->rb before releasing
+		 * event->mmap_mutex, such that any concurrent mmap() will not
+		 * attempt use this failed buffer.
+		 */
+		if (refcount_read(&event->mmap_count) == 1) {
+			/*
+			 * Minimal perf_mmap_close(); there can't be AUX or
+			 * other events on account of this being the first.
+			 */
+			mapped = get_mapped(event, event_unmapped);
+			if (mapped)
+				mapped(event, vma->vm_mm);
+			perf_mmap_unaccount(vma, event->rb);
+			ring_buffer_attach(event, NULL);	/* drops last rb->refcount */
+			refcount_set(&event->mmap_count, 0);
+			return ret;
+		}
 	}
 
+	perf_mmap_close(vma);
 	return ret;
 }
 
kernel/events/internal.h +1 -0
index d9cc57083091..c03c4f2eea57 100644
@@ -67,6 +67,7 @@ static inline void rb_free_rcu(struct rcu_head *rcu_head)
 	struct perf_buffer *rb;
 
 	rb = container_of(rcu_head, struct perf_buffer, rcu_head);
+	free_uid(rb->mmap_user);
 	rb_free(rb);
 }
 
kernel/events/ring_buffer.c +2 -0
index 3e7de2661417..9fe92161715e 100644
@@ -340,6 +340,8 @@ ring_buffer_init(struct perf_buffer *rb, long watermark, int flags)
 		rb->paused = 1;
 
 	mutex_init(&rb->aux_mutex);
+	rb->mmap_user = get_current_user();
+	refcount_set(&rb->mmap_count, 1);
 }
 
 void perf_aux_output_flag(struct perf_output_handle *handle, u64 flags)
#12 lore raw

PATCH v4 1/1 perf/core: Fix refcount bug and potential UAF in perf_mmap

From: Haocheng Yu <[email protected]>

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.

v2:
Because expanding the guarded region would cause the event->mmap_mutex to be acquired repeatedly in the perf_mmap_close function, potentially leading to a self deadlock, the original logic of perf_mmap_close was retained, and the mutex-holding logic was modified to obtain the perf_mmap_close_locked function.

v3:
The fix is made smaller by passing the parameter "holds_event_mmap_mutex" to perf_mmap_close.

v4:
This problem is solved in a smarter way.

---
 kernel/events/core.c        | 59 +++++++++++++++++++++++++++++--------
 kernel/events/internal.h    |  1 +
 kernel/events/ring_buffer.c |  2 ++
 3 files changed, 49 insertions(+), 13 deletions(-)
kernel/events/core.c +46 -13
index 22a0f405585b..d3f978402b1e 100644
@@ -7010,7 +7010,7 @@ static void perf_mmap_open(struct vm_area_struct *vma)
 }
 
 static void perf_pmu_output_stop(struct perf_event *event);
-
+static void perf_mmap_unaccount(struct vm_area_struct *vma, struct perf_buffer *rb);
 /*
  * A buffer can be mmap()ed multiple times; either directly through the same
  * event, or through other events by use of perf_event_set_output().
@@ -7025,8 +7025,6 @@ static void perf_mmap_close(struct vm_area_struct *vma)
 	mapped_f unmapped = get_mapped(event, event_unmapped);
 	struct perf_buffer *rb = ring_buffer_get(event);
 	struct user_struct *mmap_user = rb->mmap_user;
-	int mmap_locked = rb->mmap_locked;
-	unsigned long size = perf_data_size(rb);
 	bool detach_rest = false;
 
 	/* FIXIES vs perf_pmu_unregister() */
@@ -7121,11 +7119,7 @@ static void perf_mmap_close(struct vm_area_struct *vma)
 	 * Aside from that, this buffer is 'fully' detached and unmapped,
 	 * undo the VM accounting.
 	 */
-
-	atomic_long_sub((size >> PAGE_SHIFT) + 1 - mmap_locked,
-			&mmap_user->locked_vm);
-	atomic64_sub(mmap_locked, &vma->vm_mm->pinned_vm);
-	free_uid(mmap_user);
+	perf_mmap_unaccount(vma, rb);
 
 out_put:
 	ring_buffer_put(rb); /* could be last */
@@ -7265,6 +7259,15 @@ static void perf_mmap_account(struct vm_area_struct *vma, long user_extra, long
 	atomic64_add(extra, &vma->vm_mm->pinned_vm);
 }
 
+static void perf_mmap_unaccount(struct vm_area_struct *vma, struct perf_buffer *rb)
+{
+	struct user_struct *user = rb->mmap_user;
+
+	atomic_long_sub((perf_data_size(rb) >> PAGE_SHIFT) + 1 - rb->mmap_locked,
+			&user->locked_vm);
+	atomic64_sub(rb->mmap_locked, &vma->vm_mm->pinned_vm);
+}
+
 static int perf_mmap_rb(struct vm_area_struct *vma, struct perf_event *event,
 			unsigned long nr_pages)
 {
@@ -7327,8 +7330,6 @@ static int perf_mmap_rb(struct vm_area_struct *vma, struct perf_event *event,
 	if (!rb)
 		return -ENOMEM;
 
-	refcount_set(&rb->mmap_count, 1);
-	rb->mmap_user = get_current_user();
 	rb->mmap_locked = extra;
 
 	ring_buffer_attach(event, rb);
@@ -7484,10 +7485,42 @@ static int perf_mmap(struct file *file, struct vm_area_struct *vma)
 		 * vmops::close().
 		 */
 		ret = map_range(event->rb, vma);
-		if (ret)
-			perf_mmap_close(vma);
-	}
+		if (likely(!ret))
+			return 0;
+
+		/* Error path */
 
+		/*
+		 * If this is the first mmap(), then event->mmap_count should
+		 * be stable at 1. It is only modified by:
+		 * perf_mmap_{open,close}() and perf_mmap().
+		 *
+		 * The former are not possible because this mmap() hasn't been
+		 * successful yet, and the latter is serialized by
+		 * event->mmap_mutex which we still hold (note that mmap_lock
+		 * is not strictly sufficient here, because the event fd can
+		 * be passed to another process through trivial means like
+		 * fork(), leading to concurrent mmap() from different mm).
+		 *
+		 * Make sure to remove event->rb before releasing
+		 * event->mmap_mutex, such that any concurrent mmap() will not
+		 * attempt use this failed buffer.
+		 */
+		if (refcount_read(&event->mmap_count) == 1) {
+			/*
+			 * Minimal perf_mmap_close(); there can't be AUX or
+			 * other events on account of this being the first.
+			 */
+			mapped = get_mapped(event, event_unmapped);
+			if (mapped)
+				mapped(event, vma->vm_mm);
+			perf_mmap_unaccount(vma, event->rb);
+			ring_buffer_attach(event, NULL);	/* drops last rb->refcount */
+			refcount_set(&event->mmap_count, 0);
+			return ret;
+		}
+	}
+	perf_mmap_close(vma);
 	return ret;
 }
 
kernel/events/internal.h +1 -0
index d9cc57083091..c03c4f2eea57 100644
@@ -67,6 +67,7 @@ static inline void rb_free_rcu(struct rcu_head *rcu_head)
 	struct perf_buffer *rb;
 
 	rb = container_of(rcu_head, struct perf_buffer, rcu_head);
+	free_uid(rb->mmap_user);
 	rb_free(rb);
 }
 
kernel/events/ring_buffer.c +2 -0
index 3e7de2661417..9fe92161715e 100644
@@ -340,6 +340,8 @@ ring_buffer_init(struct perf_buffer *rb, long watermark, int flags)
 		rb->paused = 1;
 
 	mutex_init(&rb->aux_mutex);
+	rb->mmap_user = get_current_user();
+	refcount_set(&rb->mmap_count, 1);
 }
 
 void perf_aux_output_flag(struct perf_output_handle *handle, u64 flags)

base-commit: 77de62ad3de3967818c3dbe656b7336ebee461d2

Signature · 1 line
2.51.0
#13 lore raw

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

Your solution looks much better.

I tried to incorporate it and submit a patch v4. If you are already handling it, please ignore my patch.

Thanks,
Haocheng

Quoted 188 lines

From: Haocheng Yu <[email protected]>

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.

v2:
Because expanding the guarded region would cause the event->mmap_mutex to be acquired repeatedly in the perf_mmap_close function, potentially leading to a self deadlock, the original logic of perf_mmap_close was retained, and the mutex-holding logic was modified to obtain the perf_mmap_close_locked function.

v3:
The fix is made smaller by passing the parameter "holds_event_mmap_mutex" to perf_mmap_close.

v4:
This problem is solved in a smarter way.

---
 kernel/events/core.c        | 59 +++++++++++++++++++++++++++++--------
 kernel/events/internal.h    |  1 +
 kernel/events/ring_buffer.c |  2 ++
 3 files changed, 49 insertions(+), 13 deletions(-)
kernel/events/core.c +46 -13
index 22a0f405585b..d3f978402b1e 100644
@@ -7010,7 +7010,7 @@ static void perf_mmap_open(struct vm_area_struct *vma)
 }
 
 static void perf_pmu_output_stop(struct perf_event *event);
-
+static void perf_mmap_unaccount(struct vm_area_struct *vma, struct perf_buffer *rb);
 /*
  * A buffer can be mmap()ed multiple times; either directly through the same
  * event, or through other events by use of perf_event_set_output().
@@ -7025,8 +7025,6 @@ static void perf_mmap_close(struct vm_area_struct *vma)
        mapped_f unmapped = get_mapped(event, event_unmapped);
        struct perf_buffer *rb = ring_buffer_get(event);
        struct user_struct *mmap_user = rb->mmap_user;
-       int mmap_locked = rb->mmap_locked;
-       unsigned long size = perf_data_size(rb);
        bool detach_rest = false;
 
        /* FIXIES vs perf_pmu_unregister() */
@@ -7121,11 +7119,7 @@ static void perf_mmap_close(struct vm_area_struct *vma)
         * Aside from that, this buffer is 'fully' detached and unmapped,
         * undo the VM accounting.
         */
-
-       atomic_long_sub((size >> PAGE_SHIFT) + 1 - mmap_locked,
-                       &mmap_user->locked_vm);
-       atomic64_sub(mmap_locked, &vma->vm_mm->pinned_vm);
-       free_uid(mmap_user);
+       perf_mmap_unaccount(vma, rb);
 
 out_put:
        ring_buffer_put(rb); /* could be last */
@@ -7265,6 +7259,15 @@ static void perf_mmap_account(struct vm_area_struct *vma, long user_extra, long
        atomic64_add(extra, &vma->vm_mm->pinned_vm);
 }
 
+static void perf_mmap_unaccount(struct vm_area_struct *vma, struct perf_buffer *rb)
+{
+       struct user_struct *user = rb->mmap_user;
+
+       atomic_long_sub((perf_data_size(rb) >> PAGE_SHIFT) + 1 - rb->mmap_locked,
+                       &user->locked_vm);
+       atomic64_sub(rb->mmap_locked, &vma->vm_mm->pinned_vm);
+}
+
 static int perf_mmap_rb(struct vm_area_struct *vma, struct perf_event *event,
                        unsigned long nr_pages)
 {
@@ -7327,8 +7330,6 @@ static int perf_mmap_rb(struct vm_area_struct *vma, struct perf_event *event,
        if (!rb)
                return -ENOMEM;
 
-       refcount_set(&rb->mmap_count, 1);
-       rb->mmap_user = get_current_user();
        rb->mmap_locked = extra;
 
        ring_buffer_attach(event, rb);
@@ -7484,10 +7485,42 @@ static int perf_mmap(struct file *file, struct vm_area_struct *vma)
                 * vmops::close().
                 */
                ret = map_range(event->rb, vma);
-               if (ret)
-                       perf_mmap_close(vma);
-       }
+               if (likely(!ret))
+                       return 0;
+
+               /* Error path */
 
+               /*
+                * If this is the first mmap(), then event->mmap_count should
+                * be stable at 1. It is only modified by:
+                * perf_mmap_{open,close}() and perf_mmap().
+                *
+                * The former are not possible because this mmap() hasn't been
+                * successful yet, and the latter is serialized by
+                * event->mmap_mutex which we still hold (note that mmap_lock
+                * is not strictly sufficient here, because the event fd can
+                * be passed to another process through trivial means like
+                * fork(), leading to concurrent mmap() from different mm).
+                *
+                * Make sure to remove event->rb before releasing
+                * event->mmap_mutex, such that any concurrent mmap() will not
+                * attempt use this failed buffer.
+                */
+               if (refcount_read(&event->mmap_count) == 1) {
+                       /*
+                        * Minimal perf_mmap_close(); there can't be AUX or
+                        * other events on account of this being the first.
+                        */
+                       mapped = get_mapped(event, event_unmapped);
+                       if (mapped)
+                               mapped(event, vma->vm_mm);
+                       perf_mmap_unaccount(vma, event->rb);
+                       ring_buffer_attach(event, NULL);        /* drops last rb->refcount */
+                       refcount_set(&event->mmap_count, 0);
+                       return ret;
+               }
+       }
+       perf_mmap_close(vma);
        return ret;
 }
 
kernel/events/internal.h +1 -0
index d9cc57083091..c03c4f2eea57 100644
@@ -67,6 +67,7 @@ static inline void rb_free_rcu(struct rcu_head *rcu_head)
        struct perf_buffer *rb;
 
        rb = container_of(rcu_head, struct perf_buffer, rcu_head);
+       free_uid(rb->mmap_user);
        rb_free(rb);
 }
 
kernel/events/ring_buffer.c +2 -0
index 3e7de2661417..9fe92161715e 100644
@@ -340,6 +340,8 @@ ring_buffer_init(struct perf_buffer *rb, long watermark, int flags)
                rb->paused = 1;
 
        mutex_init(&rb->aux_mutex);
+       rb->mmap_user = get_current_user();
+       refcount_set(&rb->mmap_count, 1);
 }
 
 void perf_aux_output_flag(struct perf_output_handle *handle, u64 flags)

base-commit: 77de62ad3de3967818c3dbe656b7336ebee461d2 --
2.51.0

maintainer
#14 lore raw

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

In reply to [email protected]
Quoted 38 lines · On Fri, Mar 27, 2026 at 08:29:52PM +0800, [email protected] wrote:

From: Haocheng Yu <[email protected]>

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.

v2:
Because expanding the guarded region would cause the event->mmap_mutex to be acquired repeatedly in the perf_mmap_close function, potentially leading to a self deadlock, the original logic of perf_mmap_close was retained, and the mutex-holding logic was modified to obtain the perf_mmap_close_locked function.

v3:
The fix is made smaller by passing the parameter "holds_event_mmap_mutex" to perf_mmap_close.

Quoted 8 lines

v4:
This problem is solved in a smarter way.

You can't claim this as your patch. I was the one who wrote it -- yesterday.

merge bot Applied
#15 lore raw

[tip: perf/core] perf/core: Fix deadlock in perf_mmap() failure path

In reply to Peter Zijlstra
The following commit has been merged into the perf/core branch of tip:

The following commit has been merged into the perf/core branch of tip:

Commit-ID:     c69df06e4e26e50611190ce04eab92c5cc261b61
Gitweb:        https://git.kernel.org/tip/c69df06e4e26e50611190ce04eab92c5cc261b61
Author:        Peter Zijlstra <[email protected]>
AuthorDate:    Thu, 26 Mar 2026 12:28:21 +01:00
Committer:     Peter Zijlstra <[email protected]>
CommitterDate: Tue, 05 May 2026 12:47:20 +02:00

perf/core: Fix deadlock in perf_mmap() failure path

Ian noted that commit 77de62ad3de3 ("perf/core: Fix refcount bug and potential UAF in perf_mmap") would cause a deadlock due to event->mmap_mutex recursion.

This happens because we're now calling perf_mmap_close() under mmap_mutex, while that function itself can also take mmap_mutex.

Solve this by noting that perf_mmap_close() is far more complicated than we need at this particular point, since it deals with scenarios that cannot happen in this particular case.

Replace the call to perf_mmap_close() with a very narrow undo for the case of first-exposure. If this is not the first mmap(), there is no race and it is fine to drop the lock and call perf_mmap_close() to handle to more complicated scenarios.

Note: move the rb->mmap_user (namespace) handling into the rb init/free code such that it does not complicate the mmap handling.

---
 kernel/events/core.c        | 70 ++++++++++++++++++++++++++++--------
 kernel/events/internal.h    |  1 +-
 kernel/events/ring_buffer.c |  2 +-
 3 files changed, 58 insertions(+), 15 deletions(-)
kernel/events/core.c +55 -15
index 6d1f8ba..7935d56 100644
@@ -7006,6 +7006,7 @@ static void perf_mmap_open(struct vm_area_struct *vma)
 }
 
 static void perf_pmu_output_stop(struct perf_event *event);
+static void perf_mmap_unaccount(struct vm_area_struct *vma, struct perf_buffer *rb);
 
 /*
  * A buffer can be mmap()ed multiple times; either directly through the same
@@ -7021,8 +7022,6 @@ static void perf_mmap_close(struct vm_area_struct *vma)
 	mapped_f unmapped = get_mapped(event, event_unmapped);
 	struct perf_buffer *rb = ring_buffer_get(event);
 	struct user_struct *mmap_user = rb->mmap_user;
-	int mmap_locked = rb->mmap_locked;
-	unsigned long size = perf_data_size(rb);
 	bool detach_rest = false;
 
 	/* FIXIES vs perf_pmu_unregister() */
@@ -7117,11 +7116,7 @@ again:
 	 * Aside from that, this buffer is 'fully' detached and unmapped,
 	 * undo the VM accounting.
 	 */
-
-	atomic_long_sub((size >> PAGE_SHIFT) + 1 - mmap_locked,
-			&mmap_user->locked_vm);
-	atomic64_sub(mmap_locked, &vma->vm_mm->pinned_vm);
-	free_uid(mmap_user);
+	perf_mmap_unaccount(vma, rb);
 
 out_put:
 	ring_buffer_put(rb); /* could be last */
@@ -7261,6 +7256,15 @@ static void perf_mmap_account(struct vm_area_struct *vma, long user_extra, long 
 	atomic64_add(extra, &vma->vm_mm->pinned_vm);
 }
 
+static void perf_mmap_unaccount(struct vm_area_struct *vma, struct perf_buffer *rb)
+{
+	struct user_struct *user = rb->mmap_user;
+
+	atomic_long_sub((perf_data_size(rb) >> PAGE_SHIFT) + 1 - rb->mmap_locked,
+			&user->locked_vm);
+	atomic64_sub(rb->mmap_locked, &vma->vm_mm->pinned_vm);
+}
+
 static int perf_mmap_rb(struct vm_area_struct *vma, struct perf_event *event,
 			unsigned long nr_pages)
 {
@@ -7323,8 +7327,6 @@ static int perf_mmap_rb(struct vm_area_struct *vma, struct perf_event *event,
 	if (!rb)
 		return -ENOMEM;
 
-	refcount_set(&rb->mmap_count, 1);
-	rb->mmap_user = get_current_user();
 	rb->mmap_locked = extra;
 
 	ring_buffer_attach(event, rb);
@@ -7474,16 +7476,54 @@ static int perf_mmap(struct file *file, struct vm_area_struct *vma)
 			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().
+		 * Try to map it into the page table. On fail 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);
+		if (likely(!ret))
+			return 0;
+
+		/* Error path */
+
+		/*
+		 * If this is the first mmap(), then event->mmap_count should
+		 * be stable at 1. It is only modified by:
+		 * perf_mmap_{open,close}() and perf_mmap().
+		 *
+		 * The former are not possible because this mmap() hasn't been
+		 * successful yet, and the latter is serialized by
+		 * event->mmap_mutex which we still hold (note that mmap_lock
+		 * is not strictly sufficient here, because the event fd can
+		 * be passed to another process through trivial means like
+		 * fork(), leading to concurrent mmap() from different mm).
+		 *
+		 * Make sure to remove event->rb before releasing
+		 * event->mmap_mutex, such that any concurrent mmap() will not
+		 * attempt use this failed buffer.
+		 */
+		if (refcount_read(&event->mmap_count) == 1) {
+			/*
+			 * Minimal perf_mmap_close(); there can't be AUX or
+			 * other events on account of this being the first.
+			 */
+			mapped = get_mapped(event, event_unmapped);
+			if (mapped)
+				mapped(event, vma->vm_mm);
+			perf_mmap_unaccount(vma, event->rb);
+			ring_buffer_attach(event, NULL);	/* drops last rb->refcount */
+			refcount_set(&event->mmap_count, 0);
+			return ret;
+		}
+
+		/*
+		 * Otherwise this is an already existing buffer, and there is
+		 * no race vs first exposure, so fall-through and call
+		 * perf_mmap_close().
+		 */
 	}
 
+	perf_mmap_close(vma);
 	return ret;
 }
 
kernel/events/internal.h +1 -0
index d9cc570..c03c4f2 100644
@@ -67,6 +67,7 @@ static inline void rb_free_rcu(struct rcu_head *rcu_head)
 	struct perf_buffer *rb;
 
 	rb = container_of(rcu_head, struct perf_buffer, rcu_head);
+	free_uid(rb->mmap_user);
 	rb_free(rb);
 }
 
kernel/events/ring_buffer.c +2 -0
index 3e7de26..9fe9216 100644
@@ -340,6 +340,8 @@ ring_buffer_init(struct perf_buffer *rb, long watermark, int flags)
 		rb->paused = 1;
 
 	mutex_init(&rb->aux_mutex);
+	rb->mmap_user = get_current_user();
+	refcount_set(&rb->mmap_count, 1);
 }
 
 void perf_aux_output_flag(struct perf_output_handle *handle, u64 flags)