fix(relinker): end the guest frame-pointer chain at the entry stub
The entry stubs called the title's entry with rbp left as the process started it (0 on Linux) or as a loop counter left it (0 on Windows). The title's start code pushes rbp, so the frame-pointer chain of its main thread ended in a null frame pointer. Unreal Engine walks that chain to capture call stacks (for an ensure, for example) and reads the return address at [rbp + 8], so the walk read address 8 and crashed. Both stubs now push a zero frame record (saved rbp 0, return address 0) and point rbp at it before the call, so frame-pointer walks stop at the entry. The Linux stub pushes it below the aligned stack pointer, keeping the 16-byte alignment at the call. The Windows stub writes it into free slots of its own frame at rsp+0x40. PPSA15657 crashed in its stack walk at the title screen in about half of its runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0134RWCyiyydJBbfKz8g9Bwe
This commit is contained in:
parent
464203cea8
commit
e4dcf3e6c0
|
|
@ -269,6 +269,15 @@ if(BUILD_TESTING)
|
|||
)
|
||||
add_test(NAME windows_icon_resource COMMAND windows_icon_resource_tests)
|
||||
|
||||
add_test_executable(entry_stub_tests
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/EntryStubTests.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/EntryStubBuilder.cpp
|
||||
)
|
||||
target_include_directories(entry_stub_tests PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include
|
||||
)
|
||||
add_test(NAME entry_stub COMMAND entry_stub_tests)
|
||||
|
||||
add_test_executable(strict_nid_filter_tests
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/StrictNidFilterTests.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp
|
||||
|
|
|
|||
|
|
@ -166,6 +166,8 @@ inline constexpr std::uint32_t kDynamicLinkToStrTab = 2;
|
|||
inline constexpr std::uint8_t kStubOpAndRsp0xf0[] = {0x48, 0x83, 0xe4, 0xf0};
|
||||
inline constexpr std::uint8_t kStubOpMovRdiRsp[] = {0x48, 0x89, 0xe7};
|
||||
inline constexpr std::uint8_t kStubOpXorRsiRsi[] = {0x48, 0x31, 0xf6};
|
||||
inline constexpr std::uint8_t kStubOpPushZero[] = {0x6a, 0x00};
|
||||
inline constexpr std::uint8_t kStubOpMovRbpRsp[] = {0x48, 0x89, 0xe5};
|
||||
inline constexpr std::uint8_t kStubOpCallRel32 = 0xe8;
|
||||
inline constexpr std::size_t kStubCallInstructionSize = 5;
|
||||
inline constexpr std::uint8_t kStubOpUd2[] = {0x0f, 0x0b};
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ std::vector<std::uint8_t> EntryStubBuilder::BuildEntryStub(
|
|||
std::vector<std::uint8_t> s;
|
||||
_appendBytes(s, kStubOpMovRdiRsp, sizeof(kStubOpMovRdiRsp));
|
||||
_appendBytes(s, kStubOpAndRsp0xf0, sizeof(kStubOpAndRsp0xf0));
|
||||
_appendBytes(s, kStubOpPushZero, sizeof(kStubOpPushZero));
|
||||
_appendBytes(s, kStubOpPushZero, sizeof(kStubOpPushZero));
|
||||
_appendBytes(s, kStubOpMovRbpRsp, sizeof(kStubOpMovRbpRsp));
|
||||
_appendBytes(s, kStubOpXorRsiRsi, sizeof(kStubOpXorRsiRsi));
|
||||
const std::uint64_t callInsnVaddr = stubVaddr + s.size();
|
||||
const std::uint64_t callNextVaddr = callInsnVaddr + kStubCallInstructionSize;
|
||||
|
|
|
|||
|
|
@ -426,6 +426,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con
|
|||
call("FreeLibrary");
|
||||
code.Rip({0x48, 0x8b, 0x3d}, argumentBlock);
|
||||
const auto exitCallback = code.Branch({0x48, 0x8d, 0x35});
|
||||
code.Emit({0x48, 0xc7, 0x44, 0x24, 0x40, 0, 0, 0, 0, 0x48, 0xc7, 0x44, 0x24, 0x48, 0, 0, 0, 0, 0x48, 0x8d, 0x6c, 0x24, 0x40});
|
||||
code.Rip({0xe8}, entryRva);
|
||||
code.Emit({0x89, 0x44, 0x24, 0x58});
|
||||
guestStartup.Finalize(code, guestModules, handles, guestFinished);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
#include <elfpatcher/general/EntryStubBuilder.hpp>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
void require(const bool condition, const std::string& message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
bool bytesAt(const std::vector<std::uint8_t>& stub, std::size_t& offset, const std::vector<std::uint8_t>& expected) {
|
||||
if (offset + expected.size() > stub.size() || std::memcmp(stub.data() + offset, expected.data(), expected.size()) != 0) return false;
|
||||
offset += expected.size();
|
||||
return true;
|
||||
}
|
||||
|
||||
void stubTerminatesTheFrameChain() {
|
||||
constexpr std::uint64_t stubVaddr = 0xb32db98;
|
||||
constexpr std::uint64_t entryVaddr = 0x80;
|
||||
const auto stub = Elfpatcher::EntryStubBuilder().BuildEntryStub(stubVaddr, entryVaddr);
|
||||
std::size_t offset = 0;
|
||||
require(bytesAt(stub, offset, {0x48, 0x89, 0xe7}), "the stub does not pass the initial stack pointer in rdi");
|
||||
require(bytesAt(stub, offset, {0x48, 0x83, 0xe4, 0xf0}), "the stub does not align the stack to 16 bytes");
|
||||
require(bytesAt(stub, offset, {0x6a, 0x00, 0x6a, 0x00}), "the stub does not push a zero frame record");
|
||||
require(bytesAt(stub, offset, {0x48, 0x89, 0xe5}), "the stub does not point rbp at the zero frame record");
|
||||
require(bytesAt(stub, offset, {0x48, 0x31, 0xf6}), "the stub does not clear rsi");
|
||||
require(offset < stub.size() && stub[offset] == 0xe8, "the stub does not call the entry point");
|
||||
std::int32_t rel32 = 0;
|
||||
std::memcpy(&rel32, stub.data() + offset + 1, sizeof(rel32));
|
||||
offset += 5;
|
||||
require(stubVaddr + offset + static_cast<std::int64_t>(rel32) == entryVaddr, "the stub calls the wrong address");
|
||||
require(bytesAt(stub, offset, {0x0f, 0x0b}) && offset == stub.size(), "the stub does not end with ud2 after the call");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
stubTerminatesTheFrameChain();
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in New Issue