[PATCH v2] perf/core: fix user->locked_vm leak on secondary mmap()

From: Hui Peng

Date: Sun Sep 20 2026 - 20:33:20 EST


Commit 0c8a4e4139ad ("perf/core: Further simplify perf_mmap()") hoisted
`user_extra = nr_pages` ahead of the existing-buffer checks (`if
(event->rb)` and `if (rb_has_aux(rb))`), and subsequently
commit 5d299897f1e3 ("perf: Split out the RB allocation") and
commit 2aee37682391 ("perf: Split out the AUX buffer allocation")
carried `long extra = 0, user_extra = nr_pages` into perf_mmap_rb() and
perf_mmap_aux().

As a result, when an already-allocated ring buffer (`event->rb`) or AUX
buffer (`rb_has_aux(rb)`) is mapped again via mmap(), perf_mmap_account()
is called with `user_extra = nr_pages` instead of `0`, charging `nr_pages`
to `current_user()->locked_vm` on every additional mapping. However,
perf_mmap_unaccount() and perf_mmap_close() only subtract the ring buffer's
and AUX buffer's pages once when the final `rb->mmap_count` /
`rb->aux_mmap_count` reference drops to zero. Consequently, every secondary
mmap() + munmap() cycle on a perf event permanently leaks `nr_pages` in
`user->locked_vm`, eventually exhausting `perf_event_mlock_kb` and
`RLIMIT_MEMLOCK` (-EPERM) for that user.

Fix this by only calling perf_mmap_account() when allocating a new ring
buffer in perf_mmap_rb() or a new AUX buffer in perf_mmap_aux().

Tested in QEMU against Linux 7.3.0-rc3 with a standalone C reproducer
running as an unprivileged user (UID 1000, RLIMIT_MEMLOCK=0,
perf_event_paranoid=1) that opens a software perf event, maps a 65-page
ring buffer, performs 10 secondary mmap() + munmap() cycles on the same
event fd, and then unmaps and closes the event. On the unfixed kernel,
subsequent perf_mmap() calls by UID 1000 permanently fail with -EPERM due
to the leaked `user->locked_vm` (650 pages leaked), whereas with the fix
applied `user->locked_vm` returns to 0 and subsequent perf_mmap() calls
succeed.

Fixes: 0c8a4e4139ad ("perf/core: Further simplify perf_mmap()")
Cc: stable@xxxxxxxxxxxxxxx
Assisted-by: LLM
Signed-off-by: Hui Peng <benquike@xxxxxxxxx>
---
Changes in v2:
- Dropped the cross-MM `pinned_vm` / `rb->mmap_mm` (`mmgrab`/`mmdrop`)
changes (which caused an RCU softirq context violation in `rb_free_rcu()`
in v1, flagged by sashiko-bot) to keep this patch focused on the
`user->locked_vm` leak on secondary `mmap()`.
- Updated the `Fixes:` tag to `0c8a4e4139ad ("perf/core: Further simplify
perf_mmap()")` and added QEMU reproducer test details to the commit
message.

kernel/events/core.c | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/kernel/events/core.c b/kernel/events/core.c
index fe33fe15689d..8fc15239bd7e 100644
--- a/kernel/events/core.c
+++ b/kernel/events/core.c
@@ -7311,7 +7311,6 @@ static int perf_mmap_rb(struct vm_area_struct *vma, struct perf_event *event,
* Success -- managed to mmap() the same buffer
* multiple times.
*/
- perf_mmap_account(vma, user_extra, extra);
refcount_inc(&event->mmap_count);
return 0;
}
@@ -7399,7 +7398,6 @@ static int perf_mmap_aux(struct vm_area_struct *vma, struct perf_event *event,

if (rb_has_aux(rb)) {
refcount_inc(&rb->aux_mmap_count);
-
} else {
if (!perf_mmap_calc_limits(vma, &user_extra, &extra)) {
refcount_dec(&rb->mmap_count);
@@ -7420,9 +7418,9 @@ static int perf_mmap_aux(struct vm_area_struct *vma, struct perf_event *event,

refcount_set(&rb->aux_mmap_count, 1);
rb->aux_mmap_locked = extra;
+ perf_mmap_account(vma, user_extra, extra);
}

- perf_mmap_account(vma, user_extra, extra);
refcount_inc(&event->mmap_count);

return 0;
--
2.47.3