From 606fa2ef9124f049b26a9f30bc9cf76d5054be54 Mon Sep 17 00:00:00 2001 From: Josip Parlov Date: Wed, 14 Oct 2026 06:01:10 +0200 Subject: [PATCH] fix(relinker): take red-zone framing's function bounds from .eh_frame FDEs Function starts were direct call targets plus 16-byte-aligned code after int3 padding. A function reached only through a pointer and not preceded by int3 then ended up inside the previous region, and its pop rbp; ret exit could get the epilogue lea without a matching prologue. The bounds now come from the executable's .eh_frame FDEs (the existing EhFrameReader), each function is decoded from its FDE start to its FDE end, and code outside every FDE is not framed. Conversion stops with an error when the executable has no FDE. Sites added in one pass also count as taken for the next function. --- .../include/codegen/x86/RedZoneFramer.hpp | 6 ++ .../codegen/src/x86/RedZoneFramer.cpp | 87 ++++++++----------- .../tests/RedZoneFramerExecutionTests.cpp | 6 +- .../codegen/tests/RedZoneFramerTests.cpp | 74 ++++++++++++---- .../relinker/src/pipeline/Conversion.cpp | 9 +- .../relinker/tests/GamePatchTests.cpp | 20 +++++ docs/dev/debt/relinker.md | 2 +- 7 files changed, 132 insertions(+), 72 deletions(-) diff --git a/core/relinker/codegen/include/codegen/x86/RedZoneFramer.hpp b/core/relinker/codegen/include/codegen/x86/RedZoneFramer.hpp index b4dee9f80c..a23fa23206 100644 --- a/core/relinker/codegen/include/codegen/x86/RedZoneFramer.hpp +++ b/core/relinker/codegen/include/codegen/x86/RedZoneFramer.hpp @@ -11,6 +11,11 @@ namespace Codegen { +struct RedZoneFunction { + std::uint64_t Begin = 0; + std::uint64_t End = 0; +}; + struct RedZoneFrameReport { std::size_t Functions = 0; std::size_t RedZoneFunctions = 0; @@ -31,6 +36,7 @@ struct RedZoneDisplacement { std::vector FrameRedZoneFunctions( const std::vector& fileBytes, const std::vector& codeSegments, + const std::vector& functions, const std::vector& existing, RedZoneFrameReport& report, std::vector& displacements diff --git a/core/relinker/codegen/src/x86/RedZoneFramer.cpp b/core/relinker/codegen/src/x86/RedZoneFramer.cpp index 5e79fc46d4..b373ed3a9f 100644 --- a/core/relinker/codegen/src/x86/RedZoneFramer.cpp +++ b/core/relinker/codegen/src/x86/RedZoneFramer.cpp @@ -211,6 +211,7 @@ struct OperandStub { void frameSegment( const std::vector& fileBytes, const Domain::ProgramHeader& ph, + const std::vector& functions, const std::vector& existing, std::vector& sites, std::vector& displacements, @@ -220,24 +221,15 @@ void frameSegment( const auto segSize = static_cast(ph.FileSize); if (segOffset > fileBytes.size() || segSize > fileBytes.size() - segOffset) return; const std::vector seg(fileBytes.begin() + static_cast(segOffset), fileBytes.begin() + static_cast(segOffset + segSize)); - const auto matches = MakeInstructionScanner()->ScanCodeSection(seg, 0, seg.size()); + const auto scanner = MakeInstructionScanner(); const X64InstructionDecoder decoder; - std::vector insns; - insns.reserve(matches.size()); - for (const auto& match : matches) insns.push_back({static_cast(match.Offset), match.Length, decoder.DecodeInstruction(seg.data() + match.Offset, match.Length)}); std::set targets; - std::set starts; - for (std::size_t i = 0; i < insns.size(); ++i) { - const auto& insn = insns[i]; - if (insn.Info.HasBranchTarget && !insn.Info.HasRipRelativeDisp) { - const auto target = static_cast(insn.Offset + insn.Length) + insn.Info.BranchDisp; - if (target >= 0 && static_cast(target) < seg.size()) { - if (insn.Info.FlowKind == ControlFlowKind::Call) starts.insert(static_cast(target)); - else targets.insert(static_cast(target)); - } - } - if (i > 0 && insns[i - 1].Length == 1 && seg[insns[i - 1].Offset] == 0xcc && seg[insn.Offset] != 0xcc && insn.Offset % 16 == 0) starts.insert(insn.Offset); + for (const auto& match : scanner->ScanCodeSection(seg, 0, seg.size())) { + const auto info = decoder.DecodeInstruction(seg.data() + match.Offset, match.Length); + if (!info.HasBranchTarget || info.HasRipRelativeDisp || info.FlowKind == ControlFlowKind::Call) continue; + const auto target = static_cast(match.Offset + match.Length) + info.BranchDisp; + if (target >= 0 && static_cast(target) < seg.size()) targets.insert(static_cast(target)); } std::vector> taken; for (const auto& site : existing) { @@ -252,11 +244,6 @@ void frameSegment( const auto hit = targets.upper_bound(begin); return hit != targets.end() && *hit < end; }; - const auto indexOf = [&](const std::size_t offset) -> std::optional { - const auto found = std::lower_bound(insns.begin(), insns.end(), offset, [](const Insn& insn, const std::size_t value) { return insn.Offset < value; }); - if (found == insns.end() || found->Offset != offset) return std::nullopt; - return static_cast(found - insns.begin()); - }; const auto padding = [&](const Insn& insn) { if (insn.Info.IsTwoByteOpcode) return insn.Info.Opcode == 0x1f; return seg[insn.Offset + insn.Info.OpcodeOffset] == 0x90 && (insn.Info.RexPrefix & 1) == 0; @@ -268,52 +255,52 @@ void frameSegment( return static_cast(target); }; - for (auto it = starts.begin(); it != starts.end(); ++it) { - const auto start = *it; - const auto next = std::next(it); - const auto limit = next == starts.end() ? seg.size() : *next; - const auto first = indexOf(start); - if (!first) continue; + for (const auto& function : functions) { + if (function.Begin < ph.MappedAddress || function.End <= function.Begin || function.End - ph.MappedAddress > segSize) continue; + const auto start = static_cast(function.Begin - ph.MappedAddress); + const auto limit = static_cast(function.End - ph.MappedAddress); ++report.Functions; const auto skip = [&](const char* reason) { ++report.Skipped[reason]; }; - std::vector body; + const std::vector code(seg.begin() + static_cast(start), seg.begin() + static_cast(limit)); + std::vector body; bool afterReturn = false; std::optional jumpedTo; - for (auto i = *first; i < insns.size() && insns[i].Offset < limit; ++i) { - if (seg[insns[i].Offset] == 0xcc && insns[i].Length == 1) break; - if (!padding(insns[i])) { - if (insns[i].Offset % 16 == 0 && (afterReturn || jumpedTo == insns[i].Offset)) break; - afterReturn = insns[i].Info.FlowKind == ControlFlowKind::Return || insns[i].Info.FlowKind == ControlFlowKind::Trap; - jumpedTo = jumpTarget(insns[i]); + for (const auto& match : scanner->ScanCodeSection(code, 0, code.size())) { + if (code[match.Offset] == 0xcc && match.Length == 1) break; + const Insn insn{start + static_cast(match.Offset), match.Length, decoder.DecodeInstruction(code.data() + match.Offset, match.Length)}; + if (!padding(insn)) { + if (insn.Offset % 16 == 0 && (afterReturn || jumpedTo == insn.Offset)) break; + afterReturn = insn.Info.FlowKind == ControlFlowKind::Return || insn.Info.FlowKind == ControlFlowKind::Trap; + jumpedTo = jumpTarget(insn); } - body.push_back(i); + body.push_back(insn); } if (body.size() < 3) continue; - const auto at = [&](const std::size_t k) { return seg.data() + insns[body[k]].Offset; }; - const bool rbpPrologue = insns[body[0]].Length == 1 && at(0)[0] == 0x55 && insns[body[1]].Length == 3 && std::memcmp(at(1), "\x48\x89\xe5", 3) == 0; + const auto at = [&](const std::size_t k) { return seg.data() + body[k].Offset; }; + const bool rbpPrologue = body[0].Length == 1 && at(0)[0] == 0x55 && body[1].Length == 3 && std::memcmp(at(1), "\x48\x89\xe5", 3) == 0; std::vector pushed; std::size_t p = rbpPrologue ? 2 : 0; while (p < body.size()) - if (const auto reg = pushedRegister(at(p), insns[body[p]])) { pushed.push_back(*reg); ++p; } else break; + if (const auto reg = pushedRegister(at(p), body[p])) { pushed.push_back(*reg); ++p; } else break; std::int64_t allocation = 0; if (!rbpPrologue && p < body.size()) - if (const auto adjustment = rspAdjustment(at(p), insns[body[p]]); adjustment && *adjustment < 0) { allocation = -*adjustment; ++p; } + if (const auto adjustment = rspAdjustment(at(p), body[p]); adjustment && *adjustment < 0) { allocation = -*adjustment; ++p; } std::vector expected(pushed.begin(), pushed.end()); if (rbpPrologue) expected.insert(expected.begin(), kRegRbp); const auto popsBefore = [&](const std::size_t k) -> std::optional { std::size_t q = k; std::vector popped; while (q > p && popped.size() < expected.size()) - if (const auto reg = poppedRegister(at(q - 1), insns[body[q - 1]])) { popped.push_back(*reg); --q; } else break; + if (const auto reg = poppedRegister(at(q - 1), body[q - 1])) { popped.push_back(*reg); --q; } else break; if (popped != expected) return std::nullopt; return q; }; std::set frees; if (allocation > 0) for (std::size_t k = p; k < body.size(); ++k) - if (insns[body[k]].Info.FlowKind == ControlFlowKind::Return) - if (const auto q = popsBefore(k); q && *q > p && rspAdjustment(at(*q - 1), insns[body[*q - 1]]) == allocation) frees.insert(*q - 1); + if (body[k].Info.FlowKind == ControlFlowKind::Return) + if (const auto q = popsBefore(k); q && *q > p && rspAdjustment(at(*q - 1), body[*q - 1]) == allocation) frees.insert(*q - 1); bool leaf = true; bool redZone = false; @@ -321,18 +308,18 @@ void frameSegment( std::vector> exits; std::map>> moves; std::vector stubbed; - const auto functionEnd = insns[body.back()].Offset + insns[body.back()].Length; + const auto functionEnd = limit; std::int64_t depth = 0; bool wholeRedZone = rbpPrologue; for (std::size_t k = p; k < body.size(); ++k) { - const auto use = memoryUse(at(k), insns[body[k]]); + const auto use = memoryUse(at(k), body[k]); if (!use.RspBased) continue; if (use.RspDisp < 0) depth = std::max(depth, -use.RspDisp); if (use.Indexed || (use.Address && use.RspDisp < 0)) wholeRedZone = true; } const auto shift = wholeRedZone ? kRedZoneBytes : std::min(kRedZoneBytes, (depth + kStackAlignment - 1) / kStackAlignment * kStackAlignment); for (std::size_t k = p; k < body.size(); ++k) { - const auto& insn = insns[body[k]]; + const auto& insn = body[k]; const auto* code = at(k); const auto flow = insn.Info.FlowKind; if (flow == ControlFlowKind::Call || flow == ControlFlowKind::IndirectCall) { leaf = false; break; } @@ -367,7 +354,7 @@ void frameSegment( if (q == p || !frees.contains(q - 1)) { reason = "exit does not free the prologue's allocation"; continue; } --q; } - const auto siteStart = insns[body[q]].Offset; + const auto siteStart = body[q].Offset; const auto siteEnd = insn.Offset + 1; if (siteEnd - siteStart < kJumpBytes) { reason = "exit shorter than a jump"; continue; } if (targetInside(siteStart, siteEnd)) { reason = "branch into an exit"; continue; } @@ -376,7 +363,7 @@ void frameSegment( } if (!leaf || !redZone) continue; ++report.RedZoneFunctions; - const auto prologueEnd = p > 0 ? insns[body[p - 1]].Offset + insns[body[p - 1]].Length : start; + const auto prologueEnd = p > 0 ? body[p - 1].Offset + body[p - 1].Length : start; if (reason == nullptr && ((pushed.empty() && allocation == 0) || prologueEnd - start < kJumpBytes)) reason = "prologue shorter than a jump"; if (reason == nullptr && targetInside(start, prologueEnd)) reason = "branch into the prologue"; if (reason == nullptr && exits.empty()) reason = "no ret exit"; @@ -390,9 +377,9 @@ void frameSegment( for (const auto k : stubbed) { if (reason != nullptr) break; if (!stubs.empty() && k < stubs.back().Last) continue; - OperandStub stub{k, k, insns[body[k]].Offset, insns[body[k]].Offset, {}}; + OperandStub stub{k, k, body[k].Offset, body[k].Offset, {}}; while (stub.Last < body.size() && stub.End - stub.Begin < kJumpBytes) { - const auto& covered = insns[body[stub.Last]]; + const auto& covered = body[stub.Last]; if (covered.Offset != stub.End || covered.Info.FlowKind != ControlFlowKind::Sequential || covered.Info.HasRipRelativeDisp || frees.contains(stub.Last)) break; const auto use = memoryUse(at(stub.Last), covered); const auto bytes = use.RspBased ? withDisplacement(at(stub.Last), covered, use, use.RspDisp + shift) : std::vector(at(stub.Last), at(stub.Last) + covered.Length); @@ -418,6 +405,7 @@ void frameSegment( site.ReturnBranchOffset = code.size(); code.insert(code.end(), std::begin(kJumpBack), std::end(kJumpBack)); site.Body = std::move(code); + taken.emplace_back(begin, end); sites.push_back(std::move(site)); }; const auto original = [&](const std::size_t begin, const std::size_t end) { return std::vector(seg.begin() + static_cast(begin), seg.begin() + static_cast(end)); }; @@ -447,12 +435,13 @@ void frameSegment( std::vector FrameRedZoneFunctions( const std::vector& fileBytes, const std::vector& codeSegments, + const std::vector& functions, const std::vector& existing, RedZoneFrameReport& report, std::vector& displacements ) { std::vector sites; - for (const auto& ph : codeSegments) frameSegment(fileBytes, ph, existing, sites, displacements, report); + for (const auto& ph : codeSegments) frameSegment(fileBytes, ph, functions, existing, sites, displacements, report); return sites; } diff --git a/core/relinker/codegen/tests/RedZoneFramerExecutionTests.cpp b/core/relinker/codegen/tests/RedZoneFramerExecutionTests.cpp index 96fdf4b865..394a3807c1 100644 --- a/core/relinker/codegen/tests/RedZoneFramerExecutionTests.cpp +++ b/core/relinker/codegen/tests/RedZoneFramerExecutionTests.cpp @@ -127,6 +127,7 @@ std::vector callTo(const std::size_t from, const std::size_t targe struct Image { std::vector Bytes; std::vector Starts; + std::vector Ends; }; Image place(const std::vector& leaves) { @@ -144,6 +145,7 @@ Image place(const std::vector& leaves) { for (std::size_t index = 0; index < leaves.size(); ++index) { image.Bytes.resize(image.Starts[index], 0xcc); image.Bytes.insert(image.Bytes.end(), leaves[index].Code.begin(), leaves[index].Code.end()); + image.Ends.push_back(image.Bytes.size()); } image.Bytes.resize((image.Bytes.size() + 15) / 16 * 16 + 16, 0xcc); require(image.Bytes.size() <= kStubBase, "the leaves do not fit before the stubs"); @@ -164,7 +166,9 @@ Framing load(const Image& image, const bool frame) { if (!frame) return framing; const auto base = reinterpret_cast(arena); const std::vector segments{{1, 5, 0, base, base, image.Bytes.size(), image.Bytes.size(), 16}}; - framing.Sites = Codegen::FrameRedZoneFunctions(image.Bytes, segments, {}, framing.Report, framing.Displacements); + std::vector functions; + for (std::size_t index = 0; index < image.Starts.size(); ++index) functions.push_back({base + image.Starts[index], base + image.Ends[index]}); + framing.Sites = Codegen::FrameRedZoneFunctions(image.Bytes, segments, functions, {}, framing.Report, framing.Displacements); for (const auto& moved : framing.Displacements) std::copy(moved.Bytes.begin(), moved.Bytes.end(), arena + moved.Offset); std::size_t cursor = kStubBase; for (const auto& site : framing.Sites) { diff --git a/core/relinker/codegen/tests/RedZoneFramerTests.cpp b/core/relinker/codegen/tests/RedZoneFramerTests.cpp index 0576ca9287..a8a4bc5894 100644 --- a/core/relinker/codegen/tests/RedZoneFramerTests.cpp +++ b/core/relinker/codegen/tests/RedZoneFramerTests.cpp @@ -43,6 +43,7 @@ std::vector leaf(const std::vector& body) { struct Image { std::vector bytes; std::vector segments; + std::vector functions; }; Image image(const std::vector>& functions) { @@ -60,6 +61,7 @@ Image image(const std::vector>& functions) { for (std::size_t index = 0; index < functions.size(); ++index) { result.bytes = padTo(result.bytes, starts[index]); append(result.bytes, functions[index]); + result.functions.push_back({Base + starts[index], Base + starts[index] + functions[index].size()}); } result.bytes = padTo(result.bytes, (result.bytes.size() + 15) / 16 * 16 + 16); result.segments.push_back({1, 5, 0, Base, Base, result.bytes.size(), result.bytes.size(), 16}); @@ -72,7 +74,7 @@ void framesRedZoneLeaf() { const auto input = image({leaf(body)}); Codegen::RedZoneFrameReport report; std::vector displacements; - const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, {}, report, displacements); + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); require(report.RedZoneFunctions == 1 && report.Framed == 1 && report.Exits == 1, "a red-zone leaf was not framed"); require(sites.size() == 2, "a framed leaf needs one prologue and one exit site"); require(displacements.empty() && report.FramedWithoutFramePointer == 0, "a leaf with a frame pointer had rsp operands moved"); @@ -129,7 +131,7 @@ void framesLeafWithoutFramePointer() { const auto input = image({framelessLeaf(FramelessPrologue, body, FramelessExit)}); Codegen::RedZoneFrameReport report; std::vector displacements; - const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, {}, report, displacements); + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); require(report.RedZoneFunctions == 1 && report.Framed == 1 && report.FramedWithoutFramePointer == 1 && report.MovedOperands == 2, "a leaf that only pushes was not framed"); require(sites.size() == 2, "a framed leaf needs one prologue and one exit site"); const auto& entry = sites[0]; @@ -157,7 +159,7 @@ void shiftsByTheDeepestOperandBelowRsp() { const auto input = image({framelessLeaf(FramelessPrologue, body, FramelessExit)}); Codegen::RedZoneFrameReport report; std::vector displacements; - const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, {}, report, displacements); + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); require(report.Framed == 1 && sites.size() == 2 && displacements.size() == 3, "a leaf reaching 0x28 bytes below rsp was not framed"); std::vector entryBody = FramelessPrologue; append(entryBody, {0x48, 0x8d, 0x64, 0x24, 0xd0, 0xe9, 0, 0, 0, 0}); @@ -172,7 +174,7 @@ void movesPositiveOperandsThatFitAfterTheShift() { const auto input = image({framelessLeaf(FramelessPrologue, body, FramelessExit)}); Codegen::RedZoneFrameReport report; std::vector displacements; - const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, {}, report, displacements); + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); require(report.Framed == 1 && report.MovedOperands == 2 && sites.size() == 2, "a leaf with an 8-bit displacement above rsp that fits after the shift was not framed"); const auto loadDisplacement = 0x40 + FramelessPrologue.size() + StoreBelowRsp.size() + 4; require(displacements.size() == 2 && displacements[1].Offset == loadDisplacement && displacements[1].Bytes == std::vector{0x18}, "the load above rsp was not moved in place by the shift"); @@ -186,7 +188,7 @@ void shiftsByTheWholeRedZoneForAddressesBelowRsp() { const auto input = image({framelessLeaf(FramelessPrologue, below, FramelessExit), framelessLeaf(FramelessPrologue, above, FramelessExit)}); Codegen::RedZoneFrameReport report; std::vector displacements; - const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, {}, report, displacements); + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); require(report.Framed == 2 && sites.size() == 4 && displacements.size() == 4, "leaves that take the address of a stack slot were not framed"); require(sites[0].Body[FramelessPrologue.size() + 4] == 0x80 && displacements[0].Bytes == std::vector{0x70} && displacements[1].Bytes == std::vector{0x60}, "a leaf that takes the address of a slot below rsp was not shifted by the whole red zone"); require(sites[2].Body[FramelessPrologue.size() + 4] == 0xf0 && displacements[2].Bytes == std::vector{0x00} && displacements[3].Bytes == std::vector{0x18}, "a leaf that only takes the address of a slot above rsp was shifted by more than its depth"); @@ -198,7 +200,7 @@ void shiftsByTheWholeRedZoneForIndexedOperands() { const auto input = image({framelessLeaf(FramelessPrologue, body, FramelessExit)}); Codegen::RedZoneFrameReport report; std::vector displacements; - const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, {}, report, displacements); + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); require(report.Framed == 1 && sites.size() == 2 && displacements.size() == 2, "a leaf with an indexed operand below rsp was not framed"); require(sites[0].Body[FramelessPrologue.size() + 4] == 0x80 && displacements[0].Bytes == std::vector{0x70} && displacements[1].Bytes == std::vector{0x70}, "a leaf whose indexed operand can reach deeper than its displacement was not shifted by the whole red zone"); } @@ -210,7 +212,7 @@ void stubsOperandsWhoseDisplacementOverflows() { const auto input = image({framelessLeaf(FramelessPrologue, body, FramelessExit)}); Codegen::RedZoneFrameReport report; std::vector displacements; - const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, {}, report, displacements); + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); require(report.Framed == 1 && report.MovedOperands == 1 && report.OperandStubs == 2 && report.StubbedOperands == 2 && sites.size() == 4, "a leaf whose 8-bit displacements above rsp overflow after the shift was not framed with operand stubs"); const auto load = Base + 0x40 + FramelessPrologue.size() + StoreBelowRsp.size(); require(sites[2].Address == load && sites[2].Length == LoadFarAboveRsp.size() && sites[2].OriginalBytes == LoadFarAboveRsp, "the stub site does not cover the load that cannot move in place"); @@ -230,7 +232,7 @@ void stubsOperandsAtRspWithTheInstructionsAfterThem() { const auto input = image({framelessLeaf(FramelessPrologue, atRsp, FramelessExit), framelessLeaf(FramelessPrologue, indexed, FramelessExit)}); Codegen::RedZoneFrameReport report; std::vector displacements; - const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, {}, report, displacements); + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); require(report.Framed == 2 && report.MovedOperands == 2 && report.OperandStubs == 2 && report.StubbedOperands == 3 && sites.size() == 6, "leaves with operands at rsp were not framed with operand stubs"); const auto first = 0x40 + FramelessPrologue.size() + StoreBelowRsp.size(); require(sites[2].Address == Base + first && sites[2].Length == LoadAtRsp.size() + LoadAboveRsp.size(), "the stub site does not run on to the next instruction to make room for its jump"); @@ -246,7 +248,7 @@ void stubsVexOperandsAtRsp() { const auto input = image({framelessLeaf(FramelessPrologue, body, FramelessExit)}); Codegen::RedZoneFrameReport report; std::vector displacements; - const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, {}, report, displacements); + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); require(report.Framed == 1 && report.OperandStubs == 1 && report.StubbedOperands == 1 && sites.size() == 3, "a leaf with a VEX store at rsp was not framed with an operand stub"); require(sites[2].OriginalBytes == VexStoreAtRsp && sites[2].Body == stubBody({0xc5, 0xf8, 0x11, 0x44, 0x24, 0x10}), "the VEX store at rsp was not re-encoded with an 8-bit displacement moved by the shift"); } @@ -265,7 +267,7 @@ void skipsLeavesWithOperandsThatCannotMove() { const auto input = image({framelessLeaf(FramelessPrologue, evex, FramelessExit), framelessLeaf(FramelessPrologue, beforeExit, FramelessExit), framelessLeaf(FramelessPrologue, beforeRipRelative, FramelessExit), framelessLeaf(FramelessPrologue, beforeTarget, FramelessExit), framelessLeaf({0x41, 0x56, 0x41, 0x57}, StoreBelowRsp, {0x41, 0x5f, 0x41, 0x5e, 0xc3})}); Codegen::RedZoneFrameReport report; std::vector displacements; - const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, {}, report, displacements); + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); require(sites.empty() && displacements.empty() && report.RedZoneFunctions == 5, "a leaf whose rsp operands can neither move nor be stubbed, or whose prologue is too short, was framed"); require(report.Skipped.at("rsp operand that cannot move with rsp") == 1, "an EVEX operand, whose 8-bit displacement is scaled, was not refused"); require(report.Skipped.at("rsp operand that cannot move and has no room for a stub") == 3, "an operand at rsp followed by an exit, a rip-relative instruction or a branch target was not refused for want of room"); @@ -280,7 +282,7 @@ void skipsLeavesThatCopyRsp() { const auto input = image({framelessLeaf(FramelessPrologue, withCmov, FramelessExit), framelessLeaf(FramelessPrologue, withShlx, FramelessExit)}); Codegen::RedZoneFrameReport report; std::vector displacements; - const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, {}, report, displacements); + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); require(sites.empty() && displacements.empty() && report.Skipped.at("other use of rsp") == 2, "a leaf that copies rsp into another register was framed"); } @@ -316,7 +318,7 @@ void framesLeavesThatAllocateBelowTheirPushes() { const auto input = image({framelessLeaf(InflatePrologue, body, inflateExit())}); Codegen::RedZoneFrameReport report; std::vector displacements; - const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, {}, report, displacements); + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); require(report.RedZoneFunctions == 1 && report.Framed == 1 && report.FramedWithoutFramePointer == 1 && report.Exits == 1 && sites.size() == 2, "a leaf that allocates with sub after its pushes and spills below its allocation was not framed"); const auto& entry = sites[0]; require(entry.Address == Base + 0x40 && entry.OriginalBytes == InflatePrologue, "the prologue site does not cover the pushes and the sub"); @@ -346,7 +348,7 @@ void framesLeavesThatAllocateWithLeaOrALargeSub() { const auto input = image({framelessLeaf(leaPrologue, StoreBelowRsp, leaExit), framelessLeaf(largePrologue, largeBody, largeExit)}); Codegen::RedZoneFrameReport report; std::vector displacements; - const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, {}, report, displacements); + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); require(report.Framed == 2 && sites.size() == 4 && displacements.size() == 3, "leaves that allocate with lea or a 32-bit sub were not framed"); require(sites[0].OriginalBytes == leaPrologue && sites[1].OriginalBytes == leaExit, "the lea allocation and free are not part of the prologue and exit sites"); require(sites[2].OriginalBytes == largePrologue && sites[3].OriginalBytes == largeExit, "the 32-bit sub and add are not part of the prologue and exit sites"); @@ -362,7 +364,7 @@ void skipsAllocatingLeavesWithoutAMatchingFree() { const auto input = image({framelessLeaf(InflatePrologue, StoreBelowRsp, InflatePops), framelessLeaf(InflatePrologue, StoreBelowRsp, wrongFree), framelessLeaf(InflatePrologue, moreRsp, inflateExit())}); Codegen::RedZoneFrameReport report; std::vector displacements; - const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, {}, report, displacements); + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); require(sites.empty() && displacements.empty() && report.RedZoneFunctions == 3, "an allocating leaf without a matching free, or with more rsp arithmetic, was framed"); require(report.Skipped.at("exit does not free the prologue's allocation") == 1, "an exit that does not free the allocation was not reported with its own reason"); require(report.Skipped.at("other use of rsp") == 2, "an add of another size before the pops, or more rsp arithmetic in the body, was not reported as another use of rsp"); @@ -378,6 +380,7 @@ Image calledThenUncalled(const std::vector& called, const std::vec append(result.bytes, uncalled); result.bytes = padTo(result.bytes, 0x80); result.segments.push_back({1, 5, 0, Base, Base, result.bytes.size(), result.bytes.size(), 16}); + result.functions.push_back({Base + 0x40, Base + 0x40 + called.size()}); return result; } @@ -387,7 +390,7 @@ void endsBodiesAtAlignedCodeAfterARet() { const auto input = calledThenUncalled(framelessLeaf(FramelessPrologue, body, FramelessExit), framelessLeaf(FramelessPrologue, StoreBelowRsp, FramelessExit)); Codegen::RedZoneFrameReport report; std::vector displacements; - const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, {}, report, displacements); + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); require(report.Framed == 1 && report.Exits == 1 && sites.size() == 2 && displacements.size() == 2, "the called leaf was not framed on its own"); for (const auto& site : sites) require(site.Address < Base + 0x60, "a leaf reached without a call was framed as part of the leaf before it"); for (const auto& displacement : displacements) require(displacement.Offset < 0x60, "an rsp operand of a leaf reached without a call was moved"); @@ -402,7 +405,7 @@ void endsBodiesAtTailCallsToTheNextFunction() { const auto input = calledThenUncalled(called, framelessLeaf(FramelessPrologue, StoreBelowRsp, FramelessExit)); Codegen::RedZoneFrameReport report; std::vector displacements; - const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, {}, report, displacements); + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); require(sites.empty() && displacements.empty(), "a leaf that tail-calls the function after it was framed together with that function"); require(report.Skipped.at("branch leaves the function") == 1, "the tail call was not reported as leaving the function"); } @@ -415,7 +418,7 @@ void keepsLoopsAlignedAfterAJump() { const auto input = image({loop}); Codegen::RedZoneFrameReport report; std::vector displacements; - const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, {}, report, displacements); + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); require(report.Framed == 1 && sites.size() == 2 && displacements.size() == 2, "a leaf whose loop header is aligned after a jump was not framed"); } @@ -427,7 +430,7 @@ void leavesOtherFunctions() { const auto input = image({leaf(LoadAbovePushes), nonLeaf}); Codegen::RedZoneFrameReport report; std::vector displacements; - const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, {}, report, displacements); + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); require(sites.empty() && report.RedZoneFunctions == 0, "a leaf without red-zone use or a function that calls was framed"); } @@ -438,10 +441,41 @@ void skipsSitesTakenByOtherStubs() { other.Length = 5; Codegen::RedZoneFrameReport report; std::vector displacements; - const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, {other}, report, displacements); + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {other}, report, displacements); require(sites.empty() && report.Skipped.at("overlaps another stub") == 1, "a site over another stub was framed"); } +void framesAPointerOnlyFunctionByItsOwnBounds() { + const auto first = leaf(StoreBelowPushes); + std::vector secondBody = StoreBelowPushes; + append(secondBody, LoadBelowPushes); + const auto second = leaf(secondBody); + std::vector bytes = callTo(0, 0x40); + bytes.push_back(0xc3); + bytes = padTo(bytes, 0x40); + append(bytes, first); + const auto secondStart = bytes.size(); + append(bytes, second); + bytes = padTo(bytes, (bytes.size() + 15) / 16 * 16 + 16); + const std::vector segments{{1, 5, 0, Base, Base, bytes.size(), bytes.size(), 16}}; + const std::vector functions{{Base + 0x40, Base + 0x40 + first.size()}, {Base + secondStart, Base + secondStart + second.size()}}; + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(bytes, segments, functions, {}, report, displacements); + require(report.Functions == 2 && report.Framed == 2 && report.Exits == 2, "a function reached only through a pointer was not framed by its own bounds"); + require(sites.size() == 4, "two framed leaves need two prologue and two exit sites"); + require(sites[2].Address == Base + secondStart && sites[2].OriginalBytes == Prologue, "the second function's prologue site is not at its own start"); + require(sites[1].Address + sites[1].Length == Base + 0x40 + first.size(), "the first function's exit runs past its end"); +} + +void leavesCodeWithoutAFunctionRange() { + const auto input = image({leaf(StoreBelowPushes)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, {}, {}, report, displacements); + require(sites.empty() && report.Functions == 0, "code without a function range was framed"); +} + } int main() { @@ -465,6 +499,8 @@ int main() { endsBodiesAtAlignedCodeAfterARet(); endsBodiesAtTailCallsToTheNextFunction(); keepsLoopsAlignedAfterAJump(); + framesAPointerOnlyFunctionByItsOwnBounds(); + leavesCodeWithoutAFunctionRange(); std::cout << "red-zone framer tests passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/relinker/relinker/src/pipeline/Conversion.cpp b/core/relinker/relinker/src/pipeline/Conversion.cpp index 283f97462d..1f371ecbca 100644 --- a/core/relinker/relinker/src/pipeline/Conversion.cpp +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -110,10 +111,14 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti const auto intelSites = trampolines.size(); std::vector displacements; if (options.FrameRedZone) { + const auto headers = Relinker::ElfReader(sourceBytes).ReadProgramHeaders(); std::vector codeSegments; - for (const auto& header : Relinker::ElfReader(sourceBytes).ReadProgramHeaders()) if (header.Type == 1 && (header.Flags & 1) != 0) codeSegments.push_back(header); + for (const auto& header : headers) if (header.Type == 1 && (header.Flags & 1) != 0) codeSegments.push_back(header); + std::vector functions; + for (const auto& region : Relinker::UnusedNidFilter::ReadExceptionFunctions(sourceBytes, headers, {}, {})) functions.push_back({region.Begin, region.End}); + if (functions.empty()) throw Domain::RelinkerException("Red-zone framing needs function bounds from the executable's .eh_frame, and it has none"); Codegen::RedZoneFrameReport report; - auto framed = Codegen::FrameRedZoneFunctions(sourceBytes, codeSegments, trampolines, report, displacements); + auto framed = Codegen::FrameRedZoneFunctions(sourceBytes, codeSegments, functions, trampolines, report, displacements); for (const auto& moved : displacements) { if (moved.Offset > sourceBytes.size() || moved.Bytes.size() > sourceBytes.size() - moved.Offset) throw Domain::RelinkerException("Red-zone displacement exceeds source image", moved.Offset); diff --git a/core/relinker/relinker/tests/GamePatchTests.cpp b/core/relinker/relinker/tests/GamePatchTests.cpp index a636405a2a..0d710c1465 100644 --- a/core/relinker/relinker/tests/GamePatchTests.cpp +++ b/core/relinker/relinker/tests/GamePatchTests.cpp @@ -362,6 +362,26 @@ std::vector framedLeafFixture() { const std::vector leaf{0x53, 0x41, 0x56, 0x41, 0x57, 0x48, 0x89, 0x7c, 0x24, 0xf0, 0x48, 0x8b, 0x04, 0x24, 0x48, 0x8b, 0x44, 0x24, 0x08, 0x41, 0x5f, 0x41, 0x5e, 0x5b, 0xc3}; std::copy(caller.begin(), caller.end(), bytes.begin() + 0x1000); std::copy(leaf.begin(), leaf.end(), bytes.begin() + 0x1040); + const auto ehFrameHeader = 64 + 7 * 56; + Io::WriteU32(bytes, ehFrameHeader, 0x6474e550); + Io::WriteU32(bytes, ehFrameHeader + 4, 4); + for (const auto field : {8, 16, 24}) Io::WriteU64(bytes, ehFrameHeader + field, 0x2900); + Io::WriteU64(bytes, ehFrameHeader + 32, 32); + Io::WriteU64(bytes, ehFrameHeader + 40, 32); + Io::WriteU64(bytes, ehFrameHeader + 48, 4); + Io::WriteU32(bytes, 0x2800, 9); + const std::vector cie{1, 0, 1, 0x78, 16}; + std::copy(cie.begin(), cie.end(), bytes.begin() + 0x2808); + Io::WriteU32(bytes, 0x2840, 20); + Io::WriteU32(bytes, 0x2844, 0x44); + Io::WriteU64(bytes, 0x2848, 0x1040); + Io::WriteU64(bytes, 0x2850, leaf.size()); + bytes[0x2900] = 1; + bytes[0x2902] = 3; + Io::WriteU64(bytes, 0x2904, 0x2800); + Io::WriteU32(bytes, 0x290c, 1); + Io::WriteU64(bytes, 0x2910, 0x1040); + Io::WriteU64(bytes, 0x2918, 0x2840); return bytes; } diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 104a57ec0d..902d5942da 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -15,7 +15,7 @@ - `--frame-red-zone` frames only leaf functions that start with a chain of pushes of at least five bytes (`push rbp; mov rbp, rsp; push ...`, or pushes alone, which may be followed by `sub $n, %rsp` or `lea -n(%rsp), %rsp`) and leave through exits of at least five bytes that undo that allocation with `add` or `lea`, pop the same registers and `ret` ([framer](../../../core/relinker/codegen/src/x86/RedZoneFramer.cpp)). Leaves with a frame pointer that allocate, leaves with neither pushes nor an allocation, shorter prologues and exits, exits that free another size, and functions with indirect jumps or other RSP arithmetic keep their red zone. - In a red-zone leaf without a frame pointer, `--frame-red-zone` lowers RSP after the prologue by the deepest displacement below RSP, rounded up to 16, and moves every RSP operand by the same amount ([framer](../../../core/relinker/codegen/src/x86/RedZoneFramer.cpp)). A leaf that takes the address of a slot below RSP, or uses an RSP operand with an index register, is lowered by the whole 128 bytes, since a pointer or an index can reach deeper than any displacement; values below RSP reached through any other register are not seen. Leaves with a frame pointer are lowered by 128 bytes. Inside a moved frame the CFA rules of the guest's unwind tables are off by the shift. - An RSP operand that `--frame-red-zone` cannot move in place, an 8-bit displacement the shift takes past 0x7f or one without a displacement, moves to a stub with the instructions after it up to five bytes and is re-encoded there with the moved displacement ([framer](../../../core/relinker/codegen/src/x86/RedZoneFramer.cpp)). The leaf is skipped when that site would take in a branch target, a branch, ret or RIP-relative instruction, or another site, and when the operand is EVEX, whose 8-bit displacement is scaled. A game patch over a framed site or a moved displacement is refused. -- In `--frame-red-zone`, function starts are direct call targets and 16-byte-aligned code after int3 padding, so a leaf reached only through a pointer without padding before it is not seen. A body ends at int3 padding, at 16-byte-aligned code after a ret or ud2, and at the target of a jmp with only padding between them (a tail call to the next function), so such a leaf is not taken into the body before it there; one that follows a jmp to elsewhere, such as a loop's back edge, without int3 padding still is ([framer](../../../core/relinker/codegen/src/x86/RedZoneFramer.cpp)). +- In `--frame-red-zone`, function bounds come from the executable's `.eh_frame` FDEs: the relinker stops with an error when it has none, and code outside every FDE is not framed. A body ends at int3 padding, at 16-byte-aligned code after a ret or ud2, and at the target of a jmp with only padding between them (a tail call to the next function), so such a leaf is not taken into the body before it there; one that follows a jmp to elsewhere, such as a loop's back edge, without int3 padding still is ([framer](../../../core/relinker/codegen/src/x86/RedZoneFramer.cpp)). - `--to-intel` lowers MCOMMIT to STC plus NOP padding and RDPRU through a runtime-checked stub in the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp): AMD APM Vol. 3 defines RDPRU ECX 0 as MPERF and ECX 1 as APERF, neither readable on Intel hosts, so both selectors return the host TSC as a disclosed monotonic-counter approximation (their ratio always reads 1.0) while other selectors yield zero EDX:EAX. STC asserts CF=1 because under x86-TSO prior stores are already ordered and guest memory is host-DRAM-backed, leaving the commit-success case as the only observable one. Non-zero RDPRU selectors, the TSC-for-MPERF/APERF substitution, and any console path reporting MCOMMIT CF=0 are unverified. - [Unscoped imports on Windows](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) - an import without a module that no searched library exports under its name is searched again under the NID of that name, because homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) imports by symbol name (Quake, from the websrv homebrew launcher; Mari0, #6239) while the libraries in `libs/` export NIDs. A dependency named with `.sprx`, `.suprx` or `.debug_prx` loads the `.prx` library of the same stem from `libs/`, and `libkernel_web` also loads `libkernel.prx`, as `libSceLibcInternal` already loads `libc.prx`: AnyPS5's `libkernel_web.prx` exports only two functions, where the console's `libkernel_web.sprx` is a libkernel variant. A name that matches a library's export as written still binds to that export first. macOS resolves these imports through the host loader by name and is unchanged. - [Unscoped imports on Linux](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) - in an executable that names a dependency with `.sprx`, `.suprx` or `.debug_prx`, as homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) does, an import without a module that is named by symbol is bound to the NID of that name only. Unlike Windows, the name as written is not tried first, because the host loader also searches the host C library, which exports `malloc` or `pthread_create` under their names. A name that a bundled module exports still binds to that module, and such a dependency loads the `.prx` library of the same stem. Unqualified imports of other executables, and imports of bundled modules, are still bound by name.