Moving a PT_LOAD to the end of the file sets its p_align to
min(p_align, 0x1000), since the copy honours only the page. When that
segment carried the largest p_align, the output's largest PT_LOAD
alignment, which Linux uses to align the ET_DYN load bias, fell with
it. SeparateSharedLoadPages now returns the largest power-of-two
p_align above the page that it lowered, and WriteLayout aligns the
header block PT_LOAD to at least that value, as it already does for
the largest kept p_align.
The five Linux rel32 sites subtracted addresses and range-checked the
signed result, so two addresses more than 2^63 apart wrapped into a
small in-range displacement and was written verbatim. SignedDistance
now classifies both addresses by the 2^63 boundary, takes the
high-minus-low magnitude as a plain subtraction and rejects a distance
whose magnitude exceeds int64. The new linux_load_pages test places a
site at 0x500 and the body near 2^64: main wrote a wrapped rel32, the
change throws.
Crafted program headers can map a load segment so its page end sits at
2^63, which places the patcher's extra block above the boundary while
the original code sites stay below it. The trampoline, import trace and
guest module displacement math subtracted addresses as two int64 casts,
which overflows for such pairs before the rel32 range check runs.
Subtract in unsigned arithmetic and cast the true difference once, as
the entry stub builder already does.
The move path of SeparateSharedLoadPages padded the output byte by byte
to an offset congruent modulo max(p_align, 0x1000), so a p_align of
0x80000000 grew the output by 2 GiB and 0x8000000000000000 ended in
std::bad_alloc. A moved segment now starts at the next offset congruent
to its address modulo the page and its p_align becomes at most 0x1000,
since Linux maps every PT_LOAD from p_offset - (p_vaddr % 0x1000) and
reads p_align only to align the ET_DYN load bias. At a fixed base every
input PT_LOAD p_align is checked against the base before any segment is
moved, with the message RebaseProgramHeaders uses.
The Linux patcher kept a PT_LOAD whose p_offset and p_vaddr differ
modulo the 4 KiB page in place, so the output executable could not be
mapped: the kernel's elf_map passes p_offset minus the page offset of
p_vaddr to vm_mmap, which fails with EINVAL when that is not page
aligned. The gABI requires p_offset and p_vaddr to be congruent modulo
the page size. Such a segment is now copied to the end of the file at
an offset congruent to p_vaddr modulo max(p_align, 4 KiB), through the
path that already separates shared load pages. Congruent layouts keep
their exact bytes, and the Windows and macOS outputs are unchanged.
On Linux the kernel maps each PT_LOAD from the start of its first page.
When a segment begins inside the page that holds the end of the previous
segment's zero-filled memory, that page is mapped again from the file, and
the bss tail reads whatever bytes precede the segment in the file. Nuvio
Max'd 0.5.27 (PPSA99288) keeps a mutex there and crashes at launch.
A segment whose page would read wrong bytes is now appended to the output
with that page prefix rebuilt from the segments below it. Outputs whose
pages already read right are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>