fix(relinker): load a PT_SCE_RELRO segment that no PT_LOAD covers

PS4 executables keep their relocated read-only data, GOT and init/fini
arrays in a PT_SCE_RELRO segment outside every PT_LOAD. The segment filter
dropped it, so Windows and macOS output stopped at the first relocation
into it, and Linux output placed the relinker's extra block over its
addresses without an error. The ELF reader now reports such a segment as a
read-write PT_LOAD. A PT_SCE_RELRO inside a PT_LOAD keeps being skipped,
and one that partially overlaps a PT_LOAD throws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
saxopa 2026-10-12 16:54:13 +02:00
parent d592d54e11
commit d0502b449d
4 changed files with 94 additions and 0 deletions

View File

@ -465,6 +465,8 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_string_table_bounds.py $<TARGET_FILE:relinker>)
add_test(NAME sce_code_extent COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_code_extent.py $<TARGET_FILE:relinker>)
add_test(NAME sce_relro COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_relro.py $<TARGET_FILE:relinker>)
add_test(NAME call_registry COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_call_registry.py $<TARGET_FILE:relinker>)
add_test(NAME jmprel_table_bounds COMMAND ${Python3_EXECUTABLE}

View File

@ -130,6 +130,23 @@ std::vector<ProgramHeader> ElfReader::ReadProgramHeaders() const {
offset += header.ProgramHeaderEntrySize;
}
for (auto& relro : headers) {
if (relro.Type != PT_OS_RELRO || relro.MemorySize == 0) continue;
bool covered = false;
for (const auto& load : headers) {
if (load.Type != PT_LOAD || load.MemorySize == 0) continue;
const bool overlaps = relro.MappedAddress < load.MappedAddress ? load.MappedAddress - relro.MappedAddress < relro.MemorySize : relro.MappedAddress - load.MappedAddress < load.MemorySize;
if (!overlaps) continue;
if (relro.MappedAddress < load.MappedAddress || relro.MemorySize > load.MemorySize - (relro.MappedAddress - load.MappedAddress))
throw RelinkerException("PT_SCE_RELRO partially overlaps a PT_LOAD segment", relro.MappedAddress);
covered = true;
}
if (!covered) {
relro.Type = PT_LOAD;
relro.Flags = PF_R | PF_W;
}
}
return headers;
}

View File

@ -0,0 +1,74 @@
import os
from pathlib import Path
import struct
import subprocess
import sys
import tempfile
from test_linux_dynamic_strings import dynamic_strings, file_offset
from test_linux_load_alignment import loads
PT_LOAD = 1
PT_DYNAMIC = 2
PT_SCE_RELRO = 0x61000010
PT_SCE_VERSION = 0x6FFFFF01
def fixture(extra):
image = bytearray(0x3000)
image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9)
headers = [(PT_LOAD, 5, 0, 0, 0x1000, 0x1000, 0x1000), *extra]
tags = [(5, 0x600), (10, 1), (6, 0x620), (11, 24), (7, 0x700), (8, 24), (9, 24), (0, 0)]
headers.append((PT_DYNAMIC, 6, 0x400, 0x400, len(tags) * 16, len(tags) * 16, 8))
headers += [(PT_SCE_VERSION, 0, 0, 0, 0, 0, 1)] * (7 - len(headers))
struct.pack_into("<HHIQQQIHHHHHH", image, 16, 3, 62, 1, 0x200, 64, 0, 0, 64, 56, len(headers), 64, 0, 0)
for index, (kind, flags, offset, address, file_size, memory_size, align) in enumerate(headers):
struct.pack_into("<IIQQQQQQ", image, 64 + index * 56, kind, flags, offset, address, address, file_size, memory_size, align)
image[0x200:0x206] = b"\xff\x25\xfa\x0d\x00\x00"
image[0x210:0x216] = b"\xb8\x2a\x00\x00\x00\xc3"
for index, tag in enumerate(tags):
struct.pack_into("<qQ", image, 0x400 + index * 16, *tag)
struct.pack_into("<QQq", image, 0x700, 0x1000, 8, 0x210)
return image
def relink(relinker, work, name, image, options):
source = work / f"{name}.elf"
output = work / f"{name}.out"
source.write_bytes(image)
result = subprocess.run([str(relinker), "--skip-sce-module", *options, str(source), str(output)],
capture_output=True, text=True, timeout=20)
return result, output
def main():
relinker = Path(sys.argv[1]).resolve()
standalone = [(PT_SCE_RELRO, 4, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000)]
covered = [(PT_LOAD, 6, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000), (PT_SCE_RELRO, 4, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000)]
with tempfile.TemporaryDirectory(prefix="anyps5-sce-relro-") as directory:
work = Path(directory)
for name, extra in (("standalone", standalone), ("covered", covered)):
for target, options in (("linux", []), ("windows", ["--windows"]), ("macos", ["--macos"])):
result, output = relink(relinker, work, f"{name}-{target}", fixture(extra), options)
assert result.returncode == 0, (name, target, result.stdout, result.stderr)
if target == "linux":
data = output.read_bytes()
relro = [load for load in loads(data) if load[3] <= 0x1000 < load[3] + load[6]]
assert len(relro) == 1 and relro[0][1] == 6 and relro[0][3] == 0x1000 and relro[0][5:7] == (0x1000, 0x1000), (name, relro)
tags, _ = dynamic_strings(data)
start = file_offset(data, loads(data), tags[7])
assert [struct.unpack_from("<QQq", data, offset) for offset in range(start, start + tags[8], 24)] == [(0x1000, 8, 0x210)], name
if target == "windows" and os.name == "nt":
run = subprocess.run([str(output)], capture_output=True, timeout=20)
assert run.returncode == 42, (name, run.returncode, run.stderr)
for name, load in (("tail", (PT_LOAD, 6, 0x1000, 0x1000, 0x800, 0x800, 0x1000)),
("head", (PT_LOAD, 6, 0x1800, 0x1800, 0x800, 0x800, 0x1000))):
for options in ([], ["--windows"]):
result, output = relink(relinker, work, f"partial-{name}", fixture([load, standalone[0]]), options)
assert result.returncode == 2 and "PT_SCE_RELRO partially overlaps a PT_LOAD segment" in result.stderr, (name, result.stderr)
assert not output.exists()
print("PT_SCE_RELRO tests passed")
if __name__ == "__main__":
main()

