Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
From: "Lorenzo Stoakes (ARM)" <ljs@kernel.org>
To: Hyunwoo Kim <imv4bel@gmail.com>
Cc: akpm@linux-foundation.org, david@kernel.org, liam@infradead.org,
	 vbabka@kernel.org, rppt@kernel.org, surenb@google.com,
	mhocko@suse.com,  linux-mm@kvack.org,
	linux-kernel@vger.kernel.org
Subject: Re: [PATCH v2 2/2] selftests/mm: add stale walk->action race test
Date: Tue, 11 Aug 2026 17:33:53 +0100	[thread overview]
Message-ID: <antM5eWXxbKbdiKy@gremlin> (raw)
In-Reply-To: <20260811161949.3879321-3-imv4bel@gmail.com>

On Wed, Aug 12, 2026 at 01:18:58AM +0900, Hyunwoo Kim wrote:
> The added pagewalk_race_test maps two PMDs and faults in 2MB of the first
> one.  A second thread then faults in the second PMD and drops it again with
> MADV_DONTNEED in a loop, while the main thread reads Rss for the mapping
> from /proc/self/smaps.
>
> Clearing the second PMD while smaps_pte_range() runs leaves walk->action
> erroneously set to ACTION_AGAIN, which causes the PUD walk to be retried,
> so the first PMD is counted twice and Rss comes out twice as large as what
> was faulted in.
>
> mincore() is the caller named in the fix, but the second walk writes past
> the length mincore() copies back, so it cannot be seen from userspace
> there.  smaps reports what the callbacks counted, so the duplicate shows up
> in Rss.
>
> A failure can only come from the kernel counting the same page twice, so
> missing the race is harmless.  On an unfixed kernel the test fails after a
> few hundred reads at most and takes about half a second.
>
> Assisted-by: Claude:claude-opus-5
> Signed-off-by: Hyunwoo Kim <imv4bel@gmail.com>
> ---
>  tools/testing/selftests/mm/.gitignore         |   1 +
>  tools/testing/selftests/mm/Makefile           |   2 +
>  tools/testing/selftests/mm/ksft_pagewalk.sh   |   4 +
>  .../testing/selftests/mm/pagewalk_race_test.c | 138 ++++++++++++++++++
>  tools/testing/selftests/mm/run_vmtests.sh     |   2 +
>  tools/testing/selftests/mm/vm_util.h          |   1 +
>  6 files changed, 148 insertions(+)
>  create mode 100755 tools/testing/selftests/mm/ksft_pagewalk.sh
>  create mode 100644 tools/testing/selftests/mm/pagewalk_race_test.c
>
> diff --git a/tools/testing/selftests/mm/.gitignore b/tools/testing/selftests/mm/.gitignore
> index 9ccd9e1447e66b..92f981f97740fd 100644
> --- a/tools/testing/selftests/mm/.gitignore
> +++ b/tools/testing/selftests/mm/.gitignore
> @@ -66,3 +66,4 @@ merge
>  prctl_thp_disable
>  rmap
>  folio_split_race_test
> +pagewalk_race_test
> diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
> index e6df968f0971c8..cde9b22f121d4b 100644
> --- a/tools/testing/selftests/mm/Makefile
> +++ b/tools/testing/selftests/mm/Makefile
> @@ -105,6 +105,7 @@ TEST_GEN_FILES += guard-regions
>  TEST_GEN_FILES += merge
>  TEST_GEN_FILES += rmap
>  TEST_GEN_FILES += folio_split_race_test
> +TEST_GEN_FILES += pagewalk_race_test
>
>  ifneq ($(ARCH),arm64)
>  TEST_GEN_FILES += soft-dirty
> @@ -163,6 +164,7 @@ TEST_PROGS += ksft_mlock.sh
>  TEST_PROGS += ksft_mmap.sh
>  TEST_PROGS += ksft_mremap.sh
>  TEST_PROGS += ksft_pagemap.sh
> +TEST_PROGS += ksft_pagewalk.sh
>  TEST_PROGS += ksft_pfnmap.sh
>  TEST_PROGS += ksft_pkey.sh
>  TEST_PROGS += ksft_process_madv.sh
> diff --git a/tools/testing/selftests/mm/ksft_pagewalk.sh b/tools/testing/selftests/mm/ksft_pagewalk.sh
> new file mode 100755
> index 00000000000000..6f6c3ee1c13ef4
> --- /dev/null
> +++ b/tools/testing/selftests/mm/ksft_pagewalk.sh
> @@ -0,0 +1,4 @@
> +#!/bin/sh -e
> +# SPDX-License-Identifier: GPL-2.0
> +
> +./run_vmtests.sh -t pagewalk
> diff --git a/tools/testing/selftests/mm/pagewalk_race_test.c b/tools/testing/selftests/mm/pagewalk_race_test.c
> new file mode 100644
> index 00000000000000..42fd6e75e821ed
> --- /dev/null
> +++ b/tools/testing/selftests/mm/pagewalk_race_test.c
> @@ -0,0 +1,138 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Regression test for a stale walk->action escaping walk_pmd_range() and
> + * making walk_pud_range() walk the same range twice.
> + *
> + * The mapping is two PMDs inside one PUD. PMD 0 is populated once and left
> + * alone, PMD 1 is faulted in and dropped again by a second thread. Clearing
> + * PMD 1 under smaps_pte_range() makes it raise ACTION_AGAIN, and since it is
> + * the last entry the stale value leaves walk_pmd_range(), so smaps accounts
> + * PMD 0 twice. A kernel that does not reclaim the emptied page table never
> + * clears PMD 1 and so never hits the race.
> + *
> + * A hit can only come from the kernel counting the same page twice, so the
> + * test cannot fail spuriously.
> + */
> +#define _GNU_SOURCE
> +
> +#include <pthread.h>
> +#include <stdatomic.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <sys/mman.h>
> +#include <unistd.h>
> +
> +#include "vm_util.h"
> +#include "kselftest.h"
> +
> +#define NR_PMDS		2
> +#define NR_ROUNDS	20000

