Merge pull request #340 from QDarkRex/fix/relinker-tls-fs-disp32

fix(relinker): load Windows guest TLS values at signed displacements
This commit is contained in:
Dean 2026-10-04 20:39:50 +13:00 committed by GitHub
commit 9f9a8f5a53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 159 additions and 5 deletions

View File

@ -8,6 +8,7 @@
#include <bit>
#include <limits>
#include <set>
#include <string>
namespace Elfpatcher::Windows {
@ -15,10 +16,12 @@ namespace {
struct TlsAccess {
std::uint32_t Rva;
Domain::FileByteOffset FileOffset;
std::size_t Length;
bool StoreImmediate;
std::uint32_t Immediate;
std::uint8_t Register;
std::uint32_t Displacement;
};
void patchAccess(std::vector<PeSection>& sections, const TlsAccess& access, const std::uint32_t target) {
@ -78,11 +81,11 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector<std::uint8_t>& source, co
else if (value != 0x64 && !(prefix + 1 == position && value >= 0x40 && value <= 0x4f)) supportedPrefixes = false;
}
const auto loadRegister = info.Length - position == 7 ? static_cast<std::uint8_t>(((bytes[position + 1] >> 3) & 7) | ((info.RexPrefix & 4) << 1)) : std::uint8_t{4};
const bool loadPointer = supportedPrefixes && (info.RexPrefix == 0x48 || info.RexPrefix == 0x4c) && loadRegister != 4 && bytes[position] == 0x8b && (bytes[position + 1] & 0xc7) == 0x04 && bytes[position + 2] == 0x25 && Io::ReadU32(source, header.Offset + offset + position + 3) == 0;
const bool loadValue = supportedPrefixes && (info.RexPrefix == 0x48 || info.RexPrefix == 0x4c) && loadRegister != 4 && bytes[position] == 0x8b && (bytes[position + 1] & 0xc7) == 0x04 && bytes[position + 2] == 0x25;
const bool storeImmediate = supportedPrefixes && !hasOperandSizePrefix && (info.RexPrefix == 0 || info.RexPrefix == 0x40) && info.Length - position == 11 && bytes[position] == 0xc7 && bytes[position + 1] == 0x04 && bytes[position + 2] == 0x25 && Io::ReadU32(source, header.Offset + offset + position + 3) == 0x28;
if (!loadPointer && !storeImmediate)
if (!loadValue && !storeImmediate)
throw Domain::RelinkerException("Unsupported Windows guest TLS instruction", header.Offset + offset);
accesses.push_back({rva, info.Length, storeImmediate, storeImmediate ? Io::ReadU32(source, header.Offset + offset + position + 7) : 0, storeImmediate ? std::uint8_t{0} : loadRegister});
accesses.push_back({rva, header.Offset + offset, info.Length, storeImmediate, storeImmediate ? Io::ReadU32(source, header.Offset + offset + position + 7) : 0, storeImmediate ? std::uint8_t{0} : loadRegister, storeImmediate ? 0 : Io::ReadU32(source, header.Offset + offset + position + 3)});
}
}
}
@ -106,6 +109,13 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector<std::uint8_t>& source, co
const auto blockSize = CheckedRva((tls->MemorySize + alignment - 1) & ~(alignment - 1));
const auto templateOffset = CheckedRva((64 + alignment - 1) & ~(alignment - 1));
constexpr std::uint32_t threadControlBlockSize = 0x30;
constexpr std::int64_t stackGuardDisplacement = 0x28;
for (const auto& access : accesses) {
if (access.StoreImmediate || access.Displacement == 0) continue;
const auto displacement = static_cast<std::int64_t>(std::bit_cast<std::int32_t>(access.Displacement));
if (displacement != stackGuardDisplacement && (displacement > 0 || displacement < -static_cast<std::int64_t>(blockSize)))
throw Domain::RelinkerException("Windows guest TLS load displacement " + std::to_string(displacement) + " is not in the thread TLS block, 0 or the stack guard at 0x28", access.FileOffset);
}
PeSection data{".gtls", nextRva, SectionRead | SectionWrite | 0x40u, std::vector<std::uint8_t>(templateOffset + blockSize + threadControlBlockSize)};
const auto indexRva = nextRva + 40;
@ -143,6 +153,10 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector<std::uint8_t>& source, co
if (preserveCounter) code.Emit({0x51});
if (preserveAccumulator) code.Emit({0x50});
loadPointer();
if (!access.StoreImmediate && access.Displacement != 0) {
code.Emit({0x48, 0x8b, 0x80});
code.U32(access.Displacement);
}
if (access.StoreImmediate) {
code.Emit({0xc7, 0x40, 0x28});
code.U32(access.Immediate);

View File

@ -26,6 +26,118 @@ def register_load(register):
return len(prefix), prefix + load + rest + restore + bytes.fromhex("31 c0 c3")
def fs_load(register, displacement):
return bytes([0x64, 0x48 | (register >> 3) << 2, 0x8b, 0x04 | (register & 7) << 3, 0x25]) + struct.pack("<i", displacement)
def displacement_load(register, displacement, flags, round_trip=False):
saved = (3, 5, 6, 7, 12, 13, 14, 15)
code = bytearray()
failures = []
def emit(data):
code.extend(data)
def check():
emit(bytes.fromhex("0f 85 00 00 00 00"))
failures.append(len(code) - 4)
def immediate(target, value):
emit(bytes([0x48 | (target >> 3), 0xb8 | (target & 7)]) + struct.pack("<Q", value))
def stack_store(target, offset):
emit(bytes([0x48 | (target >> 3) << 2, 0x89, 0x84 | (target & 7) << 3, 0x24]) + struct.pack("<i", offset))
for target in saved:
emit((b"\x41" if target >= 8 else b"") + bytes([0x50 | (target & 7)]))
emit(bytes.fromhex("48 8d a4 24 00 ff ff ff"))
emit(fs_load(0, 0))
stack_store(0, 136)
positive = {8: 0x123456789abcdef0, 16: 0xfedcba9876543210, 40: 0xabcdef1278563412}
for offset, value in positive.items():
immediate(1, value)
emit(bytes([0x48, 0x89, 0x48, offset]))
immediate(1, 0x2468ace013579bdf)
emit(bytes.fromhex("48 89 08"))
if displacement == 0:
emit(fs_load(2, 40))
immediate(1, positive[40])
emit(bytes.fromhex("48 39 ca"))
check()
else:
value = 0xabcdef1289abcdef if round_trip else {
**positive, -64: 0x8877665544332211, -40: 0x1122334455667788, -8: 0,
}[displacement]
immediate(1, value)
stack_store(1, 136)
markers = [0x1020304050607000 + target * 0x101 for target in range(16)]
for target in range(16):
if target != 4:
immediate(target, markers[target])
emit(b"\x68" + struct.pack("<I", flags) + b"\x9d")
emit(bytes.fromhex("48 8d a4 24 70 ff ff ff 9c 8f 84 24 20 01 00 00 48 8d a4 24 90 00 00 00"))
for offset in range(-128, 0, 8):
emit(bytes([0x48, 0xc7, 0x44, 0x24, offset & 255]) + struct.pack("<I", 0x34560000 - offset))
if round_trip:
emit(bytes.fromhex("64 c7 04 25 28 00 00 00 ef cd ab 89"))
load_offset = len(code)
emit(fs_load(register, displacement))
for target in range(16):
stack_store(target, target * 8)
emit(bytes.fromhex("48 8d a4 24 70 ff ff ff 9c 8f 84 24 10 01 00 00 48 8d a4 24 90 00 00 00"))
emit(bytes.fromhex("48 8b 84 24 90 00 00 00 48 39 84 24 80 00 00 00"))
check()
for target in range(16):
if target == 4:
emit(bytes.fromhex("48 8d 04 24"))
elif target == register:
emit(bytes.fromhex("48 8b 84 24 88 00 00 00"))
else:
immediate(0, markers[target])
emit(bytes.fromhex("48 39 84 24") + struct.pack("<i", target * 8))
check()
for offset in range(-128, 0, 8):
emit(bytes([0x48, 0x81, 0x7c, 0x24, offset & 255]) + struct.pack("<I", 0x34560000 - offset))
check()
emit(bytes.fromhex("b8 2a 00 00 00 eb 05"))
failure = len(code)
emit(bytes.fromhex("b8 01 00 00 00 48 8d a4 24 00 01 00 00"))
for target in reversed(saved):
emit((b"\x41" if target >= 8 else b"") + bytes([0x58 | (target & 7)]))
emit(b"\xc3")
for offset in failures:
struct.pack_into("<i", code, offset, failure - offset - 4)
return load_offset, code
def displacement_cases():
for register in range(16):
if register == 4:
continue
for flags in (0x202, 0xad7):
for displacement, round_trip in ((0, False), (40, False), (-64, False), (-40, False),
(-8, False), (40, True)):
offset, body = displacement_load(register, displacement, flags, round_trip)
image = make_image("register", "unwind", body=body)
struct.pack_into("<IIQQQQQQ", image, 176, 7, 4, 0x800, 0x800, 0x800, 48, 48, 32)
image[0x800:0x830] = bytes(48)
struct.pack_into("<Q", image, 0x800, 0x8877665544332211)
struct.pack_into("<Q", image, 0x818, 0x1122334455667788)
name = f"displacement-{register}-{flags:x}-{displacement}-{'round-trip' if round_trip else 'load'}"
yield name, image, 0x1240 + offset
def displacement_bounds_cases():
for register in (0, 12):
for displacement in (1, 8, 0x10, 0x20, 0x27, 0x29, 0x2c, 0x30, -65, -0x80000000, 0x7fffffff):
image = make_image("register", "unwind", body=fs_load(register, displacement) + b"\xc3")
struct.pack_into("<IIQQQQQQ", image, 176, 7, 4, 0x800, 0x800, 0x800, 48, 48, 32)
if register == 12:
image[176:232], image[288:344] = image[288:344], image[176:232]
error = f"Windows guest TLS load displacement {displacement} is not in the thread TLS block, 0 or the stack guard at 0x28"
yield f"displacement-bounds-{register}-{displacement}", image, error
def make_image(transfer, metadata, extent=None, body=None):
image = fixture()
image.extend(b"\x90" * 0x1000)
@ -90,7 +202,7 @@ def main():
with tempfile.TemporaryDirectory(prefix="anyps5-tls-coverage-") as directory:
work = Path(directory)
def convert(name, image, error=None, tls_address=0x1240):
def convert(name, image, error=None, tls_address=0x1240, displacement=0, error_offset=None):
source = work / (name + ".elf")
output = source.with_suffix(".exe")
source.write_bytes(image)
@ -98,10 +210,18 @@ def main():
capture_output=True, text=True, timeout=30)
if error is not None:
assert result.returncode == 2 and error in result.stderr and not output.exists(), result
if error_offset is not None:
assert f"(offset {error_offset:#x})" in result.stderr, (name, result.stderr)
return
assert result.returncode == 0, (name, result.stdout, result.stderr)
pe = output.read_bytes()
assert pe_bytes_at(pe, 0x10000 + tls_address, 1) == b"\xe9", name
patched_address = 0x10000 + tls_address
patched = pe_bytes_at(pe, patched_address, 5)
assert patched[0] == 0xe9, name
if displacement != 0:
stub_address = patched_address + 5 + struct.unpack_from("<i", patched, 1)[0]
load = bytes.fromhex("48 8b 80") + struct.pack("<i", displacement)
assert load in pe_bytes_at(pe, stub_address, 64), name
assert pe_bytes_at(pe, 0x11850, len(TLS_LOAD)) == TLS_LOAD, name
if os.name == "nt":
executed = subprocess.run([str(output)], capture_output=True, timeout=30)
@ -126,6 +246,26 @@ def main():
convert("load-register-4", image, "Unsupported Windows guest TLS instruction")
else:
convert("load-register-" + str(register), image, tls_address=0x1240 + offset)
for name, image, address in displacement_cases():
displacement = struct.unpack_from("<i", image, address + 5)[0]
convert(name, image, tls_address=address, displacement=displacement)
for name, image, error in displacement_bounds_cases():
convert(name, image, error, error_offset=0x1240)
rejected = {
"rsp-displacement": fs_load(4, 40),
"dword-load": bytes.fromhex("64 8b 04 25 28 00 00 00"),
"register-address": bytes.fromhex("64 48 8b 00"),
"gs-load": bytes.fromhex("65 48 8b 04 25 28 00 00 00"),
"rex-b-load": bytes.fromhex("64 49 8b 04 25 28 00 00 00"),
"rex-x-load": bytes.fromhex("64 4a 8b 04 25 28 00 00 00"),
"compare": bytes.fromhex("64 48 3b 04 25 28 00 00 00"),
"subtract": bytes.fromhex("64 48 2b 04 25 28 00 00 00"),
"add": bytes.fromhex("64 48 03 04 25 28 00 00 00"),
"xor": bytes.fromhex("64 48 33 04 25 28 00 00 00"),
}
for name, instruction in rejected.items():
convert(name, make_image("register", "unwind", body=instruction + b"\xc3"),
"Unsupported Windows guest TLS instruction")
conflicting = make_image("register", "symbol")
unwind = make_image("register", "unwind", 0x51)
conflicting[0x900:0x9a0] = unwind[0x900:0x9a0]