Re: [PATCH] fs/proc/task_mmu: check THP exclusivity for each subpage

From: Kelvin Zhang

Date: Sun Nov 01 2026 - 13:27:30 EST


Hi David,

Thanks for the clarification.

I don't have a real application depending on precise per-subpage bit 56
values, or a production failure to report. This is a review/test-driven
report, with LLM assistance, rather than something found through a failing
workload. I followed it up with an actual old/new kernel comparison.

So I have no workload-based objection to switching to the conservative
whole-folio behavior you described. The attached kselftest-style reproducer
is just to make the current CONFIG_PAGE_MAPCOUNT=y behavior reproducible,
not to argue that exact subpage reporting must be preserved as an ABI.

The test forks with a PMD-mapped anonymous THP, then has the child unmap
one base page while the parent retains its PMD mapping. On the unpatched
kernel, unmapping subpage 0 makes the parent's bulk pagemap read report
all 512 subpages exclusive; unmapping subpage 1, 256 or 511 reports none
exclusive. With the one-line fix, only the unmapped-in-child subpage is
reported exclusive in each case. The fully private/shared controls pass
on both kernels.

Tested on x86-64 QEMU/KVM with 4KB base pages, 2MB PMD THPs and
CONFIG_PAGE_MAPCOUNT=y, using mm-hotfixes-unstable at 64703f0fee74 and
that same base plus this patch. Three runs of the same binary gave:

Before: 20 PASS, 8 FAIL per run
After: 28 PASS, 0 FAIL per run

To build, save the attachment under tools/testing/selftests/mm/ and run
from the kernel root:

cc -O2 -Wall -Wextra tools/testing/selftests/mm/pagemap_thp_exclusive.c \
-o /tmp/pagemap_thp_exclusive
/tmp/pagemap_thp_exclusive

It uses the existing ../kselftest.h and skips if a 2MB PMD THP cannot be
obtained. Its expectations are deliberately limited to the current
CONFIG_PAGE_MAPCOUNT=y implementation, not the conservative variant.

Thanks,
Kelvin
// SPDX-License-Identifier: GPL-2.0
/*
* Diagnostic for the CONFIG_PAGE_MAPCOUNT=y pagemap PMD path, not a generic
* bit-56 ABI test: other configurations may report folio-wide exclusivity.
* Requires 4KB base pages and 2MB anonymous PMD THPs.
*
* Save under tools/testing/selftests/mm/ and build from the kernel root:
* cc -O2 -Wall -Wextra tools/testing/selftests/mm/pagemap_thp_exclusive.c \
* -o /tmp/pagemap_thp_exclusive
*/
#define _GNU_SOURCE
#include <fcntl.h>
#include <signal.h>
#include <stdint.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
#include "../kselftest.h"

#ifndef MADV_COLLAPSE
#define MADV_COLLAPSE 25
#endif
#define PAGE_BYTES 4096UL
#define THP_BYTES (2UL * 1024 * 1024)
#define NR_PAGES (THP_BYTES / PAGE_BYTES)
#define PRESENT (1ULL << 63)
#define EXCLUSIVE (1ULL << 56)

static void require(int ok, const char *what)
{
if (!ok)
ksft_exit_fail_msg("%s failed (errno=%d)\n", what, errno);
}

static void notify(int fd)
{
require(write(fd, "x", 1) == 1, "pipe write");
}

static void await(int fd)
{
char c;

require(read(fd, &c, 1) == 1, "pipe read");
}

static unsigned long anon_huge_bytes(void *addr)
{
FILE *f = fopen("/proc/self/smaps", "r");
unsigned long start, end, kb = 0;
bool selected = false;
char line[512];

require(f != NULL, "open smaps");
while (fgets(line, sizeof(line), f)) {
if (sscanf(line, "%lx-%lx", &start, &end) == 2)
selected = (uintptr_t)addr >= start && (uintptr_t)addr < end;
if (selected && sscanf(line, "AnonHugePages: %lu kB", &kb) == 1)
break;
}
fclose(f);
return kb * 1024;
}

