Skip to content

Protect adjacent mappings from mremap growth - #391

Open
Ksld154 wants to merge 1 commit into
sysprog21:mainfrom
Ksld154:fix-mremap-adjacent-growth
Open

Ksld154 wants to merge 1 commit into
sysprog21:mainfrom
Ksld154:fix-mremap-adjacent-growth

Conversation

@Ksld154

@Ksld154 Ksld154 commented Sep 20, 2026

Copy link
Copy Markdown

Problem

Compatible adjacent anonymous mappings can coalesce into one tracked region.
The in-place mremap growth check skipped an overlapping region when its start
matched the source address, even when that region extended past the requested
source range.

The growth path then treated the occupied suffix as free and zeroed live bytes
in the adjacent mapping. This surfaced as a consistent crash while Alpine apk
resolved packages from local repository indexes.

Linux permits in-place expansion only when the requested source range ends at
the end of its VMA. An occupied suffix must force relocation when
MREMAP_MAYMOVE is present, or ENOMEM otherwise.

Reproduction

Map two adjacent anonymous pages with matching permissions, fill each page
with a distinct pattern, and grow the first page to two pages.

Before this change, elfuse returned the original address and cleared the
second page. After this change:

  • growth without MREMAP_MAYMOVE returns ENOMEM and preserves both mappings;
  • growth with MREMAP_MAYMOVE relocates the source, preserves its contents,
    zeroes the extension, and leaves the adjacent mapping unchanged.

Environment

  • macOS 26.7, Darwin 25.6.0
  • Apple Silicon arm64
  • Apple SDK 27.0
  • Alpine 3.21 aarch64 guest

Validation

  • make check-format
  • build/elfuse build/test-mremap: 13 passed
  • test-mremap under the QEMU aarch64 Linux reference kernel: 13 passed
  • Alpine apk --no-network --simulate add git: three consecutive runs
    completed with status 0
  • make check: all 99 core tests and all mremap suites passed; the final
    BusyBox nslookup smoke test failed because no DNS server was reachable

Closes #389


Summary by cubic

Protects adjacent coalesced mappings from being overwritten during in-place mremap growth. Previously, growth could zero live bytes in a neighboring mapping when the region extended past the source range; now it returns ENOMEM without MREMAP_MAYMOVE and relocates with MREMAP_MAYMOVE, preserving both mappings.

  • Adds a regression test covering both growth modes and verifying contents are preserved.

Written for commit 60b4185. Summary will update on new commits.

Review in cubic

A coalesced region can extend past the requested source range.

Treat an overlap in the growth tail as occupied.

MAYMOVE then relocates instead of clearing live bytes.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="tests/test-mremap.c">

<violation number="1" location="tests/test-mremap.c:417">
P3: The test's discriminating power rests on elfuse coalescing the two adjacent anonymous mappings into one tracked region; nothing verifies that coalescing happened. If region tracking stops merging adjacent anonymous mappings, both the ENOMEM and the relocation assertions pass without ever exercising the buggy in-place-growth path, and the test silently stops guarding this regression. Document the coalescing requirement, or assert it (for example, check /proc/self/maps shows one entry spanning both pages, or add a positive control growing into a free adjacent page) so the test cannot degrade into a no-op.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread tests/test-mremap.c

unsigned char *first = mmap(base, page, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
unsigned char *neighbor =

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The test's discriminating power rests on elfuse coalescing the two adjacent anonymous mappings into one tracked region; nothing verifies that coalescing happened. If region tracking stops merging adjacent anonymous mappings, both the ENOMEM and the relocation assertions pass without ever exercising the buggy in-place-growth path, and the test silently stops guarding this regression. Document the coalescing requirement, or assert it (for example, check /proc/self/maps shows one entry spanning both pages, or add a positive control growing into a free adjacent page) so the test cannot degrade into a no-op.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/test-mremap.c, line 417:

<comment>The test's discriminating power rests on elfuse coalescing the two adjacent anonymous mappings into one tracked region; nothing verifies that coalescing happened. If region tracking stops merging adjacent anonymous mappings, both the ENOMEM and the relocation assertions pass without ever exercising the buggy in-place-growth path, and the test silently stops guarding this regression. Document the coalescing requirement, or assert it (for example, check /proc/self/maps shows one entry spanning both pages, or add a positive control growing into a free adjacent page) so the test cannot degrade into a no-op.</comment>

<file context>
@@ -397,6 +398,70 @@ static void test_source_range_hole(void)
+
+    unsigned char *first = mmap(base, page, PROT_READ | PROT_WRITE,
+                                MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
+    unsigned char *neighbor =
+        mmap(base + page, page, PROT_READ | PROT_WRITE,
+             MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
</file context>

@jserv
jserv requested a review from Max042004 September 20, 2026 18:39
Comment thread src/syscall/mem.c
Comment on lines +3900 to +3901
bool can_grow =
!region_range_overlaps(g, grow_off, grow_off + grow_len);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refusing in-place growth here sends a source whose region extends past grow_off down the move path, which is right, and it removes the old path's worst case: a memset of grow_len zeros straight into a live MAP_SHARED file overlay, which wrote those zeros to the backing file.

One hole survives the move. cleanup_overlays_in_range rounds its teardown up to the host page (ALIGN_UP(end, hps), 16 KiB on Apple Silicon) while guest ranges are 4 KiB granular, and hvf_restore_slab_backing re-mmaps fresh slab without repopulating from the file, so growing the first 4 KiB of a shared file mapping leaves the next 12 KiB of the surviving suffix reading slab instead of file contents. That was already reachable through a separate mapping at grow_off, so it is a follow-up rather than something this change owes.

Comment thread tests/test-mremap.c
Comment on lines +415 to +419
unsigned char *first = mmap(base, page, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
unsigned char *neighbor =
mmap(base + page, page, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case reaches the branch the fix changed only while guest_region_add still merges the two mappings into one region. If regions_mergeable_layout ever stops merging separate same-prot anonymous mmaps, neighbor becomes its own region starting at grow_off, the unpatched code refuses growth for the right reason, and the test passes without exercising the defect.

A second case that grows the first page of a single mmap(NULL, 2 * page, ...) hits the same occupied-suffix path by construction, since one mapping is one region regardless of the merge rules.

@jserv jserv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mremap in-place growth overwrites an adjacent coalesced anonymous mapping

2 participants