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
next prev parent 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