/* hole == -1: all shared; hole == -2: all exclusive. */
static void check_bulk(int fd, char *addr, int hole, const char *label)
{
uint64_t entries[NR_PAGES];
unsigned int i, wrong = 0, exclusive = 0;
off_t offset = (uintptr_t)addr / PAGE_BYTES * sizeof(uint64_t);

require(pread(fd, entries, sizeof(entries), offset) == sizeof(entries),
"read pagemap");
for (i = 0; i < NR_PAGES; i++) {
bool want = hole == -2 || hole == (int)i;

exclusive += !!(entries[i] & EXCLUSIVE);
wrong += !(entries[i] & PRESENT) ||
!!(entries[i] & EXCLUSIVE) != want;
}
ksft_test_result(!wrong, "%s: hole=%d exclusive=%u/%lu wrong=%u\n",
label, hole, exclusive, NR_PAGES, wrong);
}

static void check_single(int fd, char *addr, unsigned int index, bool exclusive)
{
uint64_t entry;
off_t offset = ((uintptr_t)addr / PAGE_BYTES + index) * sizeof(entry);

require(pread(fd, &entry, sizeof(entry), offset) == sizeof(entry),
"read single pagemap entry");
ksft_test_result((entry & PRESENT) && !!(entry & EXCLUSIVE) == exclusive,
"single read: subpage=%u expected exclusive=%d\n",
index, exclusive);
}

static void run_case(unsigned int hole)
{
char *reserve, *addr;
int go[2], done[2], fd, status;
pid_t child;

/* Leave PROT_NONE guards on both sides to prevent VMA merging. */
reserve = mmap(NULL, 3 * THP_BYTES, PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
require(reserve != MAP_FAILED, "reserve address space");
addr = (char *)(((uintptr_t)reserve + THP_BYTES) & ~(THP_BYTES - 1));
require(mmap(addr, THP_BYTES, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == addr,
"map anonymous memory");
require(!madvise(addr, THP_BYTES, MADV_HUGEPAGE), "MADV_HUGEPAGE");
memset(addr, 0x5a, THP_BYTES);
if (anon_huge_bytes(addr) != THP_BYTES)
madvise(addr, THP_BYTES, MADV_COLLAPSE);
if (anon_huge_bytes(addr) != THP_BYTES) {
for (unsigned int i = 0; i < 7; i++)
ksft_test_result_skip("hole=%u: no 2MB PMD THP\n", hole);
munmap(reserve, 3 * THP_BYTES);
return;
}
fd = open("/proc/self/pagemap", O_RDONLY);
require(fd >= 0, "open pagemap");
check_bulk(fd, addr, -2, "before fork");
require(!pipe(go) && !pipe(done), "pipe");
child = fork();
require(child >= 0, "fork");
if (!child) {
close(go[1]);
close(done[0]);
await(go[0]);
require(!munmap(addr + hole * PAGE_BYTES, PAGE_BYTES), "child munmap");
notify(done[1]);
await(go[0]);
_exit(0);
}
close(go[0]);
close(done[1]);
check_bulk(fd, addr, -1, "after fork, before child munmap");
notify(go[1]);
await(done[0]);
ksft_test_result(anon_huge_bytes(addr) == THP_BYTES,
"hole=%u: parent still PMD mapped\n", hole);
check_bulk(fd, addr, hole, "after child munmap");
check_single(fd, addr, hole, true);
check_single(fd, addr, hole ? 0 : 1, false);
notify(go[1]);
require(waitpid(child, &status, 0) == child && WIFEXITED(status) &&
!WEXITSTATUS(status), "child exit");
check_bulk(fd, addr, -2, "after child exit");
close(go[1]);
close(done[0]);
close(fd);
require(!munmap(reserve, 3 * THP_BYTES), "unmap reservation");
}

int main(void)
{
unsigned int holes[] = { 0, 1, NR_PAGES / 2, NR_PAGES - 1 };

ksft_print_header();
if (sysconf(_SC_PAGESIZE) != PAGE_BYTES)
ksft_exit_skip("This reproducer requires 4KB base pages\n");
ksft_print_msg("Requires CONFIG_PAGE_MAPCOUNT=y; not a generic ABI test\n");
ksft_set_plan(7 * ARRAY_SIZE(holes));
alarm(30);
for (unsigned int i = 0; i < ARRAY_SIZE(holes); i++)
run_case(holes[i]);
ksft_finished();
}