fix(relinker): resolve SCE relocation tables relative to dynlibdata

This commit is contained in:
Dani 2026-10-11 11:34:59 +02:00
parent ccf9ff9044
commit cdfc11cf56
4 changed files with 153 additions and 9 deletions

View File

@ -59,15 +59,19 @@ constexpr std::size_t RelaEntSize = 24;
void collectRelativeEntries(
const std::vector<std::uint8_t>& elfBytes,
const std::vector<LoadSegment>& loads,
VirtualAddress tableVaddr,
std::uint64_t tableAddress,
std::uint64_t tableSize,
bool sceTable,
std::uint64_t sceTableBase,
std::unordered_map<VirtualAddress, VirtualAddress>& out
) {
if (tableSize == 0) return;
if (tableSize == 0 && !sceTable) return;
if (tableSize % RelaEntSize != 0)
throw RelinkerException("Invalid relocation table size", tableSize);
std::uint64_t tableFileOff = vaddrToFileOffset(loads, tableVaddr, tableSize);
if (sceTable && tableAddress > std::numeric_limits<std::uint64_t>::max() - sceTableBase)
throw RelinkerException("Relocation table file offset overflow", tableAddress);
std::uint64_t tableFileOff = sceTable ? sceTableBase + tableAddress : vaddrToFileOffset(loads, tableAddress, tableSize);
if (tableFileOff > elfBytes.size())
throw RelinkerException("Relocation table out of bounds", tableFileOff);
if (tableSize > elfBytes.size() - tableFileOff)
@ -128,12 +132,21 @@ std::unique_ptr<IRelativeRelocationIndex> BuildRelativeRelocationIndex(
std::uint16_t phCount = read16(elfBytes, 56);
if (phEntSize < 56) throw RelinkerException("ELF program header entry too small");
if (phOff > elfBytes.size() || phCount > (elfBytes.size() - phOff) / phEntSize)
throw RelinkerException("Program header out of bounds");
std::vector<LoadSegment> loads;
std::uint64_t sceTableBase = 0;
bool hasSceDynlibData = false;
for (std::uint16_t i = 0; i < phCount; ++i) {
std::size_t phPos = static_cast<std::size_t>(phOff) + i * phEntSize;
if (phPos + 56 > elfBytes.size()) throw RelinkerException("Program header out of bounds");
std::uint32_t type = read32(elfBytes, phPos);
if (type == PT_SCE_DYNLIBDATA) {
if (hasSceDynlibData) throw RelinkerException("Multiple PT_SCE_DYNLIBDATA segments", phPos);
hasSceDynlibData = true;
sceTableBase = read64(elfBytes, phPos + 8);
}
if (type != PT_LOAD) continue;
LoadSegment seg;
seg.fileOffset = read64(elfBytes, phPos + 8);
@ -159,6 +172,8 @@ std::unique_ptr<IRelativeRelocationIndex> BuildRelativeRelocationIndex(
std::uint64_t relaSz = 0;
VirtualAddress jmprelVa = 0;
std::uint64_t pltrelsz = 0;
bool relaSce = false;
bool jmprelSce = false;
for (std::uint64_t off = 0; off <= segSz && segSz - off >= 16; off += 16) {
std::size_t pos = static_cast<std::size_t>(segOff + off);
@ -166,14 +181,20 @@ std::unique_ptr<IRelativeRelocationIndex> BuildRelativeRelocationIndex(
std::uint64_t val = read64(elfBytes, pos + 8);
if (tag == DT_NULL) break;
if (tag == DT_RELA || tag == DT_OS_RELA) relaVa = val;
if (tag == DT_RELA || tag == DT_OS_RELA) {
relaVa = val;
relaSce = tag == DT_OS_RELA;
}
if (tag == DT_RELASZ || tag == DT_OS_RELASZ) relaSz = val;
if (tag == DT_JMPREL || tag == DT_OS_JMPREL) jmprelVa = val;
if (tag == DT_JMPREL || tag == DT_OS_JMPREL) {
jmprelVa = val;
jmprelSce = tag == DT_OS_JMPREL;
}
if (tag == DT_PLTRELSZ || tag == DT_OS_PLTRELSZ) pltrelsz = val;
}
collectRelativeEntries(elfBytes, loads, relaVa, relaSz, targets);
collectRelativeEntries(elfBytes, loads, jmprelVa, pltrelsz, targets);
collectRelativeEntries(elfBytes, loads, relaVa, relaSz, relaSce, sceTableBase, targets);
collectRelativeEntries(elfBytes, loads, jmprelVa, pltrelsz, jmprelSce, sceTableBase, targets);
break;
}

View File

@ -25,6 +25,11 @@ public:
requireRange(segment.Offset, segment.FileSize);
if (segment.MemorySize > std::numeric_limits<VirtualAddress>::max() - segment.MappedAddress)
throw RelinkerException("Strict filter: segment address overflow", segment.MappedAddress);
if (segment.Type == 0x61000000) {
if (hasSceDynlibData) throw RelinkerException("Strict filter: multiple PT_SCE_DYNLIBDATA segments", segment.Offset);
hasSceDynlibData = true;
sceTableBase = segment.Offset;
}
if (segment.Type == 1 && segment.FileSize > segment.MemorySize)
throw RelinkerException("Strict filter: invalid load segment size", segment.Offset);
if (segment.Type != 2) continue;
@ -136,6 +141,8 @@ private:
ElfReader reader;
std::vector<ProgramHeader> headers;
std::map<std::int64_t, std::uint64_t> tags;
std::uint64_t sceTableBase = 0;
bool hasSceDynlibData = false;
void requireRange(std::uint64_t offset, std::uint64_t size) const {
if (offset > bytes.size() || size > bytes.size() - offset) throw RelinkerException("Strict filter: file range out of bounds", offset);
@ -168,7 +175,10 @@ private:
std::optional<std::uint64_t> table(std::int64_t standardTag, std::int64_t osTag) const {
const auto address = value(standardTag, osTag);
if (!address) return std::nullopt;
return tags.contains(osTag) ? *address : fileOffset(*address, 0);
if (!tags.contains(osTag)) return fileOffset(*address, 0);
if (*address > std::numeric_limits<std::uint64_t>::max() - sceTableBase)
throw RelinkerException("Strict filter: SCE table file offset overflow", *address);
return sceTableBase + *address;
}
};

View File

@ -9,7 +9,9 @@
#include <cstring>
#include <functional>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <string>
namespace {
@ -465,6 +467,117 @@ void relativeRelocationTableBounds() {
requireFailure([&] { Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(overflowDynamicOffset); }, "PT_DYNAMIC offset near UINT64_MAX was accepted");
}
void sceDynlibDataOffsets() {
auto input = fixture();
emit(input, 0, {0xC3});
importThunk(input, 16, 0x2000);
auto bytes = elfFixture(input);
bytes.resize(0xA00);
write<std::uint16_t>(bytes, 56, 4);
write<std::uint32_t>(bytes, 176, 2);
write<std::uint32_t>(bytes, 180, 6);
write<std::uint64_t>(bytes, 184, 0x300);
write<std::uint64_t>(bytes, 192, 0x2000);
write<std::uint64_t>(bytes, 208, 0x40);
write<std::uint64_t>(bytes, 216, 0x40);
write<std::uint64_t>(bytes, 224, 8);
write<std::uint32_t>(bytes, 232, 0x61000000);
write<std::uint32_t>(bytes, 236, 4);
write<std::uint64_t>(bytes, 240, 0x500);
write<std::uint64_t>(bytes, 264, 0x500);
write<std::uint64_t>(bytes, 272, 0x500);
write<std::uint64_t>(bytes, 280, 0x10);
write<std::int64_t>(bytes, 0x300, 0x6100002F);
write<std::uint64_t>(bytes, 0x308, 0x400);
write<std::int64_t>(bytes, 0x310, 0x61000031);
write<std::uint64_t>(bytes, 0x318, 24);
write<std::int64_t>(bytes, 0x320, 0x61000033);
write<std::uint64_t>(bytes, 0x328, 24);
write<std::uint64_t>(bytes, 0x900, 0x2040);
write<std::uint64_t>(bytes, 0x908, 8);
write<std::uint64_t>(bytes, 0x910, 0x1010);
std::string failures;
try {
const auto index = Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(bytes);
if (index->TargetOfSlot(0x2040) != 0x1010) failures += "SCE relative relocation was not indexed; ";
} catch (const std::exception& error) {
failures += std::string("SCE relative relocation index: ") + error.what() + "; ";
}
try {
const std::vector<Relinker::NidReference> references = {{"live", {}, 6, 0x900, 0x2000, 0}};
const auto filtered = Relinker::MakeStrictUnusedNidFilter()->Filter(references, bytes, input.Text, input.TextVaddr);
if (filtered.size() != 1) failures += "SCE relocation target did not keep its import";
} catch (const std::exception& error) {
failures += std::string("SCE strict filter: ") + error.what();
}
require(failures.empty(), failures.c_str());
auto noSegment = bytes;
write<std::uint32_t>(noSegment, 232, 0x6FFFFF01);
write<std::uint64_t>(noSegment, 0x308, 0x900);
require(Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(noSegment)->TargetOfSlot(0x2040) == 0x1010,
"SCE relocation without PT_SCE_DYNLIBDATA was not read at its absolute file offset");
const std::vector<Relinker::NidReference> references = {{"live", {}, 6, 0x900, 0x2000, 0}};
require(Relinker::MakeStrictUnusedNidFilter()->Filter(references, noSegment, input.Text, input.TextVaddr).size() == 1,
"Strict filter lost an SCE relocation without PT_SCE_DYNLIBDATA");
auto jmprel = bytes;
write<std::uint64_t>(jmprel, 208, 0x60);
write<std::uint64_t>(jmprel, 216, 0x60);
write<std::uint64_t>(jmprel, 0x318, 0);
write<std::int64_t>(jmprel, 0x330, 0x61000029);
write<std::uint64_t>(jmprel, 0x338, 0x430);
write<std::int64_t>(jmprel, 0x340, 0x6100002D);
write<std::uint64_t>(jmprel, 0x348, 24);
write<std::uint64_t>(jmprel, 0x930, 0x2050);
write<std::uint64_t>(jmprel, 0x938, 8);
write<std::uint64_t>(jmprel, 0x940, 0x1010);
require(Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(jmprel)->TargetOfSlot(0x2050) == 0x1010,
"SCE JMPREL table was not read at its segment-relative file offset");
write<std::uint64_t>(jmprel, 0x338, 0x4F0);
requireFailure([&] { (void)Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(jmprel); },
"SCE JMPREL table past the end of the file was accepted");
auto emptyJmprel = jmprel;
write<std::uint64_t>(emptyJmprel, 0x338, 0x430);
write<std::uint64_t>(emptyJmprel, 0x348, 0);
require(!Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(emptyJmprel)->TargetOfSlot(0x2050),
"Valid empty SCE JMPREL table was rejected or parsed");
write<std::uint64_t>(emptyJmprel, 0x338, 0x501);
requireFailure([&] { (void)Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(emptyJmprel); },
"Empty SCE JMPREL table past the file was accepted");
write<std::uint64_t>(emptyJmprel, 0x338, std::numeric_limits<std::uint64_t>::max());
requireFailure([&] { (void)Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(emptyJmprel); },
"Empty SCE JMPREL table with overflowing offset was accepted");
auto emptyRela = bytes;
write<std::uint64_t>(emptyRela, 0x318, 0);
require(!Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(emptyRela)->TargetOfSlot(0x2040),
"Valid empty SCE RELA table was rejected or parsed");
write<std::uint64_t>(emptyRela, 0x308, 0x501);
requireFailure([&] { (void)Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(emptyRela); },
"Empty SCE RELA table past the file was accepted");
write<std::uint64_t>(emptyRela, 0x308, std::numeric_limits<std::uint64_t>::max());
requireFailure([&] { (void)Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(emptyRela); },
"Empty SCE RELA table with overflowing offset was accepted");
auto overflow = bytes;
write<std::uint64_t>(overflow, 0x308, std::numeric_limits<std::uint64_t>::max());
requireFailure([&] { (void)Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(overflow); },
"SCE relocation file offset overflow was accepted");
requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter()->Filter(references, overflow, input.Text, input.TextVaddr); },
"Strict filter accepted an overflowing SCE table offset");
auto duplicate = bytes;
write<std::uint16_t>(duplicate, 56, 5);
write<std::uint32_t>(duplicate, 288, 0x61000000);
requireFailure([&] { (void)Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(duplicate); },
"Duplicate PT_SCE_DYNLIBDATA segments were accepted");
requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter()->Filter(references, duplicate, input.Text, input.TextVaddr); },
"Strict filter accepted duplicate PT_SCE_DYNLIBDATA segments");
}
}
int main() {
@ -486,6 +599,7 @@ int main() {
filterCallbackDataImports();
filterAndPltCompaction();
relativeRelocationTableBounds();
sceDynlibDataOffsets();
std::cout << "Strict NID filter tests passed\n";
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';

View File

@ -466,7 +466,6 @@ Throughout the project, every function at every stage either **does exactly what
- [Image loads](../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) (shader recompiler) - an image_load reads a sampled binding, also of an image the shader stores to, so in a primitive-ordered pixel shader it misses an earlier overlapping primitive's image_store in the same draw.
- [PA_SC_CONSERVATIVE_RASTERIZATION_CNTL](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 0x6001 uses Vulkan overestimation on devices that add at most 1/256 pixel and keep degenerate triangles; the console's uncertainty region and degenerate handling are unmeasured. A value with `OVER_RAST_ENABLE` and `UNDER_RAST_ENABLE` clear is decoded as disabled even when `UNCERTAINTY_REGION_MODE` or the `OUTER_`/`INNER_UNCERTAINTY_EDGERULE_OVERRIDE` bits (19:16) are set, as PPSA01474 writes 0xe6000; that these fields have no effect without over- or underestimation is assumed from their names, not measured.
- 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.
- The [relinker pipeline](../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp) reads `DT_SCE_*` table addresses as offsets into the `PT_SCE_DYNLIBDATA` segment, where PS4 executables (CUSA00900) keep them; without that segment they are still read as file offsets. It rejects multiple `PT_SCE_DYNLIBDATA` segments, matching the single segment in the [documented PS4 ELF layout](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/wiki/PS4-ELF-Specification). The `unused-filter=1` and `unused-filter=2` analyses do not: [RelativeRelocationIndex](../../core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp) reads `DT_SCE_RELA` and `DT_SCE_JMPREL` as virtual addresses and [StrictNidFilter](../../core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp) reads the SCE tables as file offsets, so they misread a PS4 executable's tables.
- The libc [SSE4a trap emulation](../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it.
- [getifaddrs](../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists the host's IPv4 and IPv6 addresses in FreeBSD's `ifaddrs` and sockaddr layouts. FreeBSD's per-interface AF_LINK entries (MAC, index, MTU) and their `if_data` are not produced, so interfaces without an address are omitted. Names are the host's (Linux `lo`, `eth0`; Windows NDIS names such as `ethernet_32769`, or `if<index>` when one does not fit 15 bytes), not the console's. A Linux point-to-point address without a peer loses `IFF_POINTOPOINT`, and an IPv4 broadcast address the host omits is computed from the netmask. On Windows the flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST`, derived from the adapter: PPP and tunnel adapters get neither `IFF_BROADCAST` nor `IFF_POINTOPOINT` because their peer address is not reported, and an address whose prefix length the host reports as unspecified gets a host mask.
- [gethostname](../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports the fixed guest name `AnyPS5` without querying the host machine name. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); the console's configurable hostname and its integration with `sysctl` and name resolution are unverified and not implemented.