Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/relinker/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,8 @@ if(BUILD_TESTING)
${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 sce_module_code_extent COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_module_code_extent.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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <map>
#include <span>
#include <string>
#include <string_view>
#include <unordered_set>

namespace Relinker {
Expand Down Expand Up @@ -103,6 +104,8 @@ std::set<Domain::VirtualAddress> CodeInstructionCollector::Collect(const std::ve
auto begin = header.MappedAddress;
for (const auto& interpreter : headers)
if (interpreter.Type == 3 && interpreter.MappedAddress >= begin && interpreter.MappedAddress < end) begin = interpreter.MappedAddress + std::min(interpreter.FileSize, end - interpreter.MappedAddress);
const std::string_view loaderPath("/libexec/ld-elf.so.1", 21);
if (begin == header.MappedAddress && end - begin > loaderPath.size() && std::equal(loaderPath.begin(), loaderPath.end(), segment.begin())) begin += loaderPath.size();
header.Offset += begin - header.MappedAddress;
header.MappedAddress = begin;
header.FileSize = end - begin;
Expand Down
11 changes: 9 additions & 2 deletions core/relinker/relinker/src/guest/GuestModuleBuilder.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#include <relinker/guest/GuestImage.hpp>
#include <relinker/analysis/SceCodeExtent.hpp>
#include <relinker/analysis/UnusedNidFilter/EhFrameReader.hpp>
#include <elfpatcher/general/GuestModuleWriter.hpp>
#include <codegen/IAmd64OnlyConverter.hpp>
#include <domain/ImportModule.hpp>
Expand Down Expand Up @@ -304,9 +306,14 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
image.Trampolines = std::move(converted.Trampolines);
image.Bytes = std::move(converted.Bytes);
}
const bool sceImage = std::any_of(image.Headers.begin(), image.Headers.end(), [](const Domain::ProgramHeader& header) { return header.Type == 0x61000000; });
const auto unwindFunctions = sceImage ? UnusedNidFilter::ReadExceptionFunctions(image.Bytes, image.Headers, {}, {}) : std::vector<UnusedNidFilter::StrictCodeRegion>{};
for (const auto& header : codeHeaders) {
const std::vector<std::uint8_t> code(image.Bytes.begin() + header.Offset, image.Bytes.begin() + header.Offset + header.FileSize);
for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, header.FileSize, Io::Utf8Path(image.SourcePath))) {
std::vector<std::uint8_t> code(image.Bytes.begin() + header.Offset, image.Bytes.begin() + header.Offset + header.FileSize);
if (sceImage)
if (const auto end = FindSceCodeEnd(code, header.MappedAddress, unwindFunctions, image.Got, image.Dynamic.RelaPltData.size()))
code.resize(*end - header.MappedAddress);
for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, code.size(), Io::Utf8Path(image.SourcePath))) {
if (offset > code.size() || code.size() - offset < 2u)
throw Domain::RelinkerException("Syscall trap patch exceeds guest executable segment", header.MappedAddress + offset);
image.Bytes[header.Offset + offset] = 0x0F;
Expand Down
160 changes: 160 additions & 0 deletions core/relinker/relinker/tests/test_sce_module_code_extent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
from pathlib import Path
import struct
import subprocess
import sys
import tempfile

from test_guest_symbol_names import importing_executable

PT_LOAD = 1
PT_DYNAMIC = 2
PT_INTERP = 3
PT_GNU_EH_FRAME = 0x6474E550
PT_SCE_DYNLIBDATA = 0x61000000
TEXT_OFFSET = 0x1000
TEXT_SIZE = 0x1000
DATA_OFFSET = 0x3000
DATA_ADDRESS = 0x2000
BLOCK_OFFSET = 0x3800
IMAGE_SIZE = 0x4000
INTERP = b"/libexec/ld-elf.so.1\x00"
FS_LOOKALIKE = b"\x64\x2d\x65\x6c\x66\x2e"
FUNCTION = 0x100
FUNCTION_SIZE = 0x10
PLT = 0x110
RODATA = 0x140
GOT = DATA_ADDRESS + 0x100
EH_FRAME_HEADER = 0x800
EH_FRAME = 0x840


def fixture(interp_address=True, rodata_address=True, interp_header=True):
image = bytearray(IMAGE_SIZE)
base = BLOCK_OFFSET
strtab, symtab, jmprel = BLOCK_OFFSET, BLOCK_OFFSET + 0x20, BLOCK_OFFSET + 0x100
tags = [
(1, 1),
(0x61000035, strtab - base), (0x61000037, 16),
(0x61000039, symtab - base), (0x6100003B, 24), (0x6100003F, 48),
(0x6100002F, jmprel - base), (0x61000031, 0), (0x61000033, 24),
(0x61000027, GOT), (0x61000029, jmprel - base), (0x6100002B, 7), (0x6100002D, 24),
(0, 0),
]
headers = [
(PT_LOAD, 5, TEXT_OFFSET, 0, TEXT_SIZE, TEXT_SIZE, 0x1000),
(PT_LOAD, 6, DATA_OFFSET, DATA_ADDRESS, 0x400, 0x400, 0x1000),
(PT_DYNAMIC, 6, DATA_OFFSET, DATA_ADDRESS, len(tags) * 16, len(tags) * 16, 8),
(PT_SCE_DYNLIBDATA, 4, BLOCK_OFFSET, 0, 0x800, 0, 0x10),
(PT_INTERP if interp_header else 0, 4, TEXT_OFFSET, 0, len(INTERP), len(INTERP), 1),
(PT_GNU_EH_FRAME, 4, TEXT_OFFSET + EH_FRAME_HEADER, EH_FRAME_HEADER, 20, 20, 4),
]
image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9)
struct.pack_into("<HHIQQQIHHHHHH", image, 16, 3, 62, 1, FUNCTION, 64, 0, 0, 64, 56, len(headers), 64, 0, 0)
for index, (kind, flags, offset, address, file_size, memory_size, alignment) in enumerate(headers):
struct.pack_into("<IIQQQQQQ", image, 64 + index * 56, kind, flags, offset, address, address, file_size, memory_size, alignment)
for index, tag in enumerate(tags):
struct.pack_into("<qQ", image, DATA_OFFSET + index * 16, *tag)
image[strtab:strtab + 16] = b"\x00lib.so\x00symbol\x00\x00"
struct.pack_into("<IBBHQQ", image, symtab + 24, 8, 0x12, 0, 0, 0, 0)
struct.pack_into("<QQq", image, jmprel, GOT + 24, (1 << 32) | 7, 0)

