[REGRESSION] mm/mprotect: shared-dirty base-page toggle slower since v6.17
From: Chengfeng Lin
Date: Wed Jun 24 2026 - 13:28:27 EST
Hi,
I have a refreshed bare-metal result for the shared-dirty mprotect()
slowdown I reported earlier from QEMU/lab testing.
The reproducer is intentionally narrow:
- MAP_SHARED | MAP_ANONYMOUS mapping
- 64 MiB range, write-prefaulted before timing
- state check: 4 KiB base pages, no THP backing
- repeated full-range mprotect(PROT_READ)
- restore with mprotect(PROT_READ | PROT_WRITE)
- write-touch after each protect/restore cycle
So this is not a generic mprotect() regression claim. The scope is the
shared-dirty base-page PTE permission-change path.
The bare-metal machine is an Intel Core i7-14700 system. The workload is
single-threaded and pinned to one logical CPU with `taskset -c 2`. The primary
metric is `iteration_ns_per_page`, lower is better. It is the wall-clock time
for one full protect/restore/write-touch iteration, divided by the number of
4 KiB pages in the range. Each benchmark step used 9 external rounds, 1000
iterations, and 10 warmup iterations.
First, the v6.12 -> v6.19 result still reproduces on bare metal:
kernel iteration_ns_per_page
v6.12.77 26
v6.19.9 37
I then narrowed the release window with 3 interleaved boot/run steps per
kernel:
kernel values mean
v6.16 25 25 25 25.000
v6.17 37 37 37 37.000
v6.18 38 38 38 38.000
v6.18.19 38 38 38 38.000
v6.19.9 37 36 37 36.667
I also checked later context with the same standalone command:
kernel values mean
v7.0.9 36 36 36 36.000
v6.19.9 + Pedro v3 patch-only 39 39 39 39.000
v7.1.0-rc3 mm-unstable/Pedro 39 39 39 39.000
I do not treat the mm-unstable result as a clean release-kernel comparison.
It is only a follow-up check, and in this workload it did not improve the
standalone result.
All of these runs reported `expected_match_ratio=100` and
`unexpected_results=0`. The state check in the standalone output stays in the
same shape: 4 KiB pages, no THP.
This puts the slowdown in the v6.16 -> v6.17 release window.
As an attribution check, I also built a v6.17 probe kernel that only changes
the present-PTE path in `mm/mprotect.c::change_pte_range()` for this workload
back to a single-PTE start/commit/flush shape. That is not an upstream patch
and not a clean release-kernel comparison; it is only a hot-path probe.
The result was:
kernel values mean
v6.16 25 25 25 25.000
v6.17 37 37 37 37.000
v6.17 single-PTE probe 25 25 25 25.000
So the targeted probe brings v6.17 back to the v6.16 range for this workload.
That points at the v6.17 PTE-batching shape in `change_pte_range()` as the
main cost for this shared-dirty 4 KiB base-page case.
I do not want to overstate the attribution. I tried reversing the official
`cac1db8c3aad ("mm: optimize mprotect() by PTE batching")` patch onto my
linux-6.17 tree, but it did not apply cleanly. That means this is not an
exact revert result. I can only say that the slowdown appears in the
v6.16 -> v6.17 window, and that this focused probe brings the v6.17 result
back to the v6.16 range.
Evidence bundle:
https://github.com/lcf0399/linux-mm-regression-evidence/tree/acd7fef0e0276ac361971b0960e6611811edf5b3/mprotect-shared-dirty-toggle
Standalone reproducer:
https://github.com/lcf0399/linux-mm-regression-evidence/tree/acd7fef0e0276ac361971b0960e6611811edf5b3/mprotect-shared-dirty-toggle/reproducer
For each installed kernel, the standalone reproducer was run as:
taskset -c 2 env MAPPING_MB=64 ITERATIONS=1000 WARMUP=10 \
EXTERNAL_ROUNDS=9 ./run_mprotect_shared_dirty_reproducer.sh
For the release-window check, a small systemd/GRUB queue booted each target
kernel before running the same command.
Bare-metal summaries and raw run logs:
https://github.com/lcf0399/linux-mm-regression-evidence/tree/acd7fef0e0276ac361971b0960e6611811edf5b3/mprotect-shared-dirty-toggle/bare-metal
Release-window narrowing:
https://github.com/lcf0399/linux-mm-regression-evidence/tree/acd7fef0e0276ac361971b0960e6611811edf5b3/mprotect-shared-dirty-toggle/bare-metal/20260623-narrow-6.16-6.19-3rounds
v6.17 single-PTE probe:
https://github.com/lcf0399/linux-mm-regression-evidence/tree/acd7fef0e0276ac361971b0960e6611811edf5b3/mprotect-shared-dirty-toggle/bare-metal/20260624-6.17-singlepte-probe
Probe patch used for that attribution run:
https://github.com/lcf0399/linux-mm-regression-evidence/blob/acd7fef0e0276ac361971b0960e6611811edf5b3/mprotect-shared-dirty-toggle/bare-metal/20260624-6.17-singlepte-probe/0001-mm-mprotect-probe-6.17-single-pte-hotpath.patch
#regzbot introduced: v6.16..v6.17
Does this scope look useful to investigate further? If yes, I can try a more
exact commit-level check or test a patch you think is the right direction.
Thanks,
Chengfeng