I did say please don't let this be slow :) if the test takes longer than say
200ms then just let it be less reliably repro maybe?

> +/* Cap on how much of PMD 0 to fault in, so that a large PMD stays cheap. */
> +#define POP_MAX		(2 * 1024 * 1024)
> +
> +static char *area;
> +static size_t pmd_size;
> +static atomic_int stop;
> +
> +static void *racer(void *arg)
> +{
> +	char *pmd1 = area + pmd_size;
> +
> +	while (atomic_load_explicit(&stop, memory_order_acquire) == 0) {
> +		/* madvise() below keeps the compiler from lifting this out. */

Err what? :) this seems crazy? You are using an madvise to prevent a compiler
optinisation how? :) isn't atomic_load_explicit() a compiler barrier anyway?

I think you can probably do this better... see FORCE_READ() for use of volatile
to achieve the same thing for a read.

But why would zapping a PMD achieve anything for the compiler? So confused

> +		*pmd1 = 1;
> +		madvise(pmd1, pmd_size, MADV_DONTNEED);
> +	}
> +	return NULL;
> +}
> +
> +static unsigned long smaps_rss_kb(void)
> +{
> +	char buf[1024];
> +	char *entry;
> +
> +	entry = __get_smap_entry(area, "Rss:", buf, sizeof(buf));
> +	if (!entry)
> +		ksft_exit_fail_msg("no Rss: entry for the test mapping\n");
> +
> +	return strtoul(entry, NULL, 10);
> +}
> +
> +int main(void)
> +{
> +	unsigned long max_rss_kb, rss_kb = 0;
> +	size_t size, pop_size, i;
> +	pthread_t thread;
> +	char *raw;
> +
> +	ksft_print_header();
> +
> +	pmd_size = read_pmd_pagesize();
> +	if (!pmd_size)
> +		ksft_exit_skip("Cannot determine PMD size\n");
> +
> +	if (sysconf(_SC_NPROCESSORS_ONLN) < 2)
> +		ksft_exit_skip("Need at least 2 CPUs to race\n");
> +
> +	size = NR_PMDS * pmd_size;
> +
> +	/*
> +	 * Align to the mapping size to stay inside one PUD, then trim the
> +	 * slack so that smaps has exactly one VMA to report.
> +	 */
> +	raw = mmap(NULL, 2 * size, PROT_READ | PROT_WRITE,
> +		   MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);

Why MAP_NORESERVE?

> +	if (raw == MAP_FAILED)
> +		ksft_exit_fail_msg("mmap failed\n");
> +
> +	area = (char *)(((unsigned long)raw + size - 1) & ~(size - 1));

This is horrible, break it out into a sensible helper. Magical bitwise stuff all
compressed into one is not something we want.

> +	if (area != raw)
> +		munmap(raw, area - raw);
> +	if (raw + 2 * size != area + size)
> +		munmap(area + size, raw + 2 * size - (area + size));

Again you're compressing things too much. Keep it simple.

> +
> +	/* A huge PMD never reaches pte_offset_map_lock(), so keep them out. */
> +	if (madvise(area, size, MADV_NOHUGEPAGE))
> +		ksft_exit_skip("MADV_NOHUGEPAGE failed\n");

No idea why you'd skip on this.

This is another reason why you should use the kselftest harness, ASSERT_EQ(...,
0) is much easier... and it has sensible skipping stuff too.

> +
> +	pop_size = pmd_size < POP_MAX ? pmd_size : POP_MAX;
> +	memset(area, 1, pop_size);
> +
> +	max_rss_kb = (pop_size >> 10) + 256;
> +
> +	/* Over the limit before racing means this is not our own mapping. */
> +	rss_kb = smaps_rss_kb();
> +	if (rss_kb > max_rss_kb)
> +		ksft_exit_fail_msg("Rss is %lu kB before racing, expected at most %lu kB\n",
> +				   rss_kb, max_rss_kb);
> +
> +	ksft_set_plan(1);
> +	ksft_print_msg("racing smaps against MADV_DONTNEED, %d rounds\n",
> +		       NR_ROUNDS);
> +
> +	if (pthread_create(&thread, NULL, racer, NULL))
> +		ksft_exit_fail_msg("pthread_create failed\n");
> +
> +	for (i = 0; i < NR_ROUNDS; i++) {
> +		rss_kb = smaps_rss_kb();
> +		if (rss_kb > max_rss_kb)
> +			break;
> +	}
> +
> +	atomic_store_explicit(&stop, 1, memory_order_release);
> +	pthread_join(thread, NULL);
> +
> +	if (i < NR_ROUNDS) {
> +		ksft_print_msg("walk ran twice over the same range\n");
> +		ksft_test_result_fail("Rss %lu kB exceeds %lu kB, round %zu\n",
> +				      rss_kb, max_rss_kb, i);
> +	} else {
> +		ksft_test_result_pass("Rss within %lu kB over %d rounds\n",
> +				      max_rss_kb, NR_ROUNDS);
> +	}
> +
> +	ksft_exit(i == NR_ROUNDS);
> +
> +	return 0;

Putting this all into one function is disgusting. It might be test code but
that's new excuse for being schloppy. Separate things out into functions please and....

> +}

