* [BUG] perf: cross-mm mmap teardown unaccounts pinned memory from the wrong mm
@ 2026-07-22 5:59 Rafael B. Dias
2026-07-22 6:03 ` [PATCH] perf/core: unaccount mmap pages from the charged owners Rafael B. Dias
0 siblings, 1 reply; 3+ messages in thread
From: Rafael B. Dias @ 2026-07-22 5:59 UTC (permalink / raw)
To: peterz, mingo, acme, namhyung
Cc: linux-perf-users, linux-kernel, alexander.shishkin, security
Hello,
perf can unaccount a data-ring pinned_vm charge from a different mm than the
one it charged. An unprivileged user can repeat this to create negative
pinned_vm credit and allocate perf ring pages past RLIMIT_MEMLOCK.
The first VMA that allocates a perf data ring charges the excess pages to
vma->vm_mm->pinned_vm and stores the amount in rb->mmap_locked, but not the
charged mm. If another process maps the perf event fd and closes the final
VMA, perf_mmap_close() subtracts the charge from that process's mm. The
allocating mm remains charged while the final closer gets a negative balance.
I reproduced this as UID 1000 with CapEff=0, NoNewPrivs=1,
perf_event_paranoid=2, and self-owned PERF_TYPE_SOFTWARE events. On a
two-CPU Linux 6.12.94 guest with perf_event_mlock_kb=516, the reproducer kept
270 perf pages live, used 23 short-lived address spaces to create 16,385 pages
of credit, and mapped 65,540 KiB in a receiver with RLIMIT_MEMLOCK=0.
The vulnerable result was:
RESULT mode=vulnerable mmap_errno=0
LIMIT receiver_memlock_kb=0 perf_mlock_kb=516 cpus=2
BASE allowance_pages=258 filler_pages=270
CREDIT pages=16385 requested_pages=16385 requested_kb=65540
With only the accounting owner corrected, the same binary returned EPERM:
RESULT mode=fixed mmap_errno=1
LIMIT receiver_memlock_kb=0 perf_mlock_kb=516 cpus=2
BASE allowance_pages=258 filler_pages=270
CREDIT pages=16385 requested_pages=16385 requested_kb=65540
Commit 26cb63ad11e0 ("perf: Fix perf mmap bugs") introduced the data-ring
accounting pattern. It remains in Linux 6.12.96 and mainline commit
248951ddc14d. AUX teardown has the same final-VMA assumption and also uses the
data ring's mmap_user when a different user allocated AUX.
The proposed patch retains the charged data-ring mm and AUX user/mm, then uses
those owners during final unaccounting. It passes checkpatch and builds
kernel/events/core.o against mainline 248951ddc14d. On a patched 6.12.94 KASAN
kernel, the same reproducer returns EPERM. This runtime test covers the data
ring; the AUX change was reviewed and build-tested.
The source reproducer is available privately on request.
Until a fix is available, workloads can block untrusted perf_event_open()
calls with seccomp or an LSM.
Regards,
Rafael B. Dias
^ permalink raw reply [flat|nested] 3+ messages in thread* [PATCH] perf/core: unaccount mmap pages from the charged owners 2026-07-22 5:59 [BUG] perf: cross-mm mmap teardown unaccounts pinned memory from the wrong mm Rafael B. Dias @ 2026-07-22 6:03 ` Rafael B. Dias 2026-07-22 6:16 ` sashiko-bot 0 siblings, 1 reply; 3+ messages in thread From: Rafael B. Dias @ 2026-07-22 6:03 UTC (permalink / raw) To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo, Namhyung Kim Cc: linux-perf-users, linux-kernel, Alexander Shishkin, security perf mmap accounting splits pages between user_struct::locked_vm and mm_struct::pinned_vm. The first mapping that allocates a data ring stores the charged user and numeric pinned amount in perf_buffer, but it does not store the mm that received the pinned charge. A perf event fd can be mapped by more than one process. If a different mm owns the final VMA close, perf_mmap_unaccount() subtracts mmap_locked from that final mm instead of the mm charged at allocation. Repeating this across short-lived donor processes creates a negative pinned_vm balance that can fund later perf mappings beyond RLIMIT_MEMLOCK. AUX has the same last-VMA assumption. It also subtracts locked_vm from the data ring's mmap_user even when a different user created the AUX mapping. Retain references to the mm charged for the data ring and to the user and mm charged for AUX. Use those stored owners during final unaccounting and release the references after the counters are restored. On a two-CPU 6.12.94 KASAN guest, an unprivileged receiver with RLIMIT_MEMLOCK=0 accumulated 16,385 pages of cross-mm credit and mapped 65,540 KiB on the vulnerable kernel. The same reproducer returned EPERM with this ownership fix. No KASAN report or warning occurred. The patch also builds kernel/events/core.o with x86_64_defconfig on the same base. Fixes: 26cb63ad11e0 ("perf: Fix perf mmap bugs") Fixes: 36b3db03b474 ("perf/core: Fix the mlock accounting, again") Cc: stable@vger.kernel.org Assisted-by: OpenAI-Codex:gpt-5.6-sol Signed-off-by: Rafael B. Dias <rafaelbttdias@gmail.com> --- kernel/events/core.c | 27 +++++++++++++++++++-------- kernel/events/internal.h | 3 +++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/kernel/events/core.c b/kernel/events/core.c index ba5bd6a78fe7..bf046e44de20 100644 --- a/kernel/events/core.c +++ b/kernel/events/core.c @@ -7006,7 +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); +static void perf_mmap_unaccount(struct perf_buffer *rb); /* * A buffer can be mmap()ed multiple times; either directly through the same @@ -7021,7 +7021,6 @@ static void perf_mmap_close(struct vm_area_struct *vma) struct perf_event *event = vma->vm_file->private_data; mapped_f unmapped = get_mapped(event, event_unmapped); struct perf_buffer *rb = ring_buffer_get(event); - struct user_struct *mmap_user = rb->mmap_user; bool detach_rest = false; /* FIXIES vs perf_pmu_unregister() */ @@ -7043,8 +7042,13 @@ static void perf_mmap_close(struct vm_area_struct *vma) perf_pmu_output_stop(event); /* now it's safe to free the pages */ - atomic_long_sub(rb->aux_nr_pages - rb->aux_mmap_locked, &mmap_user->locked_vm); - atomic64_sub(rb->aux_mmap_locked, &vma->vm_mm->pinned_vm); + atomic_long_sub(rb->aux_nr_pages - rb->aux_mmap_locked, + &rb->aux_mmap_user->locked_vm); + atomic64_sub(rb->aux_mmap_locked, &rb->aux_mmap_mm->pinned_vm); + free_uid(rb->aux_mmap_user); + mmdrop(rb->aux_mmap_mm); + rb->aux_mmap_user = NULL; + rb->aux_mmap_mm = NULL; /* this has to be the last one */ rb_free_aux(rb); @@ -7116,7 +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. */ - perf_mmap_unaccount(vma, rb); + perf_mmap_unaccount(rb); out_put: ring_buffer_put(rb); /* could be last */ @@ -7258,13 +7262,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) +static void perf_mmap_unaccount(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); + atomic64_sub(rb->mmap_locked, &rb->mmap_mm->pinned_vm); + mmdrop(rb->mmap_mm); + rb->mmap_mm = NULL; } static int perf_mmap_rb(struct vm_area_struct *vma, struct perf_event *event, @@ -7330,6 +7336,8 @@ static int perf_mmap_rb(struct vm_area_struct *vma, struct perf_event *event, return -ENOMEM; rb->mmap_locked = extra; + rb->mmap_mm = vma->vm_mm; + mmgrab(rb->mmap_mm); ring_buffer_attach(event, rb); @@ -7411,6 +7419,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; + rb->aux_mmap_user = get_current_user(); + rb->aux_mmap_mm = vma->vm_mm; + mmgrab(rb->aux_mmap_mm); } perf_mmap_account(vma, user_extra, extra); @@ -7512,7 +7523,7 @@ static int perf_mmap(struct file *file, struct vm_area_struct *vma) mapped = get_mapped(event, event_unmapped); if (mapped) mapped(event, vma->vm_mm); - perf_mmap_unaccount(vma, event->rb); + perf_mmap_unaccount(event->rb); ring_buffer_attach(event, NULL); /* drops last rb->refcount */ refcount_set(&event->mmap_count, 0); return ret; diff --git a/kernel/events/internal.h b/kernel/events/internal.h index c03c4f2eea57..9bdf586735e8 100644 --- a/kernel/events/internal.h +++ b/kernel/events/internal.h @@ -38,6 +38,7 @@ struct perf_buffer { refcount_t mmap_count; unsigned long mmap_locked; struct user_struct *mmap_user; + struct mm_struct *mmap_mm; /* AUX area */ struct mutex aux_mutex; @@ -49,6 +50,8 @@ struct perf_buffer { int aux_overwrite; refcount_t aux_mmap_count; unsigned long aux_mmap_locked; + struct user_struct *aux_mmap_user; + struct mm_struct *aux_mmap_mm; void (*free_aux)(void *); refcount_t aux_refcount; int aux_in_sampling; ^ permalink raw reply related [flat|nested] 3+ messages in thread
* Re: [PATCH] perf/core: unaccount mmap pages from the charged owners 2026-07-22 6:03 ` [PATCH] perf/core: unaccount mmap pages from the charged owners Rafael B. Dias @ 2026-07-22 6:16 ` sashiko-bot 0 siblings, 0 replies; 3+ messages in thread From: sashiko-bot @ 2026-07-22 6:16 UTC (permalink / raw) To: Rafael B. Dias; +Cc: linux-perf-users Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: Pre-existing issues: - [High] Secondary mmaps (aliases) of a perf event ring buffer permanently leak `user->locked_vm` accounting, allowing unprivileged users to trivially exhaust their `RLIMIT_MEMLOCK`. -- --- Patch [1]: [PATCH] perf/core: unaccount mmap pages from the charged owners --- commit ab7cc1fb3c31285ce5c06e49334a15704ae57a1b Author: Rafael B. Dias <rafaelbttdias@gmail.com> Subject: perf/core: unaccount mmap pages from the charged owners This commit modifies perf mmap accounting to retain references to the mm_struct and user_struct charged during ring buffer allocation. It then uses these stored owners during the final unaccounting to prevent cross-mm accounting imbalances caused by short-lived donor processes. > diff --git a/kernel/events/core.c b/kernel/events/core.c > index 51c1200ea3fdd..12b300c58420d 100644 > --- a/kernel/events/core.c > +++ b/kernel/events/core.c [ ... ] > @@ -7259,13 +7263,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) > +static void perf_mmap_unaccount(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); > + atomic64_sub(rb->mmap_locked, &rb->mmap_mm->pinned_vm); > + mmdrop(rb->mmap_mm); > + rb->mmap_mm = NULL; > } [Severity: High] This is a pre-existing issue, but does this unaccounting logic permanently leak locked_vm charges for secondary mmaps (aliases)? Looking at perf_mmap_rb(), every time a secondary mmap is created, it charges the user's locked_vm: if (refcount_inc_not_zero(&event->rb->mmap_count)) { /* * Success -- managed to mmap() the same buffer * multiple times. */ perf_mmap_account(vma, user_extra, extra); refcount_inc(&event->mmap_count); return 0; } The same pattern exists in perf_mmap_aux() for AUX buffers, where perf_mmap_account() is unconditionally called for alias mappings. However, in perf_mmap_close(), perf_mmap_unaccount() is only called on the final unmap when the refcount drops to zero. Similarly, the AUX unaccounting is gated behind refcount_dec_and_mutex_lock(&rb->aux_mmap_count, &rb->aux_mutex), which skips secondary unmaps. Since an unprivileged user can repeatedly mmap and munmap the same perf event alias, won't this leak locked_vm charges on every secondary munmap, eventually allowing trivial exhaustion of their RLIMIT_MEMLOCK? -- Sashiko AI review · https://sashiko.dev/#/patchset/20260722060353.24-1-rafaelbttdias@gmail.com?part=1 ^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-07-22 6:16 UTC | newest] Thread overview: 3+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-07-22 5:59 [BUG] perf: cross-mm mmap teardown unaccounts pinned memory from the wrong mm Rafael B. Dias 2026-07-22 6:03 ` [PATCH] perf/core: unaccount mmap pages from the charged owners Rafael B. Dias 2026-07-22 6:16 ` sashiko-bot
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox