mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH] mm/ksm: avoid anon_vma UAF during page merging
@ 2026-10-31 18:22 Zhao Gongyi
  2026-11-01 18:11 ` David Hildenbrand (Arm)
  2026-11-02 10:18 ` xu.xin
  0 siblings, 2 replies; 7+ messages in thread
From: Zhao Gongyi @ 2026-10-31 18:22 UTC (permalink / raw)
  To: akpm, david
  Cc: xu.xin, chengming.zhou, aarcange, riel, linux-mm, linux-kernel,
	Zhao Gongyi

KASAN reports a use-after-free in page_address_in_vma() while ksmd
merges pages. ksmd keeps a folio reference across an mmap_lock drop,
during which mremap() can remove its last mapping and free its
anon_vma. page_address_in_vma() then dereferences the stale pointer
from folio->mapping.

KSM needs only a candidate address. write_protect_page() checks that
the PTE maps the folio, and replace_page() verifies the saved PTE
before replacing it. Compute the candidate from the folio index and
VMA instead of dereferencing the folio-side anon_vma. Reject KSM
folios because their page index is not meaningful.

Fixes: 21d0d443cdc1 ("rmap: resurrect page_address_in_vma anon_vma check")
Signed-off-by: Zhao Gongyi <zhaogongyi@bytedance.com>
---
Testing:
  Unpatched mm-new: fixed-pool KSM/mremap reproduced the KASAN UAF.
  Patched mm-new: the same workload ran for 12 minutes without KASAN,
  BUG or WARNING; fork/COW KSM smoke passed.

 mm/ksm.c | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/mm/ksm.c b/mm/ksm.c
