Skip to content

Conversation

@acidicoala
Copy link
Collaborator

@acidicoala acidicoala commented Oct 1, 2025

While I was on a roll, I decided to create a branch from my branch so that I could work on a fix for #217 and a few other QoL improvements. This branch includes everything from #216 plus additional commits. I did it this way, so that you could chose if how you wish to review those fixes - in isolation or together. If you chose to approve and merge this PR, I will close the #216. If you chose to approve and merge the #216, I will close this PR and create a new one with the fix for #217 specifically.

This PR implements a fix for #217 as it was described in the first post of the issue, by replacing the call rel32 instruction with push imm32.

With those 2 major issues fixed, there were still some minor issues preventing all tests from passing, so they were fixed in this branch as well. Specifically:

With all these changes, we should expect all tests to pass from this point forward.

@gemini-code-assist
Copy link

Summary of Changes

Hello @acidicoala, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request focuses on enhancing the robustness of the hooking mechanism by addressing two critical issues (#215 and #217) related to stack pointer manipulation during function calls. It introduces intelligent instruction translation for specific call patterns and establishes a new, compile-time-conditional diagnostic framework to improve the reliability and verifiability of tests. These changes ensure more accurate and stable detours, particularly in complex x86 scenarios, and are supported by new, targeted test cases and a cleaner CI setup.

Highlights

Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/main.yml
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces fixes for issues #215 and #217 by patching specific instruction patterns, and adds a diagnostics framework to verify these fixes in tests. The changes are well-structured and the addition of synthetic tests for these specific edge cases is a great improvement. My main feedback focuses on improving the error handling in the new instruction patching logic to make it more robust by propagating failures, which will prevent potential crashes if the patching fails at runtime.

Comment on lines 26 to 28
void fixSpecialCases(insts_t& prologue);
void fixCallToRoutineReturningSP(Instruction& callInst, const insts_t& routine);
void fixInlineCallToReadSP(Instruction& callInst);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

These functions perform instruction patching which can fail (e.g., if asmjit fails to generate code). Returning void hides these failures, which could lead to crashes or undefined behavior later in the hooking process if it continues with unpatched instructions. It would be more robust to change their return type to bool to propagate success or failure.

Suggested change
void fixSpecialCases(insts_t& prologue);
void fixCallToRoutineReturningSP(Instruction& callInst, const insts_t& routine);
void fixInlineCallToReadSP(Instruction& callInst);
bool fixSpecialCases(insts_t& prologue);
bool fixCallToRoutineReturningSP(Instruction& callInst, const insts_t& routine);
bool fixInlineCallToReadSP(Instruction& callInst);

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this suggestion is not without merit. I can properly implement it, if @stevemk14ebr is OK with it.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Go for it

Comment on lines 27 to 62
void x86Detour::fixCallToRoutineReturningSP(Instruction& callInst, const insts_t& routine) {
Log::log(
"Fixing special case [call to routine returning ESP]:\n" + instsToStr(std::vector{callInst}),
ErrorLevel::INFO
);

const auto destReg = routine[0].getOperands().substr(0, 3);
const uint32_t originalAddress = callInst.getAddress();
const uint32_t originalNextAddress = originalAddress + callInst.size();

// AsmTK parses strings for AsmJit, which generates the binary code.
asmjit::CodeHolder code;
asmjit::JitRuntime asmjitRt;
code.init(asmjitRt.environment());

asmjit::x86::Assembler assembler(&code);
asmtk::AsmParser parser(&assembler);

// Parse the instructions via AsmTK
if (const auto error = parser.parse(std::format("mov {}, {:#x}", destReg, originalNextAddress).c_str())) {
Log::log(std::format("AsmTK error: {}", asmjit::DebugUtils::errorAsString(error)), ErrorLevel::SEV);
return;
}

// Generate the binary code via AsmJit
uint64_t movAddress = 0;
if (const auto error = asmjitRt.add(&movAddress, &code)) {
Log::log(std::format("AsmJIT error: {}", asmjit::DebugUtils::errorAsString(error)), ErrorLevel::SEV);
return;
}

// Replace `call rel32` instruction with `mov reg, imm32`. Both are 5 bytes long.

callInst = m_disasm.disassemble(movAddress, movAddress, movAddress + callInst.size(), *this)[0];
callInst.setAddress(originalAddress);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Following the change in the header, this function should return bool to indicate whether the instruction patching was successful. On failure (e.g., from parser.parse or asmjitRt.add), it should return false. On success, it should return true at the end.

bool x86Detour::fixCallToRoutineReturningSP(Instruction& callInst, const insts_t& routine) {
    Log::log(
        "Fixing special case [call to routine returning ESP]:\n" + instsToStr(std::vector{callInst}),
        ErrorLevel::INFO
    );

    const auto destReg = routine[0].getOperands().substr(0, 3);
    const uint32_t originalAddress = callInst.getAddress();
    const uint32_t originalNextAddress = originalAddress + callInst.size();

    // AsmTK parses strings for AsmJit, which generates the binary code.
    asmjit::CodeHolder code;
    asmjit::JitRuntime asmjitRt;
    code.init(asmjitRt.environment());

    asmjit::x86::Assembler assembler(&code);
    asmtk::AsmParser parser(&assembler);

    // Parse the instructions via AsmTK
    if (const auto error = parser.parse(std::format("mov {}, {:#x}", destReg, originalNextAddress).c_str())) {
        Log::log(std::format("AsmTK error: {}", asmjit::DebugUtils::errorAsString(error)), ErrorLevel::SEV);
        return false;
    }

    // Generate the binary code via AsmJit
    uint64_t movAddress = 0;
    if (const auto error = asmjitRt.add(&movAddress, &code)) {
        Log::log(std::format("AsmJIT error: {}", asmjit::DebugUtils::errorAsString(error)), ErrorLevel::SEV);
        return false;
    }

    // Replace `call rel32` instruction with `mov reg, imm32`. Both are 5 bytes long.

    callInst = m_disasm.disassemble(movAddress, movAddress, movAddress + callInst.size(), *this)[0];
    callInst.setAddress(originalAddress);
    return true;
}

Comment on lines 64 to 98
void x86Detour::fixInlineCallToReadSP(Instruction& callInst) {
Log::log(
"Fixing special case [inline call to to read ESP]:\n" + instsToStr(std::vector{callInst}),
ErrorLevel::INFO
);

const uint32_t originalAddress = callInst.getAddress();
const uint32_t originalNextAddress = originalAddress + callInst.size();

// AsmTK parses strings for AsmJit, which generates the binary code.
asmjit::CodeHolder code;
asmjit::JitRuntime asmjitRt;
code.init(asmjitRt.environment());

asmjit::x86::Assembler assembler(&code);
asmtk::AsmParser parser(&assembler);

// Parse the instructions via AsmTK
if (const auto error = parser.parse(std::format("push {:#x}", originalNextAddress).c_str())) {
Log::log(std::format("AsmTK error: {}", asmjit::DebugUtils::errorAsString(error)), ErrorLevel::SEV);
return;
}

// Generate the binary code via AsmJit
uint64_t pushAddress = 0;
if (const auto error = asmjitRt.add(&pushAddress, &code)) {
Log::log(std::format("AsmJIT error: {}", asmjit::DebugUtils::errorAsString(error)), ErrorLevel::SEV);
return;
}

// Replace `call rel32` instruction with `push imm32`. Both are 5 bytes long.

callInst = m_disasm.disassemble(pushAddress, pushAddress, pushAddress + callInst.size(), *this)[0];
callInst.setAddress(originalAddress);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This function should return bool to indicate success or failure, for the same reasons as fixCallToRoutineReturningSP. This ensures that any failures during instruction patching are properly handled.

bool x86Detour::fixInlineCallToReadSP(Instruction& callInst) {
    Log::log(
        "Fixing special case [inline call to to read ESP]:\n" + instsToStr(std::vector{callInst}),
        ErrorLevel::INFO
    );

    const uint32_t originalAddress = callInst.getAddress();
    const uint32_t originalNextAddress = originalAddress + callInst.size();

    // AsmTK parses strings for AsmJit, which generates the binary code.
    asmjit::CodeHolder code;
    asmjit::JitRuntime asmjitRt;
    code.init(asmjitRt.environment());

    asmjit::x86::Assembler assembler(&code);
    asmtk::AsmParser parser(&assembler);

    // Parse the instructions via AsmTK
    if (const auto error = parser.parse(std::format("push {:#x}", originalNextAddress).c_str())) {
        Log::log(std::format("AsmTK error: {}", asmjit::DebugUtils::errorAsString(error)), ErrorLevel::SEV);
        return false;
    }

    // Generate the binary code via AsmJit
    uint64_t pushAddress = 0;
    if (const auto error = asmjitRt.add(&pushAddress, &code)) {
        Log::log(std::format("AsmJIT error: {}", asmjit::DebugUtils::errorAsString(error)), ErrorLevel::SEV);
        return false;
    }

    // Replace `call rel32` instruction with `push imm32`. Both are 5 bytes long.

    callInst = m_disasm.disassemble(pushAddress, pushAddress, pushAddress + callInst.size(), *this)[0];
    callInst.setAddress(originalAddress);
    return true;
}

Comment on lines 103 to 115
void x86Detour::fixSpecialCases(insts_t& prologue) {
for (auto& instruction: prologue) {
if (const auto routine = getRoutineReturningSP(instruction)) {
// Fix for #215 https://github.com/stevemk14ebr/PolyHook_2_0/issues/215
fixCallToRoutineReturningSP(instruction, *routine);
PLH_SET_DIAGNOSTIC(Diagnostic::FixedCallToRoutineReadingSP);
} else if (isInlineCallToReadSP(instruction)) {
// Fix for #217 https://github.com/stevemk14ebr/PolyHook_2_0/issues/217
fixInlineCallToReadSP(instruction);
PLH_SET_DIAGNOSTIC(Diagnostic::FixedInlineCallToReadSP);
}
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This function should return bool and check the return values of the helper functions (fixCallToRoutineReturningSP and fixInlineCallToReadSP) to propagate any failures up the call stack.

Suggested change
void x86Detour::fixSpecialCases(insts_t& prologue) {
for (auto& instruction: prologue) {
if (const auto routine = getRoutineReturningSP(instruction)) {
// Fix for #215 https://github.com/stevemk14ebr/PolyHook_2_0/issues/215
fixCallToRoutineReturningSP(instruction, *routine);
PLH_SET_DIAGNOSTIC(Diagnostic::FixedCallToRoutineReadingSP);
} else if (isInlineCallToReadSP(instruction)) {
// Fix for #217 https://github.com/stevemk14ebr/PolyHook_2_0/issues/217
fixInlineCallToReadSP(instruction);
PLH_SET_DIAGNOSTIC(Diagnostic::FixedInlineCallToReadSP);
}
}
}
bool x86Detour::fixSpecialCases(insts_t& prologue) {
for (auto& instruction: prologue) {
if (const auto routine = getRoutineReturningSP(instruction)) {
// Fix for #215 https://github.com/stevemk14ebr/PolyHook_2_0/issues/215
if (!fixCallToRoutineReturningSP(instruction, *routine)) return false;
PLH_SET_DIAGNOSTIC(Diagnostic::FixedCallToRoutineReadingSP);
} else if (isInlineCallToReadSP(instruction)) {
// Fix for #217 https://github.com/stevemk14ebr/PolyHook_2_0/issues/217
if (!fixInlineCallToReadSP(instruction)) return false;
PLH_SET_DIAGNOSTIC(Diagnostic::FixedInlineCallToReadSP);
}
}
return true;
}

bool x86Detour::makeTrampoline(insts_t& prologue, insts_t& trampolineOut) {
assert(!prologue.empty());

fixSpecialCases(prologue);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The return value of fixSpecialCases should be checked here. If it fails, makeTrampoline should also fail and return false to prevent the hook from proceeding with potentially corrupted instructions.

Suggested change
fixSpecialCases(prologue);
if (!fixSpecialCases(prologue)) {
Log::log("Failed to fix special case instructions", ErrorLevel::SEV);
return false;
}

@acidicoala
Copy link
Collaborator Author

@stevemk14ebr, I've implemented the suggestion related to error checking in fixup functions. I've also took liberty to eliminate build warnings that were bugging me this whole time. This PR is now ready for your review.

@stevemk14ebr stevemk14ebr merged commit 09a882e into stevemk14ebr:master Oct 1, 2025
12 checks passed
@stevemk14ebr
Copy link
Owner

Great changes! Really appreciate the recent work for linux compat

@stevemk14ebr stevemk14ebr mentioned this pull request Oct 1, 2025
@acidicoala
Copy link
Collaborator Author

Thanks a ton!

@acidicoala acidicoala deleted the fix/#217 branch October 1, 2025 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants