* 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