index d6f736a..f8af5ee 100644
--- a/mm/ksm.c
+++ b/mm/ksm.c
@@ -1307,7 +1307,7 @@ static int write_protect_page(struct vm_area_struct *vma, struct folio *folio,
 	if (WARN_ON_ONCE(folio_test_large(folio)))
 		return err;
 
-	pvmw.address = page_address_in_vma(folio, folio_page(folio, 0), vma);
+	pvmw.address = vma_anon_address(vma, folio_pgoff(folio), 1);
 	if (pvmw.address == -EFAULT)
 		goto out;
 
@@ -1408,7 +1408,7 @@ static int replace_page(struct vm_area_struct *vma, struct page *page,
 	int err = -EFAULT;
 	struct mmu_notifier_range range;
 
-	addr = page_address_in_vma(folio, page, vma);
+	addr = vma_anon_address(vma, page_pgoff(folio, page), 1);
 	if (addr == -EFAULT)
 		goto out;
 
@@ -1515,7 +1515,7 @@ static int try_to_merge_one_page(struct vm_area_struct *vma,
 	if (page == kpage)			/* ksm page forked */
 		return 0;
 
-	if (!folio_test_anon(folio))
+	if (!folio_test_anon(folio) || folio_test_ksm(folio))
 		goto out;
 
 	/*
base-commit: 243e8cdab64129d6207c398f8cf5fbb1962ff7e1
-- 
2.39.5 (Apple Git-154)

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH] mm/ksm: avoid anon_vma UAF during page merging
  2026-10-31 18:22 [PATCH] mm/ksm: avoid anon_vma UAF during page merging Zhao Gongyi
@ 2026-11-01 18:11 ` David Hildenbrand (Arm)
  2026-11-02  3:32   ` Zhao Gongyi
  2026-11-02 11:23   ` Lorenzo Stoakes (ARM)
  2026-11-02 10:18 ` xu.xin
  1 sibling, 2 replies; 7+ messages in thread
From: David Hildenbrand (Arm) @ 2026-11-01 18:11 UTC (permalink / raw)
  To: Zhao Gongyi, akpm
  Cc: xu.xin, chengming.zhou, aarcange, riel, linux-mm, linux-kernel,
	Lorenzo Stoakes (Arm)

On 10/31/26 19:22, Zhao Gongyi wrote:

Hi,

thanks for the report! CCing Lorenzo.

> KASAN reports a use-after-free in page_address_in_vma() while ksmd
> merges pages. ksmd keeps a folio reference across an mmap_lock drop,
> during which mremap() can remove its last mapping and free its
> anon_vma. page_address_in_vma() then dereferences the stale pointer
> from folio->mapping.

Do you have a link to the report or the actual KASAN report?

> 
> KSM needs only a candidate address. write_protect_page() checks that
> the PTE maps the folio, and replace_page() verifies the saved PTE
> before replacing it. Compute the candidate from the folio index and
> VMA instead of dereferencing the folio-side anon_vma. Reject KSM
> folios because their page index is not meaningful.
> 
> Fixes: 21d0d443cdc1 ("rmap: resurrect page_address_in_vma anon_vma check")
> Signed-off-by: Zhao Gongyi <zhaogongyi@bytedance.com>
> ---
> Testing:
>   Unpatched mm-new: fixed-pool KSM/mremap reproduced the KASAN UAF.
>   Patched mm-new: the same workload ran for 12 minutes without KASAN,
>   BUG or WARNING; fork/COW KSM smoke passed.
> 
>  mm/ksm.c | 6 +++---
>  1 file changed, 3 insertions(+), 3 deletions(-)
> 
> diff --git a/mm/ksm.c b/mm/ksm.c
> index d6f736a..f8af5ee 100644
> --- a/mm/ksm.c
> +++ b/mm/ksm.c
> @@ -1307,7 +1307,7 @@ static int write_protect_page(struct vm_area_struct *vma, struct folio *folio,
>  	if (WARN_ON_ONCE(folio_test_large(folio)))
>  		return err;
>  
> -	pvmw.address = page_address_in_vma(folio, folio_page(folio, 0), vma);
> +	pvmw.address = vma_anon_address(vma, folio_pgoff(folio), 1);
>  	if (pvmw.address == -EFAULT)
>  		goto out;
>  
> @@ -1408,7 +1408,7 @@ static int replace_page(struct vm_area_struct *vma, struct page *page,
>  	int err = -EFAULT;
>  	struct mmu_notifier_range range;
>  
> -	addr = page_address_in_vma(folio, page, vma);
> +	addr = vma_anon_address(vma, page_pgoff(folio, page), 1);
>  	if (addr == -EFAULT)
>  		goto out;
>  
> @@ -1515,7 +1515,7 @@ static int try_to_merge_one_page(struct vm_area_struct *vma,
>  	if (page == kpage)			/* ksm page forked */
>  		return 0;
>  
> -	if (!folio_test_anon(folio))
> +	if (!folio_test_anon(folio) || folio_test_ksm(folio))
>  		goto out;
>  
>  	/*
> base-commit: 243e8cdab64129d6207c398f8cf5fbb1962ff7e1


-- 
Cheers,

David

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH] mm/ksm: avoid anon_vma UAF during page merging
  2026-11-01 18:11 ` David Hildenbrand (Arm)
@ 2026-11-02  3:32   ` Zhao Gongyi
  2026-11-02 11:23   ` Lorenzo Stoakes (ARM)
  1 sibling, 0 replies; 7+ messages in thread
From: Zhao Gongyi @ 2026-11-02  3:32 UTC (permalink / raw)
  To: David Hildenbrand (Arm), akpm
  Cc: xu.xin, chengming.zhou, aarcange, linux-mm, linux-kernel,
	Lorenzo Stoakes (Arm),
	riel, ljs

Hi David,

I don't have a public link for this report, but the KASAN splat is 
below, followed by the standalone reproducer I used.

The reproducer needs CONFIG_KSM and writes the KSM sysfs knobs, so I ran 
it as root in a throwaway KASAN VM. One invocation I used was:

	./ksm_mremap_dontunmap_repro 4 2048 1 100000 64

It keeps mergeable anonymous pages at stable addresses while repeatedly 
using mremap(MREMAP_FIXED | MREMAP_DONTUNMAP) over the destination VMAs, 
so ksmd can race with removal of the last anon mapping.

KASAN report:

==================================================================
BUG: KASAN: slab-use-after-free in page_address_in_vma+0x104/0x32c
Read of size 8 at addr ffff0000d0c24c80 by task ksmd/34

Call trace:
  show_stack+0x2c/0x3c (C)
  dump_stack_lvl+0x94/0xc0
  print_report+0x174/0x71c
  kasan_report+0xc8/0x124
  __asan_report_load8_noabort+0x20/0x2c
  page_address_in_vma+0x104/0x32c
  try_to_merge_one_page+0x304/0x14b0
  try_to_merge_with_ksm_page+0x138/0x2e0
  ksm_scan_thread+0x2f70/0x4ae8
  kthread+0x310/0x400
  ret_from_fork+0x10/0x20

Allocated by task 465110:
  kasan_save_track+0x40/0x78
  kasan_save_alloc_info+0x44/0x54
  __kasan_slab_alloc+0x58/0x70
  kmem_cache_alloc_noprof+0x144/0x39c
  __anon_vma_prepare+0xd8/0x370
  handle_mm_fault+0x3a74/0x43cc
  __get_user_pages+0x1228/0x1d50
  __mm_populate+0x284/0x388
  __arm64_sys_mlockall+0x2fc/0x378
  invoke_syscall+0x8c/0x1f8
  el0_svc_common+0xe8/0x23c
  do_el0_svc+0x48/0x58
  el0_svc+0x3c/0x98
  el0t_64_sync_handler+0x84/0x12c
  el0t_64_sync+0x198/0x19c

Freed by task 461050:
  kasan_save_track+0x40/0x78
  kasan_save_free_info+0x58/0x70
  __kasan_slab_free+0x5c/0x8c
  slab_free_after_rcu_debug+0xa8/0x1d8
  rcu_core+0x6bc/0xd8c
  rcu_core_si+0x10/0x1c
  handle_softirqs+0x1d4/0x55c
  __do_softirq+0x14/0x20

Last potentially related work creation:
  kasan_save_stack+0x40/0x6c
  kasan_record_aux_stack+0x90/0xa4
  kmem_cache_free+0x2f4/0x3b0
  __put_anon_vma+0xcc/0x1f8
  unlink_anon_vmas+0x350/0x444
  free_pgtables+0x254/0x5c0
  unmap_region+0x1e8/0x23c
  vms_complete_munmap_vmas+0x300/0x7e8
  do_vmi_align_munmap+0x2b8/0x360
  do_vmi_munmap+0x190/0x1dc
  do_munmap+0x7c/0xb0
  mremap_to+0x210/0x5dc
  __arm64_sys_mremap+0x850/0xc70
  invoke_syscall+0x8c/0x1f8
  el0_svc_common+0xe8/0x23c
  do_el0_svc+0x48/0x58
  el0_svc+0x3c/0x98
  el0t_64_sync_handler+0x84/0x12c
  el0t_64_sync+0x198/0x19c


C reproducer:

// SPDX-License-Identifier: GPL-2.0
// Fixed-address KSM + MREMAP_DONTUNMAP stressor for page_address_in_vma().
#define _GNU_SOURCE

#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <unistd.h>

#ifndef MADV_MERGEABLE
#define MADV_MERGEABLE 12
#endif
#ifndef MREMAP_MAYMOVE
#define MREMAP_MAYMOVE 1
#endif
#ifndef MREMAP_FIXED
#define MREMAP_FIXED 2
#endif
#ifndef MREMAP_DONTUNMAP
#define MREMAP_DONTUNMAP 4
#endif

static size_t page_size;
static volatile sig_atomic_t stop;

static void on_signal(int sig)
{
	(void)sig;
	stop = 1;
}

static void write_file(const char *path, const char *val)
{
	int fd = open(path, O_WRONLY | O_CLOEXEC);

	if (fd < 0) {
		fprintf(stderr, "warn: open %s: %s\n", path, strerror(errno));
		return;
	}
	if (write(fd, val, strlen(val)) < 0)
		fprintf(stderr, "warn: write %s: %s\n", path, strerror(errno));
	close(fd);
}

static void tune_ksm(void)
{
	write_file("/sys/kernel/mm/ksm/run", "1\n");
	write_file("/sys/kernel/mm/ksm/pages_to_scan", "200000\n");
	write_file("/sys/kernel/mm/ksm/sleep_millisecs", "0\n");
}

static void fill_pattern(char *p, size_t len)
{
	size_t off;

	for (off = 0; off < len; off += page_size) {
		memset(p + off, 0x5a, page_size);
		memcpy(p + off, "ksm-fixed-pool", 14);
	}
}

static void *new_mergeable_area(size_t len)
{
	void *p;

	p = mmap(NULL, len, PROT_READ | PROT_WRITE,
		 MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
	if (p == MAP_FAILED)
		return MAP_FAILED;
	if (madvise(p, len, MADV_MERGEABLE) < 0) {
		fprintf(stderr, "madvise(MADV_MERGEABLE): %s\n", strerror(errno));
		munmap(p, len);
		return MAP_FAILED;
	}
	fill_pattern(p, len);
	return p;
}

static void worker_loop(int id, int slots, int pages, int expose_us, int 
seed_mb)
{
	size_t len = (size_t)pages * page_size;
	void **src = calloc((size_t)slots, sizeof(*src));
	void **dst = calloc((size_t)slots, sizeof(*dst));
	void *seed = NULL;
	unsigned long iter = 0;
	int i;

	if (!src || !dst)
		_exit(1);

	if (seed_mb > 0) {
		seed = new_mergeable_area((size_t)seed_mb << 20);
		fprintf(stderr, "worker%d seed=%p seed_mb=%d\n", id, seed, seed_mb);
	}

	for (i = 0; i < slots; i++) {
		src[i] = new_mergeable_area(len);
		dst[i] = new_mergeable_area(len);
		if (src[i] == MAP_FAILED || dst[i] == MAP_FAILED) {
			fprintf(stderr, "worker%d initial mmap failed at slot %d\n", id, i);
			_exit(1);
		}
	}

	while (!stop) {
		/*
		 * Let ksmd observe stable virtual addresses and cache rmap_items.
		 * It may return holding a folio ref after dropping mmap_lock while
		 * this process proceeds to replace the destination mapping below.
		 */
		if (expose_us > 0)
			usleep(expose_us);

		for (i = 0; i < slots && !stop; i++) {
			void *ret;

			ret = (void *)syscall(SYS_mremap, src[i], len, len,
					      MREMAP_MAYMOVE | MREMAP_FIXED |
						      MREMAP_DONTUNMAP,
					      dst[i]);
			if (ret == MAP_FAILED) {
				if (errno == EINVAL)
					fprintf(stderr, "mremap DONTUNMAP EINVAL\n");
				continue;
			}

			/*
			 * Source VMA remains in place after DONTUNMAP, but its PTEs
			 * were zapped. Fault it back in so the next cycle can again
			 * replace the same destination address.
			 */
			fill_pattern(src[i], len);
			(void)madvise(src[i], len, MADV_MERGEABLE);
			(void)madvise(dst[i], len, MADV_MERGEABLE);
		}

		if (id == 0 && (++iter & 0x3f) == 0)
			fprintf(stderr, "iter=%lu slots=%d pages=%d expose_us=%d\n",
				iter, slots, pages, expose_us);
		else
			iter++;
	}

	(void)seed;
}

int main(int argc, char **argv)
{
	int workers = argc > 1 ? atoi(argv[1]) : 4;
	int slots = argc > 2 ? atoi(argv[2]) : 2048;
	int pages = argc > 3 ? atoi(argv[3]) : 1;
	int expose_us = argc > 4 ? atoi(argv[4]) : 100000;
	int seed_mb = argc > 5 ? atoi(argv[5]) : 64;
	int i;

	page_size = (size_t)sysconf(_SC_PAGESIZE);
	if (workers <= 0)
		workers = 1;
	if (slots <= 0)
		slots = 1;
	if (pages <= 0)
		pages = 1;

	signal(SIGINT, on_signal);
	signal(SIGTERM, on_signal);
	tune_ksm();

	fprintf(stderr, "workers=%d slots=%d pages=%d expose_us=%d seed_mb=%d\n",
		workers, slots, pages, expose_us, seed_mb);

	for (i = 0; i < workers; i++) {
		pid_t pid = fork();

		if (pid == 0)
			worker_loop(i, slots, pages, expose_us, seed_mb);
		if (pid < 0) {
			perror("fork");
			stop = 1;
			break;
		}
	}

	while (!stop)
		sleep(1);

	kill(0, SIGTERM);
	while (wait(NULL) > 0)
		;
	return 0;
}

Thanks,
Gongyi

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH] mm/ksm: avoid anon_vma UAF during page merging
  2026-10-31 18:22 [PATCH] mm/ksm: avoid anon_vma UAF during page merging Zhao Gongyi
  2026-11-01 18:11 ` David Hildenbrand (Arm)
@ 2026-11-02 10:18 ` xu.xin
  2026-11-02 11:14   ` Zhao Gongyi
  2026-11-02 12:14   ` Lorenzo Stoakes (ARM)
  1 sibling, 2 replies; 7+ messages in thread
