Merge pull request #8076 from brainiac91/fix/relinker-embedded-data-tables

fix(relinker): mask embedded data tables in inter-FDE gaps
This commit is contained in:
Dean authored and GitHub committed 2026-10-26 20:36:58 +13:00
commit b744c37f4e
8 files changed
+670 -28

No files matched your search

@@ -346,7 +346,7 @@ std::set<Domain::VirtualAddress> 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<std::pair<std::uint64_t, std::uint64_t>> ranges(functions.begin(), functions.end());
for (const auto& header : codeHeaders) {
if (header.Type != 1 || (header.Flags & 1) == 0) continue;
@@ -3,6 +3,8 @@
#include <codegen/CodegenException.hpp>
#include <algorithm>
#include <cstring>
#include <optional>
#include <set>
namespace Relinker {
@@ -67,6 +69,183 @@ std::size_t TablesEnd(std::span<const std::uint8_t> 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<const std::uint8_t> 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<const std::uint8_t> 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<std::int64_t>(baseVAddr + pos + info.Length) + info.BranchDisp;
if (target < static_cast<std::int64_t>(segmentVAddr) || target >= static_cast<std::int64_t>(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<const std::uint8_t> 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<const std::uint8_t> code;
std::vector<bool> padding;
std::vector<Trailing> trailing;
std::vector<bool> syscalls;
};
std::size_t FindEmbeddedDataTableEnd(std::span<const std::uint8_t> code, VirtualAddress tableVAddr, VirtualAddress segmentVAddr, VirtualAddress segmentEnd, const Codegen::X64InstructionDecoder& decoder) {
const GapAnalysis analysis(code, tableVAddr, segmentVAddr, segmentEnd, decoder);
std::optional<std::size_t> 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<std::size_t> 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<const std::uint8_t> code, VirtualAddress codeAddress, std::size_t* tableEnd, std::size_t unwindEnd) {
@@ -121,6 +300,68 @@ std::size_t FindTrailingRelativeJumpTable(std::span<const std::uint8_t> code, Vi
std::vector<RelativeJumpTable> FindRelativeJumpTables(std::span<const std::uint8_t> segment, VirtualAddress segmentVAddr, const std::vector<std::pair<VirtualAddress, VirtualAddress>>& functions) {
std::vector<RelativeJumpTable> tables;
const auto segmentEnd = segmentVAddr + segment.size();
const Codegen::X64InstructionDecoder decoder;
std::set<VirtualAddress> 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<std::int64_t>(funcBegin + pos + info.Length) + info.BranchDisp;
if (target >= static_cast<std::int64_t>(segmentVAddr) && target < static_cast<std::int64_t>(segmentEnd))
branchTargets.insert(static_cast<VirtualAddress>(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<std::pair<VirtualAddress, VirtualAddress>> 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<VirtualAddress> pendingTargets(branchTargets.begin(), branchTargets.end());
std::set<VirtualAddress> 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<std::size_t>(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<std::int64_t>(segmentVAddr + pos + info.Length) + info.BranchDisp;
if (target >= static_cast<std::int64_t>(segmentVAddr) && target < static_cast<std::int64_t>(segmentEnd)) {
const auto targetVAddr = static_cast<VirtualAddress>(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<RelativeJumpTable> FindRelativeJumpTables(std::span<const std::uint8
inspectEnd = std::min(functions[index + 1].first, segmentEnd);
const auto code = segment.subspan(begin - segmentVAddr, inspectEnd - begin);
std::size_t tableEnd = code.size();
if (const auto tail = FindTrailingRelativeJumpTable(code, begin, &tableEnd, std::min(end, segmentEnd) - begin); tail != 0 && tail < code.size())
if (const auto tail = FindTrailingRelativeJumpTable(code, begin, &tableEnd, std::min(end, segmentEnd) - begin); tail != 0 && tail < code.size()) {
tables.push_back({begin, begin + tail, begin + tableEnd});
continue;
}
if (inspectEnd <= end) continue;
const auto funcCode = segment.subspan(begin - segmentVAddr, end - begin);
std::optional<VirtualAddress> 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<std::int64_t>(begin + next) + displacement;
if (target >= static_cast<std::int64_t>(end) && target < static_cast<std::int64_t>(inspectEnd)) {
const auto targetVAddr = static_cast<VirtualAddress>(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;
}
@@ -1,4 +1,5 @@
#include <relinker/guest/GuestImage.hpp>
#include <relinker/analysis/RelativeJumpTables.hpp>
#include <relinker/analysis/SceCodeExtent.hpp>
#include <relinker/analysis/UnusedNidFilter/EhFrameReader.hpp>
#include <elfpatcher/general/GuestModuleWriter.hpp>
@@ -400,14 +401,26 @@ std::vector<GuestArtifact> 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<UnusedNidFilter::StrictCodeRegion>{};
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<UnusedNidFilter::StrictCodeRegion>{};
std::vector<std::pair<VirtualAddress, VirtualAddress>> 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<std::uint8_t> 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<CodeRange>{};
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<std::size_t>(table.End - header.MappedAddress, scanCode.size());
const auto maskBegin = std::min<std::size_t>(table.Begin - header.MappedAddress, maskEnd);
std::fill(scanCode.begin() + static_cast<std::ptrdiff_t>(maskBegin), scanCode.begin() + static_cast<std::ptrdiff_t>(maskEnd), std::uint8_t{0x90});
}
}
const auto scanUnwindRanges = sceImage ? SceUnwindRanges(code, header.MappedAddress, unwindFunctions) : std::vector<CodeRange>{};
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()) {
@@ -395,17 +395,20 @@ RelinkResult RelinkerPipeline::Relink() {
std::vector<std::pair<VirtualAddress, VirtualAddress>> unwindRanges;
std::vector<std::vector<CodeRange>> 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<void>(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<void>(segmentFileOffset);
if (const auto codeEnd = FindSceCodeEnd(segment, segmentVAddr, functions, gotVAddr, dynJmpRelSize))
segment.resize(*codeEnd - segmentVAddr);
segmentUnwindRanges[index] = SceUnwindRanges(segment, segmentVAddr, functions);
}
}
std::vector<std::vector<std::uint8_t>> scanSegments;
@@ -8,6 +8,7 @@
#include <stdexcept>
#include <string>
#include <limits>
#include <chrono>
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<std::pair<VirtualAddress, VirtualAddress>> 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<std::pair<VirtualAddress, VirtualAddress>> 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<std::pair<VirtualAddress, VirtualAddress>> 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<std::pair<VirtualAddress, VirtualAddress>> 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<std::pair<VirtualAddress, VirtualAddress>> 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<std::pair<VirtualAddress, VirtualAddress>> 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<std::pair<VirtualAddress, VirtualAddress>> 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<std::pair<VirtualAddress, VirtualAddress>> 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<std::pair<VirtualAddress, VirtualAddress>> 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<std::pair<VirtualAddress, VirtualAddress>> 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) {
@@ -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("<HHIQQQIHHHHHH", image, 16, 3, 62, 1, entry, 64, 0, 0, 64, 56, header_count, 64, 0, 0)
tags = [
(1, 1),
(0x61000035, 0x600), (0x61000037, 16),
(0x61000039, 0x620), (0x6100003B, 24), (0x6100003F, 48),
(0x6100002F, 0x700), (0x61000031, 24 if interpreter_pointer or pointer_target is not None else 0), (0x61000033, 24),
(0x61000027, GOT), (0x61000029, 0x720), (0x6100002B, 7), (0x6100002D, 24),
(0x61000035 if sce_block else 5, 0x600), (0x61000037 if sce_block else 10, 16),
(0x61000039 if sce_block else 6, 0x620), (0x6100003B if sce_block else 11, 24), (0x6100003F, 48),
(0x6100002F if sce_block else 7, 0x700), (0x61000031 if sce_block else 8, 24 if interpreter_pointer or pointer_target is not None else 0), (0x61000033 if sce_block else 9, 24),
(0x61000027 if sce_block else 3, GOT), (0x61000029 if sce_block else 23, 0x720), (0x6100002B, 7), (0x6100002D if sce_block else 2, 24),
(0, 0),
]
struct.pack_into("<IIQQQQQQ", image, 64, PT_LOAD, 7, 0, 0, 0, len(image), len(image), 0x1000)
@@ -73,7 +78,8 @@ def fixture(sce_block=True, plt_push_offset=8, gap=0, interpreter_pointer=False,
for begin, size in functions:
image[begin:begin + size] = b"\xc3" + b"\x90" * (size - 1)
image[FUNCTION + FUNCTION_SIZE:plt] = b"\x90" * gap
last_func = LATER_FUNCTION if has_gap_embedded else FUNCTION
image[last_func + FUNCTION_SIZE:plt] = b"\x90" * gap
image[plt:plt + 2] = b"\xff\x35"
struct.pack_into("<i", image, plt + 2, GOT + plt_push_offset - (plt + 6))
image[plt + 6:plt + 8] = b"\xff\x25"
@@ -98,6 +104,28 @@ def fixture(sce_block=True, plt_push_offset=8, gap=0, interpreter_pointer=False,
image[entry:entry + len(UNSUPPORTED_TLS)] = UNSUPPORTED_TLS
if function_tls:
image[FUNCTION:FUNCTION + len(UNSUPPORTED_TLS)] = UNSUPPORTED_TLS
if gap_embedded_data or gap_data_then_syscall or gap_callback_syscall_undecodable or gap_decoded_padding_syscall or gap_stub_then_data:
table = FUNCTION + FUNCTION_SIZE + 0x10
image[FUNCTION:FUNCTION + 3] = b"\x48\x8d\x05"
struct.pack_into("<iB", image, FUNCTION + 3, table - (FUNCTION + 7), 0xC3)
image[FUNCTION + FUNCTION_SIZE:table] = b"\xcc" * 0x10
if gap_data_then_syscall:
image[table:table + 6] = b"\x0f\x78\x11\x22\x33\x44"
image[table + 6:table + 16] = b"\xcc" * 10
image[table + 16:table + 24] = b"\xb8\x3b\x00\x00\x00\x0f\x05\xc3"
image[table + 24:LATER_FUNCTION] = b"\x90" * (LATER_FUNCTION - (table + 24))
elif gap_callback_syscall_undecodable:
image[table:table + 14] = b"\xb8\x3b\x00\x00\x00\x0f\x05\x0f\x78\x11\x22\x33\x44\xc3"
image[table + 14:LATER_FUNCTION] = b"\x90" * (LATER_FUNCTION - (table + 14))
elif gap_decoded_padding_syscall:
image[table:table + 13] = b"\x0f\x78\xcc\xb8\x09\x00\x00\x00\x0f\x05\x00\x01\x90"
image[table + 13:LATER_FUNCTION] = b"\x90" * (LATER_FUNCTION - (table + 13))
elif gap_stub_then_data:
image[table:table + 15] = b"\x0f\x78\xcc\xb8\x09\x00\x00\x00\x0f\x05\xc3\x0f\x78\x33\x1e"
image[table + 15:LATER_FUNCTION] = b"\x90" * (LATER_FUNCTION - (table + 15))
else:
image[table:LATER_FUNCTION] = b"\x0f\x78\x33\x1e" * ((LATER_FUNCTION - table) // 4)
image[data:data + 0x30] = b"\x90" * 0x30
cie = EH_FRAME
cie_body = b"\x00\x00\x00\x00\x01zR\x00\x01\x78\x10\x01\x1b\x00\x00\x00"
@@ -215,6 +243,21 @@ def main():
fixture(plt_at=PLT_BEFORE_CODE, early_function=True), target=target)
run(relinker, work, "sce-code-after-plt-" + target, fixture(plt_at=PLT_BEFORE_CODE, function_tls=True),
"Unsupported " + ("Windows" if target == "windows" else "macOS") + " guest TLS instruction", target)
run(relinker, work, "sce-gap-embedded-data-" + target,
fixture(sce_block=True, gap_embedded_data=True), target=target)
run(relinker, work, "sysv-gap-embedded-data-" + target,
fixture(sce_block=False, gap_embedded_data=True), target=target)
for name, sce_block in (("sce", True), ("sysv", False)):
run(relinker, work, name + "-gap-decoded-padding-syscall-" + target,
fixture(sce_block=sce_block, gap_decoded_padding_syscall=True),
"Forbidden syscall instruction", target=target)
run(relinker, work, name + "-gap-stub-then-data-" + target,
fixture(sce_block=sce_block, gap_stub_then_data=True),
"Forbidden syscall instruction", target=target)
run(relinker, work, "gap-data-then-syscall-" + target,
fixture(sce_block=False, gap_data_then_syscall=True), "Forbidden syscall instruction", target=target)
run(relinker, work, "gap-callback-syscall-" + target,
fixture(sce_block=False, gap_callback_syscall_undecodable=True), "Forbidden syscall instruction", target=target)
run(relinker, work, "sce-tls-rewrite-over-lea-target", entry_overlap_fixture(),
"TLS rewrite would overwrite an executable entry point (offset 0x304)")
print("SCE code extent tests passed")
@@ -100,15 +100,31 @@ def unwind_table(text, base, function, size, header, frame):
frame - (header + 4), 1, function - header, fde - header)
def sce_module():
def unwind_table_multiple(text, base, functions, header, frame):
cie_body = b"\x00\x00\x00\x00\x01zR\x00\x01\x78\x10\x01\x1b\x00\x00\x00"
struct.pack_into("<I", text, frame - base, len(cie_body))
text[frame - base + 4:frame - base + 4 + len(cie_body)] = cie_body
fde = frame + 4 + len(cie_body)
struct.pack_into("<BBBBiI", text, header - base, 1, 0x1B, 0x03, 0x3B,
frame - (header + 4), len(functions))
for index, (func_addr, func_size) in enumerate(functions):
struct.pack_into("<IIiiB3x", text, fde - base, 16, fde + 4 - frame, func_addr - (fde + 8), func_size, 0)
struct.pack_into("<ii", text, header - base + 12 + index * 8, func_addr - header, fde - header)
fde += 20
struct.pack_into("<I", text, fde - base, 0)
def sce_module(gap_embedded_data=False, gap_data_then_syscall=False, gap_callback_syscall_undecodable=False,
gap_decoded_padding_syscall=False, gap_stub_then_data=False):
image = bytearray(0xC00)
tags = [(0x61000035, 0), (0x61000037, 1), (0x61000039, 0x20), (0x6100003B, 24), (0x6100003F, 24), (0, 0)]
has_gap = gap_embedded_data or gap_data_then_syscall or gap_callback_syscall_undecodable or gap_decoded_padding_syscall or gap_stub_then_data
headers = [
(PT_LOAD, 5, 0x400, 0x1000, 0x100, 0x100, 0x100),
(PT_LOAD, 6, 0x600, 0x2000, 0x400, 0x400, 0x100),
(PT_DYNAMIC, 6, 0x600, 0x2000, len(tags) * 16, len(tags) * 16, 8),
(PT_SCE_DYNLIBDATA, 4, 0xA00, 0, 0x200, 0, 0x10),
(PT_GNU_EH_FRAME, 4, 0x480, 0x1080, 20, 20, 4),
(PT_GNU_EH_FRAME, 4, 0x480, 0x1080, 48 if has_gap else 20, 48 if has_gap else 20, 4),
]
image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9)
struct.pack_into("<HHIQQQIHHHHHH", image, 16, 3, 62, 1, 0x1000, 64, 0, 0, 64, 56, len(headers), 64, 0, 0)
@@ -117,21 +133,47 @@ def sce_module():
for index, tag in enumerate(tags):
struct.pack_into("<qQ", image, 0x600 + index * 16, *tag)
text = memoryview(image)[0x400:0x500]
text[0:FUNCTION_SIZE] = b"\xc3" + b"\x90" * (FUNCTION_SIZE - 1)
text[0x20:0x22] = b"\x0f\x05"
unwind_table(text, 0x1000, 0x1000, FUNCTION_SIZE, 0x1080, 0x10A0)
if has_gap:
text[0:8] = b"\x48\x8d\x05\x19\x00\x00\x00\xc3"
table = 0x20
if gap_data_then_syscall:
text[table:table + 6] = b"\x0f\x78\x11\x22\x33\x44"
text[table + 6:table + 16] = b"\xcc" * 10
text[table + 16:table + 24] = b"\xb8\x3b\x00\x00\x00\x0f\x05\xc3"
text[table + 24:0x40] = b"\x90" * 8
elif gap_callback_syscall_undecodable:
text[table:table + 14] = b"\xb8\x3b\x00\x00\x00\x0f\x05\x0f\x78\x11\x22\x33\x44\xc3"
text[table + 14:0x40] = b"\x90" * 18
elif gap_decoded_padding_syscall:
text[table:table + 13] = b"\x0f\x78\xcc\xb8\x09\x00\x00\x00\x0f\x05\x00\x01\x90"
text[table + 13:0x40] = b"\x90" * (0x40 - (table + 13))
elif gap_stub_then_data:
text[table:table + 15] = b"\x0f\x78\xcc\xb8\x09\x00\x00\x00\x0f\x05\xc3\x0f\x78\x33\x1e"
text[table + 15:0x40] = b"\x90" * (0x40 - (table + 15))
else:
text[table:0x40] = b"\x0f\x78\x33\x1e" * 8
text[0x40:0x48] = b"\xc3" + b"\x90" * 7
unwind_table_multiple(text, 0x1000, [(0x1000, 8), (0x1040, 8)], 0x1080, 0x10A0)
else:
text[0:FUNCTION_SIZE] = b"\xc3" + b"\x90" * (FUNCTION_SIZE - 1)
text[0x20:0x22] = b"\x0f\x05"
unwind_table(text, 0x1000, 0x1000, FUNCTION_SIZE, 0x1080, 0x10A0)
return image
def run_module(relinker, work, name, module, flags):
def run_module(relinker, work, name, module, flags, expected_error=None):
case = work / name
(case / "sce_module").mkdir(parents=True)
(case / "input.elf").write_bytes(importing_executable([]))
(case / "sce_module" / "libExample.prx").write_bytes(module)
result = subprocess.run([str(relinker), *flags, str(case / "input.elf"), str(case / "output.exe")],
capture_output=True, text=True, timeout=30)
assert result.returncode == 0 and (case / "app0" / "sce_module" / "libExample.prx.guest.prx").exists(), (
name, result.stdout, result.stderr)
if expected_error is None:
assert result.returncode == 0 and (case / "app0" / "sce_module" / "libExample.prx.guest.prx").exists(), (
name, result.stdout, result.stderr)
else:
assert result.returncode == 2 and expected_error in result.stderr, (
name, result.returncode, result.stdout, result.stderr)
def run(relinker, work, name, image, flags, expected_error=None):
@@ -154,6 +196,11 @@ def main():
for system, flags in (("linux", []), ("windows", ["--windows"]), ("macos", ["--macos"])):
run(relinker, work, "sce-loader-path-without-interp-header-" + system, fixture(rodata_address=False, interp_header=False), flags)
run_module(relinker, work, "sce-module-data-after-code-" + system, sce_module(), flags)
run_module(relinker, work, "sce-module-gap-embedded-data-" + system, sce_module(gap_embedded_data=True), flags)
run_module(relinker, work, "sce-module-gap-data-then-syscall-" + system, sce_module(gap_data_then_syscall=True), flags, "Forbidden syscall instruction")
run_module(relinker, work, "sce-module-gap-callback-syscall-" + system, sce_module(gap_callback_syscall_undecodable=True), flags, "Forbidden syscall instruction")
run_module(relinker, work, "sce-module-gap-decoded-padding-syscall-" + system, sce_module(gap_decoded_padding_syscall=True), flags, "Forbidden syscall instruction")
run_module(relinker, work, "sce-module-gap-stub-then-data-" + system, sce_module(gap_stub_then_data=True), flags, "Forbidden syscall instruction")
print("SCE module code extent tests passed")
+1
View File
@@ -31,6 +31,7 @@
- In the same executables and modules, the bytes between unwind functions are 16-byte slots of function tails and padding, and a tail can begin inside an instruction. The [instruction scanner](../../../core/relinker/codegen/src/InstructionScanner.cpp) of the syscall and call-site scans therefore resumes at the next byte when bytes outside every unwind function do not decode, and ends a decoding outside them at the start of the next unwind function. Code without an unwind entry that does not decode from its first byte is skipped by these scans instead of failing the relink; inside an unwind function a decode failure still fails it.
- Read-only data can also sit between the unwind functions of such images. There, a `syscall`, `sysenter` or `int 0x80` pattern fails the [syscall scan](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) only when a constant syscall number is set before it; the others, and every `sysret`, are skipped and counted in one message. The number is looked for in the decoded instructions and, whatever alignment the decoding took, in the bytes before the pattern: a `mov eax, imm32`, `mov rax, imm32`, `mov rax, imm64` or `xor eax, eax`, optionally followed by `mov al, imm8` or `mov ax, imm16`, then by any number of register moves or zeroings that leave `rax` alone. A real syscall without an unwind entry whose number is set otherwise is not refused and runs on the host: from memory or another register (`mov eax, ecx`), by `push`/`pop` or arithmetic (`or eax, imm32`), or with any other instruction between the load and the pattern.
- In the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) of the Windows and macOS outputs for such images, a relocation or `lea` target outside every unwind function is followed as code only when its linear decoding, up to the first jump, return or trap, the next unwind function or 256 instructions, holds an `fs:[disp32]` access, or when it is 16-byte aligned, starts less than 256 bytes after an unwind function and ends in a jump, return or trap. Every other such target is taken as data and counted in one message, and the Windows TLS rewrite fails instead of overwriting one. A TLS access of another form, such as `mov rax, fs:[rax]`, or a `gs` instruction in code reached only through such a target is therefore not rewritten or trapped. Other fs and gs instructions do not tell code from data there: string bytes decode as these prefixes, and in two measured titles about 2,500 of about 5,500 such targets decode to one before their first jump.
- [Embedded data tables and relative jump tables in inter-FDE gaps](../../../core/relinker/relinker/src/analysis/RelativeJumpTables.cpp) - in modules with `PT_GNU_EH_FRAME` or `PT_SCE_DYNLIBDATA`, a constant table of undecodable bytes that a RIP-relative `LEA` references, and a trailing relative jump table, are masked with `0x90` in the scan buffer before syscall scanning and kept out of the code roots of [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp), so `0F 78` data in `ubiservices.prx` (PPSA28155, #7547) is no longer refused as an illegal opcode. Masking starts at the `LEA` target, stops at the first byte whose linear decoding reaches a jump, return or trap, keeps gap code being scanned, and needs padding only from the function end to the table; A gap that decodes as valid instructions or that a direct branch targets is left alone. The gap properties are computed once in a backward pass. Output bytes are unchanged apart from syscall traps.
- In executables and guest modules with a `PT_SCE_DYNLIBDATA` segment, the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) of the Windows and macOS outputs follows code only from the end of `PT_INTERP`, or of the `/libexec/ld-elf.so.1` string that begins the segment when no `PT_INTERP` covers it, as in OpenOrbis modules, to the end of the PLT after the last function of the unwind table, or to the end of that function when the PLT comes before the code, as in the `libSceFios2.prx` and `libc.prx` that CUSA02636 bundles (PLT at 0xc8); the pipeline's scans of executables above stop at this same end, also when the PLT comes before the code. A guest module with PLT relocations and no PLT before or after its unwind functions, or with a PLT that overlaps one, fails as the executable does. Code after that end is not analyzed, such as the `DT_FINI` routine without an unwind entry that follows the last unwind function in both modules and that the Windows and macOS outputs call when the program exits: a TLS access in it is not rewritten, the ELF entry point, `DT_INIT`, `DT_FINI`, a branch or a pointer into it is ignored, and a function symbol or init/fini array entry pointing into it fails the conversion.
- Under Rosetta (M5, macOS 27.0.1), a VMASKMOVPS, VMASKMOVPD, VPMASKMOVD or VPMASKMOVQ store or load that faults on an element other than an active first one ends the process (`unexpectedly need to EmulateForward on a synchronous exception`) instead of raising SIGSEGV or SIGBUS, so a masked store into a page the macOS write watch keeps read-only ends the title. `--to-rosetta` moves each masked store to a [stub](../../../core/relinker/codegen/src/x86/MaskedStoreLowering.cpp) that first writes to the first and last byte of every active element with `LOCK OR` of 0, which faults normally and changes nothing; each store then costs up to 16 locked byte writes. A page made read-only again by another thread between those writes and the store still ends the process. A stack-relative displacement that exceeds signed disp32 after the spill frame throws. Masked loads are not converted.
- A relocation against a dynamic symbol entry that is all zero, like the null symbol, takes the value 0 plus its addend, as the gABI defines for `STN_UNDEF` ([pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp)). The Linux output imports such an entry as an unnamed weak symbol, which the host loader resolves to 0; the Windows and macOS outputs write the value into the slot ([relocation builder](../../../core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp)). What the console's loader does with such an entry is not measured. OliSe Player (PPSA01153) has seven of them, and its code calls through those slots only when they are not 0 ([#4437](https://github.com/boykopovar/AnyPS5/issues/4437)).