diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index 2e7d216a7f..14fd038f7a 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -346,7 +346,7 @@ std::set CodeInstructionCollector::Collect(const std::ve addFunction(function.Begin, function.End - function.Begin, false); for (const auto target : function.ExtraTargets) addRoot(target); } - if (sceBase) { + if (sceBase || !functions.empty()) { const std::vector> ranges(functions.begin(), functions.end()); for (const auto& header : codeHeaders) { if (header.Type != 1 || (header.Flags & 1) == 0) continue; diff --git a/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp b/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp index 7c99111960..0171c49c9b 100644 --- a/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp +++ b/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include namespace Relinker { @@ -67,6 +69,183 @@ std::size_t TablesEnd(std::span code, std::size_t firstTable return table; } +bool IsInvalid64BitSingleByteOpcode(std::uint8_t opcode) { + switch (opcode) { + case 0x06: + case 0x07: + case 0x0e: + case 0x16: + case 0x17: + case 0x1e: + case 0x1f: + case 0x27: + case 0x2f: + case 0x37: + case 0x3f: + case 0x60: + case 0x61: + case 0x82: + case 0x9a: + case 0xce: + case 0xd4: + case 0xd5: + case 0xd6: + case 0xea: + return true; + default: + return false; + } +} + +bool IsForbiddenSyscallInstruction(std::span code, std::size_t pos, const Codegen::DecodedInstructionInfo& info) { + const auto opPos = pos + info.OpcodeOffset; + if (opPos + 1 >= pos + info.Length || opPos + 1 >= code.size()) return false; + const auto b0 = code[opPos]; + const auto b1 = code[opPos + 1]; + return (b0 == 0x0f && (b1 == 0x05 || b1 == 0x34 || b1 == 0x07)) || (b0 == 0xcd && b1 == 0x80); +} + +bool IsValid64BitInstruction(std::span code, std::size_t pos, const Codegen::DecodedInstructionInfo& info, VirtualAddress baseVAddr, VirtualAddress segmentVAddr, VirtualAddress segmentEnd) { + if (info.Length == 0 || info.Length > code.size() - pos) return false; + if (!info.IsTwoByteOpcode && IsInvalid64BitSingleByteOpcode(info.Opcode)) return false; + if (info.HasBranchTarget && !info.HasRipRelativeDisp) { + const auto target = static_cast(baseVAddr + pos + info.Length) + info.BranchDisp; + if (target < static_cast(segmentVAddr) || target >= static_cast(segmentEnd)) return false; + } + return true; +} + +bool EndsControlFlow(const Codegen::DecodedInstructionInfo& info, bool isSyscall) { + return info.FlowKind == Codegen::ControlFlowKind::Return + || info.FlowKind == Codegen::ControlFlowKind::UnconditionalJump + || info.FlowKind == Codegen::ControlFlowKind::IndirectJump + || info.FlowKind == Codegen::ControlFlowKind::Trap || isSyscall; +} + +class GapAnalysis { + enum class Trailing { Invalid, Padding, Terminator, NonTerminator }; + +public: + GapAnalysis(std::span code, VirtualAddress baseVAddr, VirtualAddress segmentVAddr, VirtualAddress segmentEnd, const Codegen::X64InstructionDecoder& decoder) + : code(code), padding(code.size() + 1), trailing(code.size() + 1), syscalls(code.size() + 1) { + padding.back() = true; + trailing.back() = Trailing::Padding; + for (std::size_t end = code.size(); end != 0;) { + const auto pos = --end; + const bool bytePadding = code[pos] == 0x00 || code[pos] == 0xcc || code[pos] == 0x90; + if (bytePadding && padding[pos + 1]) { + padding[pos] = true; + trailing[pos] = Trailing::Padding; + continue; + } + Codegen::DecodedInstructionInfo info; + try { + info = decoder.DecodeInstruction(code.data() + pos, code.size() - pos); + } catch (const Codegen::CodegenException&) { + continue; + } + if (info.Length == 0 || info.Length > code.size() - pos) continue; + const bool nop = IsNop(code, pos, info.Length); + if (nop && padding[pos + info.Length]) { + padding[pos] = true; + trailing[pos] = Trailing::Padding; + continue; + } + if (!IsValid64BitInstruction(code, pos, info, baseVAddr, segmentVAddr, segmentEnd)) continue; + const auto next = pos + info.Length; + const bool isSyscall = IsForbiddenSyscallInstruction(code, pos, info); + syscalls[pos] = isSyscall || syscalls[next]; + if (EndsControlFlow(info, isSyscall)) { + trailing[pos] = isSyscall || trailing[next] == Trailing::Padding ? Trailing::Terminator : Trailing::NonTerminator; + continue; + } + if (trailing[next] != Trailing::Padding) { + trailing[pos] = trailing[next]; + continue; + } + trailing[pos] = nop || code[pos] == 0xcc || code[pos] == 0x00 ? Trailing::Padding : Trailing::NonTerminator; + } + } + + bool OnlyPadding(std::size_t from) const { return padding[from]; } + + bool DecodesAsTrailingCodeBlock(std::size_t from, bool* outHasSyscall = nullptr) const { + if (outHasSyscall != nullptr) *outHasSyscall = false; + if (from >= code.size() || code[from] == 0x00 || code[from] == 0xcc || code[from] == 0x90) return false; + if (outHasSyscall != nullptr) *outHasSyscall = syscalls[from]; + return trailing[from] == Trailing::Terminator; + } + +private: + std::span code; + std::vector padding; + std::vector trailing; + std::vector syscalls; +}; + +std::size_t FindEmbeddedDataTableEnd(std::span code, VirtualAddress tableVAddr, VirtualAddress segmentVAddr, VirtualAddress segmentEnd, const Codegen::X64InstructionDecoder& decoder) { + const GapAnalysis analysis(code, tableVAddr, segmentVAddr, segmentEnd, decoder); + std::optional firstInvalidPos; + for (std::size_t pos = 0; pos < code.size();) { + if (analysis.OnlyPadding(pos)) break; + Codegen::DecodedInstructionInfo info; + try { + info = decoder.DecodeInstruction(code.data() + pos, code.size() - pos); + } catch (const Codegen::CodegenException&) { + firstInvalidPos = pos; + break; + } + if (!IsValid64BitInstruction(code, pos, info, tableVAddr, segmentVAddr, segmentEnd)) { + firstInvalidPos = pos; + break; + } + if (IsForbiddenSyscallInstruction(code, pos, info) || info.FlowKind != Codegen::ControlFlowKind::Sequential) return 0; + pos += info.Length; + } + if (!firstInvalidPos) return 0; + const bool duplicatedEightByteRow = code.size() >= 16 && std::memcmp(code.data(), code.data() + 8, 8) == 0; + if (*firstInvalidPos != 0 && (!duplicatedEightByteRow || *firstInvalidPos > 4)) return 0; + + std::optional firstDecodeFailure; + for (std::size_t pos = 0; pos < code.size();) { + if (analysis.OnlyPadding(pos)) break; + try { + const auto info = decoder.DecodeInstruction(code.data() + pos, code.size() - pos); + if (info.Length == 0 || info.Length > code.size() - pos) { + firstDecodeFailure = pos; + break; + } + if (IsForbiddenSyscallInstruction(code, pos, info)) return 0; + pos += info.Length; + } catch (const Codegen::CodegenException&) { + firstDecodeFailure = pos; + break; + } + } + if (!firstDecodeFailure) return 0; + + for (std::size_t pos = *firstDecodeFailure + 1; pos < code.size();) { + if (code[pos] != 0xcc && code[pos] != 0x90 && code[pos] != 0x00) { + ++pos; + continue; + } + const auto padStart = pos; + bool hasZeroPad = false; + while (pos < code.size() && (code[pos] == 0xcc || code[pos] == 0x90 || code[pos] == 0x00)) { + if (code[pos] == 0x00) hasZeroPad = true; + ++pos; + } + if (pos < code.size() && analysis.DecodesAsTrailingCodeBlock(pos)) + return hasZeroPad ? pos : padStart; + } + for (std::size_t pos = *firstDecodeFailure + 1; pos < code.size(); ++pos) { + bool hasSyscall = false; + if (analysis.DecodesAsTrailingCodeBlock(pos, &hasSyscall) && hasSyscall) + return pos; + } + return code.size(); +} + } std::size_t FindTrailingRelativeJumpTable(std::span code, VirtualAddress codeAddress, std::size_t* tableEnd, std::size_t unwindEnd) { @@ -121,6 +300,68 @@ std::size_t FindTrailingRelativeJumpTable(std::span code, Vi std::vector FindRelativeJumpTables(std::span segment, VirtualAddress segmentVAddr, const std::vector>& functions) { std::vector tables; const auto segmentEnd = segmentVAddr + segment.size(); + const Codegen::X64InstructionDecoder decoder; + std::set branchTargets; + for (const auto& [funcBegin, funcEnd] : functions) { + if (funcBegin < segmentVAddr || funcBegin >= segmentEnd || funcEnd <= funcBegin) continue; + const auto inspectFuncEnd = std::min(funcEnd, segmentEnd); + const auto funcCode = segment.subspan(funcBegin - segmentVAddr, inspectFuncEnd - funcBegin); + for (std::size_t pos = 0; pos < funcCode.size();) { + Codegen::DecodedInstructionInfo info; + try { + info = decoder.DecodeInstruction(funcCode.data() + pos, funcCode.size() - pos); + } catch (const Codegen::CodegenException&) { + break; + } + if (info.Length == 0 || info.Length > funcCode.size() - pos) break; + if (info.HasBranchTarget && !info.HasRipRelativeDisp) { + const auto target = static_cast(funcBegin + pos + info.Length) + info.BranchDisp; + if (target >= static_cast(segmentVAddr) && target < static_cast(segmentEnd)) + branchTargets.insert(static_cast(target)); + } + pos += info.Length; + } + } + auto knownFunctions = functions; + std::erase_if(knownFunctions, [](const auto& range) { return range.first >= range.second; }); + std::sort(knownFunctions.begin(), knownFunctions.end()); + std::vector> mergedFunctions; + for (const auto& range : knownFunctions) { + if (!mergedFunctions.empty() && range.first <= mergedFunctions.back().second) + mergedFunctions.back().second = std::max(mergedFunctions.back().second, range.second); + else + mergedFunctions.emplace_back(range); + } + std::vector pendingTargets(branchTargets.begin(), branchTargets.end()); + std::set exploredGaps; + while (!pendingTargets.empty()) { + const auto current = pendingTargets.back(); + pendingTargets.pop_back(); + if (current < segmentVAddr || current >= segmentEnd) continue; + const auto after = std::upper_bound(mergedFunctions.begin(), mergedFunctions.end(), current, + [](VirtualAddress value, const auto& range) { return value < range.first; }); + const bool inKnownFunction = after != mergedFunctions.begin() && current < std::prev(after)->second; + if (inKnownFunction || !exploredGaps.insert(current).second) continue; + auto pos = static_cast(current - segmentVAddr); + while (pos < segment.size()) { + Codegen::DecodedInstructionInfo info; + try { + info = decoder.DecodeInstruction(segment.data() + pos, segment.size() - pos); + } catch (const Codegen::CodegenException&) { + break; + } + if (info.Length == 0 || info.Length > segment.size() - pos) break; + if (info.HasBranchTarget && !info.HasRipRelativeDisp) { + const auto target = static_cast(segmentVAddr + pos + info.Length) + info.BranchDisp; + if (target >= static_cast(segmentVAddr) && target < static_cast(segmentEnd)) { + const auto targetVAddr = static_cast(target); + if (branchTargets.insert(targetVAddr).second) pendingTargets.push_back(targetVAddr); + } + } + if (info.FlowKind == Codegen::ControlFlowKind::Return || info.FlowKind == Codegen::ControlFlowKind::IndirectJump || info.FlowKind == Codegen::ControlFlowKind::Trap) break; + pos += info.Length; + } + } for (std::size_t index = 0; index < functions.size(); ++index) { const auto [begin, end] = functions[index]; if (begin < segmentVAddr || begin >= segmentEnd || end <= begin) continue; @@ -129,8 +370,45 @@ std::vector FindRelativeJumpTables(std::span candidateTable; + bool sawIndirectJump = false; + for (std::size_t pos = 0; pos < funcCode.size();) { + Codegen::DecodedInstructionInfo info; + try { + info = decoder.DecodeInstruction(funcCode.data() + pos, funcCode.size() - pos); + } catch (const Codegen::CodegenException&) { + break; + } + if (info.Length == 0 || info.Length > funcCode.size() - pos) break; + if (info.FlowKind == Codegen::ControlFlowKind::IndirectJump) sawIndirectJump = true; + const auto next = pos + info.Length; + if (info.HasRipRelativeDisp && !info.IsTwoByteOpcode && info.OpcodeOffset < info.Length && info.Length >= 4 && funcCode[pos + info.OpcodeOffset] == 0x8d) { + std::int32_t displacement = 0; + std::memcpy(&displacement, funcCode.data() + next - 4, sizeof(displacement)); + const auto target = static_cast(begin + next) + displacement; + if (target >= static_cast(end) && target < static_cast(inspectEnd)) { + const auto targetVAddr = static_cast(target); + candidateTable = candidateTable ? std::min(*candidateTable, targetVAddr) : targetVAddr; + } + } + pos = next; + } + if (sawIndirectJump || !candidateTable) continue; + const auto tableVAddr = *candidateTable; + const auto paddingCode = segment.subspan(end - segmentVAddr, tableVAddr - end); + if (!OnlyPadding(paddingCode, 0, paddingCode.size(), decoder)) continue; + const auto it = branchTargets.lower_bound(tableVAddr); + if (it != branchTargets.end() && *it < inspectEnd) continue; + const auto tableCode = segment.subspan(tableVAddr - segmentVAddr, inspectEnd - tableVAddr); + const auto tableSize = FindEmbeddedDataTableEnd(tableCode, tableVAddr, segmentVAddr, segmentEnd, decoder); + if (tableSize == 0) continue; + tables.push_back({begin, tableVAddr, tableVAddr + tableSize}); } return tables; } diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index e0dfcc9c6c..d234bbe4db 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -400,14 +401,26 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } if (!image.PatchedRanges.empty()) GamePatch::CheckOverlaps(Io::Utf8Path(image.SourcePath), image.PatchedRanges, {.RelaData = image.Dynamic.RelaData, .RelaPltData = image.Dynamic.RelaPltData}); const bool sceImage = std::any_of(image.Headers.begin(), image.Headers.end(), [](const Domain::ProgramHeader& header) { return header.Type == 0x61000000; }); - const auto unwindFunctions = sceImage ? UnusedNidFilter::ReadExceptionFunctions(image.Bytes, image.Headers, {}, {}) : std::vector{}; + const bool hasEhFrame = sceImage || std::any_of(image.Headers.begin(), image.Headers.end(), [](const Domain::ProgramHeader& header) { return header.Type == 0x6474e550; }); + const auto unwindFunctions = hasEhFrame ? UnusedNidFilter::ReadExceptionFunctions(image.Bytes, image.Headers, {}, {}) : std::vector{}; + std::vector> unwindRanges; + for (const auto& function : unwindFunctions) unwindRanges.emplace_back(function.Begin, function.End); + std::sort(unwindRanges.begin(), unwindRanges.end()); for (const auto& header : codeHeaders) { std::vector code(image.Bytes.begin() + header.Offset, image.Bytes.begin() + header.Offset + header.FileSize); if (sceImage) if (const auto end = FindSceCodeEnd(code, header.MappedAddress, unwindFunctions, image.Got, image.Dynamic.RelaPltData.size())) code.resize(*end - header.MappedAddress); - const auto unwindRanges = sceImage ? SceUnwindRanges(code, header.MappedAddress, unwindFunctions) : std::vector{}; - for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, code.size(), Io::Utf8Path(image.SourcePath), unwindRanges)) { + auto scanCode = code; + for (const auto& table : FindRelativeJumpTables(code, header.MappedAddress, unwindRanges)) { + if (table.Begin >= header.MappedAddress && table.End >= table.Begin) { + const auto maskEnd = std::min(table.End - header.MappedAddress, scanCode.size()); + const auto maskBegin = std::min(table.Begin - header.MappedAddress, maskEnd); + std::fill(scanCode.begin() + static_cast(maskBegin), scanCode.begin() + static_cast(maskEnd), std::uint8_t{0x90}); + } + } + const auto scanUnwindRanges = sceImage ? SceUnwindRanges(code, header.MappedAddress, unwindFunctions) : std::vector{}; + for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(scanCode, header.MappedAddress, scanCode.size(), Io::Utf8Path(image.SourcePath), scanUnwindRanges)) { if (offset > code.size() || code.size() - offset < 2u) throw Domain::RelinkerException("Syscall trap patch exceeds guest executable segment", header.MappedAddress + offset); if (!image.PatchedRanges.empty()) { diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 3a4763f481..d8b7ae3793 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -395,17 +395,20 @@ RelinkResult RelinkerPipeline::Relink() { std::vector> unwindRanges; std::vector> segmentUnwindRanges(executableSegments.size()); - if (std::any_of(programHeaders.begin(), programHeaders.end(), [](const ProgramHeader& ph) { return ph.Type == PT_SCE_DYNLIBDATA; })) { + const bool sceImage = std::any_of(programHeaders.begin(), programHeaders.end(), [](const ProgramHeader& ph) { return ph.Type == PT_SCE_DYNLIBDATA; }); + const bool hasEhFrame = sceImage || std::any_of(programHeaders.begin(), programHeaders.end(), [](const ProgramHeader& ph) { return ph.Type == PT_GNU_EH_FRAME; }); + if (hasEhFrame) { const auto functions = UnusedNidFilter::ReadExceptionFunctions(raw, programHeaders, {}, {}); for (const auto& function : functions) unwindRanges.emplace_back(function.Begin, function.End); std::sort(unwindRanges.begin(), unwindRanges.end()); - for (std::size_t index = 0; index < executableSegments.size(); ++index) { - auto& [segment, segmentVAddr, segmentFileOffset] = executableSegments[index]; - static_cast(segmentFileOffset); - if (const auto codeEnd = FindSceCodeEnd(segment, segmentVAddr, functions, gotVAddr, dynJmpRelSize)) - segment.resize(*codeEnd - segmentVAddr); - segmentUnwindRanges[index] = SceUnwindRanges(segment, segmentVAddr, functions); - } + if (sceImage) + for (std::size_t index = 0; index < executableSegments.size(); ++index) { + auto& [segment, segmentVAddr, segmentFileOffset] = executableSegments[index]; + static_cast(segmentFileOffset); + if (const auto codeEnd = FindSceCodeEnd(segment, segmentVAddr, functions, gotVAddr, dynJmpRelSize)) + segment.resize(*codeEnd - segmentVAddr); + segmentUnwindRanges[index] = SceUnwindRanges(segment, segmentVAddr, functions); + } } std::vector> scanSegments; diff --git a/core/relinker/relinker/tests/ControlFlowGraphTests.cpp b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp index 092f5fa398..aa79bae7f0 100644 --- a/core/relinker/relinker/tests/ControlFlowGraphTests.cpp +++ b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp @@ -8,6 +8,7 @@ #include #include #include +#include namespace { @@ -364,6 +365,257 @@ void CheckRelativeJumpTables() { Require(Relinker::FindRelativeJumpTables(cutShort, TextAddress, {{TextAddress, TextAddress + cutShort.size()}}).empty(), "Table followed by more of its function was accepted"); } +void CheckEmbeddedDataTables() { + constexpr std::size_t tableOffset = 24; + Bytes text(48, 0x90); + text[0] = 0x48; + text[1] = 0x8d; + text[2] = 0x05; + SetDisplacement(text, 3, 7, TextAddress + tableOffset); + text[7] = 0xc3; + for (std::size_t i = 8; i < tableOffset; ++i) text[i] = 0xcc; + text[tableOffset] = 0x0f; + text[tableOffset + 1] = 0x78; + text[tableOffset + 2] = 0x33; + text[tableOffset + 3] = 0x1e; + + const std::vector> functions{ + {TextAddress, TextAddress + 8}, + {TextAddress + text.size(), TextAddress + text.size() + 8} + }; + const auto tables = Relinker::FindRelativeJumpTables(text, TextAddress, functions); + Require(tables.size() == 1, "Expected one embedded data table"); + Require(tables[0].Function == TextAddress && tables[0].Begin == TextAddress + tableOffset && tables[0].End == TextAddress + text.size(), "Embedded data table range was incorrect"); + + Bytes withBranch = text; + withBranch[7] = 0xe8; + SetDisplacement(withBranch, 8, 12, TextAddress + tableOffset); + withBranch[12] = 0xc3; + const std::vector> functionsWithBranch{ + {TextAddress, TextAddress + 13}, + {TextAddress + withBranch.size(), TextAddress + withBranch.size() + 8} + }; + const auto tablesWithBranch = Relinker::FindRelativeJumpTables(withBranch, TextAddress, functionsWithBranch); + Require(tablesWithBranch.empty(), "Branch target was incorrectly recognized as a data table"); + + Bytes withValidCode = text; + withValidCode[tableOffset] = 0x48; + withValidCode[tableOffset + 1] = 0x89; + withValidCode[tableOffset + 2] = 0xc0; + withValidCode[tableOffset + 3] = 0xc3; + for (std::size_t i = tableOffset + 4; i < withValidCode.size(); ++i) withValidCode[i] = 0xcc; + const auto tablesWithValidCode = Relinker::FindRelativeJumpTables(withValidCode, TextAddress, functions); + Require(tablesWithValidCode.empty(), "Valid unannotated code in gap was incorrectly recognized as a data table"); + + Bytes withTransitiveBranch = text; + withTransitiveBranch[7] = 0xe8; + SetDisplacement(withTransitiveBranch, 8, 12, TextAddress + 16); + withTransitiveBranch[12] = 0xc3; + for (std::size_t i = 13; i < 16; ++i) withTransitiveBranch[i] = 0xcc; + withTransitiveBranch[16] = 0xe9; + SetDisplacement(withTransitiveBranch, 17, 21, TextAddress + tableOffset); + for (std::size_t i = 21; i < tableOffset; ++i) withTransitiveBranch[i] = 0xcc; + const std::vector> functionsTransitive{ + {TextAddress, TextAddress + 13}, + {TextAddress + withTransitiveBranch.size(), TextAddress + withTransitiveBranch.size() + 8} + }; + const auto tablesTransitive = Relinker::FindRelativeJumpTables(withTransitiveBranch, TextAddress, functionsTransitive); + Require(tablesTransitive.empty(), "Transitive branch target was incorrectly recognized as a data table"); + + Bytes withTrailingCode(64, 0x90); + withTrailingCode[0] = 0x48; + withTrailingCode[1] = 0x8d; + withTrailingCode[2] = 0x05; + SetDisplacement(withTrailingCode, 3, 7, TextAddress + 16); + withTrailingCode[7] = 0xc3; + for (std::size_t i = 8; i < 16; ++i) withTrailingCode[i] = 0xcc; + withTrailingCode[16] = 0x0f; + withTrailingCode[17] = 0x78; + withTrailingCode[18] = 0x11; + withTrailingCode[19] = 0x22; + withTrailingCode[20] = 0x33; + withTrailingCode[21] = 0x44; + for (std::size_t i = 22; i < 32; ++i) withTrailingCode[i] = 0xcc; + withTrailingCode[32] = 0xb8; + withTrailingCode[33] = 0x3b; + withTrailingCode[34] = 0x00; + withTrailingCode[35] = 0x00; + withTrailingCode[36] = 0x00; + withTrailingCode[37] = 0x0f; + withTrailingCode[38] = 0x05; + withTrailingCode[39] = 0xc3; + for (std::size_t i = 40; i < withTrailingCode.size(); ++i) withTrailingCode[i] = 0x90; + const std::vector> functionsTrailing{ + {TextAddress, TextAddress + 8}, + {TextAddress + withTrailingCode.size(), TextAddress + withTrailingCode.size() + 8} + }; + const auto tablesTrailing = Relinker::FindRelativeJumpTables(withTrailingCode, TextAddress, functionsTrailing); + Require(tablesTrailing.size() == 1, "Expected one bounded data table before trailing code"); + Require(tablesTrailing[0].Begin == TextAddress + 16 && tablesTrailing[0].End <= TextAddress + 32, "Mask overran into trailing code"); + + Bytes withCallbackSyscall(48, 0x90); + withCallbackSyscall[0] = 0x48; + withCallbackSyscall[1] = 0x8d; + withCallbackSyscall[2] = 0x05; + SetDisplacement(withCallbackSyscall, 3, 7, TextAddress + 16); + withCallbackSyscall[7] = 0xc3; + for (std::size_t i = 8; i < 16; ++i) withCallbackSyscall[i] = 0xcc; + withCallbackSyscall[16] = 0xb8; + withCallbackSyscall[17] = 0x3b; + withCallbackSyscall[18] = 0x00; + withCallbackSyscall[19] = 0x00; + withCallbackSyscall[20] = 0x00; + withCallbackSyscall[21] = 0x0f; + withCallbackSyscall[22] = 0x05; + withCallbackSyscall[23] = 0x0f; + withCallbackSyscall[24] = 0x78; + withCallbackSyscall[25] = 0x11; + withCallbackSyscall[26] = 0x22; + withCallbackSyscall[27] = 0x33; + withCallbackSyscall[28] = 0x44; + withCallbackSyscall[29] = 0xc3; + const std::vector> functionsCallback{ + {TextAddress, TextAddress + 8}, + {TextAddress + withCallbackSyscall.size(), TextAddress + withCallbackSyscall.size() + 8} + }; + const auto tablesCallback = Relinker::FindRelativeJumpTables(withCallbackSyscall, TextAddress, functionsCallback); + Require(tablesCallback.empty(), "Callback with syscall and undecodable tail was incorrectly recognized as a data table"); + + Bytes whirlpoolText(4224, 0x90); + whirlpoolText[0] = 0x48; + whirlpoolText[1] = 0x8d; + whirlpoolText[2] = 0x2d; + SetDisplacement(whirlpoolText, 3, 7, TextAddress + 32); + whirlpoolText[7] = 0xc3; + for (std::size_t i = 8; i < 32; ++i) whirlpoolText[i] = 0xcc; + static constexpr std::uint8_t whirlpoolPrefix[]{ + 0x18, 0x18, 0x60, 0x18, 0xc0, 0x78, 0x30, 0xd8, 0x18, 0x18, 0x60, 0x18, 0xc0, 0x78, 0x30, 0xd8, + 0x0f, 0x78, 0x33, 0x1e + }; + std::memcpy(whirlpoolText.data() + 32, whirlpoolPrefix, sizeof(whirlpoolPrefix)); + const std::vector> functionsWhirlpool{ + {TextAddress, TextAddress + 8}, + {TextAddress + whirlpoolText.size(), TextAddress + whirlpoolText.size() + 8} + }; + const auto tablesWhirlpool = Relinker::FindRelativeJumpTables(whirlpoolText, TextAddress, functionsWhirlpool); + Require(tablesWhirlpool.size() == 1, "Expected Whirlpool-style table to be recognized"); + Require(tablesWhirlpool[0].Begin == TextAddress + 32, "Whirlpool table start was incorrect"); +} + +void CheckDecodedPaddingSuffix() { + Bytes text(0x38, 0x90); + text[0] = 0x48; + text[1] = 0x8d; + text[2] = 0x05; + SetDisplacement(text, 3, 7, TextAddress + 0x10); + text[7] = 0xc3; + for (std::size_t i = 8; i < 0x10; ++i) text[i] = 0xcc; + static constexpr std::uint8_t suffix[]{0x0f, 0x78, 0xcc, 0xb8, 0x09, 0x00, 0x00, 0x00, 0x0f, 0x05, 0x00, 0x01, 0x90}; + std::memcpy(text.data() + 0x10, suffix, sizeof(suffix)); + text[0x30] = 0xc3; + const std::vector> functions{ + {TextAddress, TextAddress + 8}, + {TextAddress + 0x30, TextAddress + 0x38} + }; + const auto tables = Relinker::FindRelativeJumpTables(text, TextAddress, functions); + Require(tables.size() == 1, "Expected one decoded data table before a padding-suffixed syscall"); + Require(tables[0].Begin == TextAddress + 0x10 && tables[0].End == TextAddress + 0x12, "Mask extended over the syscall after a decoded padding suffix"); +} + +void CheckStubBeforeUndecodableData() { + Bytes text(0x38, 0x90); + text[0] = 0x48; + text[1] = 0x8d; + text[2] = 0x05; + SetDisplacement(text, 3, 7, TextAddress + 0x10); + text[7] = 0xc3; + for (std::size_t i = 8; i < 0x10; ++i) text[i] = 0xcc; + static constexpr std::uint8_t suffix[]{ + 0x0f, 0x78, 0xcc, 0xb8, 0x09, 0x00, 0x00, 0x00, + 0x0f, 0x05, 0xc3, 0x0f, 0x78, 0x33, 0x1e + }; + std::memcpy(text.data() + 0x10, suffix, sizeof(suffix)); + const std::vector> functions{ + {TextAddress, TextAddress + 8}, + {TextAddress + text.size(), TextAddress + text.size() + 8} + }; + const auto tables = Relinker::FindRelativeJumpTables(text, TextAddress, functions); + Require(tables.size() == 1, "Expected one data table before a syscall stub followed by undecodable data"); + Require(tables[0].Begin == TextAddress + 0x10, "Data table start before a syscall stub was incorrect"); + Require(tables[0].End <= TextAddress + 0x13, "Mask covered the syscall stub before undecodable data"); +} + +void CheckTableWithPaddingAndReturnBytes() { + constexpr std::size_t gap = 0x800; + Bytes text(0x10 + gap, 0x90); + text[0] = 0x48; + text[1] = 0x8d; + text[2] = 0x05; + SetDisplacement(text, 3, 7, TextAddress + 0x10); + text[7] = 0xc3; + for (std::size_t i = 8; i < 0x10; ++i) text[i] = 0xcc; + static constexpr std::uint8_t row[]{ + 0x18, 0x18, 0x60, 0x18, 0xc0, 0x78, 0x30, 0xd8, + 0x18, 0x18, 0x60, 0x18, 0xc0, 0x78, 0x30, 0xd8, + 0x0f, 0x78, 0x33, 0x1e + }; + for (std::size_t at = 0x10; at + sizeof(row) <= text.size(); at += sizeof(row)) + std::memcpy(text.data() + at, row, sizeof(row)); + static constexpr std::uint8_t paddedChain[]{0x00, 0xcc, 0x90, 0x48, 0x89, 0xc0, 0xc3}; + static constexpr std::uint8_t alignedChain[]{0x00, 0xcc, 0x90, 0x00, 0x48, 0x89, 0xc0, 0xc3}; + std::memcpy(text.data() + 0x10 + 0x3b8, paddedChain, sizeof(paddedChain)); + std::memcpy(text.data() + 0x10 + 0x4ac, alignedChain, sizeof(alignedChain)); + const std::vector> functions{ + {TextAddress, TextAddress + 8}, + {TextAddress + text.size(), TextAddress + text.size() + 8} + }; + const auto tables = Relinker::FindRelativeJumpTables(text, TextAddress, functions); + Require(tables.size() == 1, "Expected a table with padding and return bytes to be recognized"); + Require(tables[0].Begin == TextAddress + 0x10, "Table with padding and return bytes started at the wrong address"); + Require(tables[0].End == TextAddress + text.size(), "Mask stopped inside a table with padding and return bytes"); +} + +void CheckLargeZeroGaps() { + constexpr std::size_t stride = 0x10000; + constexpr std::size_t count = 32; + constexpr std::size_t functionSize = 8; + constexpr std::size_t codeFill = 0x8000; + Bytes text(stride * count, 0x00); + std::vector> functions; + for (std::size_t index = 0; index < count; ++index) { + const auto begin = index * stride; + text[begin] = 0x48; + text[begin + 1] = 0x8d; + text[begin + 2] = 0x05; + SetDisplacement(text, begin + 3, begin + 7, TextAddress + begin + functionSize); + text[begin + 7] = 0xc3; + const auto table = begin + functionSize; + text[table] = 0x0f; + text[table + 1] = 0x78; + text[table + 2] = 0x33; + text[table + 3] = 0x1e; + for (auto at = table + 4; at + 2 < begin + codeFill; at += 3) { + text[at] = 0x48; + text[at + 1] = 0x89; + text[at + 2] = 0xc0; + } + functions.emplace_back(TextAddress + begin, TextAddress + begin + functionSize); + } + functions.emplace_back(TextAddress + stride * count, TextAddress + stride * count + functionSize); + + const auto started = std::chrono::steady_clock::now(); + const auto tables = Relinker::FindRelativeJumpTables(text, TextAddress, functions); + const auto elapsed = std::chrono::steady_clock::now() - started; + + Require(tables.size() == count, "Expected one data table per large gap"); + for (std::size_t index = 0; index < tables.size(); ++index) { + Require(tables[index].Function == TextAddress + index * stride, "Large gap table was attached to the wrong function"); + Require(tables[index].Begin == TextAddress + index * stride + functionSize, "Large gap table start was incorrect"); + Require(tables[index].End == TextAddress + (index + 1) * stride, "Large gap table end was incorrect"); + } + Require(elapsed < std::chrono::seconds(10), "Large gap analysis took too long"); +} + } int main() { @@ -382,6 +634,11 @@ int main() { CheckAddressOverflow(); CheckHighVirtualAddress(); CheckRelativeJumpTables(); + CheckEmbeddedDataTables(); + CheckDecodedPaddingSuffix(); + CheckStubBeforeUndecodableData(); + CheckTableWithPaddingAndReturnBytes(); + CheckLargeZeroGaps(); std::cout << "Control-flow graph tests passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/relinker/relinker/tests/test_sce_code_extent.py b/core/relinker/relinker/tests/test_sce_code_extent.py index 1f58fb585e..e7dafe53c9 100644 --- a/core/relinker/relinker/tests/test_sce_code_extent.py +++ b/core/relinker/relinker/tests/test_sce_code_extent.py @@ -26,17 +26,22 @@ UNSUPPORTED_TLS = bytes.fromhex("64 2d 65 6c 66 2e c3") INVALID = bytes.fromhex("41 41 41 41 41 41 66 66 66 66 2e 0f 1f 84 00 00 00 00 00") TLS_LOAD = bytes.fromhex("64 48 8b 04 25 00 00 00 00 c3") FS_SUBTRACT = bytes.fromhex("64 2d 65 6c 66 2e") +LATER_FUNCTION = 0x300 def fixture(sce_block=True, plt_push_offset=8, gap=0, interpreter_pointer=False, data_pointer=False, gap_entry=False, plt_at=None, early_function=False, function_tls=False, pointer_target=None, gap_bytes=None, functions=None, - tls=False): - plt = FUNCTION + FUNCTION_SIZE + gap if plt_at is None else plt_at + tls=False, gap_embedded_data=False, gap_data_then_syscall=False, gap_callback_syscall_undecodable=False, + gap_decoded_padding_syscall=False, gap_stub_then_data=False): + has_gap_embedded = gap_embedded_data or gap_data_then_syscall or gap_callback_syscall_undecodable or gap_decoded_padding_syscall or gap_stub_then_data + plt = (LATER_FUNCTION + FUNCTION_SIZE + gap if has_gap_embedded else FUNCTION + FUNCTION_SIZE + gap) if plt_at is None else plt_at entry = FUNCTION + FUNCTION_SIZE if gap_entry else FUNCTION data = plt + 0x20 if plt > FUNCTION else FUNCTION + FUNCTION_SIZE if functions is None: functions = [(EARLY_FUNCTION, FUNCTION_SIZE)] if early_function else [] functions.append((FUNCTION, FUNCTION_SIZE)) + if has_gap_embedded: + functions.append((LATER_FUNCTION, FUNCTION_SIZE)) interpreter = FUNCTION - len(INTERPRETER) image = bytearray(0x1000) image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) @@ -44,10 +49,10 @@ def fixture(sce_block=True, plt_push_offset=8, gap=0, interpreter_pointer=False, struct.pack_into("