From: xu.xin @ 2026-11-02 10:18 UTC (permalink / raw)
  To: zhaogongyi
  Cc: aarcange, akpm, chengming.zhou, david, linux-kernel, linux-mm,
	riel, xu.xin

> KASAN reports a use-after-free in page_address_in_vma() while ksmd
> merges pages. ksmd keeps a folio reference across an mmap_lock drop,
> during which mremap() can remove its last mapping and free its
> anon_vma. page_address_in_vma() then dereferences the stale pointer
> from folio->mapping.
> 
> KSM needs only a candidate address. write_protect_page() checks that
> the PTE maps the folio, and replace_page() verifies the saved PTE
> before replacing it. Compute the candidate from the folio index and
> VMA instead of dereferencing the folio-side anon_vma. Reject KSM
> folios because their page index is not meaningful.
> 
> Fixes: 21d0d443cdc1 ("rmap: resurrect page_address_in_vma anon_vma check")
> Signed-off-by: Zhao Gongyi <zhaogongyi@bytedance.com>
> ---
> Testing:
>   Unpatched mm-new: fixed-pool KSM/mremap reproduced the KASAN UAF.
>   Patched mm-new: the same workload ran for 12 minutes without KASAN,
>   BUG or WARNING; fork/COW KSM smoke passed.

