fix(relinker): move a short guest TLS access's follower branch into its stub

A short fs-prefixed TLS access is patched with a 5-byte jump by
relocating the following instructions into the stub, but any branch
there was rejected outright. NHL 27 hits this on every stack-protector
epilogue: fs:xor rax,[rbx] immediately followed by jne __stack_chk_fail.

Allow relative conditional branches and short jumps: re-encode rel8
forms as their rel32 equivalents, rebase the displacement when the
stub is emitted, and keep rejecting loop/jrcxz, calls, returns, and
segment-prefixed followers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
poonasor 2026-10-10 21:50:23 -04:00
parent d146ec1b66
commit c6f934a3d0
3 changed files with 54 additions and 4 deletions

View File

@ -345,7 +345,7 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $<TARGET_FILE:relinker>)
add_test(NAME empty_tls COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_empty_tls.py $<TARGET_FILE:relinker>)
set(tlsCoverageGroups metadata padding register_loads displacement_bounds alu_execution register_address_loads rejected)
set(tlsCoverageGroups metadata padding register_loads displacement_bounds alu_execution register_address_loads moved_branch rejected)
foreach(register IN ITEMS 0 1 2 3 5 6 7 8 9 10 11 12 13 14 15)
list(APPEND tlsCoverageGroups displacement_r${register})
endforeach()

View File

@ -23,6 +23,8 @@ struct MovedInstruction {
std::uint32_t Rva;
std::vector<std::uint8_t> Bytes;
std::optional<std::size_t> RipDisplacement;
std::optional<std::uint32_t> BranchTarget;
std::size_t BranchDisplacementOffset = 0;
};
struct TlsAccess {
@ -145,9 +147,28 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector<std::uint8_t>& source, co
if (length >= header.FileSize - offset)
throw Domain::RelinkerException("Short guest TLS instruction ends its segment", header.Offset + offset);
const auto next = decoder.DecodeInstruction(bytes + length, header.FileSize - offset - length);
if (next.FlowKind != Codegen::ControlFlowKind::Sequential || next.HasBranchTarget || next.SegmentPrefix != 0)
const bool relativeBranch =
(next.FlowKind == Codegen::ControlFlowKind::ConditionalBranch && (next.IsTwoByteOpcode ? next.Opcode >= 0x80 && next.Opcode <= 0x8f : next.Opcode >= 0x70 && next.Opcode <= 0x7f)) ||
(next.FlowKind == Codegen::ControlFlowKind::UnconditionalJump && !next.HasRipRelativeDisp && !next.IsTwoByteOpcode && (next.Opcode == 0xe9 || next.Opcode == 0xeb));
if (!relativeBranch && (next.FlowKind != Codegen::ControlFlowKind::Sequential || next.HasBranchTarget || next.SegmentPrefix != 0))
throw Domain::RelinkerException("Short guest TLS instruction is followed by an instruction that cannot move", header.Offset + offset + length);
moved.push_back({CheckedRva(rva + length), std::vector<std::uint8_t>(bytes + length, bytes + length + next.Length), next.HasRipRelativeDisp ? std::optional<std::size_t>(next.RipRelativeDispOffset) : std::nullopt});
std::vector<std::uint8_t> movedBytes(bytes + length, bytes + length + next.Length);
std::optional<std::uint32_t> branchTarget;
std::size_t branchDisplacementOffset = 0;
if (relativeBranch) {
const auto target = static_cast<std::int64_t>(rva) + static_cast<std::int64_t>(length + next.Length) + next.BranchDisp;
if (target < 0 || target > std::numeric_limits<std::uint32_t>::max())
throw Domain::RelinkerException("Short guest TLS instruction is followed by a branch beyond the address space", header.Offset + offset + length);
branchTarget = static_cast<std::uint32_t>(target);
if (!next.IsTwoByteOpcode && next.Opcode != 0xe9) {
movedBytes = next.Opcode == 0xeb ? std::vector<std::uint8_t>{0xe9} : std::vector<std::uint8_t>{0x0f, static_cast<std::uint8_t>(0x80 | (next.Opcode & 0xf))};
branchDisplacementOffset = movedBytes.size();
movedBytes.insert(movedBytes.end(), 4, 0);
} else {
branchDisplacementOffset = next.Length - 4;
}
}
moved.push_back({CheckedRva(rva + length), std::move(movedBytes), next.HasRipRelativeDisp ? std::optional<std::size_t>(next.RipRelativeDispOffset) : std::nullopt, branchTarget, branchDisplacementOffset});
length += next.Length;
}
accesses.push_back({rva, header.Offset + offset, length, false, 0, addressRegister, 0, isAlu ? bytes[position] : std::uint8_t{0}, *address, wide, std::move(moved)});
@ -357,6 +378,12 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector<std::uint8_t>& source, co
throw Domain::RelinkerException("Moved RIP-relative instruction exceeds rel32 range from its TLS stub", access.FileOffset);
Io::WriteU32(bytes, *instruction.RipDisplacement, static_cast<std::uint32_t>(static_cast<std::int32_t>(displacement)));
}
if (instruction.BranchTarget) {
const auto displacement = static_cast<std::int64_t>(*instruction.BranchTarget) - (static_cast<std::int64_t>(code.GetRva()) + static_cast<std::int64_t>(bytes.size()));
if (displacement < std::numeric_limits<std::int32_t>::min() || displacement > std::numeric_limits<std::int32_t>::max())
throw Domain::RelinkerException("Moved branch exceeds rel32 range from its TLS stub", access.FileOffset);
Io::WriteU32(bytes, instruction.BranchDisplacementOffset, static_cast<std::uint32_t>(static_cast<std::int32_t>(displacement)));
}
for (const auto byte : bytes)
code.Emit({byte});
}

