fix(relinker): refuse branches into trapped guest GS instructions on Windows

Windows output replaced each guest GS memory instruction with UD2 and
NOP fill without checking the decoded direct branch targets, so a branch
past the first byte of such an instruction was converted and landed in
the second UD2 byte or the NOP fill. A branch target strictly inside a
trapped GS instruction is now refused with "Branch enters a guest GS
instruction", the rule macOS applies since #7706 and this builder
already applies to its FS rewrites. The branch targets are sorted before
the trap loop, which runs before the PT_TLS early returns, so images
without PT_TLS are covered; guest modules go through the same builder. A
branch to the first byte still converts and reaches the UD2.
This commit is contained in:
roboterspinne committed 2026-10-24 20:18:50 +02:00
1 parent befe264ffb
commit 7424949b8b
2 files changed
+9 -4

No files matched your search

@@ -221,8 +221,14 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector<std::uint8_t>& source, co
}
report(9000);
for (const auto& [rva, length] : gsTraps)
std::sort(branchTargets.begin(), branchTargets.end());
branchTargets.erase(std::unique(branchTargets.begin(), branchTargets.end()), branchTargets.end());
for (const auto& [rva, length] : gsTraps) {
const auto target = std::upper_bound(branchTargets.begin(), branchTargets.end(), rva);
if (target != branchTargets.end() && *target < rva + length)
throw Domain::RelinkerException("Branch enters a guest GS instruction", *target);
trapInstruction(sections, rva, length);
}
if (tls == nullptr) {
if (!accesses.empty())
throw Domain::RelinkerException("Guest TLS access without PT_TLS");
@@ -274,9 +280,6 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector<std::uint8_t>& source, co
code.PatchBranch(skipCallback, code.GetRva());
code.Emit({0xc3});
std::sort(branchTargets.begin(), branchTargets.end());
branchTargets.erase(std::unique(branchTargets.begin(), branchTargets.end()), branchTargets.end());
std::uint64_t patched = 0;
for (const auto& access : accesses) {
if (patched % 256 == 0) report(9000 + 900 * patched / accesses.size());
@@ -777,6 +777,8 @@ def main():
conflicting[232:344] = unwind[232:344]
struct.pack_into("<H", conflicting, 56, 5)
convert("conflicting-function-extents", conflicting, "Code analysis: conflicting function ranges")
convert("branch-into-gs", make_image("register", "unwind", body=bytes.fromhex("74 01 65 48 8b 0c 25 78 03 00 00 c3")),
"Branch enters a guest GS instruction", error_offset=0x11243)
def check_gs_traps(cases):
for name, instruction, trapped in cases: