Summary
mremap(..., MREMAP_MAYMOVE) can incorrectly grow a mapping in place when the
requested tail is occupied by an adjacent anonymous mapping. The two mappings
must have compatible metadata so the region tracker coalesces them.
The syscall returns the original address as a successful result and zeroes the
occupied tail. This corrupts the adjacent mapping. The same path can also
publish overlapping region records after the byte corruption.
This was found through an Alpine apk --simulate workload, then reduced to a
small static aarch64 guest binary.
Environment
- Host: macOS 26.7, Darwin 25.6.0, Apple Silicon arm64
- Apple SDK: 27.0
- elfuse commit:
0a633684faa5c6de252b378f6da914230adc3da2
build/elfuse SHA-256:
63879eefead430475e411c7c2cf8adde8dacc9a1439a6c793336c6d77d049526
- Guest compiler:
aarch64-linux-gnu-gcc, static binary
Minimal reproducer
Save as /tmp/mremap-adjacent.c:
#define _GNU_SOURCE
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
int main(void)
{
const size_t page = 4096;
unsigned char *base = mmap(NULL, 3 * page, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (base == MAP_FAILED || munmap(base, 3 * page) != 0)
return 10;
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);
if (first != base || neighbor != base + page)
return 11;
memset(first, 0x5a, page);
memset(neighbor, 0xa5, page);
errno = 0;
unsigned char *grown =
mremap(first, page, 2 * page, MREMAP_MAYMOVE);
printf("base=%p grown=%p errno=%d neighbor=%02x/%02x\n", base,
grown, errno, neighbor[0], neighbor[page - 1]);
if (grown == MAP_FAILED)
return 20;
if (grown == first)
return 21;
if (neighbor[0] != 0xa5 || neighbor[page - 1] != 0xa5)
return 22;
if (grown[0] != 0x5a || grown[page - 1] != 0x5a)
return 23;
return 0;
}
Compile and run:
aarch64-linux-gnu-gcc -static -O2 \
-o /tmp/mremap-adjacent /tmp/mremap-adjacent.c
build/elfuse --timeout 0 /tmp/mremap-adjacent
printf 'status=%s\n' "$?"
Expected result
The second page is occupied, so the source cannot grow in place.
MREMAP_MAYMOVE permits relocation. The call should return a different
address, preserve the first page, zero the new page at the destination, and
leave the adjacent mapping unchanged.
An equivalent call without MREMAP_MAYMOVE should fail with ENOMEM and
leave both mappings unchanged.
Linux enforces this in vrm_can_expand_in_place(): the bytes from the source
address to the end of its VMA must equal old_len. A VMA suffix beyond the
requested source range prevents in-place expansion.
Reference:
mm/mremap.c
Actual result
base=0x200000000 grown=0x200000000 errno=0 neighbor=00/00
status=21
elfuse reports successful in-place growth and clears the adjacent mapping.
The result is identical in quiet and verbose modes. It is also identical with
ELFUSE_DISABLE_TLBI_RANGE=1.
Root cause
guest_region_add_ex_owned_gpa() assigns compatible adjacent anonymous
mappings the same vma_id, then merges them into one tracker record. This is
intentional and matches the fact that Linux may merge compatible VMAs.
The in-place growth branch in sys_mremap() scans the requested tail for
overlap. It skips any overlapping region whose start equals old_off:
if (g->regions[i].start == old_off)
continue;
That test assumes the source tracker record ends at old_off + old_size. A
coalesced record can extend beyond that boundary and contain the adjacent
mapping. The branch therefore treats an occupied tail as free.
The successful branch then executes:
memset((uint8_t *) g->host_base + grow_off, 0, grow_len);
This clears live bytes in the adjacent mapping. Removing the old source range
and adding the enlarged region can then overlap the suffix retained from the
coalesced record, violating the region table's non-overlap invariant.
The existing test_grow_no_maymove() case does not cover this shape. Its
blocker uses PROT_READ specifically to prevent region coalescing with the RW
source.
Proposed fix
Treat any tracked overlap with [grow_off, grow_off + grow_len) as occupied.
The source range ends at grow_off, so no source region needs an exception in
the tail check. mem.c already provides region_range_overlaps() for this
query:
bool can_grow =
!region_range_overlaps(g, grow_off, grow_off + grow_len);
The existing control flow can then relocate when MREMAP_MAYMOVE is present
or return ENOMEM when it is absent. Region coalescing does not need to be
disabled.
Real-world impact
The original reproducer is Alpine 3.21 aarch64 apk 2.14.6 resolving git
from local repository indexes with --no-network --simulate. It consistently
exits with status 139 under elfuse.
The last relevant trace entry is:
mremap(0x204e44000, 0x38000, 0x39000, 0x1) -> 0x204e44000
EL0 data fault at 0x10 PC=0xff0002a604
(ESR=0x92000007 FSC=0x7) -> SIGSEGV/MAPERR
The requested tail starts at:
0x204e44000 + 0x38000 = 0x204e7c000
A GDB inspection found zeroes at 0x204e7c1f0, inside that page. The later
fault at 0x10 is consistent with a pointer in the adjacent allocation being
cleared before a field at offset 0x10 is read.
The equivalent local-index workload does not reproduce the guest fault in the
QEMU aarch64 reference lane.
Regression coverage
Add a portable test to tests/test-mremap.c that creates two separately
mapped, adjacent RW anonymous pages so the elfuse tracker may coalesce them.
The test should cover both outcomes:
- Without
MREMAP_MAYMOVE, growth fails with ENOMEM and both mappings keep
their contents.
- With
MREMAP_MAYMOVE, the source moves, its contents are preserved, its
extension is zero, and the adjacent mapping keeps its contents.
Run the test in both the elfuse aarch64 and QEMU aarch64 matrix lanes.
Acceptance criteria
- The minimal reproducer exits with status 0.
- The adjacent mapping remains unchanged.
- Growth without
MREMAP_MAYMOVE returns ENOMEM without modifying either
mapping.
- The local-index
apk --simulate reproducer no longer exits with status 139.
- Existing mremap tests and the elfuse and QEMU aarch64 matrix lanes pass.
Summary
mremap(..., MREMAP_MAYMOVE)can incorrectly grow a mapping in place when therequested tail is occupied by an adjacent anonymous mapping. The two mappings
must have compatible metadata so the region tracker coalesces them.
The syscall returns the original address as a successful result and zeroes the
occupied tail. This corrupts the adjacent mapping. The same path can also
publish overlapping region records after the byte corruption.
This was found through an Alpine
apk --simulateworkload, then reduced to asmall static aarch64 guest binary.
Environment
0a633684faa5c6de252b378f6da914230adc3da2build/elfuseSHA-256:63879eefead430475e411c7c2cf8adde8dacc9a1439a6c793336c6d77d049526aarch64-linux-gnu-gcc, static binaryMinimal reproducer
Save as
/tmp/mremap-adjacent.c:Compile and run:
aarch64-linux-gnu-gcc -static -O2 \ -o /tmp/mremap-adjacent /tmp/mremap-adjacent.c build/elfuse --timeout 0 /tmp/mremap-adjacent printf 'status=%s\n' "$?"Expected result
The second page is occupied, so the source cannot grow in place.
MREMAP_MAYMOVEpermits relocation. The call should return a differentaddress, preserve the first page, zero the new page at the destination, and
leave the adjacent mapping unchanged.
An equivalent call without
MREMAP_MAYMOVEshould fail withENOMEMandleave both mappings unchanged.
Linux enforces this in
vrm_can_expand_in_place(): the bytes from the sourceaddress to the end of its VMA must equal
old_len. A VMA suffix beyond therequested source range prevents in-place expansion.
Reference:
mm/mremap.cActual result
elfuse reports successful in-place growth and clears the adjacent mapping.
The result is identical in quiet and verbose modes. It is also identical with
ELFUSE_DISABLE_TLBI_RANGE=1.Root cause
guest_region_add_ex_owned_gpa()assigns compatible adjacent anonymousmappings the same
vma_id, then merges them into one tracker record. This isintentional and matches the fact that Linux may merge compatible VMAs.
The in-place growth branch in
sys_mremap()scans the requested tail foroverlap. It skips any overlapping region whose
startequalsold_off:That test assumes the source tracker record ends at
old_off + old_size. Acoalesced record can extend beyond that boundary and contain the adjacent
mapping. The branch therefore treats an occupied tail as free.
The successful branch then executes:
This clears live bytes in the adjacent mapping. Removing the old source range
and adding the enlarged region can then overlap the suffix retained from the
coalesced record, violating the region table's non-overlap invariant.
The existing
test_grow_no_maymove()case does not cover this shape. Itsblocker uses
PROT_READspecifically to prevent region coalescing with the RWsource.
Proposed fix
Treat any tracked overlap with
[grow_off, grow_off + grow_len)as occupied.The source range ends at
grow_off, so no source region needs an exception inthe tail check.
mem.calready providesregion_range_overlaps()for thisquery:
The existing control flow can then relocate when
MREMAP_MAYMOVEis presentor return
ENOMEMwhen it is absent. Region coalescing does not need to bedisabled.
Real-world impact
The original reproducer is Alpine 3.21 aarch64
apk2.14.6 resolvinggitfrom local repository indexes with
--no-network --simulate. It consistentlyexits with status 139 under elfuse.
The last relevant trace entry is:
The requested tail starts at:
A GDB inspection found zeroes at
0x204e7c1f0, inside that page. The laterfault at
0x10is consistent with a pointer in the adjacent allocation beingcleared before a field at offset
0x10is read.The equivalent local-index workload does not reproduce the guest fault in the
QEMU aarch64 reference lane.
Regression coverage
Add a portable test to
tests/test-mremap.cthat creates two separatelymapped, adjacent RW anonymous pages so the elfuse tracker may coalesce them.
The test should cover both outcomes:
MREMAP_MAYMOVE, growth fails withENOMEMand both mappings keeptheir contents.
MREMAP_MAYMOVE, the source moves, its contents are preserved, itsextension is zero, and the adjacent mapping keeps its contents.
Run the test in both the elfuse aarch64 and QEMU aarch64 matrix lanes.
Acceptance criteria
MREMAP_MAYMOVEreturnsENOMEMwithout modifying eithermapping.
apk --simulatereproducer no longer exits with status 139.