View File

@ -511,7 +511,7 @@ def add_alias(image, size, begin=0x1200):
def main():
groups = ("metadata", "padding", "register_loads", "displacement_bounds", "alu_execution",
"register_address_loads", "rejected")
"register_address_loads", "moved_branch", "rejected")
groups += tuple(f"displacement_r{register}" for register in range(16) if register != 4)
groups += tuple(f"alu_{name}" for name in ALU_READ)
parser = argparse.ArgumentParser(description="Run Windows guest TLS function coverage fixtures")
@ -699,12 +699,35 @@ def main():
stub = pe_bytes_at(pe, 0x10000 + address + 5 + struct.unpack_from("<i", patched, 1)[0], 96)
head, tail = register_alu_stub(instruction, opcode, register, wide)
assert stub.startswith(head) and tail + moved in stub, (name, head.hex(), (tail + moved).hex(), stub.hex())
if args.group in ("all", "moved_branch"):
prologue = bytes.fromhex("48 c7 c3 f0 ff ff ff 48 31 c0")
rel8_body = prologue + bytes.fromhex("64 48 33 03 75 06 31 c0 c3 90 90 90 b8 2a 00 00 00 c3")
rel32_body = prologue + bytes.fromhex("64 48 33 03 0f 85") + struct.pack("<i", 0x1259 - 0x1254) + bytes.fromhex("31 c0 c3 90 90 b8 2a 00 00 00 c3")
jmp_body = prologue + bytes.fromhex("64 48 33 03 eb 06 b8 01 00 00 00 c3 90 b8 2a 00 00 00 c3")
for name, body, access, target, branch_opcode in (
("moved-rel8-branch", rel8_body, 0x124A, 0x1256, "0f 85"),
("moved-rel32-branch", rel32_body, 0x124A, 0x1259, "0f 85"),
("moved-rel8-jump", jmp_body, 0x124A, 0x1256, "e9"),
):
image = make_image("register", "unwind", body=body)
convert(name, image, tls_address=access)
pe = (work / (name + ".exe")).read_bytes()
patched = pe_bytes_at(pe, 0x10000 + access, 6)
assert patched[0] == 0xe9 and patched[5] == 0x90, (name, patched.hex())
stub_address = 0x10000 + access + 5 + struct.unpack_from("<i", patched, 1)[0]
stub = pe_bytes_at(pe, stub_address, 128)
encoding = bytes.fromhex(branch_opcode)
offset = stub.find(encoding)
assert offset >= 0, (name, stub.hex())
displacement = struct.unpack_from("<i", stub, offset + len(encoding))[0]
assert displacement == (0x10000 + target) - (stub_address + offset + len(encoding) + 4), (name, displacement)
if args.group in ("all", "rejected"):
for name, instruction in {
"short-before-return": bytes.fromhex("64 48 8b 03 c3"),
"short-before-call": bytes.fromhex("64 48 8b 03 e8 00 00 00 00 c3"),
"short-before-tls": bytes.fromhex("64 48 8b 03 64 48 8b 03 c3"),
"short-alu-before-return": bytes.fromhex("64 48 33 03 c3"),
"short-before-loop": bytes.fromhex("64 48 33 03 e2 00"),
}.items():
convert(name, make_image("register", "unwind", body=instruction),
"Short guest TLS instruction is followed by an instruction that cannot move", error_offset=0x1244)