Merge pull request #5199 from Zenodeon/conversion-diagnostics

fix(relinker): identify failed inputs and invalid dynamic values
This commit is contained in:
Dean 2026-10-14 15:39:35 +13:00 committed by GitHub
commit 0defec4386
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 173 additions and 16 deletions

View File

@ -390,6 +390,9 @@ if(BUILD_TESTING)
set_tests_properties(windows_fixed_base PROPERTIES TIMEOUT 20)
add_test(NAME elf_offsets COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_elf_offsets.py $<TARGET_FILE:relinker>)
add_test(NAME conversion_diagnostics COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_conversion_diagnostics.py $<TARGET_FILE:relinker>)
set_tests_properties(conversion_diagnostics PROPERTIES TIMEOUT 20)
add_test(NAME dynamic_segment COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_dynamic_segment.py $<TARGET_FILE:relinker>)
add_test(NAME rela_size COMMAND ${Python3_EXECUTABLE}

View File

@ -3,6 +3,7 @@
#include <elfpatcher/windows/WindowsLoadImage.hpp>
#include <elfpatcher/windows/WindowsPeFormat.hpp>
#include <io/BufferUtils.hpp>
#include <io/NativePath.hpp>
#include <algorithm>
#include <map>
@ -114,7 +115,7 @@ std::vector<std::uint8_t> GuestModuleWriter::WriteMacOs(const Relinker::GuestIma
try {
return WriteMacOsImage(input);
} catch (Domain::RelinkerException& error) {
error.InputPath = guest.SourcePath.string();
if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(guest.SourcePath);
throw;
}
}

View File

@ -6,6 +6,7 @@
#include <elfpatcher/windows/WindowsRelocationBuilder.hpp>
#include <elfpatcher/windows/WindowsTrampolineBuilder.hpp>
#include <io/BufferUtils.hpp>
#include <io/NativePath.hpp>
#include <algorithm>
#include <map>
#include <iterator>
@ -84,7 +85,7 @@ std::vector<std::uint8_t> GuestModuleWriter::WriteWindows(const Relinker::GuestI
try {
directories[9] = WindowsTlsBuilder().Build(guest.Bytes, guest.Headers, image, sections, relocations, nextRva, &tlsIndex);
} catch (Domain::RelinkerException& error) {
error.InputPath = guest.SourcePath.string();
if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(guest.SourcePath);
throw;
}
WindowsTrampolineBuilder().Build(guest.Trampolines, image, sections, nextRva);

View File

@ -1,6 +1,7 @@
#include <relinker/guest/GuestImage.hpp>
#include <relinker/parsing/ElfReader.hpp>
#include <io/BufferUtils.hpp>
#include <io/NativePath.hpp>
#include <algorithm>
#include <bit>
#include <limits>
@ -87,8 +88,8 @@ std::vector<std::string> GuestImageReader::ReadModuleNames(const std::vector<std
return names;
}
GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector<std::uint8_t> bytes, const bool windows) const {
const auto fail = [&](const std::string& message) { throw Domain::RelinkerException(path.string() + ": " + message); };
GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector<std::uint8_t> bytes, const bool windows) const try {
const auto fail = [&](const std::string& message) { throw Domain::RelinkerException(Io::Utf8Path(path) + ": " + message); };
const auto range = [&](std::uint64_t offset, std::uint64_t size) {
if (offset > bytes.size() || size > bytes.size() - offset) {
std::ostringstream message;
@ -361,6 +362,9 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector
if (tags.contains(33) && tags.at(33) != 0 && image.Init == 0) fail("Guest PREINIT_ARRAY has no module initializer");
image.Bytes = std::move(bytes);
return image;
} catch (Domain::RelinkerException& error) {
if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(path);
throw;
}
}

View File

@ -34,7 +34,7 @@ namespace Relinker {
Conversion::Conversion(Domain::IProgress& progress) : progress(progress) {}
ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOptions& options) const {
ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOptions& options) const try {
Domain::InputProgress inputProgress(progress, Io::Utf8Path(input.InputPath));
static_assert(std::endian::native == std::endian::little);
if (options.Target != TargetSystem::Linux && options.Target != TargetSystem::Windows && options.Target != TargetSystem::MacOs) throw Domain::RelinkerException("Unknown target system");
@ -120,7 +120,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti
try {
executableBytes = patcher->Patch(sourceBytes, result.OriginalHeaders, result.DynamicSection, result.OriginalPltGotVaddr, options.RunPath, options.LazyBinding, options.WindowsDiagnostics, trampolines);
} catch (Domain::RelinkerException& error) {
error.InputPath = Io::Utf8Path(input.InputPath);
if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(input.InputPath);
throw;
}
@ -129,6 +129,9 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti
if (windows) diagnostics = std::static_pointer_cast<Elfpatcher::Windows::WindowsPePatcher>(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)};
} catch (Domain::RelinkerException& error) {
if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(input.InputPath);
throw;
}
}