...use the kselftest_harness, see guard_regions.c for an example of how it's
used.

I know it's just 1 test but it avoids all the stupid plan and manual
ksft_test_*() invocations and gets you ASSERT_*() etc.

> diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
> index 8c296dedf0474d..d90c6370814f7a 100755
> --- a/tools/testing/selftests/mm/run_vmtests.sh
> +++ b/tools/testing/selftests/mm/run_vmtests.sh
> @@ -398,6 +398,8 @@ fi
>
>  CATEGORY="pagemap" run_test ./pagemap_ioctl
>
> +CATEGORY="pagewalk" run_test ./pagewalk_race_test
> +
>  CATEGORY="pfnmap" run_test ./pfnmap
>
>  # COW tests
> diff --git a/tools/testing/selftests/mm/vm_util.h b/tools/testing/selftests/mm/vm_util.h
> index ea8fc8fdf0eb0b..62292e2417d162 100644
> --- a/tools/testing/selftests/mm/vm_util.h
> +++ b/tools/testing/selftests/mm/vm_util.h
> @@ -88,6 +88,7 @@ bool pagemap_is_populated(int fd, char *start);
>  unsigned long pagemap_get_pfn(int fd, char *start);
>  void clear_softdirty(void);
>  bool check_for_pattern(FILE *fp, const char *pattern, char *buf, size_t len);
> +char *__get_smap_entry(void *addr, const char *pattern, char *buf, size_t len);
>  uint64_t read_pmd_pagesize(void);
>  unsigned long rss_anon(void);
>  bool check_huge_anon(void *addr, int nr_hpages, uint64_t hpage_size);
> --
> 2.43.0
>

--
Cheers, Lorenzo


  reply	other threads:[~2026-08-11 16:34 UTC|newest]

Thread overview: 12+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-11 16:18 [PATCH v2 0/2] mm/pagewalk: fix stale walk->action escaping walk_pmd_range() Hyunwoo Kim
2026-08-11 16:18 ` [PATCH v2 1/2] " Hyunwoo Kim
2026-08-11 16:23   ` Lorenzo Stoakes (ARM)
2026-08-11 17:10   ` David Hildenbrand (Arm)
2026-08-11 19:37   ` Andrew Morton
2026-08-11 20:17     ` Hyunwoo Kim
2026-08-11 21:57       ` Andrew Morton
2026-08-11 16:18 ` [PATCH v2 2/2] selftests/mm: add stale walk->action race test Hyunwoo Kim
2026-08-11 16:33   ` Lorenzo Stoakes (ARM) [this message]
2026-08-11 17:11   ` David Hildenbrand (Arm)
2026-08-11 18:23     ` Lorenzo Stoakes (ARM)
2026-08-11 18:49       ` David Hildenbrand (Arm)

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=antM5eWXxbKbdiKy@gremlin \
    --to=ljs@kernel.org \
    --cc=akpm@linux-foundation.org \
    --cc=david@kernel.org \
    --cc=imv4bel@gmail.com \
    --cc=liam@infradead.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=mhocko@suse.com \
    --cc=rppt@kernel.org \
    --cc=surenb@google.com \
    --cc=vbabka@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox