* [PATCH] mm/mremap: reset unfaulted VMA page offset for MREMAP_DONTUNMAP
@ 2026-08-25 7:55 Lorenzo Stoakes (ARM)
2026-08-26 15:15 ` Kunwu Chan
` (2 more replies)
0 siblings, 3 replies; 7+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-08-25 7:55 UTC (permalink / raw)
To: Andrew Morton, Liam R. Howlett, Vlastimil Babka, Jann Horn,
Pedro Falcato, Li Xinhai
Cc: linux-mm, linux-kernel, syzbot+f12658786a4153df5113, stable,
Lorenzo Stoakes (ARM)
Uniquely an mremap() invocation using the MREMAP_DONTUNMAP flag can reset
a faulted VMA into an unfaulted one.
It does so after the page tables have been moved to the copied VMA with
MREMAP_DONTUNMAP leaving the old VMA in place which is naturally unfaulted
as the page tables it had are no longer present.
However, in doing so, it violates the invariant that the anonymous page
offset of an unfaulted VMA is vma->vm_start >> PAGE_SHIFT.
This is because a VMA may have been faulted in, mremap()'d (causing a delta
between its page offset and vma->vm_start >> PAGE_SHIFT), and then
mremap()'d again with MREMAP_DONTUNMAP resulting in the unfaulting.
This condition is a violation of a fundamental assumption in mm, but now
also triggers an assert in assert_sane_pgoff() which explicitly checks for
this condition.
Correct it by resetting the VMA's page offset at the point of completing
the MREMAP_DONTUNMAP operation.
Reported-by: syzbot+f12658786a4153df5113@syzkaller.appspotmail.com
Closes: https://lore.kernel.org/all/6a87853b.ae6ddae5.3da009.0023.GAE@google.com/
Fixes: 1583aa278f5f ("mm: mremap: unlink anon_vmas when mremap with MREMAP_DONTUNMAP success")
Cc: stable@vger.kernel.org
Signed-off-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
---
mm/mremap.c | 22 +++++++++++++++++-----
1 file changed, 17 insertions(+), 5 deletions(-)
diff --git a/mm/mremap.c b/mm/mremap.c
index e8df5cdb0ac9..2b4b523a86b8 100644
--- a/mm/mremap.c
+++ b/mm/mremap.c
@@ -1331,18 +1331,30 @@ static void dontunmap_complete(struct vma_remap_struct *vrm,
{
unsigned long start = vrm->addr;
unsigned long end = vrm->addr + vrm->old_len;
- unsigned long old_start = vrm->vma->vm_start;
- unsigned long old_end = vrm->vma->vm_end;
+ struct vm_area_struct *vma = vrm->vma;
+ unsigned long old_start = vma->vm_start;
+ unsigned long old_end = vma->vm_end;
/* We always clear VMA_LOCKED[ONFAULT]_BIT on the old VMA. */
- vma_clear_flags_mask(vrm->vma, VMA_LOCKED_MASK);
+ vma_clear_flags_mask(vma, VMA_LOCKED_MASK);
/*
* anon_vma links of the old vma is no longer needed after its page
* table has been moved.
*/
- if (new_vma != vrm->vma && start == old_start && end == old_end)
- unlink_anon_vmas(vrm->vma);
+ if (new_vma != vma && start == old_start && end == old_end) {
+ const pgoff_t pgoff_unfaulted = vma->vm_start >> PAGE_SHIFT;
+
+ unlink_anon_vmas(vma);
+ /*
+ * The VMA is now unfaulted and it is an invariant that
+ * unfaulted anonymous VMAs have page offset equal to
+ * vma->vm_start >> PAGE_SHIFT.
+ */
+ vma_set_anon_pgoff(vma, pgoff_unfaulted);
+ if (vma_is_anonymous(vma) && !vma->vm_file)
+ vma_set_pgoff(vma, pgoff_unfaulted);
+ }
/* Because we won't unmap we don't need to touch locked_vm. */
}
---
base-commit: efecab401cb15fd3bb9bc05990609acb6b267ff2
change-id: 20260824-fix-mremap-dontunmap-pgoff-a687134e995e
Best regards,
--
Lorenzo Stoakes (ARM) <ljs@kernel.org>
^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH] mm/mremap: reset unfaulted VMA page offset for MREMAP_DONTUNMAP
2026-08-25 7:55 [PATCH] mm/mremap: reset unfaulted VMA page offset for MREMAP_DONTUNMAP Lorenzo Stoakes (ARM)
@ 2026-08-26 15:15 ` Kunwu Chan
2026-08-26 15:27 ` Lorenzo Stoakes (ARM)
2026-08-27 2:26 ` Andrew Morton
2026-08-27 8:32 ` Vlastimil Babka (SUSE)
2 siblings, 1 reply; 7+ messages in thread
From: Kunwu Chan @ 2026-08-26 15:15 UTC (permalink / raw)
To: Lorenzo Stoakes (ARM)
Cc: Kunwu Chan, Andrew Morton, Liam R. Howlett, Vlastimil Babka,
Jann Horn, Pedro Falcato, Li Xinhai, linux-mm, linux-kernel,
syzbot+f12658786a4153df5113, stable
On Tue, 25 Aug 2026 08:55:26 +0100 "Lorenzo Stoakes (ARM)" <ljs@kernel.org> wrote:
> Uniquely an mremap() invocation using the MREMAP_DONTUNMAP flag can reset
> a faulted VMA into an unfaulted one.
>
> It does so after the page tables have been moved to the copied VMA with
> MREMAP_DONTUNMAP leaving the old VMA in place which is naturally unfaulted
> as the page tables it had are no longer present.
>
> However, in doing so, it violates the invariant that the anonymous page
> offset of an unfaulted VMA is vma->vm_start >> PAGE_SHIFT.
>
> This is because a VMA may have been faulted in, mremap()'d (causing a delta
> between its page offset and vma->vm_start >> PAGE_SHIFT), and then
> mremap()'d again with MREMAP_DONTUNMAP resulting in the unfaulting.
>
> This condition is a violation of a fundamental assumption in mm, but now
> also triggers an assert in assert_sane_pgoff() which explicitly checks for
> this condition.
>
> Correct it by resetting the VMA's page offset at the point of completing
> the MREMAP_DONTUNMAP operation.
>
> Reported-by: syzbot+f12658786a4153df5113@syzkaller.appspotmail.com
> Closes: https://lore.kernel.org/all/6a87853b.ae6ddae5.3da009.0023.GAE@google.com/
> Fixes: 1583aa278f5f ("mm: mremap: unlink anon_vmas when mremap with MREMAP_DONTUNMAP success")
> Cc: stable@vger.kernel.org
> Signed-off-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
> ---
> mm/mremap.c | 22 +++++++++++++++++-----
> 1 file changed, 17 insertions(+), 5 deletions(-)
>
> diff --git a/mm/mremap.c b/mm/mremap.c
> index e8df5cdb0ac9..2b4b523a86b8 100644
> --- a/mm/mremap.c
> +++ b/mm/mremap.c
> @@ -1331,18 +1331,30 @@ static void dontunmap_complete(struct vma_remap_struct *vrm,
> {
> unsigned long start = vrm->addr;
> unsigned long end = vrm->addr + vrm->old_len;
> - unsigned long old_start = vrm->vma->vm_start;
> - unsigned long old_end = vrm->vma->vm_end;
> + struct vm_area_struct *vma = vrm->vma;
> + unsigned long old_start = vma->vm_start;
> + unsigned long old_end = vma->vm_end;
>
> /* We always clear VMA_LOCKED[ONFAULT]_BIT on the old VMA. */
> - vma_clear_flags_mask(vrm->vma, VMA_LOCKED_MASK);
> + vma_clear_flags_mask(vma, VMA_LOCKED_MASK);
>
> /*
> * anon_vma links of the old vma is no longer needed after its page
> * table has been moved.
> */
> - if (new_vma != vrm->vma && start == old_start && end == old_end)
> - unlink_anon_vmas(vrm->vma);
> + if (new_vma != vma && start == old_start && end == old_end) {
> + const pgoff_t pgoff_unfaulted = vma->vm_start >> PAGE_SHIFT;
> +
> + unlink_anon_vmas(vma);
> + /*
> + * The VMA is now unfaulted and it is an invariant that
> + * unfaulted anonymous VMAs have page offset equal to
> + * vma->vm_start >> PAGE_SHIFT.
> + */
> + vma_set_anon_pgoff(vma, pgoff_unfaulted);
> + if (vma_is_anonymous(vma) && !vma->vm_file)
> + vma_set_pgoff(vma, pgoff_unfaulted);
Hi Lorenzo,
I think the fix makes sense. One thing I wanted to make sure
I understand correctly is the distinction between anon_pgoff
and vm_pgoff here.
Is the intention that anon_pgoff should be reset when the
VMA becomes unfaulted, while vm_pgoff should only be reset
for a truly anonymous VMA, since it may retain file-offset
semantics for VMAs with a vm_file?
Thanks,
KunWu
> + }
>
> /* Because we won't unmap we don't need to touch locked_vm. */
> }
>
> ---
> base-commit: efecab401cb15fd3bb9bc05990609acb6b267ff2
> change-id: 20260824-fix-mremap-dontunmap-pgoff-a687134e995e
>
> Best regards,
> --
> Lorenzo Stoakes (ARM) <ljs@kernel.org>
>
>
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH] mm/mremap: reset unfaulted VMA page offset for MREMAP_DONTUNMAP
2026-08-26 15:15 ` Kunwu Chan
@ 2026-08-26 15:27 ` Lorenzo Stoakes (ARM)
0 siblings, 0 replies; 7+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-08-26 15:27 UTC (permalink / raw)
To: Kunwu Chan
Cc: Kunwu Chan, Andrew Morton, Liam R. Howlett, Vlastimil Babka,
Jann Horn, Pedro Falcato, Li Xinhai, linux-mm, linux-kernel,
syzbot+f12658786a4153df5113, stable
On Wed, Aug 26, 2026 at 11:15:18PM +0800, Kunwu Chan wrote:
> On Tue, 25 Aug 2026 08:55:26 +0100 "Lorenzo Stoakes (ARM)" <ljs@kernel.org> wrote:
>
> > Uniquely an mremap() invocation using the MREMAP_DONTUNMAP flag can reset
> > a faulted VMA into an unfaulted one.
> >
> > It does so after the page tables have been moved to the copied VMA with
> > MREMAP_DONTUNMAP leaving the old VMA in place which is naturally unfaulted
> > as the page tables it had are no longer present.
> >
> > However, in doing so, it violates the invariant that the anonymous page
> > offset of an unfaulted VMA is vma->vm_start >> PAGE_SHIFT.
> >
> > This is because a VMA may have been faulted in, mremap()'d (causing a delta
> > between its page offset and vma->vm_start >> PAGE_SHIFT), and then
> > mremap()'d again with MREMAP_DONTUNMAP resulting in the unfaulting.
> >
> > This condition is a violation of a fundamental assumption in mm, but now
> > also triggers an assert in assert_sane_pgoff() which explicitly checks for
> > this condition.
> >
> > Correct it by resetting the VMA's page offset at the point of completing
> > the MREMAP_DONTUNMAP operation.
> >
> > Reported-by: syzbot+f12658786a4153df5113@syzkaller.appspotmail.com
> > Closes: https://lore.kernel.org/all/6a87853b.ae6ddae5.3da009.0023.GAE@google.com/
> > Fixes: 1583aa278f5f ("mm: mremap: unlink anon_vmas when mremap with MREMAP_DONTUNMAP success")
> > Cc: stable@vger.kernel.org
> > Signed-off-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
> > ---
> > mm/mremap.c | 22 +++++++++++++++++-----
> > 1 file changed, 17 insertions(+), 5 deletions(-)
> >
> > diff --git a/mm/mremap.c b/mm/mremap.c
> > index e8df5cdb0ac9..2b4b523a86b8 100644
> > --- a/mm/mremap.c
> > +++ b/mm/mremap.c
> > @@ -1331,18 +1331,30 @@ static void dontunmap_complete(struct vma_remap_struct *vrm,
> > {
> > unsigned long start = vrm->addr;
> > unsigned long end = vrm->addr + vrm->old_len;
> > - unsigned long old_start = vrm->vma->vm_start;
> > - unsigned long old_end = vrm->vma->vm_end;
> > + struct vm_area_struct *vma = vrm->vma;
> > + unsigned long old_start = vma->vm_start;
> > + unsigned long old_end = vma->vm_end;
> >
> > /* We always clear VMA_LOCKED[ONFAULT]_BIT on the old VMA. */
> > - vma_clear_flags_mask(vrm->vma, VMA_LOCKED_MASK);
> > + vma_clear_flags_mask(vma, VMA_LOCKED_MASK);
> >
> > /*
> > * anon_vma links of the old vma is no longer needed after its page
> > * table has been moved.
> > */
> > - if (new_vma != vrm->vma && start == old_start && end == old_end)
> > - unlink_anon_vmas(vrm->vma);
> > + if (new_vma != vma && start == old_start && end == old_end) {
> > + const pgoff_t pgoff_unfaulted = vma->vm_start >> PAGE_SHIFT;
> > +
> > + unlink_anon_vmas(vma);
> > + /*
> > + * The VMA is now unfaulted and it is an invariant that
> > + * unfaulted anonymous VMAs have page offset equal to
> > + * vma->vm_start >> PAGE_SHIFT.
> > + */
> > + vma_set_anon_pgoff(vma, pgoff_unfaulted);
> > + if (vma_is_anonymous(vma) && !vma->vm_file)
> > + vma_set_pgoff(vma, pgoff_unfaulted);
>
>
> Hi Lorenzo,
>
> I think the fix makes sense. One thing I wanted to make sure
> I understand correctly is the distinction between anon_pgoff
> and vm_pgoff here.
> Is the intention that anon_pgoff should be reset when the
> VMA becomes unfaulted, while vm_pgoff should only be reset
> for a truly anonymous VMA, since it may retain file-offset
> semantics for VMAs with a vm_file?
Yes.
This is to account for both MAP_PRIVATE file-backed and pure anon. But to
keep everything consistent (+ simple) we always update anon pgoff for
everything.
>
> Thanks,
> KunWu
>
> > + }
> >
> > /* Because we won't unmap we don't need to touch locked_vm. */
> > }
> >
> > ---
> > base-commit: efecab401cb15fd3bb9bc05990609acb6b267ff2
> > change-id: 20260824-fix-mremap-dontunmap-pgoff-a687134e995e
> >
> > Best regards,
> > --
> > Lorenzo Stoakes (ARM) <ljs@kernel.org>
> >
> >
>
--
Cheers, Lorenzo
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH] mm/mremap: reset unfaulted VMA page offset for MREMAP_DONTUNMAP
2026-08-25 7:55 [PATCH] mm/mremap: reset unfaulted VMA page offset for MREMAP_DONTUNMAP Lorenzo Stoakes (ARM)
2026-08-26 15:15 ` Kunwu Chan
@ 2026-08-27 2:26 ` Andrew Morton
2026-08-27 6:26 ` Kunwu Chan
2026-08-27 8:32 ` Vlastimil Babka (SUSE)
2 siblings, 1 reply; 7+ messages in thread
From: Andrew Morton @ 2026-08-27 2:26 UTC (permalink / raw)
To: Lorenzo Stoakes (ARM)
Cc: Liam R. Howlett, Vlastimil Babka, Jann Horn, Pedro Falcato,
Li Xinhai, linux-mm, linux-kernel, syzbot+f12658786a4153df5113,
stable
On Tue, 25 Aug 2026 08:55:26 +0100 "Lorenzo Stoakes (ARM)" <ljs@kernel.org> wrote:
> Uniquely an mremap() invocation using the MREMAP_DONTUNMAP flag can reset
> a faulted VMA into an unfaulted one.
>
> It does so after the page tables have been moved to the copied VMA with
> MREMAP_DONTUNMAP leaving the old VMA in place which is naturally unfaulted
> as the page tables it had are no longer present.
>
> However, in doing so, it violates the invariant that the anonymous page
> offset of an unfaulted VMA is vma->vm_start >> PAGE_SHIFT.
>
> This is because a VMA may have been faulted in, mremap()'d (causing a delta
> between its page offset and vma->vm_start >> PAGE_SHIFT), and then
> mremap()'d again with MREMAP_DONTUNMAP resulting in the unfaulting.
>
> This condition is a violation of a fundamental assumption in mm, but now
> also triggers an assert in assert_sane_pgoff() which explicitly checks for
> this condition.
>
> Correct it by resetting the VMA's page offset at the point of completing
> the MREMAP_DONTUNMAP operation.
Thanks. I'll park this in mm-new until mm.git is all merged up
(simplifying my life..)
Unrelatedly, Sashiko thinks we're messing up locked_vm accounting with
MREMAP_DONTUNMAP on a locked VMA.
https://sashiko.dev/#/patchset/20260825-fix-mremap-dontunmap-pgoff-v1-1-39a40b2c98b3@kernel.org
I had Sashiko write code to demonstrate this but am too lazy to test it
on a current kernel. If someone could oblige?
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <fcntl.h>
/* Read VMLck (in kB) from /proc/self/status */
static long get_vmlck_kb(void) {
FILE *f = fopen("/proc/self/status", "r");
if (!f) {
perror("fopen /proc/self/status");
return -1;
}
char line[256];
long vmlck = -1;
while (fgets(line, sizeof(line), f)) {
if (strncmp(line, "VMLck:", 6) == 0) {
sscanf(line + 6, "%ld", &vmlck);
break;
}
}
fclose(f);
return vmlck;
}
int main(void) {
size_t size = 4096 * 10; // 40 kB
long initial_vmlck, post_mlock, post_mremap, post_munmap;
initial_vmlck = get_vmlck_kb();
printf("[1] Initial VMLck: %ld kB\n", initial_vmlck);
/* 1. Allocate initial VMA */
void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED) {
perror("mmap initial");
return 1;
}
/* 2. Lock the VMA (increments mm->locked_vm) */
if (mlock(addr, size) != 0) {
perror("mlock");
return 1;
}
post_mlock = get_vmlck_kb();
printf("[2] Post-mlock VMLck: %ld kB (+%ld kB)\n",
post_mlock, post_mlock - initial_vmlck);
/* 3. mremap with MREMAP_DONTUNMAP
* move_vma() increments mm->locked_vm for the destination VMA,
* while dontunmap_complete() clears VMA_LOCKED_MASK on source VMA
* without decrementing mm->locked_vm.
*/
void *new_addr = mremap(addr, size, size,
MREMAP_MAYMOVE | MREMAP_DONTUNMAP, NULL);
if (new_addr == MAP_FAILED) {
perror("mremap MREMAP_DONTUNMAP");
return 1;
}
post_mremap = get_vmlck_kb();
printf("[3] Post-mremap VMLck: %ld kB (+%ld kB from initial)\n",
post_mremap, post_mremap - initial_vmlck);
/* 4. Unmap source VMA
* Since VMA_LOCKED_BIT was cleared on source VMA,
* munmap fails to decrement mm->locked_vm for this region.
*/
munmap(addr, size);
post_munmap = get_vmlck_kb();
printf("[4] Post-munmap source: %ld kB\n", post_munmap);
/* 5. Clean up destination VMA */
munmap(new_addr, size);
long final_vmlck = get_vmlck_kb();
printf("[5] Final VMLck: %ld kB\n", final_vmlck);
/* Evaluation */
printf("\n--- Result ---\n");
if (final_vmlck > initial_vmlck) {
printf("BUG DEMONSTRATED: Leaked %ld kB in mm->locked_vm counter.\n",
final_vmlck - initial_vmlck);
} else {
printf("NO LEAK: mm->locked_vm returned to initial state.\n");
}
return 0;
}
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH] mm/mremap: reset unfaulted VMA page offset for MREMAP_DONTUNMAP
2026-08-27 2:26 ` Andrew Morton
@ 2026-08-27 6:26 ` Kunwu Chan
0 siblings, 0 replies; 7+ messages in thread
From: Kunwu Chan @ 2026-08-27 6:26 UTC (permalink / raw)
Cc: Kunwu Chan, Lorenzo Stoakes (ARM), Liam R. Howlett,
Vlastimil Babka, Jann Horn, Pedro Falcato, Li Xinhai, linux-mm,
linux-kernel, syzbot+f12658786a4153df5113, stable
On Wed, 26 Aug 2026 19:26:42 -0700 Andrew Morton <akpm@linux-foundation.org> wrote:
> On Tue, 25 Aug 2026 08:55:26 +0100 "Lorenzo Stoakes (ARM)" <ljs@kernel.org> wrote:
>
> > Uniquely an mremap() invocation using the MREMAP_DONTUNMAP flag can reset
> > a faulted VMA into an unfaulted one.
> >
> > It does so after the page tables have been moved to the copied VMA with
> > MREMAP_DONTUNMAP leaving the old VMA in place which is naturally unfaulted
> > as the page tables it had are no longer present.
> >
> > However, in doing so, it violates the invariant that the anonymous page
> > offset of an unfaulted VMA is vma->vm_start >> PAGE_SHIFT.
> >
> > This is because a VMA may have been faulted in, mremap()'d (causing a delta
> > between its page offset and vma->vm_start >> PAGE_SHIFT), and then
> > mremap()'d again with MREMAP_DONTUNMAP resulting in the unfaulting.
> >
> > This condition is a violation of a fundamental assumption in mm, but now
> > also triggers an assert in assert_sane_pgoff() which explicitly checks for
> > this condition.
> >
> > Correct it by resetting the VMA's page offset at the point of completing
> > the MREMAP_DONTUNMAP operation.
>
> Thanks. I'll park this in mm-new until mm.git is all merged up
> (simplifying my life..)
>
>
> Unrelatedly, Sashiko thinks we're messing up locked_vm accounting with
> MREMAP_DONTUNMAP on a locked VMA.
> https://sashiko.dev/#/patchset/20260825-fix-mremap-dontunmap-pgoff-v1-1-39a40b2c98b3@kernel.org
>
>
> I had Sashiko write code to demonstrate this but am too lazy to test it
> on a current kernel. If someone could oblige?
Hi Andrew,
I'll test the reproducer on a current kernel and check the mm->locked_vm
accounting before and after MREMAP_DONTUNMAP.
Thanks,
Kunwu
>
>
> #define _GNU_SOURCE
> #include <stdio.h>
> #include <stdlib.h>
> #include <string.h>
> #include <unistd.h>
> #include <sys/mman.h>
> #include <fcntl.h>
>
> /* Read VMLck (in kB) from /proc/self/status */
> static long get_vmlck_kb(void) {
> FILE *f = fopen("/proc/self/status", "r");
> if (!f) {
> perror("fopen /proc/self/status");
> return -1;
> }
>
> char line[256];
> long vmlck = -1;
> while (fgets(line, sizeof(line), f)) {
> if (strncmp(line, "VMLck:", 6) == 0) {
> sscanf(line + 6, "%ld", &vmlck);
> break;
> }
> }
> fclose(f);
> return vmlck;
> }
>
> int main(void) {
> size_t size = 4096 * 10; // 40 kB
> long initial_vmlck, post_mlock, post_mremap, post_munmap;
>
> initial_vmlck = get_vmlck_kb();
> printf("[1] Initial VMLck: %ld kB\n", initial_vmlck);
>
> /* 1. Allocate initial VMA */
> void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
> MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> if (addr == MAP_FAILED) {
> perror("mmap initial");
> return 1;
> }
>
> /* 2. Lock the VMA (increments mm->locked_vm) */
> if (mlock(addr, size) != 0) {
> perror("mlock");
> return 1;
> }
> post_mlock = get_vmlck_kb();
> printf("[2] Post-mlock VMLck: %ld kB (+%ld kB)\n",
> post_mlock, post_mlock - initial_vmlck);
>
> /* 3. mremap with MREMAP_DONTUNMAP
> * move_vma() increments mm->locked_vm for the destination VMA,
> * while dontunmap_complete() clears VMA_LOCKED_MASK on source VMA
> * without decrementing mm->locked_vm.
> */
> void *new_addr = mremap(addr, size, size,
> MREMAP_MAYMOVE | MREMAP_DONTUNMAP, NULL);
> if (new_addr == MAP_FAILED) {
> perror("mremap MREMAP_DONTUNMAP");
> return 1;
> }
> post_mremap = get_vmlck_kb();
> printf("[3] Post-mremap VMLck: %ld kB (+%ld kB from initial)\n",
> post_mremap, post_mremap - initial_vmlck);
>
> /* 4. Unmap source VMA
> * Since VMA_LOCKED_BIT was cleared on source VMA,
> * munmap fails to decrement mm->locked_vm for this region.
> */
> munmap(addr, size);
> post_munmap = get_vmlck_kb();
> printf("[4] Post-munmap source: %ld kB\n", post_munmap);
>
> /* 5. Clean up destination VMA */
> munmap(new_addr, size);
> long final_vmlck = get_vmlck_kb();
> printf("[5] Final VMLck: %ld kB\n", final_vmlck);
>
> /* Evaluation */
> printf("\n--- Result ---\n");
> if (final_vmlck > initial_vmlck) {
> printf("BUG DEMONSTRATED: Leaked %ld kB in mm->locked_vm counter.\n",
> final_vmlck - initial_vmlck);
> } else {
> printf("NO LEAK: mm->locked_vm returned to initial state.\n");
> }
>
> return 0;
> }
>
>
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH] mm/mremap: reset unfaulted VMA page offset for MREMAP_DONTUNMAP
2026-08-25 7:55 [PATCH] mm/mremap: reset unfaulted VMA page offset for MREMAP_DONTUNMAP Lorenzo Stoakes (ARM)
2026-08-26 15:15 ` Kunwu Chan
2026-08-27 2:26 ` Andrew Morton
@ 2026-08-27 8:32 ` Vlastimil Babka (SUSE)
2026-08-27 8:36 ` Lorenzo Stoakes (ARM)
2 siblings, 1 reply; 7+ messages in thread
From: Vlastimil Babka (SUSE) @ 2026-08-27 8:32 UTC (permalink / raw)
To: Lorenzo Stoakes (ARM), Andrew Morton, Liam R. Howlett, Jann Horn,
Pedro Falcato, Li Xinhai
Cc: linux-mm, linux-kernel, syzbot+f12658786a4153df5113, stable
On 8/25/26 9:55 AM, Lorenzo Stoakes (ARM) wrote:
> Uniquely an mremap() invocation using the MREMAP_DONTUNMAP flag can reset
> a faulted VMA into an unfaulted one.
>
> It does so after the page tables have been moved to the copied VMA with
> MREMAP_DONTUNMAP leaving the old VMA in place which is naturally unfaulted
> as the page tables it had are no longer present.
>
> However, in doing so, it violates the invariant that the anonymous page
> offset of an unfaulted VMA is vma->vm_start >> PAGE_SHIFT.
>
> This is because a VMA may have been faulted in, mremap()'d (causing a delta
> between its page offset and vma->vm_start >> PAGE_SHIFT), and then
> mremap()'d again with MREMAP_DONTUNMAP resulting in the unfaulting.
>
> This condition is a violation of a fundamental assumption in mm, but now
> also triggers an assert in assert_sane_pgoff() which explicitly checks for
> this condition.
Oof. So what's the worst thing that could happen before the assert was
added? We'd use the "unfaulted" state to allow a merge, but the wrong
pgoff could mess up the result of the merge somehow?
> Correct it by resetting the VMA's page offset at the point of completing
> the MREMAP_DONTUNMAP operation.
>
> Reported-by: syzbot+f12658786a4153df5113@syzkaller.appspotmail.com
> Closes: https://lore.kernel.org/all/6a87853b.ae6ddae5.3da009.0023.GAE@google.com/
> Fixes: 1583aa278f5f ("mm: mremap: unlink anon_vmas when mremap with MREMAP_DONTUNMAP success")
> Cc: stable@vger.kernel.org
> Signed-off-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
Acked-by: Vlastimil Babka (SUSE) <vbabka@kernel.org>
> ---
> mm/mremap.c | 22 +++++++++++++++++-----
> 1 file changed, 17 insertions(+), 5 deletions(-)
>
> diff --git a/mm/mremap.c b/mm/mremap.c
> index e8df5cdb0ac9..2b4b523a86b8 100644
> --- a/mm/mremap.c
> +++ b/mm/mremap.c
> @@ -1331,18 +1331,30 @@ static void dontunmap_complete(struct vma_remap_struct *vrm,
> {
> unsigned long start = vrm->addr;
> unsigned long end = vrm->addr + vrm->old_len;
> - unsigned long old_start = vrm->vma->vm_start;
> - unsigned long old_end = vrm->vma->vm_end;
> + struct vm_area_struct *vma = vrm->vma;
> + unsigned long old_start = vma->vm_start;
> + unsigned long old_end = vma->vm_end;
>
> /* We always clear VMA_LOCKED[ONFAULT]_BIT on the old VMA. */
> - vma_clear_flags_mask(vrm->vma, VMA_LOCKED_MASK);
> + vma_clear_flags_mask(vma, VMA_LOCKED_MASK);
>
> /*
> * anon_vma links of the old vma is no longer needed after its page
> * table has been moved.
> */
> - if (new_vma != vrm->vma && start == old_start && end == old_end)
> - unlink_anon_vmas(vrm->vma);
> + if (new_vma != vma && start == old_start && end == old_end) {
> + const pgoff_t pgoff_unfaulted = vma->vm_start >> PAGE_SHIFT;
> +
> + unlink_anon_vmas(vma);
> + /*
> + * The VMA is now unfaulted and it is an invariant that
> + * unfaulted anonymous VMAs have page offset equal to
> + * vma->vm_start >> PAGE_SHIFT.
> + */
> + vma_set_anon_pgoff(vma, pgoff_unfaulted);
> + if (vma_is_anonymous(vma) && !vma->vm_file)
> + vma_set_pgoff(vma, pgoff_unfaulted);
> + }
>
> /* Because we won't unmap we don't need to touch locked_vm. */
> }
>
> ---
> base-commit: efecab401cb15fd3bb9bc05990609acb6b267ff2
> change-id: 20260824-fix-mremap-dontunmap-pgoff-a687134e995e
>
> Best regards,
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH] mm/mremap: reset unfaulted VMA page offset for MREMAP_DONTUNMAP
2026-08-27 8:32 ` Vlastimil Babka (SUSE)
@ 2026-08-27 8:36 ` Lorenzo Stoakes (ARM)
0 siblings, 0 replies; 7+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-08-27 8:36 UTC (permalink / raw)
To: Vlastimil Babka (SUSE)
Cc: Andrew Morton, Liam R. Howlett, Jann Horn, Pedro Falcato,
Li Xinhai, linux-mm, linux-kernel, syzbot+f12658786a4153df5113,
stable
On Thu, Aug 27, 2026 at 10:32:15AM +0200, Vlastimil Babka (SUSE) wrote:
> On 8/25/26 9:55 AM, Lorenzo Stoakes (ARM) wrote:
> > Uniquely an mremap() invocation using the MREMAP_DONTUNMAP flag can reset
> > a faulted VMA into an unfaulted one.
> >
> > It does so after the page tables have been moved to the copied VMA with
> > MREMAP_DONTUNMAP leaving the old VMA in place which is naturally unfaulted
> > as the page tables it had are no longer present.
> >
> > However, in doing so, it violates the invariant that the anonymous page
> > offset of an unfaulted VMA is vma->vm_start >> PAGE_SHIFT.
> >
> > This is because a VMA may have been faulted in, mremap()'d (causing a delta
> > between its page offset and vma->vm_start >> PAGE_SHIFT), and then
> > mremap()'d again with MREMAP_DONTUNMAP resulting in the unfaulting.
> >
> > This condition is a violation of a fundamental assumption in mm, but now
> > also triggers an assert in assert_sane_pgoff() which explicitly checks for
> > this condition.
>
> Oof. So what's the worst thing that could happen before the assert was
> added? We'd use the "unfaulted" state to allow a merge, but the wrong
> pgoff could mess up the result of the merge somehow?
Yep you'd just get merging not working. It's a bit of a unique set of
circumstances so it's not a huge impact, but it's an edge case that'd break
scalable CoW assumptions that I want to use to avoid having to track remaps
so it's a good one to find :)
Definitely incorrect however even if low impact in the past.
>
> > Correct it by resetting the VMA's page offset at the point of completing
> > the MREMAP_DONTUNMAP operation.
> >
> > Reported-by: syzbot+f12658786a4153df5113@syzkaller.appspotmail.com
> > Closes: https://lore.kernel.org/all/6a87853b.ae6ddae5.3da009.0023.GAE@google.com/
> > Fixes: 1583aa278f5f ("mm: mremap: unlink anon_vmas when mremap with MREMAP_DONTUNMAP success")
> > Cc: stable@vger.kernel.org
> > Signed-off-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
>
> Acked-by: Vlastimil Babka (SUSE) <vbabka@kernel.org>
Thanks!
>
> > ---
> > mm/mremap.c | 22 +++++++++++++++++-----
> > 1 file changed, 17 insertions(+), 5 deletions(-)
> >
> > diff --git a/mm/mremap.c b/mm/mremap.c
> > index e8df5cdb0ac9..2b4b523a86b8 100644
> > --- a/mm/mremap.c
> > +++ b/mm/mremap.c
> > @@ -1331,18 +1331,30 @@ static void dontunmap_complete(struct vma_remap_struct *vrm,
> > {
> > unsigned long start = vrm->addr;
> > unsigned long end = vrm->addr + vrm->old_len;
> > - unsigned long old_start = vrm->vma->vm_start;
> > - unsigned long old_end = vrm->vma->vm_end;
> > + struct vm_area_struct *vma = vrm->vma;
> > + unsigned long old_start = vma->vm_start;
> > + unsigned long old_end = vma->vm_end;
> >
> > /* We always clear VMA_LOCKED[ONFAULT]_BIT on the old VMA. */
> > - vma_clear_flags_mask(vrm->vma, VMA_LOCKED_MASK);
> > + vma_clear_flags_mask(vma, VMA_LOCKED_MASK);
> >
> > /*
> > * anon_vma links of the old vma is no longer needed after its page
> > * table has been moved.
> > */
> > - if (new_vma != vrm->vma && start == old_start && end == old_end)
> > - unlink_anon_vmas(vrm->vma);
> > + if (new_vma != vma && start == old_start && end == old_end) {
> > + const pgoff_t pgoff_unfaulted = vma->vm_start >> PAGE_SHIFT;
> > +
> > + unlink_anon_vmas(vma);
> > + /*
> > + * The VMA is now unfaulted and it is an invariant that
> > + * unfaulted anonymous VMAs have page offset equal to
> > + * vma->vm_start >> PAGE_SHIFT.
> > + */
> > + vma_set_anon_pgoff(vma, pgoff_unfaulted);
> > + if (vma_is_anonymous(vma) && !vma->vm_file)
> > + vma_set_pgoff(vma, pgoff_unfaulted);
> > + }
> >
> > /* Because we won't unmap we don't need to touch locked_vm. */
> > }
> >
> > ---
> > base-commit: efecab401cb15fd3bb9bc05990609acb6b267ff2
> > change-id: 20260824-fix-mremap-dontunmap-pgoff-a687134e995e
> >
> > Best regards,
>
--
Cheers, Lorenzo
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-08-27 8:37 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-25 7:55 [PATCH] mm/mremap: reset unfaulted VMA page offset for MREMAP_DONTUNMAP Lorenzo Stoakes (ARM)
2026-08-26 15:15 ` Kunwu Chan
2026-08-26 15:27 ` Lorenzo Stoakes (ARM)
2026-08-27 2:26 ` Andrew Morton
2026-08-27 6:26 ` Kunwu Chan
2026-08-27 8:32 ` Vlastimil Babka (SUSE)
2026-08-27 8:36 ` Lorenzo Stoakes (ARM)
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox