Linux Perf Users
 help / color / mirror / Atom feed
* [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