From 123ba70839dbdb8888ea64223262a9582254e535 Mon Sep 17 00:00:00 2001 From: Rookie <175737347+rooklz@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:25:33 +0100 Subject: [PATCH] fix(relinker): load Linux executables at 0x400000 by default --- .../elfpatcher/linux/LinuxElfPatcher.hpp | 2 + core/relinker/main.cpp | 5 +- .../include/relinker/patching/GamePatch.hpp | 3 +- .../include/relinker/pipeline/Conversion.hpp | 1 + .../relinker/src/patching/GamePatch.cpp | 31 +++++-- .../relinker/src/pipeline/Conversion.cpp | 9 +- .../relinker/tests/ConversionTests.cpp | 16 +++- core/relinker/relinker/tests/ElfFixture.hpp | 16 ++-- .../relinker/tests/GamePatchTests.cpp | 22 ++++- .../tests/test_executable_tls_relocations.py | 6 +- .../relinker/tests/test_game_patches.py | 1 + .../tests/test_linux_eh_frame_header.py | 8 +- .../relinker/tests/test_linux_entry_argv.py | 3 +- .../relinker/tests/test_linux_fixed_base.py | 82 +++++++++++++------ .../relinker/tests/test_linux_gnu_segments.py | 11 +-- .../tests/test_linux_program_header_order.py | 7 +- .../relinker/tests/test_none_relocations.py | 11 ++- .../tests/test_null_symbol_relocations.py | 5 +- .../relinker/tests/test_optional_rela.py | 5 +- .../tests/test_overlapping_relocations.py | 5 +- .../relinker/relinker/tests/test_sce_relro.py | 7 +- docs/dev/debt/relinker.md | 1 + docs/user/USAGE.md | 14 ++-- 23 files changed, 191 insertions(+), 80 deletions(-) diff --git a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp index c24b3114f0..58aebbcd67 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp @@ -14,6 +14,8 @@ namespace Elfpatcher::Linux { +inline constexpr std::uint64_t DefaultFixedBase = 0x400000; + class LinuxElfPatcher : public IElfPatcher { public: LinuxElfPatcher( diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 33240459b0..9e328dd454 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -35,6 +35,7 @@ struct ConvertedExecutable { std::vector Bytes; std::string Diagnostics; std::vector AppliedPatches; + std::optional LinuxBase; }; ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, const std::string& absPath, const bool mainExecutable, const std::string& targetName, const std::vector& patches, Cli::ConsoleProgress& progress) { @@ -73,7 +74,7 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, progress.IncludeFile(Io::Utf8Path(input.OutputPath)); for (const auto& module : input.Modules) progress.IncludeFile(Io::Utf8Path(module.Path)); auto converted = Relinker::Conversion(progress).Convert(std::move(input), options); - return {inputPath, absPath, std::move(converted.Analysis), std::move(converted.Modules), std::move(converted.Executable), std::move(converted.WindowsDiagnostics), std::move(converted.AppliedPatches)}; + return {inputPath, absPath, std::move(converted.Analysis), std::move(converted.Modules), std::move(converted.Executable), std::move(converted.WindowsDiagnostics), std::move(converted.AppliedPatches), converted.LinuxBase}; } class OutputIdentityDirectory { @@ -332,7 +333,7 @@ int Run(const int argc, char* argv[]) { std::filesystem::path manifestPath; std::string manifestText; if (!appliedPatches.empty()) manifestPath = Io::NativePath(absPath).parent_path() / Io::NativePath(Io::Utf8Path(Io::NativePath(absPath).stem()) + Relinker::GamePatch::ManifestSuffix); - if (!manifestPath.empty()) manifestText = Relinker::GamePatch::WriteManifest(appliedPatches, mainTarget, Io::Utf8Path(patchFile)); + if (!manifestPath.empty()) manifestText = Relinker::GamePatch::WriteManifest(appliedPatches, mainTarget, Io::Utf8Path(patchFile), executables.front().LinuxBase); const Elfpatcher::Linux::LinuxDesktopEntryWriter desktopEntryWriter; const auto desktopEntry = args.toWindows || args.toMacos ? std::nullopt : desktopEntryWriter.Prepare(Io::NativePath(args.inputPath).parent_path() / "sce_sys", Io::NativePath(absPath)); diff --git a/core/relinker/relinker/include/relinker/patching/GamePatch.hpp b/core/relinker/relinker/include/relinker/patching/GamePatch.hpp index d893a7015e..6b76048dfb 100644 --- a/core/relinker/relinker/include/relinker/patching/GamePatch.hpp +++ b/core/relinker/relinker/include/relinker/patching/GamePatch.hpp @@ -40,6 +40,7 @@ struct PatchEntry { struct PatchFile { std::vector Entries; std::optional MainTarget; + std::optional LinuxBase; }; struct PatchedRange { @@ -114,7 +115,7 @@ void MatchTargets(const std::vector& entries, const std::vector AggregateManifest(const std::vector>& records); -std::string WriteManifest(const std::vector& patches, const std::string& mainTarget, const std::string& configPath); +std::string WriteManifest(const std::vector& patches, const std::string& mainTarget, const std::string& configPath, std::optional linuxBase = std::nullopt); std::vector VerifyPatches(const PatchFile& file, const std::vector& executables, const std::vector& modules, bool outputMode); diff --git a/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp index 194262b78e..6998e95873 100644 --- a/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp +++ b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp @@ -46,6 +46,7 @@ struct ConversionOutput { RelinkResult Analysis; std::string WindowsDiagnostics; std::vector AppliedPatches; + std::optional LinuxBase; }; class Conversion { diff --git a/core/relinker/relinker/src/patching/GamePatch.cpp b/core/relinker/relinker/src/patching/GamePatch.cpp index 05d334868a..403264c79b 100644 --- a/core/relinker/relinker/src/patching/GamePatch.cpp +++ b/core/relinker/relinker/src/patching/GamePatch.cpp @@ -33,8 +33,8 @@ constexpr std::array kIntegerTypes{{ {"i8", 8, true}, {"i16", 16, true}, {"i32", 32, true}, {"i64", 64, true}, }}; -constexpr std::array kIgnoredTopLevelKeys{"relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "settings", "settings_sha256"}; -constexpr std::array kReservedNames{"format", "relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "settings", "settings_sha256"}; +constexpr std::array kIgnoredTopLevelKeys{"relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "linux_base", "settings", "settings_sha256"}; +constexpr std::array kReservedNames{"format", "relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "linux_base", "settings", "settings_sha256"}; constexpr std::array kEntryKeys{"name", "target", "va", "type", "expected", "value", "min", "max", "align", "description"}; std::string HexValue(const std::uint64_t value) { @@ -300,8 +300,8 @@ void RejectEntryOverlaps(const std::vector& entries, const std::vect } } -bool IsElfDyn(const std::vector& bytes) { - return bytes.size() >= 18 && bytes[0] == 0x7f && bytes[1] == 'E' && bytes[2] == 'L' && bytes[3] == 'F' && bytes[16] == Elfpatcher::ET_DYN && bytes[17] == 0; +bool IsElfOfType(const std::vector& bytes, const std::uint16_t type) { + return bytes.size() >= 18 && bytes[0] == 0x7f && bytes[1] == 'E' && bytes[2] == 'L' && bytes[3] == 'F' && bytes[16] == (type & 0xff) && bytes[17] == (type >> 8); } VerifyResult VerifyOne(const PatchEntry& entry, const EntryRange& resolved, const std::vector& bytes, const bool outputMode) { @@ -410,6 +410,10 @@ PatchFile ParsePatchFile(const std::string_view text) { if (!mainTarget->IsString || mainTarget->Text.empty()) FailLine(mainTarget->Line, "main_target must be a nonempty string", "main_target"); file.MainTarget = mainTarget->Text; } + if (const auto* linuxBase = Find(top, "linux_base")) { + if (linuxBase->IsString || linuxBase->Number.Negative) FailLine(linuxBase->Line, "linux_base must be a non-negative integer", "linux_base"); + file.LinuxBase = linuxBase->Number.Magnitude; + } return file; } @@ -505,7 +509,7 @@ std::vector AggregateManifest(const std::vector& patches, const std::string& mainTarget, const std::string& configPath) { +std::string WriteManifest(const std::vector& patches, const std::string& mainTarget, const std::string& configPath, const std::optional linuxBase) { const auto holdable = [](const std::string& text) { return std::none_of(text.begin(), text.end(), [](const char character) { return character == '"' || character == '\\' || (static_cast(character) < 0x20 && character != '\t') || character == 0x7f; @@ -519,6 +523,7 @@ std::string WriteManifest(const std::vector& patches, const std::s if (!config.empty() && !holdable(config)) config = std::filesystem::path(Io::NativePath(configPath)).filename().generic_string(); std::ostringstream manifest; manifest << "format = 1\nmain_target = " << quoted(mainTarget, "main_target") << '\n'; + if (linuxBase) manifest << "linux_base = " << HexValue(*linuxBase) << '\n'; if (!config.empty()) manifest << "config = " << quoted(config, "config") << '\n'; for (const auto& patch : patches) { manifest << "\n[[patch]]\n"; @@ -553,8 +558,12 @@ std::vector VerifyPatches(const PatchFile& file, const std::vector for (std::size_t index = 0; index < executables.size(); ++index) for (std::size_t other = index + 1; other < executables.size(); ++other) if (executables[index].Name == executables[other].Name) throw Domain::RelinkerException("ambiguous patch target: " + executables[index].Name); - for (const auto& executable : executables) - if (!IsElfDyn(executable.Bytes)) throw Domain::RelinkerException(executable.Name + " is not an ET_DYN Linux ELF output (ET_EXEC from --linux-fixed-base, PE and Mach-O outputs are not supported): verify the input instead"); + for (const auto& executable : executables) { + const bool fixed = IsElfOfType(executable.Bytes, Elfpatcher::ET_EXEC); + if (!fixed && !IsElfOfType(executable.Bytes, Elfpatcher::ET_DYN)) throw Domain::RelinkerException(executable.Name + " is not a Linux ELF output (PE and Mach-O outputs are not supported): verify the input instead"); + if (fixed && !file.LinuxBase) throw Domain::RelinkerException(executable.Name + " is an ET_EXEC Linux ELF output, but the patch file has no linux_base: verify the manifest written by its conversion"); + if (!fixed && file.LinuxBase) throw Domain::RelinkerException(executable.Name + " is an ET_DYN Linux ELF output, but the patch file has linux_base = " + HexValue(*file.LinuxBase) + ": verify the output of the conversion that wrote it"); + } } else { std::vector names; for (const auto& executable : executables) names.push_back(executable.Name); @@ -583,13 +592,19 @@ std::vector VerifyPatches(const PatchFile& file, const std::vector } continue; } + const std::uint64_t base = outputMode && IsElfOfType(target->Bytes, Elfpatcher::ET_EXEC) ? *file.LinuxBase : 0; std::vector own; - for (const auto other : indices) own.push_back(file.Entries[other]); + for (const auto other : indices) { + own.push_back(file.Entries[other]); + if (own.back().VirtualAddress > std::numeric_limits::max() - base) throw Domain::RelinkerException(Describe(own.back()) + " lies outside the address space at linux_base = " + HexValue(base)); + own.back().VirtualAddress += base; + } const ElfReader reader(target->Bytes); const auto resolved = ResolveEach(reader, own); RejectEntryOverlaps(own, resolved); for (std::size_t position = 0; position < indices.size(); ++position) { results[indices[position]] = VerifyOne(own[position], resolved[position], target->Bytes, outputMode); + results[indices[position]].VirtualAddress = file.Entries[indices[position]].VirtualAddress; done[indices[position]] = true; } } diff --git a/core/relinker/relinker/src/pipeline/Conversion.cpp b/core/relinker/relinker/src/pipeline/Conversion.cpp index ad008fa002..1cf1879466 100644 --- a/core/relinker/relinker/src/pipeline/Conversion.cpp +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -125,6 +125,11 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti auto byteWriter = std::make_shared(); + std::optional linuxBase; + if (!windows && !macos) + linuxBase = options.LinuxFixedBase || options.LazyBinding ? options.LinuxFixedBase : std::optional(Elfpatcher::Linux::DefaultFixedBase); + if (!windows && !macos && options.LazyBinding) + inputProgress.Message("Warning: with --lazy-binding the Linux executable stays position independent and is not placed at 0x400000; a title whose own sceLibcMspaceCreate checks the 40-bit address window stops at start-up\n"); std::shared_ptr patcher; if (macos) { patcher = std::make_shared(options.MacosFixedBase, [&](std::string_view text) { inputProgress.Message(text); }); @@ -139,7 +144,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti ), std::make_shared(byteWriter), byteWriter, - options.LinuxFixedBase, + linuxBase, options.TraceImports ); } @@ -157,7 +162,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti std::string diagnostics; if (windows) diagnostics = std::static_pointer_cast(patcher)->GetDiagnostics(); inputProgress.Report({Domain::ProgressStage::Executable, Io::Utf8Path(input.InputPath), 1, 1}); - return {std::move(executableBytes), std::move(guestArtifacts), std::move(result), std::move(diagnostics), std::move(appliedPatches)}; + return {std::move(executableBytes), std::move(guestArtifacts), std::move(result), std::move(diagnostics), std::move(appliedPatches), linuxBase}; } catch (Domain::RelinkerException& error) { if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(input.InputPath); throw; diff --git a/core/relinker/relinker/tests/ConversionTests.cpp b/core/relinker/relinker/tests/ConversionTests.cpp index 468921cf4f..3357892f5e 100644 --- a/core/relinker/relinker/tests/ConversionTests.cpp +++ b/core/relinker/relinker/tests/ConversionTests.cpp @@ -132,14 +132,22 @@ void fixedBase() { const Relinker::Conversion converter(progress); auto source = input(); source.Modules.push_back({"virtual/title/sce_module/plugin.prx", fixture()}); - const auto plain = converter.Convert(source, {}); + const auto standard = converter.Convert(source, {}); + Relinker::ConversionOptions lazy; + lazy.LazyBinding = true; + const auto independent = converter.Convert(input(), lazy); Relinker::ConversionOptions options; options.LinuxFixedBase = 0x400000000; const auto fixed = converter.Convert(source, options); - require(Io::ReadU16(plain.Executable, 16) == 3 && Io::ReadU16(fixed.Executable, 16) == 2, "Fixed base did not select ET_EXEC"); - require(Io::ReadU64(fixed.Executable, 24) == Io::ReadU64(plain.Executable, 24) + *options.LinuxFixedBase, "Fixed entry was not rebased above 4 GiB"); - require(fixed.Modules.front().Bytes == plain.Modules.front().Bytes, "Executable fixed base changed a bundled module"); + require(Io::ReadU16(standard.Executable, 16) == 2 && Io::ReadU16(fixed.Executable, 16) == 2, "Fixed base did not select ET_EXEC"); + require(Io::ReadU16(independent.Executable, 16) == 3, "Lazy binding did not keep ET_DYN"); + require(standard.LinuxBase == 0x400000 && fixed.LinuxBase == *options.LinuxFixedBase && !independent.LinuxBase, "Conversion reported the wrong Linux base"); + require(Io::ReadU64(fixed.Executable, 24) == Io::ReadU64(standard.Executable, 24) + *options.LinuxFixedBase - 0x400000, "Fixed entry was not rebased above 4 GiB"); + require(fixed.Modules.front().Bytes == standard.Modules.front().Bytes, "Executable fixed base changed a bundled module"); for (const auto target : {Relinker::TargetSystem::Windows, Relinker::TargetSystem::MacOs}) { + Relinker::ConversionOptions other; + other.Target = target; + require(!converter.Convert(source, other).LinuxBase, "A non-Linux conversion reported a Linux base"); options.Target = target; reject([&] { converter.Convert(source, options); }); } diff --git a/core/relinker/relinker/tests/ElfFixture.hpp b/core/relinker/relinker/tests/ElfFixture.hpp index daa1f322cb..2ed1a52cae 100644 --- a/core/relinker/relinker/tests/ElfFixture.hpp +++ b/core/relinker/relinker/tests/ElfFixture.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -117,12 +118,17 @@ public: const auto phOffset = Read(elf, 32); const auto phSize = Read(elf, 54); const auto phCount = Read(elf, 56); + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + linkBase = std::numeric_limits::max(); + std::uint64_t last = 0; for (std::uint16_t index = 0; index < phCount; ++index) { const auto header = phOffset + index * phSize; if (Read(elf, header) != 1) continue; - size = std::max(size, Read(elf, header + 16) + Read(elf, header + 40)); + linkBase = std::min(linkBase, Read(elf, header + 16) / page * page); + last = std::max(last, Read(elf, header + 16) + Read(elf, header + 40)); } - if (size == 0) throw std::runtime_error("ELF has no PT_LOAD segment"); + if (last == 0) throw std::runtime_error("ELF has no PT_LOAD segment"); + size = static_cast(last - linkBase); base = mmap(nullptr, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (base == MAP_FAILED) throw std::runtime_error("Cannot map executable memory for the ELF image"); for (std::uint16_t index = 0; index < phCount; ++index) { @@ -131,15 +137,14 @@ public: const auto offset = Read(elf, header + 8); const auto fileSize = Read(elf, header + 32); if (offset > elf.size() || fileSize > elf.size() - offset) throw std::runtime_error("PT_LOAD exceeds the ELF file"); - std::memcpy(static_cast(base) + Read(elf, header + 16), elf.data() + offset, fileSize); + std::memcpy(static_cast(base) + (Read(elf, header + 16) - linkBase), elf.data() + offset, fileSize); } if (mprotect(base, size, PROT_NONE) != 0) throw std::runtime_error("Cannot protect the ELF image"); - const auto page = static_cast(sysconf(_SC_PAGESIZE)); for (std::uint16_t index = 0; index < phCount; ++index) { const auto header = phOffset + index * phSize; if (Read(elf, header) != 1) continue; const auto flags = Read(elf, header + 4); - const auto vaddr = Read(elf, header + 16); + const auto vaddr = Read(elf, header + 16) - linkBase; const auto start = vaddr / page * page; const auto end = (vaddr + Read(elf, header + 40) + page - 1) / page * page; const int protection = ((flags & 4) != 0 ? PROT_READ : 0) | ((flags & 2) != 0 ? PROT_WRITE : 0) | ((flags & 1) != 0 ? PROT_EXEC : 0); @@ -163,6 +168,7 @@ public: private: void* base = nullptr; std::size_t size = 0; + std::uint64_t linkBase = 0; }; } // namespace RelinkerTests diff --git a/core/relinker/relinker/tests/GamePatchTests.cpp b/core/relinker/relinker/tests/GamePatchTests.cpp index f81bc1f2fe..bde267682d 100644 --- a/core/relinker/relinker/tests/GamePatchTests.cpp +++ b/core/relinker/relinker/tests/GamePatchTests.cpp @@ -385,7 +385,20 @@ void verify() { rejects([&] { VerifyPatches(ParsePatchFile(header("[[patch]]\ntarget = \"main.elf\"\nva = 0x1000\nexpected = \"00\"\nvalue = \"01\"\n")), outputs, {}, true); }, "no main_target"); auto exec = twoSegmentFixture(); Io::WriteU16(exec, 16, 2); - rejects([&] { VerifyPatches(file, {{"main.elf", exec}}, {}, true); }, "verify the input instead"); + rejects([&] { VerifyPatches(file, {{"main.elf", exec}}, {}, true); }, "has no linux_base"); + const auto based = ParsePatchFile(header("main_target = \"main.elf\"\nlinux_base = 0x400000\n[[patch]]\nname = \"one\"\ntarget = \"main.elf\"\nva = 0x1000\nexpected = \"0001\"\nvalue = \"0908\"\n" + "[[patch]]\ntarget = \"main.elf\"\nva = 0x1100\nexpected = \"ff\"\nvalue = \"01\"\n")); + require(based.LinuxBase == 0x400000, "linux_base was not read"); + auto rebased = patchedBytes; + Io::WriteU16(rebased, 16, 2); + for (const std::size_t field : {64 + 16, 64 + 24, 120 + 16, 120 + 24}) Io::WriteU64(rebased, field, Io::ReadU64(rebased, field) + 0x400000); + output = VerifyPatches(based, {{"main.elf", rebased}}, {}, true); + require(output.size() == 2 && output[0].Status == VerifyStatus::Applied && output[1].Status == VerifyStatus::Applied, "fixed-base output applied"); + require(output[0].VirtualAddress == 0x1000 && output[0].Offset == 0x1000 && output[1].Offset == 0x2000, "fixed-base output reported the rebased address"); + output = VerifyPatches(based, {{"main.elf", exec}}, {}, true); + require(output[0].Status == VerifyStatus::Mismatch && output[0].Detail.find("PT_LOAD") != std::string::npos, "linux_base was not applied"); + rejects([&] { VerifyPatches(based, outputs, {}, true); }, "has linux_base = 0x400000"); + rejects([&] { VerifyPatches(ParsePatchFile(header("main_target = \"main.elf\"\nlinux_base = 0x400000\n[[patch]]\ntarget = \"main.elf\"\nva = 0xffffffffffffff00\nexpected = \"00\"\nvalue = \"01\"\n")), {{"main.elf", rebased}}, {}, true); }, "lies outside the address space at linux_base"); rejects([&] { VerifyPatches(file, {{"main.elf", {'M', 'Z', 0x90, 0}}}, {}, true); }, "verify the input instead"); rejects([&] { VerifyPatches(file, {{"main.elf", {0xcf, 0xfa, 0xed, 0xfe, 7, 0, 0, 1}}}, {}, true); }, "verify the input instead"); rejects([&] { VerifyPatches(file, {{"main.elf", {}}}, {}, true); }, "verify the input instead"); @@ -424,10 +437,15 @@ void manifest() { require(parsed.Entries[1].Name.empty() && parsed.Entries[1].Target == "sce_module/libc.prx" && parsed.Entries[1].Description.empty(), "second entry"); require(text.find("name = \"alpha\"") != std::string::npos && text.find("main_target = \"main.elf\"") != std::string::npos, "manifest text"); require(WriteManifest(AggregateManifest({{plain}, {second}}), "main.elf", "") == text, "manifest is not deterministic"); + require(text.find("linux_base") == std::string::npos && !parsed.LinuxBase, "a manifest without a base wrote linux_base"); + const auto based = WriteManifest({plain}, "main.elf", "", 0x400000); + require(based.find("main_target = \"main.elf\"\nlinux_base = 0x400000\n") != std::string::npos && ParsePatchFile(based).LinuxBase == 0x400000, "linux_base did not round-trip"); + rejectsText(header("linux_base = \"0x400000\"\n"), 2, "linux_base must be a non-negative integer"); + rejectsText(header("linux_base = -1\n"), 2, "linux_base must be a non-negative integer"); } void reservedNames() { - for (const char* reserved : {"format", "relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "settings", "settings_sha256"}) + for (const char* reserved : {"format", "relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "linux_base", "settings", "settings_sha256"}) rejects([&] { ParsePatchFile(namedEntry(reserved)); }, "line 3: name is reserved"); ParsePatchFile(header("settings = \"a\"\nsettings_sha256 = \"b\"\n")); } diff --git a/core/relinker/relinker/tests/test_executable_tls_relocations.py b/core/relinker/relinker/tests/test_executable_tls_relocations.py index fd6ceff424..5ce918047b 100644 --- a/core/relinker/relinker/tests/test_executable_tls_relocations.py +++ b/core/relinker/relinker/tests/test_executable_tls_relocations.py @@ -20,6 +20,7 @@ PT_TLS = 7 DTPMOD64, DTPOFF64, TPOFF64, TPOFF32 = 16, 17, 18, 23 TLS_MEMSZ, TLS_ALIGN = 0x24, 16 TLS_BLOCK = 0x30 +DEFAULT_BASE = 0x400000 STRINGS = b"\0Tls\0Data\0ImportedTls\0" TLS_NAME, DATA_NAME, IMPORT_NAME = 1, 5, 10 @@ -107,8 +108,9 @@ def main(): elf = output.read_bytes() relocations = dynamic_relocations(elf) tls = sorted(relocation for relocation in relocations if relocation[1] in (DTPMOD64, DTPOFF64, TPOFF64)) - assert tls == [(0x1000, TPOFF64, "", 0, 4), (0x1008, DTPOFF64, "", 0, 8), (0x1010, TPOFF64, "", 0, 0x12), - (0x1018, DTPMOD64, "", 0, 0), (0x1020, TPOFF64, "ImportedTls", 6, 0)], relocations + assert tls == [(DEFAULT_BASE + 0x1000, TPOFF64, "", 0, 4), (DEFAULT_BASE + 0x1008, DTPOFF64, "", 0, 8), + (DEFAULT_BASE + 0x1010, TPOFF64, "", 0, 0x12), (DEFAULT_BASE + 0x1018, DTPMOD64, "", 0, 0), + (DEFAULT_BASE + 0x1020, TPOFF64, "ImportedTls", 6, 0)], relocations assert any(struct.unpack_from("= len(image), "The grown program header table must be moved past the input" assert "PT_GNU_EH_FRAME" not in result.stderr, result.stderr @@ -125,7 +127,7 @@ def main(): result, elf = relink(relinker, directory, "few-slots-" + str(with_frame), image) headers = program_headers(elf) assert len(headers) == (8 if with_frame else 7), headers - assert [header for header in headers if header[0] == PT_GNU_EH_FRAME] == ([EH_FRAME_HEADER] if with_frame else []), headers + assert [header for header in headers if header[0] == PT_GNU_EH_FRAME] == ([EH_FRAME_OUTPUT] if with_frame else []), headers assert struct.unpack_from("= len(image), "The grown program header table must be moved past the input" assert elf[64 + 5 * 56:64 + 6 * 56] == image[64 + 5 * 56:64 + 6 * 56], "Bytes after the input program header table changed" diff --git a/core/relinker/relinker/tests/test_linux_entry_argv.py b/core/relinker/relinker/tests/test_linux_entry_argv.py index 4dad8c6e07..87604a5743 100644 --- a/core/relinker/relinker/tests/test_linux_entry_argv.py +++ b/core/relinker/relinker/tests/test_linux_entry_argv.py @@ -12,6 +12,7 @@ from signal_run import run_without_core_dump from test_linux_load_alignment import PT_LOAD, PT_SCE_VERSION, fixture, loads ENTRY = 0x10 +DEFAULT_BASE = 0x400000 ARGV_CHECK = bytes.fromhex( "833f02" "751e" "48837f0800" "7417" @@ -79,7 +80,7 @@ def main(): capture_output=True, text=True, timeout=20) if error is None: assert result.returncode == 0, (address, result.stdout, result.stderr) - assert entry_call_target(far.read_bytes()) == ENTRY, hex(address) + assert entry_call_target(far.read_bytes()) == DEFAULT_BASE + ENTRY, hex(address) else: assert result.returncode == 2 and error in result.stderr and not far.exists(), (address, result) print("Linux entry argv test passed") diff --git a/core/relinker/relinker/tests/test_linux_fixed_base.py b/core/relinker/relinker/tests/test_linux_fixed_base.py index 84244f4533..ee31ad59ef 100644 --- a/core/relinker/relinker/tests/test_linux_fixed_base.py +++ b/core/relinker/relinker/tests/test_linux_fixed_base.py @@ -1,4 +1,4 @@ -"""Check that --linux-fixed-base places the executable at the requested address and rebases every address it holds.""" +"""Check that the Linux executable is placed at 0x400000 by default or at the --linux-fixed-base address, with every address it holds rebased, that --lazy-binding keeps it position independent and warns, and that the largest image the relinker accepts ends below the guest arena.""" from pathlib import Path import platform @@ -16,9 +16,13 @@ PT_LOAD = 1 PT_DYNAMIC = 2 PT_GNU_STACK = 0x6474E551 BASE = 0x400000 +HIGH_BASE = 0x400000000 +SHIFT = HIGH_BASE - BASE ENTRY = 0x10 SLOT = 0x800 ADDRESS_TAGS = {3, 4, 5, 6, 7, 23} +GUEST_ARENA_START = 0x200000000 +LAZY_WARNING = "with --lazy-binding the Linux executable stays position independent" def check_code(base): @@ -86,58 +90,83 @@ def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-fixed-base-") as directory: work = Path(directory) - result, plain_path = relink(relinker, work, "plain", fixture()) + result, fixed_path = relink(relinker, work, "default", fixture()) assert result.returncode == 0, (result.stdout, result.stderr) - result, fixed_path = relink(relinker, work, "fixed", fixture(), "--linux-fixed-base", hex(BASE)) + assert LAZY_WARNING not in result.stdout + result.stderr, (result.stdout, result.stderr) + result, explicit_path = relink(relinker, work, "explicit", fixture(), "--linux-fixed-base", hex(BASE)) assert result.returncode == 0, (result.stdout, result.stderr) - plain = plain_path.read_bytes() + result, high_path = relink(relinker, work, "high", fixture(), "--linux-fixed-base", hex(HIGH_BASE)) + assert result.returncode == 0, (result.stdout, result.stderr) + result, lazy_path = relink(relinker, work, "lazy", fixture(), "--lazy-binding") + assert result.returncode == 0, (result.stdout, result.stderr) + assert LAZY_WARNING in result.stdout + result.stderr, (result.stdout, result.stderr) fixed = fixed_path.read_bytes() + high = high_path.read_bytes() + lazy = lazy_path.read_bytes() - assert struct.unpack_from(" struct.unpack_from("> 32 for _, info, _ in actual) return diff --git a/core/relinker/relinker/tests/test_sce_relro.py b/core/relinker/relinker/tests/test_sce_relro.py index 51de2a0953..42cbcb5bf8 100644 --- a/core/relinker/relinker/tests/test_sce_relro.py +++ b/core/relinker/relinker/tests/test_sce_relro.py @@ -12,6 +12,7 @@ from test_linux_load_alignment import loads PT_LOAD = 1 PT_DYNAMIC = 2 PT_SCE_RELRO = 0x61000010 +DEFAULT_BASE = 0x400000 PT_SCE_VERSION = 0x6FFFFF01 @@ -77,11 +78,11 @@ def main(): 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) + relro = [load for load in loads(data) if load[3] <= DEFAULT_BASE + 0x1000 < load[3] + load[6]] + assert len(relro) == 1 and relro[0][1] == 6 and relro[0][3] == DEFAULT_BASE + 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("` | Map the lowest ELF load page at this fixed Windows address, given in decimal or hexadecimal (`0x`). Requires `--windows`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and a complete image fitting in the user address space. The main executable has no ASLR or base relocations; an occupied address fails to load. Bundled modules keep their normal placement. | | `--trace-imports` | Pass each function import through the libc.prx call tracer, see [tracing system library calls](#tracing-system-library-calls). Windows and Linux output only; conflicts with `--lazy-binding`. | -| `--linux-fixed-base
` | Load the Linux executable at this fixed address, given in decimal or hexadecimal (`0x`): every virtual address of the image is offset by it, so virtual address 0 is placed at the address, as the console places an executable. Requires a 64 KiB-aligned address from `0x10000` through `0x7fffffff0000` that is a multiple of every `PT_LOAD` alignment, and an image that ends in the user address space. The executable is written as `ET_EXEC`, without ASLR. Bundled modules keep their normal placement. Conflicts with `--windows`, `--macos` and `--lazy-binding`. | +| `--linux-fixed-base
` | Load the Linux executable at this fixed address instead of the default `0x400000`, given in decimal or hexadecimal (`0x`): every virtual address of the image is offset by it, so virtual address 0 is placed at the address, as the console places an executable. Requires a 64 KiB-aligned address from `0x10000` through `0x7fffffff0000` that is a multiple of every `PT_LOAD` alignment, and an image that ends in the user address space. The executable is written as `ET_EXEC`, without ASLR, as it is at the default address. Bundled modules keep their normal placement. Conflicts with `--windows`, `--macos` and `--lazy-binding`. | | `--macos-fixed-base
` | Map the lowest ELF load page of the macOS executable at this fixed address, given in decimal or hexadecimal (`0x`). Requires `--macos`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and an image that ends in the macOS user address space. `__PAGEZERO` ends 64 KiB below the address and the Mach-O header occupies those 64 KiB. The executable has no ASLR; an address the process cannot map fails to load. Bundled modules keep their normal placement. | | `--to-intel` | Convert supported AMD-only instructions in the executable and bundled modules. Unsupported instructions or unreachable conversion stubs cause an error. | | `--to-rosetta` | Convert as `--to-intel` does and also lower what Rosetta lacks: ANDN becomes scalar NOT/AND, RDSEED becomes RDRAND, CLWB a NOP, and RDPID and the ECX of RDTSCP return processor 0. Implies `--to-intel`. Independent of `--macos`: `--macos` alone leaves these instructions as they are. Conflicts with `--windows`. | @@ -85,7 +85,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--exclude-sce-module ` | Deprecated. Exclude a bundled module by exact filename, not path. Repeat for multiple files; a missing filename is an error. Conflicts with `--skip-sce-module`. | | `--skip-syscall-check` | Deprecated. Disable syscall scanning in the executable and bundled modules. | | `--trap-sdk-syscall-stubs` | Replace the exact ps5-payload-dev generic and numbered syscall wrapper stubs and the recognized inline mmap/mprotect sequences from SDK libc/mman.c with `UD2`, so unused wrappers do not block relinking. Other syscall instructions still cause an error. If a trapped wrapper runs, it raises an illegal-instruction exception. For debugging only. | -| `--lazy-binding` | Deprecated. Enable lazy symbol binding instead of eager binding. Incompatible with bundled ELF modules. | +| `--lazy-binding` | Deprecated. Enable lazy symbol binding instead of eager binding. Incompatible with bundled ELF modules. The Linux executable stays position independent (`ET_DYN`) instead of being placed at `0x400000`. | Specify `unused-filter=0|1|2` without `--`, at most once. Unknown options and extra positional arguments are errors. Use `relinker --help` to print the usage syntax to stdout and exit with code `0` without converting files. Invoking `relinker` without arguments prints the usage syntax to stderr and exits with code `1`. @@ -120,11 +120,11 @@ align = 0x1000 description = "pool size" ``` -`format = 1` is required. The top-level keys `relinker_version`, `relinker_commit`, `config`, `config_sha256`, `main_target`, `settings` and `settings_sha256` are accepted in any file; only `main_target` is used, by `--verify-output`. Any other top-level key is an error. +`format = 1` is required. The top-level keys `relinker_version`, `relinker_commit`, `config`, `config_sha256`, `main_target`, `linux_base`, `settings` and `settings_sha256` are accepted in any file; only `main_target` and `linux_base` are used, by `--verify-output`. Any other top-level key is an error. | Entry key | Meaning | |-----------|---------| -| `name` | Optional. A setting name of `[a-z0-9_]`, unique within the file and not one of the reserved top-level key names (`format` and the seven keys above), used by a later `--set` option. Shown in the progress line, the manifest and the verify report. | +| `name` | Optional. A setting name of `[a-z0-9_]`, unique within the file and not one of the reserved top-level key names (`format` and the eight keys above), used by a later `--set` option. Shown in the progress line, the manifest and the verify report. | | `target` | Required. The main executable's file name, or for `--executable` files and bundled modules the path relative to the module parent directory, for example `sce_module/libc.prx`. | | `va` | Required. The virtual address of the ELF as a disassembler shows it. | | `type` | Optional: `u8 u16 u32 u64 i8 i16 i32 i64`. Then `expected`, `value`, `min`, `max` and `align` are integers, range-checked and written little-endian. Without it, `expected` and `value` are hexadecimal byte strings of the same length. | @@ -137,11 +137,11 @@ x86 `mov r/m64, imm32` and `add`/`sub`/`cmp r/m64, imm32` sign-extend their imme Entries apply in file order. A patch range must lie in the file bytes of one `PT_LOAD` and outside the ELF header and program header table. The conversion fails before writing any output if bytes differ from `expected`, a range is not valid, two entries overlap, a target matches no file or more than one file, or a range overlaps bytes the relinker itself rewrites (`--to-intel` replacements and trampolines, relinker patches, syscall traps) or the 8-byte target of a relocation of the file. -When at least one patch is applied, `.patches.toml` is written beside the output. It lists every applied entry for every converted file, with `main_target` and `config` (the patch file path in forward-slash form, reduced to its file name when the path holds a `"`, backslash or control character; the conversion fails before writing any output when even that cannot be held); typed entries keep their `type`, integers and `min`, `max` and `align`, and `--patches` accepts it to reproduce the conversion. The manifest must not replace the patch file it was read from. +When at least one patch is applied, `.patches.toml` is written beside the output. It lists every applied entry for every converted file, with `main_target`, `linux_base` (the address a Linux `ET_EXEC` output is placed at) and `config` (the patch file path in forward-slash form, reduced to its file name when the path holds a `"`, backslash or control character; the conversion fails before writing any output when even that cannot be held); typed entries keep their `type`, integers and `min`, `max` and `align`, and `--patches` accepts it to reproduce the conversion. The manifest must not replace the patch file it was read from. `--verify-patches ` writes nothing and exits `0` when no entry is a mismatch. Without `--verify-output`, `` is the input executable of any type, modules are found as in a conversion (module options and `--executable` apply), and each entry is `applicable` (the input holds `expected`) or `mismatch`. A target that matches nothing is an error that suggests `--verify-output`. -With `--verify-output`, `` is a converted Linux ELF (`ET_DYN`); `ET_EXEC` output from `--linux-fixed-base`, PE and Mach-O outputs are refused. The file must have `main_target`, so verify the generated manifest, not a hand-written file. The entry whose target is `main_target` is checked against ``, and entries for the `--executable` strings given are checked against the output file derived as in a conversion; an entry holding `value` is `applied`. Every other entry is `not-checked (module, or executable not named with --executable)`, including a mistyped module target. Two targets with the same name, such as an `--executable` equal to `main_target`, are an `ambiguous patch target` error. Module discovery options are rejected with `--verify-output`. +With `--verify-output`, `` is a converted Linux ELF; PE and Mach-O outputs are refused. The file must have `main_target`, so verify the generated manifest, not a hand-written file. Entries are checked at their `va` plus the manifest's `linux_base` in an `ET_EXEC` output, which requires `linux_base`; an `ET_DYN` output, from `--lazy-binding`, requires a manifest without it. The entry whose target is `main_target` is checked against ``, and entries for the `--executable` strings given are checked against the output file derived as in a conversion; an entry holding `value` is `applied`. Every other entry is `not-checked (module, or executable not named with --executable)`, including a mistyped module target. Two targets with the same name, such as an `--executable` equal to `main_target`, are an `ambiguous patch target` error. Module discovery options are rejected with `--verify-output`. Verify mode rejects the conversion options (`--windows`, `--macos`, `--to-intel`, `--rpath`, `--patches`, `--no-patches` and the like) and a second path. @@ -149,7 +149,7 @@ Verify mode rejects the conversion options (`--windows`, `--macos`, `--to-intel` For an executable that assumes its image starts at `0x400000`, use `relinker --windows --windows-fixed-base 0x400000 source/input.elf app.exe`. This maps its lowest ELF load page there; the PE headers occupy the preceding 64 KiB. Use this option only when the executable requires an exact address. Without it, the Windows executable has no `DYNAMIC_BASE` and loads at its preferred base `0x140000000`; relocations are kept, so Windows relocates it if that range is taken. Modules keep `DYNAMIC_BASE`. This option does not change macOS output. -For an executable that assumes its image starts at `0x400000`, as Ruffle (PPSA68091), VLC (PPSA85300) and XPSemu (PPSA97358) do when they read their `app0/imports.txt`, use `relinker --linux-fixed-base 0x400000 source/input.elf app.elf` on Linux, or `relinker --macos --macos-fixed-base 0x400000 source/input.elf app` on macOS. Use these options only when the executable requires a fixed address. Without them, Linux output is a position-independent executable as before, and macOS output places the lowest ELF load page at `0x140010000`. +For an executable that assumes its image starts at `0x400000`, as Ruffle (PPSA68091), VLC (PPSA85300) and XPSemu (PPSA97358) do when they read their `app0/imports.txt`, use `relinker --macos --macos-fixed-base 0x400000 source/input.elf app` on macOS. Use this option only when the executable requires a fixed address; without it, macOS output places the lowest ELF load page at `0x140010000`. Linux output places virtual address 0 at `0x400000` without an option, inside the 40-bit window that a title's own `sceLibcMspaceCreate` checks; `--linux-fixed-base` selects another address, and with `--lazy-binding` the executable stays position independent. The same base is applied to executables selected with `--executable`; bundled PRX modules are not rebased.