text = memoryview(image)[TEXT_OFFSET:TEXT_OFFSET + TEXT_SIZE]
text[:len(INTERP)] = INTERP
nop7 = b"\x0f\x1f\x80\x00\x00\x00\x00"
text[FUNCTION:FUNCTION + 7] = b"\x48\x8d\x3d" + struct.pack("<i", 10 - (FUNCTION + 7)) if interp_address else nop7
text[FUNCTION + 7:FUNCTION + 14] = b"\x48\x8d\x35" + struct.pack("<i", RODATA - (FUNCTION + 14)) if rodata_address else nop7
text[FUNCTION + 14:FUNCTION + 16] = b"\xc3\x90"
text[PLT:PLT + 2] = b"\xff\x35"
struct.pack_into("<i", text, PLT + 2, GOT + 8 - (PLT + 6))
text[PLT + 6:PLT + 8] = b"\xff\x25"
struct.pack_into("<i", text, PLT + 8, GOT + 16 - (PLT + 12))
text[PLT + 12:PLT + 16] = b"\x0f\x1f\x40\x00"
text[PLT + 16:PLT + 18] = b"\xff\x25"
struct.pack_into("<i", text, PLT + 18, GOT + 24 - (PLT + 22))
text[PLT + 22] = 0x68
struct.pack_into("<I", text, PLT + 23, 0)
text[PLT + 27] = 0xE9
struct.pack_into("<i", text, PLT + 28, PLT - (PLT + 32))
text[RODATA:RODATA + len(FS_LOOKALIKE)] = FS_LOOKALIKE

cie = EH_FRAME
cie_body = b"\x00\x00\x00\x00\x01zR\x00\x01\x78\x10\x01\x1b\x00\x00\x00"
struct.pack_into("<I", text, cie, len(cie_body))
text[cie + 4:cie + 4 + len(cie_body)] = cie_body
fde = cie + 4 + len(cie_body)
struct.pack_into("<IIiiB3x", text, fde, 16, fde + 4 - cie, FUNCTION - (fde + 8), FUNCTION_SIZE, 0)
struct.pack_into("<I", text, fde + 20, 0)
struct.pack_into("<BBBBiIii", text, EH_FRAME_HEADER, 1, 0x1B, 0x03, 0x3B,
EH_FRAME - (EH_FRAME_HEADER + 4), 1, FUNCTION - EH_FRAME_HEADER, fde - EH_FRAME_HEADER)
return image


def unwind_table(text, base, function, size, header, frame):
cie_body = b"\x00\x00\x00\x00\x01zR\x00\x01\x78\x10\x01\x1b\x00\x00\x00"
struct.pack_into("<I", text, frame - base, len(cie_body))
text[frame - base + 4:frame - base + 4 + len(cie_body)] = cie_body
fde = frame + 4 + len(cie_body)
struct.pack_into("<IIiiB3x", text, fde - base, 16, fde + 4 - frame, function - (fde + 8), size, 0)
struct.pack_into("<I", text, fde - base + 20, 0)
struct.pack_into("<BBBBiIii", text, header - base, 1, 0x1B, 0x03, 0x3B,
frame - (header + 4), 1, function - header, fde - header)