View File

@ -579,6 +579,7 @@ Throughout the project, every function at every stage either **does exactly what
- [Ordered not-equal, less-than and greater-than compares](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual`, and `FPOrdLessThan32` and `FPOrdGreaterThan32` as their ordered forms, each with both operands checked for NaN on their bits; the results are the same for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of not-equal feeds a ballot on a 32-wide subgroup (#825), and so does a less-than or greater-than against an infinity the driver sees as a constant (#3536), presumably rewritten into a not-equal by its compiler
- [sce::Job::JobManager](../../core/libs/prx/libSceJobManager/Export.cpp) (libSceJobManager) is written from the disassembly of the copies PPSA01474 and PPSA36082 ship in `sce_module`, and `guest_job_manager_tests` passes against both of those modules as well: the memory size formula, the error codes, sync point ordering, the per-sequence limits and the priority order are the original's. Only CPU sequences exist: `initialize` and `calculateRequiredMemorySize` with compute sequences, and both compute dispatches, are not implemented, and `createCompute` and `destroyCompute` return the errors the original returns when no compute sequence was configured. The manager's state lives on the host heap, with only the block header written to the title's memory; the original keeps everything in that block, and a block smaller than 0x2d8 bytes throws here. The worker sleep policy, the spin count and the persistent thread count are validated and stored but change nothing, a factory priority above 1 throws, `shutdown` throws while a thread is blocked in a sequence wait, and using a sequence after its `destroy` throws. `initialize` returns 0 where the original returns the result of its last import (`BG26hBGiNlw`, not resolved to a name), the `sceRazorCpuJobManager*` profiler calls are not made, and a worker thread that cannot be created or configured throws where the original does not check. The order of the sequence and factory virtual functions is read from the modules' vtables only, since no title that calls them has been run
- The length-changing path of the [instruction rewriter](../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length.
- A `PT_SCE_RELRO` segment that no `PT_LOAD` covers, the PS4 layout, is [loaded](../../core/relinker/relinker/src/parsing/ElfReader.cpp) as a read-write `PT_LOAD` and stays writable after relocation; FreeBSD rtld makes `PT_GNU_RELRO` read-only once relocation is done, and what the console does with `PT_SCE_RELRO` is not checked. The `unused-filter=1` and `unused-filter=2` readers parse the program headers themselves and do not load it.
- In executables with a `PT_SCE_DYNLIBDATA` segment, the PS4 layout, the executable segment also holds read-only data after the code (CUSA00900: 37 MiB after its last function). The [relinker pipeline](../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp) therefore scans for syscalls and import call sites only up to the end of the PLT that follows the last function of the unwind table; code after the PLT without an unwind entry is not scanned. `--to-intel` and the `unused-filter=1` and `unused-filter=2` analyses are not limited this way.
- [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured.
- [DB_SHADER_CONTROL.STENCIL_TEST_VAL_EXPORT_ENABLE](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) becomes `FragStencilRefEXT`, taken from the second MRTZ channel of the 32_GR and 32_ABGR formats as Mesa's AMD drivers write it ([`emit_ps_mrtz_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L194)); the console was not measured. The host value is the test value of both faces and what `REPLACE_TEST` writes, so a stencil operation that writes all ones or the operation value throws with it, as do a stencil clear draw, the UINT16_ABGR layout, `STENCIL_OP_VAL_EXPORT_ENABLE` and a device without `VK_EXT_shader_stencil_export` (NVIDIA's driver 610.57 offers none).