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 <noreply@anthropic.com>
This commit is contained in:
parent
029c1bc17d
commit
1205d03daa
|
|
@ -444,6 +444,8 @@ if(BUILD_TESTING)
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_preservation.py $<TARGET_FILE:relinker>)
|
||||
add_test(NAME input_file COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_file.py $<TARGET_FILE:relinker>)
|
||||
add_test(NAME null_symbol_relocations COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_null_symbol_relocations.py $<TARGET_FILE:relinker>)
|
||||
add_test(NAME input_nonascii COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_nonascii.py $<TARGET_FILE:relinker>)
|
||||
add_test(NAME string_table_bounds COMMAND ${Python3_EXECUTABLE}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include <elfpatcher/windows/WindowsRelocationBuilder.hpp>
|
||||
#include <elfpatcher/general/ElfConstants.hpp>
|
||||
#include <io/BufferUtils.hpp>
|
||||
#include <map>
|
||||
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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<std::uint8_t>((STB_WEAK << 4) | STT_NOTYPE);
|
||||
return static_cast<std::uint8_t>((STB_GLOBAL << 4) | STT_FUNC);
|
||||
};
|
||||
|
||||
static constexpr std::size_t kRelaEntSize = 24;
|
||||
|
||||
std::vector<const NidReference*> 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<std::uint8_t>((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<std::uint64_t>(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<std::uint8_t>((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<std::uint64_t>(symIdx) << 32) | ref.RelocationTypeValue;
|
||||
_appendRela(result.RelaData, ref.RelocationAddress, relaInfo, ref.Addend);
|
||||
|
|
|
|||
|
|
@ -258,6 +258,10 @@ RelinkResult RelinkerPipeline::Relink(const std::vector<std::uint8_t>& 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<std::ptrdiff_t>(symOff), raw.begin() + static_cast<std::ptrdiff_t>(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<std::uint8_t>& sourceElf
|
|||
std::vector<CallRegistryEntry> 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;
|
||||
|
|
|
|||
|
|
@ -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("<Q", image, 0x458, len(relocations) * 24)
|
||||
for index, relocation in enumerate(relocations):
|
||||
struct.pack_into("<QQq", image, 0x700 + index * 24, *relocation)
|
||||
image[0x200:0x200 + len(CHECK_SLOTS)] = CHECK_SLOTS
|
||||
struct.pack_into("<QQ", image, GLOB_DAT_SLOT, 0x5A5A5A5A5A5A5A5A, 0x5A5A5A5A5A5A5A5A)
|
||||
image[0x638:0x638 + len(symbol)] = symbol
|
||||
struct.pack_into("<qQ", image, 0x470, 0x6100003f, 48)
|
||||
struct.pack_into("<QQ", image, 152, 0x90, 0x90)
|
||||
return image
|
||||
|
||||
|
||||
def linux_imports(data):
|
||||
tags, strings = dynamic_strings(data)
|
||||
segments = loads(data)
|
||||
symbols = file_offset(data, segments, tags[6])
|
||||
relocations = file_offset(data, segments, tags[7])
|
||||
imports = []
|
||||
for offset in range(relocations, relocations + tags[8], 24):
|
||||
target, info, addend = struct.unpack_from("<QQq", data, offset)
|
||||
if info & 0xffffffff == 8:
|
||||
continue
|
||||
name, bind_type, _, section, value, size = struct.unpack_from("<IBBHQQ", data, symbols + (info >> 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("<IBBHQQ", 0, 0x12, 0, 0, 0, 0)))
|
||||
for target, options, _ in TARGETS:
|
||||
output = work / f"named-unnamed-{target}.out"
|
||||
result = subprocess.run([str(relinker), "--skip-sce-module", *options, str(source), str(output)],
|
||||
capture_output=True, text=True, timeout=20)
|
||||
assert result.returncode == 2 and "Unnamed import symbol is not a null symbol entry" in result.stderr, (target, result.stderr)
|
||||
assert not output.exists()
|
||||
print("Null symbol relocation tests passed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -575,6 +575,7 @@ Throughout the project, every function at every stage either **does exactly what
|
|||
- [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.
|
||||
- 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.
|
||||
- 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)).
|
||||
- [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).
|
||||
- [DB_SHADER_CONTROL.PRIMITIVE_ORDERED_PIXEL_SHADER](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the host pixel interlock spans the whole shader, not only up to `s_sendmsg` ORDERED_PS_DONE.
|
||||
|
|
|
|||
Loading…
Reference in New Issue