Merge pull request #5262 from NaitikJoshiPro/pr/relinker-executable-tls

feat(relinker): executable TLS relocations (TPOFF64, DTPOFF64, DTPMOD64)
This commit is contained in:
Dean 2026-10-14 15:41:31 +13:00 committed by GitHub
commit a1e1b4d161
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 234 additions and 10 deletions

View File

@ -432,6 +432,8 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executable_segments.py $<TARGET_FILE:relinker>)
add_test(NAME linux_executable_exports COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_executable_exports.py $<TARGET_FILE:relinker>)
add_test(NAME executable_tls_relocations COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executable_tls_relocations.py $<TARGET_FILE:relinker>)
add_test(NAME linux_entry_argv COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_entry_argv.py $<TARGET_FILE:relinker>)
add_test(NAME linux_program_header_order COMMAND ${Python3_EXECUTABLE}

View File

@ -63,6 +63,7 @@ inline constexpr std::uint32_t PT_DYNAMIC = 2;
inline constexpr std::uint32_t PT_INTERP = 3;
inline constexpr std::uint32_t PT_NOTE = 4;
inline constexpr std::uint32_t PT_PHDR = 6;
inline constexpr std::uint32_t PT_TLS = 7;
inline constexpr std::uint32_t PT_GNU_EH_FRAME = 0x6474e550;
inline constexpr std::uint32_t PT_GNU_STACK = 0x6474e551;
inline constexpr std::uint32_t PT_GNU_RELRO = 0x6474e552;
@ -137,6 +138,9 @@ inline constexpr std::uint32_t R_X86_64_RELATIVE = 8;
inline constexpr std::uint32_t R_X86_64_GOTPCREL = 9;
inline constexpr std::uint32_t R_X86_64_32 = 10;
inline constexpr std::uint32_t R_X86_64_32S = 11;
inline constexpr std::uint32_t R_X86_64_DTPMOD64 = 16;
inline constexpr std::uint32_t R_X86_64_DTPOFF64 = 17;
inline constexpr std::uint32_t R_X86_64_TPOFF64 = 18;
inline constexpr std::uint32_t R_X86_64_GOTPCRELX = 41;
inline constexpr std::uint32_t R_X86_64_REX_GOTPCRELX = 42;
@ -144,6 +148,7 @@ 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 STT_TLS = 6;
inline constexpr std::uint8_t STV_DEFAULT = 0;
inline constexpr std::uint16_t SHN_UNDEF = 0;

View File

@ -7,7 +7,8 @@ namespace Elfpatcher::Windows {
class WindowsRelocationBuilder {
public:
PeRelocations Apply(WindowsLoadImage& image, const Domain::SysVDynamicSection& dynamicSection) const;
PeRelocations Apply(WindowsLoadImage& image, const Domain::SysVDynamicSection& dynamicSection,
const std::vector<Domain::ProgramHeader>& headers) const;
std::vector<std::uint8_t> BuildBaseRelocations(const std::vector<std::uint32_t>& targets) const;
};

View File

@ -46,7 +46,7 @@ std::vector<std::uint8_t> MacOsMachOPatcher::Patch(const std::vector<std::uint8_
Windows::WindowsLoadImage image(sourceElf, originalHeaders);
if (originalPltGotVaddr != 0)
image.GetRva(originalPltGotVaddr, 8);
const auto relocations = Windows::WindowsRelocationBuilder().Apply(image, dynamicSection);
const auto relocations = Windows::WindowsRelocationBuilder().Apply(image, dynamicSection, originalHeaders);
MacOsImageInput input;
input.Executable = true;

View File

@ -44,7 +44,7 @@ std::vector<std::uint8_t> WindowsPePatcher::Patch(const std::vector<std::uint8_t
image.GetRva(originalPltGotVaddr, 8);
report(500);
const WindowsRelocationBuilder relocationBuilder;
auto relocations = relocationBuilder.Apply(image, dynamicSection);
auto relocations = relocationBuilder.Apply(image, dynamicSection, originalHeaders);
report(1000);
auto sections = image.BuildSections();
report(1500);

View File

@ -1,11 +1,15 @@
#include <elfpatcher/windows/WindowsRelocationBuilder.hpp>
#include <elfpatcher/general/ElfConstants.hpp>
#include <io/BufferUtils.hpp>
#include <algorithm>
#include <bit>
#include <map>
#include <string>
namespace Elfpatcher::Windows {
PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Domain::SysVDynamicSection& dynamicSection) const {
PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Domain::SysVDynamicSection& dynamicSection,
const std::vector<Domain::ProgramHeader>& headers) const {
if (dynamicSection.DynSymData.empty() || dynamicSection.DynSymData.size() % 24 != 0)
throw Domain::RelinkerException("Invalid ELF dynamic symbol table size");
PeRelocations result;
@ -19,7 +23,7 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom
const auto addend = Io::ReadU64(table, offset + 16);
const auto type = static_cast<std::uint32_t>(info);
const auto symbol = info >> 32;
if ((plt && type != 7) || (type != 1 && type != 6 && type != 7 && type != 8))
if ((plt && type != 7) || (type != 1 && type != 6 && type != 7 && type != 8 && type != 16 && type != 17 && type != 18))
throw Domain::RelinkerException("Unsupported Windows relocation type " + std::to_string(type), target);
const auto rva = image.GetRva(target, 8);
const auto next = ranges.lower_bound(target);
@ -33,6 +37,21 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom
result.BaseRelocations.push_back(rva);
continue;
}
if (type == 16 || type == 17 || type == 18) {
if (symbol != 0)
throw Domain::RelinkerException("Imported TLS relocation " + std::to_string(type) + " is not supported for this target", target);
if (type == 16)
throw Domain::RelinkerException("Executable DTPMOD64 relocation is not supported for this target", target);
const auto tls = std::find_if(headers.begin(), headers.end(), [](const auto& header) { return header.Type == 7; });
if (tls == headers.end() || tls->MemorySize == 0)
throw Domain::RelinkerException("TLS relocation without a non-empty PT_TLS segment", target);
const auto alignment = std::max<std::uint64_t>(tls->Alignment, 1);
if (!std::has_single_bit(alignment) || tls->MemorySize > 0x7fff0000u)
throw Domain::RelinkerException("Invalid or unsupported ELF TLS layout", tls->Offset);
image.RequireWritable(target, 8);
image.WritePointer(target, type == 17 ? addend : addend - Io::AlignUp64(tls->MemorySize, alignment));
continue;
}
if (symbol == 0 || symbol >= dynamicSection.DynSymData.size() / 24)
throw Domain::RelinkerException("Invalid import symbol index", target);
const auto symbolOffset = static_cast<std::size_t>(symbol) * 24;

View File

@ -29,6 +29,9 @@ void ValidationPolicy::_initializeSupportedRelocationTypes() {
R_X86_64_GOTPCREL,
R_X86_64_32,
R_X86_64_32S,
R_X86_64_DTPMOD64,
R_X86_64_DTPOFF64,
R_X86_64_TPOFF64,
R_X86_64_GOTPCRELX,
R_X86_64_REX_GOTPCRELX,
};

View File

@ -84,7 +84,9 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection(
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>(((ref.Weak ? STB_WEAK : STB_GLOBAL) << 4) | STT_FUNC);
const bool tls = ref.RelocationTypeValue == R_X86_64_DTPMOD64 || ref.RelocationTypeValue == R_X86_64_DTPOFF64
|| ref.RelocationTypeValue == R_X86_64_TPOFF64;
return static_cast<std::uint8_t>(((ref.Weak ? STB_WEAK : STB_GLOBAL) << 4) | (tls ? STT_TLS : STT_FUNC));
};
static constexpr std::size_t kRelaEntSize = 24;

View File

@ -42,6 +42,9 @@ std::string RelinkerPipeline::relocationTypeName(std::uint32_t type) {
case 6: return "R_X86_64_GLOB_DAT";
case 7: return "R_X86_64_JUMP_SLOT";
case 10: return "R_X86_64_32";
case 16: return "R_X86_64_DTPMOD64";
case 17: return "R_X86_64_DTPOFF64";
case 18: return "R_X86_64_TPOFF64";
default: {
std::ostringstream oss;
oss << "UNKNOWN(" << type << ")";
@ -272,6 +275,31 @@ RelinkResult RelinkerPipeline::Relink(const std::vector<std::uint8_t>& sourceElf
return (raw[symOff + 4] & 0xf) == 3 && value == 0;
};
const auto isTlsRelocation = [](const std::uint32_t type) {
return type == R_X86_64_DTPMOD64 || type == R_X86_64_DTPOFF64 || type == R_X86_64_TPOFF64;
};
const bool hasTlsSegment = std::any_of(programHeaders.begin(), programHeaders.end(),
[](const ProgramHeader& ph) { return ph.Type == PT_TLS; });
const auto definedTlsSymbolValue = [&](const std::uint32_t symIdx, const FileByteOffset pos) -> std::optional<std::uint64_t> {
const FileByteOffset symOff = dynSymTabOffset + static_cast<FileByteOffset>(symIdx) * symEntSize;
if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < symEntSize)
throw RelinkerException("Symbol table entry out of bounds", symOff);
std::uint16_t section = 0;
std::memcpy(&section, raw.data() + symOff + 6, sizeof(section));
if (section == SHN_UNDEF)
return std::nullopt;
if ((raw[symOff + 4] & 15) != STT_TLS)
throw RelinkerException("TLS relocation names a defined symbol that is not STT_TLS", pos);
std::uint64_t value = 0;
std::memcpy(&value, raw.data() + symOff + 8, sizeof(value));
return value;
};
const auto definedTlsSymbol = [&](const std::uint32_t symIdx, const FileByteOffset pos) {
return definedTlsSymbolValue(symIdx, pos).has_value();
};
auto extractRela = [&](const FileByteOffset relaOff, const ByteCount relaSize) {
for (ByteCount off = 0; relaSize >= relaEntSize && off <= relaSize - relaEntSize; off += relaEntSize) {
const FileByteOffset pos = relaEntryPos(relaOff, off);
@ -294,6 +322,9 @@ RelinkResult RelinkerPipeline::Relink(const std::vector<std::uint8_t>& sourceElf
continue;
}
if (isTlsRelocation(relType) && (symIdx == 0 || definedTlsSymbol(symIdx, pos)))
continue;
const FileByteOffset symOff = dynSymTabOffset + static_cast<FileByteOffset>(symIdx) * symEntSize;
if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < 5)
throw RelinkerException("Symbol table entry out of bounds", symOff);
@ -457,7 +488,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector<std::uint8_t>& sourceElf
std::memcpy(buf.data() + pos + 16, &addend, 8);
};
auto extractRelative = [&](const FileByteOffset relaOff, const ByteCount relaSize) {
auto extractLocal = [&](const FileByteOffset relaOff, const ByteCount relaSize) {
for (ByteCount off = 0; relaSize >= relaEntSize && off <= relaSize - relaEntSize; off += relaEntSize) {
const FileByteOffset pos = relaEntryPos(relaOff, off);
std::uint64_t rOffset = 0, rInfo = 0;
@ -465,14 +496,32 @@ RelinkResult RelinkerPipeline::Relink(const std::vector<std::uint8_t>& sourceElf
std::memcpy(&rOffset, raw.data() + pos, 8);
std::memcpy(&rInfo, raw.data() + pos + 8, 8);
std::memcpy(&rAddend, raw.data() + pos + 16, 8);
const std::uint32_t symIdx = static_cast<std::uint32_t>(rInfo >> 32);
const std::uint32_t relType = static_cast<std::uint32_t>(rInfo & 0xffffffff);
if (relType == R_X86_64_RELATIVE)
if (relType == R_X86_64_RELATIVE) {
appendRela(dynSection.RelaData, rOffset, static_cast<std::uint64_t>(R_X86_64_RELATIVE), rAddend);
continue;
}
if (!isTlsRelocation(relType))
continue;
std::uint64_t offset = 0;
if (symIdx != 0) {
const auto value = definedTlsSymbolValue(symIdx, pos);
if (!value)
continue;
offset = *value;
}
if (!hasTlsSegment)
throw RelinkerException("TLS relocation in an executable without a PT_TLS segment", pos);
if (relType == R_X86_64_DTPMOD64 && rAddend != 0)
throw RelinkerException("DTPMOD64 relocation has a nonzero addend", pos);
const auto addend = relType == R_X86_64_DTPMOD64 ? 0 : static_cast<std::int64_t>(offset + static_cast<std::uint64_t>(rAddend));
appendRela(dynSection.RelaData, rOffset, static_cast<std::uint64_t>(relType), addend);
}
};
extractRelative(dynRelaOffset, dynRelaSize);
extractRelative(dynJmpRelOffset, dynJmpRelSize);
extractLocal(dynRelaOffset, dynRelaSize);
extractLocal(dynJmpRelOffset, dynJmpRelSize);
std::vector<CallRegistryEntry> entries;
entries.reserve(nidRefs.size());

View File

@ -0,0 +1,143 @@
"""Initial-exec TLS relocations in the executable (R_X86_64_TPOFF64, DTPOFF64, DTPMOD64), as a PS5 eboot
has them in its GOT. The Linux ELF keeps them dynamic and symbolless, with the variable's offset in the
TLS block as the addend, for the dynamic loader to resolve against where it puts the block; a named one
against a defined STT_TLS symbol is folded the same way, and one against an import stays named, typed
STT_TLS. The Windows and macOS images resolve TPOFF64 and DTPOFF64 statically against their block,
AlignUp(p_memsz, p_align) below the thread pointer, and reject what they cannot express."""
from pathlib import Path
import struct
import subprocess
import sys
import tempfile
from test_linux_load_alignment import fixture
from test_macos_output import MachO
from test_tls_function_coverage import pe_bytes_at
PT_LOAD = 1
PT_TLS = 7
DTPMOD64, DTPOFF64, TPOFF64, TPOFF32 = 16, 17, 18, 23
TLS_MEMSZ, TLS_ALIGN = 0x24, 16
TLS_BLOCK = 0x30
STRINGS = b"\0Tls\0Data\0ImportedTls\0"
TLS_NAME, DATA_NAME, IMPORT_NAME = 1, 5, 10
NULL, TLS_SYMBOL, DATA_SYMBOL, IMPORTED = 0, 1, 2, 3
def image(relocations, tls=True):
"""The load-alignment fixture with a writable segment (vaddr 0x1000) holding a GOT and an 8-byte
TLS template, a symbol table and the given (offset, type, symbol, addend) relocations."""
data = fixture()
struct.pack_into("<Q", data, 24, 0x200)
data[0x4200:0x4206] = b"\xb8\x2a\x00\x00\x00\xc3"
struct.pack_into("<IIQQQQQQ", data, 64 + 2 * 56, PT_LOAD, 6, 0x5000, 0x1000, 0x1000, 0x1000, 0x1000, 0x4000)
if tls:
struct.pack_into("<IIQQQQQQ", data, 64 + 3 * 56, PT_TLS, 4, 0x5800, 0x1800, 0x1800, 8, TLS_MEMSZ, TLS_ALIGN)
data[0x5800:0x5808] = bytes(range(1, 9))
data[0x4600:0x4600 + len(STRINGS)] = STRINGS
struct.pack_into("<IBBHQQ", data, 0x4900 + TLS_SYMBOL * 24, TLS_NAME, 0x16, 0, 1, 0x10, 4)
struct.pack_into("<IBBHQQ", data, 0x4900 + DATA_SYMBOL * 24, DATA_NAME, 0x11, 0, 1, 0x1100, 8)
struct.pack_into("<IBBHQQ", data, 0x4900 + IMPORTED * 24, IMPORT_NAME, 0x16, 0, 0, 0, 0)
for index, (offset, kind, symbol, addend) in enumerate(relocations):
struct.pack_into("<QQq", data, 0x4700 + index * 24, offset, symbol << 32 | kind, addend)
tags = [(5, 0x600), (10, len(STRINGS)), (6, 0x900), (11, 24), (7, 0x700), (8, len(relocations) * 24), (9, 24), (0, 0)]
struct.pack_into("<IIQQQQQQ", data, 120, 2, 6, 0x4800, 0x800, 0x800, len(tags) * 16, len(tags) * 16, 8)
for index, tag in enumerate(tags):
struct.pack_into("<qQ", data, 0x4800 + index * 16, *tag)
return data
def relink(relinker, directory, name, data, *flags):
source = Path(directory) / (name + ".elf")
output = Path(directory) / (name + ".out")
source.write_bytes(data)
result = subprocess.run([str(relinker), "--skip-sce-module", *flags, str(source), str(output)],
capture_output=True, text=True, timeout=20)
return result, output
def dynamic_relocations(elf):
"""The output's (offset, type, symbol name, symbol type, addend) relocations from DT_RELA."""
phoff, = struct.unpack_from("<Q", elf, 0x20)
phentsize, phnum = struct.unpack_from("<HH", elf, 0x36)
headers = [struct.unpack_from("<IIQQQQQQ", elf, phoff + index * phentsize) for index in range(phnum)]
def offset(address):
for kind, _, file_offset, vaddr, _, size, _, _ in headers:
if kind == PT_LOAD and vaddr <= address < vaddr + size:
return file_offset + address - vaddr
raise AssertionError(f"unmapped address {address:#x}")
dynamic = next(header for header in headers if header[0] == 2)
tags = {}
for position in range(dynamic[2], dynamic[2] + dynamic[5], 16):
tag, value = struct.unpack_from("<qQ", elf, position)
if tag == 0:
break
tags.setdefault(tag, value)
symbols, strings = offset(tags[6]), offset(tags[5])
result = []
for position in range(offset(tags[7]), offset(tags[7]) + tags[8], 24):
target, info, addend = struct.unpack_from("<QQq", elf, position)
name, symbol_info = struct.unpack_from("<IB", elf, symbols + (info >> 32) * 24)
end = elf.index(b"\0", strings + name)
result.append((target, info & 0xffffffff, elf[strings + name:end].decode(), symbol_info & 15, addend))
return result
def macho_bytes_at(data, address, size):
for segment in MachO(data).segments:
if segment["vmaddr"] <= address and address + size <= segment["vmaddr"] + segment["filesize"]:
offset = segment["fileoff"] + address - segment["vmaddr"]
return data[offset:offset + size]
raise AssertionError(f"unmapped address {address:#x}")
def main():
relinker = Path(sys.argv[1]).resolve()
with tempfile.TemporaryDirectory(prefix="anyps5-exec-tls-") as directory:
local = [(0x1000, TPOFF64, NULL, 4), (0x1008, DTPOFF64, NULL, 8), (0x1010, TPOFF64, TLS_SYMBOL, 2)]
result, output = relink(relinker, directory, "linux", image(local + [(0x1018, DTPMOD64, NULL, 0),
(0x1020, TPOFF64, IMPORTED, 0)]))
assert result.returncode == 0, (result.stdout, result.stderr)
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 any(struct.unpack_from("<I", elf, 64 + index * 56)[0] == PT_TLS
for index in range(struct.unpack_from("<H", elf, 0x38)[0])), "PT_TLS must be kept"
result, output = relink(relinker, directory, "windows", image(local), "--windows")
assert result.returncode == 0, (result.stdout, result.stderr)
pe = output.read_bytes()
values = [struct.unpack("<q", pe_bytes_at(pe, 0x10000 + address, 8))[0] for address in (0x1000, 0x1008, 0x1010)]
assert values == [4 - TLS_BLOCK, 8, 0x12 - TLS_BLOCK], values
result, output = relink(relinker, directory, "macos", image(local), "--macos")
assert result.returncode == 0, (result.stdout, result.stderr)
macho = output.read_bytes()
values = [struct.unpack("<q", macho_bytes_at(macho, 0x140010000 + address, 8))[0] for address in (0x1000, 0x1008, 0x1010)]
assert values == [4 - TLS_BLOCK, 8, 0x12 - TLS_BLOCK], values
failures = [
("tpoff32", [(0x1000, TPOFF32, NULL, 0)], True, (), "Unsupported relocation type 0x17"),
("not-tls-symbol", [(0x1000, TPOFF64, DATA_SYMBOL, 0)], True, (), "not STT_TLS"),
("no-tls-segment", [(0x1000, TPOFF64, NULL, 0)], False, (), "without a PT_TLS segment"),
("dtpmod-addend", [(0x1000, DTPMOD64, NULL, 1)], True, (), "DTPMOD64 relocation has a nonzero addend"),
("windows-dtpmod", [(0x1000, DTPMOD64, NULL, 0)], True, ("--windows",), "DTPMOD64 relocation is not supported"),
("windows-import", [(0x1000, TPOFF64, IMPORTED, 0)], True, ("--windows",), "Imported TLS relocation"),
("macos-dtpmod", [(0x1000, DTPMOD64, NULL, 0)], True, ("--macos",), "DTPMOD64 relocation is not supported"),
("macos-import", [(0x1000, TPOFF64, IMPORTED, 0)], True, ("--macos",), "Imported TLS relocation"),
]
for name, relocations, has_tls, flags, error in failures:
result, output = relink(relinker, directory, name, image(relocations, has_tls), *flags)
assert result.returncode == 2 and error in result.stderr, (name, result.returncode, result.stderr)
assert not output.exists(), name
print("Executable TLS relocation test passed")
if __name__ == "__main__":
main()