Thanks for the report.

I’d like to ask whether this issue is easy to reproduce on 6.18 since
I suspect the accuracy of the “fixes” tag in the commit description.

Besides, adding an extra folio_test_ksm() check is unneeded, see reasons below. 

> 
>  mm/ksm.c | 6 +++---
>  1 file changed, 3 insertions(+), 3 deletions(-)
> 
> diff --git a/mm/ksm.c b/mm/ksm.c
> index d6f736a..f8af5ee 100644
> --- a/mm/ksm.c
> +++ b/mm/ksm.c
> @@ -1307,7 +1307,7 @@ static int write_protect_page(struct vm_area_struct *vma, struct folio *folio,
>  	if (WARN_ON_ONCE(folio_test_large(folio)))
>  		return err;
>  
> -	pvmw.address = page_address_in_vma(folio, folio_page(folio, 0), vma);
> +	pvmw.address = vma_anon_address(vma, folio_pgoff(folio), 1);
>  	if (pvmw.address == -EFAULT)
>  		goto out;
>  
> @@ -1408,7 +1408,7 @@ static int replace_page(struct vm_area_struct *vma, struct page *page,
>  	int err = -EFAULT;
>  	struct mmu_notifier_range range;
>  
> -	addr = page_address_in_vma(folio, page, vma);
> +	addr = vma_anon_address(vma, page_pgoff(folio, page), 1);
>  	if (addr == -EFAULT)
>  		goto out;
>  
> @@ -1515,7 +1515,7 @@ static int try_to_merge_one_page(struct vm_area_struct *vma,
>  	if (page == kpage)			/* ksm page forked */
>  		return 0;
>  
> -	if (!folio_test_anon(folio))
> +	if (!folio_test_anon(folio) || folio_test_ksm(folio))
>  		goto out;

The check folio_test_ksm(folio)) in try_to_merge_one_page is unnecessary, because before
try_to_merge_one_page() is called, cmp_and_merge_page() invokes folio_stable_node(), which already checks
whether this is a KSM page. Only KSM fork pages are allowed to pass through; even if they do pass, they
will ultimately be intercepted by the if (page == kpage) check in try_to_merge_one_page.




^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH] mm/ksm: avoid anon_vma UAF during page merging
  2026-11-02 10:18 ` xu.xin
@ 2026-11-02 11:14   ` Zhao Gongyi
  2026-11-02 12:14   ` Lorenzo Stoakes (ARM)
  1 sibling, 0 replies; 7+ messages in thread
From: Zhao Gongyi @ 2026-11-02 11:14 UTC (permalink / raw)
  To: xu.xin
  Cc: aarcange, akpm, chengming.zhou, david, linux-kernel, linux-mm, riel

Hi Xu,

Thanks for the review.

On 11/2/26 6:18 PM, xu.xin@linux.dev wrote:
> 
> I’d like to ask whether this issue is easy to reproduce on 6.18 since
> I suspect the accuracy of the “fixes” tag in the commit description.
> 
> Besides, adding an extra folio_test_ksm() check is unneeded, see reasons below.
> 

I reproduced the issue twice on v6.18
(7d0a66e4bb90, CONFIG_KSM=y, CONFIG_KASAN=y and
CONFIG_KASAN_GENERIC=y).

The exact invocation from my previous email:

   /tmp/ksm_fixed_pool_static 4 2048 1 100000 64

reproduced the issue after about 220 seconds:

   BUG: KASAN: slab-use-after-free in page_address_in_vma+0x292/0x2b0
   Read of size 8 by task ksmd

   page_address_in_vma
   write_protect_page
   try_to_merge_one_page
   try_to_merge_with_ksm_page
   ksm_scan_thread

The corresponding free path included:

   __put_anon_vma
   unlink_anon_vmas
   ...
   mremap_to
   do_mremap

A second run with 4096 slots reproduced the same UAF after about
142 seconds. This confirms that the issue is present in v6.18.

I also rechecked the Fixes tag. Before 21d0d443cdc1
("rmap: resurrect page_address_in_vma anon_vma check"), the anonymous
page path in page_address_in_vma() computed the address without
dereferencing the page-side anon_vma. That commit added the comparison
between vma->anon_vma->root and page_anon_vma(page)->root.

At that time, the KSM scan path already retained a page reference,
dropped mmap_sem, and later called page_address_in_vma() from
write_protect_page() or replace_page(). The page reference does not
pin the page-side anon_vma.

Commit 4829b906cc06 ("ksm: fix page_address_in_vma anon_vma oops")
added a NULL check, but the helper still dereferences the anon_vma
when it is non-NULL but stale.

MREMAP_DONTUNMAP, especially commit 1583aa278f5f
("mm: mremap: unlink anon_vmas when mremap with MREMAP_DONTUNMAP
success"), makes this race easy to reproduce and explains the
unlink_anon_vmas() free path. However, it did not introduce the
unpinned anon_vma dereference that this patch removes. Based on that,
I think 21d0d443cdc1 remains the earliest appropriate Fixes commit.
Please let me know if I am missing a lifetime guarantee that made the
dereference safe before the MREMAP_DONTUNMAP changes.

You are also right that the folio_test_ksm() check is unnecessary.
cmp_and_merge_page() already checks folio_stable_node(), and a KSM
fork page that continues through this path is handled by the
page == kpage check in try_to_merge_one_page(). I will drop that hunk
and the corresponding commit-message text in v2.

Thanks,
Gongyi

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH] mm/ksm: avoid anon_vma UAF during page merging
  2026-11-01 18:11 ` David Hildenbrand (Arm)
  2026-11-02  3:32   ` Zhao Gongyi
@ 2026-11-02 11:23   ` Lorenzo Stoakes (ARM)
  1 sibling, 0 replies; 7+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-11-02 11:23 UTC (permalink / raw)
  To: David Hildenbrand (Arm)
  Cc: Zhao Gongyi, akpm, xu.xin, chengming.zhou, aarcange, riel,
	linux-mm, linux-kernel

On Sun, Nov 01, 2026 at 07:11:25PM +0100, David Hildenbrand (Arm) wrote:
> On 10/31/26 19:22, Zhao Gongyi wrote:
>
> Hi,
>
> thanks for the report! CCing Lorenzo.
>
> > KASAN reports a use-after-free in page_address_in_vma() while ksmd
> > merges pages. ksmd keeps a folio reference across an mmap_lock drop,
> > during which mremap() can remove its last mapping and free its
> > anon_vma. page_address_in_vma() then dereferences the stale pointer
> > from folio->mapping.

Fun fun. KSM is dropping the lock then assuming the VMA doesn't change.

I want to take a different approach to this fix so I'll take it over.

>
> Do you have a link to the report or the actual KASAN report?

Thanks, the attached report is useful.

--
Cheers, Lorenzo

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH] mm/ksm: avoid anon_vma UAF during page merging
  2026-11-02 10:18 ` xu.xin
  2026-11-02 11:14   ` Zhao Gongyi
