From 1205d03daa089a654cc1cdfda3416122d069ea98 Mon Sep 17 00:00:00 2001 From: Ori Shamir Date: Mon, 12 Oct 2026 14:53:02 +0300 Subject: [PATCH] fix(relinker): resolve relocations against null symbol entries to 0 A relocation whose dynamic symbol entry is all zero, like the null symbol, now takes the value 0 plus its addend, as the gABI defines for STN_UNDEF. 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 instead of failing with "Empty import symbol name". Such references are left out of the call registry. An unnamed symbol entry that is not all zero still fails the relink. OliSe Player (PPSA01153) has seven of these entries and calls through the slots only when they are not 0 (#4437). Co-Authored-By: Claude Opus 5.5 --- core/relinker/CMakeLists.txt | 2 + .../elfpatcher/general/ElfConstants.hpp | 1 + .../src/windows/WindowsRelocationBuilder.cpp | 7 +- .../src/output/SysVDynamicSectionBuilder.cpp | 11 ++- .../src/pipeline/RelinkerPipeline.cpp | 5 ++ .../tests/test_null_symbol_relocations.py | 87 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 7 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 core/relinker/relinker/tests/test_null_symbol_relocations.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index cee9b57f2..f498f966e 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -444,6 +444,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_preservation.py $) add_test(NAME input_file COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_file.py $) + add_test(NAME null_symbol_relocations COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_null_symbol_relocations.py $) add_test(NAME input_nonascii COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_nonascii.py $) add_test(NAME string_table_bounds COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp index 615c7c774..40802530e 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp @@ -141,6 +141,7 @@ inline constexpr std::uint32_t R_X86_64_REX_GOTPCRELX = 42; inline constexpr std::uint8_t STB_GLOBAL = 1; inline constexpr std::uint8_t STB_WEAK = 2; +inline constexpr std::uint8_t STT_NOTYPE = 0; inline constexpr std::uint8_t STT_FUNC = 2; inline constexpr std::uint8_t STV_DEFAULT = 0; inline constexpr std::uint16_t SHN_UNDEF = 0; diff --git a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp index b1f71e3f3..9706bfd1b 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -38,11 +39,15 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom if (Io::ReadU16(dynamicSection.DynSymData, symbolOffset + 6) != 0) throw Domain::RelinkerException("Defined symbols require explicit relocation support", target); const auto name = ReadString(dynamicSection.DynStrData, Io::ReadU32(dynamicSection.DynSymData, symbolOffset)); - if (name.empty()) + if (name.empty() && dynamicSection.DynSymData[symbolOffset + 4] >> 4 != STB_WEAK) throw Domain::RelinkerException("Empty import symbol name", target); if (type != 1 && addend != 0) throw Domain::RelinkerException("GLOB_DAT/JUMP_SLOT relocation has a nonzero addend", target); image.RequireWritable(target, 8); + if (name.empty()) { + image.WritePointer(target, addend); + continue; + } image.WritePointer(target, 0); result.Imports.push_back({name, rva, addend}); if (const auto module = dynamicSection.ImportModules.find(target); module != dynamicSection.ImportModules.end()) diff --git a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp index d3ad5fee4..b63960866 100644 --- a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp +++ b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp @@ -82,6 +82,11 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( return value.substr(0, hashPos); }; + auto symbolInfo = [](const NidReference& ref) -> std::uint8_t { + if (ref.Nid.empty()) return static_cast((STB_WEAK << 4) | STT_NOTYPE); + return static_cast((STB_GLOBAL << 4) | STT_FUNC); + }; + static constexpr std::size_t kRelaEntSize = 24; std::vector pltSlots(originalJmprelCount, nullptr); @@ -132,8 +137,7 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( const NidReference& ref = *slot; const std::uint32_t nameOff = _appendStr(result.DynStrData, stripHashSuffix(ref.Nid)); - const auto info = static_cast((STB_GLOBAL << 4) | STT_FUNC); - _appendElfSym(result.DynSymData, nameOff, info, STV_DEFAULT, 0, 0, 0); + _appendElfSym(result.DynSymData, nameOff, symbolInfo(ref), STV_DEFAULT, 0, 0, 0); const std::uint64_t relaInfo = (static_cast(symIdx) << 32) | R_X86_64_JUMP_SLOT; _appendRela(result.RelaPltData, ref.RelocationAddress, relaInfo, ref.Addend); @@ -143,8 +147,7 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( for (const NidReference* slotPtr : nonPltRefs) { const NidReference& ref = *slotPtr; const std::uint32_t nameOff = _appendStr(result.DynStrData, stripHashSuffix(ref.Nid)); - const auto info = static_cast((STB_GLOBAL << 4) | STT_FUNC); - _appendElfSym(result.DynSymData, nameOff, info, STV_DEFAULT, 0, 0, 0); + _appendElfSym(result.DynSymData, nameOff, symbolInfo(ref), STV_DEFAULT, 0, 0, 0); const std::uint64_t relaInfo = (static_cast(symIdx) << 32) | ref.RelocationTypeValue; _appendRela(result.RelaData, ref.RelocationAddress, relaInfo, ref.Addend); diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index d1ff243f4..64aa5b865 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -258,6 +258,10 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::memcpy(&nameOff, raw.data() + symOff, 4); const auto name = readCStr(nameOff); + if (name.empty() && (raw.size() - symOff < symEntSize + || std::any_of(raw.begin() + static_cast(symOff), raw.begin() + static_cast(symOff + symEntSize), + [](const std::uint8_t byte) { return byte != 0; }))) + throw RelinkerException("Unnamed import symbol is not a null symbol entry", symOff); nidRefs.push_back({name, Domain::ImportModule(name, importModules, neededLibraries), relType, pos, rOffset, rAddend}); } }; @@ -426,6 +430,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::vector entries; entries.reserve(nidRefs.size()); for (const auto& ref : nidRefs) { + if (ref.Nid.empty()) continue; CallRegistryEntry entry; entry.Nid = ref.Nid; entry.Library = ref.Library; diff --git a/core/relinker/relinker/tests/test_null_symbol_relocations.py b/core/relinker/relinker/tests/test_null_symbol_relocations.py new file mode 100644 index 000000000..23aa8c8d4 --- /dev/null +++ b/core/relinker/relinker/tests/test_null_symbol_relocations.py @@ -0,0 +1,87 @@ +import json +import os +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_dynamic_segment import fixture +from test_linux_dynamic_strings import dynamic_strings, file_offset +from test_linux_load_alignment import loads + + +GLOB_DAT_SLOT = 0x308 +ABSOLUTE_SLOT = 0x310 +ADDEND = 0x1234 +CHECK_SLOTS = bytes.fromhex( + "488b0501010000" "4885c0" "751a" + "488b0dfd000000" "4881f934120000" "750a" + "b82a000000c3" "cccccccc" "b807000000c3") +TARGETS = (("linux", [], b"\x7fELF"), ("windows", ["--windows"], b"MZ"), ("macos", ["--macos"], b"\xcf\xfa\xed\xfe")) + + +def image_with(symbol=b""): + image = fixture() + relocations = [(GLOB_DAT_SLOT, (1 << 32) | 6, 0), (ABSOLUTE_SLOT, (1 << 32) | 1, ADDEND)] + struct.pack_into("> 32) * 24) + imports.append((target, info & 0xffffffff, addend, strings[name:strings.index(b"\0", name)], bind_type, section, value, size)) + return sorted(imports) + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix="anyps5-null-symbol-") as directory: + work = Path(directory) + source = work / "null-entry.elf" + source.write_bytes(image_with()) + for target, options, magic in TARGETS: + for level in (0, 1): + output = work / f"null-entry-{target}-{level}.out" + result = subprocess.run([str(relinker), "--skip-sce-module", "--registry", f"unused-filter={level}", + *options, str(source), str(output)], capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (target, level, result.stdout, result.stderr) + data = output.read_bytes() + assert data.startswith(magic), target + registry = json.loads(output.with_suffix(".registry.json").read_text(encoding="utf-8")) + assert registry == [], (target, level, registry) + if target == "linux": + assert linux_imports(data) == [(GLOB_DAT_SLOT, 6, 0, b"", 0x20, 0, 0, 0), + (ABSOLUTE_SLOT, 1, ADDEND, b"", 0x20, 0, 0, 0)], level + if target == "windows" and os.name == "nt": + run = subprocess.run([str(output)], capture_output=True, timeout=20) + assert run.returncode == 42, (level, run.returncode, run.stderr) + source = work / "named-unnamed.elf" + source.write_bytes(image_with(struct.pack("