def sce_module():
image = bytearray(0xC00)
tags = [(0x61000035, 0), (0x61000037, 1), (0x61000039, 0x20), (0x6100003B, 24), (0x6100003F, 24), (0, 0)]
headers = [
(PT_LOAD, 5, 0x400, 0x1000, 0x100, 0x100, 0x100),
(PT_LOAD, 6, 0x600, 0x2000, 0x400, 0x400, 0x100),
(PT_DYNAMIC, 6, 0x600, 0x2000, len(tags) * 16, len(tags) * 16, 8),
(PT_SCE_DYNLIBDATA, 4, 0xA00, 0, 0x200, 0, 0x10),
(PT_GNU_EH_FRAME, 4, 0x480, 0x1080, 20, 20, 4),
]
image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9)
struct.pack_into("<HHIQQQIHHHHHH", image, 16, 3, 62, 1, 0x1000, 64, 0, 0, 64, 56, len(headers), 64, 0, 0)
for index, (kind, flags, offset, address, file_size, memory_size, alignment) in enumerate(headers):
struct.pack_into("<IIQQQQQQ", image, 64 + index * 56, kind, flags, offset, address, address, file_size, memory_size, alignment)
for index, tag in enumerate(tags):
struct.pack_into("<qQ", image, 0x600 + index * 16, *tag)
text = memoryview(image)[0x400:0x500]
text[0:FUNCTION_SIZE] = b"\xc3" + b"\x90" * (FUNCTION_SIZE - 1)
text[0x20:0x22] = b"\x0f\x05"
unwind_table(text, 0x1000, 0x1000, FUNCTION_SIZE, 0x1080, 0x10A0)
return image


def run_module(relinker, work, name, module):
case = work / name
(case / "sce_module").mkdir(parents=True)
(case / "input.elf").write_bytes(importing_executable([]))
(case / "sce_module" / "libExample.prx").write_bytes(module)
result = subprocess.run([str(relinker), "--windows", str(case / "input.elf"), str(case / "output.exe")],
capture_output=True, text=True, timeout=30)
assert result.returncode == 0 and (case / "app0" / "sce_module" / "libExample.prx.guest.prx").exists(), (
name, result.stdout, result.stderr)


def run(relinker, work, name, image, expected_error=None):
source = work / (name + ".elf")
output = work / (name + ".exe")
source.write_bytes(image)
result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", str(source), str(output)],
capture_output=True, text=True, timeout=20)
if expected_error is None:
assert result.returncode == 0 and output.exists(), (name, result.stdout, result.stderr)
else:
assert result.returncode == 2 and expected_error in result.stderr and not output.exists(), (
name, result.returncode, result.stdout, result.stderr)


def main():
relinker = Path(sys.argv[1]).resolve()
with tempfile.TemporaryDirectory(prefix="anyps5-sce-module-code-extent-") as directory:
work = Path(directory)
run(relinker, work, "sce-loader-path-without-interp-header", fixture(rodata_address=False, interp_header=False))
run_module(relinker, work, "sce-module-data-after-code", sce_module())
print("SCE module code extent tests passed")


if __name__ == "__main__":
main()
3 changes: 2 additions & 1 deletion docs/dev/debt/relinker.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
- 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.
- 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.
- In executables and guest modules with a `PT_SCE_DYNLIBDATA` segment, the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) of the Windows and macOS outputs follows code only from the end of `PT_INTERP` to the end of the PLT after the last function of the unwind table, the end the pipeline's scans above use; a guest module with PLT relocations and no PLT after its last unwind function fails as the executable does. Code after that PLT is not analyzed: a TLS access in it is not rewritten, an entry point or pointer into it is ignored, and a function symbol or init/fini array entry pointing into it fails the conversion.
- The [syscall scan of `sce_module` modules](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) with a `PT_SCE_DYNLIBDATA` segment stops at the same end of the PLT; code after it is not scanned.
- In executables and guest modules with a `PT_SCE_DYNLIBDATA` segment, the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) of the Windows and macOS outputs follows code only from the end of `PT_INTERP`, or of the `/libexec/ld-elf.so.1` string that begins the segment when no `PT_INTERP` covers it, as in OpenOrbis modules, to the end of the PLT after the last function of the unwind table, the end the pipeline's scans above use; a guest module with PLT relocations and no PLT after its last unwind function fails as the executable does. Code after that PLT is not analyzed: a TLS access in it is not rewritten, an entry point or pointer into it is ignored, and a function symbol or init/fini array entry pointing into it fails the conversion.
- A relocation against a dynamic symbol entry that is all zero, like the null symbol, takes the value 0 plus its addend, as the gABI defines for `STN_UNDEF` ([pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp)). The Linux output imports such an entry as an unnamed weak symbol, which the host loader resolves to 0; the Windows and macOS outputs write the value into the slot ([relocation builder](../../../core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp)). What the console's loader does with such an entry is not measured. OliSe Player (PPSA01153) has seven of them, and its code calls through those slots only when they are not 0 ([#4437](https://github.com/boykopovar/AnyPS5/issues/4437)).
- The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test.
- `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). Rosetta runs AVX2, FMA, BMI1/2, F16C and MOVBE but reports none of them through CPUID, so code that checks CPUID takes its baseline path. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB; the same value reaches RDTSCP's ECX, which is not converted. ADCX/ADOX fault under Rosetta and are not converted: compilers do not emit them, and code that checks CPUID sees no ADX. The conversions are covered by the converter tests; that the converted code runs under Rosetta was checked on an M5 Max only, outside this repository's CI.
Expand Down
Loading