@ 2026-11-02 12:14   ` Lorenzo Stoakes (ARM)
  1 sibling, 0 replies; 7+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-11-02 12:14 UTC (permalink / raw)
  To: xu.xin
  Cc: zhaogongyi, aarcange, akpm, chengming.zhou, david, linux-kernel,
	linux-mm, riel

On Mon, Nov 02, 2026 at 06:18:31PM +0800, xu.xin@linux.dev wrote:
> > KASAN reports a use-after-free in page_address_in_vma() while ksmd
> > merges pages. ksmd keeps a folio reference across an mmap_lock drop,
> > during which mremap() can remove its last mapping and free its
> > anon_vma. page_address_in_vma() then dereferences the stale pointer
> > from folio->mapping.
> >
> > KSM needs only a candidate address. write_protect_page() checks that
> > the PTE maps the folio, and replace_page() verifies the saved PTE
> > before replacing it. Compute the candidate from the folio index and
> > VMA instead of dereferencing the folio-side anon_vma. Reject KSM
> > folios because their page index is not meaningful.
> >
> > Fixes: 21d0d443cdc1 ("rmap: resurrect page_address_in_vma anon_vma check")
> > Signed-off-by: Zhao Gongyi <zhaogongyi@bytedance.com>
> > ---
> > Testing:
> >   Unpatched mm-new: fixed-pool KSM/mremap reproduced the KASAN UAF.
> >   Patched mm-new: the same workload ran for 12 minutes without KASAN,
> >   BUG or WARNING; fork/COW KSM smoke passed.
>
> Thanks for the report.
>
> I’d like to ask whether this issue is easy to reproduce on 6.18 since
> I suspect the accuracy of the “fixes” tag in the commit description.
>
> Besides, adding an extra folio_test_ksm() check is unneeded, see reasons below.
>
> >
> >  mm/ksm.c | 6 +++---
> >  1 file changed, 3 insertions(+), 3 deletions(-)
> >
> > diff --git a/mm/ksm.c b/mm/ksm.c
> > index d6f736a..f8af5ee 100644
> > --- a/mm/ksm.c
> > +++ b/mm/ksm.c
> > @@ -1307,7 +1307,7 @@ static int write_protect_page(struct vm_area_struct *vma, struct folio *folio,
> >  	if (WARN_ON_ONCE(folio_test_large(folio)))
> >  		return err;
> >
> > -	pvmw.address = page_address_in_vma(folio, folio_page(folio, 0), vma);
> > +	pvmw.address = vma_anon_address(vma, folio_pgoff(folio), 1);
> >  	if (pvmw.address == -EFAULT)
> >  		goto out;
> >
> > @@ -1408,7 +1408,7 @@ static int replace_page(struct vm_area_struct *vma, struct page *page,
> >  	int err = -EFAULT;
> >  	struct mmu_notifier_range range;
> >
> > -	addr = page_address_in_vma(folio, page, vma);
> > +	addr = vma_anon_address(vma, page_pgoff(folio, page), 1);
> >  	if (addr == -EFAULT)
> >  		goto out;
> >
> > @@ -1515,7 +1515,7 @@ static int try_to_merge_one_page(struct vm_area_struct *vma,
> >  	if (page == kpage)			/* ksm page forked */
> >  		return 0;
> >
> > -	if (!folio_test_anon(folio))
> > +	if (!folio_test_anon(folio) || folio_test_ksm(folio))
> >  		goto out;
>
> The check folio_test_ksm(folio)) in try_to_merge_one_page is unnecessary, because before
> try_to_merge_one_page() is called, cmp_and_merge_page() invokes folio_stable_node(), which already checks
> whether this is a KSM page. Only KSM fork pages are allowed to pass through; even if they do pass, they
> will ultimately be intercepted by the if (page == kpage) check in try_to_merge_one_page.

This doesn't quite cover it, as cmp_and_merge_page() doesn't bail if
!folio_test_ksm(), it can then fall through to stable_tree_search() which can
return NULL legitimately there, so the page ends up in th eunstable tree.

Actually right now what's saving us is page_address_in_vma() implicitly
rejecting KSM folios (since folio_anon_vma() returns NULL for them).

So if you remove that, you have to re-add the check there.

This is very fiddly, and KSM is very worrying :)

(I'm sending a fix separately.)

--
Cheers, Lorenzo

^ permalink raw reply	[flat|nested] 7+ messages in thread

end of thread, other threads:[~2026-11-02 12:14 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-31 18:22 [PATCH] mm/ksm: avoid anon_vma UAF during page merging Zhao Gongyi
2026-11-01 18:11 ` David Hildenbrand (Arm)
2026-11-02  3:32   ` Zhao Gongyi
2026-11-02 11:23   ` Lorenzo Stoakes (ARM)
2026-11-02 10:18 ` xu.xin
2026-11-02 11:14   ` Zhao Gongyi
2026-11-02 12:14   ` 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

all inboxes | Powered by JetHome®