View File

@ -112,7 +112,35 @@ RelinkResult RelinkerPipeline::Relink(const std::vector<std::uint8_t>& sourceElf
for (const auto& t : dynTags)
if (t.Tag == tag)
return t.Value;
throw RelinkerException("DT tag not found");
const char* name = "unknown";
switch (tag) {
case DT_OS_STRTAB: name = "DT_OS_STRTAB"; break;
case DT_STRTAB: name = "DT_STRTAB"; break;
case DT_OS_STRSZ: name = "DT_OS_STRSZ"; break;
case DT_STRSZ: name = "DT_STRSZ"; break;
case DT_OS_SYMTAB: name = "DT_OS_SYMTAB"; break;
case DT_SYMTAB: name = "DT_SYMTAB"; break;
case DT_OS_SYMENT: name = "DT_OS_SYMENT"; break;
case DT_SYMENT: name = "DT_SYMENT"; break;
case DT_OS_PLTGOT: name = "DT_OS_PLTGOT"; break;
case DT_PLTGOT: name = "DT_PLTGOT"; break;
case DT_OS_PLTREL: name = "DT_OS_PLTREL"; break;
case DT_PLTREL: name = "DT_PLTREL"; break;
case DT_OS_PLTRELSZ: name = "DT_OS_PLTRELSZ"; break;
case DT_PLTRELSZ: name = "DT_PLTRELSZ"; break;
case DT_OS_JMPREL: name = "DT_OS_JMPREL"; break;
case DT_JMPREL: name = "DT_JMPREL"; break;
case DT_OS_RELA: name = "DT_OS_RELA"; break;
case DT_RELA: name = "DT_RELA"; break;
case DT_OS_RELASZ: name = "DT_OS_RELASZ"; break;
case DT_RELASZ: name = "DT_RELASZ"; break;
case DT_OS_RELAENT: name = "DT_OS_RELAENT"; break;
case DT_RELAENT: name = "DT_RELAENT"; break;
case DT_OS_SYMTABSZ: name = "DT_OS_SYMTABSZ"; break;
}
std::ostringstream message;
message << "Required dynamic tag " << name << " (0x" << std::hex << tag << ") not found";
throw RelinkerException(message.str());
};
auto requireExactlyOneOf = [&](const std::int64_t osTag, const std::int64_t sysvTag, const char* name) {
@ -120,8 +148,11 @@ RelinkResult RelinkerPipeline::Relink(const std::vector<std::uint8_t>& sourceElf
const bool hasSysv = hasTag(sysvTag);
if (hasOs && hasSysv)
throw RelinkerException(std::string("Both DT_OS_ and DT_ variants present for ") + name);
if (!hasOs && !hasSysv)
throw RelinkerException(std::string("Neither DT_OS_ nor DT_ variant present for ") + name);
if (!hasOs && !hasSysv) {
std::ostringstream message;
message << "Neither DT_OS_ nor DT_ variant present for " << name << " (tags 0x" << std::hex << osTag << " and 0x" << sysvTag << ")";
throw RelinkerException(message.str());
}
return hasOs;
};
@ -154,8 +185,9 @@ RelinkResult RelinkerPipeline::Relink(const std::vector<std::uint8_t>& sourceElf
const FileByteOffset dynSymTabOffset = readAsOffset(DT_OS_SYMTAB, DT_SYMTAB, "DT_SYMTAB");
constexpr std::size_t symEntSize = 24;
if (readAsSize(DT_OS_SYMENT, DT_SYMENT, "DT_SYMENT") != symEntSize)
throw RelinkerException("Unsupported DT_SYMENT value");
const auto symbolEntrySize = readAsSize(DT_OS_SYMENT, DT_SYMENT, "DT_SYMENT");
if (symbolEntrySize != symEntSize)
throw RelinkerException("Unsupported DT_SYMENT value: " + std::to_string(symbolEntrySize) + "; expected 24 bytes");
FileByteOffset dynJmpRelOffset = 0;
if (hasPltRelocations) {
@ -164,20 +196,21 @@ RelinkResult RelinkerPipeline::Relink(const std::vector<std::uint8_t>& sourceElf
? getTagValue(DT_OS_PLTREL)
: getTagValue(DT_PLTREL);
if (jmprelType != DT_RELA)
throw RelinkerException("Unsupported DT_PLTREL type");
throw RelinkerException("Unsupported DT_PLTREL type: " + std::to_string(jmprelType) + "; expected DT_RELA (7)");
dynJmpRelOffset = readAsOffset(DT_OS_JMPREL, DT_JMPREL, "DT_JMPREL");
if (gotSize % 24 != 0)
throw RelinkerException("Invalid DT_PLTRELSZ value");
throw RelinkerException("Invalid DT_PLTRELSZ value: " + std::to_string(gotSize) + " bytes; expected a multiple of 24 bytes");
}
const ByteCount dynJmpRelSize = gotSize;
const FileByteOffset dynRelaOffset = readAsOffset(DT_OS_RELA, DT_RELA, "DT_RELA");
const ByteCount dynRelaSize = readAsSize(DT_OS_RELASZ, DT_RELASZ, "DT_RELASZ");
constexpr std::size_t relaEntSize = 24;
if (readAsSize(DT_OS_RELAENT, DT_RELAENT, "DT_RELAENT") != relaEntSize)
throw RelinkerException("Unsupported DT_RELAENT value");
const auto relocationEntrySize = readAsSize(DT_OS_RELAENT, DT_RELAENT, "DT_RELAENT");
if (relocationEntrySize != relaEntSize)
throw RelinkerException("Unsupported DT_RELAENT value: " + std::to_string(relocationEntrySize) + "; expected 24 bytes");
if (dynRelaSize % relaEntSize != 0)
throw RelinkerException("Invalid DT_RELASZ value");
throw RelinkerException("Invalid DT_RELASZ value: " + std::to_string(dynRelaSize) + " bytes; expected a multiple of 24 bytes");
std::vector<std::pair<std::uint64_t, std::string>> neededLibraryNamesByStrOffset;
for (const auto& tag : dynTags)

View File

@ -3,6 +3,7 @@
#include <relinker/analysis/SyscallScanner.hpp>
#include <relinker/analysis/CodeInstructionCollector.hpp>
#include <io/BufferUtils.hpp>
#include <io/NativePath.hpp>
#include <codegen/IAmd64OnlyConverter.hpp>
#include <algorithm>
#include <array>
@ -162,6 +163,33 @@ void scanning() {
require(progress.Events.back().Completed < progress.Events.back().Total, "Failed scan reported completion");
}
class FailingProgress final : public Domain::IProgress {
public:
std::string InputPath;
void Report(const Domain::ProgressEvent&) override {
Domain::RelinkerException error("diagnostic context", 0x1234);
error.InputPath = InputPath;
throw error;
}
void Message(std::string_view) override {}
};
void diagnosticContext() {
for (const bool specific : {false, true}) {
FailingProgress progress;
if (specific) progress.InputPath = "virtual/title/sce_module/plugin.prx";
const auto source = input();
try { Relinker::Conversion(progress).Convert(source, {}); }
catch (const Domain::RelinkerException& error) {
require(error.InputPath == (specific ? progress.InputPath : Io::Utf8Path(source.InputPath)), "Wrong diagnostic input path");
require(error.FailureOffset == 0x1234, "Diagnostic offset changed");
require(std::string(error.what()) == "diagnostic context", "Diagnostic message changed");
continue;
}
throw std::runtime_error("Diagnostic failure was swallowed");
}
}
class CancellingProgress final : public Domain::IProgress {
public:
Domain::ProgressStage Stage = Domain::ProgressStage::Decode;
@ -192,6 +220,7 @@ int main() {
try {
convert();
scanning();
diagnosticContext();
cancellation();
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';

View File

@ -0,0 +1,83 @@
from pathlib import Path
import struct
import subprocess
import sys
import tempfile
from test_optional_plt import fixture
from test_guest_intel_trampolines import guest_fixture, main_fixture, PLAIN_SITE
def main():
relinker = Path(sys.argv[1]).resolve()
failures = []
with tempfile.TemporaryDirectory(prefix="anyps5-diagnostics-") as directory:
work = Path(directory) / "title space \u00e9"
work.mkdir()
def convert(name, image, expected, flags, module=None, extra=False):
case = work / name
case.mkdir()
source = case / "eboot.bin"
source.write_bytes(main_fixture() if module is not None or extra else image)
options = []
offending = source
if module is not None:
modules = case / "sce_module"
modules.mkdir()
offending = modules / "broken \u00e9.prx"
offending.write_bytes(module)
options = ["--sce-module-path", str(case)]
elif extra:
offending = case / "picker.bin"
offending.write_bytes(image)
(case / "sce_module").mkdir()
options = ["--sce-module-path", str(case), "--executable", "picker.bin"]
else:
options = ["--skip-sce-module"]
output = case / "converted.out"
result = subprocess.run([str(relinker), *flags, *options, str(source), str(output)],
capture_output=True, text=True, encoding="utf-8", timeout=20)
valid = (result.returncode == 2 and expected in result.stderr
and result.stderr.splitlines().count(f"Input: {offending}") == 1
and not output.exists() and not list(case.rglob("*.guest.prx")))
if extra:
valid = valid and not (case / "picker.bin.out").exists()
if not valid:
failures.append((name, result.returncode, result.stdout, result.stderr))
cases = []
for tag, value, message in (
(11, 16, "Unsupported DT_SYMENT value: 16; expected 24 bytes"),
(9, 8, "Unsupported DT_RELAENT value: 8; expected 24 bytes"),
(8, 25, "Invalid DT_RELASZ value: 25 bytes; expected a multiple of 24 bytes"),
):
image = fixture()
for offset in range(0x400, 0x470, 16):
if struct.unpack_from("<q", image, offset)[0] == tag:
struct.pack_into("<Q", image, offset + 8, value)
cases.append((str(tag), image, message))
missing = fixture()
struct.pack_into("<q", missing, 0x400, 0x70000000)
cases.append(("missing", missing, "Neither DT_OS_ nor DT_ variant present for DT_STRTAB (tags 0x61000035 and 0x5)"))
cases.append(("plt-type", fixture([(3, 0x300), (2, 0), (20, 17), (23, 0x720)]),
"Unsupported DT_PLTREL type: 17; expected DT_RELA (7)"))
cases.append(("plt-size", fixture([(3, 0x300), (2, 25), (20, 7), (23, 0x720)]),
"Invalid DT_PLTRELSZ value: 25 bytes; expected a multiple of 24 bytes"))
invalid_header = fixture()
struct.pack_into("<I", invalid_header, 20, 2)
cases.append(("header", invalid_header, "Unsupported ELF object version: expected 1 (offset 0x14)"))
for index, flags in enumerate(([], ["--windows"], ["--to-intel"], ["--windows", "--to-intel"])):
for name, image, message in cases:
convert(f"{index}-{name}", image, message, flags)
convert(f"{index}-extra", cases[0][1], cases[0][2], flags, extra=True)
module = guest_fixture(PLAIN_SITE)
struct.pack_into("<QQ", module, 176 + 32, 17, 17)
convert(f"{index}-module", None, "Missing or invalid ELF load/dynamic segments", flags, module=module)
if failures:
raise AssertionError(failures)
print("Conversion diagnostics tests passed")
if __name__ == "__main__":
main()