From d41d7cfef81b1a3d9c5f84a5093af1cd7adbe555 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Mon, 13 Jun 2022 20:19:27 -0700 Subject: [PATCH 001/134] Partial commit --- shared/trampoline-allocator.hpp | 6 ++-- src/And64InlineHook.cpp | 2 +- src/trampoline-allocator.cpp | 62 ++++++++++++++++++++++++++------- 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/shared/trampoline-allocator.hpp b/shared/trampoline-allocator.hpp index 19ea05a..598b239 100644 --- a/shared/trampoline-allocator.hpp +++ b/shared/trampoline-allocator.hpp @@ -20,7 +20,7 @@ struct Trampoline { Trampoline(uint32_t* ptr, std::size_t allocationSize, std::size_t& sz) : address(ptr), alloc_size(allocationSize), pageSizeRef(sz) {} void Write(uint32_t instruction); - void Write(void const* address); + void Write(void const* ptr); void WriteCallback(uint32_t const* target); void WriteB(int64_t imm); void WriteBl(int64_t imm); @@ -33,7 +33,9 @@ struct Trampoline { /// DATA void WriteLdrBrData(uint32_t const* target); void WriteFixup(uint32_t const* target); - void WriteFixups(uint32_t const* target, uint16_t fixupSize); + void WriteFixups(uint32_t const* target, uint16_t countToFixup); + /// @brief Logs various information about the trampoline. + void Log(); /// @brief A TRAMPOLINE IS NOT COMPLETE UNTIL FINISH IS CALLED! void Finish(); }; diff --git a/src/And64InlineHook.cpp b/src/And64InlineHook.cpp index ca0e0c8..b6876da 100644 --- a/src/And64InlineHook.cpp +++ b/src/And64InlineHook.cpp @@ -53,7 +53,7 @@ struct context uint32_t *bp; uint32_t ls; // left-shift counts uint32_t ad; // & operand - } __attribute__((aligned(16))); + }; struct insns_info { union diff --git a/src/trampoline-allocator.cpp b/src/trampoline-allocator.cpp index f2b6d70..3692edf 100644 --- a/src/trampoline-allocator.cpp +++ b/src/trampoline-allocator.cpp @@ -1,7 +1,7 @@ #include "trampoline-allocator.hpp" #include #include -#include +#include #include "beatsaber-hook/shared/utils/utils-functions.h" #ifdef ID @@ -22,6 +22,23 @@ #define VERSION __VERSION_BKP #endif +// TODO: We should consider an optimization where we have a location for fixup data instead of inling all fixups. +// This would allow us to write out actual assembly verbatim and then have ldrs and whatnot for grabbing the data +// This would save a few instructions per all of the fixups, since we wouldn't need to branch over the data +// In addition, it would allow us to have a better time disassembling. +// It also shouldn't cost us any space whatsoever +// Though, if we perform any hooks AFTER the fact, we would need to properly expand both our data and our instruction space +// and THAT could be somewhat tricky. +// Perhaps a full recompile for a hook is actually preferred, though, if we know we need to leapfrog +// Since we would need to expand our trampoline and our original instructions regardless. +// TODO: Consider a full recompile and permit late installations + +/// @brief Helper function that returns an encoded b for a particular offset +consteval uint32_t get_b(int offset) { + constexpr uint32_t b_opcode = 0b00010100000000000000000000000000U; + return (b_opcode | (offset >> 2)); +} + void Trampoline::Write(uint32_t instruction) { assert((instruction_count + 1) * sizeof(uint32_t) <= alloc_size); // Log what we are writing (and also our state) @@ -141,12 +158,15 @@ void Trampoline::WriteAdrp(uint8_t reg, int64_t imm) { template void WriteCondBranch(Trampoline& value, uint32_t instruction, int64_t imm) { + uint32_t imm_mask; if constexpr (imm_19) { // Imm 19 - constexpr static uint32_t imm_mask = 0b00000000111111111111111111100000; + constexpr static uint32_t imm_mask_19 = 0b00000000111111111111111111100000; + imm_mask = imm_mask_19; } else { // Imm 14 - constexpr static uint32_t imm_mask = 0b00000000000001111111111111100000; + constexpr static uint32_t imm_mask_14 = 0b00000000000001111111111111100000; + imm_mask = imm_mask_14; } int64_t pc = reinterpret_cast(value.address + value.instruction_count); int64_t delta = pc - imm; @@ -181,12 +201,6 @@ void Trampoline::WriteLdrBrData(uint32_t const* target) { Write(target); } -/// @brief Helper function that returns an encoded b for a particular offset -consteval uint32_t get_b(int64_t offset) { - constexpr uint32_t b_opcode = 0b00010100000000000000000000000000U; - return (b_opcode | (offset >> 2)); -} - auto get_branch_immediate(cs_insn const& inst) { assert(inst.detail->arm64.op_count == 1); return inst.detail->arm64.operands[0].imm; @@ -203,7 +217,7 @@ void Trampoline::WriteFixup(uint32_t const* target) { // Target is where we want to grab original instruction from // Log everything we do here original_instructions.push_back(*target); - cs_insn* insns; + cs_insn* insns = nullptr; auto count = cs_disasm(cs::getHandle(), reinterpret_cast(target), sizeof(uint32_t), reinterpret_cast(target), 1, &insns); assert(count == 1); auto inst = insns[0]; @@ -248,7 +262,23 @@ void Trampoline::WriteFixup(uint32_t const* target) { // Handle fixups for load literals case ARM64_INS_LDR: + { + // TODO: Finish this fixup + constexpr static uint32_t b_31 = 0b10000000000000000000000000000000; + constexpr static uint32_t ldr_lit_opc_mask = 0b10111111000000000000000000000000; + if ((*target & ldr_lit_opc_mask) == 0b00011000000000000000000000000000) { + // This is an ldr literal + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- + auto [reg, dst] = get_second_immediate(inst); + } else if ((*target & (ldr_lit_opc_mask & ~b_31)) == 0b00011100000000000000000000000000000) { + // This is an ldr literal, SIMD + // https://developer.arm.com/documentation/ddi0596/2021-12/SIMD-FP-Instructions/LDR--literal--SIMD-FP---Load-SIMD-FP-Register--PC-relative-literal-- + auto [reg, dst] = get_second_immediate(inst); + + } + } case ARM64_INS_LDRSW: + // TODO: Handle this fixup // Handle pc-relative loads case ARM64_INS_ADR: @@ -274,8 +304,8 @@ void Trampoline::WriteFixup(uint32_t const* target) { void Trampoline::WriteCallback(uint32_t const* target) { constexpr static uint32_t branch_imm_mask = 0b00000011111111111111111111111111U; - int64_t pc = reinterpret_cast(address + instruction_count); - int64_t delta = pc - reinterpret_cast(target); + auto pc = reinterpret_cast(address + instruction_count); + auto delta = pc - reinterpret_cast(target); if (std::llabs(delta) > (branch_imm_mask << 1) + 1) { // To far for b. Emit a br instead. WriteLdrBrData(target); @@ -301,6 +331,12 @@ void Trampoline::Finish() { pageSizeRef -= alloc_size - (instruction_count * sizeof(uint32_t)); } +void Trampoline::Log() { + // TODO: Log the trampoline and various information here + // This will probably be necessary given the potential for failure + +} + struct PageType { void* ptr; std::size_t used_size; @@ -356,7 +392,7 @@ void TrampolineAllocator::Free(Trampoline const& toFree) { if (p.ptr == page_addr) { p.trampoline_count--; if (p.trampoline_count == 0) { - if (!::mprotect(p.ptr, PageSize, PROT_READ)) { + if (::mprotect(p.ptr, PageSize, PROT_READ) != 0) { // Log error on mprotect SAFE_ABORT_MSG("Failed to mark page at: %p as read only!", p.ptr); } From 4239b7deb6e39463227bed79e439b801d334c1ef Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Tue, 16 May 2023 00:37:10 -0700 Subject: [PATCH 002/134] Add .clang-tidy, update build process, add gtest and gsl --- .clang-tidy | 11 +++++ .gitignore | 5 +- CMakeLists.txt | 41 ++++++++++++++-- build.ps1 | 14 ++++-- qpm.json | 7 ++- qpm.shared.json | 124 ++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 190 insertions(+), 12 deletions(-) create mode 100644 .clang-tidy create mode 100644 qpm.shared.json diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..3a71610 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,11 @@ +Checks: '*,-llvm*, + -google-readability-namespace, + -llvmlib, + -fuchsi, + -alter, + -modernize-use-trailing-return, + -readability-avoid-const-params-in, + -cppcoreguidelines-macro-usage, + -readability-identifier-length, + -google-readability-todo' +FormatStyle: file diff --git a/.gitignore b/.gitignore index 585ea17..a8529c7 100644 --- a/.gitignore +++ b/.gitignore @@ -52,8 +52,9 @@ ndkpath.txt libs/ obj/ extern/ -qpm.shared.json *.backup extern.cmake qpm_defines.cmake -build/ \ No newline at end of file +build/ + +build-linux/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 3787220..8a4b6ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,23 @@ include(qpm_defines.cmake) cmake_minimum_required(VERSION 3.22) project(${COMPILE_ID}) +# Get GSL +include(FetchContent) + +FetchContent_Declare(GSL + GIT_REPOSITORY "https://github.com/microsoft/GSL" + GIT_TAG "v4.0.0" +) + +FetchContent_MakeAvailable(GSL) + +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip +) +# For Windows: Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) # c++ standard set(CMAKE_CXX_STANDARD 20) @@ -13,9 +30,9 @@ set(SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src) set(INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/include) # compile options used -add_compile_options(-frtti -fPIE -fPIC -fexceptions) -add_compile_definitions(MOD_VERSION=\"${MOD_VERSION}\") -add_compile_definitions(MOD_ID=\"${MOD_ID}\") +add_compile_options(-frtti -flto -fPIE -fPIC -fexceptions) +add_compile_definitions(MOD_VERSION="${MOD_VERSION}") +add_compile_definitions(MOD_ID="${MOD_ID}") # add_compile_options(-Wall -Wextra -Werror -Wpedantic) # compile definitions used @@ -52,7 +69,7 @@ target_include_directories(${COMPILE_ID} PRIVATE ${INCLUDE_DIR}) # add shared dir as include dir target_include_directories(${COMPILE_ID} PUBLIC ${SHARED_DIR}) -target_link_libraries(${COMPILE_ID} PRIVATE -llog) +target_link_libraries(${COMPILE_ID} PRIVATE -llog GSL) include(extern.cmake) MESSAGE(STATUS "extern added!") @@ -71,3 +88,19 @@ add_custom_command(TARGET ${COMPILE_ID} POST_BUILD COMMAND ${CMAKE_COMMAND} -E rename stripped_lib${COMPILE_ID}.so lib${COMPILE_ID}.so COMMENT "Rename the stripped lib to regular" ) + +enable_testing() + +add_compile_options(-Wall -Wextra -Werror -Wpedantic) +add_executable( + test + test/main.cpp +) +target_include_directories(test PUBLIC ${SHARED_DIR}) +target_link_libraries( + test + GTest::gtest_main +) + +include(GoogleTest) +gtest_discover_tests(test) diff --git a/build.ps1 b/build.ps1 index 5d4f438..3b1fc72 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,10 +1,10 @@ function Clean-Build-Folder { - if (Test-Path -Path "build") - { + if (Test-Path -Path "build") { remove-item build -R new-item -Path build -ItemType Directory - } else { + } + else { new-item -Path build -ItemType Directory } } @@ -36,8 +36,12 @@ Clean-Build-Folder $ExitCode = $LastExitCode # Post build, we actually want to transform the compile_commands.json file such that it has no \\ characters and ONLY has / characters -(Get-Content -Path build/compile_commands.json) | - ForEach-Object {$_ -Replace '\\\\', '/'} | Set-Content -Path build/compile_commands.json +# (Get-Content -Path build/compile_commands.json) | +# ForEach-Object {$_ -Replace '\\\\', '/'} | Set-Content -Path build/compile_commands.json +# To build tests, we just compile with our local clang++ into an executable +# Kind of wacky but will work on linux +# Requires libcapstone-dev installed, and GSL/gtest headers fetched from cmake +# clang++ test/main.cpp src/trampoline.cpp src/trampoline-allocator.cpp -o build/test -std=c++20 -I/usr/include/ -Ishared -Ibuild/_deps/googletest-src/googletest/include -Ibuild/_deps/gsl-src/include -lcapstone -Iextern/includes/fmt/fmt/include -L/usr/lib/x86_64-linux-gnu -DFMT_HEADER_ONLY -Wall -Wextra -Werror -g exit $ExitCode \ No newline at end of file diff --git a/qpm.json b/qpm.json index 662c8bc..b942b26 100644 --- a/qpm.json +++ b/qpm.json @@ -30,7 +30,12 @@ "id": "beatsaber-hook", "versionRange": "^3.8.0", "additionalData": {} + }, + { + "id": "paper", + "versionRange": "^1.2.9", + "additionalData": {} } ], - "additionalData": {} + "workspace": null } \ No newline at end of file diff --git a/qpm.shared.json b/qpm.shared.json new file mode 100644 index 0000000..b7f5f2f --- /dev/null +++ b/qpm.shared.json @@ -0,0 +1,124 @@ +{ + "config": { + "sharedDir": "shared", + "dependenciesDir": "extern", + "info": { + "name": "flamingo", + "id": "flamingo", + "version": "0.1.0", + "url": "https://github.com/sc2ad/Flamingo", + "additionalData": { + "overrideSoName": "libflamingo.so" + } + }, + "dependencies": [ + { + "id": "capstone", + "versionRange": "^0.1.0", + "additionalData": {} + }, + { + "id": "paper", + "versionRange": "^1.0.0", + "additionalData": {} + }, + { + "id": "modloader", + "versionRange": "^1.0.2", + "additionalData": {} + }, + { + "id": "beatsaber-hook", + "versionRange": "^3.8.0", + "additionalData": {} + }, + { + "id": "paper", + "versionRange": "^1.2.9", + "additionalData": {} + } + ], + "workspace": null + }, + "restoredDependencies": [ + { + "dependency": { + "id": "paper", + "versionRange": "=1.2.9", + "additionalData": { + "soLink": "https://github.com/Fernthedev/paperlog/releases/download/v1.2.9/libpaperlog.so", + "debugSoLink": "https://github.com/Fernthedev/paperlog/releases/download/v1.2.9/debug_libpaperlog.so", + "overrideSoName": "libpaperlog.so", + "modLink": "https://github.com/Fernthedev/paperlog/releases/download/v1.2.9/paperlog.qmod", + "branchName": "version-v1.2.9" + } + }, + "version": "1.2.9" + }, + { + "dependency": { + "id": "modloader", + "versionRange": "=1.2.3", + "additionalData": { + "soLink": "https://github.com/sc2ad/QuestLoader/releases/download/v1.2.3/libmodloader64.so", + "overrideSoName": "libmodloader.so", + "branchName": "version-v1.2.3" + } + }, + "version": "1.2.3" + }, + { + "dependency": { + "id": "libil2cpp", + "versionRange": "=0.2.3", + "additionalData": { + "headersOnly": true + } + }, + "version": "0.2.3" + }, + { + "dependency": { + "id": "beatsaber-hook", + "versionRange": "=3.14.0", + "additionalData": { + "soLink": "https://github.com/sc2ad/beatsaber-hook/releases/download/v3.14.0/libbeatsaber-hook_3_14_0.so", + "debugSoLink": "https://github.com/sc2ad/beatsaber-hook/releases/download/v3.14.0/debug_libbeatsaber-hook_3_14_0.so", + "branchName": "version-v3.14.0" + } + }, + "version": "3.14.0" + }, + { + "dependency": { + "id": "fmt", + "versionRange": "=9.0.0", + "additionalData": { + "headersOnly": true, + "branchName": "version-v9.0.0", + "compileOptions": { + "systemIncludes": [ + "fmt/include/" + ], + "cppFlags": [ + "-DFMT_HEADER_ONLY" + ] + } + } + }, + "version": "9.0.0" + }, + { + "dependency": { + "id": "capstone", + "versionRange": "=0.1.0", + "additionalData": { + "staticLinking": true, + "soLink": "https://github.com/sc2ad/capstone-qpm/releases/download/v0.1.0/libcapstone.a", + "overrideSoName": "libcapstone.a" + } + }, + "version": "0.1.0" + } + ] +} \ No newline at end of file From e7059fb55dd00bb9dfd82a07700bd39b7e4b4ed6 Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Tue, 16 May 2023 00:38:29 -0700 Subject: [PATCH 003/134] Move from trampoline-allocator to trampoline, improve utils Also adds a bunch of compatibility layers for working without any ANDROID support. It should be possible to include trampoline and trampoline-allocator to get non-android required interfaces. --- shared/trampoline-allocator.hpp | 47 +--- shared/trampoline.hpp | 49 ++++ shared/util.hpp | 63 +++-- src/trampoline-allocator.cpp | 356 ++------------------------- src/trampoline.cpp | 414 ++++++++++++++++++++++++++++++++ 5 files changed, 525 insertions(+), 404 deletions(-) create mode 100644 shared/trampoline.hpp create mode 100644 src/trampoline.cpp diff --git a/shared/trampoline-allocator.hpp b/shared/trampoline-allocator.hpp index 598b239..52a4336 100644 --- a/shared/trampoline-allocator.hpp +++ b/shared/trampoline-allocator.hpp @@ -1,46 +1,19 @@ #pragma once -#include #include -#include -#include -// #include "beatsaber-hook/shared/utils/capstone-utils.hpp" +#include +#include "capstone/capstone.h" -struct Trampoline { - // In bytes for a single fixup - constexpr static uint16_t MaximumFixupSize = 20; +namespace flamingo { - uint32_t* address; - std::size_t alloc_size; - // size is number of instructions - std::size_t instruction_count; - std::size_t& pageSizeRef; - std::vector original_instructions; - - Trampoline(uint32_t* ptr, std::size_t allocationSize, std::size_t& sz) : address(ptr), alloc_size(allocationSize), pageSizeRef(sz) {} - - void Write(uint32_t instruction); - void Write(void const* ptr); - void WriteCallback(uint32_t const* target); - void WriteB(int64_t imm); - void WriteBl(int64_t imm); - void WriteAdr(uint8_t reg, int64_t imm); - void WriteAdrp(uint8_t reg, int64_t imm); - - /// @brief Helper function to write: - /// LDR x17, #0x8 - /// BR X17 - /// DATA - void WriteLdrBrData(uint32_t const* target); - void WriteFixup(uint32_t const* target); - void WriteFixups(uint32_t const* target, uint16_t countToFixup); - /// @brief Logs various information about the trampoline. - void Log(); - /// @brief A TRAMPOLINE IS NOT COMPLETE UNTIL FINISH IS CALLED! - void Finish(); -}; +struct Trampoline; struct TrampolineAllocator { static Trampoline Allocate(std::size_t trampolineSize); static void Free(Trampoline const& toFree); -}; \ No newline at end of file +}; + +// TODO: DO NOT EXPOSE THIS SYMBOL (USE IT FOR TESTING ONLY) +csh getHandle(); + +} // namespace flamingo diff --git a/shared/trampoline.hpp b/shared/trampoline.hpp new file mode 100644 index 0000000..36bfd43 --- /dev/null +++ b/shared/trampoline.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include + +namespace flamingo { + +struct Trampoline { + // In bytes for a single fixup + constexpr static uint16_t MaximumFixupSize = 20; + + uint32_t* address; + std::size_t alloc_size; + // size is number of instructions + std::size_t instruction_count = 0; + std::size_t& pageSizeRef; + std::vector original_instructions{}; + + Trampoline(uint32_t* ptr, std::size_t allocationSize, std::size_t& sz) : address(ptr), alloc_size(allocationSize), pageSizeRef(sz) {} + + void Write(uint32_t instruction); + /// @brief Writes the specified pointer as a target specific immediate to the data block. + /// @param ptr The pointer to write as a raw literal piece of data. NOT dereferenced. + void WriteData(void const* ptr); + /// @brief Performs a memcpy from the specified pointer and size + /// @param ptr The pointer to memcpy from + /// @param size The number of 4 byte words to copy + void WriteData(void const* ptr, uint32_t size); + void WriteCallback(uint32_t const* target); + void WriteB(int64_t imm); + void WriteBl(int64_t imm); + void WriteAdr(uint8_t reg, int64_t imm); + void WriteAdrp(uint8_t reg, int64_t imm); + void WriteLdr(uint32_t inst, uint8_t reg, int64_t imm); + + /// @brief Helper function to write: + /// LDR x17, #0x8 + /// BR X17 + /// DATA + void WriteLdrBrData(uint32_t const* target); + void WriteFixup(uint32_t const* target); + void WriteFixups(uint32_t const* target, uint16_t countToFixup); + /// @brief Logs various information about the trampoline. + void Log(); + /// @brief A TRAMPOLINE IS NOT COMPLETE UNTIL FINISH IS CALLED! + void Finish(); +}; + +} // namespace flamingo \ No newline at end of file diff --git a/shared/util.hpp b/shared/util.hpp index 32fc66d..5c6e2b4 100644 --- a/shared/util.hpp +++ b/shared/util.hpp @@ -1,28 +1,39 @@ #pragma once -#include +#include -// https://en.cppreference.com/w/cpp/experimental/make_array -namespace details { - template struct is_ref_wrapper : std::false_type {}; - template struct is_ref_wrapper> : std::true_type {}; - - template - using not_ref_wrapper = std::negation>>; - - template - struct return_type_helper { - using type = D; - }; - template - struct return_type_helper : std::common_type { - static_assert(std::conjunction_v...>, "Types cannot contain reference_wrappers when D is void"); - }; - - template - using return_type = std::array::type, sizeof...(Types)>; -} - -template -constexpr details::return_type make_array(Types&&... t) { - return {std::forward(t)... }; -} \ No newline at end of file +#ifdef ANDROID +#include "beatsaber-hook/shared/utils/utils-functions.h" +#include "paper/shared/logger.hpp" + +#ifndef NDEBUG +#define FLAMINGO_ASSERT(...) assert(__VA_ARGS__) +#define FLAMINGO_DEBUG(...) Paper::Logger::fmtLog(__VA_ARGS__) +#else +#define FLAMINGO_ASSERT(...) __builtin_assume(__VA_ARGS__) +#define FLAMINGO_DEBUG(...) +#endif + +#define FLAMINGO_CRITICAL(...) Paper::Logger::fmtLog(__VA_ARGS__) +#define FLAMINGO_ABORT(...) \ + do { \ + SAFE_ABORT_MSG(__VA_ARGS__); \ + Paper::Logger::WaitForFlush(); \ + } while (0) + +#else +#include +#include +#include + +#define FLAMINGO_ASSERT(...) assert(__VA_ARGS__) +#define FLAMINGO_DEBUG(...) \ + fmt::print(__VA_ARGS__); \ + puts("") +#define FLAMINGO_CRITICAL(...) \ + fmt::print(__VA_ARGS__); \ + puts("") +#define FLAMINGO_ABORT(...) \ + fmt::print(__VA_ARGS__); \ + puts(""); \ + std::abort() +#endif diff --git a/src/trampoline-allocator.cpp b/src/trampoline-allocator.cpp index 3692edf..d825090 100644 --- a/src/trampoline-allocator.cpp +++ b/src/trampoline-allocator.cpp @@ -1,342 +1,11 @@ #include "trampoline-allocator.hpp" -#include #include #include -#include "beatsaber-hook/shared/utils/utils-functions.h" - -#ifdef ID -#define __ID_BKP ID -#endif -#define ID MOD_ID -#ifdef VERSION -#define __VERSION_BKP VERSION -#endif -#define VERSION MOD_VERSION -#include "beatsaber-hook/shared/utils/capstone-utils.hpp" -#undef ID -#ifdef __ID_BKP -#define ID __ID_BKP -#endif -#undef VERSION -#ifdef __VERSION_BKP -#define VERSION __VERSION_BKP -#endif - -// TODO: We should consider an optimization where we have a location for fixup data instead of inling all fixups. -// This would allow us to write out actual assembly verbatim and then have ldrs and whatnot for grabbing the data -// This would save a few instructions per all of the fixups, since we wouldn't need to branch over the data -// In addition, it would allow us to have a better time disassembling. -// It also shouldn't cost us any space whatsoever -// Though, if we perform any hooks AFTER the fact, we would need to properly expand both our data and our instruction space -// and THAT could be somewhat tricky. -// Perhaps a full recompile for a hook is actually preferred, though, if we know we need to leapfrog -// Since we would need to expand our trampoline and our original instructions regardless. -// TODO: Consider a full recompile and permit late installations - -/// @brief Helper function that returns an encoded b for a particular offset -consteval uint32_t get_b(int offset) { - constexpr uint32_t b_opcode = 0b00010100000000000000000000000000U; - return (b_opcode | (offset >> 2)); -} - -void Trampoline::Write(uint32_t instruction) { - assert((instruction_count + 1) * sizeof(uint32_t) <= alloc_size); - // Log what we are writing (and also our state) - *(address + instruction_count) = instruction; - instruction_count++; -} - -void Trampoline::Write(void const* ptr) { - assert(instruction_count * sizeof(uint32_t) + sizeof(void*) <= alloc_size); - // Log what we are writing (and also our state) - *reinterpret_cast(address + instruction_count) = const_cast(ptr); - instruction_count += sizeof(void*) / sizeof(uint32_t); -} - -void Trampoline::WriteB(int64_t imm) { - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/B--Branch- - WriteCallback(reinterpret_cast(imm)); -} - -void Trampoline::WriteBl(int64_t imm) { - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/BL--Branch-with-Link- - constexpr static uint32_t branch_imm_mask = 0b00000011111111111111111111111111U; - int64_t pc = reinterpret_cast(address + instruction_count); - int64_t delta = pc - imm; - if (std::llabs(delta) > (branch_imm_mask << 1) + 1) { - // Too far to emit a b. Emit a br instead. - // We cannot emit a blr here because the pc + 4 for return will be in our offset. - // LDR X17, #12 - constexpr static uint32_t ldr_x17 = 0x58000071U; - Write(ldr_x17); - // ADR X30, #16 - constexpr static uint32_t adr_x30 = 0x1000009EU; - Write(adr_x30); - // BR x17 - constexpr static uint32_t br_x17 = 0xD61F0220U; - Write(br_x17); - // Data - Write(reinterpret_cast(imm)); - } else { - // Small enough to emit a b/bl. - // bl opcode | encoded immediate (delta >> 2) - // Note, abs(delta >> 2) must be < (1 << 26) - constexpr static uint32_t bl_opcode = 0b10010100000000000000000000000000U; - Write(bl_opcode | ((delta >> 2) & branch_imm_mask)); - } -} - -void Trampoline::WriteAdr(uint8_t reg, int64_t imm) { - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/ADR--Form-PC-relative-address-?lang=en - constexpr static uint32_t adr_maximum_imm = 0b00000000000111111111111111111111U; - constexpr static uint32_t reg_mask = 0b11111; - int64_t pc = reinterpret_cast(address + instruction_count); - int64_t delta = pc - imm; - if (std::llabs(delta) >= (adr_maximum_imm >> 1)) { - // Too far to emit just an adr. - // LDR (used register), #0x8 - constexpr static uint32_t ldr_mask = 0b01011000000000000000000000000000U; - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- - // imm is encoded as << 2, LSB just to the right of reg - constexpr static uint32_t ldr_imm = (8U >> 2U) << 5; - Write(ldr_mask | ldr_imm | (reg_mask & reg)); - // B #0xC - constexpr static uint32_t b_0xc = 0x14000003U; - static_assert(b_0xc == get_b(0xC)); - Write(get_b(0xC)); - // Immediate data - Write(reinterpret_cast(imm)); - } else { - // Close enough to emit an adr. - // Note that delta should be within +-1 MB - constexpr static uint32_t adr_opcode = 0b00010000000000000000000000000000; - // Get immlo - uint32_t imm_lo = ((static_cast(delta) & 3) << 29); - // Get immhi - uint32_t imm_hi = (static_cast(delta) >> 2) << 5; - Write(adr_opcode | imm_lo | imm_hi | (reg_mask & reg)); - } -} - -void Trampoline::WriteAdrp(uint8_t reg, int64_t imm) { - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/ADR--Form-PC-relative-address-?lang=en - // constexpr static uint32_t adr_maximum_imm = 0b00000000000111111111111111111111U; - constexpr static uint32_t reg_mask = 0b11111; - constexpr static uint32_t pc_imm_mask = ~0b111111111111; - constexpr static int64_t adrp_maximum_imm = 0xFFFFF000U; - int64_t pc = reinterpret_cast(address + instruction_count); - int64_t delta = (pc & pc_imm_mask) - imm; - if (std::llabs(delta) >= adrp_maximum_imm) { - // Too far to emit just an adr. - // LDR (used register), #0x8 - constexpr static uint32_t ldr_mask = 0b01011000000000000000000000000000U; - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- - // imm is encoded as << 2, LSB just to the right of reg - constexpr static uint32_t ldr_imm = (8U >> 2U) << 5; - Write(ldr_mask | ldr_imm | (reg_mask & reg)); - // B #0xC - constexpr static uint32_t b_0xc = 0x14000003U; - // TODO: Remove this assertion - static_assert(b_0xc == get_b(0xC)); - Write(get_b(0xC)); - // Write(b_0xc); - // Immediate data - Write(reinterpret_cast(imm)); - } else { - // Close enough to emit an adrp. - // Note that delta should be within +-4 GB - constexpr static uint32_t adrp_opcode = 0b10010000000000000000000000000000; - // Imm is << 12 in parse of instruction - delta >>= 12; - // Get immlo - uint32_t imm_lo = ((static_cast(delta) & 3) << 29); - // Get immhi - uint32_t imm_hi = (static_cast(delta) >> 2) << 5; - Write(adrp_opcode | imm_lo | imm_hi | (reg_mask & reg)); - } -} - -template -void WriteCondBranch(Trampoline& value, uint32_t instruction, int64_t imm) { - uint32_t imm_mask; - if constexpr (imm_19) { - // Imm 19 - constexpr static uint32_t imm_mask_19 = 0b00000000111111111111111111100000; - imm_mask = imm_mask_19; - } else { - // Imm 14 - constexpr static uint32_t imm_mask_14 = 0b00000000000001111111111111100000; - imm_mask = imm_mask_14; - } - int64_t pc = reinterpret_cast(value.address + value.instruction_count); - int64_t delta = pc - imm; - // imm_mask >> 1 for maximum positive value - // << 2 because branch imms are << 2 - // >> 5 because the mask is too high - if (llabs(delta) < (imm_mask >> 4)) { - // Small enough to optimize, just write the instruction - // But with the modified offset - // Delta should be >> 2 for branch imm - // Then << 5 to be in the correct location - value.Write((instruction & ~imm_mask) | (((delta >> 2) << 5) & imm_mask)); - } else { - // Otherwise, we need to write the same expression but with a known offset - // Specifically, write the instruction but with an offset of 8 - // 2, because 8 >> 2 is 2 - // << 5 to place in correct location for immediate - value.Write((instruction & ~imm_mask) | (2 << 5) & imm_mask); - value.Write(get_b(0x14)); - value.WriteLdrBrData(reinterpret_cast(imm)); - } -} - -void Trampoline::WriteLdrBrData(uint32_t const* target) { - // LDR x17, 0x8 - constexpr static uint32_t ldr_x17 = 0x58000051U; - Write(ldr_x17); - // BR x17 - constexpr static uint32_t br_x17 = 0xD61F0220U; - Write(br_x17); - // Data - Write(target); -} - -auto get_branch_immediate(cs_insn const& inst) { - assert(inst.detail->arm64.op_count == 1); - return inst.detail->arm64.operands[0].imm; -} - -std::pair get_second_immediate(cs_insn const& inst) { - // register is just bottom 5 bits - constexpr static uint32_t reg_mask = 0b11111; - assert(inst.detail->arm64.op_count == 2); - return {*reinterpret_cast(inst.bytes) & reg_mask, inst.detail->arm64.operands[1].imm}; -} - -void Trampoline::WriteFixup(uint32_t const* target) { - // Target is where we want to grab original instruction from - // Log everything we do here - original_instructions.push_back(*target); - cs_insn* insns = nullptr; - auto count = cs_disasm(cs::getHandle(), reinterpret_cast(target), sizeof(uint32_t), reinterpret_cast(target), 1, &insns); - assert(count == 1); - auto inst = insns[0]; - // constexpr uint32_t cond_branch_mask = 0b11111111000000000000000000011111; - // TODO: Finish writing fixups here - switch (inst.id) { - // Handle fixups for branch immediate - case ARM64_INS_B: - { - if (inst.detail->arm64.cc != ARM64_CC_INVALID) { - // TODO: Handle this like a conditional branch - auto dst = get_branch_immediate(inst); - WriteCondBranch(*this, *target, dst); - } else { - auto dst = get_branch_immediate(inst); - WriteB(dst); - } - } - break; - case ARM64_INS_BL: - { - auto dst = get_branch_immediate(inst); - WriteBl(dst); - } - break; - - // Handle fixups for conditional branches - case ARM64_INS_CBNZ: - case ARM64_INS_CBZ: - { - auto [reg, dst] = get_second_immediate(inst); - WriteCondBranch(*this, *target, dst); - } - break; - case ARM64_INS_TBNZ: - case ARM64_INS_TBZ: - { - auto [reg, dst] = get_second_immediate(inst); - WriteCondBranch(*this, *target, dst); - } - break; - - // Handle fixups for load literals - case ARM64_INS_LDR: - { - // TODO: Finish this fixup - constexpr static uint32_t b_31 = 0b10000000000000000000000000000000; - constexpr static uint32_t ldr_lit_opc_mask = 0b10111111000000000000000000000000; - if ((*target & ldr_lit_opc_mask) == 0b00011000000000000000000000000000) { - // This is an ldr literal - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- - auto [reg, dst] = get_second_immediate(inst); - } else if ((*target & (ldr_lit_opc_mask & ~b_31)) == 0b00011100000000000000000000000000000) { - // This is an ldr literal, SIMD - // https://developer.arm.com/documentation/ddi0596/2021-12/SIMD-FP-Instructions/LDR--literal--SIMD-FP---Load-SIMD-FP-Register--PC-relative-literal-- - auto [reg, dst] = get_second_immediate(inst); - - } - } - case ARM64_INS_LDRSW: - // TODO: Handle this fixup - - // Handle pc-relative loads - case ARM64_INS_ADR: - { - auto [reg, dst] = get_second_immediate(inst); - WriteAdr(reg, dst); - } - break; - case ARM64_INS_ADRP: - { - auto [reg, dst] = get_second_immediate(inst); - WriteAdrp(reg, dst); - } - break; - - // Otherwise, just write the instruction verbatim - default: - Write(*reinterpret_cast(inst.bytes)); - break; - } - -} - -void Trampoline::WriteCallback(uint32_t const* target) { - constexpr static uint32_t branch_imm_mask = 0b00000011111111111111111111111111U; - auto pc = reinterpret_cast(address + instruction_count); - auto delta = pc - reinterpret_cast(target); - if (std::llabs(delta) > (branch_imm_mask << 1) + 1) { - // To far for b. Emit a br instead. - WriteLdrBrData(target); - } else { - // Small enough to emit a b/bl. - // b opcode | encoded immediate (delta >> 2) - // Note, abs(delta >> 2) must be < (1 << 26) - constexpr static uint32_t b_opcode = 0b00010100000000000000000000000000U; - Write(b_opcode | ((delta >> 2) & branch_imm_mask)); - } -} - -void Trampoline::WriteFixups(uint32_t const* target, uint16_t countToFixup) { - original_instructions.reserve(countToFixup); - while (countToFixup-- > 0) { - WriteFixup(target++); - } - WriteCallback(target); - Finish(); -} - -void Trampoline::Finish() { - pageSizeRef -= alloc_size - (instruction_count * sizeof(uint32_t)); -} - -void Trampoline::Log() { - // TODO: Log the trampoline and various information here - // This will probably be necessary given the potential for failure - -} +#include +#include "trampoline.hpp" +#include "util.hpp" +namespace { struct PageType { void* ptr; std::size_t used_size; @@ -347,6 +16,9 @@ struct PageType { std::list pages; constexpr static std::size_t PageSize = 4096; +} // namespace + +namespace flamingo { Trampoline TrampolineAllocator::Allocate(std::size_t trampolineSize) { // Allocation should work by grabbing a full page at a time @@ -369,15 +41,15 @@ Trampoline TrampolineAllocator::Allocate(std::size_t trampolineSize) { void* ptr; if (!::posix_memalign(&ptr, PageSize, PageSize)) { // Log error on memalign allocation! - SAFE_ABORT_MSG("Failed to allocate trampoline page of size: %zu for size: %zu", PageSize, trampolineSize); + FLAMINGO_ABORT("Failed to allocate trampoline page of size: {} for size: {}", PageSize, trampolineSize); } // Mark full page as rxw if (!::mprotect(ptr, PageSize, PROT_READ | PROT_WRITE | PROT_EXEC)) { // Log error on mprotect! - SAFE_ABORT_MSG("Failed to mark allocated page at: %p as +rwx!", ptr); + FLAMINGO_ABORT("Failed to mark allocated page at: {} as +rwx!", fmt::ptr(ptr)); } auto& page = pages.emplace_back(ptr, trampolineSize); - return {static_cast(ptr), trampolineSize, page.used_size}; + return { static_cast(ptr), trampolineSize, page.used_size }; } void TrampolineAllocator::Free(Trampoline const& toFree) { @@ -394,7 +66,7 @@ void TrampolineAllocator::Free(Trampoline const& toFree) { if (p.trampoline_count == 0) { if (::mprotect(p.ptr, PageSize, PROT_READ) != 0) { // Log error on mprotect - SAFE_ABORT_MSG("Failed to mark page at: %p as read only!", p.ptr); + FLAMINGO_ABORT("Failed to mark page at: {} as read only!", fmt::ptr(p.ptr)); } ::free(p.ptr); } @@ -402,5 +74,7 @@ void TrampolineAllocator::Free(Trampoline const& toFree) { } } // If we get here, we couldn't free the provided Trampoline! - SAFE_ABORT_MSG("Failed to free trampoline at: %p, no matching page with page addr: %p!", toFree.address, page_addr); -} \ No newline at end of file + FLAMINGO_ABORT("Failed to free trampoline at: {}, no matching page with page addr: {}!", fmt::ptr(toFree.address), fmt::ptr(page_addr)); +} + +} // namespace flamingo diff --git a/src/trampoline.cpp b/src/trampoline.cpp new file mode 100644 index 0000000..3529222 --- /dev/null +++ b/src/trampoline.cpp @@ -0,0 +1,414 @@ +#include "trampoline.hpp" +#include +#include "capstone/capstone.h" +#include "capstone/platform.h" +#include "fmt/format.h" +#include "util.hpp" + +namespace flamingo { + +// TODO: We should consider an optimization where we have a location for fixup data instead of inling all fixups. +// This would allow us to write out actual assembly verbatim and then have ldrs and whatnot for grabbing the data +// This would save a few instructions per all of the fixups, since we wouldn't need to branch over the data +// In addition, it would allow us to have a better time disassembling. +// It also shouldn't cost us any space whatsoever +// Though, if we perform any hooks AFTER the fact, we would need to properly expand both our data and our instruction space +// and THAT could be somewhat tricky. +// Perhaps a full recompile for a hook is actually preferred, though, if we know we need to leapfrog +// Since we would need to expand our trampoline and our original instructions regardless. +// TODO: Consider a full recompile and permit late installations + +/// @brief Helper function that returns an encoded b for a particular offset +consteval uint32_t get_b(int offset) { + constexpr uint32_t b_opcode = 0b00010100000000000000000000000000U; + return (b_opcode | (offset >> 2)); +} + +constexpr int64_t get_untagged_pc(uint64_t pc) { + // Upper byte is tagged for PC addresses on android 11+ + constexpr uint64_t mask = ~(0xFFULL << 56); + return static_cast(static_cast(pc) & mask); +} + +void Trampoline::Write(uint32_t instruction) { + FLAMINGO_ASSERT((instruction_count + 1) * sizeof(uint32_t) <= alloc_size); + // Log what we are writing (and also our state) + address[instruction_count] = instruction; + instruction_count++; +} + +void Trampoline::WriteData(void const* ptr) { + // TODO: Write this to a different buffer and return a pointer to it + // This would allow the control flow for a hook to be much cleaner and the data section to be well defined + FLAMINGO_ASSERT(instruction_count * sizeof(uint32_t) + sizeof(void*) <= alloc_size); + // Log what we are writing (and also our state) + *reinterpret_cast(&address[instruction_count]) = const_cast(ptr); + instruction_count += sizeof(void*) / sizeof(uint32_t); +} + +void Trampoline::WriteData(void const* ptr, uint32_t size) { + // TODO: Write this to a different buffer and return a pointer to it + FLAMINGO_ASSERT((size + instruction_count) * sizeof(uint32_t) <= alloc_size); + FLAMINGO_DEBUG("Writing data from: {} of size: {}", ptr, size * sizeof(uint32_t)); + std::memcpy(&address[instruction_count], ptr, size * sizeof(uint32_t)); + instruction_count += size; +} + +void Trampoline::WriteB(int64_t imm) { + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/B--Branch- + WriteCallback(reinterpret_cast(imm)); +} + +void Trampoline::WriteBl(int64_t imm) { + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/BL--Branch-with-Link- + constexpr uint32_t branch_imm_mask = 0b00000011111111111111111111111111U; + int64_t pc = get_untagged_pc(reinterpret_cast(&address[instruction_count])); + int64_t delta = imm - pc; + if (std::llabs(delta) > (branch_imm_mask << 1) + 1) { + // Too far to emit a b. Emit a br instead. + // We cannot emit a blr here because the pc + 4 for return will be in our offset. + // LDR X17, #12 + constexpr uint32_t ldr_x17 = 0x58000071U; + Write(ldr_x17); + // ADR X30, #16 + constexpr uint32_t adr_x30 = 0x1000009EU; + Write(adr_x30); + // BR x17 + constexpr uint32_t br_x17 = 0xD61F0220U; + Write(br_x17); + // Data + WriteData(reinterpret_cast(imm)); + } else { + // Small enough to emit a b/bl. + // bl opcode | encoded immediate (delta >> 2) + // Note, abs(delta >> 2) must be < (1 << 26) + constexpr uint32_t bl_opcode = 0b10010100000000000000000000000000U; + Write(bl_opcode | ((delta >> 2) & branch_imm_mask)); + } +} + +void Trampoline::WriteAdr(uint8_t reg, int64_t imm) { + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/ADR--Form-PC-relative-address-?lang=en + constexpr uint32_t adr_maximum_imm = 0b00000000000111111111111111111111U; + constexpr uint32_t reg_mask = 0b11111; + int64_t pc = get_untagged_pc(reinterpret_cast(&address[instruction_count])); + int64_t delta = imm - pc; + if (std::llabs(delta) >= (adr_maximum_imm >> 1)) { + // Too far to emit just an adr. + // LDR (used register), #0x8 + constexpr uint32_t ldr_mask = 0b01011000000000000000000000000000U; + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- + // imm is encoded as << 2, LSB just to the right of reg + constexpr uint32_t ldr_imm = (8U >> 2U) << 5; + Write(ldr_mask | ldr_imm | (reg_mask & reg)); + // B #0xC + constexpr uint32_t b_0xc = 0x14000003U; + static_assert(b_0xc == get_b(0xC)); + Write(get_b(0xC)); + // Immediate data + WriteData(reinterpret_cast(imm)); + } else { + // Close enough to emit an adr. + // Note that delta should be within +-1 MB + constexpr uint32_t adr_opcode = 0b00010000000000000000000000000000; + // Get immlo + uint32_t imm_lo = ((static_cast(delta) & 3) << 29); + // Get immhi + uint32_t imm_hi = (static_cast(delta) >> 2) << 5; + Write(adr_opcode | imm_lo | imm_hi | (reg_mask & reg)); + } +} + +void Trampoline::WriteAdrp(uint8_t reg, int64_t imm) { + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/ADR--Form-PC-relative-address-?lang=en + // constexpr uint32_t adr_maximum_imm = 0b00000000000111111111111111111111U; + constexpr uint32_t reg_mask = 0b11111; + constexpr uint32_t pc_imm_mask = ~0b111111111111; + constexpr int64_t adrp_maximum_imm = 0xFFFFF000U; + int64_t pc = get_untagged_pc(reinterpret_cast(&address[instruction_count])); + int64_t delta = (pc & pc_imm_mask) - imm; + if (std::llabs(delta) >= adrp_maximum_imm) { + // Too far to emit just an adr. + // LDR (used register), #0x8 + constexpr uint32_t ldr_mask = 0b01011000000000000000000000000000U; + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- + // imm is encoded as << 2, LSB just to the right of reg + constexpr uint32_t ldr_imm = (8U >> 2U) << 5; + Write(ldr_mask | ldr_imm | (reg_mask & reg)); + // B #0xC + constexpr uint32_t b_0xc = 0x14000003U; + // TODO: Remove this assertion + static_assert(b_0xc == get_b(0xC)); + Write(get_b(0xC)); + // Write(b_0xc); + // Immediate data + WriteData(reinterpret_cast(imm)); + } else { + // Close enough to emit an adrp. + // Note that delta should be within +-4 GB + constexpr uint32_t adrp_opcode = 0b10010000000000000000000000000000U; + // Imm is << 12 in parse of instruction + delta >>= 12; + // Get immlo + uint32_t imm_lo = ((static_cast(delta) & 3) << 29); + // Get immhi + uint32_t imm_hi = (static_cast(delta) >> 2) << 5; + Write(adrp_opcode | imm_lo | imm_hi | (reg_mask & reg)); + } +} + +void Trampoline::WriteLdr(uint32_t inst, uint8_t reg, int64_t imm) { + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- + constexpr uint32_t reg_mask = 0b11111; + // 20 bits because signed range is only allowed + // constexpr int64_t max_ldr_range = (1LL << 20); + if ((inst & 0xFF000000U) == 0xD8000000U) { + // This is a prefetch instruction. + // Lets just skip it. + return; + } + // int64_t pc = get_untagged_pc(reinterpret_cast(&address[instruction_count])); + // int64_t delta = imm - pc; + // TODO: Note missed optimization opportunity + // TODO: Should perform a small LDR + FLAMINGO_DEBUG("Potentially missed optimization opportunity for near LDRs!"); + + // if (std::llabs(delta) >= max_ldr_range) { + + // Too far to emit an equivalent LDR + // Fallback to performing a direct memory write/read + // LDR (used register), #0x8 + constexpr uint32_t ldr_mask = 0b01011000000000000000000000000000U; + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- + // imm is encoded as << 2, LSB just to the right of reg + constexpr uint32_t ldr_imm = (8U >> 2U) << 5; + Write(ldr_mask | ldr_imm | (reg_mask & reg)); + // B #0xC + Write(get_b(0xC)); + // Immediate data + // 4, 8, 16(?) for our sizes + constexpr uint32_t size_mask = 0x40000000U; + // TODO: Pull the start address from this data write + WriteData(reinterpret_cast(imm), 1); + if ((inst & size_mask) != 0) { + WriteData(reinterpret_cast(imm + sizeof(uint32_t)), 1); + } + + // } else { + // // Close enough to emit an LDR + // FLAMINGO_ABORT("Close LDR optimization is not implemented (with no fallback)!"); + // } +} + +template +void WriteCondBranch(Trampoline& value, uint32_t instruction, int64_t imm) { + uint32_t imm_mask; + if constexpr (imm_19) { + // Imm 19 + constexpr uint32_t imm_mask_19 = 0b00000000111111111111111111100000; + imm_mask = imm_mask_19; + } else { + // Imm 14 + constexpr uint32_t imm_mask_14 = 0b00000000000001111111111111100000; + imm_mask = imm_mask_14; + } + int64_t pc = get_untagged_pc(reinterpret_cast(&value.address[value.instruction_count])); + int64_t delta = imm - pc; + // imm_mask >> 1 for maximum positive value + // << 2 because branch imms are << 2 + // >> 5 because the mask is too high + if (std::llabs(delta) < (imm_mask >> 4)) { + // Small enough to optimize, just write the instruction + // But with the modified offset + // Delta should be >> 2 for branch imm + // Then << 5 to be in the correct location + value.Write((instruction & ~imm_mask) | (((delta >> 2) << 5) & imm_mask)); + } else { + // Otherwise, we need to write the same expression but with a known offset + // Specifically, write the instruction but with an offset of 8 + // 2, because 8 >> 2 is 2 + // << 5 to place in correct location for immediate + value.Write((instruction & ~imm_mask) | ((2 << 5) & imm_mask)); + value.Write(get_b(0x14)); + value.WriteLdrBrData(reinterpret_cast(imm)); + } +} + +void Trampoline::WriteLdrBrData(uint32_t const* target) { + // LDR x17, 0x8 + constexpr uint32_t ldr_x17 = 0x58000051U; + Write(ldr_x17); + // BR x17 + constexpr uint32_t br_x17 = 0xD61F0220U; + Write(br_x17); + // Data + WriteData(target); +} + +auto get_branch_immediate(cs_insn const& inst) { + FLAMINGO_ASSERT(inst.detail->arm64.op_count == 1); + return inst.detail->arm64.operands[0].imm; +} + +std::pair get_second_immediate(cs_insn const& inst) { + // register is just bottom 5 bits + constexpr uint32_t reg_mask = 0b11111; + FLAMINGO_ASSERT(inst.detail->arm64.op_count == 2); + return { *reinterpret_cast(inst.bytes) & reg_mask, inst.detail->arm64.operands[1].imm }; +} + +std::pair get_last_immediate(cs_insn const& inst) { + // register is just bottom 5 bits + constexpr uint32_t reg_mask = 0b11111; + FLAMINGO_ASSERT(inst.detail->arm64.op_count >= 2); + return { *reinterpret_cast(inst.bytes) & reg_mask, inst.detail->arm64.operands[inst.detail->arm64.op_count - 11].imm }; +} + +csh getHandle() { + static csh handle = 0; + if (!handle) { + cs_err e1 = cs_open(CS_ARCH_ARM64, CS_MODE_ARM, &handle); + cs_option(handle, CS_OPT_DETAIL, 1); + if (e1) { + FLAMINGO_ABORT("Capstone initialization failed"); + } + } + return handle; +} + +cs_insn debugInst(uint32_t const* inst) { + cs_insn* insns = nullptr; + auto count = cs_disasm(getHandle(), reinterpret_cast(inst), sizeof(uint32_t), static_cast(get_untagged_pc(reinterpret_cast(inst))), 1, &insns); + if (count == 1) { + return insns[0]; + } + return {}; +} + +void Trampoline::WriteFixup(uint32_t const* target) { + // TODO: Make this faster for cases where we will write many fixups + FLAMINGO_ASSERT(target); + // Target is where we want to grab original instruction from + // Log everything we do here + original_instructions.push_back(*target); + cs_insn* insns = nullptr; + auto count = cs_disasm(getHandle(), reinterpret_cast(target), sizeof(uint32_t), static_cast(get_untagged_pc(reinterpret_cast(target))), 1, &insns); + FLAMINGO_ASSERT(count == 1); + auto inst = insns[0]; + // constexpr uint32_t cond_branch_mask = 0b11111111000000000000000000011111; + // TODO: Finish writing fixups here + FLAMINGO_DEBUG("Fixup for inst: 0x{:x} at {}: {} {}, id: {}", *target, fmt::ptr(target), fmt::string_view(inst.mnemonic, sizeof(inst.mnemonic)), fmt::string_view(inst.op_str, sizeof(inst.op_str)), + static_cast(inst.id)); + switch (inst.id) { + // Handle fixups for branch immediate + case ARM64_INS_B: { + FLAMINGO_DEBUG("Fixing up B..."); + if (inst.detail->arm64.cc != ARM64_CC_INVALID) { + // TODO: Handle this like a conditional branch + auto dst = get_branch_immediate(inst); + WriteCondBranch(*this, *target, dst); + } else { + auto dst = get_branch_immediate(inst); + WriteB(dst); + } + } break; + case ARM64_INS_BL: { + FLAMINGO_DEBUG("Fixing up BL..."); + auto dst = get_branch_immediate(inst); + WriteBl(dst); + } break; + + // Handle fixups for conditional branches + case ARM64_INS_CBNZ: + case ARM64_INS_CBZ: { + FLAMINGO_DEBUG("Fixing up CBNZ/CBZ..."); + auto [reg, dst] = get_last_immediate(inst); + WriteCondBranch(*this, *target, dst); + } break; + case ARM64_INS_TBNZ: + case ARM64_INS_TBZ: { + FLAMINGO_DEBUG("Fixing up TBNZ/TBZ..."); + auto [reg, dst] = get_last_immediate(inst); + WriteCondBranch(*this, *target, dst); + } break; + + // Handle fixups for load literals + case ARM64_INS_LDR: { + FLAMINGO_DEBUG("Fixing up LDR..."); + // TODO: Finish this fixup + constexpr uint32_t b_31 = 0b10000000000000000000000000000000; + constexpr uint32_t ldr_lit_opc_mask = 0b10111111000000000000000000000000; + if ((*target & ldr_lit_opc_mask) == 0b00011000000000000000000000000000) { + // This is an ldr literal + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- + auto [reg, dst] = get_second_immediate(inst); + WriteLdr(*target, reg, dst); + } else if ((*target & (ldr_lit_opc_mask & ~b_31)) == 0b00011100000000000000000000000000000) { + // This is an ldr literal, SIMD + // https://developer.arm.com/documentation/ddi0596/2021-12/SIMD-FP-Instructions/LDR--literal--SIMD-FP---Load-SIMD-FP-Register--PC-relative-literal-- + FLAMINGO_ABORT("LDR of the SIMD variant is not yet supported!"); + } + } break; + case ARM64_INS_LDRSW: { + // This is an ldrsw literal + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDRSW--literal---Load-Register-Signed-Word--literal-- + FLAMINGO_ABORT("LDRSW fixup not yet supported!"); + } break; + + // Handle pc-relative loads + case ARM64_INS_ADR: { + FLAMINGO_DEBUG("Fixing up ADR..."); + auto [reg, dst] = get_second_immediate(inst); + WriteAdr(reg, dst); + } break; + case ARM64_INS_ADRP: { + FLAMINGO_DEBUG("Fixing up ADRP..."); + auto [reg, dst] = get_second_immediate(inst); + WriteAdrp(reg, dst); + } break; + + // Otherwise, just write the instruction verbatim + default: + FLAMINGO_DEBUG("Fixing up UNKNOWN: {}...", inst.id); + Write(*reinterpret_cast(inst.bytes)); + break; + } +} + +void Trampoline::WriteCallback(uint32_t const* target) { + constexpr uint32_t branch_imm_mask = 0b00000011111111111111111111111111U; + int64_t pc = get_untagged_pc(reinterpret_cast(&address[instruction_count])); + auto delta = reinterpret_cast(target) - pc; + if (std::llabs(delta) > (branch_imm_mask << 1) + 1) { + // Too far for b. Emit a br instead. + WriteLdrBrData(target); + } else { + // Small enough to emit a b. + // b opcode | encoded immediate (delta >> 2) + // Note, abs(delta >> 2) must be < (1 << 26) + constexpr uint32_t b_opcode = 0b00010100000000000000000000000000U; + Write(b_opcode | ((delta >> 2) & branch_imm_mask)); + } +} + +void Trampoline::WriteFixups(uint32_t const* target, uint16_t countToFixup) { + original_instructions.reserve(countToFixup); + FLAMINGO_DEBUG("Fixing up: {} instructions!", countToFixup); + while (countToFixup-- > 0) { + WriteFixup(target++); + } + WriteCallback(target); + Finish(); +} + +void Trampoline::Finish() { + pageSizeRef -= alloc_size - (instruction_count * sizeof(uint32_t)); + FLAMINGO_DEBUG("Completed trampoline allocation of: {} instructions!", instruction_count); +} + +void Trampoline::Log() { + // TODO: Log the trampoline and various information here + // This will probably be necessary given the potential for failure +} + +} // namespace flamingo From 18c0812f67d8199a1a961e79cb4e567e9a20d648 Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Tue, 16 May 2023 00:39:05 -0700 Subject: [PATCH 004/134] Add tests and TODOs, should be run on linux --- test/main.cpp | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 test/main.cpp diff --git a/test/main.cpp b/test/main.cpp new file mode 100644 index 0000000..36819a9 --- /dev/null +++ b/test/main.cpp @@ -0,0 +1,94 @@ +// Main test runner for testing fixups behave as intended +#include +#include +#include +#include + +#include "../shared/trampoline-allocator.hpp" +#include "../shared/trampoline.hpp" +#include "capstone/capstone.h" + +decltype(auto) test_near(uint32_t* target, uint32_t const* callback) { + constexpr size_t hookSize = 32; + constexpr size_t pageSize = 4096; + constexpr size_t trampolineSize = 64; + static std::array test_trampoline; + size_t page_size = pageSize; + printf("TRAMPOLINE: %p\n", test_trampoline.data()); + flamingo::Trampoline trampoline(test_trampoline.data(), sizeof(test_trampoline), page_size); + // Attempt to write a hook from target --> callback (just for testing purposes) + std::size_t trampoline_size = hookSize; + // Hook size is 5, but we only fixup 4 + trampoline.WriteFixups(target, 4); + // Write actual hook to be a callback + // We need to mark the location of target as writable (so we can write to it correctly) + ::mprotect(target, hookSize, PROT_READ | PROT_WRITE | PROT_EXEC); + ::flamingo::Trampoline targetHook(target, hookSize, trampoline_size); + targetHook.WriteCallback(callback); + targetHook.Finish(); + return &test_trampoline; +} + +void print_decode_loop(uint32_t* val, int n) { + auto handle = flamingo::getHandle(); + for (int i = 0; i < n; i++) { + cs_insn* insns = nullptr; + auto count = cs_disasm(handle, reinterpret_cast(val), sizeof(uint32_t), static_cast(reinterpret_cast(val)), 1, &insns); + if (count == 1) { + printf("Addr: %p Value: 0x%x, %s %s\n", val, *val, insns[0].mnemonic, insns[0].op_str); + } else { + printf("Addr: %p Value: 0x%x\n", val, *val); + } + val++; + } +} + +void perform_near_hook_test(uint8_t* to_hook) { + printf("TO HOOK: %p\n", to_hook); + print_decode_loop(reinterpret_cast(to_hook), 6); + puts("TEST NEAR..."); + auto* trampoline_data = test_near(reinterpret_cast(to_hook), (const uint32_t*)(0xDEADBEEFBAADF00DULL)); + // Use 20 here as a reasonable guesstimate + print_decode_loop(trampoline_data->data(), 20); + puts("HOOKED:"); + print_decode_loop(reinterpret_cast(to_hook), 6); +} + +void test_near_no_fixups() { + puts("Testing near -- no fixups!"); + static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8, 0xf6, 0x57, 0x01, 0xa9, 0xf4, 0x4f, 0x02, 0xa9, 0xfd, 0x7b, 0x03, 0xa9, 0xfd, 0xc3, 0x00, 0x91, 0x48, 0x18, + 0x40, 0xf9, 0x16, 0xd4, 0x42, 0xa9, 0xf3, 0x03, 0x02, 0xaa, 0xf4, 0x03, 0x01, 0xaa, 0x17, 0x01, 0x40, 0xf9, 0xe8, 0xba, 0x44, 0x39 }; + perform_near_hook_test(to_hook); +} + +void test_near_bls_tbzs() { + puts("Testing near -- bls/tbzs"); + // TODO: tbnz fixup: + /* +Addr: 0x803d244 Value: 0x37000048, tbnz w8, #0, #0x803d24c +Addr: 0x803d248 Value: 0x14000005, b #0x803d25c +Addr: 0x803d24c Value: 0x58000051, ldr x17, #0x803d254 +Addr: 0x803d250 Value: 0xd61f0220, br x17 +Addr: 0x803d254 Value: 0x0 +Addr: 0x803d258 Value: 0x0 +Addr: 0x803d25c Value: 0xaa1703e0, mov x0, x23 + */ + // WHICH IS WRONG! Because it should not branch to 0??? + static uint8_t to_hook[]{ 0x68, 0x00, 0x00, 0x37, 0xe0, 0x03, 0x17, 0xaa, 0x52, 0x3e, 0xfd, 0x97, 0xe0, 0x03, 0x17, 0xaa, 0x64, 0x7b, 0xfe, 0x97, 0x00, 0x00, 0x00, 0x00 }; + perform_near_hook_test(to_hook); +} + +void test_ldr_ldrb_tbnz_bl() { + puts("Testing near -- ldr/ldrb/tbnz/bl"); + // TODO: ldr fixup for: + // Fixup for inst: 0xf9400117 at 0x803d220: ldr x23, [x8], id: 162 + // SHOULD result in a naive copy, but INSTEAD, nothing is emitted! + static uint8_t to_hook[]{ 0x17, 0x01, 0x40, 0xf9, 0xe8, 0xba, 0x44, 0x39, 0x68, 0x00, 0x00, 0x37, 0xe0, 0x03, 0x17, 0xaa, 0x52, 0x3e, 0xfd, 0x97, 0x00, 0x00, 0x00, 0x00 }; + perform_near_hook_test(to_hook); +} + +int main() { + test_near_no_fixups(); + test_near_bls_tbzs(); + test_ldr_ldrb_tbnz_bl(); +} From 1431fa5fefe337e03d67596844c987031d826f0c Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Tue, 16 May 2023 20:20:00 -0700 Subject: [PATCH 005/134] Fix tbnz fixup using wrong immediate, ldr emit --- src/trampoline.cpp | 11 +++++++++-- test/main.cpp | 20 ++++---------------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/trampoline.cpp b/src/trampoline.cpp index 3529222..754a77c 100644 --- a/src/trampoline.cpp +++ b/src/trampoline.cpp @@ -222,7 +222,7 @@ void WriteCondBranch(Trampoline& value, uint32_t instruction, int64_t imm) { // But with the modified offset // Delta should be >> 2 for branch imm // Then << 5 to be in the correct location - value.Write((instruction & ~imm_mask) | (((delta >> 2) << 5) & imm_mask)); + value.Write((instruction & ~imm_mask) | (static_cast((delta >> 2) << 5) & imm_mask)); } else { // Otherwise, we need to write the same expression but with a known offset // Specifically, write the instruction but with an offset of 8 @@ -261,7 +261,7 @@ std::pair get_last_immediate(cs_insn const& inst) { // register is just bottom 5 bits constexpr uint32_t reg_mask = 0b11111; FLAMINGO_ASSERT(inst.detail->arm64.op_count >= 2); - return { *reinterpret_cast(inst.bytes) & reg_mask, inst.detail->arm64.operands[inst.detail->arm64.op_count - 11].imm }; + return { *reinterpret_cast(inst.bytes) & reg_mask, inst.detail->arm64.operands[inst.detail->arm64.op_count - 1].imm }; } csh getHandle() { @@ -287,6 +287,9 @@ cs_insn debugInst(uint32_t const* inst) { void Trampoline::WriteFixup(uint32_t const* target) { // TODO: Make this faster for cases where we will write many fixups + // TODO: tbnz fixup (and other branches too!) need to note if they are referential and know where to branch to + // We need to track the fact that it isn't yet written + // and write the correct instruction when we perform the fixup for the instruction we DO care about. FLAMINGO_ASSERT(target); // Target is where we want to grab original instruction from // Log everything we do here @@ -347,6 +350,10 @@ void Trampoline::WriteFixup(uint32_t const* target) { // This is an ldr literal, SIMD // https://developer.arm.com/documentation/ddi0596/2021-12/SIMD-FP-Instructions/LDR--literal--SIMD-FP---Load-SIMD-FP-Register--PC-relative-literal-- FLAMINGO_ABORT("LDR of the SIMD variant is not yet supported!"); + } else { + // This is an LDR that doesn't need to be fixed up + FLAMINGO_DEBUG("Fixing up standard LDR..."); + Write(*reinterpret_cast(inst.bytes)); } } break; case ARM64_INS_LDRSW: { diff --git a/test/main.cpp b/test/main.cpp index 36819a9..a884ccb 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -61,34 +61,22 @@ void test_near_no_fixups() { perform_near_hook_test(to_hook); } -void test_near_bls_tbzs() { +void test_near_bls_tbzs_within_hook() { puts("Testing near -- bls/tbzs"); - // TODO: tbnz fixup: - /* -Addr: 0x803d244 Value: 0x37000048, tbnz w8, #0, #0x803d24c -Addr: 0x803d248 Value: 0x14000005, b #0x803d25c -Addr: 0x803d24c Value: 0x58000051, ldr x17, #0x803d254 -Addr: 0x803d250 Value: 0xd61f0220, br x17 -Addr: 0x803d254 Value: 0x0 -Addr: 0x803d258 Value: 0x0 -Addr: 0x803d25c Value: 0xaa1703e0, mov x0, x23 - */ - // WHICH IS WRONG! Because it should not branch to 0??? static uint8_t to_hook[]{ 0x68, 0x00, 0x00, 0x37, 0xe0, 0x03, 0x17, 0xaa, 0x52, 0x3e, 0xfd, 0x97, 0xe0, 0x03, 0x17, 0xaa, 0x64, 0x7b, 0xfe, 0x97, 0x00, 0x00, 0x00, 0x00 }; perform_near_hook_test(to_hook); } void test_ldr_ldrb_tbnz_bl() { puts("Testing near -- ldr/ldrb/tbnz/bl"); - // TODO: ldr fixup for: - // Fixup for inst: 0xf9400117 at 0x803d220: ldr x23, [x8], id: 162 - // SHOULD result in a naive copy, but INSTEAD, nothing is emitted! static uint8_t to_hook[]{ 0x17, 0x01, 0x40, 0xf9, 0xe8, 0xba, 0x44, 0x39, 0x68, 0x00, 0x00, 0x37, 0xe0, 0x03, 0x17, 0xaa, 0x52, 0x3e, 0xfd, 0x97, 0x00, 0x00, 0x00, 0x00 }; perform_near_hook_test(to_hook); } +// TODO: Test a case where we have a loop in the first 4 instructions + int main() { test_near_no_fixups(); - test_near_bls_tbzs(); + test_near_bls_tbzs_within_hook(); test_ldr_ldrb_tbnz_bl(); } From 4ac096a952e938581657bc44c1a3fe84a0f14c98 Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Wed, 17 May 2023 00:37:17 -0700 Subject: [PATCH 006/134] Restructure WriteFixups to allow for deferred writes Deferred writes allow for referential branches to be handled correctly. More work needs to be done to properly manage these referential writes and to better expose the trampoline API only where needed --- shared/trampoline.hpp | 6 +- src/trampoline.cpp | 332 +++++++++++++++++++++++++++++------------- 2 files changed, 236 insertions(+), 102 deletions(-) diff --git a/shared/trampoline.hpp b/shared/trampoline.hpp index 36bfd43..cd99fa3 100644 --- a/shared/trampoline.hpp +++ b/shared/trampoline.hpp @@ -38,8 +38,10 @@ struct Trampoline { /// BR X17 /// DATA void WriteLdrBrData(uint32_t const* target); - void WriteFixup(uint32_t const* target); - void WriteFixups(uint32_t const* target, uint16_t countToFixup); + // TODO: Put this in an internal-trampoline header instead so that it can be used from other TUs but is not exposed + template + void WriteFixups(uint32_t const* target); + void WriteHookFixups(uint32_t const* target); /// @brief Logs various information about the trampoline. void Log(); /// @brief A TRAMPOLINE IS NOT COMPLETE UNTIL FINISH IS CALLED! diff --git a/src/trampoline.cpp b/src/trampoline.cpp index 754a77c..c9a44de 100644 --- a/src/trampoline.cpp +++ b/src/trampoline.cpp @@ -1,5 +1,7 @@ #include "trampoline.hpp" +#include #include +#include #include "capstone/capstone.h" #include "capstone/platform.h" #include "fmt/format.h" @@ -18,6 +20,31 @@ namespace flamingo { // Since we would need to expand our trampoline and our original instructions regardless. // TODO: Consider a full recompile and permit late installations +// Helper types for holding immediate masks, lshifts and rshifts for conversions to immediates from PC differences +template +struct BranchImmTypeTrait; + +template +requires(type == ARM64_INS_B || type == ARM64_INS_BL) struct BranchImmTypeTrait { + constexpr static uint32_t imm_mask = 0b00000011111111111111111111111111U; + constexpr static uint32_t lshift = 0; + constexpr static uint32_t rshift = 2; +}; + +template +requires(type == ARM64_INS_CBZ || type == ARM64_INS_CBNZ) struct BranchImmTypeTrait { + constexpr static uint32_t imm_mask = 0b00000000111111111111111111100000; + constexpr static uint32_t lshift = 5; + constexpr static uint32_t rshift = 2; +}; + +template +requires(type == ARM64_INS_TBZ || type == ARM64_INS_TBNZ) struct BranchImmTypeTrait { + constexpr static uint32_t imm_mask = 0b00000000000001111111111111100000; + constexpr static uint32_t lshift = 5; + constexpr static uint32_t rshift = 2; +}; + /// @brief Helper function that returns an encoded b for a particular offset consteval uint32_t get_b(int offset) { constexpr uint32_t b_opcode = 0b00010100000000000000000000000000U; @@ -278,108 +305,223 @@ csh getHandle() { cs_insn debugInst(uint32_t const* inst) { cs_insn* insns = nullptr; - auto count = cs_disasm(getHandle(), reinterpret_cast(inst), sizeof(uint32_t), static_cast(get_untagged_pc(reinterpret_cast(inst))), 1, &insns); + auto count = cs_disasm(getHandle(), reinterpret_cast(inst), sizeof(uint32_t), + static_cast(get_untagged_pc(reinterpret_cast(inst))), 1, &insns); if (count == 1) { return insns[0]; } return {}; } -void Trampoline::WriteFixup(uint32_t const* target) { - // TODO: Make this faster for cases where we will write many fixups - // TODO: tbnz fixup (and other branches too!) need to note if they are referential and know where to branch to - // We need to track the fact that it isn't yet written - // and write the correct instruction when we perform the fixup for the instruction we DO care about. +struct BranchReferenceTag { + /// @brief The immediate mask to use when rewriting the instruction + uint32_t imm_mask; + /// @brief The amount to shift the immediate to the left to encode it correctly such that the mask would be valid + uint32_t lshift; + /// @brief The amount to shift the immediate to the right to encode it correctly such that the mask would be valid + uint32_t rshift; + /// @brief Index to overwrite + uint32_t target_index; +}; + +template +bool TryDeferBranch(Trampoline& self, uint16_t i, int64_t dst, int64_t target_start, int64_t target_end, uint32_t inst, + std::array const& target_to_fixups, std::array, Sz>& branch_ref_map) { + // If we are within OUR fixup range, that's when things get interesting. + // TODO: If we are in SOME OTHER TRAMPOLINE'S fixup range, then we should use their call + using trait_t = BranchImmTypeTrait; + constexpr uint32_t imm_mask = trait_t::imm_mask; + constexpr uint32_t lshift = trait_t::lshift; + constexpr uint32_t rshift = trait_t::rshift; + if (dst < target_end && dst >= target_start) { + FLAMINGO_DEBUG("Potentially deferring branch at: {} because it is within: {} and {}", dst, target_start, target_end); + auto target_offset = (dst - target_start) / sizeof(uint32_t); + // Always emit the instruction with AN immediate. + // For forward references, we need to defer. + // This difference could be negative, but for those cases we will defer and overwrite. + auto fixup_difference = static_cast(get_untagged_pc(reinterpret_cast(&self.address[self.instruction_count])) - + get_untagged_pc(reinterpret_cast(&self.address[target_to_fixups[target_offset]]))); + self.Write((inst & ~imm_mask) | ((fixup_difference >> rshift) << lshift)); + if (target_offset > i) { + FLAMINGO_DEBUG("Deferring at: {} with target offset: {}", i, target_offset); + // Need to defer. + // Deference SHOULD never cause the instruction being deferred to expand in size. + // It should always be possible to point the deferred instruction to the new one without emitting more instructions + branch_ref_map[target_offset].push_back({ + .imm_mask = imm_mask, + .lshift = lshift, + .rshift = rshift, + .target_index = i, + }); + } + return true; + } + return false; +} + +template +void Trampoline::WriteFixups(uint32_t const* target) { FLAMINGO_ASSERT(target); - // Target is where we want to grab original instruction from - // Log everything we do here - original_instructions.push_back(*target); + // Copy over original instructions + FLAMINGO_DEBUG("Fixing up: {} instructions!", countToFixup); + original_instructions.resize(countToFixup); + std::memcpy(original_instructions.data(), target, countToFixup); + // The end of the fixups is used for referential branches + auto target_start = get_untagged_pc(reinterpret_cast(target)); + auto target_end = get_untagged_pc(reinterpret_cast(&target[countToFixup])); + // Disassemble the instructions into this pointer, which we can then index into per instruction cs_insn* insns = nullptr; - auto count = cs_disasm(getHandle(), reinterpret_cast(target), sizeof(uint32_t), static_cast(get_untagged_pc(reinterpret_cast(target))), 1, &insns); - FLAMINGO_ASSERT(count == 1); - auto inst = insns[0]; - // constexpr uint32_t cond_branch_mask = 0b11111111000000000000000000011111; - // TODO: Finish writing fixups here - FLAMINGO_DEBUG("Fixup for inst: 0x{:x} at {}: {} {}, id: {}", *target, fmt::ptr(target), fmt::string_view(inst.mnemonic, sizeof(inst.mnemonic)), fmt::string_view(inst.op_str, sizeof(inst.op_str)), - static_cast(inst.id)); - switch (inst.id) { - // Handle fixups for branch immediate - case ARM64_INS_B: { - FLAMINGO_DEBUG("Fixing up B..."); - if (inst.detail->arm64.cc != ARM64_CC_INVALID) { - // TODO: Handle this like a conditional branch + auto count = cs_disasm(getHandle(), reinterpret_cast(target), sizeof(uint32_t) * countToFixup, + static_cast(get_untagged_pc(reinterpret_cast(target))), countToFixup, &insns); + // We should never try to write fixups for something that isn't a valid instruction + FLAMINGO_ASSERT(count == countToFixup); + // We use this set to track referential branches going forwards + // If it is a branch, check to see if the target immediate would place us within our fixup range + // If so, we need to: + // - If the target is behind us, use the new target directly + // - If the target is in front of us, defer the write until later. + // We defer the write by basically writing the branch itself (since it must be a close branch) + // and then add its index (and its immediate mask and shift amount) to some set. + // Then, when we start the fixup for an instruction, we check the set to see if we should go back and fix the specified indices. + // To fix them, we simply walk all of the indices we wish to fix, and for each: + // - Current PC of instruction - &target[index] to replace + // - Use as argument for shift + mask? + + // TODO: Avoid heap alloc if possible + std::array, countToFixup> branch_ref_map{}; + // Holds the mapping of target index to fixup index + // TODO: Make this a publicly exposed member? + // TODO: Technically, we need to see if ANY branch target would leave us in ANY fixup block... + // TODO: Should collect a set of references so that if we ever install a hook over somewhere we would jump to we would force a recompile + // We should check against the full set of trampolines for this + std::array target_to_fixups{}; + + for (uint16_t i = 0; i < countToFixup; i++) { + // If it is a branch, check to see if the target immediate would place us within our fixup range + // If so, we need to: + // - If the target is behind us, use the new target directly + // - If the target is in front of us, defer the write until later. + // We defer the write by basically writing the branch itself (since it must be a close branch) + // and then add its index (and its immediate mask and shift amount) to some set. + // Then, when we start the fixup for an instruction, we check the set to see if we should go back and fix the specified indices. + // To fix them, we simply walk all of the indices we wish to fix, and for each: + // - Current PC of instruction - &target[index] to replace + // - Use as argument for shift + mask? + + // For each input instruction, perform a fixup on it + auto const& inst = insns[i]; + auto current_inst_ptr = &target[i]; + FLAMINGO_DEBUG("Fixup for inst: 0x{:x} at {}: {} {}, id: {}", *current_inst_ptr, fmt::ptr(current_inst_ptr), + fmt::string_view(inst.mnemonic, sizeof(inst.mnemonic)), fmt::string_view(inst.op_str, sizeof(inst.op_str)), static_cast(inst.id)); + // For this incoming instruction, check to see if we have any forward references on this + // If we do, for each, rewrite the target instruction with the adjusted value + for (auto const& tag : branch_ref_map[i]) { + // Current PC is get_untagged_pc(&address[instruction_count]) + // The instruction we emit's PC is the map from target --> fixup + // This difference is always positive, since we are jumping FORWARD + uint32_t difference = static_cast(get_untagged_pc(reinterpret_cast(&address[instruction_count])) - + get_untagged_pc(reinterpret_cast(&address[target_to_fixups[tag.target_index]]))); + FLAMINGO_DEBUG("Performing deferred write at: {}, rewriting: {} with difference: {}", i, tag.target_index, difference); + address[target_to_fixups[tag.target_index]] = + (address[target_to_fixups[tag.target_index]] & ~tag.imm_mask) | (tag.imm_mask & ((difference >> tag.rshift) << tag.lshift)); + } + // Set the target map entry for this incoming instruction to the current offset of the output + target_to_fixups[i] = instruction_count; + switch (inst.id) { + // Handle fixups for branch immediate + case ARM64_INS_B: { + FLAMINGO_DEBUG("Fixing up B..."); auto dst = get_branch_immediate(inst); - WriteCondBranch(*this, *target, dst); - } else { + if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, branch_ref_map)) { + if (inst.detail->arm64.cc != ARM64_CC_INVALID) { + WriteCondBranch(*this, *current_inst_ptr, dst); + } else { + WriteB(dst); + } + } + } break; + case ARM64_INS_BL: { + FLAMINGO_DEBUG("Fixing up BL..."); auto dst = get_branch_immediate(inst); - WriteB(dst); - } - } break; - case ARM64_INS_BL: { - FLAMINGO_DEBUG("Fixing up BL..."); - auto dst = get_branch_immediate(inst); - WriteBl(dst); - } break; - - // Handle fixups for conditional branches - case ARM64_INS_CBNZ: - case ARM64_INS_CBZ: { - FLAMINGO_DEBUG("Fixing up CBNZ/CBZ..."); - auto [reg, dst] = get_last_immediate(inst); - WriteCondBranch(*this, *target, dst); - } break; - case ARM64_INS_TBNZ: - case ARM64_INS_TBZ: { - FLAMINGO_DEBUG("Fixing up TBNZ/TBZ..."); - auto [reg, dst] = get_last_immediate(inst); - WriteCondBranch(*this, *target, dst); - } break; - - // Handle fixups for load literals - case ARM64_INS_LDR: { - FLAMINGO_DEBUG("Fixing up LDR..."); - // TODO: Finish this fixup - constexpr uint32_t b_31 = 0b10000000000000000000000000000000; - constexpr uint32_t ldr_lit_opc_mask = 0b10111111000000000000000000000000; - if ((*target & ldr_lit_opc_mask) == 0b00011000000000000000000000000000) { - // This is an ldr literal - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- + if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, branch_ref_map)) { + WriteBl(dst); + } + } break; + + // Handle fixups for conditional branches + case ARM64_INS_CBNZ: + case ARM64_INS_CBZ: { + FLAMINGO_DEBUG("Fixing up CBNZ/CBZ..."); + auto [reg, dst] = get_last_immediate(inst); + if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, branch_ref_map)) { + WriteCondBranch(*this, *current_inst_ptr, dst); + } + } break; + case ARM64_INS_TBNZ: + case ARM64_INS_TBZ: { + FLAMINGO_DEBUG("Fixing up TBNZ/TBZ..."); + auto [reg, dst] = get_last_immediate(inst); + if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, branch_ref_map)) { + WriteCondBranch(*this, *current_inst_ptr, dst); + } + } break; + + // Handle fixups for load literals + case ARM64_INS_LDR: { + FLAMINGO_DEBUG("Fixing up LDR..."); + // TODO: Handle the case where an LDR would land in a fixup range, so we need to copy the raw values + constexpr uint32_t b_31 = 0b10000000000000000000000000000000; + constexpr uint32_t ldr_lit_opc_mask = 0b10111111000000000000000000000000; + if ((*current_inst_ptr & ldr_lit_opc_mask) == 0b00011000000000000000000000000000) { + // This is an ldr literal + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- + auto [reg, dst] = get_second_immediate(inst); + WriteLdr(*current_inst_ptr, reg, dst); + } else if ((*current_inst_ptr & (ldr_lit_opc_mask & ~b_31)) == 0b00011100000000000000000000000000000) { + // This is an ldr literal, SIMD + // https://developer.arm.com/documentation/ddi0596/2021-12/SIMD-FP-Instructions/LDR--literal--SIMD-FP---Load-SIMD-FP-Register--PC-relative-literal-- + FLAMINGO_ABORT("LDR of the SIMD variant is not yet supported!"); + } else { + // This is an LDR that doesn't need to be fixed up + FLAMINGO_DEBUG("Fixing up standard LDR..."); + Write(*reinterpret_cast(inst.bytes)); + } + } break; + case ARM64_INS_LDRSW: { + // This is an ldrsw literal + // See TODOs for LDR + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDRSW--literal---Load-Register-Signed-Word--literal-- + FLAMINGO_ABORT("LDRSW fixup not yet supported!"); + } break; + + // Handle pc-relative loads + case ARM64_INS_ADR: { + FLAMINGO_DEBUG("Fixing up ADR..."); + auto [reg, dst] = get_second_immediate(inst); + WriteAdr(reg, dst); + } break; + case ARM64_INS_ADRP: { + FLAMINGO_DEBUG("Fixing up ADRP..."); auto [reg, dst] = get_second_immediate(inst); - WriteLdr(*target, reg, dst); - } else if ((*target & (ldr_lit_opc_mask & ~b_31)) == 0b00011100000000000000000000000000000) { - // This is an ldr literal, SIMD - // https://developer.arm.com/documentation/ddi0596/2021-12/SIMD-FP-Instructions/LDR--literal--SIMD-FP---Load-SIMD-FP-Register--PC-relative-literal-- - FLAMINGO_ABORT("LDR of the SIMD variant is not yet supported!"); - } else { - // This is an LDR that doesn't need to be fixed up - FLAMINGO_DEBUG("Fixing up standard LDR..."); - Write(*reinterpret_cast(inst.bytes)); - } - } break; - case ARM64_INS_LDRSW: { - // This is an ldrsw literal - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDRSW--literal---Load-Register-Signed-Word--literal-- - FLAMINGO_ABORT("LDRSW fixup not yet supported!"); - } break; - - // Handle pc-relative loads - case ARM64_INS_ADR: { - FLAMINGO_DEBUG("Fixing up ADR..."); - auto [reg, dst] = get_second_immediate(inst); - WriteAdr(reg, dst); - } break; - case ARM64_INS_ADRP: { - FLAMINGO_DEBUG("Fixing up ADRP..."); - auto [reg, dst] = get_second_immediate(inst); - WriteAdrp(reg, dst); - } break; - - // Otherwise, just write the instruction verbatim - default: - FLAMINGO_DEBUG("Fixing up UNKNOWN: {}...", inst.id); - Write(*reinterpret_cast(inst.bytes)); - break; + WriteAdrp(reg, dst); + } break; + + // Otherwise, just write the instruction verbatim + default: + FLAMINGO_DEBUG("Fixing up UNKNOWN: {}...", inst.id); + Write(*reinterpret_cast(inst.bytes)); + break; + } } + + // Free the disassembled instructions from before the fixups + cs_free(insns, countToFixup); +} + +void Trampoline::WriteHookFixups(uint32_t const* target) { + // Write the fixups for a standard hook. + // Ideally, this is hidden entirely and only the hook API can do this + WriteFixups<4>(target); } void Trampoline::WriteCallback(uint32_t const* target) { @@ -398,16 +540,6 @@ void Trampoline::WriteCallback(uint32_t const* target) { } } -void Trampoline::WriteFixups(uint32_t const* target, uint16_t countToFixup) { - original_instructions.reserve(countToFixup); - FLAMINGO_DEBUG("Fixing up: {} instructions!", countToFixup); - while (countToFixup-- > 0) { - WriteFixup(target++); - } - WriteCallback(target); - Finish(); -} - void Trampoline::Finish() { pageSizeRef -= alloc_size - (instruction_count * sizeof(uint32_t)); FLAMINGO_DEBUG("Completed trampoline allocation of: {} instructions!", instruction_count); From 504438eb7149293cd22fecba476ed8a05f09b137 Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Wed, 17 May 2023 00:39:11 -0700 Subject: [PATCH 007/134] Shrink line limit a bit --- .clang-format | 4 ++-- src/trampoline.cpp | 25 ++++++++++++++++--------- test/main.cpp | 19 +++++++++++++------ 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/.clang-format b/.clang-format index 1e59dce..170d9ba 100644 --- a/.clang-format +++ b/.clang-format @@ -2,7 +2,7 @@ BasedOnStyle: Google AllowShortBlocksOnASingleLine: false AllowShortFunctionsOnASingleLine: Empty AllowShortIfStatementsOnASingleLine: true -ColumnLimit: 200 +ColumnLimit: 140 CommentPragmas: NOLINT:.* DerivePointerAlignment: false IncludeBlocks: Preserve @@ -10,4 +10,4 @@ IndentWidth: 4 PointerAlignment: Left TabWidth: 4 UseTab: Never -Cpp11BracedListStyle: false \ No newline at end of file +Cpp11BracedListStyle: false diff --git a/src/trampoline.cpp b/src/trampoline.cpp index c9a44de..c61ad21 100644 --- a/src/trampoline.cpp +++ b/src/trampoline.cpp @@ -339,8 +339,9 @@ bool TryDeferBranch(Trampoline& self, uint16_t i, int64_t dst, int64_t target_st // Always emit the instruction with AN immediate. // For forward references, we need to defer. // This difference could be negative, but for those cases we will defer and overwrite. - auto fixup_difference = static_cast(get_untagged_pc(reinterpret_cast(&self.address[self.instruction_count])) - - get_untagged_pc(reinterpret_cast(&self.address[target_to_fixups[target_offset]]))); + auto fixup_difference = + static_cast(get_untagged_pc(reinterpret_cast(&self.address[self.instruction_count])) - + get_untagged_pc(reinterpret_cast(&self.address[target_to_fixups[target_offset]]))); self.Write((inst & ~imm_mask) | ((fixup_difference >> rshift) << lshift)); if (target_offset > i) { FLAMINGO_DEBUG("Deferring at: {} with target offset: {}", i, target_offset); @@ -412,15 +413,17 @@ void Trampoline::WriteFixups(uint32_t const* target) { auto const& inst = insns[i]; auto current_inst_ptr = &target[i]; FLAMINGO_DEBUG("Fixup for inst: 0x{:x} at {}: {} {}, id: {}", *current_inst_ptr, fmt::ptr(current_inst_ptr), - fmt::string_view(inst.mnemonic, sizeof(inst.mnemonic)), fmt::string_view(inst.op_str, sizeof(inst.op_str)), static_cast(inst.id)); + fmt::string_view(inst.mnemonic, sizeof(inst.mnemonic)), fmt::string_view(inst.op_str, sizeof(inst.op_str)), + static_cast(inst.id)); // For this incoming instruction, check to see if we have any forward references on this // If we do, for each, rewrite the target instruction with the adjusted value for (auto const& tag : branch_ref_map[i]) { // Current PC is get_untagged_pc(&address[instruction_count]) // The instruction we emit's PC is the map from target --> fixup // This difference is always positive, since we are jumping FORWARD - uint32_t difference = static_cast(get_untagged_pc(reinterpret_cast(&address[instruction_count])) - - get_untagged_pc(reinterpret_cast(&address[target_to_fixups[tag.target_index]]))); + uint32_t difference = + static_cast(get_untagged_pc(reinterpret_cast(&address[instruction_count])) - + get_untagged_pc(reinterpret_cast(&address[target_to_fixups[tag.target_index]]))); FLAMINGO_DEBUG("Performing deferred write at: {}, rewriting: {} with difference: {}", i, tag.target_index, difference); address[target_to_fixups[tag.target_index]] = (address[target_to_fixups[tag.target_index]] & ~tag.imm_mask) | (tag.imm_mask & ((difference >> tag.rshift) << tag.lshift)); @@ -432,7 +435,8 @@ void Trampoline::WriteFixups(uint32_t const* target) { case ARM64_INS_B: { FLAMINGO_DEBUG("Fixing up B..."); auto dst = get_branch_immediate(inst); - if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, branch_ref_map)) { + if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, + branch_ref_map)) { if (inst.detail->arm64.cc != ARM64_CC_INVALID) { WriteCondBranch(*this, *current_inst_ptr, dst); } else { @@ -443,7 +447,8 @@ void Trampoline::WriteFixups(uint32_t const* target) { case ARM64_INS_BL: { FLAMINGO_DEBUG("Fixing up BL..."); auto dst = get_branch_immediate(inst); - if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, branch_ref_map)) { + if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, + branch_ref_map)) { WriteBl(dst); } } break; @@ -453,7 +458,8 @@ void Trampoline::WriteFixups(uint32_t const* target) { case ARM64_INS_CBZ: { FLAMINGO_DEBUG("Fixing up CBNZ/CBZ..."); auto [reg, dst] = get_last_immediate(inst); - if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, branch_ref_map)) { + if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, + branch_ref_map)) { WriteCondBranch(*this, *current_inst_ptr, dst); } } break; @@ -461,7 +467,8 @@ void Trampoline::WriteFixups(uint32_t const* target) { case ARM64_INS_TBZ: { FLAMINGO_DEBUG("Fixing up TBNZ/TBZ..."); auto [reg, dst] = get_last_immediate(inst); - if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, branch_ref_map)) { + if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, + branch_ref_map)) { WriteCondBranch(*this, *current_inst_ptr, dst); } } break; diff --git a/test/main.cpp b/test/main.cpp index a884ccb..c261659 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -19,7 +19,10 @@ decltype(auto) test_near(uint32_t* target, uint32_t const* callback) { // Attempt to write a hook from target --> callback (just for testing purposes) std::size_t trampoline_size = hookSize; // Hook size is 5, but we only fixup 4 - trampoline.WriteFixups(target, 4); + trampoline.WriteHookFixups(target); + // Write the jump back to instruction 5 + trampoline.WriteCallback(&target[5]); + trampoline.Finish(); // Write actual hook to be a callback // We need to mark the location of target as writable (so we can write to it correctly) ::mprotect(target, hookSize, PROT_READ | PROT_WRITE | PROT_EXEC); @@ -33,7 +36,8 @@ void print_decode_loop(uint32_t* val, int n) { auto handle = flamingo::getHandle(); for (int i = 0; i < n; i++) { cs_insn* insns = nullptr; - auto count = cs_disasm(handle, reinterpret_cast(val), sizeof(uint32_t), static_cast(reinterpret_cast(val)), 1, &insns); + auto count = cs_disasm(handle, reinterpret_cast(val), sizeof(uint32_t), + static_cast(reinterpret_cast(val)), 1, &insns); if (count == 1) { printf("Addr: %p Value: 0x%x, %s %s\n", val, *val, insns[0].mnemonic, insns[0].op_str); } else { @@ -56,20 +60,23 @@ void perform_near_hook_test(uint8_t* to_hook) { void test_near_no_fixups() { puts("Testing near -- no fixups!"); - static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8, 0xf6, 0x57, 0x01, 0xa9, 0xf4, 0x4f, 0x02, 0xa9, 0xfd, 0x7b, 0x03, 0xa9, 0xfd, 0xc3, 0x00, 0x91, 0x48, 0x18, - 0x40, 0xf9, 0x16, 0xd4, 0x42, 0xa9, 0xf3, 0x03, 0x02, 0xaa, 0xf4, 0x03, 0x01, 0xaa, 0x17, 0x01, 0x40, 0xf9, 0xe8, 0xba, 0x44, 0x39 }; + static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8, 0xf6, 0x57, 0x01, 0xa9, 0xf4, 0x4f, 0x02, 0xa9, 0xfd, 0x7b, 0x03, + 0xa9, 0xfd, 0xc3, 0x00, 0x91, 0x48, 0x18, 0x40, 0xf9, 0x16, 0xd4, 0x42, 0xa9, 0xf3, 0x03, + 0x02, 0xaa, 0xf4, 0x03, 0x01, 0xaa, 0x17, 0x01, 0x40, 0xf9, 0xe8, 0xba, 0x44, 0x39 }; perform_near_hook_test(to_hook); } void test_near_bls_tbzs_within_hook() { puts("Testing near -- bls/tbzs"); - static uint8_t to_hook[]{ 0x68, 0x00, 0x00, 0x37, 0xe0, 0x03, 0x17, 0xaa, 0x52, 0x3e, 0xfd, 0x97, 0xe0, 0x03, 0x17, 0xaa, 0x64, 0x7b, 0xfe, 0x97, 0x00, 0x00, 0x00, 0x00 }; + static uint8_t to_hook[]{ 0x68, 0x00, 0x00, 0x37, 0xe0, 0x03, 0x17, 0xaa, 0x52, 0x3e, 0xfd, 0x97, + 0xe0, 0x03, 0x17, 0xaa, 0x64, 0x7b, 0xfe, 0x97, 0x00, 0x00, 0x00, 0x00 }; perform_near_hook_test(to_hook); } void test_ldr_ldrb_tbnz_bl() { puts("Testing near -- ldr/ldrb/tbnz/bl"); - static uint8_t to_hook[]{ 0x17, 0x01, 0x40, 0xf9, 0xe8, 0xba, 0x44, 0x39, 0x68, 0x00, 0x00, 0x37, 0xe0, 0x03, 0x17, 0xaa, 0x52, 0x3e, 0xfd, 0x97, 0x00, 0x00, 0x00, 0x00 }; + static uint8_t to_hook[]{ 0x17, 0x01, 0x40, 0xf9, 0xe8, 0xba, 0x44, 0x39, 0x68, 0x00, 0x00, 0x37, + 0xe0, 0x03, 0x17, 0xaa, 0x52, 0x3e, 0xfd, 0x97, 0x00, 0x00, 0x00, 0x00 }; perform_near_hook_test(to_hook); } From 8dbfaffa4c0f18e28fff368667ecee3de90440c9 Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Wed, 17 May 2023 19:06:07 -0700 Subject: [PATCH 008/134] Install callback to correct place, fmt/format.h include --- src/trampoline-allocator.cpp | 4 +++- test/main.cpp | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/trampoline-allocator.cpp b/src/trampoline-allocator.cpp index d825090..f74130d 100644 --- a/src/trampoline-allocator.cpp +++ b/src/trampoline-allocator.cpp @@ -4,6 +4,7 @@ #include #include "trampoline.hpp" #include "util.hpp" +#include namespace { struct PageType { @@ -30,7 +31,8 @@ Trampoline TrampolineAllocator::Allocate(std::size_t trampolineSize) { // If we have enough space in our page for this trampoline, squeeze it in! if (PageSize - page.used_size > trampolineSize) { // TODO: We have to be aligned 16 here - Trampoline to_ret(reinterpret_cast(reinterpret_cast(page.ptr) + page.used_size), trampolineSize, page.used_size); + Trampoline to_ret(reinterpret_cast(reinterpret_cast(page.ptr) + page.used_size), trampolineSize, + page.used_size); page.used_size += trampolineSize; page.trampoline_count++; // Log allocated trampoline here diff --git a/test/main.cpp b/test/main.cpp index c261659..c29d161 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -21,7 +21,7 @@ decltype(auto) test_near(uint32_t* target, uint32_t const* callback) { // Hook size is 5, but we only fixup 4 trampoline.WriteHookFixups(target); // Write the jump back to instruction 5 - trampoline.WriteCallback(&target[5]); + trampoline.WriteCallback(&target[4]); trampoline.Finish(); // Write actual hook to be a callback // We need to mark the location of target as writable (so we can write to it correctly) From e3bb62ddb61888ad7a8d661d97102c66ffa8af1d Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Tue, 23 May 2023 09:53:40 -0700 Subject: [PATCH 009/134] Add far tests, fix allocator error reporting, prepare for hook API --- shared/trampoline.hpp | 3 -- shared/util.hpp | 9 +++--- src/trampoline-allocator.cpp | 12 +++---- test/main.cpp | 61 ++++++++++++++++++++++++++++++++++-- 4 files changed, 70 insertions(+), 15 deletions(-) diff --git a/shared/trampoline.hpp b/shared/trampoline.hpp index cd99fa3..b4f3c9b 100644 --- a/shared/trampoline.hpp +++ b/shared/trampoline.hpp @@ -6,9 +6,6 @@ namespace flamingo { struct Trampoline { - // In bytes for a single fixup - constexpr static uint16_t MaximumFixupSize = 20; - uint32_t* address; std::size_t alloc_size; // size is number of instructions diff --git a/shared/util.hpp b/shared/util.hpp index 5c6e2b4..72f718b 100644 --- a/shared/util.hpp +++ b/shared/util.hpp @@ -14,10 +14,11 @@ #endif #define FLAMINGO_CRITICAL(...) Paper::Logger::fmtLog(__VA_ARGS__) -#define FLAMINGO_ABORT(...) \ - do { \ - SAFE_ABORT_MSG(__VA_ARGS__); \ - Paper::Logger::WaitForFlush(); \ +#define FLAMINGO_ABORT(...) \ + do { \ + FLAMINGO_CRITICAL(__VA_ARGS__); \ + Paper::Logger::WaitForFlush(); \ + SAFE_ABORT(); \ } while (0) #else diff --git a/src/trampoline-allocator.cpp b/src/trampoline-allocator.cpp index f74130d..e1e8138 100644 --- a/src/trampoline-allocator.cpp +++ b/src/trampoline-allocator.cpp @@ -1,10 +1,10 @@ #include "trampoline-allocator.hpp" +#include #include #include #include #include "trampoline.hpp" #include "util.hpp" -#include namespace { struct PageType { @@ -41,14 +41,14 @@ Trampoline TrampolineAllocator::Allocate(std::size_t trampolineSize) { } // No pages with enough space available. void* ptr; - if (!::posix_memalign(&ptr, PageSize, PageSize)) { + if (::posix_memalign(&ptr, PageSize, PageSize) != 0) { // Log error on memalign allocation! - FLAMINGO_ABORT("Failed to allocate trampoline page of size: {} for size: {}", PageSize, trampolineSize); + FLAMINGO_ABORT("Failed to allocate trampoline page of size: {} for size: {}. err: {}", PageSize, trampolineSize, strerror(errno)); } // Mark full page as rxw - if (!::mprotect(ptr, PageSize, PROT_READ | PROT_WRITE | PROT_EXEC)) { + if (::mprotect(ptr, PageSize, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) { // Log error on mprotect! - FLAMINGO_ABORT("Failed to mark allocated page at: {} as +rwx!", fmt::ptr(ptr)); + FLAMINGO_ABORT("Failed to mark allocated page at: {} as +rwx. err: {}", fmt::ptr(ptr), strerror(errno)); } auto& page = pages.emplace_back(ptr, trampolineSize); return { static_cast(ptr), trampolineSize, page.used_size }; @@ -68,7 +68,7 @@ void TrampolineAllocator::Free(Trampoline const& toFree) { if (p.trampoline_count == 0) { if (::mprotect(p.ptr, PageSize, PROT_READ) != 0) { // Log error on mprotect - FLAMINGO_ABORT("Failed to mark page at: {} as read only!", fmt::ptr(p.ptr)); + FLAMINGO_ABORT("Failed to mark page at: {} as read only. err: {}", fmt::ptr(p.ptr), strerror(errno)); } ::free(p.ptr); } diff --git a/test/main.cpp b/test/main.cpp index c29d161..27fe136 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -58,6 +58,38 @@ void perform_near_hook_test(uint8_t* to_hook) { print_decode_loop(reinterpret_cast(to_hook), 6); } +decltype(auto) test_far(uint32_t* target, uint32_t const* callback) { + constexpr size_t hookSize = 32; + constexpr size_t trampolineSize = 64; + auto trampoline = flamingo::TrampolineAllocator::Allocate(trampolineSize); + printf("TRAMPOLINE: %p\n", trampoline.address); + // Attempt to write a hook from target --> callback (just for testing purposes) + std::size_t trampoline_size = hookSize; + // Hook size is 5, but we only fixup 4 + trampoline.WriteHookFixups(target); + // Write the jump back to instruction 5 + trampoline.WriteCallback(&target[4]); + trampoline.Finish(); + // Write actual hook to be a callback + // We need to mark the location of target as writable (so we can write to it correctly) + ::mprotect(target, hookSize, PROT_READ | PROT_WRITE | PROT_EXEC); + ::flamingo::Trampoline targetHook(target, hookSize, trampoline_size); + targetHook.WriteCallback(callback); + targetHook.Finish(); + return trampoline; +} + +void perform_far_hook_test(uint8_t* to_hook) { + printf("TO HOOK: %p\n", to_hook); + print_decode_loop(reinterpret_cast(to_hook), 6); + puts("TEST FAR..."); + auto trampoline = test_far(reinterpret_cast(to_hook), (const uint32_t*)(0xDEADBEEFBAADF00DULL)); + // Use 20 here as a reasonable guesstimate + print_decode_loop(trampoline.address, 20); + puts("HOOKED:"); + print_decode_loop(reinterpret_cast(to_hook), 6); +} + void test_near_no_fixups() { puts("Testing near -- no fixups!"); static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8, 0xf6, 0x57, 0x01, 0xa9, 0xf4, 0x4f, 0x02, 0xa9, 0xfd, 0x7b, 0x03, @@ -66,6 +98,14 @@ void test_near_no_fixups() { perform_near_hook_test(to_hook); } +void test_far_no_fixups() { + puts("Testing far -- no fixups!"); + static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8, 0xf6, 0x57, 0x01, 0xa9, 0xf4, 0x4f, 0x02, 0xa9, 0xfd, 0x7b, 0x03, + 0xa9, 0xfd, 0xc3, 0x00, 0x91, 0x48, 0x18, 0x40, 0xf9, 0x16, 0xd4, 0x42, 0xa9, 0xf3, 0x03, + 0x02, 0xaa, 0xf4, 0x03, 0x01, 0xaa, 0x17, 0x01, 0x40, 0xf9, 0xe8, 0xba, 0x44, 0x39 }; + perform_far_hook_test(to_hook); +} + void test_near_bls_tbzs_within_hook() { puts("Testing near -- bls/tbzs"); static uint8_t to_hook[]{ 0x68, 0x00, 0x00, 0x37, 0xe0, 0x03, 0x17, 0xaa, 0x52, 0x3e, 0xfd, 0x97, @@ -73,17 +113,34 @@ void test_near_bls_tbzs_within_hook() { perform_near_hook_test(to_hook); } -void test_ldr_ldrb_tbnz_bl() { +void test_far_bls_tbzs_within_hook() { + puts("Testing far -- bls/tbzs"); + static uint8_t to_hook[]{ 0x68, 0x00, 0x00, 0x37, 0xe0, 0x03, 0x17, 0xaa, 0x52, 0x3e, 0xfd, 0x97, + 0xe0, 0x03, 0x17, 0xaa, 0x64, 0x7b, 0xfe, 0x97, 0x00, 0x00, 0x00, 0x00 }; + perform_far_hook_test(to_hook); +} + +void test_near_ldr_ldrb_tbnz_bl() { puts("Testing near -- ldr/ldrb/tbnz/bl"); static uint8_t to_hook[]{ 0x17, 0x01, 0x40, 0xf9, 0xe8, 0xba, 0x44, 0x39, 0x68, 0x00, 0x00, 0x37, 0xe0, 0x03, 0x17, 0xaa, 0x52, 0x3e, 0xfd, 0x97, 0x00, 0x00, 0x00, 0x00 }; perform_near_hook_test(to_hook); } +void test_far_ldr_ldrb_tbnz_bl() { + puts("Testing far -- ldr/ldrb/tbnz/bl"); + static uint8_t to_hook[]{ 0x17, 0x01, 0x40, 0xf9, 0xe8, 0xba, 0x44, 0x39, 0x68, 0x00, 0x00, 0x37, + 0xe0, 0x03, 0x17, 0xaa, 0x52, 0x3e, 0xfd, 0x97, 0x00, 0x00, 0x00, 0x00 }; + perform_far_hook_test(to_hook); +} + // TODO: Test a case where we have a loop in the first 4 instructions int main() { test_near_no_fixups(); test_near_bls_tbzs_within_hook(); - test_ldr_ldrb_tbnz_bl(); + test_near_ldr_ldrb_tbnz_bl(); + test_far_no_fixups(); + test_far_bls_tbzs_within_hook(); + test_far_ldr_ldrb_tbnz_bl(); } From 352907094feabd1f6d827d14addd44d8a8e47cc2 Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Sat, 14 Oct 2023 22:05:27 -0700 Subject: [PATCH 010/134] Syle fixes and improvements to flow --- .clang-tidy | 13 +++--- .gitignore | 1 + .vscode/extensions.json | 5 +++ qpm.json | 32 +++++-------- qpm.shared.json | 99 +++++------------------------------------ 5 files changed, 34 insertions(+), 116 deletions(-) create mode 100644 .vscode/extensions.json diff --git a/.clang-tidy b/.clang-tidy index 3a71610..0e3c870 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,11 +1,14 @@ -Checks: '*,-llvm*, +Checks: "*,-llvm*, -google-readability-namespace, -llvmlib, - -fuchsi, - -alter, + -fuchsia*, + -altera*, + -cppcoreguidelines-pro-type-union-access, -modernize-use-trailing-return, + -modernize-use-trailing-return-type, -readability-avoid-const-params-in, -cppcoreguidelines-macro-usage, -readability-identifier-length, - -google-readability-todo' -FormatStyle: file + -cppcoreguidelines-avoid-do-while, + -google-readability-todo" +FormatStyle: file diff --git a/.gitignore b/.gitignore index a8529c7..1e3d988 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ # VSCode config stuff !.vscode/c_cpp_properties.json !.vscode/tasks.json +.cache/ # NDK stuff out/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..7c35db3 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "llvm-vs-code-extensions.vscode-clangd" + ] +} \ No newline at end of file diff --git a/qpm.json b/qpm.json index b942b26..0550095 100644 --- a/qpm.json +++ b/qpm.json @@ -14,28 +14,16 @@ { "id": "capstone", "versionRange": "^0.1.0", - "additionalData": {} - }, - { - "id": "paper", - "versionRange": "^1.0.0", - "additionalData": {} - }, - { - "id": "modloader", - "versionRange": "^1.0.2", - "additionalData": {} - }, - { - "id": "beatsaber-hook", - "versionRange": "^3.8.0", - "additionalData": {} - }, - { - "id": "paper", - "versionRange": "^1.2.9", - "additionalData": {} + "additionalData": { + "private": true + } } ], - "workspace": null + "workspace": { + "scripts": { + "build": [ + "pwsh ./build.ps1" + ] + } + } } \ No newline at end of file diff --git a/qpm.shared.json b/qpm.shared.json index b7f5f2f..2d201a6 100644 --- a/qpm.shared.json +++ b/qpm.shared.json @@ -15,99 +15,20 @@ { "id": "capstone", "versionRange": "^0.1.0", - "additionalData": {} - }, - { - "id": "paper", - "versionRange": "^1.0.0", - "additionalData": {} - }, - { - "id": "modloader", - "versionRange": "^1.0.2", - "additionalData": {} - }, - { - "id": "beatsaber-hook", - "versionRange": "^3.8.0", - "additionalData": {} - }, - { - "id": "paper", - "versionRange": "^1.2.9", - "additionalData": {} + "additionalData": { + "private": true + } } ], - "workspace": null + "workspace": { + "scripts": { + "build": [ + "pwsh ./build.ps1" + ] + } + } }, "restoredDependencies": [ - { - "dependency": { - "id": "paper", - "versionRange": "=1.2.9", - "additionalData": { - "soLink": "https://github.com/Fernthedev/paperlog/releases/download/v1.2.9/libpaperlog.so", - "debugSoLink": "https://github.com/Fernthedev/paperlog/releases/download/v1.2.9/debug_libpaperlog.so", - "overrideSoName": "libpaperlog.so", - "modLink": "https://github.com/Fernthedev/paperlog/releases/download/v1.2.9/paperlog.qmod", - "branchName": "version-v1.2.9" - } - }, - "version": "1.2.9" - }, - { - "dependency": { - "id": "modloader", - "versionRange": "=1.2.3", - "additionalData": { - "soLink": "https://github.com/sc2ad/QuestLoader/releases/download/v1.2.3/libmodloader64.so", - "overrideSoName": "libmodloader.so", - "branchName": "version-v1.2.3" - } - }, - "version": "1.2.3" - }, - { - "dependency": { - "id": "libil2cpp", - "versionRange": "=0.2.3", - "additionalData": { - "headersOnly": true - } - }, - "version": "0.2.3" - }, - { - "dependency": { - "id": "beatsaber-hook", - "versionRange": "=3.14.0", - "additionalData": { - "soLink": "https://github.com/sc2ad/beatsaber-hook/releases/download/v3.14.0/libbeatsaber-hook_3_14_0.so", - "debugSoLink": "https://github.com/sc2ad/beatsaber-hook/releases/download/v3.14.0/debug_libbeatsaber-hook_3_14_0.so", - "branchName": "version-v3.14.0" - } - }, - "version": "3.14.0" - }, - { - "dependency": { - "id": "fmt", - "versionRange": "=9.0.0", - "additionalData": { - "headersOnly": true, - "branchName": "version-v9.0.0", - "compileOptions": { - "systemIncludes": [ - "fmt/include/" - ], - "cppFlags": [ - "-DFMT_HEADER_ONLY" - ] - } - } - }, - "version": "9.0.0" - }, { "dependency": { "id": "capstone", From 060cdea942f88b7c845d284cb99b8cd6e624908c Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Sat, 14 Oct 2023 22:07:09 -0700 Subject: [PATCH 011/134] Swap to fmt logs, ensure compilation of trampoline --- CMakeLists.txt | 63 +++++++++++++++++---------------- shared/trampoline-allocator.hpp | 2 +- shared/trampoline.hpp | 9 +++-- shared/util.hpp | 43 +++++++++++++++++++++- src/trampoline-allocator.cpp | 13 +++---- src/trampoline.cpp | 44 ++++++++++++----------- test/main.cpp | 4 +-- 7 files changed, 114 insertions(+), 64 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a4b6ea..e9bc286 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,22 +4,22 @@ include(qpm_defines.cmake) cmake_minimum_required(VERSION 3.22) project(${COMPILE_ID}) # Get GSL -include(FetchContent) +# include(FetchContent) -FetchContent_Declare(GSL - GIT_REPOSITORY "https://github.com/microsoft/GSL" - GIT_TAG "v4.0.0" -) +# FetchContent_Declare(GSL +# GIT_REPOSITORY "https://github.com/microsoft/GSL" +# GIT_TAG "v4.0.0" +# ) -FetchContent_MakeAvailable(GSL) +# FetchContent_MakeAvailable(GSL) -FetchContent_Declare( - googletest - URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip -) -# For Windows: Prevent overriding the parent project's compiler/linker settings -set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) -FetchContent_MakeAvailable(googletest) +# FetchContent_Declare( +# googletest +# URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip +# ) +# # For Windows: Prevent overriding the parent project's compiler/linker settings +# set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +# FetchContent_MakeAvailable(googletest) # c++ standard set(CMAKE_CXX_STANDARD 20) @@ -30,10 +30,12 @@ set(SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src) set(INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/include) # compile options used -add_compile_options(-frtti -flto -fPIE -fPIC -fexceptions) +add_compile_options(-fno-rtti -flto -fPIE -fPIC -fno-exceptions -fcolor-diagnostics) add_compile_definitions(MOD_VERSION="${MOD_VERSION}") add_compile_definitions(MOD_ID="${MOD_ID}") -# add_compile_options(-Wall -Wextra -Werror -Wpedantic) +# TODO: For now, this is the only safe way we can build a binary +add_compile_definitions(FLAMINGO_HEADER_ONLY) +add_compile_options(-Wall -Wextra -Werror -Wpedantic) # compile definitions used set(CMAKE_EXPORT_COMPILE_COMMANDS ON) @@ -48,9 +50,9 @@ add_library( ) # recursively get all src files -RECURSE_FILES(cpp_file_list_utils ${SOURCE_DIR}/*.cpp) +# RECURSE_FILES(cpp_file_list_utils ${SOURCE_DIR}/*.cpp) # RECURSE_FILES(c_file_list_utils ${SOURCE_DIR}/*.c) -target_sources(${COMPILE_ID} PRIVATE ${cpp_file_list_utils}) +target_sources(${COMPILE_ID} PRIVATE ${SOURCE_DIR}/trampoline.cpp ${SOURCE_DIR}/trampoline-allocator.cpp) # target_sources(${COMPILE_ID} PRIVATE ${c_file_list_utils}) # if (DEFINED TEST_BUILD) @@ -68,8 +70,7 @@ target_include_directories(${COMPILE_ID} PRIVATE ${SOURCE_DIR}) target_include_directories(${COMPILE_ID} PRIVATE ${INCLUDE_DIR}) # add shared dir as include dir target_include_directories(${COMPILE_ID} PUBLIC ${SHARED_DIR}) - -target_link_libraries(${COMPILE_ID} PRIVATE -llog GSL) +target_link_libraries(${COMPILE_ID} PRIVATE -llog) include(extern.cmake) MESSAGE(STATUS "extern added!") @@ -92,15 +93,15 @@ add_custom_command(TARGET ${COMPILE_ID} POST_BUILD enable_testing() add_compile_options(-Wall -Wextra -Werror -Wpedantic) -add_executable( - test - test/main.cpp -) -target_include_directories(test PUBLIC ${SHARED_DIR}) -target_link_libraries( - test - GTest::gtest_main -) - -include(GoogleTest) -gtest_discover_tests(test) +# add_executable( +# test +# test/main.cpp +# ) +# target_include_directories(test PUBLIC ${SHARED_DIR}) +# target_link_libraries( +# test +# GTest::gtest_main +# ) + +# include(GoogleTest) +# gtest_discover_tests(test) diff --git a/shared/trampoline-allocator.hpp b/shared/trampoline-allocator.hpp index 52a4336..055f842 100644 --- a/shared/trampoline-allocator.hpp +++ b/shared/trampoline-allocator.hpp @@ -2,7 +2,7 @@ #include #include -#include "capstone/capstone.h" +#include "capstone/shared/capstone/capstone.h" namespace flamingo { diff --git a/shared/trampoline.hpp b/shared/trampoline.hpp index b4f3c9b..9ac1b50 100644 --- a/shared/trampoline.hpp +++ b/shared/trampoline.hpp @@ -1,19 +1,22 @@ #pragma once #include +#include #include namespace flamingo { struct Trampoline { - uint32_t* address; - std::size_t alloc_size; + std::span address; // size is number of instructions + // TODO: Make a transparent type here to avoid accidental comparison with other types of size + std::size_t num_insts; std::size_t instruction_count = 0; std::size_t& pageSizeRef; std::vector original_instructions{}; - Trampoline(uint32_t* ptr, std::size_t allocationSize, std::size_t& sz) : address(ptr), alloc_size(allocationSize), pageSizeRef(sz) {} + Trampoline(uint32_t* ptr, std::size_t num_insts, std::size_t& sz) + : address(ptr, &ptr[num_insts]), num_insts(num_insts), pageSizeRef(sz) {} void Write(uint32_t instruction); /// @brief Writes the specified pointer as a target specific immediate to the data block. diff --git a/shared/util.hpp b/shared/util.hpp index 72f718b..e9f1031 100644 --- a/shared/util.hpp +++ b/shared/util.hpp @@ -1,4 +1,44 @@ #pragma once + +#define FLAMINGO_ID "flamingo" +#define FLAMINGO_VERSION "0.1.0" + +#ifdef FLAMINGO_HEADER_ONLY + +#ifdef ANDROID +#include +#include +#include +#include + +#define LOGA(lvl, ...) \ + do { \ + std::string __ss = fmt::format(__VA_ARGS__); \ + __android_log_print(lvl, FLAMINGO_ID "|v" FLAMINGO_VERSION, "%s", __ss.c_str()); \ + } while (0) + +#ifndef NO_DEBUG_LOGS +#define FLAMINGO_DEBUG(...) LOGA(ANDROID_LOG_DEBUG, __VA_ARGS__) +#define FLAMINGO_ASSERT(...) \ + do { \ + if (!(__VA_ARGS__)) FLAMINGO_ABORT("Failed condition: " #__VA_ARGS__); \ + } while (0) +#else +#define FLAMINGO_DEBUG(...) +#define FLAMINGO_ASSERT(...) __builtin_assume(__VA_ARGS__) +#endif + +#define FLAMINGO_CRITICAL(...) LOGA(ANDROID_LOG_FATAL, __VA_ARGS__) +#define FLAMINGO_ABORT(...) \ + do { \ + FLAMINGO_CRITICAL(__VA_ARGS__); \ + std::abort(); \ + } while (0) +#else // ANDROID +#error "Need logging definitions here, for non-ANDROID, header only support!" +#endif + +#else // FLAMINGO_HEADER_ONLY #include #ifdef ANDROID @@ -22,7 +62,6 @@ } while (0) #else -#include #include #include @@ -38,3 +77,5 @@ puts(""); \ std::abort() #endif + +#endif \ No newline at end of file diff --git a/src/trampoline-allocator.cpp b/src/trampoline-allocator.cpp index e1e8138..37d7b8d 100644 --- a/src/trampoline-allocator.cpp +++ b/src/trampoline-allocator.cpp @@ -1,5 +1,4 @@ #include "trampoline-allocator.hpp" -#include #include #include #include @@ -43,12 +42,13 @@ Trampoline TrampolineAllocator::Allocate(std::size_t trampolineSize) { void* ptr; if (::posix_memalign(&ptr, PageSize, PageSize) != 0) { // Log error on memalign allocation! - FLAMINGO_ABORT("Failed to allocate trampoline page of size: {} for size: {}. err: {}", PageSize, trampolineSize, strerror(errno)); + FLAMINGO_ABORT("Failed to allocate trampoline page of size: {} for size: {}. err: {}", PageSize, trampolineSize, + std::strerror(errno)); } // Mark full page as rxw if (::mprotect(ptr, PageSize, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) { // Log error on mprotect! - FLAMINGO_ABORT("Failed to mark allocated page at: {} as +rwx. err: {}", fmt::ptr(ptr), strerror(errno)); + FLAMINGO_ABORT("Failed to mark allocated page at: {} as +rwx. err: {}", fmt::ptr(ptr), std::strerror(errno)); } auto& page = pages.emplace_back(ptr, trampolineSize); return { static_cast(ptr), trampolineSize, page.used_size }; @@ -61,14 +61,14 @@ void TrampolineAllocator::Free(Trampoline const& toFree) { // 2. deallocate the page // Find page we are allocated on - auto page_addr = reinterpret_cast(reinterpret_cast(toFree.address) & ~(PageSize - 1)); + auto page_addr = reinterpret_cast(reinterpret_cast(toFree.address.data()) & ~(PageSize - 1)); for (auto& p : pages) { if (p.ptr == page_addr) { p.trampoline_count--; if (p.trampoline_count == 0) { if (::mprotect(p.ptr, PageSize, PROT_READ) != 0) { // Log error on mprotect - FLAMINGO_ABORT("Failed to mark page at: {} as read only. err: {}", fmt::ptr(p.ptr), strerror(errno)); + FLAMINGO_ABORT("Failed to mark page at: {} as read only. err: {}", fmt::ptr(p.ptr), std::strerror(errno)); } ::free(p.ptr); } @@ -76,7 +76,8 @@ void TrampolineAllocator::Free(Trampoline const& toFree) { } } // If we get here, we couldn't free the provided Trampoline! - FLAMINGO_ABORT("Failed to free trampoline at: {}, no matching page with page addr: {}!", fmt::ptr(toFree.address), fmt::ptr(page_addr)); + FLAMINGO_ABORT("Failed to free trampoline at: {}, no matching page with page addr: {}!", fmt::ptr(toFree.address.data()), + fmt::ptr(page_addr)); } } // namespace flamingo diff --git a/src/trampoline.cpp b/src/trampoline.cpp index c61ad21..3109fad 100644 --- a/src/trampoline.cpp +++ b/src/trampoline.cpp @@ -2,9 +2,8 @@ #include #include #include -#include "capstone/capstone.h" -#include "capstone/platform.h" -#include "fmt/format.h" +#include "capstone/shared/capstone/capstone.h" +#include "capstone/shared/capstone/platform.h" #include "util.hpp" namespace flamingo { @@ -25,21 +24,24 @@ template struct BranchImmTypeTrait; template -requires(type == ARM64_INS_B || type == ARM64_INS_BL) struct BranchImmTypeTrait { + requires(type == ARM64_INS_B || type == ARM64_INS_BL) +struct BranchImmTypeTrait { constexpr static uint32_t imm_mask = 0b00000011111111111111111111111111U; constexpr static uint32_t lshift = 0; constexpr static uint32_t rshift = 2; }; template -requires(type == ARM64_INS_CBZ || type == ARM64_INS_CBNZ) struct BranchImmTypeTrait { + requires(type == ARM64_INS_CBZ || type == ARM64_INS_CBNZ) +struct BranchImmTypeTrait { constexpr static uint32_t imm_mask = 0b00000000111111111111111111100000; constexpr static uint32_t lshift = 5; constexpr static uint32_t rshift = 2; }; template -requires(type == ARM64_INS_TBZ || type == ARM64_INS_TBNZ) struct BranchImmTypeTrait { + requires(type == ARM64_INS_TBZ || type == ARM64_INS_TBNZ) +struct BranchImmTypeTrait { constexpr static uint32_t imm_mask = 0b00000000000001111111111111100000; constexpr static uint32_t lshift = 5; constexpr static uint32_t rshift = 2; @@ -48,17 +50,17 @@ requires(type == ARM64_INS_TBZ || type == ARM64_INS_TBNZ) struct BranchImmTypeTr /// @brief Helper function that returns an encoded b for a particular offset consteval uint32_t get_b(int offset) { constexpr uint32_t b_opcode = 0b00010100000000000000000000000000U; - return (b_opcode | (offset >> 2)); + return (b_opcode | (static_cast(offset) >> 2U)); } constexpr int64_t get_untagged_pc(uint64_t pc) { // Upper byte is tagged for PC addresses on android 11+ - constexpr uint64_t mask = ~(0xFFULL << 56); + constexpr uint64_t mask = ~(0xFFULL << (64U - 8U)); return static_cast(static_cast(pc) & mask); } void Trampoline::Write(uint32_t instruction) { - FLAMINGO_ASSERT((instruction_count + 1) * sizeof(uint32_t) <= alloc_size); + FLAMINGO_ASSERT(instruction_count + 1 <= num_insts); // Log what we are writing (and also our state) address[instruction_count] = instruction; instruction_count++; @@ -67,15 +69,15 @@ void Trampoline::Write(uint32_t instruction) { void Trampoline::WriteData(void const* ptr) { // TODO: Write this to a different buffer and return a pointer to it // This would allow the control flow for a hook to be much cleaner and the data section to be well defined - FLAMINGO_ASSERT(instruction_count * sizeof(uint32_t) + sizeof(void*) <= alloc_size); + FLAMINGO_ASSERT(instruction_count + sizeof(void*) / sizeof(uint32_t) <= num_insts); // Log what we are writing (and also our state) - *reinterpret_cast(&address[instruction_count]) = const_cast(ptr); + *reinterpret_cast(&address[instruction_count]) = ptr; instruction_count += sizeof(void*) / sizeof(uint32_t); } void Trampoline::WriteData(void const* ptr, uint32_t size) { // TODO: Write this to a different buffer and return a pointer to it - FLAMINGO_ASSERT((size + instruction_count) * sizeof(uint32_t) <= alloc_size); + FLAMINGO_ASSERT((size + instruction_count) <= num_insts); FLAMINGO_DEBUG("Writing data from: {} of size: {}", ptr, size * sizeof(uint32_t)); std::memcpy(&address[instruction_count], ptr, size * sizeof(uint32_t)); instruction_count += size; @@ -120,13 +122,13 @@ void Trampoline::WriteAdr(uint8_t reg, int64_t imm) { constexpr uint32_t reg_mask = 0b11111; int64_t pc = get_untagged_pc(reinterpret_cast(&address[instruction_count])); int64_t delta = imm - pc; - if (std::llabs(delta) >= (adr_maximum_imm >> 1)) { + if (std::llabs(delta) >= (adr_maximum_imm >> 1U)) { // Too far to emit just an adr. // LDR (used register), #0x8 constexpr uint32_t ldr_mask = 0b01011000000000000000000000000000U; // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- // imm is encoded as << 2, LSB just to the right of reg - constexpr uint32_t ldr_imm = (8U >> 2U) << 5; + constexpr uint32_t ldr_imm = (8U >> 2U) << 5U; Write(ldr_mask | ldr_imm | (reg_mask & reg)); // B #0xC constexpr uint32_t b_0xc = 0x14000003U; @@ -139,9 +141,9 @@ void Trampoline::WriteAdr(uint8_t reg, int64_t imm) { // Note that delta should be within +-1 MB constexpr uint32_t adr_opcode = 0b00010000000000000000000000000000; // Get immlo - uint32_t imm_lo = ((static_cast(delta) & 3) << 29); + uint32_t imm_lo = ((static_cast(delta) & 3U) << 29); // Get immhi - uint32_t imm_hi = (static_cast(delta) >> 2) << 5; + uint32_t imm_hi = (static_cast(delta) >> 2U) << 5; Write(adr_opcode | imm_lo | imm_hi | (reg_mask & reg)); } } @@ -150,7 +152,7 @@ void Trampoline::WriteAdrp(uint8_t reg, int64_t imm) { // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/ADR--Form-PC-relative-address-?lang=en // constexpr uint32_t adr_maximum_imm = 0b00000000000111111111111111111111U; constexpr uint32_t reg_mask = 0b11111; - constexpr uint32_t pc_imm_mask = ~0b111111111111; + constexpr uint32_t pc_imm_mask = ~0b111111111111U; constexpr int64_t adrp_maximum_imm = 0xFFFFF000U; int64_t pc = get_untagged_pc(reinterpret_cast(&address[instruction_count])); int64_t delta = (pc & pc_imm_mask) - imm; @@ -336,6 +338,8 @@ bool TryDeferBranch(Trampoline& self, uint16_t i, int64_t dst, int64_t target_st if (dst < target_end && dst >= target_start) { FLAMINGO_DEBUG("Potentially deferring branch at: {} because it is within: {} and {}", dst, target_start, target_end); auto target_offset = (dst - target_start) / sizeof(uint32_t); + FLAMINGO_ASSERT(target_offset < target_to_fixups.size()); + FLAMINGO_ASSERT(target_offset < branch_ref_map.size()); // Always emit the instruction with AN immediate. // For forward references, we need to defer. // This difference could be negative, but for those cases we will defer and overwrite. @@ -421,7 +425,7 @@ void Trampoline::WriteFixups(uint32_t const* target) { // Current PC is get_untagged_pc(&address[instruction_count]) // The instruction we emit's PC is the map from target --> fixup // This difference is always positive, since we are jumping FORWARD - uint32_t difference = + auto difference = static_cast(get_untagged_pc(reinterpret_cast(&address[instruction_count])) - get_untagged_pc(reinterpret_cast(&address[target_to_fixups[tag.target_index]]))); FLAMINGO_DEBUG("Performing deferred write at: {}, rewriting: {} with difference: {}", i, tag.target_index, difference); @@ -484,7 +488,7 @@ void Trampoline::WriteFixups(uint32_t const* target) { // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- auto [reg, dst] = get_second_immediate(inst); WriteLdr(*current_inst_ptr, reg, dst); - } else if ((*current_inst_ptr & (ldr_lit_opc_mask & ~b_31)) == 0b00011100000000000000000000000000000) { + } else if ((*current_inst_ptr & (ldr_lit_opc_mask & ~b_31)) == 0b00011100000000000000000000000000) { // This is an ldr literal, SIMD // https://developer.arm.com/documentation/ddi0596/2021-12/SIMD-FP-Instructions/LDR--literal--SIMD-FP---Load-SIMD-FP-Register--PC-relative-literal-- FLAMINGO_ABORT("LDR of the SIMD variant is not yet supported!"); @@ -548,7 +552,7 @@ void Trampoline::WriteCallback(uint32_t const* target) { } void Trampoline::Finish() { - pageSizeRef -= alloc_size - (instruction_count * sizeof(uint32_t)); + pageSizeRef -= (num_insts - instruction_count) * sizeof(uint32_t); FLAMINGO_DEBUG("Completed trampoline allocation of: {} instructions!", instruction_count); } diff --git a/test/main.cpp b/test/main.cpp index 27fe136..919f356 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -62,7 +62,7 @@ decltype(auto) test_far(uint32_t* target, uint32_t const* callback) { constexpr size_t hookSize = 32; constexpr size_t trampolineSize = 64; auto trampoline = flamingo::TrampolineAllocator::Allocate(trampolineSize); - printf("TRAMPOLINE: %p\n", trampoline.address); + printf("TRAMPOLINE: %p\n", trampoline.address.data()); // Attempt to write a hook from target --> callback (just for testing purposes) std::size_t trampoline_size = hookSize; // Hook size is 5, but we only fixup 4 @@ -85,7 +85,7 @@ void perform_far_hook_test(uint8_t* to_hook) { puts("TEST FAR..."); auto trampoline = test_far(reinterpret_cast(to_hook), (const uint32_t*)(0xDEADBEEFBAADF00DULL)); // Use 20 here as a reasonable guesstimate - print_decode_loop(trampoline.address, 20); + print_decode_loop(trampoline.address.data(), 20); puts("HOOKED:"); print_decode_loop(reinterpret_cast(to_hook), 6); } From 0d1b7528919d1f84e76d5b9a059f8051e958526a Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Sat, 14 Oct 2023 22:13:20 -0700 Subject: [PATCH 012/134] Update action for building, ensure creation works --- .github/workflows/build-ndk.yml | 170 +++++++++++++----------------- .github/workflows/clang-check.yml | 20 ---- .gitignore | 1 + createqmod.ps1 | 46 ++++++++ 4 files changed, 119 insertions(+), 118 deletions(-) delete mode 100644 .github/workflows/clang-check.yml create mode 100644 createqmod.ps1 diff --git a/.github/workflows/build-ndk.yml b/.github/workflows/build-ndk.yml index 14f5ddf..c3f431c 100644 --- a/.github/workflows/build-ndk.yml +++ b/.github/workflows/build-ndk.yml @@ -4,110 +4,84 @@ on: workflow_dispatch: push: branches-ignore: - - 'version-*' + - "version-*" pull_request: branches-ignore: - - 'version-*' + - "version-*" env: - module_id: flamingo + module_id: flamingo + cache-name: flamingo_cache + qmodName: flamingo jobs: build: runs-on: ubuntu-latest - + steps: - - uses: actions/checkout@v2 - name: Checkout - with: - submodules: true - lfs: true - - - uses: seanmiddleditch/gha-setup-ninja@v3 - - - name: Create ndkpath.txt - run: | - echo "$ANDROID_NDK_LATEST_HOME" > ${GITHUB_WORKSPACE}/ndkpath.txt - cat ${GITHUB_WORKSPACE}/ndkpath.txt - - - name: Get QPM - if: steps.cache-qpm.outputs.cache-hit != 'true' - uses: dawidd6/action-download-artifact@v2 - with: - github_token: ${{secrets.GITHUB_TOKEN}} - workflow: cargo-build.yml - name: linux-qpm-rust - path: QPM - repo: RedBrumbler/QuestPackageManager-Rust - - - name: QPM Collapse - run: | - chmod +x ./QPM/qpm-rust - ./QPM/qpm-rust collapse - - - name: QPM Dependencies Cache - id: cache-qpm-deps - uses: actions/cache@v2 - env: - cache-name: cache-qpm-deps - with: - path: /home/runner/.local/share/QPM-Rust/cache - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('qpm.json') }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- - - - - name: QPM Restore - run: | - ./QPM/qpm-rust restore - - - name: List Post Restore - run: | - echo includes: - ls -la ${GITHUB_WORKSPACE}/extern/includes - echo libs: - ls -la ${GITHUB_WORKSPACE}/extern/libs - echo cache: - ls -la $HOME/.local/share/QPM-Rust/cache - - - name: Prepare Clang Tidy Suggestions - if: ${{ github.event_name == 'pull_request' }} - run: cmake -G "Ninja" -DCMAKE_BUILD_TYPE="RelWithDebInfo" -DTEST_BUILD=1 -B build - - - name: Clang Tidy Suggestions - if: ${{ github.event_name == 'pull_request' }} - uses: ZedThree/clang-tidy-review@v0.7.0 - id: review - # If there are any comments, don't build - - name: Clang Tidy Exit - if: ${{ github.event_name == 'pull_request' }} && steps.review.outputs.total_comments > 0 - run: exit 1 - - - name: Build - run: | - cd ${GITHUB_WORKSPACE} - pwsh -Command ./build.ps1 - - - name: Get Library Name - id: libname - run: | - cd ./build/ - pattern="lib${module_id}*.so" - files=( $pattern ) - echo ::set-output name=NAME::"${files[0]}" - - - name: Upload non-debug artifact - uses: actions/upload-artifact@v2 - with: - name: ${{ steps.libname.outputs.NAME }} - path: ./build/${{ steps.libname.outputs.NAME }} - if-no-files-found: error - - - name: Upload debug artifact - uses: actions/upload-artifact@v2 - with: - name: debug_${{ steps.libname.outputs.NAME }} - path: ./build/debug_${{ steps.libname.outputs.NAME }} - if-no-files-found: error + - uses: actions/checkout@v2 + name: Checkout + with: + submodules: true + lfs: true + + - uses: seanmiddleditch/gha-setup-ninja@v3 + + - name: Create ndkpath.txt + run: | + echo "$ANDROID_NDK_LATEST_HOME" > ${GITHUB_WORKSPACE}/ndkpath.txt + cat ${GITHUB_WORKSPACE}/ndkpath.txt + + - name: QPM Rust Action + uses: Fernthedev/qpm-rust-action@main + with: + #required + workflow_token: ${{secrets.GITHUB_TOKEN}} + + restore: true # will run restore on download + cache: true #will cache dependencies + + # Name of qmod in release asset. Assumes exists, same as prior + qpm_qmod: ${{env.qmodName}}.qmod + + - name: QPM Collapse + run: qpm-rust collapse + + - name: Build + run: | + cd ${GITHUB_WORKSPACE} + qpm-rust restore + qpm-rust s build + + - name: Create Qmod + run: | + pwsh -Command ./createqmod.ps1 ${{env.qmodName}} + + - name: Get Library Name + id: libname + run: | + cd ./build/ + pattern="lib${module_id}*.so" + files=( $pattern ) + echo ::set-output name=NAME::"${files[0]}" + + - name: Upload non-debug artifact + uses: actions/upload-artifact@v2 + with: + name: ${{ steps.libname.outputs.NAME }} + path: ./build/${{ steps.libname.outputs.NAME }} + if-no-files-found: error + + - name: Upload debug artifact + uses: actions/upload-artifact@v2 + with: + name: debug_${{ steps.libname.outputs.NAME }} + path: ./build/debug_${{ steps.libname.outputs.NAME }} + if-no-files-found: error + + - name: Upload qmod artifact + uses: actions/upload-artifact@v2 + with: + name: ${{env.qmodName}}.qmod + path: ./${{ env.qmodName }}.qmod + if-no-files-found: error diff --git a/.github/workflows/clang-check.yml b/.github/workflows/clang-check.yml deleted file mode 100644 index 1792311..0000000 --- a/.github/workflows/clang-check.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: clang-format Check -on: - workflow_dispatch: - push: - branches-ignore: - - 'version-*' - pull_request: - branches-ignore: - - 'version-*' -jobs: - formatting-check: - name: Formatting Check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Run clang-format style check for C/C++/Protobuf programs. - uses: jidicula/clang-format-action@v4.5.0 - with: - clang-format-version: '13' - check-path: 'src' diff --git a/.gitignore b/.gitignore index 1e3d988..1e4329e 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ out/ [Oo]bjs/ ndkpath.txt *.zip +*.txt # Log files/backups *.log diff --git a/createqmod.ps1 b/createqmod.ps1 new file mode 100644 index 0000000..4153c57 --- /dev/null +++ b/createqmod.ps1 @@ -0,0 +1,46 @@ +Param( + [String]$qmodname = "", + [Parameter(Mandatory = $false)] + [Switch]$clean +) + +if ($qmodName -eq "") { + echo "Give a proper qmod name and try again" + exit +} +$mod = "./mod.json" +$modJson = Get-Content $mod -Raw | ConvertFrom-Json + +$filelist = @($mod) + +$cover = "./" + $modJson.coverImage +if ((-not ($cover -eq "./")) -and (Test-Path $cover)) { + $filelist += , $cover +} + +foreach ($mod in $modJson.modFiles) { + $path = "./build/" + $mod + if (-not (Test-Path $path)) { + $path = "./extern/libs/" + $mod + } + $filelist += $path +} + +foreach ($lib in $modJson.libraryFiles) { + $path = "./extern/libs/" + $lib + if (-not (Test-Path $path)) { + $path = "./build/" + $lib + } + $filelist += $path +} + +$zip = $qmodName + ".zip" +$qmod = $qmodName + ".qmod" + +if ((-not ($clean.IsPresent)) -and (Test-Path $qmod)) { + echo "Making Clean Qmod" + Move-Item $qmod $zip -Force +} + +Compress-Archive -Path $filelist -DestinationPath $zip -Update +Move-Item $zip $qmod -Force \ No newline at end of file From dcb7bddb0539af7348c245e92a86f4c634970688 Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Sat, 14 Oct 2023 22:18:20 -0700 Subject: [PATCH 013/134] Add fmt dependency --- qpm.json | 7 +++++++ qpm.shared.json | 26 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/qpm.json b/qpm.json index 0550095..a9f5881 100644 --- a/qpm.json +++ b/qpm.json @@ -17,6 +17,13 @@ "additionalData": { "private": true } + }, + { + "id": "fmt", + "versionRange": "^10.0.0", + "additionalData": { + "private": true + } } ], "workspace": { diff --git a/qpm.shared.json b/qpm.shared.json index 2d201a6..34f89ed 100644 --- a/qpm.shared.json +++ b/qpm.shared.json @@ -18,6 +18,13 @@ "additionalData": { "private": true } + }, + { + "id": "fmt", + "versionRange": "^10.0.0", + "additionalData": { + "private": true + } } ], "workspace": { @@ -29,6 +36,25 @@ } }, "restoredDependencies": [ + { + "dependency": { + "id": "fmt", + "versionRange": "=10.0.0", + "additionalData": { + "headersOnly": true, + "branchName": "version/v10_0_0", + "compileOptions": { + "systemIncludes": [ + "fmt/include/" + ], + "cppFlags": [ + "-DFMT_HEADER_ONLY" + ] + } + } + }, + "version": "10.0.0" + }, { "dependency": { "id": "capstone", From d9cb24e1674d657a841b6c3a24807d54c3004c57 Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Sat, 14 Oct 2023 22:25:48 -0700 Subject: [PATCH 014/134] Improve build flow to build qmod --- build.ps1 | 18 +++++++++--------- mod.json | 15 +++++++++++++++ mod.template.json | 13 +++++++++++++ 3 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 mod.json create mode 100644 mod.template.json diff --git a/build.ps1 b/build.ps1 index 3b1fc72..4559903 100644 --- a/build.ps1 +++ b/build.ps1 @@ -26,14 +26,14 @@ if (-not ($ExitCode -eq 0)) { exit $ExitCode } -# clean folder -Clean-Build-Folder -# build mod +# # clean folder +# Clean-Build-Folder +# # build mod -& cmake -G "Ninja" -DCMAKE_BUILD_TYPE="RelWithDebInfo" -B build -& cmake --build ./build +# & cmake -G "Ninja" -DCMAKE_BUILD_TYPE="RelWithDebInfo" -B build +# & cmake --build ./build -$ExitCode = $LastExitCode +# $ExitCode = $LastExitCode # Post build, we actually want to transform the compile_commands.json file such that it has no \\ characters and ONLY has / characters # (Get-Content -Path build/compile_commands.json) | @@ -41,7 +41,7 @@ $ExitCode = $LastExitCode # To build tests, we just compile with our local clang++ into an executable # Kind of wacky but will work on linux -# Requires libcapstone-dev installed, and GSL/gtest headers fetched from cmake -# clang++ test/main.cpp src/trampoline.cpp src/trampoline-allocator.cpp -o build/test -std=c++20 -I/usr/include/ -Ishared -Ibuild/_deps/googletest-src/googletest/include -Ibuild/_deps/gsl-src/include -lcapstone -Iextern/includes/fmt/fmt/include -L/usr/lib/x86_64-linux-gnu -DFMT_HEADER_ONLY -Wall -Wextra -Werror -g - +# Requires libcapstone-dev installed +# sudo apt install +# clang++ test/main.cpp src/trampoline.cpp src/trampoline-allocator.cpp -o build/test -std=c++20 -Ishared -Iextern/includes -lcapstone -Iextern/includes/fmt/fmt/include -L/usr/lib/x86_64-linux-gnu -DFMT_HEADER_ONLY -Wall -Wextra -Werror -g exit $ExitCode \ No newline at end of file diff --git a/mod.json b/mod.json new file mode 100644 index 0000000..fe2b7b3 --- /dev/null +++ b/mod.json @@ -0,0 +1,15 @@ +{ + "_QPVersion": "1.0.0", + "name": "flamingo", + "id": "flamingo", + "author": "Sc2ad", + "version": "0.1.0", + "description": "General purpose Android hook library", + "dependencies": [], + "modFiles": [ + "libflamingo.so" + ], + "libraryFiles": [], + "fileCopies": [], + "copyExtensions": [] +} \ No newline at end of file diff --git a/mod.template.json b/mod.template.json new file mode 100644 index 0000000..7b672ef --- /dev/null +++ b/mod.template.json @@ -0,0 +1,13 @@ +{ + "_QPVersion": "1.0.0", + "name": "${mod_name}", + "id": "${mod_id}", + "author": "Sc2ad", + "version": "${version}", + "description": "General purpose Android hook library", + "dependencies": [], + "modFiles": [], + "libraryFiles": [], + "fileCopies": [], + "copyExtensions": [] +} \ No newline at end of file From fbb89921099e8f71c734243919026b805e1e775a Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Sat, 14 Oct 2023 22:31:14 -0700 Subject: [PATCH 015/134] Add publish script, ensure building of qmod works --- .github/workflows/build-ndk.yml | 2 +- .github/workflows/publish.yml | 93 +++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/build-ndk.yml b/.github/workflows/build-ndk.yml index c3f431c..27ace02 100644 --- a/.github/workflows/build-ndk.yml +++ b/.github/workflows/build-ndk.yml @@ -50,11 +50,11 @@ jobs: - name: Build run: | cd ${GITHUB_WORKSPACE} - qpm-rust restore qpm-rust s build - name: Create Qmod run: | + qpm-rust qmod build pwsh -Command ./createqmod.ps1 ${{env.qmodName}} - name: Get Library Name diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..c898c4a --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,93 @@ +name: Publish QPM Package + +env: + module_id: flamingo + qmodName: flamingo + cache-name: flamingo_cache + +on: + push: + tags: + - "v*" + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + name: Checkout + with: + submodules: true + lfs: true + + - uses: seanmiddleditch/gha-setup-ninja@v3 + + - name: Create ndkpath.txt + run: | + echo "$ANDROID_NDK_LATEST_HOME" > ${GITHUB_WORKSPACE}/ndkpath.txt + cat ${GITHUB_WORKSPACE}/ndkpath.txt + + - name: Get Tag Version + id: get_tag_version + run: | + echo ${GITHUB_REF#refs/tags/} + echo ::set-output name=TAG::${GITHUB_REF#refs/tags/} + echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v} + + - name: QPM Rust Action + uses: Fernthedev/qpm-rust-action@main + with: + #required + workflow_token: ${{secrets.GITHUB_TOKEN}} + + restore: true # will run restore on download + cache: true #will cache dependencies + + publish: true + publish_token: ${{secrets.QPM_TOKEN}} + + version: ${{ steps.get_tag_version.outputs.VERSION }} + tag: ${{ steps.get_tag_version.outputs.TAG }} + + # set to true if applicable, ASSUMES the file is already a release asset + qpm_release_bin: true + qpm_debug_bin: true + + # Name of qmod in release asset. Assumes exists, same as prior + qpm_qmod: ${{env.qmodName}}.qmod + + - name: Build + run: | + cd ${GITHUB_WORKSPACE} + qpm-rust s build + qpm-rust qmod build + + - name: Get Library Name + id: libname + run: | + cd ./build/ + pattern="lib${module_id}*.so" + files=( $pattern ) + echo ::set-output name=NAME::"${files[0]}" + + - name: Rename debug + run: | + mv ./build/debug/${{ steps.libname.outputs.NAME }} ./build/debug/debug_${{ steps.libname.outputs.NAME }} + + - name: Create Qmod + run: | + pwsh -Command ./createqmod.ps1 ${{env.qmodName}} + + - name: Upload to Release + id: upload_file_release + uses: softprops/action-gh-release@v0.1.15 + with: + name: ${{ github.event.inputs.release_msg }} + tag_name: ${{ github.event.inputs.version }} + files: | + ./build/${{ steps.libname.outputs.NAME }} + ./build/debug/debug_${{ steps.libname.outputs.NAME }} + ./${{ env.qmodName }}.qmod + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 26570ec0c905876c7afbed6545bf530f3e324fbc Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Sat, 14 Oct 2023 22:34:49 -0700 Subject: [PATCH 016/134] Update publish to avoid rename of debug so --- .github/workflows/publish.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c898c4a..5de2c18 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -71,10 +71,6 @@ jobs: files=( $pattern ) echo ::set-output name=NAME::"${files[0]}" - - name: Rename debug - run: | - mv ./build/debug/${{ steps.libname.outputs.NAME }} ./build/debug/debug_${{ steps.libname.outputs.NAME }} - - name: Create Qmod run: | pwsh -Command ./createqmod.ps1 ${{env.qmodName}} @@ -87,7 +83,7 @@ jobs: tag_name: ${{ github.event.inputs.version }} files: | ./build/${{ steps.libname.outputs.NAME }} - ./build/debug/debug_${{ steps.libname.outputs.NAME }} + ./build/debug_${{ steps.libname.outputs.NAME }} ./${{ env.qmodName }}.qmod env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 0d33e4a7bfac99752d801d9c59dec78d87788bb8 Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Sat, 14 Oct 2023 22:54:55 -0700 Subject: [PATCH 017/134] Move trampoline files to new folder So that extraFiles behaves when being used downstream --- CMakeLists.txt | 2 +- src/{ => trampoline}/trampoline-allocator.cpp | 0 src/{ => trampoline}/trampoline.cpp | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename src/{ => trampoline}/trampoline-allocator.cpp (100%) rename src/{ => trampoline}/trampoline.cpp (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index e9bc286..4d8ae7d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,7 +52,7 @@ add_library( # recursively get all src files # RECURSE_FILES(cpp_file_list_utils ${SOURCE_DIR}/*.cpp) # RECURSE_FILES(c_file_list_utils ${SOURCE_DIR}/*.c) -target_sources(${COMPILE_ID} PRIVATE ${SOURCE_DIR}/trampoline.cpp ${SOURCE_DIR}/trampoline-allocator.cpp) +target_sources(${COMPILE_ID} PRIVATE ${SOURCE_DIR}/trampoline/trampoline.cpp ${SOURCE_DIR}/trampoline/trampoline-allocator.cpp) # target_sources(${COMPILE_ID} PRIVATE ${c_file_list_utils}) # if (DEFINED TEST_BUILD) diff --git a/src/trampoline-allocator.cpp b/src/trampoline/trampoline-allocator.cpp similarity index 100% rename from src/trampoline-allocator.cpp rename to src/trampoline/trampoline-allocator.cpp diff --git a/src/trampoline.cpp b/src/trampoline/trampoline.cpp similarity index 100% rename from src/trampoline.cpp rename to src/trampoline/trampoline.cpp From b411c25e8f1fa277ba8ee455107ad8fa4e89b6fd Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Thu, 2 Nov 2023 21:01:30 -0700 Subject: [PATCH 018/134] trampoline: Improve protection guarantees, use instruction counts --- shared/trampoline.hpp | 1 + src/trampoline/trampoline-allocator.cpp | 122 ++-- src/trampoline/trampoline.cpp | 858 ++++++++++++------------ 3 files changed, 503 insertions(+), 478 deletions(-) diff --git a/shared/trampoline.hpp b/shared/trampoline.hpp index 9ac1b50..1e45c0d 100644 --- a/shared/trampoline.hpp +++ b/shared/trampoline.hpp @@ -18,6 +18,7 @@ struct Trampoline { Trampoline(uint32_t* ptr, std::size_t num_insts, std::size_t& sz) : address(ptr, &ptr[num_insts]), num_insts(num_insts), pageSizeRef(sz) {} + // TODO: Move all writes out of this type and instead have them as part of a transparent writer type void Write(uint32_t instruction); /// @brief Writes the specified pointer as a target specific immediate to the data block. /// @param ptr The pointer to write as a raw literal piece of data. NOT dereferenced. diff --git a/src/trampoline/trampoline-allocator.cpp b/src/trampoline/trampoline-allocator.cpp index 37d7b8d..56d180c 100644 --- a/src/trampoline/trampoline-allocator.cpp +++ b/src/trampoline/trampoline-allocator.cpp @@ -7,11 +7,11 @@ namespace { struct PageType { - void* ptr; - std::size_t used_size; - uint16_t trampoline_count; + void* ptr; + std::size_t used_size; + uint16_t trampoline_count; - constexpr PageType(void* p, std::size_t used) : ptr(p), used_size(used), trampoline_count(1) {} + constexpr PageType(void* p, std::size_t used) : ptr(p), used_size(used), trampoline_count(1) {} }; std::list pages; @@ -21,63 +21,79 @@ constexpr static std::size_t PageSize = 4096; namespace flamingo { Trampoline TrampolineAllocator::Allocate(std::size_t trampolineSize) { - // Allocation should work by grabbing a full page at a time - // Then we mark the page as rwx - // Then we should be allowed to use anything on that page until we would need to make another - // (due to new trampoline being too big to fit) - // Repeat. - for (auto& page : pages) { - // If we have enough space in our page for this trampoline, squeeze it in! - if (PageSize - page.used_size > trampolineSize) { - // TODO: We have to be aligned 16 here - Trampoline to_ret(reinterpret_cast(reinterpret_cast(page.ptr) + page.used_size), trampolineSize, - page.used_size); - page.used_size += trampolineSize; - page.trampoline_count++; - // Log allocated trampoline here - return to_ret; - } - } - // No pages with enough space available. - void* ptr; - if (::posix_memalign(&ptr, PageSize, PageSize) != 0) { - // Log error on memalign allocation! - FLAMINGO_ABORT("Failed to allocate trampoline page of size: {} for size: {}. err: {}", PageSize, trampolineSize, - std::strerror(errno)); - } - // Mark full page as rxw - if (::mprotect(ptr, PageSize, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) { + // Allocation should work by grabbing a full page at a time + // Then we mark the page as rwx + // Then we should be allowed to use anything on that page until we would need to make another + // (due to new trampoline being too big to fit) + // Repeat. + for (auto& page : pages) { + // If we have enough space in our page for this trampoline, squeeze it in! + if (PageSize - page.used_size > trampolineSize) { + // If we can fit in our existing page, we should try to + // Mark full page as rxw AGAIN (since we may have swapped it off before) + if (::mprotect(page.ptr, PageSize, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) { // Log error on mprotect! - FLAMINGO_ABORT("Failed to mark allocated page at: {} as +rwx. err: {}", fmt::ptr(ptr), std::strerror(errno)); + FLAMINGO_ABORT("Failed to mark allocated page at: {} as +rwx. err: {}", fmt::ptr(page.ptr), std::strerror(errno)); + } + // We have to be aligned 16 here + constexpr static auto kAlignment = 0x10U; + auto unaligned_start = reinterpret_cast(page.ptr) + page.used_size; + uintptr_t aligned_start; + if (unaligned_start % kAlignment != 0) { + // Pad upwards + auto pad_bytes = (kAlignment - (unaligned_start % kAlignment)); + aligned_start = unaligned_start + pad_bytes; + page.used_size += pad_bytes; + } else { + aligned_start = unaligned_start; + } + Trampoline to_ret(reinterpret_cast(aligned_start), trampolineSize / sizeof(uint32_t), page.used_size); + page.used_size += trampolineSize; + page.trampoline_count++; + // Log allocated trampoline here + return to_ret; } - auto& page = pages.emplace_back(ptr, trampolineSize); - return { static_cast(ptr), trampolineSize, page.used_size }; + } + // No pages with enough space available. + void* ptr; + if (::posix_memalign(&ptr, PageSize, PageSize) != 0) { + // Log error on memalign allocation! + FLAMINGO_ABORT("Failed to allocate trampoline page of size: {} for size: {}. err: {}", PageSize, trampolineSize, std::strerror(errno)); + } + // Mark full page as rxw + if (::mprotect(ptr, PageSize, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) { + // Log error on mprotect! + FLAMINGO_ABORT("Failed to mark allocated page at: {} as +rwx. err: {}", fmt::ptr(ptr), std::strerror(errno)); + } + auto& page = pages.emplace_back(ptr, trampolineSize); + // Convert from trampoline size to number of instructions in the trampoline + return { static_cast(ptr), trampolineSize / sizeof(uint32_t), page.used_size }; } void TrampolineAllocator::Free(Trampoline const& toFree) { - // Freeing a trampoline should decrease the page it was allocated on's size by a known amount - // If we reach a point where a trampoline was deallocated on a page and it was the last one in that page, then we should - // 1. Mark the page as read/write only - // 2. deallocate the page + // Freeing a trampoline should decrease the page it was allocated on's size by a known amount + // If we reach a point where a trampoline was deallocated on a page and it was the last one in that page, then we should + // 1. Mark the page as read/write only + // 2. deallocate the page - // Find page we are allocated on - auto page_addr = reinterpret_cast(reinterpret_cast(toFree.address.data()) & ~(PageSize - 1)); - for (auto& p : pages) { - if (p.ptr == page_addr) { - p.trampoline_count--; - if (p.trampoline_count == 0) { - if (::mprotect(p.ptr, PageSize, PROT_READ) != 0) { - // Log error on mprotect - FLAMINGO_ABORT("Failed to mark page at: {} as read only. err: {}", fmt::ptr(p.ptr), std::strerror(errno)); - } - ::free(p.ptr); - } - return; + // Find page we are allocated on + auto page_addr = reinterpret_cast(reinterpret_cast(toFree.address.data()) & ~(PageSize - 1)); + for (auto& p : pages) { + if (p.ptr == page_addr) { + p.trampoline_count--; + if (p.trampoline_count == 0) { + if (::mprotect(p.ptr, PageSize, PROT_READ) != 0) { + // Log error on mprotect + FLAMINGO_ABORT("Failed to mark page at: {} as read only. err: {}", fmt::ptr(p.ptr), std::strerror(errno)); } + ::free(p.ptr); + } + return; } - // If we get here, we couldn't free the provided Trampoline! - FLAMINGO_ABORT("Failed to free trampoline at: {}, no matching page with page addr: {}!", fmt::ptr(toFree.address.data()), - fmt::ptr(page_addr)); + } + // If we get here, we couldn't free the provided Trampoline! + FLAMINGO_ABORT("Failed to free trampoline at: {}, no matching page with page addr: {}!", fmt::ptr(toFree.address.data()), + fmt::ptr(page_addr)); } } // namespace flamingo diff --git a/src/trampoline/trampoline.cpp b/src/trampoline/trampoline.cpp index 3109fad..8c498a1 100644 --- a/src/trampoline/trampoline.cpp +++ b/src/trampoline/trampoline.cpp @@ -1,4 +1,5 @@ #include "trampoline.hpp" +#include #include #include #include @@ -24,188 +25,140 @@ template struct BranchImmTypeTrait; template - requires(type == ARM64_INS_B || type == ARM64_INS_BL) + requires(type == ARM64_INS_B || type == ARM64_INS_BL) struct BranchImmTypeTrait { - constexpr static uint32_t imm_mask = 0b00000011111111111111111111111111U; - constexpr static uint32_t lshift = 0; - constexpr static uint32_t rshift = 2; + constexpr static uint32_t imm_mask = 0b00000011111111111111111111111111U; + constexpr static uint32_t lshift = 0; + constexpr static uint32_t rshift = 2; }; template - requires(type == ARM64_INS_CBZ || type == ARM64_INS_CBNZ) + requires(type == ARM64_INS_CBZ || type == ARM64_INS_CBNZ) struct BranchImmTypeTrait { - constexpr static uint32_t imm_mask = 0b00000000111111111111111111100000; - constexpr static uint32_t lshift = 5; - constexpr static uint32_t rshift = 2; + constexpr static uint32_t imm_mask = 0b00000000111111111111111111100000; + constexpr static uint32_t lshift = 5; + constexpr static uint32_t rshift = 2; }; template - requires(type == ARM64_INS_TBZ || type == ARM64_INS_TBNZ) + requires(type == ARM64_INS_TBZ || type == ARM64_INS_TBNZ) struct BranchImmTypeTrait { - constexpr static uint32_t imm_mask = 0b00000000000001111111111111100000; - constexpr static uint32_t lshift = 5; - constexpr static uint32_t rshift = 2; + constexpr static uint32_t imm_mask = 0b00000000000001111111111111100000; + constexpr static uint32_t lshift = 5; + constexpr static uint32_t rshift = 2; }; /// @brief Helper function that returns an encoded b for a particular offset consteval uint32_t get_b(int offset) { - constexpr uint32_t b_opcode = 0b00010100000000000000000000000000U; - return (b_opcode | (static_cast(offset) >> 2U)); + constexpr uint32_t b_opcode = 0b00010100000000000000000000000000U; + return (b_opcode | (static_cast(offset) >> 2U)); } constexpr int64_t get_untagged_pc(uint64_t pc) { - // Upper byte is tagged for PC addresses on android 11+ - constexpr uint64_t mask = ~(0xFFULL << (64U - 8U)); - return static_cast(static_cast(pc) & mask); + // Upper byte is tagged for PC addresses on android 11+ + constexpr uint64_t mask = ~(0xFFULL << (64U - 8U)); + return static_cast(static_cast(pc) & mask); } void Trampoline::Write(uint32_t instruction) { - FLAMINGO_ASSERT(instruction_count + 1 <= num_insts); - // Log what we are writing (and also our state) - address[instruction_count] = instruction; - instruction_count++; + FLAMINGO_ASSERT(instruction_count + 1 <= num_insts); + // Log what we are writing (and also our state) + address[instruction_count] = instruction; + instruction_count++; } void Trampoline::WriteData(void const* ptr) { - // TODO: Write this to a different buffer and return a pointer to it - // This would allow the control flow for a hook to be much cleaner and the data section to be well defined - FLAMINGO_ASSERT(instruction_count + sizeof(void*) / sizeof(uint32_t) <= num_insts); - // Log what we are writing (and also our state) - *reinterpret_cast(&address[instruction_count]) = ptr; - instruction_count += sizeof(void*) / sizeof(uint32_t); + // TODO: Write this to a different buffer and return a pointer to it + // This would allow the control flow for a hook to be much cleaner and the data section to be well defined + FLAMINGO_ASSERT(instruction_count + sizeof(void*) / sizeof(uint32_t) <= num_insts); + // Log what we are writing (and also our state) + *reinterpret_cast(&address[instruction_count]) = ptr; + instruction_count += sizeof(void*) / sizeof(uint32_t); } void Trampoline::WriteData(void const* ptr, uint32_t size) { - // TODO: Write this to a different buffer and return a pointer to it - FLAMINGO_ASSERT((size + instruction_count) <= num_insts); - FLAMINGO_DEBUG("Writing data from: {} of size: {}", ptr, size * sizeof(uint32_t)); - std::memcpy(&address[instruction_count], ptr, size * sizeof(uint32_t)); - instruction_count += size; + // TODO: Write this to a different buffer and return a pointer to it + FLAMINGO_ASSERT((size + instruction_count) <= num_insts); + FLAMINGO_DEBUG("Writing data from: {} of size: {}", ptr, size * sizeof(uint32_t)); + std::memcpy(&address[instruction_count], ptr, size * sizeof(uint32_t)); + instruction_count += size; } void Trampoline::WriteB(int64_t imm) { - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/B--Branch- - WriteCallback(reinterpret_cast(imm)); + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/B--Branch- + WriteCallback(reinterpret_cast(imm)); } void Trampoline::WriteBl(int64_t imm) { - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/BL--Branch-with-Link- - constexpr uint32_t branch_imm_mask = 0b00000011111111111111111111111111U; - int64_t pc = get_untagged_pc(reinterpret_cast(&address[instruction_count])); - int64_t delta = imm - pc; - if (std::llabs(delta) > (branch_imm_mask << 1) + 1) { - // Too far to emit a b. Emit a br instead. - // We cannot emit a blr here because the pc + 4 for return will be in our offset. - // LDR X17, #12 - constexpr uint32_t ldr_x17 = 0x58000071U; - Write(ldr_x17); - // ADR X30, #16 - constexpr uint32_t adr_x30 = 0x1000009EU; - Write(adr_x30); - // BR x17 - constexpr uint32_t br_x17 = 0xD61F0220U; - Write(br_x17); - // Data - WriteData(reinterpret_cast(imm)); - } else { - // Small enough to emit a b/bl. - // bl opcode | encoded immediate (delta >> 2) - // Note, abs(delta >> 2) must be < (1 << 26) - constexpr uint32_t bl_opcode = 0b10010100000000000000000000000000U; - Write(bl_opcode | ((delta >> 2) & branch_imm_mask)); - } + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/BL--Branch-with-Link- + constexpr uint32_t branch_imm_mask = 0b00000011111111111111111111111111U; + int64_t pc = get_untagged_pc(reinterpret_cast(&address[instruction_count])); + int64_t delta = imm - pc; + if (std::llabs(delta) > (branch_imm_mask << 1) + 1) { + // Too far to emit a b. Emit a br instead. + // We cannot emit a blr here because the pc + 4 for return will be in our offset. + // LDR X17, #12 + constexpr uint32_t ldr_x17 = 0x58000071U; + Write(ldr_x17); + // ADR X30, #16 + constexpr uint32_t adr_x30 = 0x1000009EU; + Write(adr_x30); + // BR x17 + constexpr uint32_t br_x17 = 0xD61F0220U; + Write(br_x17); + // Data + WriteData(reinterpret_cast(imm)); + } else { + // Small enough to emit a b/bl. + // bl opcode | encoded immediate (delta >> 2) + // Note, abs(delta >> 2) must be < (1 << 26) + constexpr uint32_t bl_opcode = 0b10010100000000000000000000000000U; + Write(bl_opcode | ((delta >> 2) & branch_imm_mask)); + } } void Trampoline::WriteAdr(uint8_t reg, int64_t imm) { - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/ADR--Form-PC-relative-address-?lang=en - constexpr uint32_t adr_maximum_imm = 0b00000000000111111111111111111111U; - constexpr uint32_t reg_mask = 0b11111; - int64_t pc = get_untagged_pc(reinterpret_cast(&address[instruction_count])); - int64_t delta = imm - pc; - if (std::llabs(delta) >= (adr_maximum_imm >> 1U)) { - // Too far to emit just an adr. - // LDR (used register), #0x8 - constexpr uint32_t ldr_mask = 0b01011000000000000000000000000000U; - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- - // imm is encoded as << 2, LSB just to the right of reg - constexpr uint32_t ldr_imm = (8U >> 2U) << 5U; - Write(ldr_mask | ldr_imm | (reg_mask & reg)); - // B #0xC - constexpr uint32_t b_0xc = 0x14000003U; - static_assert(b_0xc == get_b(0xC)); - Write(get_b(0xC)); - // Immediate data - WriteData(reinterpret_cast(imm)); - } else { - // Close enough to emit an adr. - // Note that delta should be within +-1 MB - constexpr uint32_t adr_opcode = 0b00010000000000000000000000000000; - // Get immlo - uint32_t imm_lo = ((static_cast(delta) & 3U) << 29); - // Get immhi - uint32_t imm_hi = (static_cast(delta) >> 2U) << 5; - Write(adr_opcode | imm_lo | imm_hi | (reg_mask & reg)); - } + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/ADR--Form-PC-relative-address-?lang=en + constexpr uint32_t adr_maximum_imm = 0b00000000000111111111111111111111U; + constexpr uint32_t reg_mask = 0b11111; + int64_t pc = get_untagged_pc(reinterpret_cast(&address[instruction_count])); + int64_t delta = imm - pc; + if (std::llabs(delta) >= (adr_maximum_imm >> 1U)) { + // Too far to emit just an adr. + // LDR (used register), #0x8 + constexpr uint32_t ldr_mask = 0b01011000000000000000000000000000U; + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- + // imm is encoded as << 2, LSB just to the right of reg + constexpr uint32_t ldr_imm = (8U >> 2U) << 5U; + Write(ldr_mask | ldr_imm | (reg_mask & reg)); + // B #0xC + constexpr uint32_t b_0xc = 0x14000003U; + static_assert(b_0xc == get_b(0xC)); + Write(get_b(0xC)); + // Immediate data + WriteData(reinterpret_cast(imm)); + } else { + // Close enough to emit an adr. + // Note that delta should be within +-1 MB + constexpr uint32_t adr_opcode = 0b00010000000000000000000000000000; + // Get immlo + uint32_t imm_lo = ((static_cast(delta) & 3U) << 29); + // Get immhi + uint32_t imm_hi = (static_cast(delta) >> 2U) << 5; + Write(adr_opcode | imm_lo | imm_hi | (reg_mask & reg)); + } } void Trampoline::WriteAdrp(uint8_t reg, int64_t imm) { - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/ADR--Form-PC-relative-address-?lang=en - // constexpr uint32_t adr_maximum_imm = 0b00000000000111111111111111111111U; - constexpr uint32_t reg_mask = 0b11111; - constexpr uint32_t pc_imm_mask = ~0b111111111111U; - constexpr int64_t adrp_maximum_imm = 0xFFFFF000U; - int64_t pc = get_untagged_pc(reinterpret_cast(&address[instruction_count])); - int64_t delta = (pc & pc_imm_mask) - imm; - if (std::llabs(delta) >= adrp_maximum_imm) { - // Too far to emit just an adr. - // LDR (used register), #0x8 - constexpr uint32_t ldr_mask = 0b01011000000000000000000000000000U; - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- - // imm is encoded as << 2, LSB just to the right of reg - constexpr uint32_t ldr_imm = (8U >> 2U) << 5; - Write(ldr_mask | ldr_imm | (reg_mask & reg)); - // B #0xC - constexpr uint32_t b_0xc = 0x14000003U; - // TODO: Remove this assertion - static_assert(b_0xc == get_b(0xC)); - Write(get_b(0xC)); - // Write(b_0xc); - // Immediate data - WriteData(reinterpret_cast(imm)); - } else { - // Close enough to emit an adrp. - // Note that delta should be within +-4 GB - constexpr uint32_t adrp_opcode = 0b10010000000000000000000000000000U; - // Imm is << 12 in parse of instruction - delta >>= 12; - // Get immlo - uint32_t imm_lo = ((static_cast(delta) & 3) << 29); - // Get immhi - uint32_t imm_hi = (static_cast(delta) >> 2) << 5; - Write(adrp_opcode | imm_lo | imm_hi | (reg_mask & reg)); - } -} - -void Trampoline::WriteLdr(uint32_t inst, uint8_t reg, int64_t imm) { - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- - constexpr uint32_t reg_mask = 0b11111; - // 20 bits because signed range is only allowed - // constexpr int64_t max_ldr_range = (1LL << 20); - if ((inst & 0xFF000000U) == 0xD8000000U) { - // This is a prefetch instruction. - // Lets just skip it. - return; - } - // int64_t pc = get_untagged_pc(reinterpret_cast(&address[instruction_count])); - // int64_t delta = imm - pc; - // TODO: Note missed optimization opportunity - // TODO: Should perform a small LDR - FLAMINGO_DEBUG("Potentially missed optimization opportunity for near LDRs!"); - - // if (std::llabs(delta) >= max_ldr_range) { - - // Too far to emit an equivalent LDR - // Fallback to performing a direct memory write/read + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/ADR--Form-PC-relative-address-?lang=en + // constexpr uint32_t adr_maximum_imm = 0b00000000000111111111111111111111U; + constexpr uint32_t reg_mask = 0b11111; + constexpr uint32_t pc_imm_mask = ~0b111111111111U; + constexpr int64_t adrp_maximum_imm = 0xFFFFF000U; + int64_t pc = get_untagged_pc(reinterpret_cast(&address[instruction_count])); + int64_t delta = (pc & pc_imm_mask) - imm; + if (std::llabs(delta) >= adrp_maximum_imm) { + // Too far to emit just an adr. // LDR (used register), #0x8 constexpr uint32_t ldr_mask = 0b01011000000000000000000000000000U; // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- @@ -213,174 +166,243 @@ void Trampoline::WriteLdr(uint32_t inst, uint8_t reg, int64_t imm) { constexpr uint32_t ldr_imm = (8U >> 2U) << 5; Write(ldr_mask | ldr_imm | (reg_mask & reg)); // B #0xC + constexpr uint32_t b_0xc = 0x14000003U; + // TODO: Remove this assertion + static_assert(b_0xc == get_b(0xC)); Write(get_b(0xC)); + // Write(b_0xc); // Immediate data - // 4, 8, 16(?) for our sizes - constexpr uint32_t size_mask = 0x40000000U; - // TODO: Pull the start address from this data write - WriteData(reinterpret_cast(imm), 1); - if ((inst & size_mask) != 0) { - WriteData(reinterpret_cast(imm + sizeof(uint32_t)), 1); - } + WriteData(reinterpret_cast(imm)); + } else { + // Close enough to emit an adrp. + // Note that delta should be within +-4 GB + constexpr uint32_t adrp_opcode = 0b10010000000000000000000000000000U; + // Imm is << 12 in parse of instruction + delta >>= 12; + // Get immlo + uint32_t imm_lo = ((static_cast(delta) & 3) << 29); + // Get immhi + uint32_t imm_hi = (static_cast(delta) >> 2) << 5; + Write(adrp_opcode | imm_lo | imm_hi | (reg_mask & reg)); + } +} - // } else { - // // Close enough to emit an LDR - // FLAMINGO_ABORT("Close LDR optimization is not implemented (with no fallback)!"); - // } +void Trampoline::WriteLdr(uint32_t inst, uint8_t reg, int64_t imm) { + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- + constexpr uint32_t reg_mask = 0b11111; + // 20 bits because signed range is only allowed + // constexpr int64_t max_ldr_range = (1LL << 20); + if ((inst & 0xFF000000U) == 0xD8000000U) { + // This is a prefetch instruction. + // Lets just skip it. + return; + } + // int64_t pc = get_untagged_pc(reinterpret_cast(&address[instruction_count])); + // int64_t delta = imm - pc; + // TODO: Note missed optimization opportunity + // TODO: Should perform a small LDR + FLAMINGO_DEBUG("Potentially missed optimization opportunity for near LDRs!"); + + // if (std::llabs(delta) >= max_ldr_range) { + + // Too far to emit an equivalent LDR + // Fallback to performing a direct memory write/read + // LDR (used register), #0x8 + constexpr uint32_t ldr_mask = 0b01011000000000000000000000000000U; + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- + // imm is encoded as << 2, LSB just to the right of reg + constexpr uint32_t ldr_imm = (8U >> 2U) << 5; + Write(ldr_mask | ldr_imm | (reg_mask & reg)); + // B #0xC + Write(get_b(0xC)); + // Immediate data + // 4, 8, 16(?) for our sizes + constexpr uint32_t size_mask = 0x40000000U; + // TODO: Pull the start address from this data write + WriteData(reinterpret_cast(imm), 1); + if ((inst & size_mask) != 0) { + WriteData(reinterpret_cast(imm + sizeof(uint32_t)), 1); + } + + // } else { + // // Close enough to emit an LDR + // FLAMINGO_ABORT("Close LDR optimization is not implemented (with no fallback)!"); + // } } template void WriteCondBranch(Trampoline& value, uint32_t instruction, int64_t imm) { - uint32_t imm_mask; - if constexpr (imm_19) { - // Imm 19 - constexpr uint32_t imm_mask_19 = 0b00000000111111111111111111100000; - imm_mask = imm_mask_19; - } else { - // Imm 14 - constexpr uint32_t imm_mask_14 = 0b00000000000001111111111111100000; - imm_mask = imm_mask_14; - } - int64_t pc = get_untagged_pc(reinterpret_cast(&value.address[value.instruction_count])); - int64_t delta = imm - pc; - // imm_mask >> 1 for maximum positive value - // << 2 because branch imms are << 2 - // >> 5 because the mask is too high - if (std::llabs(delta) < (imm_mask >> 4)) { - // Small enough to optimize, just write the instruction - // But with the modified offset - // Delta should be >> 2 for branch imm - // Then << 5 to be in the correct location - value.Write((instruction & ~imm_mask) | (static_cast((delta >> 2) << 5) & imm_mask)); - } else { - // Otherwise, we need to write the same expression but with a known offset - // Specifically, write the instruction but with an offset of 8 - // 2, because 8 >> 2 is 2 - // << 5 to place in correct location for immediate - value.Write((instruction & ~imm_mask) | ((2 << 5) & imm_mask)); - value.Write(get_b(0x14)); - value.WriteLdrBrData(reinterpret_cast(imm)); - } + uint32_t imm_mask; + if constexpr (imm_19) { + // Imm 19 + constexpr uint32_t imm_mask_19 = 0b00000000111111111111111111100000; + imm_mask = imm_mask_19; + } else { + // Imm 14 + constexpr uint32_t imm_mask_14 = 0b00000000000001111111111111100000; + imm_mask = imm_mask_14; + } + int64_t pc = get_untagged_pc(reinterpret_cast(&value.address[value.instruction_count])); + int64_t delta = imm - pc; + // imm_mask >> 1 for maximum positive value + // << 2 because branch imms are << 2 + // >> 5 because the mask is too high + if (std::llabs(delta) < (imm_mask >> 4)) { + // Small enough to optimize, just write the instruction + // But with the modified offset + // Delta should be >> 2 for branch imm + // Then << 5 to be in the correct location + value.Write((instruction & ~imm_mask) | (static_cast((delta >> 2) << 5) & imm_mask)); + } else { + // Otherwise, we need to write the same expression but with a known offset + // Specifically, write the instruction but with an offset of 8 + // 2, because 8 >> 2 is 2 + // << 5 to place in correct location for immediate + value.Write((instruction & ~imm_mask) | ((2 << 5) & imm_mask)); + value.Write(get_b(0x14)); + value.WriteLdrBrData(reinterpret_cast(imm)); + } } void Trampoline::WriteLdrBrData(uint32_t const* target) { - // LDR x17, 0x8 - constexpr uint32_t ldr_x17 = 0x58000051U; - Write(ldr_x17); - // BR x17 - constexpr uint32_t br_x17 = 0xD61F0220U; - Write(br_x17); - // Data - WriteData(target); + // LDR x17, 0x8 + constexpr uint32_t ldr_x17 = 0x58000051U; + Write(ldr_x17); + // BR x17 + constexpr uint32_t br_x17 = 0xD61F0220U; + Write(br_x17); + // Data + WriteData(target); } auto get_branch_immediate(cs_insn const& inst) { - FLAMINGO_ASSERT(inst.detail->arm64.op_count == 1); - return inst.detail->arm64.operands[0].imm; + FLAMINGO_ASSERT(inst.detail->arm64.op_count == 1); + return inst.detail->arm64.operands[0].imm; } std::pair get_second_immediate(cs_insn const& inst) { - // register is just bottom 5 bits - constexpr uint32_t reg_mask = 0b11111; - FLAMINGO_ASSERT(inst.detail->arm64.op_count == 2); - return { *reinterpret_cast(inst.bytes) & reg_mask, inst.detail->arm64.operands[1].imm }; + // register is just bottom 5 bits + constexpr uint32_t reg_mask = 0b11111; + FLAMINGO_ASSERT(inst.detail->arm64.op_count == 2); + return { *reinterpret_cast(inst.bytes) & reg_mask, inst.detail->arm64.operands[1].imm }; } std::pair get_last_immediate(cs_insn const& inst) { - // register is just bottom 5 bits - constexpr uint32_t reg_mask = 0b11111; - FLAMINGO_ASSERT(inst.detail->arm64.op_count >= 2); - return { *reinterpret_cast(inst.bytes) & reg_mask, inst.detail->arm64.operands[inst.detail->arm64.op_count - 1].imm }; + // register is just bottom 5 bits + constexpr uint32_t reg_mask = 0b11111; + FLAMINGO_ASSERT(inst.detail->arm64.op_count >= 2); + return { *reinterpret_cast(inst.bytes) & reg_mask, inst.detail->arm64.operands[inst.detail->arm64.op_count - 1].imm }; } csh getHandle() { - static csh handle = 0; - if (!handle) { - cs_err e1 = cs_open(CS_ARCH_ARM64, CS_MODE_ARM, &handle); - cs_option(handle, CS_OPT_DETAIL, 1); - if (e1) { - FLAMINGO_ABORT("Capstone initialization failed"); - } + static csh handle = 0; + if (!handle) { + cs_err e1 = cs_open(CS_ARCH_ARM64, CS_MODE_ARM, &handle); + cs_option(handle, CS_OPT_DETAIL, 1); + if (e1) { + FLAMINGO_ABORT("Capstone initialization failed"); } - return handle; + } + return handle; } cs_insn debugInst(uint32_t const* inst) { - cs_insn* insns = nullptr; - auto count = cs_disasm(getHandle(), reinterpret_cast(inst), sizeof(uint32_t), - static_cast(get_untagged_pc(reinterpret_cast(inst))), 1, &insns); - if (count == 1) { - return insns[0]; - } - return {}; + cs_insn* insns = nullptr; + auto count = cs_disasm(getHandle(), reinterpret_cast(inst), sizeof(uint32_t), + static_cast(get_untagged_pc(reinterpret_cast(inst))), 1, &insns); + if (count == 1) { + return insns[0]; + } + return {}; } struct BranchReferenceTag { - /// @brief The immediate mask to use when rewriting the instruction - uint32_t imm_mask; - /// @brief The amount to shift the immediate to the left to encode it correctly such that the mask would be valid - uint32_t lshift; - /// @brief The amount to shift the immediate to the right to encode it correctly such that the mask would be valid - uint32_t rshift; - /// @brief Index to overwrite - uint32_t target_index; + /// @brief The immediate mask to use when rewriting the instruction + uint32_t imm_mask; + /// @brief The amount to shift the immediate to the left to encode it correctly such that the mask would be valid + uint32_t lshift; + /// @brief The amount to shift the immediate to the right to encode it correctly such that the mask would be valid + uint32_t rshift; + /// @brief Index to overwrite + uint32_t target_index; }; template bool TryDeferBranch(Trampoline& self, uint16_t i, int64_t dst, int64_t target_start, int64_t target_end, uint32_t inst, std::array const& target_to_fixups, std::array, Sz>& branch_ref_map) { - // If we are within OUR fixup range, that's when things get interesting. - // TODO: If we are in SOME OTHER TRAMPOLINE'S fixup range, then we should use their call - using trait_t = BranchImmTypeTrait; - constexpr uint32_t imm_mask = trait_t::imm_mask; - constexpr uint32_t lshift = trait_t::lshift; - constexpr uint32_t rshift = trait_t::rshift; - if (dst < target_end && dst >= target_start) { - FLAMINGO_DEBUG("Potentially deferring branch at: {} because it is within: {} and {}", dst, target_start, target_end); - auto target_offset = (dst - target_start) / sizeof(uint32_t); - FLAMINGO_ASSERT(target_offset < target_to_fixups.size()); - FLAMINGO_ASSERT(target_offset < branch_ref_map.size()); - // Always emit the instruction with AN immediate. - // For forward references, we need to defer. - // This difference could be negative, but for those cases we will defer and overwrite. - auto fixup_difference = - static_cast(get_untagged_pc(reinterpret_cast(&self.address[self.instruction_count])) - - get_untagged_pc(reinterpret_cast(&self.address[target_to_fixups[target_offset]]))); - self.Write((inst & ~imm_mask) | ((fixup_difference >> rshift) << lshift)); - if (target_offset > i) { - FLAMINGO_DEBUG("Deferring at: {} with target offset: {}", i, target_offset); - // Need to defer. - // Deference SHOULD never cause the instruction being deferred to expand in size. - // It should always be possible to point the deferred instruction to the new one without emitting more instructions - branch_ref_map[target_offset].push_back({ - .imm_mask = imm_mask, - .lshift = lshift, - .rshift = rshift, - .target_index = i, - }); - } - return true; + // If we are within OUR fixup range, that's when things get interesting. + // TODO: If we are in SOME OTHER TRAMPOLINE'S fixup range, then we should use their call + using trait_t = BranchImmTypeTrait; + constexpr uint32_t imm_mask = trait_t::imm_mask; + constexpr uint32_t lshift = trait_t::lshift; + constexpr uint32_t rshift = trait_t::rshift; + if (dst < target_end && dst >= target_start) { + FLAMINGO_DEBUG("Potentially deferring branch at: {} because it is within: {} and {}", dst, target_start, target_end); + auto target_offset = (dst - target_start) / sizeof(uint32_t); + FLAMINGO_ASSERT(target_offset < target_to_fixups.size()); + FLAMINGO_ASSERT(target_offset < branch_ref_map.size()); + // Always emit the instruction with AN immediate. + // For forward references, we need to defer. + // This difference could be negative, but for those cases we will defer and overwrite. + auto fixup_difference = + static_cast(get_untagged_pc(reinterpret_cast(&self.address[self.instruction_count])) - + get_untagged_pc(reinterpret_cast(&self.address[target_to_fixups[target_offset]]))); + self.Write((inst & ~imm_mask) | ((fixup_difference >> rshift) << lshift)); + if (target_offset > i) { + FLAMINGO_DEBUG("Deferring at: {} with target offset: {}", i, target_offset); + // Need to defer. + // Deference SHOULD never cause the instruction being deferred to expand in size. + // It should always be possible to point the deferred instruction to the new one without emitting more instructions + branch_ref_map[target_offset].push_back({ + .imm_mask = imm_mask, + .lshift = lshift, + .rshift = rshift, + .target_index = i, + }); } - return false; + return true; + } + return false; } template void Trampoline::WriteFixups(uint32_t const* target) { - FLAMINGO_ASSERT(target); - // Copy over original instructions - FLAMINGO_DEBUG("Fixing up: {} instructions!", countToFixup); - original_instructions.resize(countToFixup); - std::memcpy(original_instructions.data(), target, countToFixup); - // The end of the fixups is used for referential branches - auto target_start = get_untagged_pc(reinterpret_cast(target)); - auto target_end = get_untagged_pc(reinterpret_cast(&target[countToFixup])); - // Disassemble the instructions into this pointer, which we can then index into per instruction - cs_insn* insns = nullptr; - auto count = cs_disasm(getHandle(), reinterpret_cast(target), sizeof(uint32_t) * countToFixup, - static_cast(get_untagged_pc(reinterpret_cast(target))), countToFixup, &insns); - // We should never try to write fixups for something that isn't a valid instruction - FLAMINGO_ASSERT(count == countToFixup); - // We use this set to track referential branches going forwards + FLAMINGO_ASSERT(target); + // Copy over original instructions + FLAMINGO_DEBUG("Fixing up: {} instructions!", countToFixup); + original_instructions.resize(countToFixup); + std::memcpy(original_instructions.data(), target, countToFixup * sizeof(uint32_t)); + // The end of the fixups is used for referential branches + auto target_start = get_untagged_pc(reinterpret_cast(target)); + auto target_end = get_untagged_pc(reinterpret_cast(&target[countToFixup])); + // Disassemble the instructions into this pointer, which we can then index into per instruction + cs_insn* insns = nullptr; + auto count = cs_disasm(getHandle(), reinterpret_cast(target), sizeof(uint32_t) * countToFixup, + static_cast(get_untagged_pc(reinterpret_cast(target))), countToFixup, &insns); + // We should never try to write fixups for something that isn't a valid instruction + FLAMINGO_ASSERT(count == countToFixup); + // We use this set to track referential branches going forwards + // If it is a branch, check to see if the target immediate would place us within our fixup range + // If so, we need to: + // - If the target is behind us, use the new target directly + // - If the target is in front of us, defer the write until later. + // We defer the write by basically writing the branch itself (since it must be a close branch) + // and then add its index (and its immediate mask and shift amount) to some set. + // Then, when we start the fixup for an instruction, we check the set to see if we should go back and fix the specified indices. + // To fix them, we simply walk all of the indices we wish to fix, and for each: + // - Current PC of instruction - &target[index] to replace + // - Use as argument for shift + mask? + + // TODO: Avoid heap alloc if possible + std::array, countToFixup> branch_ref_map{}; + // Holds the mapping of target index to fixup index + // TODO: Make this a publicly exposed member? + // TODO: Technically, we need to see if ANY branch target would leave us in ANY fixup block... + // TODO: Should collect a set of references so that if we ever install a hook over somewhere we would jump to we would force a recompile + // We should check against the full set of trampolines for this + std::array target_to_fixups{}; + + for (uint16_t i = 0; i < countToFixup; i++) { // If it is a branch, check to see if the target immediate would place us within our fixup range // If so, we need to: // - If the target is behind us, use the new target directly @@ -392,173 +414,159 @@ void Trampoline::WriteFixups(uint32_t const* target) { // - Current PC of instruction - &target[index] to replace // - Use as argument for shift + mask? - // TODO: Avoid heap alloc if possible - std::array, countToFixup> branch_ref_map{}; - // Holds the mapping of target index to fixup index - // TODO: Make this a publicly exposed member? - // TODO: Technically, we need to see if ANY branch target would leave us in ANY fixup block... - // TODO: Should collect a set of references so that if we ever install a hook over somewhere we would jump to we would force a recompile - // We should check against the full set of trampolines for this - std::array target_to_fixups{}; - - for (uint16_t i = 0; i < countToFixup; i++) { - // If it is a branch, check to see if the target immediate would place us within our fixup range - // If so, we need to: - // - If the target is behind us, use the new target directly - // - If the target is in front of us, defer the write until later. - // We defer the write by basically writing the branch itself (since it must be a close branch) - // and then add its index (and its immediate mask and shift amount) to some set. - // Then, when we start the fixup for an instruction, we check the set to see if we should go back and fix the specified indices. - // To fix them, we simply walk all of the indices we wish to fix, and for each: - // - Current PC of instruction - &target[index] to replace - // - Use as argument for shift + mask? - - // For each input instruction, perform a fixup on it - auto const& inst = insns[i]; - auto current_inst_ptr = &target[i]; - FLAMINGO_DEBUG("Fixup for inst: 0x{:x} at {}: {} {}, id: {}", *current_inst_ptr, fmt::ptr(current_inst_ptr), - fmt::string_view(inst.mnemonic, sizeof(inst.mnemonic)), fmt::string_view(inst.op_str, sizeof(inst.op_str)), - static_cast(inst.id)); - // For this incoming instruction, check to see if we have any forward references on this - // If we do, for each, rewrite the target instruction with the adjusted value - for (auto const& tag : branch_ref_map[i]) { - // Current PC is get_untagged_pc(&address[instruction_count]) - // The instruction we emit's PC is the map from target --> fixup - // This difference is always positive, since we are jumping FORWARD - auto difference = - static_cast(get_untagged_pc(reinterpret_cast(&address[instruction_count])) - - get_untagged_pc(reinterpret_cast(&address[target_to_fixups[tag.target_index]]))); - FLAMINGO_DEBUG("Performing deferred write at: {}, rewriting: {} with difference: {}", i, tag.target_index, difference); - address[target_to_fixups[tag.target_index]] = - (address[target_to_fixups[tag.target_index]] & ~tag.imm_mask) | (tag.imm_mask & ((difference >> tag.rshift) << tag.lshift)); + // For each input instruction, perform a fixup on it + auto const& inst = insns[i]; + auto current_inst_ptr = &target[i]; + FLAMINGO_DEBUG("Fixup for inst: 0x{:x} at {}: {} {}, id: {}", *current_inst_ptr, fmt::ptr(current_inst_ptr), + fmt::string_view(inst.mnemonic, sizeof(inst.mnemonic)), fmt::string_view(inst.op_str, sizeof(inst.op_str)), + static_cast(inst.id)); + // For this incoming instruction, check to see if we have any forward references on this + // If we do, for each, rewrite the target instruction with the adjusted value + for (auto const& tag : branch_ref_map[i]) { + // Current PC is get_untagged_pc(&address[instruction_count]) + // The instruction we emit's PC is the map from target --> fixup + // This difference is always positive, since we are jumping FORWARD + auto difference = static_cast(get_untagged_pc(reinterpret_cast(&address[instruction_count])) - + get_untagged_pc(reinterpret_cast(&address[target_to_fixups[tag.target_index]]))); + FLAMINGO_DEBUG("Performing deferred write at: {}, rewriting: {} with difference: {}", i, tag.target_index, difference); + address[target_to_fixups[tag.target_index]] = + (address[target_to_fixups[tag.target_index]] & ~tag.imm_mask) | (tag.imm_mask & ((difference >> tag.rshift) << tag.lshift)); + } + // Set the target map entry for this incoming instruction to the current offset of the output + target_to_fixups[i] = instruction_count; + switch (inst.id) { + // Handle fixups for branch immediate + case ARM64_INS_B: { + FLAMINGO_DEBUG("Fixing up B..."); + auto dst = get_branch_immediate(inst); + if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, branch_ref_map)) { + if (inst.detail->arm64.cc != ARM64_CC_INVALID) { + WriteCondBranch(*this, *current_inst_ptr, dst); + } else { + WriteB(dst); + } } - // Set the target map entry for this incoming instruction to the current offset of the output - target_to_fixups[i] = instruction_count; - switch (inst.id) { - // Handle fixups for branch immediate - case ARM64_INS_B: { - FLAMINGO_DEBUG("Fixing up B..."); - auto dst = get_branch_immediate(inst); - if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, - branch_ref_map)) { - if (inst.detail->arm64.cc != ARM64_CC_INVALID) { - WriteCondBranch(*this, *current_inst_ptr, dst); - } else { - WriteB(dst); - } - } - } break; - case ARM64_INS_BL: { - FLAMINGO_DEBUG("Fixing up BL..."); - auto dst = get_branch_immediate(inst); - if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, - branch_ref_map)) { - WriteBl(dst); - } - } break; - - // Handle fixups for conditional branches - case ARM64_INS_CBNZ: - case ARM64_INS_CBZ: { - FLAMINGO_DEBUG("Fixing up CBNZ/CBZ..."); - auto [reg, dst] = get_last_immediate(inst); - if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, - branch_ref_map)) { - WriteCondBranch(*this, *current_inst_ptr, dst); - } - } break; - case ARM64_INS_TBNZ: - case ARM64_INS_TBZ: { - FLAMINGO_DEBUG("Fixing up TBNZ/TBZ..."); - auto [reg, dst] = get_last_immediate(inst); - if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, - branch_ref_map)) { - WriteCondBranch(*this, *current_inst_ptr, dst); - } - } break; - - // Handle fixups for load literals - case ARM64_INS_LDR: { - FLAMINGO_DEBUG("Fixing up LDR..."); - // TODO: Handle the case where an LDR would land in a fixup range, so we need to copy the raw values - constexpr uint32_t b_31 = 0b10000000000000000000000000000000; - constexpr uint32_t ldr_lit_opc_mask = 0b10111111000000000000000000000000; - if ((*current_inst_ptr & ldr_lit_opc_mask) == 0b00011000000000000000000000000000) { - // This is an ldr literal - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- - auto [reg, dst] = get_second_immediate(inst); - WriteLdr(*current_inst_ptr, reg, dst); - } else if ((*current_inst_ptr & (ldr_lit_opc_mask & ~b_31)) == 0b00011100000000000000000000000000) { - // This is an ldr literal, SIMD - // https://developer.arm.com/documentation/ddi0596/2021-12/SIMD-FP-Instructions/LDR--literal--SIMD-FP---Load-SIMD-FP-Register--PC-relative-literal-- - FLAMINGO_ABORT("LDR of the SIMD variant is not yet supported!"); - } else { - // This is an LDR that doesn't need to be fixed up - FLAMINGO_DEBUG("Fixing up standard LDR..."); - Write(*reinterpret_cast(inst.bytes)); - } - } break; - case ARM64_INS_LDRSW: { - // This is an ldrsw literal - // See TODOs for LDR - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDRSW--literal---Load-Register-Signed-Word--literal-- - FLAMINGO_ABORT("LDRSW fixup not yet supported!"); - } break; - - // Handle pc-relative loads - case ARM64_INS_ADR: { - FLAMINGO_DEBUG("Fixing up ADR..."); - auto [reg, dst] = get_second_immediate(inst); - WriteAdr(reg, dst); - } break; - case ARM64_INS_ADRP: { - FLAMINGO_DEBUG("Fixing up ADRP..."); - auto [reg, dst] = get_second_immediate(inst); - WriteAdrp(reg, dst); - } break; - - // Otherwise, just write the instruction verbatim - default: - FLAMINGO_DEBUG("Fixing up UNKNOWN: {}...", inst.id); - Write(*reinterpret_cast(inst.bytes)); - break; + } break; + case ARM64_INS_BL: { + FLAMINGO_DEBUG("Fixing up BL..."); + auto dst = get_branch_immediate(inst); + if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, branch_ref_map)) { + WriteBl(dst); } + } break; + + // Handle fixups for conditional branches + case ARM64_INS_CBNZ: + case ARM64_INS_CBZ: { + FLAMINGO_DEBUG("Fixing up CBNZ/CBZ..."); + auto [reg, dst] = get_last_immediate(inst); + if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, branch_ref_map)) { + WriteCondBranch(*this, *current_inst_ptr, dst); + } + } break; + case ARM64_INS_TBNZ: + case ARM64_INS_TBZ: { + FLAMINGO_DEBUG("Fixing up TBNZ/TBZ..."); + auto [reg, dst] = get_last_immediate(inst); + if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, branch_ref_map)) { + WriteCondBranch(*this, *current_inst_ptr, dst); + } + } break; + + // Handle fixups for load literals + case ARM64_INS_LDR: { + FLAMINGO_DEBUG("Fixing up LDR..."); + // TODO: Handle the case where an LDR would land in a fixup range, so we need to copy the raw values + constexpr uint32_t b_31 = 0b10000000000000000000000000000000; + constexpr uint32_t ldr_lit_opc_mask = 0b10111111000000000000000000000000; + if ((*current_inst_ptr & ldr_lit_opc_mask) == 0b00011000000000000000000000000000) { + // This is an ldr literal + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- + auto [reg, dst] = get_second_immediate(inst); + WriteLdr(*current_inst_ptr, reg, dst); + } else if ((*current_inst_ptr & (ldr_lit_opc_mask & ~b_31)) == 0b00011100000000000000000000000000) { + // This is an ldr literal, SIMD + // https://developer.arm.com/documentation/ddi0596/2021-12/SIMD-FP-Instructions/LDR--literal--SIMD-FP---Load-SIMD-FP-Register--PC-relative-literal-- + FLAMINGO_ABORT("LDR of the SIMD variant is not yet supported!"); + } else { + // This is an LDR that doesn't need to be fixed up + FLAMINGO_DEBUG("Fixing up standard LDR..."); + Write(*reinterpret_cast(inst.bytes)); + } + } break; + case ARM64_INS_LDRSW: { + // This is an ldrsw literal + // See TODOs for LDR + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDRSW--literal---Load-Register-Signed-Word--literal-- + FLAMINGO_ABORT("LDRSW fixup not yet supported!"); + } break; + + // Handle pc-relative loads + case ARM64_INS_ADR: { + FLAMINGO_DEBUG("Fixing up ADR..."); + auto [reg, dst] = get_second_immediate(inst); + WriteAdr(reg, dst); + } break; + case ARM64_INS_ADRP: { + FLAMINGO_DEBUG("Fixing up ADRP..."); + auto [reg, dst] = get_second_immediate(inst); + WriteAdrp(reg, dst); + } break; + + // Otherwise, just write the instruction verbatim + default: + FLAMINGO_DEBUG("Fixing up UNKNOWN: {}...", inst.id); + Write(*reinterpret_cast(inst.bytes)); + break; } + } - // Free the disassembled instructions from before the fixups - cs_free(insns, countToFixup); + // Free the disassembled instructions from before the fixups + cs_free(insns, countToFixup); } void Trampoline::WriteHookFixups(uint32_t const* target) { - // Write the fixups for a standard hook. - // Ideally, this is hidden entirely and only the hook API can do this - WriteFixups<4>(target); + // Write the fixups for a standard hook. + // Ideally, this is hidden entirely and only the hook API can do this + WriteFixups<4>(target); } void Trampoline::WriteCallback(uint32_t const* target) { - constexpr uint32_t branch_imm_mask = 0b00000011111111111111111111111111U; - int64_t pc = get_untagged_pc(reinterpret_cast(&address[instruction_count])); - auto delta = reinterpret_cast(target) - pc; - if (std::llabs(delta) > (branch_imm_mask << 1) + 1) { - // Too far for b. Emit a br instead. - WriteLdrBrData(target); - } else { - // Small enough to emit a b. - // b opcode | encoded immediate (delta >> 2) - // Note, abs(delta >> 2) must be < (1 << 26) - constexpr uint32_t b_opcode = 0b00010100000000000000000000000000U; - Write(b_opcode | ((delta >> 2) & branch_imm_mask)); - } + constexpr uint32_t branch_imm_mask = 0b00000011111111111111111111111111U; + int64_t pc = get_untagged_pc(reinterpret_cast(&address[instruction_count])); + auto delta = reinterpret_cast(target) - pc; + if (std::llabs(delta) > (branch_imm_mask << 1) + 1) { + // Too far for b. Emit a br instead. + WriteLdrBrData(target); + } else { + // Small enough to emit a b. + // b opcode | encoded immediate (delta >> 2) + // Note, abs(delta >> 2) must be < (1 << 26) + constexpr uint32_t b_opcode = 0b00010100000000000000000000000000U; + Write(b_opcode | ((delta >> 2) & branch_imm_mask)); + } } void Trampoline::Finish() { - pageSizeRef -= (num_insts - instruction_count) * sizeof(uint32_t); - FLAMINGO_DEBUG("Completed trampoline allocation of: {} instructions!", instruction_count); + auto bytes_to_subtract = (num_insts - instruction_count) * sizeof(uint32_t); + pageSizeRef -= bytes_to_subtract; + FLAMINGO_DEBUG("Completed trampoline allocation of: {} instructions! Out of: {} Subtracting: {} with page size: {}", instruction_count, + num_insts, bytes_to_subtract, pageSizeRef); + FLAMINGO_ASSERT(bytes_to_subtract < pageSizeRef); + // Reset memory protection to be r+x to avoid potential overwrites + constexpr static auto kPageSize = 0x1000ULL; + auto* page_aligned_target = reinterpret_cast(reinterpret_cast(address.data()) & ~(kPageSize - 1)); + FLAMINGO_DEBUG("Marking target: {} as NON writable, page aligned: {}", fmt::ptr(address.data()), fmt::ptr(page_aligned_target)); + if (::mprotect(page_aligned_target, kPageSize, PROT_READ | PROT_EXEC) != 0) { + // Log error on mprotect! + FLAMINGO_ABORT("Failed to mark: {} (page aligned: {}) as +rwx. err: {}", fmt::ptr(address.data()), fmt::ptr(page_aligned_target), + std::strerror(errno)); + } } void Trampoline::Log() { - // TODO: Log the trampoline and various information here - // This will probably be necessary given the potential for failure + // TODO: Log the trampoline and various information here + // This will probably be necessary given the potential for failure } } // namespace flamingo From 6ece580882ef0b8af1deb908a29aacff5652a0ed Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Fri, 10 Nov 2023 23:57:08 -0800 Subject: [PATCH 019/134] trampoline: Fix poor assertion logic --- src/trampoline/trampoline.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/trampoline/trampoline.cpp b/src/trampoline/trampoline.cpp index 8c498a1..81e673e 100644 --- a/src/trampoline/trampoline.cpp +++ b/src/trampoline/trampoline.cpp @@ -549,10 +549,10 @@ void Trampoline::WriteCallback(uint32_t const* target) { void Trampoline::Finish() { auto bytes_to_subtract = (num_insts - instruction_count) * sizeof(uint32_t); - pageSizeRef -= bytes_to_subtract; FLAMINGO_DEBUG("Completed trampoline allocation of: {} instructions! Out of: {} Subtracting: {} with page size: {}", instruction_count, num_insts, bytes_to_subtract, pageSizeRef); FLAMINGO_ASSERT(bytes_to_subtract < pageSizeRef); + pageSizeRef -= bytes_to_subtract; // Reset memory protection to be r+x to avoid potential overwrites constexpr static auto kPageSize = 0x1000ULL; auto* page_aligned_target = reinterpret_cast(reinterpret_cast(address.data()) & ~(kPageSize - 1)); From bf97363ae0a1127be65799e8ac0f84587997c459 Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Sat, 2 Mar 2024 11:50:42 -0800 Subject: [PATCH 020/134] TMP: Update format/tidy, add HookData type --- .clang-format | 5 +- .clang-tidy | 31 +++++--- shared/calling-convention.hpp | 7 ++ shared/calling_convention.hpp | 7 -- shared/enum-helpers.hpp | 13 ++-- shared/hook-data.hpp | 138 ++++++++++++++++++++++++++++------ shared/util.hpp | 2 + 7 files changed, 152 insertions(+), 51 deletions(-) create mode 100644 shared/calling-convention.hpp delete mode 100644 shared/calling_convention.hpp diff --git a/.clang-format b/.clang-format index 170d9ba..e32c4cc 100644 --- a/.clang-format +++ b/.clang-format @@ -6,8 +6,9 @@ ColumnLimit: 140 CommentPragmas: NOLINT:.* DerivePointerAlignment: false IncludeBlocks: Preserve -IndentWidth: 4 +IndentWidth: 2 PointerAlignment: Left -TabWidth: 4 +TabWidth: 2 UseTab: Never Cpp11BracedListStyle: false +QualifierAlignment: Right diff --git a/.clang-tidy b/.clang-tidy index 0e3c870..a411712 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,14 +1,25 @@ -Checks: "*,-llvm*, - -google-readability-namespace, - -llvmlib, - -fuchsia*, - -altera*, - -cppcoreguidelines-pro-type-union-access, - -modernize-use-trailing-return, +Checks: "clang-diagnostic-*, + clang-analyzer-*, + fuchsia-statically-constructed-objects, + *, + -llvmlibc-callee-namespace, + -readability-identifier-length, + -llvmlibc-implementation-in-namespace, + -llvmlibc-restrict-system-libc-headers, + -llvm-namespace-comment, + -llvm-include-order, + -altera-*, + -fuchsia-*, -modernize-use-trailing-return-type, - -readability-avoid-const-params-in, + -cppcoreguidelines-avoid-non-const-global-variables, + -llvm-header-guard, -cppcoreguidelines-macro-usage, - -readability-identifier-length, + -cppcoreguidelines-pro-type-vararg, -cppcoreguidelines-avoid-do-while, - -google-readability-todo" + -hicpp-vararg, + -concurrency-mt-unsafe, + -abseil-*" +WarningsAsErrors: true +HeaderFilterRegex: "" +AnalyzeTemporaryDtors: false FormatStyle: file diff --git a/shared/calling-convention.hpp b/shared/calling-convention.hpp new file mode 100644 index 0000000..9d114f9 --- /dev/null +++ b/shared/calling-convention.hpp @@ -0,0 +1,7 @@ +#pragma once + +namespace flamingo { +/// @brief Represents the calling convention for a given hook. +/// Used primarily for type checking +enum struct CallingConvention { Cdecl, Fastcall, Thiscall }; +} // namespace flamingo diff --git a/shared/calling_convention.hpp b/shared/calling_convention.hpp deleted file mode 100644 index fd399bd..0000000 --- a/shared/calling_convention.hpp +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once -/// @brief Represents the calling convention for a given hook. -enum struct CallingConvention { - Cdecl, - Fastcall, - Thiscall -}; \ No newline at end of file diff --git a/shared/enum-helpers.hpp b/shared/enum-helpers.hpp index 1ff9bf6..eddf31b 100644 --- a/shared/enum-helpers.hpp +++ b/shared/enum-helpers.hpp @@ -1,20 +1,19 @@ #pragma once +#include #include namespace flamingo::enum_helpers { /// @brief Adds the provided enumeration flag to the given enumeration value. -template -requires (std::is_enum_v) -constexpr auto AddFlag(auto lhs) noexcept { +template +requires(std::is_enum_v) constexpr auto AddFlag(auto lhs) noexcept { return decltype(lhs)(static_cast(lhs) | static_cast(flag)); } /// @brief Checks if a given enumeration value has the provided enumeration flag. -template -requires (std::is_enum_v) -constexpr auto HasFlag(auto lhs) noexcept { +template +requires(std::is_enum_v) constexpr auto HasFlag(auto lhs) noexcept { return (static_cast(lhs) & static_cast(flag)) != 0; } -} \ No newline at end of file +} // namespace flamingo::enum_helpers diff --git a/shared/hook-data.hpp b/shared/hook-data.hpp index 12412d5..0925488 100644 --- a/shared/hook-data.hpp +++ b/shared/hook-data.hpp @@ -1,34 +1,122 @@ #pragma once +#include +#include #include -#include "calling_convention.hpp" +#include #include +#include +#include +#include +#include #include +#include "calling-convention.hpp" +#include "hook-installation-result.hpp" +#include "trampoline.hpp" -struct TargetData { - void* target_method; - uint16_t method_size; - CallingConvention calling_convention; -}; +namespace flamingo { + +struct Hook; +struct TargetData; struct InstallationMetadata { - uint8_t need_orig : 1; - uint8_t is_midpoint : 1; - // uint32_t permissible_fixup_registers; + bool need_orig; + bool is_midpoint; + // uint32_t permissible_fixup_registers; + + installation::Result combine(InstallationMetadata const& other); +}; + +/// @brief Describes the name metadata of the hook, used for lookups and priorities. +/// Lookups are described using userdata when the HookInfo is made at first. +struct HookNameMetadata { + std::string name{}; + std::any userdata{}; +}; + +/// @brief Represents a priority for how to align hook orderings. Note that a change in priority MAY require a full list recreation. +/// But SHOULD NOT require a hook recompile or a trampoline recompile. +struct HookPriority { + /// @brief The set of constraints for this hook to be installed before (called earlier than) + std::vector befores{}; + /// @brief The set of constraints for this hook to be installed after (called later than) + std::vector afters{}; +}; + +struct HookMetadata { + CallingConvention convention; + InstallationMetadata metadata; + uint16_t method_num_insts; + HookNameMetadata name_info; + HookPriority priority; +#ifndef FLAMINGO_NO_REGISTRATION_CHECKS + std::vector parameter_info; + TypeInfo return_info; +#endif +}; + +/// @brief Represents a hook that a user of this library will use. +/// On install, we collect this information into a single TargetInfo structure, which contains a collection of multiple Hook references. +/// We map target --> TargetInfo and every time we have a new hook installed there, we move orig pointers around accordingly. +/// To do priorities, we have to track that state within a given Hook +/// We don't necessarily need O(1) hook installation, but we could. +/// A TargetInfo may basically just be a Hook, except with the added list of Hooks, which we use to validate. +struct HookInfo { + // TODO: friend struct this to something? + friend struct TargetData; + template + using HookFuncType = R (*)(TArgs...); + // TODO: Could replace this with a single function, instead of hook by hook? + // This function should return true if both userdata infos are equivalent. + // This is to be used for modloader unaware code, but stateful hook infos + using HookPriorityLookupFunction = std::function; + + template + HookInfo(HookFuncType hook_func, void* target, HookFuncType* orig_ptr, uint16_t num_insts, + CallingConvention conv, InstallationMetadata metadata, HookNameMetadata&& name_info, HookPriority&& priority, + HookPriorityLookupFunction&& priority_lookup_func) + : target(target), + orig_ptr(orig_ptr), + hook_ptr(hook_func), + metadata(HookMetadata{ + .convention = conv, + .metadata = metadata, + .method_num_insts = num_insts, + .name_info = name_info, + .priority = priority, +#ifndef FLAMINGO_NO_REGISTRATION_CHECKS + .parameter_info = { TypeInfo::from()... }, + .return_info = TypeInfo::from(), +#endif + }), + priority_lookup_func(priority_lookup_func) { + } + + void* target; + + private: + void** orig_ptr; + void* hook_ptr; + HookMetadata metadata; + HookPriorityLookupFunction priority_lookup_func; +}; + +/// @brief Represents the status of a particular address +/// If hooked, will contain the same members as a hook, but additionally with a list of Hooks +/// The idea being we can O(1) install hooks (and uninstall via iterator) +struct TargetData { + HookMetadata metadata; + std::optional orig_trampoline{}; + std::list hooks{}; + Trampoline target; + + installation::Result combine(HookInfo&& incoming); }; -struct HookData { - using TargetResolutionPtr = TargetData (*)(); - friend struct HookInstaller; - - TargetResolutionPtr resolution_function; - std::vector parameter_sizes; - std::size_t return_size; - void** orig_ptr; - void* hook_ptr; - CallingConvention convention; - InstallationMetadata metadata; - - static void RegisterHook(HookData&& d); - private: - static std::list hooks_to_install; -}; \ No newline at end of file +/// @brief To install a hook, we require a constructed HookInfo. We want the value to be thrown away, so we require an rvalue (we may also +/// forward params?). Because a HookInfo is just data, we go find our TargetInfo that matches our target. +/// Then, we attempt to install that HookInfo onto the TargetData, mutating the TargetData (but not invalidating other HookInfo referneces +/// within the list). We update the shared information within the HookInfo and perform the install as necessary. +/// The order of priorities is: lowest value runs before higher values. +/// TODO: Make priorities potentially use named IDs for cleaer ordering (before x, after y). This may require a full reassmebly of the list! +auto Install(HookInfo&& hook); +} // namespace flamingo diff --git a/shared/util.hpp b/shared/util.hpp index e9f1031..fd9d479 100644 --- a/shared/util.hpp +++ b/shared/util.hpp @@ -3,6 +3,8 @@ #define FLAMINGO_ID "flamingo" #define FLAMINGO_VERSION "0.1.0" +#define FLAMINGO_EXPORT __attribute__((visibility("default"))) + #ifdef FLAMINGO_HEADER_ONLY #ifdef ANDROID From 5da5fcd871247f40234f9e9a41b6b21a5decd20e Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Sun, 19 May 2024 22:53:01 -0700 Subject: [PATCH 021/134] Modify format rules and remove using check --- .clang-format | 3 ++- .clang-tidy | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.clang-format b/.clang-format index e32c4cc..e0a8a36 100644 --- a/.clang-format +++ b/.clang-format @@ -2,7 +2,7 @@ BasedOnStyle: Google AllowShortBlocksOnASingleLine: false AllowShortFunctionsOnASingleLine: Empty AllowShortIfStatementsOnASingleLine: true -ColumnLimit: 140 +ColumnLimit: 120 CommentPragmas: NOLINT:.* DerivePointerAlignment: false IncludeBlocks: Preserve @@ -12,3 +12,4 @@ TabWidth: 2 UseTab: Never Cpp11BracedListStyle: false QualifierAlignment: Right +BracedInitializerIndentWidth: 2 diff --git a/.clang-tidy b/.clang-tidy index a411712..6cfdd11 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -18,6 +18,7 @@ Checks: "clang-diagnostic-*, -cppcoreguidelines-avoid-do-while, -hicpp-vararg, -concurrency-mt-unsafe, + -modernize-use-using, -abseil-*" WarningsAsErrors: true HeaderFilterRegex: "" From 65c4bae2c33a754cb7b918e6d1ba27d1dccde436 Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Fri, 24 May 2024 20:41:06 -0700 Subject: [PATCH 022/134] Add fixups file and trampoline fixups Revamp Trampoline to now only be a Fixups structure. Fixups are now applied via a FixupContext type which holds context needed for clean fixups (tbd how we want to inject hook related metadata, i.e. other hooks fixup ranges, into the context structure) Update test code to be functional on ubuntu with and without debuggers attached (ASLR disabled vs. enabled) Add tests for near and far adrps, handle them by ignoring near ones Add a more standard PageAllocator Add some RTTI pointer wrapper types to make permissions cleanup easier Next steps are resolving fixup discrepancies that come up and making a wrapper type for holding the fixups structure (i.e. TargetHook instances as defined in the partial API) --- shared/fixups.hpp | 70 +++ shared/page-allocator.hpp | 65 +++ shared/trampoline-allocator.hpp | 19 - shared/trampoline.hpp | 52 -- src/fixups.cpp | 608 ++++++++++++++++++++++++ src/page-allocator.cpp | 80 ++++ src/trampoline/trampoline-allocator.cpp | 99 ---- src/trampoline/trampoline.cpp | 572 ---------------------- test/main.cpp | 220 +++++---- 9 files changed, 929 insertions(+), 856 deletions(-) create mode 100644 shared/fixups.hpp create mode 100644 shared/page-allocator.hpp delete mode 100644 shared/trampoline-allocator.hpp delete mode 100644 shared/trampoline.hpp create mode 100644 src/fixups.cpp create mode 100644 src/page-allocator.cpp delete mode 100644 src/trampoline/trampoline-allocator.cpp delete mode 100644 src/trampoline/trampoline.cpp diff --git a/shared/fixups.hpp b/shared/fixups.hpp new file mode 100644 index 0000000..4d579d9 --- /dev/null +++ b/shared/fixups.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "page-allocator.hpp" +#include "util.hpp" + +namespace flamingo { + +template +struct ProtectionWriter { + // The target to write to + PointerWrapper target; + PageProtectionType original_permissions; + // Where in the target we are currently about to write to + uint_fast16_t target_offset{ 0 }; + + ProtectionWriter(PointerWrapper ptr) : target(ptr), original_permissions(target.protection) { + // When we construct this writer, we mark the page we are operating on as writable. + // To do this, we align the pointer down to the multiple of the PageSize + // and then we protect it with the write permission. + target.protection |= PageProtectionType::kWrite; + target.Protect(); + } + ProtectionWriter(ProtectionWriter const&) = delete; + ProtectionWriter(ProtectionWriter&& other) + : target(other.target), original_permissions(other.original_permissions), target_offset(other.target_offset) { + // Set other's target to null to avoid a case where we re-protect on the first instance's dtor + other.target.addr = {}; + } + + ~ProtectionWriter() { + target.protection = original_permissions; + target.Protect(); + } + // Write data to this writer. Returns the index that we wrote to. + uint_fast16_t Write(T inst) { + if (target_offset >= target.addr.size()) { + FLAMINGO_ABORT("Cannot write if there is no space available! {} should be < {}", target_offset, + target.addr.size()); + } + target.addr[target_offset] = inst; + auto to_return = target_offset; + target_offset++; + return to_return; + } +}; + +struct Fixups { + // The location to read as input for fixup writes + PointerWrapper target; + // The location to write fixups to + PointerWrapper fixup_inst_destination; + std::vector original_instructions{}; + + /// @brief Logs various information about the fixups. + /// Will log the original instructions and the full set of fixups, including data, for the full allocation window + void Log() const; + // For the input target, walks over the size passed in to the span + // For each instruction listed, fixes it up + void PerformFixupsAndCallback(); +}; + +// TODO: DO NOT EXPOSE THIS SYMBOL (USE IT FOR TESTING ONLY) +csh getHandle(); + +} // namespace flamingo \ No newline at end of file diff --git a/shared/page-allocator.hpp b/shared/page-allocator.hpp new file mode 100644 index 0000000..7b2f89d --- /dev/null +++ b/shared/page-allocator.hpp @@ -0,0 +1,65 @@ +#pragma once +#include +#include +#include +#include +#include "util.hpp" + +namespace flamingo { +enum struct [[clang::flag_enum]] PageProtectionType : int { + kNone = PROT_NONE, + kRead = PROT_READ, + kWrite = PROT_WRITE, + kExecute = PROT_EXEC, +}; + +inline PageProtectionType operator|(PageProtectionType lhs, PageProtectionType rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} +inline PageProtectionType& operator|=(PageProtectionType& lhs, PageProtectionType rhs) { + return lhs = lhs | rhs; +} +inline PageProtectionType operator&(PageProtectionType lhs, PageProtectionType rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} +inline PageProtectionType& operator&=(PageProtectionType& lhs, PageProtectionType rhs) { + return lhs = lhs & rhs; +} + +struct Page { + constexpr static uint_fast16_t PageSize = 4096; + void* ptr; + uint_fast16_t used_size; + PageProtectionType protection; + + static auto PageAlign(auto ptr) { + return reinterpret_cast(reinterpret_cast(ptr) & (~(static_cast(PageSize) - 1))); + } +}; + +// Holds a pointer with a size and a protection. +// Provides a way of protecting the memory at this pointer by page aligning +template +struct PointerWrapper { + std::span addr; + PageProtectionType protection; + + PointerWrapper(std::span addr, PageProtectionType prot) : addr(addr), protection(prot) {} + + void Protect() const { + // If we have nothing in the address, don't bother protecting + if (addr.empty()) return; + auto const page_aligned = Page::PageAlign(addr.data()); + auto const page_offset = reinterpret_cast(addr.data()) % Page::PageSize; + if (::mprotect(page_aligned, addr.size_bytes() + page_offset, static_cast(protection)) != 0) { + // Log error on mprotect! + FLAMINGO_ABORT("Failed to mark ptr at: {} (page aligned: {}) with size: {} with permissions: {}. err: {}", + fmt::ptr(addr.data()), fmt::ptr(page_aligned), page_offset + addr.size_bytes(), + static_cast(protection), std::strerror(errno)); + } + } +}; + +PointerWrapper Allocate(uint_fast16_t alignment, uint_fast16_t size, PageProtectionType protection); + +} // namespace flamingo diff --git a/shared/trampoline-allocator.hpp b/shared/trampoline-allocator.hpp deleted file mode 100644 index 055f842..0000000 --- a/shared/trampoline-allocator.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include -#include -#include "capstone/shared/capstone/capstone.h" - -namespace flamingo { - -struct Trampoline; - -struct TrampolineAllocator { - static Trampoline Allocate(std::size_t trampolineSize); - static void Free(Trampoline const& toFree); -}; - -// TODO: DO NOT EXPOSE THIS SYMBOL (USE IT FOR TESTING ONLY) -csh getHandle(); - -} // namespace flamingo diff --git a/shared/trampoline.hpp b/shared/trampoline.hpp deleted file mode 100644 index 1e45c0d..0000000 --- a/shared/trampoline.hpp +++ /dev/null @@ -1,52 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace flamingo { - -struct Trampoline { - std::span address; - // size is number of instructions - // TODO: Make a transparent type here to avoid accidental comparison with other types of size - std::size_t num_insts; - std::size_t instruction_count = 0; - std::size_t& pageSizeRef; - std::vector original_instructions{}; - - Trampoline(uint32_t* ptr, std::size_t num_insts, std::size_t& sz) - : address(ptr, &ptr[num_insts]), num_insts(num_insts), pageSizeRef(sz) {} - - // TODO: Move all writes out of this type and instead have them as part of a transparent writer type - void Write(uint32_t instruction); - /// @brief Writes the specified pointer as a target specific immediate to the data block. - /// @param ptr The pointer to write as a raw literal piece of data. NOT dereferenced. - void WriteData(void const* ptr); - /// @brief Performs a memcpy from the specified pointer and size - /// @param ptr The pointer to memcpy from - /// @param size The number of 4 byte words to copy - void WriteData(void const* ptr, uint32_t size); - void WriteCallback(uint32_t const* target); - void WriteB(int64_t imm); - void WriteBl(int64_t imm); - void WriteAdr(uint8_t reg, int64_t imm); - void WriteAdrp(uint8_t reg, int64_t imm); - void WriteLdr(uint32_t inst, uint8_t reg, int64_t imm); - - /// @brief Helper function to write: - /// LDR x17, #0x8 - /// BR X17 - /// DATA - void WriteLdrBrData(uint32_t const* target); - // TODO: Put this in an internal-trampoline header instead so that it can be used from other TUs but is not exposed - template - void WriteFixups(uint32_t const* target); - void WriteHookFixups(uint32_t const* target); - /// @brief Logs various information about the trampoline. - void Log(); - /// @brief A TRAMPOLINE IS NOT COMPLETE UNTIL FINISH IS CALLED! - void Finish(); -}; - -} // namespace flamingo \ No newline at end of file diff --git a/src/fixups.cpp b/src/fixups.cpp new file mode 100644 index 0000000..4ebc434 --- /dev/null +++ b/src/fixups.cpp @@ -0,0 +1,608 @@ +#include "fixups.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include "capstone/capstone.h" +#include "capstone/platform.h" +#include "util.hpp" + +namespace { +auto get_branch_immediate(cs_insn const& inst) { + FLAMINGO_ASSERT(inst.detail->arm64.op_count == 1); + return inst.detail->arm64.operands[0].imm; +} + +std::pair get_second_immediate(cs_insn const& inst) { + // register is just bottom 5 bits + constexpr uint32_t reg_mask = 0b11111; + FLAMINGO_ASSERT(inst.detail->arm64.op_count == 2); + return { *reinterpret_cast(inst.bytes) & reg_mask, inst.detail->arm64.operands[1].imm }; +} + +std::pair get_last_immediate(cs_insn const& inst) { + // register is just bottom 5 bits + constexpr uint32_t reg_mask = 0b11111; + FLAMINGO_ASSERT(inst.detail->arm64.op_count >= 2); + return { *reinterpret_cast(inst.bytes) & reg_mask, + inst.detail->arm64.operands[inst.detail->arm64.op_count - 1].imm }; +} + +constexpr int64_t get_untagged_pc(uint64_t pc) { + // Upper byte is tagged for PC addresses on android 11+ + constexpr uint64_t mask = ~(0xFFULL << (64U - 8U)); + return static_cast(static_cast(pc) & mask); +} +int64_t get_untagged_pc(void const* pc) { + return get_untagged_pc(reinterpret_cast(pc)); +} + +// Helper types for holding immediate masks, lshifts and rshifts for conversions to immediates from PC differences +template +struct BranchImmTypeTrait; + +template + requires(type == ARM64_INS_B || type == ARM64_INS_BL) +struct BranchImmTypeTrait { + constexpr static uint32_t imm_mask = 0b00000011111111111111111111111111U; + constexpr static uint32_t lshift = 0; + constexpr static uint32_t rshift = 2; +}; + +template + requires(type == ARM64_INS_CBZ || type == ARM64_INS_CBNZ) +struct BranchImmTypeTrait { + constexpr static uint32_t imm_mask = 0b00000000111111111111111111100000; + constexpr static uint32_t lshift = 5; + constexpr static uint32_t rshift = 2; +}; + +template + requires(type == ARM64_INS_TBZ || type == ARM64_INS_TBNZ) +struct BranchImmTypeTrait { + constexpr static uint32_t imm_mask = 0b00000000000001111111111111100000; + constexpr static uint32_t lshift = 5; + constexpr static uint32_t rshift = 2; +}; + +/// @brief Helper function that returns an encoded b for a particular offset +consteval uint32_t get_b(int offset) { + constexpr uint32_t b_opcode = 0b00010100000000000000000000000000U; + return (b_opcode | (static_cast(offset) >> 2U)); +} + +constexpr uint32_t ldr_imm_mask = 0b111111111111111111100000U; + +struct ImmediateReferenceTag { + /// @brief The immediate mask to use when rewriting the instruction + uint32_t imm_mask; + /// @brief The amount to shift the immediate to the left to encode it correctly such that the mask would be valid + uint_fast16_t lshift; + /// @brief The amount to shift the immediate to the right to encode it correctly such that the mask would be valid + uint_fast16_t rshift; + /// @brief Index of the fixup index with the immediate to be overwritten + uint_fast16_t fixup_index; + /// @brief Index into the resultant data section to resolve to + uint_fast16_t data_index; +}; +struct BranchReferenceTag { + /// @brief The immediate mask to use when rewriting the instruction + uint32_t imm_mask; + /// @brief The amount to shift the immediate to the left to encode it correctly such that the mask would be valid + uint32_t lshift; + /// @brief The amount to shift the immediate to the right to encode it correctly such that the mask would be valid + uint32_t rshift; + /// @brief Index to overwrite + uint32_t target_index; +}; +// Holds the context for performing fixups that we don't want to expose to the caller. +struct FixupContext { + // The initial target pointer + std::span target; + flamingo::ProtectionWriter fixup_writer; + // Holds sequentially laid out data for usage within fixups + std::vector data_block{}; + uint_fast16_t data_index = 0; + // Holds a collection of data elements, which describes which fixups to perform overwrites of after data is allocated + std::vector data_ref_tags{}; + // Holds a mapping from fixup index to a list of branches targeting that location + std::vector> branch_ref_map{}; + // Holds the mapping of target index to fixup index for branch references + // TODO: Technically, we need to see if ANY branch target would leave us in ANY fixup block... + // TODO: Should collect a set of references so that if we ever install a hook over somewhere we would jump to we would + // force a recompile. We should check against the full set of all fixups for this. + std::vector target_to_fixups{}; + // The raw address of the target start/end as an untagged PC address + uint64_t target_start; + uint64_t target_end; + + FixupContext(flamingo::PointerWrapper fixup_ptr, std::span target) + : target(target), + fixup_writer(fixup_ptr), + target_start(get_untagged_pc(target.data())), + target_end(get_untagged_pc(&target[target.size()])) { + target_to_fixups.resize(target.size()); + branch_ref_map.resize(target.size()); + // based off of the size of the fixups, we allocate accordingly. + // TODO: This is an overestimate (each fixup needs a uint64_t) + data_block.reserve(target.size() * 2); + data_ref_tags.reserve(target.size()); + } + + auto GetFixupPC() const { + return get_untagged_pc(&fixup_writer.target.addr[fixup_writer.target_offset]); + } + + auto Write(uint32_t inst) { + return fixup_writer.Write(inst); + } + void WriteData(uint_fast16_t fixup_idx, uint32_t data, uint32_t imm_mask, uint_fast16_t lshift, + uint_fast16_t rshift) { + FLAMINGO_ASSERT(fixup_idx < fixup_writer.target_offset); + uint_fast16_t data_index = data_block.size(); + // Pointer is known to be little endian + FLAMINGO_DEBUG("Adding 32b data: 0x{:x} at data index: {} for fixup index: {} ({})", data, data_index, fixup_idx, + fmt::ptr(&fixup_writer.target.addr[fixup_idx])); + data_block.push_back(data); + data_ref_tags.emplace_back(ImmediateReferenceTag{ + .imm_mask = imm_mask, + .lshift = lshift, + .rshift = rshift, + .fixup_index = fixup_idx, + .data_index = data_index, + }); + } + // When we call WriteData, we are using the previously written fixup as our fixup index. This means, however, that we + // must have written at least one fixup already. + void WriteData(uint_fast16_t fixup_idx, uint64_t large_data, uint32_t imm_mask, uint_fast16_t lshift, + uint_fast16_t rshift) { + FLAMINGO_ASSERT(fixup_idx < fixup_writer.target_offset); + uint_fast16_t data_index = data_block.size(); + // Pointer is known to be little endian + FLAMINGO_DEBUG("Adding 64b data: 0x{:x} at data index: {} for fixup index: {} ({})", large_data, data_index, + fixup_idx, fmt::ptr(&fixup_writer.target.addr[fixup_idx])); + data_block.push_back(large_data & (UINT32_MAX)); + data_block.push_back((large_data >> 32) & UINT32_MAX); + data_ref_tags.emplace_back(ImmediateReferenceTag{ + .imm_mask = imm_mask, + .lshift = lshift, + .rshift = rshift, + .fixup_index = fixup_idx, + .data_index = data_index, + }); + } + void WriteLdrWithData(int64_t data, uint_fast8_t reg) { + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- + // imm is encoded as << 2, LSB just to the right of reg + constexpr uint32_t ldr_mask = 0b01011000000000000000000000000000U; + constexpr uint32_t reg_mask = 0b11111; + auto ldr_idx = Write(ldr_mask | (reg_mask & reg)); + WriteData(ldr_idx, static_cast(data), ldr_imm_mask, 5, 2); + } + void WriteLdrBrData(int64_t target) { + // LDR x17, DATA OFFSET FOR BRANCH TARGET + WriteLdrWithData(target, 17); + // BR x17 + constexpr uint32_t br_x17 = 0xD61F0220U; + Write(br_x17); + } + void WriteCallback(uint32_t const* target) { + constexpr uint32_t branch_imm_mask = 0b00000011111111111111111111111111U; + auto delta = get_untagged_pc(target) - GetFixupPC(); + if (std::llabs(delta) > (branch_imm_mask << 1) + 1) { + // Too far for b. Emit a br instead. + WriteLdrBrData(get_untagged_pc(target)); + } else { + // Small enough to emit a b. + // b opcode | encoded immediate (delta >> 2) + // Note, abs(delta >> 2) must be < (1 << 26) + constexpr uint32_t b_opcode = 0b00010100000000000000000000000000U; + Write(b_opcode | ((delta >> 2) & branch_imm_mask)); + } + } + void WriteB(int64_t imm) { + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/B--Branch- + WriteCallback(reinterpret_cast(imm)); + } + + void WriteBl(int64_t imm) { + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/BL--Branch-with-Link- + constexpr uint32_t branch_imm_mask = 0b00000011111111111111111111111111U; + int64_t delta = imm - GetFixupPC(); + if (std::llabs(delta) > (branch_imm_mask << 1) + 1) { + // Too far to emit a b. Emit a br instead. + // We CAN emit a blr here because the pc + 4 for return will no longer be in the data section. + // LDR X17, DATA OFFSET FOR BRANCH + WriteLdrWithData(imm, 17); + // BLR x17 + constexpr uint32_t blr_x17 = 0xD63F0220U; + Write(blr_x17); + } else { + // Small enough to emit a b/bl. + // bl opcode | encoded immediate (delta >> 2) + // Note, abs(delta >> 2) must be < (1 << 26) + constexpr uint32_t bl_opcode = 0b10010100000000000000000000000000U; + Write(bl_opcode | ((delta >> 2) & branch_imm_mask)); + } + } + + void WriteAdr(uint8_t reg, int64_t imm) { + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/ADR--Form-PC-relative-address-?lang=en + constexpr uint32_t adr_maximum_imm = 0b00000000000111111111111111111111U; + constexpr uint32_t reg_mask = 0b11111; + int64_t delta = imm - GetFixupPC(); + if (std::llabs(delta) >= (adr_maximum_imm >> 1U)) { + // Too far to emit just an adr. + // LDR (used register), DATA OFSET FOR IMM + WriteLdrWithData(imm, reg); + } else { + // Close enough to emit an adr. + // Note that delta should be within +-1 MB + constexpr uint32_t adr_opcode = 0b00010000000000000000000000000000; + // Get immlo + uint32_t imm_lo = ((static_cast(delta) & 3U) << 29); + // Get immhi + uint32_t imm_hi = (static_cast(delta) >> 2U) << 5; + Write(adr_opcode | imm_lo | imm_hi | (reg_mask & reg)); + } + } + + void WriteAdrp(uint8_t reg, int64_t imm) { + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/ADR--Form-PC-relative-address-?lang=en + // constexpr uint32_t adr_maximum_imm = 0b00000000000111111111111111111111U; + constexpr uint32_t pc_imm_mask = ~0b111111111111U; + constexpr int64_t adrp_maximum_imm = 0xFFFFF000U; + int64_t delta = (GetFixupPC() & pc_imm_mask) - imm; + + if (std::llabs(delta) < adrp_maximum_imm) { + // TODO: Note missed optimization opportunity + // TODO: Should perform a small ADRP + FLAMINGO_DEBUG("Potentially missed optimization opportunity for near ADRP, imm: {}, target pc: {}", imm, + GetFixupPC()); + // This optimization fails sometimes: + // Orig: D flamingo|v0.1.0: Addr: 0x7b6d100 Value: 0xb000eee0, adrp x0, #0x994a000 + // Fixup: D flamingo|v0.1.0: Addr: 0x7fb68fd0903c Value: 0xf0431de0, adrp x0, #0x7fb7160c8000 + + // // Close enough to emit an adrp. + // // Note that delta should be within +-4 GB + // constexpr uint32_t adrp_opcode = 0b10010000000000000000000000000000U; + // constexpr uint32_t reg_mask = 0b11111; + // // Imm is << 12 in parse of instruction + // delta >>= 12; + // // Get immlo + // uint32_t imm_lo = ((static_cast(delta) & 3) << 29); + // // Get immhi + // uint32_t imm_hi = (static_cast(delta) >> 2) << 5; + // Write(adrp_opcode | imm_lo | imm_hi | (reg_mask & reg)); + } + // Too far to emit just an adr. + // LDR (used register), DATA OFFSET FOR IMM + WriteLdrWithData(imm, reg); + } + + void WriteLdr(uint32_t inst, uint8_t reg, int64_t imm) { + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- + // 20 bits because signed range is only allowed + constexpr int64_t max_ldr_range = (1LL << 20); + if ((inst & 0xFF000000U) == 0xD8000000U) { + // This is a prefetch instruction. + // Lets just skip it. + return; + } + int64_t delta = imm - GetFixupPC(); + if (std::llabs(delta) < max_ldr_range) { + // TODO: Should perform a small LDR + // constexpr uint32_t reg_mask = 0b11111; + FLAMINGO_DEBUG("Potentially missed optimization opportunity for near LDR, imm: {} target pc: {}", imm, + GetFixupPC()); + } + + // Too far to emit an equivalent LDR + // Fallback to performing a direct memory write/read + // LDR (used register), DATA OFFSET FOR IMM + // TODO: Instead of assuming int64_t data at imm, use the size_mask and swap accordingly + // 4, 8, 16(?) for our sizes + // constexpr uint32_t size_mask = 0x40000000U; + WriteLdrWithData(*reinterpret_cast(imm), reg); + } + template + void WriteCondBranch(uint32_t instruction, int64_t imm) { + uint32_t imm_mask; + if constexpr (imm_19) { + // Imm 19 + constexpr uint32_t imm_mask_19 = 0b00000000111111111111111111100000; + imm_mask = imm_mask_19; + } else { + // Imm 14 + constexpr uint32_t imm_mask_14 = 0b00000000000001111111111111100000; + imm_mask = imm_mask_14; + } + int64_t delta = imm - GetFixupPC(); + // imm_mask >> 1 for maximum positive value + // << 2 because branch imms are << 2 + // >> 5 because the mask is too high + if (std::llabs(delta) < (imm_mask >> 4)) { + // Small enough to optimize, just write the instruction + // But with the modified offset + // Delta should be >> 2 for branch imm + // Then << 5 to be in the correct location + Write((instruction & ~imm_mask) | (static_cast((delta >> 2) << 5) & imm_mask)); + } else { + // Otherwise, we need to write the same expression but with a known offset + // Specifically, write the instruction but with an offset of 8 (to skip the B we write later in this function) + // 2, because 8 >> 2 is 2 + // << 5 to place in correct location for immediate + Write((instruction & ~imm_mask) | ((2 << 5) & imm_mask)); + // 0xC to skip over: LDR x17, BR x17 + // TODO: Instead of hardcoding the b and forcing an ldr + br data entry, for near immediates, we can ecode them + // closer + + Write(get_b(0xC)); + WriteLdrBrData(imm); + } + } + + template + bool TryDeferBranch(uint16_t i, int64_t dst, uint32_t inst) { + // If it is a branch, check to see if the target immediate would place us within our fixup range + // If so, we need to: + // - If the target is behind us, use the new target directly + // - If the target is in front of us, defer the write until later. + // We defer the write by basically writing the branch itself (since it must be a close branch) + // and then add its index (and its immediate mask and shift amount) to some set. + // Then, when we start the fixup for an instruction, we check the set to see if we should go back and fix the + // specified indices. To fix them, we simply walk all of the indices we wish to fix, and for each: + // - Current PC of instruction - &target[index] to replace + // - Use as argument for shift + mask? + + // If we are within OUR fixup range, that's when things get interesting. + // TODO: If we are in SOME OTHER TRAMPOLINE'S fixup range, then we should use their call + using trait_t = BranchImmTypeTrait; + constexpr uint32_t imm_mask = trait_t::imm_mask; + constexpr uint32_t lshift = trait_t::lshift; + constexpr uint32_t rshift = trait_t::rshift; + if (dst < static_cast(target_end) && dst >= static_cast(target_start)) { + FLAMINGO_DEBUG("Potentially deferring branch at: 0x{:x} because it is within: 0x{:x} and 0x{:x}", dst, + target_start, target_end); + auto target_offset = (dst - target_start) / sizeof(uint32_t); + FLAMINGO_ASSERT(target_offset < target_to_fixups.size()); + FLAMINGO_ASSERT(target_offset < branch_ref_map.size()); + // Always emit the instruction with AN immediate that is valid. + // For forward references, we need to defer. + // This difference could be negative, but for those cases we will defer and overwrite. + auto fixup_difference = static_cast( + get_untagged_pc(reinterpret_cast(&fixup_writer.target.addr[fixup_writer.target_offset])) - + get_untagged_pc(reinterpret_cast(&fixup_writer.target.addr[target_to_fixups[target_offset]]))); + Write((inst & ~imm_mask) | ((fixup_difference >> rshift) << lshift)); + if (target_offset > i) { + FLAMINGO_DEBUG("Deferring at: {} with target offset: 0x{:x}", i, target_offset); + // Need to defer. + // Deference SHOULD never cause the instruction being deferred to expand in size. + // It should always be possible to point the deferred instruction to the new one without emitting more + // instructions + branch_ref_map[target_offset].emplace_back(BranchReferenceTag{ + .imm_mask = imm_mask, + .lshift = lshift, + .rshift = rshift, + .target_index = i, + }); + } + return true; + } + return false; + } + void PerformFixupFor(cs_insn const& inst, int i, uint32_t const* const current_inst_ptr) { + // Set the target map entry for this incoming instruction to the current offset of the output + target_to_fixups[i] = fixup_writer.target_offset; + switch (inst.id) { + // Handle fixups for branch immediate + case ARM64_INS_B: { + FLAMINGO_DEBUG("Fixing up B..."); + auto dst = get_branch_immediate(inst); + if (!TryDeferBranch(i, dst, *current_inst_ptr)) { + if (inst.detail->arm64.cc != ARM64_CC_INVALID) { + WriteCondBranch(*current_inst_ptr, dst); + } else { + WriteB(dst); + } + } + } break; + case ARM64_INS_BL: { + FLAMINGO_DEBUG("Fixing up BL..."); + auto dst = get_branch_immediate(inst); + if (!TryDeferBranch(i, dst, *current_inst_ptr)) { + WriteBl(dst); + } + } break; + + // Handle fixups for conditional branches + case ARM64_INS_CBNZ: + case ARM64_INS_CBZ: { + FLAMINGO_DEBUG("Fixing up CBNZ/CBZ..."); + auto [reg, dst] = get_last_immediate(inst); + if (!TryDeferBranch(i, dst, *current_inst_ptr)) { + WriteCondBranch(*current_inst_ptr, dst); + } + } break; + case ARM64_INS_TBNZ: + case ARM64_INS_TBZ: { + FLAMINGO_DEBUG("Fixing up TBNZ/TBZ..."); + auto [reg, dst] = get_last_immediate(inst); + if (!TryDeferBranch(i, dst, *current_inst_ptr)) { + WriteCondBranch(*current_inst_ptr, dst); + } + } break; + + // Handle fixups for load literals + case ARM64_INS_LDR: { + FLAMINGO_DEBUG("Fixing up LDR..."); + // TODO: Handle the case where an LDR would land in a fixup range, so we need to copy the raw values + constexpr uint32_t b_31 = 0b10000000000000000000000000000000; + constexpr uint32_t ldr_lit_opc_mask = 0b10111111000000000000000000000000; + if ((*current_inst_ptr & ldr_lit_opc_mask) == 0b00011000000000000000000000000000) { + // This is an ldr literal + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- + auto [reg, dst] = get_second_immediate(inst); + WriteLdr(*current_inst_ptr, reg, dst); + } else if ((*current_inst_ptr & (ldr_lit_opc_mask & ~b_31)) == 0b00011100000000000000000000000000) { + // This is an ldr literal, SIMD + // https://developer.arm.com/documentation/ddi0596/2021-12/SIMD-FP-Instructions/LDR--literal--SIMD-FP---Load-SIMD-FP-Register--PC-relative-literal-- + FLAMINGO_ABORT("LDR of the SIMD variant is not yet supported!"); + } else { + // This is an LDR that doesn't need to be fixed up + FLAMINGO_DEBUG("Fixing up standard LDR..."); + Write(*reinterpret_cast(inst.bytes)); + } + } break; + case ARM64_INS_LDRSW: { + // This is an ldrsw literal + // See TODOs for LDR + // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDRSW--literal---Load-Register-Signed-Word--literal-- + FLAMINGO_ABORT("LDRSW fixup not yet supported!"); + } break; + + // Handle pc-relative loads + case ARM64_INS_ADR: { + FLAMINGO_DEBUG("Fixing up ADR..."); + auto [reg, dst] = get_second_immediate(inst); + WriteAdr(reg, dst); + } break; + case ARM64_INS_ADRP: { + FLAMINGO_DEBUG("Fixing up ADRP..."); + auto [reg, dst] = get_second_immediate(inst); + WriteAdrp(reg, dst); + } break; + + // Otherwise, just write the instruction verbatim + default: + FLAMINGO_DEBUG("Fixing up UNKNOWN: {}...", inst.id); + Write(*reinterpret_cast(inst.bytes)); + break; + } + } +}; +} // namespace + +namespace flamingo { + +csh getHandle() { + static csh handle = 0; + static bool init = false; + if (!init) { + cs_err e1 = cs_open(CS_ARCH_ARM64, CS_MODE_ARM, &handle); + cs_err e2 = cs_option(handle, CS_OPT_DETAIL, 1); + if (e1 != CS_ERR_OK || e2 != CS_ERR_OK) { + FLAMINGO_ABORT("Capstone initialization failed: {}, {}", static_cast(e1), static_cast(e2)); + } + init = true; + } + return handle; +} + +cs_insn debugInst(uint32_t const* inst) { + cs_insn* insns = nullptr; + auto count = cs_disasm(getHandle(), reinterpret_cast(inst), sizeof(uint32_t), + static_cast(get_untagged_pc(reinterpret_cast(inst))), 1, &insns); + if (count == 1) { + return insns[0]; + } + return {}; +} + +void Fixups::PerformFixupsAndCallback() { + FLAMINGO_ASSERT(!target.addr.empty()); + FLAMINGO_ASSERT(!fixup_inst_destination.addr.empty()); + original_instructions.resize(target.addr.size()); + std::copy(target.addr.begin(), target.addr.end(), original_instructions.begin()); + // TODO: It is not thread safe to perform hooks on the same page as other threads! + // This is because we could have a fixup writer complete on one thread after the other has started. + // So, we want to lock on hook creation to ensure no one else is doing any type of hook creation, ideally. + + // Make the FixupContext instance that we will use for performing fixups + FixupContext context(fixup_inst_destination, target.addr); + + // Now, for each instruction at target + // Fix it up, maybe add an entry to the data block, maybe add an entry to the branch remapping + // Then, after that, write our callback to target.addr + // Finally, iterate our data ref tags and branch tags and edit the written fixups + // Flush our icache and protection is restored by the ProtectionWriter instance getting dtor'd + // The final layout should look something like: + // - Instructions... + // - Callback + // - Data section... + + cs_insn* insns = nullptr; + [[maybe_unused]] auto count = cs_disasm( + flamingo::getHandle(), reinterpret_cast(&target.addr[0]), target.addr.size_bytes(), + static_cast(get_untagged_pc(reinterpret_cast(&target.addr[0]))), target.addr.size(), &insns); + // We should never try to write fixups for something that isn't a valid instruction + // However, sometimes capstone isn't the latest version or whatever, so we don't assert here + // FLAMINGO_ASSERT(count == target.addr.size()); + + for (uint_fast16_t i = 0; i < target.addr.size(); i++) { + // For each input instruction, perform a fixup on it + auto const& inst = insns[i]; + auto current_inst_ptr = &target.addr[i]; + FLAMINGO_DEBUG("Fixup for inst: 0x{:x} at {}: {} {}, id: {}", *current_inst_ptr, fmt::ptr(current_inst_ptr), + fmt::string_view(inst.mnemonic, sizeof(inst.mnemonic)), + fmt::string_view(inst.op_str, sizeof(inst.op_str)), static_cast(inst.id)); + // For this incoming instruction, check to see if we have any forward references on this + // If we do, for each, rewrite the target instruction with the adjusted value + for (auto const& tag : context.branch_ref_map[i]) { + // Current PC is GetFixupPC() + // The instruction we emit's PC is the map from target --> fixup + // This difference is always positive, since we are jumping FORWARD + auto difference = static_cast(context.GetFixupPC()) - + get_untagged_pc(reinterpret_cast( + &fixup_inst_destination.addr[context.target_to_fixups[tag.target_index]])); + FLAMINGO_DEBUG("Performing deferred write at: {}, rewriting: {} with difference: {}", i, tag.target_index, + difference); + fixup_inst_destination.addr[context.target_to_fixups[tag.target_index]] = + (fixup_inst_destination.addr[context.target_to_fixups[tag.target_index]] & ~tag.imm_mask) | + (tag.imm_mask & ((difference >> tag.rshift) << tag.lshift)); + } + context.PerformFixupFor(inst, i, current_inst_ptr); + } + + // Free the disassembled instructions from before the fixups + cs_free(insns, target.addr.size()); + // Now, write the callback after all of our fixups. + context.WriteCallback(&target.addr[target.addr.size()]); + // After we have written ALL of our fixups initially AND our callback, perform our second pass where we inject + // immediate offsets. Most specifically, for data. To do this, we first start by laying out our data section directly, + // and marking the start address as "base". Then, we compute offsets based off of base + data_index * sizeof(uint32_t) + // - &fixups[fixup_idx] + auto data_base = context.GetFixupPC(); + for (auto const data : context.data_block) { + context.Write(data); + } + for (auto const& tag : context.data_ref_tags) { + int_fast16_t offset = static_cast(data_base + tag.data_index * sizeof(context.data_block[0]) - + get_untagged_pc(&fixup_inst_destination.addr[tag.fixup_index])); + fixup_inst_destination.addr[tag.fixup_index] = (fixup_inst_destination.addr[tag.fixup_index] & ~tag.imm_mask) | + (tag.imm_mask & ((offset >> tag.rshift) << tag.lshift)); + } + // Flush the icache for our fixups in case they were already cached from another hook call + __builtin___clear_cache(reinterpret_cast(&fixup_inst_destination.addr[0]), + reinterpret_cast(&fixup_inst_destination.addr[fixup_inst_destination.addr.size()])); +} + +void Fixups::Log() const { + // To log fixups, we walk the instructions and perform a translation for each +} + +// TODO: We should consider an optimization where we have a location for fixup data instead of inling all fixups. +// This would allow us to write out actual assembly verbatim and then have ldrs and whatnot for grabbing the data +// This would save a few instructions per all of the fixups, since we wouldn't need to branch over the data +// In addition, it would allow us to have a better time disassembling. +// It also shouldn't cost us any space whatsoever +// Though, if we perform any hooks AFTER the fact, we would need to properly expand both our data and our instruction +// space and THAT could be somewhat tricky. Perhaps a full recompile for a hook is actually preferred, though, if we +// know we need to leapfrog Since we would need to expand our trampoline and our original instructions regardless. +// TODO: Consider a full recompile and permit late installations + +} // namespace flamingo diff --git a/src/page-allocator.cpp b/src/page-allocator.cpp new file mode 100644 index 0000000..79d3def --- /dev/null +++ b/src/page-allocator.cpp @@ -0,0 +1,80 @@ +// Make a page +// Pages should have sizes and are otherwise 4kb +// If we do some allocation and determine that we didnt need the full set of allocation for our +// page, we want to give it back. For now, though, we should just allocate out the pages regardless +// And call a function to finalize our allocation pool or something on the allocator +// Note that in order to properly handle deallocation as well as finalization among other things, we need to handle: +// 1. When an allocation is "complete", shrink the allocation +// 2. Shrinking of allocations need to be done in such a way that future allocations are not broken. i.e. bump allocator +// 3. Deallocations need to be done in such a way that full pages are not destroyed +#include "page-allocator.hpp" +#include +#include +#include +#include "util.hpp" + +namespace { +constexpr auto AlignUp(auto offset, auto alignment) { + // We can't align to a size that is greater than our allocation + __builtin_assume(alignment < flamingo::Page::PageSize); + // Alignment must be a power of 2 + __builtin_assume((alignment != 0) && ((alignment & (alignment - 1)) == 0)); + if (offset % alignment != 0) { + return offset + (alignment - (offset % alignment)); + } + return offset; +} + +// We don't want to rely on the dlopen constructor calling this, we will allocate it on first call to Allocate. +// Hence, it's a pointer that we directly manage. +std::unordered_multimap* all_pages; +} // namespace + +namespace flamingo { + +PointerWrapper Allocate(uint_fast16_t alignment, uint_fast16_t size, PageProtectionType protection) { + // We assume that size is never > the size of a Page + // Note: This is NOT a thread safe allocator (for now) + __builtin_assume(size <= Page::PageSize); + if (all_pages == nullptr) { + all_pages = new std::unordered_multimap{}; + } + // We allocate first by trying to find a matching page that has space + for (auto& [perms, page] : *all_pages) { + if (perms == protection) { + // If we match the protection bits we set + // Check to see if we have enough free space for an allocation + auto start_offset = AlignUp(page.used_size, alignment); + if (Page::PageSize - start_offset >= size) { + // We have enough space to allocate within an existing page + page.used_size = start_offset + size; + return PointerWrapper( + std::span{ + reinterpret_cast(&reinterpret_cast(page.ptr)[start_offset]), + reinterpret_cast(&reinterpret_cast(page.ptr)[start_offset + size]) }, + protection); + } + } + } + // No page exists that has matching permissions and has enough space + // Make one. + void* ptr; + if (::posix_memalign(&ptr, Page::PageSize, Page::PageSize) != 0) { + // Log error on memalign allocation! + FLAMINGO_ABORT("Failed to allocate page of size: {} for size: {} with protection: {}. err: {}", Page::PageSize, + size, static_cast(protection), std::strerror(errno)); + } + // Mark full page for protection + if (::mprotect(ptr, Page::PageSize, static_cast(protection)) != 0) { + // Log error on mprotect! + FLAMINGO_ABORT("Failed to mark allocated page at: {} with permissions: {}. err: {}", fmt::ptr(ptr), + static_cast(protection), std::strerror(errno)); + } + auto const page = all_pages->emplace(protection, Page{ .ptr = ptr, .used_size = size, .protection = protection }); + return PointerWrapper( + std::span{ reinterpret_cast(&reinterpret_cast(page->second.ptr)[0]), + reinterpret_cast(&reinterpret_cast(page->second.ptr)[size]) }, + protection); +} + +} // namespace flamingo diff --git a/src/trampoline/trampoline-allocator.cpp b/src/trampoline/trampoline-allocator.cpp deleted file mode 100644 index 56d180c..0000000 --- a/src/trampoline/trampoline-allocator.cpp +++ /dev/null @@ -1,99 +0,0 @@ -#include "trampoline-allocator.hpp" -#include -#include -#include -#include "trampoline.hpp" -#include "util.hpp" - -namespace { -struct PageType { - void* ptr; - std::size_t used_size; - uint16_t trampoline_count; - - constexpr PageType(void* p, std::size_t used) : ptr(p), used_size(used), trampoline_count(1) {} -}; - -std::list pages; -constexpr static std::size_t PageSize = 4096; -} // namespace - -namespace flamingo { - -Trampoline TrampolineAllocator::Allocate(std::size_t trampolineSize) { - // Allocation should work by grabbing a full page at a time - // Then we mark the page as rwx - // Then we should be allowed to use anything on that page until we would need to make another - // (due to new trampoline being too big to fit) - // Repeat. - for (auto& page : pages) { - // If we have enough space in our page for this trampoline, squeeze it in! - if (PageSize - page.used_size > trampolineSize) { - // If we can fit in our existing page, we should try to - // Mark full page as rxw AGAIN (since we may have swapped it off before) - if (::mprotect(page.ptr, PageSize, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) { - // Log error on mprotect! - FLAMINGO_ABORT("Failed to mark allocated page at: {} as +rwx. err: {}", fmt::ptr(page.ptr), std::strerror(errno)); - } - // We have to be aligned 16 here - constexpr static auto kAlignment = 0x10U; - auto unaligned_start = reinterpret_cast(page.ptr) + page.used_size; - uintptr_t aligned_start; - if (unaligned_start % kAlignment != 0) { - // Pad upwards - auto pad_bytes = (kAlignment - (unaligned_start % kAlignment)); - aligned_start = unaligned_start + pad_bytes; - page.used_size += pad_bytes; - } else { - aligned_start = unaligned_start; - } - Trampoline to_ret(reinterpret_cast(aligned_start), trampolineSize / sizeof(uint32_t), page.used_size); - page.used_size += trampolineSize; - page.trampoline_count++; - // Log allocated trampoline here - return to_ret; - } - } - // No pages with enough space available. - void* ptr; - if (::posix_memalign(&ptr, PageSize, PageSize) != 0) { - // Log error on memalign allocation! - FLAMINGO_ABORT("Failed to allocate trampoline page of size: {} for size: {}. err: {}", PageSize, trampolineSize, std::strerror(errno)); - } - // Mark full page as rxw - if (::mprotect(ptr, PageSize, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) { - // Log error on mprotect! - FLAMINGO_ABORT("Failed to mark allocated page at: {} as +rwx. err: {}", fmt::ptr(ptr), std::strerror(errno)); - } - auto& page = pages.emplace_back(ptr, trampolineSize); - // Convert from trampoline size to number of instructions in the trampoline - return { static_cast(ptr), trampolineSize / sizeof(uint32_t), page.used_size }; -} - -void TrampolineAllocator::Free(Trampoline const& toFree) { - // Freeing a trampoline should decrease the page it was allocated on's size by a known amount - // If we reach a point where a trampoline was deallocated on a page and it was the last one in that page, then we should - // 1. Mark the page as read/write only - // 2. deallocate the page - - // Find page we are allocated on - auto page_addr = reinterpret_cast(reinterpret_cast(toFree.address.data()) & ~(PageSize - 1)); - for (auto& p : pages) { - if (p.ptr == page_addr) { - p.trampoline_count--; - if (p.trampoline_count == 0) { - if (::mprotect(p.ptr, PageSize, PROT_READ) != 0) { - // Log error on mprotect - FLAMINGO_ABORT("Failed to mark page at: {} as read only. err: {}", fmt::ptr(p.ptr), std::strerror(errno)); - } - ::free(p.ptr); - } - return; - } - } - // If we get here, we couldn't free the provided Trampoline! - FLAMINGO_ABORT("Failed to free trampoline at: {}, no matching page with page addr: {}!", fmt::ptr(toFree.address.data()), - fmt::ptr(page_addr)); -} - -} // namespace flamingo diff --git a/src/trampoline/trampoline.cpp b/src/trampoline/trampoline.cpp deleted file mode 100644 index 81e673e..0000000 --- a/src/trampoline/trampoline.cpp +++ /dev/null @@ -1,572 +0,0 @@ -#include "trampoline.hpp" -#include -#include -#include -#include -#include "capstone/shared/capstone/capstone.h" -#include "capstone/shared/capstone/platform.h" -#include "util.hpp" - -namespace flamingo { - -// TODO: We should consider an optimization where we have a location for fixup data instead of inling all fixups. -// This would allow us to write out actual assembly verbatim and then have ldrs and whatnot for grabbing the data -// This would save a few instructions per all of the fixups, since we wouldn't need to branch over the data -// In addition, it would allow us to have a better time disassembling. -// It also shouldn't cost us any space whatsoever -// Though, if we perform any hooks AFTER the fact, we would need to properly expand both our data and our instruction space -// and THAT could be somewhat tricky. -// Perhaps a full recompile for a hook is actually preferred, though, if we know we need to leapfrog -// Since we would need to expand our trampoline and our original instructions regardless. -// TODO: Consider a full recompile and permit late installations - -// Helper types for holding immediate masks, lshifts and rshifts for conversions to immediates from PC differences -template -struct BranchImmTypeTrait; - -template - requires(type == ARM64_INS_B || type == ARM64_INS_BL) -struct BranchImmTypeTrait { - constexpr static uint32_t imm_mask = 0b00000011111111111111111111111111U; - constexpr static uint32_t lshift = 0; - constexpr static uint32_t rshift = 2; -}; - -template - requires(type == ARM64_INS_CBZ || type == ARM64_INS_CBNZ) -struct BranchImmTypeTrait { - constexpr static uint32_t imm_mask = 0b00000000111111111111111111100000; - constexpr static uint32_t lshift = 5; - constexpr static uint32_t rshift = 2; -}; - -template - requires(type == ARM64_INS_TBZ || type == ARM64_INS_TBNZ) -struct BranchImmTypeTrait { - constexpr static uint32_t imm_mask = 0b00000000000001111111111111100000; - constexpr static uint32_t lshift = 5; - constexpr static uint32_t rshift = 2; -}; - -/// @brief Helper function that returns an encoded b for a particular offset -consteval uint32_t get_b(int offset) { - constexpr uint32_t b_opcode = 0b00010100000000000000000000000000U; - return (b_opcode | (static_cast(offset) >> 2U)); -} - -constexpr int64_t get_untagged_pc(uint64_t pc) { - // Upper byte is tagged for PC addresses on android 11+ - constexpr uint64_t mask = ~(0xFFULL << (64U - 8U)); - return static_cast(static_cast(pc) & mask); -} - -void Trampoline::Write(uint32_t instruction) { - FLAMINGO_ASSERT(instruction_count + 1 <= num_insts); - // Log what we are writing (and also our state) - address[instruction_count] = instruction; - instruction_count++; -} - -void Trampoline::WriteData(void const* ptr) { - // TODO: Write this to a different buffer and return a pointer to it - // This would allow the control flow for a hook to be much cleaner and the data section to be well defined - FLAMINGO_ASSERT(instruction_count + sizeof(void*) / sizeof(uint32_t) <= num_insts); - // Log what we are writing (and also our state) - *reinterpret_cast(&address[instruction_count]) = ptr; - instruction_count += sizeof(void*) / sizeof(uint32_t); -} - -void Trampoline::WriteData(void const* ptr, uint32_t size) { - // TODO: Write this to a different buffer and return a pointer to it - FLAMINGO_ASSERT((size + instruction_count) <= num_insts); - FLAMINGO_DEBUG("Writing data from: {} of size: {}", ptr, size * sizeof(uint32_t)); - std::memcpy(&address[instruction_count], ptr, size * sizeof(uint32_t)); - instruction_count += size; -} - -void Trampoline::WriteB(int64_t imm) { - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/B--Branch- - WriteCallback(reinterpret_cast(imm)); -} - -void Trampoline::WriteBl(int64_t imm) { - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/BL--Branch-with-Link- - constexpr uint32_t branch_imm_mask = 0b00000011111111111111111111111111U; - int64_t pc = get_untagged_pc(reinterpret_cast(&address[instruction_count])); - int64_t delta = imm - pc; - if (std::llabs(delta) > (branch_imm_mask << 1) + 1) { - // Too far to emit a b. Emit a br instead. - // We cannot emit a blr here because the pc + 4 for return will be in our offset. - // LDR X17, #12 - constexpr uint32_t ldr_x17 = 0x58000071U; - Write(ldr_x17); - // ADR X30, #16 - constexpr uint32_t adr_x30 = 0x1000009EU; - Write(adr_x30); - // BR x17 - constexpr uint32_t br_x17 = 0xD61F0220U; - Write(br_x17); - // Data - WriteData(reinterpret_cast(imm)); - } else { - // Small enough to emit a b/bl. - // bl opcode | encoded immediate (delta >> 2) - // Note, abs(delta >> 2) must be < (1 << 26) - constexpr uint32_t bl_opcode = 0b10010100000000000000000000000000U; - Write(bl_opcode | ((delta >> 2) & branch_imm_mask)); - } -} - -void Trampoline::WriteAdr(uint8_t reg, int64_t imm) { - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/ADR--Form-PC-relative-address-?lang=en - constexpr uint32_t adr_maximum_imm = 0b00000000000111111111111111111111U; - constexpr uint32_t reg_mask = 0b11111; - int64_t pc = get_untagged_pc(reinterpret_cast(&address[instruction_count])); - int64_t delta = imm - pc; - if (std::llabs(delta) >= (adr_maximum_imm >> 1U)) { - // Too far to emit just an adr. - // LDR (used register), #0x8 - constexpr uint32_t ldr_mask = 0b01011000000000000000000000000000U; - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- - // imm is encoded as << 2, LSB just to the right of reg - constexpr uint32_t ldr_imm = (8U >> 2U) << 5U; - Write(ldr_mask | ldr_imm | (reg_mask & reg)); - // B #0xC - constexpr uint32_t b_0xc = 0x14000003U; - static_assert(b_0xc == get_b(0xC)); - Write(get_b(0xC)); - // Immediate data - WriteData(reinterpret_cast(imm)); - } else { - // Close enough to emit an adr. - // Note that delta should be within +-1 MB - constexpr uint32_t adr_opcode = 0b00010000000000000000000000000000; - // Get immlo - uint32_t imm_lo = ((static_cast(delta) & 3U) << 29); - // Get immhi - uint32_t imm_hi = (static_cast(delta) >> 2U) << 5; - Write(adr_opcode | imm_lo | imm_hi | (reg_mask & reg)); - } -} - -void Trampoline::WriteAdrp(uint8_t reg, int64_t imm) { - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/ADR--Form-PC-relative-address-?lang=en - // constexpr uint32_t adr_maximum_imm = 0b00000000000111111111111111111111U; - constexpr uint32_t reg_mask = 0b11111; - constexpr uint32_t pc_imm_mask = ~0b111111111111U; - constexpr int64_t adrp_maximum_imm = 0xFFFFF000U; - int64_t pc = get_untagged_pc(reinterpret_cast(&address[instruction_count])); - int64_t delta = (pc & pc_imm_mask) - imm; - if (std::llabs(delta) >= adrp_maximum_imm) { - // Too far to emit just an adr. - // LDR (used register), #0x8 - constexpr uint32_t ldr_mask = 0b01011000000000000000000000000000U; - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- - // imm is encoded as << 2, LSB just to the right of reg - constexpr uint32_t ldr_imm = (8U >> 2U) << 5; - Write(ldr_mask | ldr_imm | (reg_mask & reg)); - // B #0xC - constexpr uint32_t b_0xc = 0x14000003U; - // TODO: Remove this assertion - static_assert(b_0xc == get_b(0xC)); - Write(get_b(0xC)); - // Write(b_0xc); - // Immediate data - WriteData(reinterpret_cast(imm)); - } else { - // Close enough to emit an adrp. - // Note that delta should be within +-4 GB - constexpr uint32_t adrp_opcode = 0b10010000000000000000000000000000U; - // Imm is << 12 in parse of instruction - delta >>= 12; - // Get immlo - uint32_t imm_lo = ((static_cast(delta) & 3) << 29); - // Get immhi - uint32_t imm_hi = (static_cast(delta) >> 2) << 5; - Write(adrp_opcode | imm_lo | imm_hi | (reg_mask & reg)); - } -} - -void Trampoline::WriteLdr(uint32_t inst, uint8_t reg, int64_t imm) { - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- - constexpr uint32_t reg_mask = 0b11111; - // 20 bits because signed range is only allowed - // constexpr int64_t max_ldr_range = (1LL << 20); - if ((inst & 0xFF000000U) == 0xD8000000U) { - // This is a prefetch instruction. - // Lets just skip it. - return; - } - // int64_t pc = get_untagged_pc(reinterpret_cast(&address[instruction_count])); - // int64_t delta = imm - pc; - // TODO: Note missed optimization opportunity - // TODO: Should perform a small LDR - FLAMINGO_DEBUG("Potentially missed optimization opportunity for near LDRs!"); - - // if (std::llabs(delta) >= max_ldr_range) { - - // Too far to emit an equivalent LDR - // Fallback to performing a direct memory write/read - // LDR (used register), #0x8 - constexpr uint32_t ldr_mask = 0b01011000000000000000000000000000U; - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- - // imm is encoded as << 2, LSB just to the right of reg - constexpr uint32_t ldr_imm = (8U >> 2U) << 5; - Write(ldr_mask | ldr_imm | (reg_mask & reg)); - // B #0xC - Write(get_b(0xC)); - // Immediate data - // 4, 8, 16(?) for our sizes - constexpr uint32_t size_mask = 0x40000000U; - // TODO: Pull the start address from this data write - WriteData(reinterpret_cast(imm), 1); - if ((inst & size_mask) != 0) { - WriteData(reinterpret_cast(imm + sizeof(uint32_t)), 1); - } - - // } else { - // // Close enough to emit an LDR - // FLAMINGO_ABORT("Close LDR optimization is not implemented (with no fallback)!"); - // } -} - -template -void WriteCondBranch(Trampoline& value, uint32_t instruction, int64_t imm) { - uint32_t imm_mask; - if constexpr (imm_19) { - // Imm 19 - constexpr uint32_t imm_mask_19 = 0b00000000111111111111111111100000; - imm_mask = imm_mask_19; - } else { - // Imm 14 - constexpr uint32_t imm_mask_14 = 0b00000000000001111111111111100000; - imm_mask = imm_mask_14; - } - int64_t pc = get_untagged_pc(reinterpret_cast(&value.address[value.instruction_count])); - int64_t delta = imm - pc; - // imm_mask >> 1 for maximum positive value - // << 2 because branch imms are << 2 - // >> 5 because the mask is too high - if (std::llabs(delta) < (imm_mask >> 4)) { - // Small enough to optimize, just write the instruction - // But with the modified offset - // Delta should be >> 2 for branch imm - // Then << 5 to be in the correct location - value.Write((instruction & ~imm_mask) | (static_cast((delta >> 2) << 5) & imm_mask)); - } else { - // Otherwise, we need to write the same expression but with a known offset - // Specifically, write the instruction but with an offset of 8 - // 2, because 8 >> 2 is 2 - // << 5 to place in correct location for immediate - value.Write((instruction & ~imm_mask) | ((2 << 5) & imm_mask)); - value.Write(get_b(0x14)); - value.WriteLdrBrData(reinterpret_cast(imm)); - } -} - -void Trampoline::WriteLdrBrData(uint32_t const* target) { - // LDR x17, 0x8 - constexpr uint32_t ldr_x17 = 0x58000051U; - Write(ldr_x17); - // BR x17 - constexpr uint32_t br_x17 = 0xD61F0220U; - Write(br_x17); - // Data - WriteData(target); -} - -auto get_branch_immediate(cs_insn const& inst) { - FLAMINGO_ASSERT(inst.detail->arm64.op_count == 1); - return inst.detail->arm64.operands[0].imm; -} - -std::pair get_second_immediate(cs_insn const& inst) { - // register is just bottom 5 bits - constexpr uint32_t reg_mask = 0b11111; - FLAMINGO_ASSERT(inst.detail->arm64.op_count == 2); - return { *reinterpret_cast(inst.bytes) & reg_mask, inst.detail->arm64.operands[1].imm }; -} - -std::pair get_last_immediate(cs_insn const& inst) { - // register is just bottom 5 bits - constexpr uint32_t reg_mask = 0b11111; - FLAMINGO_ASSERT(inst.detail->arm64.op_count >= 2); - return { *reinterpret_cast(inst.bytes) & reg_mask, inst.detail->arm64.operands[inst.detail->arm64.op_count - 1].imm }; -} - -csh getHandle() { - static csh handle = 0; - if (!handle) { - cs_err e1 = cs_open(CS_ARCH_ARM64, CS_MODE_ARM, &handle); - cs_option(handle, CS_OPT_DETAIL, 1); - if (e1) { - FLAMINGO_ABORT("Capstone initialization failed"); - } - } - return handle; -} - -cs_insn debugInst(uint32_t const* inst) { - cs_insn* insns = nullptr; - auto count = cs_disasm(getHandle(), reinterpret_cast(inst), sizeof(uint32_t), - static_cast(get_untagged_pc(reinterpret_cast(inst))), 1, &insns); - if (count == 1) { - return insns[0]; - } - return {}; -} - -struct BranchReferenceTag { - /// @brief The immediate mask to use when rewriting the instruction - uint32_t imm_mask; - /// @brief The amount to shift the immediate to the left to encode it correctly such that the mask would be valid - uint32_t lshift; - /// @brief The amount to shift the immediate to the right to encode it correctly such that the mask would be valid - uint32_t rshift; - /// @brief Index to overwrite - uint32_t target_index; -}; - -template -bool TryDeferBranch(Trampoline& self, uint16_t i, int64_t dst, int64_t target_start, int64_t target_end, uint32_t inst, - std::array const& target_to_fixups, std::array, Sz>& branch_ref_map) { - // If we are within OUR fixup range, that's when things get interesting. - // TODO: If we are in SOME OTHER TRAMPOLINE'S fixup range, then we should use their call - using trait_t = BranchImmTypeTrait; - constexpr uint32_t imm_mask = trait_t::imm_mask; - constexpr uint32_t lshift = trait_t::lshift; - constexpr uint32_t rshift = trait_t::rshift; - if (dst < target_end && dst >= target_start) { - FLAMINGO_DEBUG("Potentially deferring branch at: {} because it is within: {} and {}", dst, target_start, target_end); - auto target_offset = (dst - target_start) / sizeof(uint32_t); - FLAMINGO_ASSERT(target_offset < target_to_fixups.size()); - FLAMINGO_ASSERT(target_offset < branch_ref_map.size()); - // Always emit the instruction with AN immediate. - // For forward references, we need to defer. - // This difference could be negative, but for those cases we will defer and overwrite. - auto fixup_difference = - static_cast(get_untagged_pc(reinterpret_cast(&self.address[self.instruction_count])) - - get_untagged_pc(reinterpret_cast(&self.address[target_to_fixups[target_offset]]))); - self.Write((inst & ~imm_mask) | ((fixup_difference >> rshift) << lshift)); - if (target_offset > i) { - FLAMINGO_DEBUG("Deferring at: {} with target offset: {}", i, target_offset); - // Need to defer. - // Deference SHOULD never cause the instruction being deferred to expand in size. - // It should always be possible to point the deferred instruction to the new one without emitting more instructions - branch_ref_map[target_offset].push_back({ - .imm_mask = imm_mask, - .lshift = lshift, - .rshift = rshift, - .target_index = i, - }); - } - return true; - } - return false; -} - -template -void Trampoline::WriteFixups(uint32_t const* target) { - FLAMINGO_ASSERT(target); - // Copy over original instructions - FLAMINGO_DEBUG("Fixing up: {} instructions!", countToFixup); - original_instructions.resize(countToFixup); - std::memcpy(original_instructions.data(), target, countToFixup * sizeof(uint32_t)); - // The end of the fixups is used for referential branches - auto target_start = get_untagged_pc(reinterpret_cast(target)); - auto target_end = get_untagged_pc(reinterpret_cast(&target[countToFixup])); - // Disassemble the instructions into this pointer, which we can then index into per instruction - cs_insn* insns = nullptr; - auto count = cs_disasm(getHandle(), reinterpret_cast(target), sizeof(uint32_t) * countToFixup, - static_cast(get_untagged_pc(reinterpret_cast(target))), countToFixup, &insns); - // We should never try to write fixups for something that isn't a valid instruction - FLAMINGO_ASSERT(count == countToFixup); - // We use this set to track referential branches going forwards - // If it is a branch, check to see if the target immediate would place us within our fixup range - // If so, we need to: - // - If the target is behind us, use the new target directly - // - If the target is in front of us, defer the write until later. - // We defer the write by basically writing the branch itself (since it must be a close branch) - // and then add its index (and its immediate mask and shift amount) to some set. - // Then, when we start the fixup for an instruction, we check the set to see if we should go back and fix the specified indices. - // To fix them, we simply walk all of the indices we wish to fix, and for each: - // - Current PC of instruction - &target[index] to replace - // - Use as argument for shift + mask? - - // TODO: Avoid heap alloc if possible - std::array, countToFixup> branch_ref_map{}; - // Holds the mapping of target index to fixup index - // TODO: Make this a publicly exposed member? - // TODO: Technically, we need to see if ANY branch target would leave us in ANY fixup block... - // TODO: Should collect a set of references so that if we ever install a hook over somewhere we would jump to we would force a recompile - // We should check against the full set of trampolines for this - std::array target_to_fixups{}; - - for (uint16_t i = 0; i < countToFixup; i++) { - // If it is a branch, check to see if the target immediate would place us within our fixup range - // If so, we need to: - // - If the target is behind us, use the new target directly - // - If the target is in front of us, defer the write until later. - // We defer the write by basically writing the branch itself (since it must be a close branch) - // and then add its index (and its immediate mask and shift amount) to some set. - // Then, when we start the fixup for an instruction, we check the set to see if we should go back and fix the specified indices. - // To fix them, we simply walk all of the indices we wish to fix, and for each: - // - Current PC of instruction - &target[index] to replace - // - Use as argument for shift + mask? - - // For each input instruction, perform a fixup on it - auto const& inst = insns[i]; - auto current_inst_ptr = &target[i]; - FLAMINGO_DEBUG("Fixup for inst: 0x{:x} at {}: {} {}, id: {}", *current_inst_ptr, fmt::ptr(current_inst_ptr), - fmt::string_view(inst.mnemonic, sizeof(inst.mnemonic)), fmt::string_view(inst.op_str, sizeof(inst.op_str)), - static_cast(inst.id)); - // For this incoming instruction, check to see if we have any forward references on this - // If we do, for each, rewrite the target instruction with the adjusted value - for (auto const& tag : branch_ref_map[i]) { - // Current PC is get_untagged_pc(&address[instruction_count]) - // The instruction we emit's PC is the map from target --> fixup - // This difference is always positive, since we are jumping FORWARD - auto difference = static_cast(get_untagged_pc(reinterpret_cast(&address[instruction_count])) - - get_untagged_pc(reinterpret_cast(&address[target_to_fixups[tag.target_index]]))); - FLAMINGO_DEBUG("Performing deferred write at: {}, rewriting: {} with difference: {}", i, tag.target_index, difference); - address[target_to_fixups[tag.target_index]] = - (address[target_to_fixups[tag.target_index]] & ~tag.imm_mask) | (tag.imm_mask & ((difference >> tag.rshift) << tag.lshift)); - } - // Set the target map entry for this incoming instruction to the current offset of the output - target_to_fixups[i] = instruction_count; - switch (inst.id) { - // Handle fixups for branch immediate - case ARM64_INS_B: { - FLAMINGO_DEBUG("Fixing up B..."); - auto dst = get_branch_immediate(inst); - if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, branch_ref_map)) { - if (inst.detail->arm64.cc != ARM64_CC_INVALID) { - WriteCondBranch(*this, *current_inst_ptr, dst); - } else { - WriteB(dst); - } - } - } break; - case ARM64_INS_BL: { - FLAMINGO_DEBUG("Fixing up BL..."); - auto dst = get_branch_immediate(inst); - if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, branch_ref_map)) { - WriteBl(dst); - } - } break; - - // Handle fixups for conditional branches - case ARM64_INS_CBNZ: - case ARM64_INS_CBZ: { - FLAMINGO_DEBUG("Fixing up CBNZ/CBZ..."); - auto [reg, dst] = get_last_immediate(inst); - if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, branch_ref_map)) { - WriteCondBranch(*this, *current_inst_ptr, dst); - } - } break; - case ARM64_INS_TBNZ: - case ARM64_INS_TBZ: { - FLAMINGO_DEBUG("Fixing up TBNZ/TBZ..."); - auto [reg, dst] = get_last_immediate(inst); - if (!TryDeferBranch(*this, i, dst, target_start, target_end, *current_inst_ptr, target_to_fixups, branch_ref_map)) { - WriteCondBranch(*this, *current_inst_ptr, dst); - } - } break; - - // Handle fixups for load literals - case ARM64_INS_LDR: { - FLAMINGO_DEBUG("Fixing up LDR..."); - // TODO: Handle the case where an LDR would land in a fixup range, so we need to copy the raw values - constexpr uint32_t b_31 = 0b10000000000000000000000000000000; - constexpr uint32_t ldr_lit_opc_mask = 0b10111111000000000000000000000000; - if ((*current_inst_ptr & ldr_lit_opc_mask) == 0b00011000000000000000000000000000) { - // This is an ldr literal - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDR--literal---Load-Register--literal-- - auto [reg, dst] = get_second_immediate(inst); - WriteLdr(*current_inst_ptr, reg, dst); - } else if ((*current_inst_ptr & (ldr_lit_opc_mask & ~b_31)) == 0b00011100000000000000000000000000) { - // This is an ldr literal, SIMD - // https://developer.arm.com/documentation/ddi0596/2021-12/SIMD-FP-Instructions/LDR--literal--SIMD-FP---Load-SIMD-FP-Register--PC-relative-literal-- - FLAMINGO_ABORT("LDR of the SIMD variant is not yet supported!"); - } else { - // This is an LDR that doesn't need to be fixed up - FLAMINGO_DEBUG("Fixing up standard LDR..."); - Write(*reinterpret_cast(inst.bytes)); - } - } break; - case ARM64_INS_LDRSW: { - // This is an ldrsw literal - // See TODOs for LDR - // https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDRSW--literal---Load-Register-Signed-Word--literal-- - FLAMINGO_ABORT("LDRSW fixup not yet supported!"); - } break; - - // Handle pc-relative loads - case ARM64_INS_ADR: { - FLAMINGO_DEBUG("Fixing up ADR..."); - auto [reg, dst] = get_second_immediate(inst); - WriteAdr(reg, dst); - } break; - case ARM64_INS_ADRP: { - FLAMINGO_DEBUG("Fixing up ADRP..."); - auto [reg, dst] = get_second_immediate(inst); - WriteAdrp(reg, dst); - } break; - - // Otherwise, just write the instruction verbatim - default: - FLAMINGO_DEBUG("Fixing up UNKNOWN: {}...", inst.id); - Write(*reinterpret_cast(inst.bytes)); - break; - } - } - - // Free the disassembled instructions from before the fixups - cs_free(insns, countToFixup); -} - -void Trampoline::WriteHookFixups(uint32_t const* target) { - // Write the fixups for a standard hook. - // Ideally, this is hidden entirely and only the hook API can do this - WriteFixups<4>(target); -} - -void Trampoline::WriteCallback(uint32_t const* target) { - constexpr uint32_t branch_imm_mask = 0b00000011111111111111111111111111U; - int64_t pc = get_untagged_pc(reinterpret_cast(&address[instruction_count])); - auto delta = reinterpret_cast(target) - pc; - if (std::llabs(delta) > (branch_imm_mask << 1) + 1) { - // Too far for b. Emit a br instead. - WriteLdrBrData(target); - } else { - // Small enough to emit a b. - // b opcode | encoded immediate (delta >> 2) - // Note, abs(delta >> 2) must be < (1 << 26) - constexpr uint32_t b_opcode = 0b00010100000000000000000000000000U; - Write(b_opcode | ((delta >> 2) & branch_imm_mask)); - } -} - -void Trampoline::Finish() { - auto bytes_to_subtract = (num_insts - instruction_count) * sizeof(uint32_t); - FLAMINGO_DEBUG("Completed trampoline allocation of: {} instructions! Out of: {} Subtracting: {} with page size: {}", instruction_count, - num_insts, bytes_to_subtract, pageSizeRef); - FLAMINGO_ASSERT(bytes_to_subtract < pageSizeRef); - pageSizeRef -= bytes_to_subtract; - // Reset memory protection to be r+x to avoid potential overwrites - constexpr static auto kPageSize = 0x1000ULL; - auto* page_aligned_target = reinterpret_cast(reinterpret_cast(address.data()) & ~(kPageSize - 1)); - FLAMINGO_DEBUG("Marking target: {} as NON writable, page aligned: {}", fmt::ptr(address.data()), fmt::ptr(page_aligned_target)); - if (::mprotect(page_aligned_target, kPageSize, PROT_READ | PROT_EXEC) != 0) { - // Log error on mprotect! - FLAMINGO_ABORT("Failed to mark: {} (page aligned: {}) as +rwx. err: {}", fmt::ptr(address.data()), fmt::ptr(page_aligned_target), - std::strerror(errno)); - } -} - -void Trampoline::Log() { - // TODO: Log the trampoline and various information here - // This will probably be necessary given the potential for failure -} - -} // namespace flamingo diff --git a/test/main.cpp b/test/main.cpp index 919f356..0428a0a 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -1,146 +1,138 @@ // Main test runner for testing fixups behave as intended +#include #include #include +#include #include #include +#include -#include "../shared/trampoline-allocator.hpp" -#include "../shared/trampoline.hpp" +#include "../shared/fixups.hpp" +#include "../shared/page-allocator.hpp" #include "capstone/capstone.h" -decltype(auto) test_near(uint32_t* target, uint32_t const* callback) { - constexpr size_t hookSize = 32; - constexpr size_t pageSize = 4096; - constexpr size_t trampolineSize = 64; - static std::array test_trampoline; - size_t page_size = pageSize; - printf("TRAMPOLINE: %p\n", test_trampoline.data()); - flamingo::Trampoline trampoline(test_trampoline.data(), sizeof(test_trampoline), page_size); - // Attempt to write a hook from target --> callback (just for testing purposes) - std::size_t trampoline_size = hookSize; - // Hook size is 5, but we only fixup 4 - trampoline.WriteHookFixups(target); - // Write the jump back to instruction 5 - trampoline.WriteCallback(&target[4]); - trampoline.Finish(); - // Write actual hook to be a callback - // We need to mark the location of target as writable (so we can write to it correctly) - ::mprotect(target, hookSize, PROT_READ | PROT_WRITE | PROT_EXEC); - ::flamingo::Trampoline targetHook(target, hookSize, trampoline_size); - targetHook.WriteCallback(callback); - targetHook.Finish(); - return &test_trampoline; -} -void print_decode_loop(uint32_t* val, int n) { - auto handle = flamingo::getHandle(); - for (int i = 0; i < n; i++) { - cs_insn* insns = nullptr; - auto count = cs_disasm(handle, reinterpret_cast(val), sizeof(uint32_t), - static_cast(reinterpret_cast(val)), 1, &insns); - if (count == 1) { - printf("Addr: %p Value: 0x%x, %s %s\n", val, *val, insns[0].mnemonic, insns[0].op_str); - } else { - printf("Addr: %p Value: 0x%x\n", val, *val); - } - val++; +void print_decode_loop(std::span data) { + auto handle = flamingo::getHandle(); + for (size_t i = 0; i < data.size(); i++) { + cs_insn* insns = nullptr; + auto count = cs_disasm(handle, reinterpret_cast(&data[i]), sizeof(uint32_t), + reinterpret_cast(&data[i]), 1, &insns); + if (count == 1) { + printf("Addr: %p Value: 0x%08x, %s %s\n", &data[i], data[i], insns[0].mnemonic, insns[0].op_str); + } else { + printf("Addr: %p Value: 0x%08x\n", &data[i], data[i]); } + } } -void perform_near_hook_test(uint8_t* to_hook) { - printf("TO HOOK: %p\n", to_hook); - print_decode_loop(reinterpret_cast(to_hook), 6); - puts("TEST NEAR..."); - auto* trampoline_data = test_near(reinterpret_cast(to_hook), (const uint32_t*)(0xDEADBEEFBAADF00DULL)); - // Use 20 here as a reasonable guesstimate - print_decode_loop(trampoline_data->data(), 20); - puts("HOOKED:"); - print_decode_loop(reinterpret_cast(to_hook), 6); -} - -decltype(auto) test_far(uint32_t* target, uint32_t const* callback) { - constexpr size_t hookSize = 32; - constexpr size_t trampolineSize = 64; - auto trampoline = flamingo::TrampolineAllocator::Allocate(trampolineSize); - printf("TRAMPOLINE: %p\n", trampoline.address.data()); - // Attempt to write a hook from target --> callback (just for testing purposes) - std::size_t trampoline_size = hookSize; - // Hook size is 5, but we only fixup 4 - trampoline.WriteHookFixups(target); - // Write the jump back to instruction 5 - trampoline.WriteCallback(&target[4]); - trampoline.Finish(); - // Write actual hook to be a callback - // We need to mark the location of target as writable (so we can write to it correctly) - ::mprotect(target, hookSize, PROT_READ | PROT_WRITE | PROT_EXEC); - ::flamingo::Trampoline targetHook(target, hookSize, trampoline_size); - targetHook.WriteCallback(callback); - targetHook.Finish(); - return trampoline; +decltype(auto) test_near(uint32_t* target, [[maybe_unused]] uint32_t const* callback) { + constexpr size_t hookSizeNumInsts = 5; + constexpr size_t trampolineSize = 64; + static std::array test_trampoline; + printf("TRAMPOLINE: %p\n", test_trampoline.data()); + flamingo::Fixups fixups{ + .target = flamingo::PointerWrapper(std::span{ target, &target[hookSizeNumInsts - 1] }, + flamingo::PageProtectionType::kExecute | + flamingo::PageProtectionType::kRead | + flamingo::PageProtectionType::kWrite), + .fixup_inst_destination = flamingo::PointerWrapper( + test_trampoline, flamingo::PageProtectionType::kExecute | flamingo::PageProtectionType::kRead | + flamingo::PageProtectionType::kWrite), + }; + fixups.PerformFixupsAndCallback(); + return fixups; } -void perform_far_hook_test(uint8_t* to_hook) { - printf("TO HOOK: %p\n", to_hook); - print_decode_loop(reinterpret_cast(to_hook), 6); - puts("TEST FAR..."); - auto trampoline = test_far(reinterpret_cast(to_hook), (const uint32_t*)(0xDEADBEEFBAADF00DULL)); - // Use 20 here as a reasonable guesstimate - print_decode_loop(trampoline.address.data(), 20); - puts("HOOKED:"); - print_decode_loop(reinterpret_cast(to_hook), 6); +void perform_near_hook_test(std::span to_hook) { + std::span hook_span = std::span(reinterpret_cast(&to_hook[0]), + reinterpret_cast(&to_hook[to_hook.size()])); + printf("TO HOOK: %p\n", to_hook.data()); + print_decode_loop(hook_span); + puts("TEST NEAR..."); + auto trampoline_data = test_near(hook_span.data(), (uint32_t const*)(0xDEADBEEFBAADF00DULL)); + // Use 20 here as a reasonable guesstimate + print_decode_loop(trampoline_data.fixup_inst_destination.addr); + puts("HOOKED:"); + print_decode_loop(hook_span); } -void test_near_no_fixups() { - puts("Testing near -- no fixups!"); - static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8, 0xf6, 0x57, 0x01, 0xa9, 0xf4, 0x4f, 0x02, 0xa9, 0xfd, 0x7b, 0x03, - 0xa9, 0xfd, 0xc3, 0x00, 0x91, 0x48, 0x18, 0x40, 0xf9, 0x16, 0xd4, 0x42, 0xa9, 0xf3, 0x03, - 0x02, 0xaa, 0xf4, 0x03, 0x01, 0xaa, 0x17, 0x01, 0x40, 0xf9, 0xe8, 0xba, 0x44, 0x39 }; - perform_near_hook_test(to_hook); +decltype(auto) test_far(uint32_t* target, [[maybe_unused]] uint32_t const* callback) { + constexpr size_t hookSizeNumInsts = 5; + constexpr size_t trampolineSize = 64; + // We allocate the page with r-x perms, we will mark it as writable when we do the writes and otherwise put it back to + // this state. + auto fixup_ptr = flamingo::Allocate(16, trampolineSize * sizeof(uint32_t), + flamingo::PageProtectionType::kExecute | flamingo::PageProtectionType::kRead); + flamingo::Fixups fixups{ + .target = flamingo::PointerWrapper( + std::span{ target, &target[hookSizeNumInsts - 1] }, + flamingo::PageProtectionType::kExecute | flamingo::PageProtectionType::kRead), + .fixup_inst_destination = fixup_ptr, + }; + printf("TRAMPOLINE: %p\n", &fixup_ptr.addr[0]); + // Attempt to write a hook from target --> callback (just for testing purposes) + // Hook size is 5, but we only fixup 4 + fixups.PerformFixupsAndCallback(); + return fixups; } -void test_far_no_fixups() { - puts("Testing far -- no fixups!"); - static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8, 0xf6, 0x57, 0x01, 0xa9, 0xf4, 0x4f, 0x02, 0xa9, 0xfd, 0x7b, 0x03, - 0xa9, 0xfd, 0xc3, 0x00, 0x91, 0x48, 0x18, 0x40, 0xf9, 0x16, 0xd4, 0x42, 0xa9, 0xf3, 0x03, - 0x02, 0xaa, 0xf4, 0x03, 0x01, 0xaa, 0x17, 0x01, 0x40, 0xf9, 0xe8, 0xba, 0x44, 0x39 }; - perform_far_hook_test(to_hook); +void perform_far_hook_test(std::span to_hook) { + std::span hook_span = std::span(reinterpret_cast(&to_hook[0]), + reinterpret_cast(&to_hook[to_hook.size()])); + printf("TO HOOK: %p\n", to_hook.data()); + print_decode_loop(hook_span); + puts("TEST FAR..."); + auto trampoline = test_far(hook_span.data(), (uint32_t const*)(0xDEADBEEFBAADF00DULL)); + // Use 20 here as a reasonable guesstimate + print_decode_loop(trampoline.fixup_inst_destination.addr); + puts("HOOKED:"); + print_decode_loop(hook_span); } -void test_near_bls_tbzs_within_hook() { - puts("Testing near -- bls/tbzs"); - static uint8_t to_hook[]{ 0x68, 0x00, 0x00, 0x37, 0xe0, 0x03, 0x17, 0xaa, 0x52, 0x3e, 0xfd, 0x97, - 0xe0, 0x03, 0x17, 0xaa, 0x64, 0x7b, 0xfe, 0x97, 0x00, 0x00, 0x00, 0x00 }; - perform_near_hook_test(to_hook); +void test_no_fixups() { + puts("Testing near -- no fixups!"); + static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8, 0xf6, 0x57, 0x01, 0xa9, 0xf4, 0x4f, 0x02, 0xa9, 0xfd, 0x7b, 0x03, + 0xa9, 0xfd, 0xc3, 0x00, 0x91, 0x48, 0x18, 0x40, 0xf9, 0x16, 0xd4, 0x42, 0xa9, 0xf3, 0x03, + 0x02, 0xaa, 0xf4, 0x03, 0x01, 0xaa, 0x17, 0x01, 0x40, 0xf9, 0xe8, 0xba, 0x44, 0x39 }; + perform_near_hook_test(to_hook); + puts("Testing far -- no fixups!"); + perform_far_hook_test(to_hook); } -void test_far_bls_tbzs_within_hook() { - puts("Testing far -- bls/tbzs"); - static uint8_t to_hook[]{ 0x68, 0x00, 0x00, 0x37, 0xe0, 0x03, 0x17, 0xaa, 0x52, 0x3e, 0xfd, 0x97, - 0xe0, 0x03, 0x17, 0xaa, 0x64, 0x7b, 0xfe, 0x97, 0x00, 0x00, 0x00, 0x00 }; - perform_far_hook_test(to_hook); +void test_bls_tbzs_within_hook() { + puts("Testing near -- bls/tbzs"); + static uint8_t to_hook[]{ 0x68, 0x00, 0x00, 0x37, 0xe0, 0x03, 0x17, 0xaa, 0x52, 0x3e, 0xfd, 0x97, + 0xe0, 0x03, 0x17, 0xaa, 0x64, 0x7b, 0xfe, 0x97, 0x00, 0x00, 0x00, 0x00 }; + perform_near_hook_test(to_hook); + puts("Testing far -- bls/tbzs"); + perform_far_hook_test(to_hook); } -void test_near_ldr_ldrb_tbnz_bl() { - puts("Testing near -- ldr/ldrb/tbnz/bl"); - static uint8_t to_hook[]{ 0x17, 0x01, 0x40, 0xf9, 0xe8, 0xba, 0x44, 0x39, 0x68, 0x00, 0x00, 0x37, - 0xe0, 0x03, 0x17, 0xaa, 0x52, 0x3e, 0xfd, 0x97, 0x00, 0x00, 0x00, 0x00 }; - perform_near_hook_test(to_hook); +void test_ldr_ldrb_tbnz_bl() { + puts("Testing near -- ldr/ldrb/tbnz/bl"); + static uint8_t to_hook[]{ 0x17, 0x01, 0x40, 0xf9, 0xe8, 0xba, 0x44, 0x39, 0x68, 0x00, 0x00, 0x37, + 0xe0, 0x03, 0x17, 0xaa, 0x52, 0x3e, 0xfd, 0x97, 0x00, 0x00, 0x00, 0x00 }; + perform_near_hook_test(to_hook); + puts("Testing far -- ldr/ldrb/tbnz/bl"); + perform_far_hook_test(to_hook); } -void test_far_ldr_ldrb_tbnz_bl() { - puts("Testing far -- ldr/ldrb/tbnz/bl"); - static uint8_t to_hook[]{ 0x17, 0x01, 0x40, 0xf9, 0xe8, 0xba, 0x44, 0x39, 0x68, 0x00, 0x00, 0x37, - 0xe0, 0x03, 0x17, 0xaa, 0x52, 0x3e, 0xfd, 0x97, 0x00, 0x00, 0x00, 0x00 }; - perform_far_hook_test(to_hook); +void test_adrp() { + puts("Testing near -- adrp"); + static uint8_t to_hook[]{ 0x09, 0x00, 0x00, 0x90, 0xa8, 0x00, 0x80, 0x52, 0x28, 0x01, + 0x00, 0xb9, 0x28, 0x01, 0x00, 0xb9, 0xc0, 0x03, 0x5f, 0xd6 }; + perform_near_hook_test(to_hook); + puts("Testing far -- adrp"); + perform_far_hook_test(to_hook); } // TODO: Test a case where we have a loop in the first 4 instructions +// TODO: Test a case where we have an ldr literal that loads from within fixup range int main() { - test_near_no_fixups(); - test_near_bls_tbzs_within_hook(); - test_near_ldr_ldrb_tbnz_bl(); - test_far_no_fixups(); - test_far_bls_tbzs_within_hook(); - test_far_ldr_ldrb_tbnz_bl(); + test_no_fixups(); + test_bls_tbzs_within_hook(); + test_ldr_ldrb_tbnz_bl(); + test_adrp(); } From e69a34d4aa1c233ee187cc24a7f176a052517b2f Mon Sep 17 00:00:00 2001 From: Adam ? Date: Sun, 21 Jul 2024 13:34:49 -0700 Subject: [PATCH 023/134] Add workflow to test in CI --- .github/workflows/build-ndk.yml | 59 +++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/.github/workflows/build-ndk.yml b/.github/workflows/build-ndk.yml index 27ace02..33f06e8 100644 --- a/.github/workflows/build-ndk.yml +++ b/.github/workflows/build-ndk.yml @@ -15,6 +15,65 @@ env: qmodName: flamingo jobs: + test-local: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + name: Checkout + with: + submodules: true + + - name: Set up Clang + uses: egor-tensin/setup-clang@v1 + with: + version: latest + platform: x64 + + - uses: actions/setup-python@v5 + with: + python-version: '3.8' + + # Get QPM so we can bring in dependencies + - name: QPM Rust Action + uses: Fernthedev/qpm-action@v1 + with: + #required + workflow_token: ${{secrets.GITHUB_TOKEN}} + + restore: true # will run restore on download + cache: true #will cache dependencies + + - name: QPM Collapse + run: qpm-rust collapse + + # Get the CORRECT version of capstone + - name: Get Capstone + run: pip install capstone==5.0.1 -U --user + + - name: General info + run: | + python3 --version + clang --version + ls ~/.local/lib/python3.8/site-packages/capstone/include + + - name: Build simple test + run: | + mkdir -p build-linux + clang++ test/main.cpp src/fixups.cpp src/page-allocator.cpp -o build-linux/test -std=c++20 -Ishared -I$(realpath ~/.local/lib/python3.8/site-packages/capstone/include) -l:libcapstone.a -Iextern/includes/fmt/fmt/include -L$(realpath ~/.local/lib/python3.8/site-packages/capstone/lib) -DFMT_HEADER_ONLY -Wall -Wextra -Werror -g + + # Run the test to ensure validity + - name: Run simple test + run: ./build-linux/test 2>&1 | tee ./build-linux/test-output.txt + + # Upload test artifact + - name: Upload test artifact + uses: actions/upload-artifact@v2 + if: always() + with: + name: test_output.txt + path: ./build-linux/test-output.txt + if-no-files-found: error build: runs-on: ubuntu-latest From 2be348b9f42842fb514789fb69b00994bc7888e7 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Sun, 21 Jul 2024 20:34:03 -0700 Subject: [PATCH 024/134] Add test-wrapper.hpp and add self-checking validation to fixups and hooks Ideally should be split up even further, but this will do for a start --- test/main.cpp | 226 ++++++++++++++++++++++++++++++++++++------ test/test-wrapper.hpp | 147 +++++++++++++++++++++++++++ 2 files changed, 345 insertions(+), 28 deletions(-) create mode 100644 test/test-wrapper.hpp diff --git a/test/main.cpp b/test/main.cpp index 0428a0a..fb097e0 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -1,18 +1,23 @@ // Main test runner for testing fixups behave as intended +#include +#include +#include +#include #include #include #include #include #include #include +#include #include #include "../shared/fixups.hpp" #include "../shared/page-allocator.hpp" #include "capstone/capstone.h" +#include "test-wrapper.hpp" - -void print_decode_loop(std::span data) { +static void print_decode_loop(std::span data) { auto handle = flamingo::getHandle(); for (size_t i = 0; i < data.size(); i++) { cs_insn* insns = nullptr; @@ -26,7 +31,7 @@ void print_decode_loop(std::span data) { } } -decltype(auto) test_near(uint32_t* target, [[maybe_unused]] uint32_t const* callback) { +static decltype(auto) test_near(uint32_t* target, [[maybe_unused]] uint32_t const* callback) { constexpr size_t hookSizeNumInsts = 5; constexpr size_t trampolineSize = 64; static std::array test_trampoline; @@ -44,7 +49,7 @@ decltype(auto) test_near(uint32_t* target, [[maybe_unused]] uint32_t const* call return fixups; } -void perform_near_hook_test(std::span to_hook) { +static auto perform_near_hook_test(std::span to_hook) { std::span hook_span = std::span(reinterpret_cast(&to_hook[0]), reinterpret_cast(&to_hook[to_hook.size()])); printf("TO HOOK: %p\n", to_hook.data()); @@ -55,9 +60,10 @@ void perform_near_hook_test(std::span to_hook) { print_decode_loop(trampoline_data.fixup_inst_destination.addr); puts("HOOKED:"); print_decode_loop(hook_span); + return trampoline_data; } -decltype(auto) test_far(uint32_t* target, [[maybe_unused]] uint32_t const* callback) { +static decltype(auto) test_far(uint32_t* target, [[maybe_unused]] uint32_t const* callback) { constexpr size_t hookSizeNumInsts = 5; constexpr size_t trampolineSize = 64; // We allocate the page with r-x perms, we will mark it as writable when we do the writes and otherwise put it back to @@ -77,54 +83,217 @@ decltype(auto) test_far(uint32_t* target, [[maybe_unused]] uint32_t const* callb return fixups; } -void perform_far_hook_test(std::span to_hook) { +static auto perform_far_hook_test(std::span to_hook) { std::span hook_span = std::span(reinterpret_cast(&to_hook[0]), reinterpret_cast(&to_hook[to_hook.size()])); printf("TO HOOK: %p\n", to_hook.data()); print_decode_loop(hook_span); puts("TEST FAR..."); - auto trampoline = test_far(hook_span.data(), (uint32_t const*)(0xDEADBEEFBAADF00DULL)); + auto fixups = test_far(hook_span.data(), (uint32_t const*)(0xDEADBEEFBAADF00DULL)); // Use 20 here as a reasonable guesstimate - print_decode_loop(trampoline.fixup_inst_destination.addr); + print_decode_loop(fixups.fixup_inst_destination.addr); puts("HOOKED:"); print_decode_loop(hook_span); + return fixups; } -void test_no_fixups() { - puts("Testing near -- no fixups!"); +static void test_no_fixups() { + puts("Testing no fixups!"); static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8, 0xf6, 0x57, 0x01, 0xa9, 0xf4, 0x4f, 0x02, 0xa9, 0xfd, 0x7b, 0x03, 0xa9, 0xfd, 0xc3, 0x00, 0x91, 0x48, 0x18, 0x40, 0xf9, 0x16, 0xd4, 0x42, 0xa9, 0xf3, 0x03, 0x02, 0xaa, 0xf4, 0x03, 0x01, 0xaa, 0x17, 0x01, 0x40, 0xf9, 0xe8, 0xba, 0x44, 0x39 }; - perform_near_hook_test(to_hook); - puts("Testing far -- no fixups!"); - perform_far_hook_test(to_hook); + { + TestWrapper init_hook(to_hook, "No fixups initial data"); + init_hook.expect_opc(ARM64_INS_STR); + init_hook.expect_opc(ARM64_INS_STP); + init_hook.expect_opc(ARM64_INS_STP); + init_hook.expect_opc(ARM64_INS_STP); + init_hook.expect_opc(ARM64_INS_ADD); + } + { + auto results = perform_near_hook_test(to_hook); + TestWrapper fixup_validator(results.fixup_inst_destination.addr, "Near hook no fixups"); + fixup_validator.expect_opc(ARM64_INS_STR); + fixup_validator.expect_opc(ARM64_INS_STP); + fixup_validator.expect_opc(ARM64_INS_STP); + fixup_validator.expect_opc(ARM64_INS_STP); + // Callback + fixup_validator.expect_b(&results.target.addr[4]); + } + { + auto results = perform_far_hook_test(to_hook); + TestWrapper fixup_validator(results.fixup_inst_destination.addr, "Far hook no fixups"); + fixup_validator.expect_opc(ARM64_INS_STR); + fixup_validator.expect_opc(ARM64_INS_STP); + fixup_validator.expect_opc(ARM64_INS_STP); + fixup_validator.expect_opc(ARM64_INS_STP); + // Callback (ldr x17, DATA[0]; br x17) + fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X17, + (int64_t)&results.fixup_inst_destination.addr[6]); + fixup_validator.expect_ops(ARM64_INS_BR, ARM64_REG_X17); + // Data validation + // Check callback point is valid + fixup_validator.expect_big_data(reinterpret_cast(&results.target.addr[4])); + } } -void test_bls_tbzs_within_hook() { - puts("Testing near -- bls/tbzs"); +static void test_bls_tbzs_within_hook() { + puts("Testing bls/tbzs"); static uint8_t to_hook[]{ 0x68, 0x00, 0x00, 0x37, 0xe0, 0x03, 0x17, 0xaa, 0x52, 0x3e, 0xfd, 0x97, 0xe0, 0x03, 0x17, 0xaa, 0x64, 0x7b, 0xfe, 0x97, 0x00, 0x00, 0x00, 0x00 }; - perform_near_hook_test(to_hook); - puts("Testing far -- bls/tbzs"); - perform_far_hook_test(to_hook); + { + TestWrapper init_hook(to_hook, "bls/tbzs"); + init_hook.expect_ops(ARM64_INS_TBNZ, ARM64_REG_W8, 0, + (int64_t)&init_hook.data[3]); + init_hook.expect_opc(ARM64_INS_MOV); + init_hook.expect_opc(ARM64_INS_BL); + init_hook.expect_opc(ARM64_INS_MOV); + init_hook.expect_opc(ARM64_INS_BL); + } + { + auto results = perform_near_hook_test(to_hook); + TestWrapper fixup_validator(results.fixup_inst_destination.addr, "Near hook bls/tbzs"); + fixup_validator.expect_ops( + ARM64_INS_TBNZ, ARM64_REG_W8, 0, (int64_t)&results.fixup_inst_destination.addr[3]); + fixup_validator.expect_opc(ARM64_INS_MOV); + fixup_validator.expect_opc(ARM64_INS_BL); + fixup_validator.expect_opc(ARM64_INS_MOV); + // Callback + fixup_validator.expect_b(&results.target.addr[4]); + } + { + auto results = perform_far_hook_test(to_hook); + TestWrapper fixup_validator(results.fixup_inst_destination.addr, "Far hook bls/tbzs"); + // tbnz is still close, should still be emitted, but should still point to the mov, which is at idx 4 + fixup_validator.expect_ops( + ARM64_INS_TBNZ, ARM64_REG_W8, 0, (int64_t)&results.fixup_inst_destination.addr[4]); + // mov is the same + fixup_validator.expect_opc(ARM64_INS_MOV); + // bl should turn into an ldr x17, DATA[0]; blr x17 + fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X17); + fixup_validator.expect_ops(ARM64_INS_BLR, ARM64_REG_X17); + fixup_validator.expect_opc(ARM64_INS_MOV); + // Callback (ldr x17, DATA[1]; br x17) + fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X17, + (int64_t)&results.fixup_inst_destination.addr[9]); + fixup_validator.expect_ops(ARM64_INS_BR, ARM64_REG_X17); + // Data validation + // The branch destination should be -0xB06B0 relative to the start of the hook. + // Thus: target == reinterpret_cast(& + fixup_validator.expect_big_data(reinterpret_cast(results.target.addr.data()) - 0xB06B0); + // Check callback point is valid + fixup_validator.expect_big_data(reinterpret_cast(&results.target.addr[4])); + } } -void test_ldr_ldrb_tbnz_bl() { - puts("Testing near -- ldr/ldrb/tbnz/bl"); +static void test_ldr_ldrb_tbnz_bl() { + puts("Testing ldr/ldrb/tbnz/bl"); static uint8_t to_hook[]{ 0x17, 0x01, 0x40, 0xf9, 0xe8, 0xba, 0x44, 0x39, 0x68, 0x00, 0x00, 0x37, 0xe0, 0x03, 0x17, 0xaa, 0x52, 0x3e, 0xfd, 0x97, 0x00, 0x00, 0x00, 0x00 }; - perform_near_hook_test(to_hook); - puts("Testing far -- ldr/ldrb/tbnz/bl"); - perform_far_hook_test(to_hook); + { + TestWrapper init_hook(to_hook, "ldr/ldrb/tbnz/bl"); + init_hook.expect_opc(ARM64_INS_LDR); + init_hook.expect_opc(ARM64_INS_LDRB); + init_hook.expect_ops(ARM64_INS_TBNZ, ARM64_REG_W8, 0, + (int64_t)&init_hook.data[5]); + init_hook.expect_opc(ARM64_INS_MOV); + } + { + auto results = perform_near_hook_test(to_hook); + TestWrapper fixup_validator(results.fixup_inst_destination.addr, "Near hook ldr/ldrb/tbnz/bl"); + fixup_validator.expect_opc(ARM64_INS_LDR); + fixup_validator.expect_opc(ARM64_INS_LDRB); + // TBNZ should jump over the following instruction if taken + // TODO: This test should change once we support near tbz/tbnz optimizations + fixup_validator.expect_ops( + ARM64_INS_TBNZ, ARM64_REG_W8, 0, (int64_t)&results.fixup_inst_destination.addr[4]); + // B instruction should jump to skip the following far branch call + fixup_validator.expect_b(&results.fixup_inst_destination.addr[6]); + // Far branch call is given by an ldr x17, DATA[0]; br x17 + fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X17, + (int64_t)&results.fixup_inst_destination.addr[8]); + fixup_validator.expect_ops(ARM64_INS_BR, ARM64_REG_X17); + fixup_validator.expect_opc(ARM64_INS_MOV); + // Callback + fixup_validator.expect_b(&results.target.addr[4]); + // Data validation + // Branch destination for tbnz taken should be hook[5] + fixup_validator.expect_big_data((uint64_t)&results.target.addr[5]); + } + { + auto results = perform_far_hook_test(to_hook); + TestWrapper fixup_validator(results.fixup_inst_destination.addr, "Far hook ldr/ldrb/tbnz/bl"); + fixup_validator.expect_opc(ARM64_INS_LDR); + fixup_validator.expect_opc(ARM64_INS_LDRB); + // TBNZ should jump over the following instruction if taken + fixup_validator.expect_ops( + ARM64_INS_TBNZ, ARM64_REG_W8, 0, (int64_t)&results.fixup_inst_destination.addr[4]); + // B instruction should jump to skip the following far branch call + fixup_validator.expect_b(&results.fixup_inst_destination.addr[6]); + // Far branch call is given by an ldr x17, DATA[0]; br x17 + fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X17, + (int64_t)&results.fixup_inst_destination.addr[9]); + fixup_validator.expect_ops(ARM64_INS_BR, ARM64_REG_X17); + fixup_validator.expect_opc(ARM64_INS_MOV); + // Callback (ldr x17, DATA[1]; br x17) + fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X17, + (int64_t)&results.fixup_inst_destination.addr[11]); + fixup_validator.expect_ops(ARM64_INS_BR, ARM64_REG_X17); + // Data validation + // Branch destination for tbnz taken should be hook[5] + fixup_validator.expect_big_data((uint64_t)&results.target.addr[5]); + // Check callback point is valid + fixup_validator.expect_big_data(reinterpret_cast(&results.target.addr[4])); + } } -void test_adrp() { - puts("Testing near -- adrp"); +static void test_adrp() { + puts("Testing adrp"); static uint8_t to_hook[]{ 0x09, 0x00, 0x00, 0x90, 0xa8, 0x00, 0x80, 0x52, 0x28, 0x01, 0x00, 0xb9, 0x28, 0x01, 0x00, 0xb9, 0xc0, 0x03, 0x5f, 0xd6 }; - perform_near_hook_test(to_hook); - puts("Testing far -- adrp"); - perform_far_hook_test(to_hook); + { + TestWrapper init_hook(to_hook, "adrp"); + init_hook.expect_ops(ARM64_INS_ADRP, ARM64_REG_X9, (int64_t)(&to_hook[0]) & ~0xfff); + init_hook.expect_opc(ARM64_INS_MOV); + init_hook.expect_opc(ARM64_INS_STR); + init_hook.expect_opc(ARM64_INS_STR); + init_hook.expect_opc(ARM64_INS_RET); + } + { + auto results = perform_near_hook_test(to_hook); + TestWrapper fixup_validator(results.fixup_inst_destination.addr, "Near hook adrp"); + // ADRP is replaced with an ldr to load the data directly + // LDR x9, DATA[0] + fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X9, + (int64_t)&results.fixup_inst_destination.addr[5]); + fixup_validator.expect_opc(ARM64_INS_MOV); + fixup_validator.expect_opc(ARM64_INS_STR); + fixup_validator.expect_opc(ARM64_INS_STR); + // Callback + fixup_validator.expect_b(&results.target.addr[4]); + // Data validation + // ADRP result must match + fixup_validator.expect_big_data((int64_t)(&to_hook[0]) & ~0xfff); + } + { + auto results = perform_far_hook_test(to_hook); + TestWrapper fixup_validator(results.fixup_inst_destination.addr, "Far hook adrp"); + // ADRP is replaced with an ldr to load the data directly + // LDR x9, DATA[0] + fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X9, + (int64_t)&results.fixup_inst_destination.addr[6]); + fixup_validator.expect_opc(ARM64_INS_MOV); + fixup_validator.expect_opc(ARM64_INS_STR); + fixup_validator.expect_opc(ARM64_INS_STR); + // Callback (ldr x17, DATA[1]; br x17) + fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X17, + (int64_t)&results.fixup_inst_destination.addr[8]); + fixup_validator.expect_ops(ARM64_INS_BR, ARM64_REG_X17); + // ADRP result must match + fixup_validator.expect_big_data((int64_t)(&to_hook[0]) & ~0xfff); + // Check callback point is valid + fixup_validator.expect_big_data(reinterpret_cast(&results.target.addr[4])); + } } // TODO: Test a case where we have a loop in the first 4 instructions @@ -135,4 +304,5 @@ int main() { test_bls_tbzs_within_hook(); test_ldr_ldrb_tbnz_bl(); test_adrp(); + puts("ALL GOOD!"); } diff --git a/test/test-wrapper.hpp b/test/test-wrapper.hpp new file mode 100644 index 0000000..c2d2b89 --- /dev/null +++ b/test/test-wrapper.hpp @@ -0,0 +1,147 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "capstone/arm64.h" +#include "capstone/capstone.h" + +#include "../shared/fixups.hpp" + +// TODO: Only do a dump if an error happens +// Dump should do a normal decode loop: +// - Dump the original instructions for the hook +// - Dump the trampoline +// - Dump the new hook +// Perhaps we make TestWrapper take a Fixups instance for this? That way we can do all three of those. +#define ERROR(S, ...) \ + fmt::print(stderr, FMT_COMPILE(S), __VA_ARGS__); \ + fmt::print(stderr, "\n"); \ + std::exit(1); + +// Helper construct to validate data from a hooked target +struct TestWrapper { + std::span data; + uint32_t idx{ 0 }; + std::string test_name; + TestWrapper(std::span bytes, std::string_view test) : data(bytes), test_name(test) { + start_test(); + } + TestWrapper(std::span bytes, std::string_view test) + : data(std::span(reinterpret_cast(&bytes[0]), + reinterpret_cast(&bytes[bytes.size()]))), + test_name(test) { + start_test(); + } + ~TestWrapper() { + fmt::print("---Passed test: {}\n", test_name); + fflush(stdout); + } + + void start_test() const { + fmt::print("---Starting test: {}\n", test_name); + fflush(stdout); + } + + cs_insn* get_next() { + auto handle = flamingo::getHandle(); + cs_insn* insns = nullptr; + auto count = cs_disasm(handle, reinterpret_cast(&data[idx]), sizeof(uint32_t), + reinterpret_cast(&data[idx]), 1, &insns); + idx++; + if (count == 1) { + // We just leak this, who cares. + return insns; + } + // Failed to disassemble! + return nullptr; + } + uint32_t get_next_data() { + return data[idx++]; + } + uint64_t get_next_big_data() { + uint64_t value = static_cast(get_next_data()); + value |= static_cast(get_next_data()) << 32U; + return value; + } + + void expect_inst_opc(cs_insn* inst, unsigned int opcode) const { + if (inst == nullptr) { + ERROR("Mismatched instruction at index: {}\n Expected opcode: {}\n Got: Invalid instruction", idx - 1, opcode); + } + if (inst->id != opcode) { + ERROR("Mismatched instruction at index: {}\n Expected opcode: {}\n Got: {}", idx - 1, opcode, inst->id); + } + } + void expect_opc(unsigned int opcode) { + auto inst = get_next(); + expect_inst_opc(inst, opcode); + cs_free(inst, 1); + } + void expect_b(uint32_t* addr) { + auto inst = get_next(); + expect_inst_opc(inst, ARM64_INS_B); + if (inst->detail->arm64.operands[0].imm != reinterpret_cast(addr)) { + ERROR("Mismatched B at index: {}\n Expected immediate: {}\n Got: {:#x}", idx - 1, fmt::ptr(addr), + inst->detail->arm64.operands[0].imm); + } + cs_free(inst, 1); + } + template + static bool compare_op(cs_arm64_op const& op, T value) { + if (op.type != OpType) return false; + switch (OpType) { + case arm64_op_type::ARM64_OP_IMM: + return op.imm == value; + case arm64_op_type::ARM64_OP_REG: + return op.reg == value; + default: + ERROR("Cannot compare operand of type: {}", static_cast(OpType)); + } + } + template + void validate_ops(std::index_sequence, cs_arm64_op* ops, std::tuple expected) { + auto expect_op = [&]() { + if (!compare_op(ops[Size], std::get(expected))) { + ERROR("Mismatched instruction at index: {} Mismatched operand at index: {}\n Expected: {}: {}\n Got: {}: {}", + idx, Size, static_cast(OpType), static_cast(std::get(expected)), + static_cast(ops[Size].type), ops[Size].imm); + } + }; + (expect_op.template operator()(), ...); + } + template + void expect_ops(unsigned int opcode, TArgs&&... args) { + auto inst = get_next(); + expect_inst_opc(inst, opcode); + auto opcount = inst->detail->arm64.op_count; + static_assert(sizeof...(OpTypes) == sizeof...(TArgs), "Must have a type for each operand"); + if (sizeof...(args) > opcount) { + ERROR("Mismatched instruction at index: {}\n Expected opcount: {}\n Got: {}", idx - 1, sizeof...(args), opcount); + } + // Index wrapper to validate each operand in order + validate_ops(std::make_index_sequence{}, inst->detail->arm64.operands, + std::make_tuple(std::forward(args)...)); + cs_free(inst, 1); + } + void expect_data(uint32_t expected) { + auto data = get_next_data(); + if (data != expected) { + ERROR("Mismatched 32b data at index: {}\n Expected: {}\n Got: {}", idx - 1, expected, data); + } + } + void expect_big_data(uint64_t expected) { + auto data = get_next_big_data(); + if (data != expected) { + ERROR("Mismatched 64b data at index: {}\n Expected: {}\n Got: {}", idx - 1, expected, data); + } + } +}; \ No newline at end of file From 5572a13d716872810f460965ae4ba0f5df79bae0 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Sun, 21 Jul 2024 20:35:45 -0700 Subject: [PATCH 025/134] Add TODO for ensuring alignment of data section for different sizes of data The way we would want to do this would be by moving the index forward by an additional step if needed, i.e., pad with 0s I think in ARM this isn't a FUNCTIONAL requirement, but rather an improvement to performance, so might still be nice to do --- src/fixups.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fixups.cpp b/src/fixups.cpp index 4ebc434..45773ee 100644 --- a/src/fixups.cpp +++ b/src/fixups.cpp @@ -165,6 +165,7 @@ struct FixupContext { // Pointer is known to be little endian FLAMINGO_DEBUG("Adding 64b data: 0x{:x} at data index: {} for fixup index: {} ({})", large_data, data_index, fixup_idx, fmt::ptr(&fixup_writer.target.addr[fixup_idx])); + // TODO: May need to add some extra logic for making sure 64B data is aligned 8, instead of aligned 4! data_block.push_back(large_data & (UINT32_MAX)); data_block.push_back((large_data >> 32) & UINT32_MAX); data_ref_tags.emplace_back(ImmediateReferenceTag{ From 15fa78bcea12f5e8d76e5a6a002222825f70cd3f Mon Sep 17 00:00:00 2001 From: Adam ? Date: Mon, 22 Jul 2024 22:01:54 -0700 Subject: [PATCH 026/134] Add support for ensuring alignment of data --- src/fixups.cpp | 40 ++++++++++++++++++++++++++++++++-------- test/main.cpp | 19 ++++++++++--------- test/test-wrapper.hpp | 14 ++++++++++++++ 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/src/fixups.cpp b/src/fixups.cpp index 45773ee..094ede3 100644 --- a/src/fixups.cpp +++ b/src/fixups.cpp @@ -99,13 +99,22 @@ struct BranchReferenceTag { /// @brief Index to overwrite uint32_t target_index; }; +struct DataEntry { + /// @brief The data to hold in this entry + uint32_t data; + /// @brief The alignment (in multiples of 4 bytes) that we should perform for this entry + uint_fast8_t alignment; + /// @brief The actual index into the data section this data entry maps to. + /// This is only assigned to AFTER all data entries have been added. + uint32_t actual_idx{}; +}; // Holds the context for performing fixups that we don't want to expose to the caller. struct FixupContext { // The initial target pointer std::span target; flamingo::ProtectionWriter fixup_writer; // Holds sequentially laid out data for usage within fixups - std::vector data_block{}; + std::vector data_block{}; uint_fast16_t data_index = 0; // Holds a collection of data elements, which describes which fixups to perform overwrites of after data is allocated std::vector data_ref_tags{}; @@ -147,7 +156,7 @@ struct FixupContext { // Pointer is known to be little endian FLAMINGO_DEBUG("Adding 32b data: 0x{:x} at data index: {} for fixup index: {} ({})", data, data_index, fixup_idx, fmt::ptr(&fixup_writer.target.addr[fixup_idx])); - data_block.push_back(data); + data_block.push_back({ .data = data, .alignment = 1 }); data_ref_tags.emplace_back(ImmediateReferenceTag{ .imm_mask = imm_mask, .lshift = lshift, @@ -165,9 +174,9 @@ struct FixupContext { // Pointer is known to be little endian FLAMINGO_DEBUG("Adding 64b data: 0x{:x} at data index: {} for fixup index: {} ({})", large_data, data_index, fixup_idx, fmt::ptr(&fixup_writer.target.addr[fixup_idx])); - // TODO: May need to add some extra logic for making sure 64B data is aligned 8, instead of aligned 4! - data_block.push_back(large_data & (UINT32_MAX)); - data_block.push_back((large_data >> 32) & UINT32_MAX); + // The first entry is aligned 64, the second entry has 32b alignment. + data_block.push_back({ .data = static_cast(large_data & (UINT32_MAX)), .alignment = 2 }); + data_block.push_back({ .data = static_cast((large_data >> 32) & UINT32_MAX), .alignment = 1 }); data_ref_tags.emplace_back(ImmediateReferenceTag{ .imm_mask = imm_mask, .lshift = lshift, @@ -578,12 +587,27 @@ void Fixups::PerformFixupsAndCallback() { // and marking the start address as "base". Then, we compute offsets based off of base + data_index * sizeof(uint32_t) // - &fixups[fixup_idx] auto data_base = context.GetFixupPC(); - for (auto const data : context.data_block) { - context.Write(data); + for (auto& data : context.data_block) { + // Check our location for alignment + auto const align_bytes = (data.alignment * sizeof(uint32_t)); + auto misalignment = context.GetFixupPC() % align_bytes; + if (misalignment != 0) { + FLAMINGO_DEBUG("MISALIGNED ADDRESS: {:#x} ALIGNING TO: {} REQUIRES: {} BYTES", context.GetFixupPC(), align_bytes, + (align_bytes - misalignment)); + // Need to write 0s to pad + for (size_t i = 0; i < (align_bytes - misalignment); i += sizeof(uint32_t)) { + context.Write(0U); + } + } + data.actual_idx = (context.GetFixupPC() - data_base) / sizeof(uint32_t); + context.Write(data.data); } for (auto const& tag : context.data_ref_tags) { - int_fast16_t offset = static_cast(data_base + tag.data_index * sizeof(context.data_block[0]) - + auto const actual_data_idx = context.data_block[tag.data_index].actual_idx; + int_fast16_t offset = static_cast(data_base + actual_data_idx * sizeof(uint32_t) - get_untagged_pc(&fixup_inst_destination.addr[tag.fixup_index])); + FLAMINGO_DEBUG("ACTUAL DATA INDEX: {} FOR TAG AT FIXUP: {} OFFSET IN BYTES: {} AT: {}", actual_data_idx, + tag.fixup_index, offset, data_base + actual_data_idx * sizeof(uint32_t)); fixup_inst_destination.addr[tag.fixup_index] = (fixup_inst_destination.addr[tag.fixup_index] & ~tag.imm_mask) | (tag.imm_mask & ((offset >> tag.rshift) << tag.lshift)); } diff --git a/test/main.cpp b/test/main.cpp index fb097e0..80f0d9b 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -129,7 +129,7 @@ static void test_no_fixups() { fixup_validator.expect_opc(ARM64_INS_STP); // Callback (ldr x17, DATA[0]; br x17) fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X17, - (int64_t)&results.fixup_inst_destination.addr[6]); + round_up8(&results.fixup_inst_destination.addr[6])); fixup_validator.expect_ops(ARM64_INS_BR, ARM64_REG_X17); // Data validation // Check callback point is valid @@ -170,12 +170,13 @@ static void test_bls_tbzs_within_hook() { // mov is the same fixup_validator.expect_opc(ARM64_INS_MOV); // bl should turn into an ldr x17, DATA[0]; blr x17 - fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X17); + fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X17, + round_up8(&results.fixup_inst_destination.addr[7])); fixup_validator.expect_ops(ARM64_INS_BLR, ARM64_REG_X17); fixup_validator.expect_opc(ARM64_INS_MOV); // Callback (ldr x17, DATA[1]; br x17) fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X17, - (int64_t)&results.fixup_inst_destination.addr[9]); + round_up8(&results.fixup_inst_destination.addr[9])); fixup_validator.expect_ops(ARM64_INS_BR, ARM64_REG_X17); // Data validation // The branch destination should be -0xB06B0 relative to the start of the hook. @@ -211,7 +212,7 @@ static void test_ldr_ldrb_tbnz_bl() { fixup_validator.expect_b(&results.fixup_inst_destination.addr[6]); // Far branch call is given by an ldr x17, DATA[0]; br x17 fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X17, - (int64_t)&results.fixup_inst_destination.addr[8]); + round_up8(&results.fixup_inst_destination.addr[8])); fixup_validator.expect_ops(ARM64_INS_BR, ARM64_REG_X17); fixup_validator.expect_opc(ARM64_INS_MOV); // Callback @@ -232,12 +233,12 @@ static void test_ldr_ldrb_tbnz_bl() { fixup_validator.expect_b(&results.fixup_inst_destination.addr[6]); // Far branch call is given by an ldr x17, DATA[0]; br x17 fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X17, - (int64_t)&results.fixup_inst_destination.addr[9]); + round_up8(&results.fixup_inst_destination.addr[9])); fixup_validator.expect_ops(ARM64_INS_BR, ARM64_REG_X17); fixup_validator.expect_opc(ARM64_INS_MOV); // Callback (ldr x17, DATA[1]; br x17) fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X17, - (int64_t)&results.fixup_inst_destination.addr[11]); + round_up8(&results.fixup_inst_destination.addr[11])); fixup_validator.expect_ops(ARM64_INS_BR, ARM64_REG_X17); // Data validation // Branch destination for tbnz taken should be hook[5] @@ -265,7 +266,7 @@ static void test_adrp() { // ADRP is replaced with an ldr to load the data directly // LDR x9, DATA[0] fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X9, - (int64_t)&results.fixup_inst_destination.addr[5]); + round_up8(&results.fixup_inst_destination.addr[5])); fixup_validator.expect_opc(ARM64_INS_MOV); fixup_validator.expect_opc(ARM64_INS_STR); fixup_validator.expect_opc(ARM64_INS_STR); @@ -281,13 +282,13 @@ static void test_adrp() { // ADRP is replaced with an ldr to load the data directly // LDR x9, DATA[0] fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X9, - (int64_t)&results.fixup_inst_destination.addr[6]); + round_up8(&results.fixup_inst_destination.addr[6])); fixup_validator.expect_opc(ARM64_INS_MOV); fixup_validator.expect_opc(ARM64_INS_STR); fixup_validator.expect_opc(ARM64_INS_STR); // Callback (ldr x17, DATA[1]; br x17) fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X17, - (int64_t)&results.fixup_inst_destination.addr[8]); + round_up8(&results.fixup_inst_destination.addr[8])); fixup_validator.expect_ops(ARM64_INS_BR, ARM64_REG_X17); // ADRP result must match fixup_validator.expect_big_data((int64_t)(&to_hook[0]) & ~0xfff); diff --git a/test/test-wrapper.hpp b/test/test-wrapper.hpp index c2d2b89..8afb89f 100644 --- a/test/test-wrapper.hpp +++ b/test/test-wrapper.hpp @@ -27,6 +27,16 @@ fmt::print(stderr, "\n"); \ std::exit(1); +// Converts a pointer to data into the next 64b multiple for use with tests with differing alignments. +int64_t round_up8(auto* ptr) { + auto value = reinterpret_cast(ptr); + if ((value % 8) != 0) { + return value + 4; + } + return value; +} + +// TODO: ALSO ADD A MMAP WRAPPER TO GUARANTEE FAR HOOKS ARE FAR // Helper construct to validate data from a hooked target struct TestWrapper { std::span data; @@ -68,6 +78,10 @@ struct TestWrapper { return data[idx++]; } uint64_t get_next_big_data() { + // If &data[idx] is not aligned 64b, we need to increment index first + if ((reinterpret_cast(&data[idx]) % 8) != 0) { + idx++; + } uint64_t value = static_cast(get_next_data()); value |= static_cast(get_next_data()) << 32U; return value; From b0614e10b94c434fdb01994e5e78477a21282f84 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Tue, 23 Jul 2024 00:50:24 -0700 Subject: [PATCH 027/134] Add guaranteed near and far hooks via consistent allocation Adjust tests to match and improve debugging facilities of page allocator --- shared/page-allocator.hpp | 1 + src/page-allocator.cpp | 2 ++ test/main.cpp | 52 ++++++++++++++++++--------------------- test/test-wrapper.hpp | 52 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 78 insertions(+), 29 deletions(-) diff --git a/shared/page-allocator.hpp b/shared/page-allocator.hpp index 7b2f89d..e1c8ed6 100644 --- a/shared/page-allocator.hpp +++ b/shared/page-allocator.hpp @@ -45,6 +45,7 @@ struct PointerWrapper { PageProtectionType protection; PointerWrapper(std::span addr, PageProtectionType prot) : addr(addr), protection(prot) {} + PointerWrapper(PointerWrapper const&) = default; void Protect() const { // If we have nothing in the address, don't bother protecting diff --git a/src/page-allocator.cpp b/src/page-allocator.cpp index 79d3def..8dc782b 100644 --- a/src/page-allocator.cpp +++ b/src/page-allocator.cpp @@ -8,6 +8,7 @@ // 2. Shrinking of allocations need to be done in such a way that future allocations are not broken. i.e. bump allocator // 3. Deallocations need to be done in such a way that full pages are not destroyed #include "page-allocator.hpp" +#include #include #include #include @@ -71,6 +72,7 @@ PointerWrapper Allocate(uint_fast16_t alignment, uint_fast16_t size, P static_cast(protection), std::strerror(errno)); } auto const page = all_pages->emplace(protection, Page{ .ptr = ptr, .used_size = size, .protection = protection }); + FLAMINGO_DEBUG("Allocated fixup page with ptr: {} with size: {}", fmt::ptr(ptr), size); return PointerWrapper( std::span{ reinterpret_cast(&reinterpret_cast(page->second.ptr)[0]), reinterpret_cast(&reinterpret_cast(page->second.ptr)[size]) }, diff --git a/test/main.cpp b/test/main.cpp index 80f0d9b..f48dd82 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -31,19 +31,20 @@ static void print_decode_loop(std::span data) { } } -static decltype(auto) test_near(uint32_t* target, [[maybe_unused]] uint32_t const* callback) { +static decltype(auto) test_near(std::span target, [[maybe_unused]] uint32_t const* callback) { constexpr size_t hookSizeNumInsts = 5; - constexpr size_t trampolineSize = 64; - static std::array test_trampoline; - printf("TRAMPOLINE: %p\n", test_trampoline.data()); + constexpr size_t trampolineSize = 32; + // First, perform a near allocation to the allocated location + auto near_data = alloc_near(target, trampolineSize); + fmt::println("NEAR TRAMPOLINE RESULT: {}", fmt::ptr(near_data.fixups.data())); flamingo::Fixups fixups{ - .target = flamingo::PointerWrapper(std::span{ target, &target[hookSizeNumInsts - 1] }, - flamingo::PageProtectionType::kExecute | - flamingo::PageProtectionType::kRead | - flamingo::PageProtectionType::kWrite), + .target = flamingo::PointerWrapper( + std::span(near_data.target.begin(), near_data.target.begin() + hookSizeNumInsts - 1), + flamingo::PageProtectionType::kExecute | flamingo::PageProtectionType::kRead | + flamingo::PageProtectionType::kWrite), .fixup_inst_destination = flamingo::PointerWrapper( - test_trampoline, flamingo::PageProtectionType::kExecute | flamingo::PageProtectionType::kRead | - flamingo::PageProtectionType::kWrite), + near_data.fixups, flamingo::PageProtectionType::kExecute | flamingo::PageProtectionType::kRead | + flamingo::PageProtectionType::kWrite), }; fixups.PerformFixupsAndCallback(); return fixups; @@ -55,7 +56,7 @@ static auto perform_near_hook_test(std::span to_hook) { printf("TO HOOK: %p\n", to_hook.data()); print_decode_loop(hook_span); puts("TEST NEAR..."); - auto trampoline_data = test_near(hook_span.data(), (uint32_t const*)(0xDEADBEEFBAADF00DULL)); + auto trampoline_data = test_near(hook_span, (uint32_t const*)(0xDEADBEEFBAADF00DULL)); // Use 20 here as a reasonable guesstimate print_decode_loop(trampoline_data.fixup_inst_destination.addr); puts("HOOKED:"); @@ -63,16 +64,19 @@ static auto perform_near_hook_test(std::span to_hook) { return trampoline_data; } -static decltype(auto) test_far(uint32_t* target, [[maybe_unused]] uint32_t const* callback) { +static decltype(auto) test_far(std::span target, [[maybe_unused]] uint32_t const* callback) { constexpr size_t hookSizeNumInsts = 5; - constexpr size_t trampolineSize = 64; + constexpr size_t trampolineSize = 32; // We allocate the page with r-x perms, we will mark it as writable when we do the writes and otherwise put it back to // this state. auto fixup_ptr = flamingo::Allocate(16, trampolineSize * sizeof(uint32_t), flamingo::PageProtectionType::kExecute | flamingo::PageProtectionType::kRead); + // To ensure we test far hooks correctly, we copy from target to a page allocated far from fixup_ptr + auto actual_target = alloc_far(fixup_ptr, target); + fmt::println("FAR TRAMPOLINE RESULT: {}", fmt::ptr(actual_target.data())); flamingo::Fixups fixups{ .target = flamingo::PointerWrapper( - std::span{ target, &target[hookSizeNumInsts - 1] }, + std::span(actual_target.begin(), actual_target.begin() + hookSizeNumInsts - 1), flamingo::PageProtectionType::kExecute | flamingo::PageProtectionType::kRead), .fixup_inst_destination = fixup_ptr, }; @@ -89,7 +93,7 @@ static auto perform_far_hook_test(std::span to_hook) { printf("TO HOOK: %p\n", to_hook.data()); print_decode_loop(hook_span); puts("TEST FAR..."); - auto fixups = test_far(hook_span.data(), (uint32_t const*)(0xDEADBEEFBAADF00DULL)); + auto fixups = test_far(hook_span, (uint32_t const*)(0xDEADBEEFBAADF00DULL)); // Use 20 here as a reasonable guesstimate print_decode_loop(fixups.fixup_inst_destination.addr); puts("HOOKED:"); @@ -204,22 +208,14 @@ static void test_ldr_ldrb_tbnz_bl() { TestWrapper fixup_validator(results.fixup_inst_destination.addr, "Near hook ldr/ldrb/tbnz/bl"); fixup_validator.expect_opc(ARM64_INS_LDR); fixup_validator.expect_opc(ARM64_INS_LDRB); - // TBNZ should jump over the following instruction if taken + // TBNZ should jump straight to the hook location if taken // TODO: This test should change once we support near tbz/tbnz optimizations - fixup_validator.expect_ops( - ARM64_INS_TBNZ, ARM64_REG_W8, 0, (int64_t)&results.fixup_inst_destination.addr[4]); - // B instruction should jump to skip the following far branch call - fixup_validator.expect_b(&results.fixup_inst_destination.addr[6]); - // Far branch call is given by an ldr x17, DATA[0]; br x17 - fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X17, - round_up8(&results.fixup_inst_destination.addr[8])); - fixup_validator.expect_ops(ARM64_INS_BR, ARM64_REG_X17); + fixup_validator.expect_ops(ARM64_INS_TBNZ, ARM64_REG_W8, 0, + (int64_t)&results.target.addr[5]); fixup_validator.expect_opc(ARM64_INS_MOV); // Callback fixup_validator.expect_b(&results.target.addr[4]); // Data validation - // Branch destination for tbnz taken should be hook[5] - fixup_validator.expect_big_data((uint64_t)&results.target.addr[5]); } { auto results = perform_far_hook_test(to_hook); @@ -274,7 +270,7 @@ static void test_adrp() { fixup_validator.expect_b(&results.target.addr[4]); // Data validation // ADRP result must match - fixup_validator.expect_big_data((int64_t)(&to_hook[0]) & ~0xfff); + fixup_validator.expect_big_data((int64_t)(results.target.addr.data()) & ~0xfff); } { auto results = perform_far_hook_test(to_hook); @@ -291,7 +287,7 @@ static void test_adrp() { round_up8(&results.fixup_inst_destination.addr[8])); fixup_validator.expect_ops(ARM64_INS_BR, ARM64_REG_X17); // ADRP result must match - fixup_validator.expect_big_data((int64_t)(&to_hook[0]) & ~0xfff); + fixup_validator.expect_big_data((int64_t)(results.target.addr.data()) & ~0xfff); // Check callback point is valid fixup_validator.expect_big_data(reinterpret_cast(&results.target.addr[4])); } diff --git a/test/test-wrapper.hpp b/test/test-wrapper.hpp index 8afb89f..65e243a 100644 --- a/test/test-wrapper.hpp +++ b/test/test-wrapper.hpp @@ -1,11 +1,18 @@ #pragma once +#include #include #include #include +#include +#include +#include +#include #include #include #include +#include +#include #include #include #include @@ -158,4 +165,47 @@ struct TestWrapper { ERROR("Mismatched 64b data at index: {}\n Expected: {}\n Got: {}", idx - 1, expected, data); } } -}; \ No newline at end of file +}; + +struct TestAllocResult { + std::span target; + std::span fixups; +}; + +inline auto alloc_near(std::span target, size_t fixup_size) { + // Alloc 2 pages so we can remap target too + auto result = mmap(nullptr, PAGE_SIZE * 2, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); + if (result == MAP_FAILED) { + perror(strerror(errno)); + } + std::memcpy(result, target.data(), target.size_bytes()); + return TestAllocResult{ + .target = std::span((uint32_t*)result, (uint32_t*)result + target.size()), + .fixups = std::span((uint32_t*)result + PAGE_SIZE / sizeof(uint32_t), + (uint32_t*)result + PAGE_SIZE / sizeof(uint32_t) + fixup_size), + }; +} + +inline auto alloc_far(flamingo::PointerWrapper target_fixups, std::span data_to_copy) { + // Continue fetching pages until we find one that's far enough away from target_fixups + // TODO: Cache the found page and give it back each time we call this function + // but I don't care enough for that + constexpr auto kPageCount = 16384; + constexpr auto kRange = 0x800000LL; + for (int i = 0; i < kPageCount; i++) { + auto result = mmap(nullptr, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); + if (result == MAP_FAILED) { + perror(strerror(errno)); + break; + } + if (std::llabs((int64_t)result - (int64_t)target_fixups.addr.data()) > kRange) { + // Found a page we can use + std::memcpy(result, data_to_copy.data(), data_to_copy.size_bytes()); + // Return the new result as a span + return std::span((uint32_t*)result, (uint32_t*)result + data_to_copy.size()); + } + } + // If we can't find ANY pages that match this criteria even after trying a bunch, abort + ERROR("Could not find any pages (tried: {}) that are outside of the range: {} of: {}", kPageCount, kRange, + fmt::ptr(target_fixups.addr.data())); +} From 68b9acf9a9c88ec069af9d3f49adab12c84e0400 Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Thu, 8 Aug 2024 23:41:45 -0700 Subject: [PATCH 028/134] Initial work on adding actual hook installation API --- shared/fixups.hpp | 11 +++- shared/hook-data.hpp | 126 +++++++++++---------------------------- shared/hook-metadata.hpp | 45 ++++++++++++++ shared/target-data.hpp | 42 +++++++++++++ shared/type-info.hpp | 27 +++++++++ 5 files changed, 158 insertions(+), 93 deletions(-) create mode 100644 shared/hook-metadata.hpp create mode 100644 shared/target-data.hpp create mode 100644 shared/type-info.hpp diff --git a/shared/fixups.hpp b/shared/fixups.hpp index 4d579d9..0c47d70 100644 --- a/shared/fixups.hpp +++ b/shared/fixups.hpp @@ -49,9 +49,18 @@ struct ProtectionWriter { } }; +struct ShimTarget : PointerWrapper { + /// @brief Holds the original instructions at this target BEFORE a HOOK was written there. + /// This is not the same as a Fixups' original_instructions, which are populated across ALL fixups performed. + std::vector original_instructions{}; + void WriteJump(void* addr); +}; + struct Fixups { + /// @brief The number of instructions to typically use for normal fixups + constexpr static auto kNormalFixupInstCount = 4U; // The location to read as input for fixup writes - PointerWrapper target; + ShimTarget target; // The location to write fixups to PointerWrapper fixup_inst_destination; std::vector original_instructions{}; diff --git a/shared/hook-data.hpp b/shared/hook-data.hpp index 0925488..3ee795b 100644 --- a/shared/hook-data.hpp +++ b/shared/hook-data.hpp @@ -1,122 +1,64 @@ #pragma once -#include #include #include -#include -#include -#include -#include -#include -#include -#include + #include "calling-convention.hpp" -#include "hook-installation-result.hpp" -#include "trampoline.hpp" +#include "hook-metadata.hpp" namespace flamingo { -struct Hook; -struct TargetData; - -struct InstallationMetadata { - bool need_orig; - bool is_midpoint; - // uint32_t permissible_fixup_registers; - - installation::Result combine(InstallationMetadata const& other); -}; - -/// @brief Describes the name metadata of the hook, used for lookups and priorities. -/// Lookups are described using userdata when the HookInfo is made at first. -struct HookNameMetadata { - std::string name{}; - std::any userdata{}; -}; - -/// @brief Represents a priority for how to align hook orderings. Note that a change in priority MAY require a full list recreation. -/// But SHOULD NOT require a hook recompile or a trampoline recompile. -struct HookPriority { - /// @brief The set of constraints for this hook to be installed before (called earlier than) - std::vector befores{}; - /// @brief The set of constraints for this hook to be installed after (called later than) - std::vector afters{}; -}; - -struct HookMetadata { - CallingConvention convention; - InstallationMetadata metadata; - uint16_t method_num_insts; - HookNameMetadata name_info; - HookPriority priority; -#ifndef FLAMINGO_NO_REGISTRATION_CHECKS - std::vector parameter_info; - TypeInfo return_info; -#endif -}; - /// @brief Represents a hook that a user of this library will use. -/// On install, we collect this information into a single TargetInfo structure, which contains a collection of multiple Hook references. -/// We map target --> TargetInfo and every time we have a new hook installed there, we move orig pointers around accordingly. -/// To do priorities, we have to track that state within a given Hook -/// We don't necessarily need O(1) hook installation, but we could. -/// A TargetInfo may basically just be a Hook, except with the added list of Hooks, which we use to validate. +/// On install, we collect this information into a single TargetInfo structure, which contains a collection of multiple +/// Hook references. We map target --> TargetInfo and every time we have a new hook installed there, we move orig +/// pointers around accordingly. To do priorities, we have to track that state within a given Hook We don't necessarily +/// need O(1) hook installation, but we could. A TargetInfo may basically just be a Hook, except with the added list of +/// Hooks, which we use to validate. struct HookInfo { // TODO: friend struct this to something? friend struct TargetData; template using HookFuncType = R (*)(TArgs...); - // TODO: Could replace this with a single function, instead of hook by hook? - // This function should return true if both userdata infos are equivalent. - // This is to be used for modloader unaware code, but stateful hook infos - using HookPriorityLookupFunction = std::function; + + /// @brief The default number of instructions to install a hook with + constexpr static uint16_t kDefaultNumInsts = 10U; template - HookInfo(HookFuncType hook_func, void* target, HookFuncType* orig_ptr, uint16_t num_insts, - CallingConvention conv, InstallationMetadata metadata, HookNameMetadata&& name_info, HookPriority&& priority, - HookPriorityLookupFunction&& priority_lookup_func) + HookInfo(HookFuncType hook_func, void* target, HookFuncType* orig_ptr = nullptr, + uint16_t num_insts = kDefaultNumInsts, CallingConvention conv = CallingConvention::Cdecl, + HookNameMetadata&& name_info = + HookNameMetadata{ + .name = "", + }, + HookPriority&& priority = + HookPriority{ + .befores = {}, + .afters = {}, + }, + bool is_midpoint = false) : target(target), orig_ptr(orig_ptr), hook_ptr(hook_func), metadata(HookMetadata{ - .convention = conv, - .metadata = metadata, - .method_num_insts = num_insts, - .name_info = name_info, - .priority = priority, + .convention = conv, + .metadata = + InstallationMetadata{ + .need_orig = orig_ptr != nullptr, + .is_midpoint = is_midpoint, + }, + .method_num_insts = num_insts, + .name_info = name_info, + .priority = priority, #ifndef FLAMINGO_NO_REGISTRATION_CHECKS - .parameter_info = { TypeInfo::from()... }, - .return_info = TypeInfo::from(), + .parameter_info = { TypeInfo::from()... }, + .return_info = TypeInfo::from(), #endif - }), - priority_lookup_func(priority_lookup_func) { + }) { } void* target; - - private: void** orig_ptr; void* hook_ptr; HookMetadata metadata; - HookPriorityLookupFunction priority_lookup_func; -}; - -/// @brief Represents the status of a particular address -/// If hooked, will contain the same members as a hook, but additionally with a list of Hooks -/// The idea being we can O(1) install hooks (and uninstall via iterator) -struct TargetData { - HookMetadata metadata; - std::optional orig_trampoline{}; - std::list hooks{}; - Trampoline target; - - installation::Result combine(HookInfo&& incoming); }; -/// @brief To install a hook, we require a constructed HookInfo. We want the value to be thrown away, so we require an rvalue (we may also -/// forward params?). Because a HookInfo is just data, we go find our TargetInfo that matches our target. -/// Then, we attempt to install that HookInfo onto the TargetData, mutating the TargetData (but not invalidating other HookInfo referneces -/// within the list). We update the shared information within the HookInfo and perform the install as necessary. -/// The order of priorities is: lowest value runs before higher values. -/// TODO: Make priorities potentially use named IDs for cleaer ordering (before x, after y). This may require a full reassmebly of the list! -auto Install(HookInfo&& hook); } // namespace flamingo diff --git a/shared/hook-metadata.hpp b/shared/hook-metadata.hpp new file mode 100644 index 0000000..ab189bb --- /dev/null +++ b/shared/hook-metadata.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +#include "calling-convention.hpp" +#include "type-info.hpp" + +namespace flamingo { +struct Hook; +struct TargetData; + +struct InstallationMetadata { + bool need_orig; + bool is_midpoint; +}; + +/// @brief Describes the name metadata of the hook, used for lookups and priorities. +/// Lookups are described using userdata when the HookInfo is made at first. +struct HookNameMetadata { + std::string name{}; +}; + +/// @brief Represents a priority for how to align hook orderings. Note that a change in priority MAY require a full list +/// recreation. But SHOULD NOT require a hook recompile or a trampoline recompile. +struct HookPriority { + /// @brief The set of constraints for this hook to be installed before (called earlier than) + std::vector befores{}; + /// @brief The set of constraints for this hook to be installed after (called later than) + std::vector afters{}; +}; + +struct HookMetadata { + CallingConvention convention; + InstallationMetadata installation_metadata; + uint16_t method_num_insts; + HookNameMetadata name_info; + HookPriority priority; +#ifndef FLAMINGO_NO_REGISTRATION_CHECKS + std::vector parameter_info; + TypeInfo return_info; +#endif +}; + +} // namespace flamingo \ No newline at end of file diff --git a/shared/target-data.hpp b/shared/target-data.hpp new file mode 100644 index 0000000..195900b --- /dev/null +++ b/shared/target-data.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +#include "fixups.hpp" +#include "hook-data.hpp" +#include "hook-metadata.hpp" + +namespace flamingo { + +struct TargetDescriptor { + void* target; +}; + +/// @brief Represents the metadata associated with a target. POD, constructed from HookMetadata +struct TargetMetadata { + PointerWrapper target; + CallingConvention convention; + InstallationMetadata metadata; + uint16_t method_num_insts; +#ifndef FLAMINGO_NO_REGISTRATION_CHECKS + std::vector parameter_info; + TypeInfo return_info; +#endif +}; + +/// @brief Represents the status of a particular address +/// If hooked, will contain the same members as a hook, but additionally with a list of Hooks +/// The idea being we can O(1) install hooks (and uninstall via iterator) +struct TargetData { + TargetMetadata metadata; + Fixups orig; + std::list hooks{}; +}; + +/// @brief A handle to an installed hook. Used for uninstalls. +struct [[nodiscard("HookHandle instances must be used for uninstalls or explicitly thrown away")]] HookHandle { + std::list::iterator hook_location; +}; + +} // namespace flamingo diff --git a/shared/type-info.hpp b/shared/type-info.hpp new file mode 100644 index 0000000..166d8c2 --- /dev/null +++ b/shared/type-info.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +namespace flamingo { +/// @brief Represents the type info for representing a type in a hook +struct TypeInfo { + friend struct HookInfo; + // TODO: Add more members here. Ideally some form of relaxed type checking? + std::size_t size{}; + + private: + template + [[nodiscard]] inline static TypeInfo from() { + if constexpr (std::is_reference_v) { + return TypeInfo{ + .size = sizeof(void*), + }; + } else { + return TypeInfo{ + .size = sizeof(T), + }; + } + } +}; +} // namespace flamingo From 13d04871f1200984f2aa4d8c1ac8974bbc6c36f3 Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Mon, 19 Aug 2024 23:39:12 -0700 Subject: [PATCH 029/134] Add callback writing for hook installs, reinstall logic Note that we still need to expose more of the API through the targets data (perhaps even move to a .cpp file to keep it nice and local) --- shared/fixups.hpp | 4 +- shared/hook-installation-result.hpp | 140 ++++++++++++++++++++++++++++ shared/installer.hpp | 97 +++++++++++++++++++ src/fixups.cpp | 42 +++++++++ 4 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 shared/hook-installation-result.hpp create mode 100644 shared/installer.hpp diff --git a/shared/fixups.hpp b/shared/fixups.hpp index 0c47d70..8540f96 100644 --- a/shared/fixups.hpp +++ b/shared/fixups.hpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include "page-allocator.hpp" #include "util.hpp" @@ -54,6 +53,9 @@ struct ShimTarget : PointerWrapper { /// This is not the same as a Fixups' original_instructions, which are populated across ALL fixups performed. std::vector original_instructions{}; void WriteJump(void* addr); + + private: + void WriteCallback(ProtectionWriter& writer, uint32_t const* target); }; struct Fixups { diff --git a/shared/hook-installation-result.hpp b/shared/hook-installation-result.hpp new file mode 100644 index 0000000..d4caa04 --- /dev/null +++ b/shared/hook-installation-result.hpp @@ -0,0 +1,140 @@ +#pragma once +#include +#include +#include +#include +#include + +#include "calling-convention.hpp" +#include "target-data.hpp" +#include "type-info.hpp" + +namespace flamingo { + +template +struct Result { + std::variant data; + template + static Result Ok(TArgs&&... args) { + return Result{ std::in_place_index_t<1>{}, std::forward(args)... }; + } + template + static Result Err(TArgs&&... args) { + return Result{ std::in_place_index_t<0>{}, std::forward(args)... }; + } + T const& value() const { + return std::get<1>(data); + } + E const& error() const { + return std::get<0>(data); + } + bool has_value() const { + return data.index() == 1; + } +}; + +namespace installation { + +/// @brief Holds metadata about the successful install +struct Ok { + HookHandle returned_handle; +}; + +// TODO: Should we add the incoming hook IDs? + +struct MismatchReturn { + TypeInfo existing{}; + TypeInfo incoming{}; +}; + +struct MismatchParam { + size_t idx{}; + TypeInfo existing{}; + TypeInfo incoming{}; +}; + +struct MismatchTargetConv { + CallingConvention existing{}; + CallingConvention incoming{}; +}; + +struct MismatchMidpoint { + bool existing{}; + bool incoming{}; +}; + +namespace util { + +template +consteval static bool all_unique() { + if constexpr (sizeof...(TArgs) == 0ULL) { + return true; + } else { + return (!std::is_same_v && ...) && all_unique(); + } +} + +template + requires(all_unique()) +using OptionTuple = std::tuple...>; + +template + requires(all_unique()) +struct OptionalErrors { + private: + OptionTuple maybe_errors; + + public: + [[nodiscard]] constexpr bool has_error() const { + return std::apply([](auto&&... opts) { return (opts || ...); }, maybe_errors); + } + template + void assign(T&& val) { + std::get>(maybe_errors) = std::forward(val); + } +}; + +} // namespace util + +using Error = std::tuple, std::optional, std::optional, + std::optional>; + +using Result = std::variant; + +template +auto& get(Error const& obj) { + return std::get>(obj); +} + +template +void merge_optional(std::optional& lhs, std::optional const& rhs) { + if (rhs) { + lhs.emplace(*rhs); + } +} + +inline Error& operator|=(Error& lhs, Error const& rhs) { + // Merge rhs into lhs for every non-nullopt in rhs + std::apply( + [&lhs](auto&&... values) { + (merge_optional(std::get>(lhs), std::forward(values)), + ...); + }, + rhs); + return lhs; +} + +inline Result& operator|=(Result& lhs, Result&& rhs) { + // TODO: Figure out a good way of merging stuff here + // TODO: WHY ARE WE DOING IT THIS WAY + if (std::holds_alternative(rhs)) { + lhs.emplace(std::get(rhs)); + } else { + lhs.emplace(std::get(rhs)); + } + return lhs; +} + +} // namespace installation + +} // namespace flamingo diff --git a/shared/installer.hpp b/shared/installer.hpp new file mode 100644 index 0000000..791ddc0 --- /dev/null +++ b/shared/installer.hpp @@ -0,0 +1,97 @@ + + +#include +#include +#include "fixups.hpp" +#include "hook-data.hpp" +#include "hook-installation-result.hpp" +#include "page-allocator.hpp" +#include "target-data.hpp" +#include "util.hpp" + +namespace flamingo { + +constexpr static auto kHookAlignment = 16U; +constexpr static auto kNumFixupsPerInst = 2U; + +inline static std::unordered_map targets; + +/// @brief To install a hook, we require a constructed HookInfo. We want to hold exclusive ownership, so we require an +/// rvalue (we may also forward params?). Because a HookInfo is just data, we go find our TargetInfo that matches our +/// target. Then, we attempt to install that HookInfo onto the TargetData, mutating the TargetData (but not invalidating +/// other HookInfo references within the list). We update the shared information within the HookInfo and perform the +/// install as necessary. Priorities use named IDs for cleaer ordering (before x, after y). This may require a full +/// reassmebly of the list! +inline installation::Result Install(HookInfo&& hook) { + // The set of all targets of hooks that are installed to + TargetDescriptor target_info{ hook.target }; + auto hooked_target = targets.find(target_info); + if (hooked_target == targets.end()) { + // To make the first hook, we need to create the TargetData + // For leapfrog hooks, we need to do something special anyways. + // TODO: Support leapfrog hooks (where the installation space is fewer than 4U) + constexpr static auto kInstallSize = Fixups::kNormalFixupInstCount; + FLAMINGO_ASSERT(hook.metadata.method_num_insts >= Fixups::kNormalFixupInstCount); + auto target_pointer = PointerWrapper( + std::span(reinterpret_cast(hook.target), + reinterpret_cast(hook.target) + hook.metadata.method_num_insts), + PageProtectionType::kExecute | PageProtectionType::kRead); + auto result = targets.emplace( + target_info, TargetData{ .metadata = + TargetMetadata{ + .target = target_pointer, + .convention = hook.metadata.convention, + .metadata = hook.metadata.installation_metadata, + .method_num_insts = hook.metadata.method_num_insts, +#ifndef FLAMINGO_NO_REGISTRATION_CHECKS + .parameter_info = hook.metadata.parameter_info, + .return_info = hook.metadata.return_info, +#endif + }, + .orig = Fixups{ + // Our fixup target is a subspan the same size as our install size + .target = { target_pointer.Subspan(kInstallSize) }, + .fixup_inst_destination = + Allocate(kHookAlignment, + std::min(Page::PageSize, hook.metadata.method_num_insts * + sizeof(uint32_t) * kNumFixupsPerInst), + PageProtectionType::kExecute | PageProtectionType::kRead), + } }); + auto& target_data = result.first->second; + // If we want to make an orig, we fill it out now + if (hook.metadata.installation_metadata.need_orig) { + target_data.orig.PerformFixupsAndCallback(); + } + // Add the hook itself to the set of hooks we have, taking ownership + auto const hook_data_result = target_data.hooks.emplace(target_data.hooks.end(), std::move(hook)); + // Now actually INSTALL the hook at target to point to the first hook in target_data.hooks + target_data.orig.target.WriteJump(hook_data_result->hook_ptr); + return installation::Result{ std::in_place_type_t{}, + flamingo::installation::Ok{ HookHandle{ .hook_location = hook_data_result } } }; + } else { + // Install onto the target, respecting priorities. + // Note that we may need to recompile some callbacks/fixups to change things + return installation::Result{ std::in_place_type_t{}, {} }; + } +} + +/// @brief Called on a target to reinstall all targets present at that location. +/// A reinstall is done by re-performing orig fixups at the target, and rewriting a jump to the first hook. +/// All other hooks remain unchanged. +/// This function returns Ok(true) if all hooks were reinstalled correctly, Ok(false) if there were no hooks to +/// reinstall, and Error(...) otherwise. +inline Result Reinstall(TargetDescriptor target) { + using RetType = Result; + auto itr = targets.find(target); + if (itr == targets.end()) { + return RetType::Ok(false); + } + // Reinstall the orig by calling PerformFixupsAndCallback() again (as needed) + if (itr->second.metadata.metadata.need_orig) { + itr->second.orig.PerformFixupsAndCallback(); + } + // Perform the write of the jump to the first hook + itr->second.orig.target.WriteJump(itr->second.hooks.begin()->hook_ptr); + return RetType::Ok(true); +} +} // namespace flamingo \ No newline at end of file diff --git a/src/fixups.cpp b/src/fixups.cpp index 094ede3..2f6de4b 100644 --- a/src/fixups.cpp +++ b/src/fixups.cpp @@ -523,6 +523,45 @@ cs_insn debugInst(uint32_t const* inst) { return {}; } +void ShimTarget::WriteJump(void* address) { + FLAMINGO_ASSERT(!addr.empty()); + original_instructions.resize(addr.size()); + // TODO: Maybe do a span copy instead of a memcpy here at some point + std::memcpy(original_instructions.data(), address, addr.size_bytes()); + + // TODO: We also want to correctly report if we were near! Because if so, then we DON'T actually need to fixup all N + // instructions, but only need to do 1. So, the return from this should be a number of instructions that we will + // DEFINITELY need to fixup for our actual fixups call (if we wish to create fixups) + + // The writer for ensuring correct permissions and also performing the write + ProtectionWriter writer(*this); + WriteCallback(writer, reinterpret_cast(address)); +} + +void ShimTarget::WriteCallback(ProtectionWriter& writer, uint32_t const* target) { + constexpr uint32_t branch_imm_mask = 0b00000011111111111111111111111111U; + auto delta = get_untagged_pc(target) - get_untagged_pc(&writer.target.addr[writer.target_offset]); + if (std::llabs(delta) > (branch_imm_mask << 1) + 1) { + // Too far for b. Emit a br instead. + // TODO: Allow for different registers other than just x17 + // Note: If we change it here, we will also need to change it in the fixups too. + constexpr uint32_t ldr_x17 = 0x58000051U; + writer.Write(ldr_x17); + constexpr uint32_t br_x17 = 0xD61F0220U; + writer.Write(br_x17); + // And write the target + auto large_data = reinterpret_cast(target); + writer.Write(static_cast(large_data & (UINT32_MAX))); + writer.Write(static_cast((large_data >> 32) & UINT32_MAX)); + } else { + // Small enough to emit a b. + // b opcode | encoded immediate (delta >> 2) + // Note, abs(delta >> 2) must be < (1 << 26) + constexpr uint32_t b_opcode = 0b00010100000000000000000000000000U; + writer.Write(b_opcode | ((delta >> 2) & branch_imm_mask)); + } +} + void Fixups::PerformFixupsAndCallback() { FLAMINGO_ASSERT(!target.addr.empty()); FLAMINGO_ASSERT(!fixup_inst_destination.addr.empty()); @@ -630,4 +669,7 @@ void Fixups::Log() const { // know we need to leapfrog Since we would need to expand our trampoline and our original instructions regardless. // TODO: Consider a full recompile and permit late installations +// If we have a fixup that has a ret, we need to avoid the callback after the ret potentially (though it would be +// redundant anyways) + } // namespace flamingo From fed16f0c25e76ad04a22df6b5736199ba819debf Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Tue, 20 Aug 2024 00:52:16 -0700 Subject: [PATCH 030/134] Add simple uninstall, reinstall logic Also add the no_fixups void function that serves the purpose of reporting an error when orig is called when not actually valid --- shared/installer.hpp | 88 +++++++----------------------- src/fixups.cpp | 8 +++ src/installer.cpp | 127 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 69 deletions(-) create mode 100644 src/installer.cpp diff --git a/shared/installer.hpp b/shared/installer.hpp index 791ddc0..6e7e87b 100644 --- a/shared/installer.hpp +++ b/shared/installer.hpp @@ -12,9 +12,7 @@ namespace flamingo { constexpr static auto kHookAlignment = 16U; -constexpr static auto kNumFixupsPerInst = 2U; - -inline static std::unordered_map targets; +constexpr static auto kNumFixupsPerInst = 4U; /// @brief To install a hook, we require a constructed HookInfo. We want to hold exclusive ownership, so we require an /// rvalue (we may also forward params?). Because a HookInfo is just data, we go find our TargetInfo that matches our @@ -22,76 +20,28 @@ inline static std::unordered_map targets; /// other HookInfo references within the list). We update the shared information within the HookInfo and perform the /// install as necessary. Priorities use named IDs for cleaer ordering (before x, after y). This may require a full /// reassmebly of the list! -inline installation::Result Install(HookInfo&& hook) { - // The set of all targets of hooks that are installed to - TargetDescriptor target_info{ hook.target }; - auto hooked_target = targets.find(target_info); - if (hooked_target == targets.end()) { - // To make the first hook, we need to create the TargetData - // For leapfrog hooks, we need to do something special anyways. - // TODO: Support leapfrog hooks (where the installation space is fewer than 4U) - constexpr static auto kInstallSize = Fixups::kNormalFixupInstCount; - FLAMINGO_ASSERT(hook.metadata.method_num_insts >= Fixups::kNormalFixupInstCount); - auto target_pointer = PointerWrapper( - std::span(reinterpret_cast(hook.target), - reinterpret_cast(hook.target) + hook.metadata.method_num_insts), - PageProtectionType::kExecute | PageProtectionType::kRead); - auto result = targets.emplace( - target_info, TargetData{ .metadata = - TargetMetadata{ - .target = target_pointer, - .convention = hook.metadata.convention, - .metadata = hook.metadata.installation_metadata, - .method_num_insts = hook.metadata.method_num_insts, -#ifndef FLAMINGO_NO_REGISTRATION_CHECKS - .parameter_info = hook.metadata.parameter_info, - .return_info = hook.metadata.return_info, -#endif - }, - .orig = Fixups{ - // Our fixup target is a subspan the same size as our install size - .target = { target_pointer.Subspan(kInstallSize) }, - .fixup_inst_destination = - Allocate(kHookAlignment, - std::min(Page::PageSize, hook.metadata.method_num_insts * - sizeof(uint32_t) * kNumFixupsPerInst), - PageProtectionType::kExecute | PageProtectionType::kRead), - } }); - auto& target_data = result.first->second; - // If we want to make an orig, we fill it out now - if (hook.metadata.installation_metadata.need_orig) { - target_data.orig.PerformFixupsAndCallback(); - } - // Add the hook itself to the set of hooks we have, taking ownership - auto const hook_data_result = target_data.hooks.emplace(target_data.hooks.end(), std::move(hook)); - // Now actually INSTALL the hook at target to point to the first hook in target_data.hooks - target_data.orig.target.WriteJump(hook_data_result->hook_ptr); - return installation::Result{ std::in_place_type_t{}, - flamingo::installation::Ok{ HookHandle{ .hook_location = hook_data_result } } }; - } else { - // Install onto the target, respecting priorities. - // Note that we may need to recompile some callbacks/fixups to change things - return installation::Result{ std::in_place_type_t{}, {} }; - } -} +[[nodiscard]] installation::Result Install(HookInfo&& hook); /// @brief Called on a target to reinstall all targets present at that location. /// A reinstall is done by re-performing orig fixups at the target, and rewriting a jump to the first hook. /// All other hooks remain unchanged. /// This function returns Ok(true) if all hooks were reinstalled correctly, Ok(false) if there were no hooks to /// reinstall, and Error(...) otherwise. -inline Result Reinstall(TargetDescriptor target) { - using RetType = Result; - auto itr = targets.find(target); - if (itr == targets.end()) { - return RetType::Ok(false); - } - // Reinstall the orig by calling PerformFixupsAndCallback() again (as needed) - if (itr->second.metadata.metadata.need_orig) { - itr->second.orig.PerformFixupsAndCallback(); - } - // Perform the write of the jump to the first hook - itr->second.orig.target.WriteJump(itr->second.hooks.begin()->hook_ptr); - return RetType::Ok(true); -} +[[nodiscard]] Result Reinstall(TargetDescriptor target); + +/// @brief Called on an installed hook to uninstall it from the set of all hooks. +/// Functionally, this behaves as follows: +/// 1. If it is the only hook, destroys the fixups, uninstalls the hook by replacing the original instructions. Note +/// that this also destroys leapfrog hooks. +/// 2. If this is the first hook in a set of many, rewrites the target to jump to the hook past this one. Note that this +/// MAY also break leapfrog hooks, if this hook was installed as a branch but the next hook needs to be larger. +/// 3. If this is the last hook, makes the previous hook's orig point to the fixups directly, or to the no_fixups +/// function. +/// 4. If this is a hook in the middle, the hook before us's orig will point to the next hook's hook function. +/// After all that is done, the iterator is removed from the list of all hooks, and if empty, the entry from the targets +/// map is destroyed. Note that this invalidates all other held HookHandles to the SAME entry. Other entries will not be +/// invalidated. +/// @returns Ok(true) if the target remains, Ok(false) if the full target was removed from the map, Error(false) if no +/// target was found from this handle, Error(true) if a remapping failure happened. +[[nodiscard]] Result Uninstall(HookHandle handle); } // namespace flamingo \ No newline at end of file diff --git a/src/fixups.cpp b/src/fixups.cpp index 2f6de4b..3c0e7a4 100644 --- a/src/fixups.cpp +++ b/src/fixups.cpp @@ -659,6 +659,14 @@ void Fixups::Log() const { // To log fixups, we walk the instructions and perform a translation for each } +void Fixups::Uninstall() { + // To perform an uninstall, we just iterate over all of our original instructions and copy them all back to the target + ProtectionWriter writer(target); + for (auto const inst : original_instructions) { + writer.Write(inst); + } +} + // TODO: We should consider an optimization where we have a location for fixup data instead of inling all fixups. // This would allow us to write out actual assembly verbatim and then have ldrs and whatnot for grabbing the data // This would save a few instructions per all of the fixups, since we wouldn't need to branch over the data diff --git a/src/installer.cpp b/src/installer.cpp new file mode 100644 index 0000000..96a2dc9 --- /dev/null +++ b/src/installer.cpp @@ -0,0 +1,127 @@ +#include "installer.hpp" +#include +#include +#include "util.hpp" + +namespace { +/// @brief The set of all targets hooked. An ordered map so we can perform large-scale walks by doing binary search. +inline static std::map targets; + +/// @brief This function is assigned to the orig of a hook when the hook in question has no fixups written. +void no_fixups() { + FLAMINGO_ABORT("CALL TO ORIG ON FUNCTION WHERE NO ORIG IS PRESENT WOULD RESULT IN AN EXCEPTION!"); +} +} // namespace + +namespace flamingo { +installation::Result Install(HookInfo&& hook) { + // The set of all targets of hooks that are installed to + TargetDescriptor target_info{ hook.target }; + auto hooked_target = targets.find(target_info); + if (hooked_target == targets.end()) { + // To make the first hook, we need to create the TargetData + // For leapfrog hooks, we need to do something special anyways. + // TODO: Support leapfrog hooks (where the installation space is fewer than 4U) + constexpr static auto kInstallSize = Fixups::kNormalFixupInstCount; + FLAMINGO_ASSERT(hook.metadata.method_num_insts >= Fixups::kNormalFixupInstCount); + auto target_pointer = PointerWrapper( + std::span(reinterpret_cast(hook.target), + reinterpret_cast(hook.target) + hook.metadata.method_num_insts), + PageProtectionType::kExecute | PageProtectionType::kRead); + auto result = targets.emplace( + target_info, TargetData{ .metadata = + TargetMetadata{ + .target = target_pointer, + .convention = hook.metadata.convention, + .metadata = hook.metadata.installation_metadata, + .method_num_insts = hook.metadata.method_num_insts, +#ifndef FLAMINGO_NO_REGISTRATION_CHECKS + .parameter_info = hook.metadata.parameter_info, + .return_info = hook.metadata.return_info, +#endif + }, + .orig = Fixups{ + // Our fixup target is a subspan the same size as our install size + .target = { target_pointer.Subspan(kInstallSize) }, + .fixup_inst_destination = + Allocate(kHookAlignment, + std::min(Page::PageSize, hook.metadata.method_num_insts * + sizeof(uint32_t) * kNumFixupsPerInst), + PageProtectionType::kExecute | PageProtectionType::kRead), + } }); + auto& target_data = result.first->second; + *hook.orig_ptr = reinterpret_cast(&no_fixups); + // If we want to make an orig, we fill it out now + if (hook.metadata.installation_metadata.need_orig) { + target_data.orig.PerformFixupsAndCallback(); + *hook.orig_ptr = target_data.orig.fixup_inst_destination.addr.data(); + } + // Add the hook itself to the set of hooks we have, taking ownership + auto const hook_data_result = target_data.hooks.emplace(target_data.hooks.end(), std::move(hook)); + // Now actually INSTALL the hook at target to point to the first hook in target_data.hooks + target_data.orig.target.WriteJump(hook_data_result->hook_ptr); + return installation::Result{ std::in_place_type_t{}, + flamingo::installation::Ok{ HookHandle{ .hook_location = hook_data_result } } }; + } else { + // Install onto the target, respecting priorities. + // Note that we may need to recompile some callbacks/fixups to change things + return installation::Result{ std::in_place_type_t{}, {} }; + } +} + +Result Reinstall(TargetDescriptor target) { + using RetType = Result; + auto itr = targets.find(target); + if (itr == targets.end()) { + return RetType::Ok(false); + } + // Reinstall the orig by calling PerformFixupsAndCallback() again (as needed) + if (itr->second.metadata.metadata.need_orig) { + itr->second.orig.PerformFixupsAndCallback(); + } + // Perform the write of the jump to the first hook + itr->second.orig.target.WriteJump(itr->second.hooks.begin()->hook_ptr); + return RetType::Ok(true); +} + +Result Uninstall(HookHandle handle) { + using RetType = Result; + // Find the target entry. Note that this assumes the handle is not invalidated. + auto target_entry = targets.find(TargetDescriptor(handle.hook_location->target)); + if (target_entry == targets.end()) { + return RetType::Err(false); + } + // 1. If it is the only hook, destroys the fixups, uninstalls the hook by replacing the original instructions. Note + // that this also destroys leapfrog hooks. + if (target_entry->second.hooks.size() == 1) { + target_entry->second.orig.Uninstall(); + // At this point the original memory at our target is restored, we are safe to clear out the target entry here and + // return + // TODO: Invalidate leapfrog entries + // TODO: Cleanup whatever dangling pointers we would have here (the fixup pointer being one of them) + targets.erase(target_entry); + return RetType::Ok(false); + } + // 2. If this is the first hook in a set of many, rewrites the target to jump to the hook past this one. Note that + // this MAY also break leapfrog hooks, if this hook was installed as a branch but the next hook needs to be larger. + if (handle.hook_location == target_entry->second.hooks.begin()) { + target_entry->second.orig.target.WriteJump(std::next(handle.hook_location)->hook_ptr); + } + // 3. If this is the last hook, makes the previous hook's orig point to the fixups directly, or to the no_fixups + // function. + else if (std::next(handle.hook_location) == target_entry->second.hooks.end()) { + *std::prev(handle.hook_location)->orig_ptr = target_entry->second.metadata.metadata.need_orig + ? target_entry->second.orig.fixup_inst_destination.addr.data() + : reinterpret_cast(&no_fixups); + } + // 4. If this is a hook in the middle, the hook before us's orig will point to the next hook's hook function. + else { + *std::prev(handle.hook_location)->orig_ptr = std::next(handle.hook_location)->hook_ptr; + } + // After all that is done, the iterator is removed from the list of all hooks, and if empty, the entry from the + // targets map is destroyed. Note that this invalidates all other held HookHandles to the SAME entry. Other entries + // will not be invalidated. + target_entry->second.hooks.erase(handle.hook_location); + return RetType::Ok(true); +} +} // namespace flamingo \ No newline at end of file From 9c3b10f6287d17ef989d996f537e0bf14198b092 Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Tue, 20 Aug 2024 01:18:19 -0700 Subject: [PATCH 031/134] Add more correct installation logic and simple error support, missing functions Still need to implement correct installations which respect priorities on top of existing targets, but this should serve as a starting ground for getting the api tests up and running. Also need to add the test logic to build the installer.cpp file (among other things) and probably just generally set everything up through cmake instead --- shared/fixups.hpp | 3 ++ shared/hook-data.hpp | 4 +++ shared/hook-installation-result.hpp | 49 ++++++++++++++--------------- shared/installer.hpp | 1 + shared/page-allocator.hpp | 5 +++ src/installer.cpp | 36 ++++++++++++--------- 6 files changed, 58 insertions(+), 40 deletions(-) diff --git a/shared/fixups.hpp b/shared/fixups.hpp index 8540f96..ab694c6 100644 --- a/shared/fixups.hpp +++ b/shared/fixups.hpp @@ -73,6 +73,9 @@ struct Fixups { // For the input target, walks over the size passed in to the span // For each instruction listed, fixes it up void PerformFixupsAndCallback(); + /// @brief Uninstalls the fixups for this target, rewriting the original instructions back to the target. + /// TODO: Eventually this will also clear the allocation at fixup_inst_destination, but for now, we will leak it. + void Uninstall(); }; // TODO: DO NOT EXPOSE THIS SYMBOL (USE IT FOR TESTING ONLY) diff --git a/shared/hook-data.hpp b/shared/hook-data.hpp index 3ee795b..9f47125 100644 --- a/shared/hook-data.hpp +++ b/shared/hook-data.hpp @@ -55,6 +55,10 @@ struct HookInfo { }) { } + void assign_orig(void* ptr) { + if (orig_ptr != nullptr) *orig_ptr = ptr; + } + void* target; void** orig_ptr; void* hook_ptr; diff --git a/shared/hook-installation-result.hpp b/shared/hook-installation-result.hpp index d4caa04..0a0fcaa 100644 --- a/shared/hook-installation-result.hpp +++ b/shared/hook-installation-result.hpp @@ -6,6 +6,7 @@ #include #include "calling-convention.hpp" +#include "hook-metadata.hpp" #include "target-data.hpp" #include "type-info.hpp" @@ -40,6 +41,24 @@ struct Ok { HookHandle returned_handle; }; +/// @brief The general base type for reporting hook errors. Holds the ID of the failing hook. +struct HookErrorInfo { + HookNameMetadata installing_hook; + HookErrorInfo(HookNameMetadata const& m) : installing_hook(m) {} +}; + +/// @brief An error when the target of a hook install is null. +struct TargetIsNull : HookErrorInfo { + TargetIsNull(HookNameMetadata const& m) : HookErrorInfo(m) {} +}; +/// @brief An error when the target method is described as too small for the hook strategy being employed. +struct TargetTooSmall : HookErrorInfo { + TargetTooSmall(HookMetadata const& m, uint_fast16_t needed) + : HookErrorInfo(m.name_info), actual_num_insts(m.method_num_insts), needed_num_insts(needed) {} + uint_fast16_t actual_num_insts; + uint_fast16_t needed_num_insts; +}; + // TODO: Should we add the incoming hook IDs? struct MismatchReturn { @@ -96,10 +115,12 @@ struct OptionalErrors { } // namespace util -using Error = std::tuple, std::optional, std::optional, - std::optional>; +// Can be one of many cases. +// The first obvious case is that the target is nullptr +using Error = std::variant; -using Result = std::variant; +// using Result = std::variant; +using Result = flamingo::Result; template auto& get(Error const& obj) { @@ -113,28 +134,6 @@ void merge_optional(std::optional& lhs, std::optional const& rhs) { } } -inline Error& operator|=(Error& lhs, Error const& rhs) { - // Merge rhs into lhs for every non-nullopt in rhs - std::apply( - [&lhs](auto&&... values) { - (merge_optional(std::get>(lhs), std::forward(values)), - ...); - }, - rhs); - return lhs; -} - -inline Result& operator|=(Result& lhs, Result&& rhs) { - // TODO: Figure out a good way of merging stuff here - // TODO: WHY ARE WE DOING IT THIS WAY - if (std::holds_alternative(rhs)) { - lhs.emplace(std::get(rhs)); - } else { - lhs.emplace(std::get(rhs)); - } - return lhs; -} - } // namespace installation } // namespace flamingo diff --git a/shared/installer.hpp b/shared/installer.hpp index 6e7e87b..45d55bd 100644 --- a/shared/installer.hpp +++ b/shared/installer.hpp @@ -30,6 +30,7 @@ constexpr static auto kNumFixupsPerInst = 4U; [[nodiscard]] Result Reinstall(TargetDescriptor target); /// @brief Called on an installed hook to uninstall it from the set of all hooks. +/// Note that uninstalling a hook never requires a recompile of the set, as no priorities are altered. /// Functionally, this behaves as follows: /// 1. If it is the only hook, destroys the fixups, uninstalls the hook by replacing the original instructions. Note /// that this also destroys leapfrog hooks. diff --git a/shared/page-allocator.hpp b/shared/page-allocator.hpp index e1c8ed6..c9975a6 100644 --- a/shared/page-allocator.hpp +++ b/shared/page-allocator.hpp @@ -1,6 +1,7 @@ #pragma once #include #include +#include #include #include #include "util.hpp" @@ -59,6 +60,10 @@ struct PointerWrapper { static_cast(protection), std::strerror(errno)); } } + /// @brief Returns a subspan that is of the (potentially shrunken) size. + PointerWrapper Subspan(size_t n) const { + return PointerWrapper(addr.front(std::min(n, addr.size())), protection); + } }; PointerWrapper Allocate(uint_fast16_t alignment, uint_fast16_t size, PageProtectionType protection); diff --git a/src/installer.cpp b/src/installer.cpp index 96a2dc9..0f07534 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -3,19 +3,23 @@ #include #include "util.hpp" +/// @brief This function is assigned to the orig of a hook when the hook in question has no fixups written. +extern "C" void no_fixups() { + FLAMINGO_ABORT( + "CALL TO ORIG ON FUNCTION WHERE NO ORIG IS PRESENT! THIS WOULD NORMALLY RESULT IN A REALLY ANNOYING JUMP TO 0!"); +} namespace { /// @brief The set of all targets hooked. An ordered map so we can perform large-scale walks by doing binary search. inline static std::map targets; - -/// @brief This function is assigned to the orig of a hook when the hook in question has no fixups written. -void no_fixups() { - FLAMINGO_ABORT("CALL TO ORIG ON FUNCTION WHERE NO ORIG IS PRESENT WOULD RESULT IN AN EXCEPTION!"); -} } // namespace namespace flamingo { installation::Result Install(HookInfo&& hook) { - // The set of all targets of hooks that are installed to + // Null targets to install to are prohibited, but null hook functions are allowed (and will most likely cause + // horrible crashes when called) + if (hook.target == nullptr) { + return installation::Result::Err(installation::TargetIsNull{ hook.metadata.name_info }); + } TargetDescriptor target_info{ hook.target }; auto hooked_target = targets.find(target_info); if (hooked_target == targets.end()) { @@ -23,7 +27,9 @@ installation::Result Install(HookInfo&& hook) { // For leapfrog hooks, we need to do something special anyways. // TODO: Support leapfrog hooks (where the installation space is fewer than 4U) constexpr static auto kInstallSize = Fixups::kNormalFixupInstCount; - FLAMINGO_ASSERT(hook.metadata.method_num_insts >= Fixups::kNormalFixupInstCount); + if (hook.metadata.method_num_insts >= Fixups::kNormalFixupInstCount) { + return installation::Result::Err(installation::TargetTooSmall{ hook.metadata, Fixups::kNormalFixupInstCount }); + } auto target_pointer = PointerWrapper( std::span(reinterpret_cast(hook.target), reinterpret_cast(hook.target) + hook.metadata.method_num_insts), @@ -50,18 +56,17 @@ installation::Result Install(HookInfo&& hook) { PageProtectionType::kExecute | PageProtectionType::kRead), } }); auto& target_data = result.first->second; - *hook.orig_ptr = reinterpret_cast(&no_fixups); + hook.assign_orig(reinterpret_cast(&no_fixups)); // If we want to make an orig, we fill it out now if (hook.metadata.installation_metadata.need_orig) { target_data.orig.PerformFixupsAndCallback(); - *hook.orig_ptr = target_data.orig.fixup_inst_destination.addr.data(); + hook.assign_orig(target_data.orig.fixup_inst_destination.addr.data()); } // Add the hook itself to the set of hooks we have, taking ownership auto const hook_data_result = target_data.hooks.emplace(target_data.hooks.end(), std::move(hook)); // Now actually INSTALL the hook at target to point to the first hook in target_data.hooks target_data.orig.target.WriteJump(hook_data_result->hook_ptr); - return installation::Result{ std::in_place_type_t{}, - flamingo::installation::Ok{ HookHandle{ .hook_location = hook_data_result } } }; + return installation::Result::Ok(flamingo::installation::Ok{ HookHandle{ .hook_location = hook_data_result } }); } else { // Install onto the target, respecting priorities. // Note that we may need to recompile some callbacks/fixups to change things @@ -110,13 +115,14 @@ Result Uninstall(HookHandle handle) { // 3. If this is the last hook, makes the previous hook's orig point to the fixups directly, or to the no_fixups // function. else if (std::next(handle.hook_location) == target_entry->second.hooks.end()) { - *std::prev(handle.hook_location)->orig_ptr = target_entry->second.metadata.metadata.need_orig - ? target_entry->second.orig.fixup_inst_destination.addr.data() - : reinterpret_cast(&no_fixups); + std::prev(handle.hook_location) + ->assign_orig(target_entry->second.metadata.metadata.need_orig + ? target_entry->second.orig.fixup_inst_destination.addr.data() + : reinterpret_cast(&no_fixups)); } // 4. If this is a hook in the middle, the hook before us's orig will point to the next hook's hook function. else { - *std::prev(handle.hook_location)->orig_ptr = std::next(handle.hook_location)->hook_ptr; + std::prev(handle.hook_location)->assign_orig(std::next(handle.hook_location)->hook_ptr); } // After all that is done, the iterator is removed from the list of all hooks, and if empty, the entry from the // targets map is destroyed. Note that this invalidates all other held HookHandles to the SAME entry. Other entries From 4f22dd6654db46eb2fe214995c38d1c8c288ea46 Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Tue, 20 Aug 2024 22:22:00 -0700 Subject: [PATCH 032/134] Fix tests to use ShimTarget --- test/main.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/main.cpp b/test/main.cpp index f48dd82..5227334 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -38,10 +38,10 @@ static decltype(auto) test_near(std::span target, [[maybe_unused]] uin auto near_data = alloc_near(target, trampolineSize); fmt::println("NEAR TRAMPOLINE RESULT: {}", fmt::ptr(near_data.fixups.data())); flamingo::Fixups fixups{ - .target = flamingo::PointerWrapper( - std::span(near_data.target.begin(), near_data.target.begin() + hookSizeNumInsts - 1), - flamingo::PageProtectionType::kExecute | flamingo::PageProtectionType::kRead | - flamingo::PageProtectionType::kWrite), + .target = { flamingo::PointerWrapper{ + std::span(near_data.target.begin(), near_data.target.begin() + hookSizeNumInsts - 1), + flamingo::PageProtectionType::kExecute | flamingo::PageProtectionType::kRead | + flamingo::PageProtectionType::kWrite } }, .fixup_inst_destination = flamingo::PointerWrapper( near_data.fixups, flamingo::PageProtectionType::kExecute | flamingo::PageProtectionType::kRead | flamingo::PageProtectionType::kWrite), @@ -75,9 +75,9 @@ static decltype(auto) test_far(std::span target, [[maybe_unused]] uint auto actual_target = alloc_far(fixup_ptr, target); fmt::println("FAR TRAMPOLINE RESULT: {}", fmt::ptr(actual_target.data())); flamingo::Fixups fixups{ - .target = flamingo::PointerWrapper( - std::span(actual_target.begin(), actual_target.begin() + hookSizeNumInsts - 1), - flamingo::PageProtectionType::kExecute | flamingo::PageProtectionType::kRead), + .target = { flamingo::PointerWrapper{ + std::span(actual_target.begin(), actual_target.begin() + hookSizeNumInsts - 1), + flamingo::PageProtectionType::kExecute | flamingo::PageProtectionType::kRead } }, .fixup_inst_destination = fixup_ptr, }; printf("TRAMPOLINE: %p\n", &fixup_ptr.addr[0]); From 112236fb15c20354e7624a6b05086344b75c7180 Mon Sep 17 00:00:00 2001 From: Sc2ad Date: Mon, 10 Mar 2025 00:03:30 -0700 Subject: [PATCH 033/134] Add simple shim API and simple test that does not yet compile --- shared/capi.h | 37 +++++++++++++++++++++++++++++++++++++ test/api.cpp | 19 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 shared/capi.h create mode 100644 test/api.cpp diff --git a/shared/capi.h b/shared/capi.h new file mode 100644 index 0000000..6a5dfd4 --- /dev/null +++ b/shared/capi.h @@ -0,0 +1,37 @@ +#pragma once + +#include +// The flamingo C API + +#ifdef __cplusplus +extern "C" { +#endif +// General C API: +// Create a hook (get back a handle) +// Uninstall/remove a hook (with the handle) + +// Creation of the hook is actually a non-trivial process. +// We require lots of meta information, potentially. + +// Check if a location is hooked +// Additionally return a window over the original data (to allow for xref traces) +// Reinstall all hooks at a target, or a range + +/// @brief Returned from a call to query if a region is hooked, and what the original instructions at that location are. +typedef struct { + /// @brief The size of the hook present at this address, in number of instructions. + uint32_t hook_size; + /// @brief A pointer to the hook's original instructions. Safe to dereference up to 'hook_size'. + uint32_t const* original_instructions; +} FlamingoOriginalInstructionsResult; + +/// @brief Returns a FlamingoOriginalInstructionsResult. +/// The hook_size of the returned instance will be 0 if the provided address is not the START of an installed hook. +/// The returned original_instructions pointer is safe to read in the range [0..hook_size). +/// This function is commonly used for ensuring correct results when going through an xref trace, or validating real +/// instructions. +FlamingoOriginalInstructionsResult flamingo_orig_for(uint32_t const* addr); + +#ifdef __cplusplus +} +#endif diff --git a/test/api.cpp b/test/api.cpp new file mode 100644 index 0000000..e887af1 --- /dev/null +++ b/test/api.cpp @@ -0,0 +1,19 @@ +#include +#include "calling-convention.hpp" +#include "hook-data.hpp" +#include "installer.hpp" + +int to_call(int x) {} + +int (*to_call_orig)(int); + +void test() { + auto result = flamingo::Install(flamingo::HookInfo{ &to_call, (void*)0x1234, &to_call_orig }); + + flamingo::HookInfo my_hook{ &to_call, nullptr, &to_call_orig }; + flamingo::HookInfo info(&to_call, (void*)nullptr, &to_call_orig, flamingo::CallingConvention::Cdecl, + flamingo::HookNameMetadata{ + .name = "to_call_hook", + }); + flamingo::Install(std::move(info)); +} From 4eb36f0d3863fc8c91d5a385a3d1e2aac6bcc6ba Mon Sep 17 00:00:00 2001 From: Adam ? Date: Tue, 29 Apr 2025 01:14:28 -0700 Subject: [PATCH 034/134] Add cmake strategy for fixup-test --- .github/workflows/build-ndk.yml | 35 ++++------ CMakeLists.txt | 119 +++++++++----------------------- 2 files changed, 46 insertions(+), 108 deletions(-) diff --git a/.github/workflows/build-ndk.yml b/.github/workflows/build-ndk.yml index 33f06e8..2790e2d 100644 --- a/.github/workflows/build-ndk.yml +++ b/.github/workflows/build-ndk.yml @@ -24,16 +24,14 @@ jobs: with: submodules: true + - uses: seanmiddleditch/gha-setup-ninja@v3 + - name: Set up Clang uses: egor-tensin/setup-clang@v1 with: version: latest platform: x64 - - uses: actions/setup-python@v5 - with: - python-version: '3.8' - # Get QPM so we can bring in dependencies - name: QPM Rust Action uses: Fernthedev/qpm-action@v1 @@ -47,32 +45,25 @@ jobs: - name: QPM Collapse run: qpm-rust collapse - # Get the CORRECT version of capstone - - name: Get Capstone - run: pip install capstone==5.0.1 -U --user - - - name: General info + - name: CMake with LOCAL_TEST enabled run: | - python3 --version - clang --version - ls ~/.local/lib/python3.8/site-packages/capstone/include + CC=clang CXX=clang cmake -B build -DTEST_BUILD=1 -GNinja - - name: Build simple test + - name: Build run: | - mkdir -p build-linux - clang++ test/main.cpp src/fixups.cpp src/page-allocator.cpp -o build-linux/test -std=c++20 -Ishared -I$(realpath ~/.local/lib/python3.8/site-packages/capstone/include) -l:libcapstone.a -Iextern/includes/fmt/fmt/include -L$(realpath ~/.local/lib/python3.8/site-packages/capstone/lib) -DFMT_HEADER_ONLY -Wall -Wextra -Werror -g - + ninja -C build + # Run the test to ensure validity - name: Run simple test - run: ./build-linux/test 2>&1 | tee ./build-linux/test-output.txt + run: ./build/fixup-test 2>&1 | tee ./build/test-output.txt # Upload test artifact - name: Upload test artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: always() with: name: test_output.txt - path: ./build-linux/test-output.txt + path: ./build/test-output.txt if-no-files-found: error build: runs-on: ubuntu-latest @@ -125,21 +116,21 @@ jobs: echo ::set-output name=NAME::"${files[0]}" - name: Upload non-debug artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: ${{ steps.libname.outputs.NAME }} path: ./build/${{ steps.libname.outputs.NAME }} if-no-files-found: error - name: Upload debug artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: debug_${{ steps.libname.outputs.NAME }} path: ./build/debug_${{ steps.libname.outputs.NAME }} if-no-files-found: error - name: Upload qmod artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: ${{env.qmodName}}.qmod path: ./${{ env.qmodName }}.qmod diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d8ae7d..4f5eb0d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,25 +1,4 @@ -# include some defines automatically made by qpm -include(qpm_defines.cmake) - cmake_minimum_required(VERSION 3.22) -project(${COMPILE_ID}) -# Get GSL -# include(FetchContent) - -# FetchContent_Declare(GSL -# GIT_REPOSITORY "https://github.com/microsoft/GSL" -# GIT_TAG "v4.0.0" -# ) - -# FetchContent_MakeAvailable(GSL) - -# FetchContent_Declare( -# googletest -# URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip -# ) -# # For Windows: Prevent overriding the parent project's compiler/linker settings -# set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) -# FetchContent_MakeAvailable(googletest) # c++ standard set(CMAKE_CXX_STANDARD 20) @@ -28,80 +7,48 @@ set(CMAKE_CXX_STANDARD_REQUIRED 20) # define that stores the actual source directory set(SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src) set(INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/include) +set(EXTERN_DIR ${CMAKE_CURRENT_SOURCE_DIR}/extern) +set(SHARED_DIR ${CMAKE_CURRENT_SOURCE_DIR}/shared) -# compile options used -add_compile_options(-fno-rtti -flto -fPIE -fPIC -fno-exceptions -fcolor-diagnostics) -add_compile_definitions(MOD_VERSION="${MOD_VERSION}") -add_compile_definitions(MOD_ID="${MOD_ID}") -# TODO: For now, this is the only safe way we can build a binary -add_compile_definitions(FLAMINGO_HEADER_ONLY) -add_compile_options(-Wall -Wextra -Werror -Wpedantic) -# compile definitions used - -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - -if (DEFINED TEST_BUILD) - MESSAGE(STATUS "Compiling with test defines") +if (NOT DEFINED CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Debug") endif() -add_library( - ${COMPILE_ID} - SHARED -) - -# recursively get all src files -# RECURSE_FILES(cpp_file_list_utils ${SOURCE_DIR}/*.cpp) -# RECURSE_FILES(c_file_list_utils ${SOURCE_DIR}/*.c) -target_sources(${COMPILE_ID} PRIVATE ${SOURCE_DIR}/trampoline/trampoline.cpp ${SOURCE_DIR}/trampoline/trampoline-allocator.cpp) -# target_sources(${COMPILE_ID} PRIVATE ${c_file_list_utils}) +add_compile_options(-fno-rtti -fPIE -fPIC -fno-exceptions -fcolor-diagnostics) +add_link_options(-lstdc++ -lm) -# if (DEFINED TEST_BUILD) -# RECURSE_FILES(cpp_file_list_tests ${SOURCE_DIR}/tests/*.cpp) -# RECURSE_FILES(c_file_list_tests ${SOURCE_DIR}/tests/*.c) -# target_sources(${COMPILE_ID} PRIVATE ${cpp_file_list_tests}) -# target_sources(${COMPILE_ID} PRIVATE ${c_file_list_tests}) -# endif() +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -# add root dir as include dir -target_include_directories(${COMPILE_ID} PRIVATE ${CMAKE_SOURCE_DIR}) -# add src dir as include dir -target_include_directories(${COMPILE_ID} PRIVATE ${SOURCE_DIR}) -# add include dir as include dir -target_include_directories(${COMPILE_ID} PRIVATE ${INCLUDE_DIR}) -# add shared dir as include dir -target_include_directories(${COMPILE_ID} PUBLIC ${SHARED_DIR}) -target_link_libraries(${COMPILE_ID} PRIVATE -llog) +# Flamingo static build always exists +add_library(flamingo-static ${SOURCE_DIR}/fixups.cpp ${SOURCE_DIR}/installer.cpp ${SOURCE_DIR}/page-allocator.cpp) +target_compile_options(flamingo-static PRIVATE -Wall -Wextra -Werror -Wpedantic -DFMT_HEADER_ONLY) -include(extern.cmake) -MESSAGE(STATUS "extern added!") +option(TEST_BUILD "Enable local executable test builds for sanity checks" ON) -add_custom_command(TARGET ${COMPILE_ID} POST_BUILD - COMMAND ${CMAKE_STRIP} -g -S -d --strip-all - "lib${COMPILE_ID}.so" -o "stripped_lib${COMPILE_ID}.so" - COMMENT "Strip debug symbols done on final binary.") +# If we are making a test build, go into this block instead +if (TEST_BUILD) + project(flamingo-test) -add_custom_command(TARGET ${COMPILE_ID} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E rename lib${COMPILE_ID}.so debug_lib${COMPILE_ID}.so - COMMENT "Rename the lib to debug_ since it has debug symbols" - ) + # Fetch capstone (v5.0.1) via: pip install capstone==5.0.1 -U --user + include(FetchContent) + FetchContent_Declare( + capstone + GIT_REPOSITORY https://github.com/capstone-engine/capstone.git + GIT_TAG 5.0.1 + ) -add_custom_command(TARGET ${COMPILE_ID} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E rename stripped_lib${COMPILE_ID}.so lib${COMPILE_ID}.so - COMMENT "Rename the stripped lib to regular" - ) + set(CAPSTONE_ARCHITECTURE_DEFAULT OFF) + set(CAPSTONE_ARM64_SUPPORT ON) -enable_testing() + FetchContent_MakeAvailable(capstone) + target_link_libraries(flamingo-static PUBLIC capstone) + target_include_directories(flamingo-static PUBLIC ${SHARED_DIR} ${capstone_SOURCE_DIR}/include ${EXTERN_DIR}/includes/fmt/fmt/include) -add_compile_options(-Wall -Wextra -Werror -Wpedantic) -# add_executable( -# test -# test/main.cpp -# ) -# target_include_directories(test PUBLIC ${SHARED_DIR}) -# target_link_libraries( -# test -# GTest::gtest_main -# ) + add_compile_definitions(TEST_BUILD) + MESSAGE(STATUS "Compiling test build") -# include(GoogleTest) -# gtest_discover_tests(test) + add_executable(fixup-test ${CMAKE_CURRENT_SOURCE_DIR}/test/main.cpp) + target_link_libraries(fixup-test PRIVATE flamingo-static) +else() + MESSAGE(ERROR "No NDK support yet! Try using -DTEST_BUILD=ON") +endif() From f891a71171284dae042b1a0e888685d93a57ed99 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Tue, 29 Apr 2025 01:18:22 -0700 Subject: [PATCH 035/134] Add simple installation logic, get full bundle compiling Note that we still do not correctly model priorities here, and we also do not have api tests. The C API is also woefully under-represented so far, but ultimately, we have the core testing stuff set up. We still need to add validation for the installation failures and improved logging to ensure reasonable visibility of the hook graph (in some pretty-ish way) And also we want to have tests that run on arm64 too :-) --- shared/hook-installation-result.hpp | 33 ++++++++++-- shared/hook-metadata.hpp | 1 + shared/page-allocator.hpp | 2 +- shared/target-data.hpp | 7 ++- src/installer.cpp | 79 ++++++++++++++++++++++++----- test/main.cpp | 1 + 6 files changed, 103 insertions(+), 20 deletions(-) diff --git a/shared/hook-installation-result.hpp b/shared/hook-installation-result.hpp index 0a0fcaa..f675c8d 100644 --- a/shared/hook-installation-result.hpp +++ b/shared/hook-installation-result.hpp @@ -12,16 +12,32 @@ namespace flamingo { +template +struct is_variant { + constexpr static bool value = false; +}; + +template +struct is_variant> { + constexpr static bool value = true; +}; + template struct Result { std::variant data; template static Result Ok(TArgs&&... args) { - return Result{ std::in_place_index_t<1>{}, std::forward(args)... }; + return Result{ std::variant(std::in_place_index_t<1>{}, std::forward(args)...) }; } template static Result Err(TArgs&&... args) { - return Result{ std::in_place_index_t<0>{}, std::forward(args)... }; + return Result{ std::variant(std::in_place_index_t<0>{}, std::forward(args)...) }; + } + // Helper function for if E is a variant (we have multiple errors and need to construct one) + template + requires (is_variant::value) + static Result ErrAt(TArgs&&... args) { + return Result{ std::variant(std::in_place_index_t<0>{}, E(std::in_place_type_t{}, std::forward(args)...))}; } T const& value() const { return std::get<1>(data); @@ -58,6 +74,15 @@ struct TargetTooSmall : HookErrorInfo { uint_fast16_t actual_num_insts; uint_fast16_t needed_num_insts; }; +/// @brief An error when the target method is impossible to install given its priorities and other hooks to install it onto. +struct TargetBadPriorities : HookErrorInfo { + // TODO: Add a bunch of stuff here + TargetBadPriorities(HookMetadata const& m) : HookErrorInfo(m.name_info) {} +}; +/// @brief An error when the target method has some validation failure with respect to the metadata it holds. +struct TargetMismatch : HookErrorInfo { + TargetMismatch(HookMetadata const& m) : HookErrorInfo(m.name_info) {} +}; // TODO: Should we add the incoming hook IDs? @@ -116,10 +141,8 @@ struct OptionalErrors { } // namespace util // Can be one of many cases. -// The first obvious case is that the target is nullptr -using Error = std::variant; +using Error = std::variant; -// using Result = std::variant; using Result = flamingo::Result; template diff --git a/shared/hook-metadata.hpp b/shared/hook-metadata.hpp index ab189bb..b90d7d3 100644 --- a/shared/hook-metadata.hpp +++ b/shared/hook-metadata.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include diff --git a/shared/page-allocator.hpp b/shared/page-allocator.hpp index c9975a6..dd92025 100644 --- a/shared/page-allocator.hpp +++ b/shared/page-allocator.hpp @@ -62,7 +62,7 @@ struct PointerWrapper { } /// @brief Returns a subspan that is of the (potentially shrunken) size. PointerWrapper Subspan(size_t n) const { - return PointerWrapper(addr.front(std::min(n, addr.size())), protection); + return PointerWrapper(addr.first(std::min(n, addr.size())), protection); } }; diff --git a/shared/target-data.hpp b/shared/target-data.hpp index 195900b..d79cf96 100644 --- a/shared/target-data.hpp +++ b/shared/target-data.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -25,12 +26,16 @@ struct TargetMetadata { #endif }; +inline bool operator<(TargetDescriptor const& lhs, TargetDescriptor const& rhs) { + return reinterpret_cast(lhs.target) < reinterpret_cast(rhs.target); +} + /// @brief Represents the status of a particular address /// If hooked, will contain the same members as a hook, but additionally with a list of Hooks /// The idea being we can O(1) install hooks (and uninstall via iterator) struct TargetData { TargetMetadata metadata; - Fixups orig; + Fixups fixups; std::list hooks{}; }; diff --git a/src/installer.cpp b/src/installer.cpp index 0f07534..33c07a5 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -1,6 +1,12 @@ #include "installer.hpp" #include +#include #include +#include +#include "hook-data.hpp" +#include "hook-installation-result.hpp" +#include "hook-metadata.hpp" +#include "target-data.hpp" #include "util.hpp" /// @brief This function is assigned to the orig of a hook when the hook in question has no fixups written. @@ -14,6 +20,27 @@ inline static std::map targets } // namespace namespace flamingo { +Result::iterator, installation::TargetBadPriorities> find_suitable_priority_location_for(std::list& hooks, HookPriority priority) { + // Install onto the target, respecting priorities. + // Note that we may need to recompile some callbacks/fixups to change things + // 1. Topological sort on our hooks that exist here by priority + // - Find a suitable location where we can fit (note that we MAY need to recompile and move hooks around in order to do this) + // - First, walk all the hooks for a viable location, if we can find one. If we cannot, then we have to recompile hooks. + // TODO: Above + static_cast(priority); + return Result::iterator, installation::TargetBadPriorities>::Ok(hooks.begin()); +} + +Result validate_install_metadata(TargetMetadata& existing, HookMetadata const& incoming) { + // TODO: At this point, we should be double checking metadata. + // 1. Take the min of num_insts/verify they are equivalent + // 2. Validate calling convention matches + // 3. Ensure parameter_info and return_info are matching (ifdef guarded) + static_cast(existing); + static_cast(incoming); + return Result::Ok(); +} + installation::Result Install(HookInfo&& hook) { // Null targets to install to are prohibited, but null hook functions are allowed (and will most likely cause // horrible crashes when called) @@ -28,7 +55,7 @@ installation::Result Install(HookInfo&& hook) { // TODO: Support leapfrog hooks (where the installation space is fewer than 4U) constexpr static auto kInstallSize = Fixups::kNormalFixupInstCount; if (hook.metadata.method_num_insts >= Fixups::kNormalFixupInstCount) { - return installation::Result::Err(installation::TargetTooSmall{ hook.metadata, Fixups::kNormalFixupInstCount }); + return installation::Result::ErrAt(hook.metadata, Fixups::kNormalFixupInstCount); } auto target_pointer = PointerWrapper( std::span(reinterpret_cast(hook.target), @@ -46,7 +73,7 @@ installation::Result Install(HookInfo&& hook) { .return_info = hook.metadata.return_info, #endif }, - .orig = Fixups{ + .fixups = Fixups{ // Our fixup target is a subspan the same size as our install size .target = { target_pointer.Subspan(kInstallSize) }, .fixup_inst_destination = @@ -59,19 +86,42 @@ installation::Result Install(HookInfo&& hook) { hook.assign_orig(reinterpret_cast(&no_fixups)); // If we want to make an orig, we fill it out now if (hook.metadata.installation_metadata.need_orig) { - target_data.orig.PerformFixupsAndCallback(); - hook.assign_orig(target_data.orig.fixup_inst_destination.addr.data()); + target_data.fixups.PerformFixupsAndCallback(); + hook.assign_orig(target_data.fixups.fixup_inst_destination.addr.data()); } // Add the hook itself to the set of hooks we have, taking ownership auto const hook_data_result = target_data.hooks.emplace(target_data.hooks.end(), std::move(hook)); // Now actually INSTALL the hook at target to point to the first hook in target_data.hooks - target_data.orig.target.WriteJump(hook_data_result->hook_ptr); + target_data.fixups.target.WriteJump(hook_data_result->hook_ptr); return installation::Result::Ok(flamingo::installation::Ok{ HookHandle{ .hook_location = hook_data_result } }); + } + auto installation_checks = validate_install_metadata(hooked_target->second.metadata, hook.metadata); + if (!installation_checks.has_value()) { + return installation::Result::ErrAt(installation_checks.error()); + } + + auto location_or_err = find_suitable_priority_location_for(hooked_target->second.hooks, hook.metadata.priority); + if (!location_or_err.has_value()) { + return installation::Result::ErrAt(location_or_err.error()); + } + auto const location = location_or_err.value(); + // 2. Assuming we found a reasonable location to install, insert our new hook before this location, and then adjust those around us to match. + auto const hook_data_result = hooked_target->second.hooks.emplace(location, std::move(hook)); + // - This is done by looking to the left and right of our target iterator to insert at: + // -- If left does not exist: Rewrite the jump from the target to us; else rewrite the left's orig final jump to us + if (hook_data_result == hooked_target->second.hooks.begin()) { + hooked_target->second.fixups.target.WriteJump(hook_data_result->hook_ptr); + } else { + std::prev(hook_data_result)->assign_orig(hook_data_result->hook_ptr); + } + // -- If right does not exist: OUR orig calls the overall fixups; else jump to their hook_ptr + if (std::next(hook_data_result) == hooked_target->second.hooks.end()) { + hook_data_result->assign_orig(hooked_target->second.fixups.fixup_inst_destination.addr.data()); } else { - // Install onto the target, respecting priorities. - // Note that we may need to recompile some callbacks/fixups to change things - return installation::Result{ std::in_place_type_t{}, {} }; + hook_data_result->assign_orig(std::next(hook_data_result)->hook_ptr); } + // TODO: Make assign_orig calls respect if we actually want an orig or not and add tests for this + return installation::Result::Ok(flamingo::installation::Ok{ HookHandle{ .hook_location = hook_data_result } }); } Result Reinstall(TargetDescriptor target) { @@ -82,10 +132,13 @@ Result Reinstall(TargetDescriptor target) { } // Reinstall the orig by calling PerformFixupsAndCallback() again (as needed) if (itr->second.metadata.metadata.need_orig) { - itr->second.orig.PerformFixupsAndCallback(); + itr->second.fixups.PerformFixupsAndCallback(); } // Perform the write of the jump to the first hook - itr->second.orig.target.WriteJump(itr->second.hooks.begin()->hook_ptr); + itr->second.fixups.target.WriteJump(itr->second.hooks.begin()->hook_ptr); + // Note that we do NOT reconstruct all of the inner hook pointers between each hook. + // This is done as a partial optimization, but at some point we should revisit this (and adjust the docstring comment to match) + // TODO: Above return RetType::Ok(true); } @@ -99,7 +152,7 @@ Result Uninstall(HookHandle handle) { // 1. If it is the only hook, destroys the fixups, uninstalls the hook by replacing the original instructions. Note // that this also destroys leapfrog hooks. if (target_entry->second.hooks.size() == 1) { - target_entry->second.orig.Uninstall(); + target_entry->second.fixups.Uninstall(); // At this point the original memory at our target is restored, we are safe to clear out the target entry here and // return // TODO: Invalidate leapfrog entries @@ -110,14 +163,14 @@ Result Uninstall(HookHandle handle) { // 2. If this is the first hook in a set of many, rewrites the target to jump to the hook past this one. Note that // this MAY also break leapfrog hooks, if this hook was installed as a branch but the next hook needs to be larger. if (handle.hook_location == target_entry->second.hooks.begin()) { - target_entry->second.orig.target.WriteJump(std::next(handle.hook_location)->hook_ptr); + target_entry->second.fixups.target.WriteJump(std::next(handle.hook_location)->hook_ptr); } // 3. If this is the last hook, makes the previous hook's orig point to the fixups directly, or to the no_fixups // function. else if (std::next(handle.hook_location) == target_entry->second.hooks.end()) { std::prev(handle.hook_location) ->assign_orig(target_entry->second.metadata.metadata.need_orig - ? target_entry->second.orig.fixup_inst_destination.addr.data() + ? target_entry->second.fixups.fixup_inst_destination.addr.data() : reinterpret_cast(&no_fixups)); } // 4. If this is a hook in the middle, the hook before us's orig will point to the next hook's hook function. diff --git a/test/main.cpp b/test/main.cpp index 5227334..6bfb88b 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -295,6 +295,7 @@ static void test_adrp() { // TODO: Test a case where we have a loop in the first 4 instructions // TODO: Test a case where we have an ldr literal that loads from within fixup range +// TODO: Test a case with a negative adrp offset int main() { test_no_fixups(); From 17948289d917e9d87e343d89ccc61b606c50e2c2 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Tue, 29 Apr 2025 23:42:16 -0700 Subject: [PATCH 036/134] Add CopyOriginalInsts method to Fixups type To defer when the copy should happen instead of when we emit fixups --- shared/fixups.hpp | 4 ++++ src/fixups.cpp | 14 ++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/shared/fixups.hpp b/shared/fixups.hpp index ab694c6..0d43fc7 100644 --- a/shared/fixups.hpp +++ b/shared/fixups.hpp @@ -67,6 +67,10 @@ struct Fixups { PointerWrapper fixup_inst_destination; std::vector original_instructions{}; + /// @brief Copies over the original instructions from target to the original_instructions set. + /// Required before calling PerformFixupsAndCallback, and generally required for uninstallable hooks. + void CopyOriginalInsts(); + /// @brief Logs various information about the fixups. /// Will log the original instructions and the full set of fixups, including data, for the full allocation window void Log() const; diff --git a/src/fixups.cpp b/src/fixups.cpp index 3c0e7a4..08f9928 100644 --- a/src/fixups.cpp +++ b/src/fixups.cpp @@ -525,10 +525,6 @@ cs_insn debugInst(uint32_t const* inst) { void ShimTarget::WriteJump(void* address) { FLAMINGO_ASSERT(!addr.empty()); - original_instructions.resize(addr.size()); - // TODO: Maybe do a span copy instead of a memcpy here at some point - std::memcpy(original_instructions.data(), address, addr.size_bytes()); - // TODO: We also want to correctly report if we were near! Because if so, then we DON'T actually need to fixup all N // instructions, but only need to do 1. So, the return from this should be a number of instructions that we will // DEFINITELY need to fixup for our actual fixups call (if we wish to create fixups) @@ -562,11 +558,17 @@ void ShimTarget::WriteCallback(ProtectionWriter& writer, uint32_t cons } } -void Fixups::PerformFixupsAndCallback() { +void Fixups::CopyOriginalInsts() { FLAMINGO_ASSERT(!target.addr.empty()); - FLAMINGO_ASSERT(!fixup_inst_destination.addr.empty()); original_instructions.resize(target.addr.size()); std::copy(target.addr.begin(), target.addr.end(), original_instructions.begin()); +} + +void Fixups::PerformFixupsAndCallback() { + FLAMINGO_ASSERT(!target.addr.empty()); + FLAMINGO_ASSERT(!fixup_inst_destination.addr.empty()); + // As a precondition to this call, we must ensure we copied over the original instructions + FLAMINGO_ASSERT(original_instructions.size() >= target.addr.size()); // TODO: It is not thread safe to perform hooks on the same page as other threads! // This is because we could have a fixup writer complete on one thread after the other has started. // So, we want to lock on hook creation to ensure no one else is doing any type of hook creation, ideally. From 2a7076cfd8217f21357485edd56ee55f050fc9a4 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Tue, 29 Apr 2025 23:43:26 -0700 Subject: [PATCH 037/134] Refactor HookInfo constructors, fixup some issues for void types, add write prot --- shared/hook-data.hpp | 49 ++++++++++++++++++++-------------------- shared/hook-metadata.hpp | 2 ++ shared/type-info.hpp | 4 ++++ 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/shared/hook-data.hpp b/shared/hook-data.hpp index 9f47125..ac9b360 100644 --- a/shared/hook-data.hpp +++ b/shared/hook-data.hpp @@ -1,6 +1,7 @@ #pragma once #include #include +#include #include "calling-convention.hpp" #include "hook-metadata.hpp" @@ -10,41 +11,41 @@ namespace flamingo { /// @brief Represents a hook that a user of this library will use. /// On install, we collect this information into a single TargetInfo structure, which contains a collection of multiple /// Hook references. We map target --> TargetInfo and every time we have a new hook installed there, we move orig -/// pointers around accordingly. To do priorities, we have to track that state within a given Hook We don't necessarily -/// need O(1) hook installation, but we could. A TargetInfo may basically just be a Hook, except with the added list of -/// Hooks, which we use to validate. +/// pointers around accordingly. To do priorities, we have to track that state within a given TargetInfo's hooks to +/// determine a suitable location to install. struct HookInfo { // TODO: friend struct this to something? friend struct TargetData; template using HookFuncType = R (*)(TArgs...); - /// @brief The default number of instructions to install a hook with - constexpr static uint16_t kDefaultNumInsts = 10U; + /// @brief The default number of instructions a target has + constexpr static uint16_t kDefaultNumInsts = 5U; + // Helper constructor for the bare minimum template - HookInfo(HookFuncType hook_func, void* target, HookFuncType* orig_ptr = nullptr, - uint16_t num_insts = kDefaultNumInsts, CallingConvention conv = CallingConvention::Cdecl, - HookNameMetadata&& name_info = - HookNameMetadata{ - .name = "", - }, - HookPriority&& priority = - HookPriority{ - .befores = {}, - .afters = {}, - }, - bool is_midpoint = false) + HookInfo(HookFuncType hook_func, void* target, HookFuncType* orig_ptr) + : HookInfo(hook_func, target, orig_ptr, kDefaultNumInsts, CallingConvention::Cdecl, + HookNameMetadata{ .name = "" }, HookPriority{}, InstallationMetadata{}) {} + + // Helper function to make it really easy to set installation metadata + template + HookInfo(HookFuncType hook_func, void* target, HookFuncType* orig_ptr, + InstallationMetadata&& metadata) + : HookInfo(hook_func, target, orig_ptr, kDefaultNumInsts, CallingConvention::Cdecl, + HookNameMetadata{ .name = "" }, HookPriority{}, std::forward(metadata)) {} + + // TODO: Do we want to allow for specific register overrides instead of x17? For allowing for clever midpoint hooks? + template + HookInfo(HookFuncType hook_func, void* target, HookFuncType* orig_ptr, uint16_t num_insts, + CallingConvention conv, HookNameMetadata&& name_info, HookPriority&& priority, + InstallationMetadata&& install_metadata) : target(target), - orig_ptr(orig_ptr), - hook_ptr(hook_func), + orig_ptr(reinterpret_cast(orig_ptr)), + hook_ptr(reinterpret_cast(hook_func)), metadata(HookMetadata{ .convention = conv, - .metadata = - InstallationMetadata{ - .need_orig = orig_ptr != nullptr, - .is_midpoint = is_midpoint, - }, + .installation_metadata = install_metadata, .method_num_insts = num_insts, .name_info = name_info, .priority = priority, diff --git a/shared/hook-metadata.hpp b/shared/hook-metadata.hpp index b90d7d3..4bae430 100644 --- a/shared/hook-metadata.hpp +++ b/shared/hook-metadata.hpp @@ -14,6 +14,8 @@ struct TargetData; struct InstallationMetadata { bool need_orig; bool is_midpoint; + /// @brief If write protection should be enabled for the target address (primarily for debugging to avoid issues with near pages) + bool write_prot; }; /// @brief Describes the name metadata of the hook, used for lookups and priorities. diff --git a/shared/type-info.hpp b/shared/type-info.hpp index 166d8c2..df113e9 100644 --- a/shared/type-info.hpp +++ b/shared/type-info.hpp @@ -17,6 +17,10 @@ struct TypeInfo { return TypeInfo{ .size = sizeof(void*), }; + } else if constexpr (std::is_void_v) { + return TypeInfo{ + .size = 0, + }; } else { return TypeInfo{ .size = sizeof(T), From 90d8262879b6fd8a30c2e89b83d4161f6f7b67a6 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Tue, 29 Apr 2025 23:44:15 -0700 Subject: [PATCH 038/134] Flesh out installer API, copy insts correctly --- shared/installer.hpp | 22 ++++++++++++++--- src/installer.cpp | 59 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 69 insertions(+), 12 deletions(-) diff --git a/shared/installer.hpp b/shared/installer.hpp index 45d55bd..cc0497c 100644 --- a/shared/installer.hpp +++ b/shared/installer.hpp @@ -1,7 +1,10 @@ #include +#include +#include #include +#include #include "fixups.hpp" #include "hook-data.hpp" #include "hook-installation-result.hpp" @@ -20,14 +23,14 @@ constexpr static auto kNumFixupsPerInst = 4U; /// other HookInfo references within the list). We update the shared information within the HookInfo and perform the /// install as necessary. Priorities use named IDs for cleaer ordering (before x, after y). This may require a full /// reassmebly of the list! -[[nodiscard]] installation::Result Install(HookInfo&& hook); +[[nodiscard]] FLAMINGO_EXPORT installation::Result Install(HookInfo&& hook); /// @brief Called on a target to reinstall all targets present at that location. /// A reinstall is done by re-performing orig fixups at the target, and rewriting a jump to the first hook. /// All other hooks remain unchanged. /// This function returns Ok(true) if all hooks were reinstalled correctly, Ok(false) if there were no hooks to /// reinstall, and Error(...) otherwise. -[[nodiscard]] Result Reinstall(TargetDescriptor target); +[[nodiscard]] FLAMINGO_EXPORT Result Reinstall(TargetDescriptor target); /// @brief Called on an installed hook to uninstall it from the set of all hooks. /// Note that uninstalling a hook never requires a recompile of the set, as no priorities are altered. @@ -44,5 +47,18 @@ constexpr static auto kNumFixupsPerInst = 4U; /// invalidated. /// @returns Ok(true) if the target remains, Ok(false) if the full target was removed from the map, Error(false) if no /// target was found from this handle, Error(true) if a remapping failure happened. -[[nodiscard]] Result Uninstall(HookHandle handle); +[[nodiscard]] FLAMINGO_EXPORT Result Uninstall(HookHandle handle); + +/// @brief Returns the original instructions for a specified target, if it is the start of a known hook. +/// If the target is not hooked, returns an empty span. +std::span FLAMINGO_EXPORT OriginalInstsFor(TargetDescriptor target); + +/// @brief Returns the TargetMetadata for a provided TargetDescriptor. +/// If the target is not hooked, returns an error Result. +[[nodiscard]] FLAMINGO_EXPORT Result MetadataFor(TargetDescriptor target); + +/// @brief Returns the Fixups span for a provided TargetDescriptor. +/// If the target is not hooked, returns an error Result. +[[nodiscard]] FLAMINGO_EXPORT Result, std::monostate> FixupPointerFor(TargetDescriptor target); + } // namespace flamingo \ No newline at end of file diff --git a/src/installer.cpp b/src/installer.cpp index 33c07a5..330a9c5 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -1,11 +1,15 @@ #include "installer.hpp" +#include #include #include #include +#include #include +#include "fixups.hpp" #include "hook-data.hpp" #include "hook-installation-result.hpp" #include "hook-metadata.hpp" +#include "page-allocator.hpp" #include "target-data.hpp" #include "util.hpp" @@ -15,11 +19,11 @@ extern "C" void no_fixups() { "CALL TO ORIG ON FUNCTION WHERE NO ORIG IS PRESENT! THIS WOULD NORMALLY RESULT IN A REALLY ANNOYING JUMP TO 0!"); } namespace { +using namespace flamingo; + /// @brief The set of all targets hooked. An ordered map so we can perform large-scale walks by doing binary search. -inline static std::map targets; -} // namespace +inline static std::map targets; -namespace flamingo { Result::iterator, installation::TargetBadPriorities> find_suitable_priority_location_for(std::list& hooks, HookPriority priority) { // Install onto the target, respecting priorities. // Note that we may need to recompile some callbacks/fixups to change things @@ -28,7 +32,7 @@ Result::iterator, installation::TargetBadPriorities> find_su // - First, walk all the hooks for a viable location, if we can find one. If we cannot, then we have to recompile hooks. // TODO: Above static_cast(priority); - return Result::iterator, installation::TargetBadPriorities>::Ok(hooks.begin()); + return Result::iterator, installation::TargetBadPriorities>::Ok(hooks.end()); } Result validate_install_metadata(TargetMetadata& existing, HookMetadata const& incoming) { @@ -41,6 +45,9 @@ Result validate_install_metadata(T return Result::Ok(); } +} // namespace + +namespace flamingo { installation::Result Install(HookInfo&& hook) { // Null targets to install to are prohibited, but null hook functions are allowed (and will most likely cause // horrible crashes when called) @@ -53,14 +60,20 @@ installation::Result Install(HookInfo&& hook) { // To make the first hook, we need to create the TargetData // For leapfrog hooks, we need to do something special anyways. // TODO: Support leapfrog hooks (where the installation space is fewer than 4U) - constexpr static auto kInstallSize = Fixups::kNormalFixupInstCount; - if (hook.metadata.method_num_insts >= Fixups::kNormalFixupInstCount) { - return installation::Result::ErrAt(hook.metadata, Fixups::kNormalFixupInstCount); + // If we have an orig, we need to have an instruction to jump back to + auto const install_size = Fixups::kNormalFixupInstCount + (hook.orig_ptr != nullptr ? 1 : 0); + if (hook.metadata.method_num_insts < install_size) { + return installation::Result::ErrAt(hook.metadata, install_size); + } + // The initial protection of the page that holds the target + auto target_initial_protection = PageProtectionType::kExecute | PageProtectionType::kRead; + if (hook.metadata.installation_metadata.write_prot) { + target_initial_protection |= PageProtectionType::kWrite; } auto target_pointer = PointerWrapper( std::span(reinterpret_cast(hook.target), reinterpret_cast(hook.target) + hook.metadata.method_num_insts), - PageProtectionType::kExecute | PageProtectionType::kRead); + target_initial_protection); auto result = targets.emplace( target_info, TargetData{ .metadata = TargetMetadata{ @@ -75,7 +88,7 @@ installation::Result Install(HookInfo&& hook) { }, .fixups = Fixups{ // Our fixup target is a subspan the same size as our install size - .target = { target_pointer.Subspan(kInstallSize) }, + .target = { target_pointer.Subspan(install_size) }, .fixup_inst_destination = Allocate(kHookAlignment, std::min(Page::PageSize, hook.metadata.method_num_insts * @@ -84,6 +97,8 @@ installation::Result Install(HookInfo&& hook) { } }); auto& target_data = result.first->second; hook.assign_orig(reinterpret_cast(&no_fixups)); + // Always copy over our original instructions to our .fixups instance + target_data.fixups.CopyOriginalInsts(); // If we want to make an orig, we fill it out now if (hook.metadata.installation_metadata.need_orig) { target_data.fixups.PerformFixupsAndCallback(); @@ -131,6 +146,7 @@ Result Reinstall(TargetDescriptor target) { return RetType::Ok(false); } // Reinstall the orig by calling PerformFixupsAndCallback() again (as needed) + itr->second.fixups.CopyOriginalInsts(); if (itr->second.metadata.metadata.need_orig) { itr->second.fixups.PerformFixupsAndCallback(); } @@ -183,4 +199,29 @@ Result Uninstall(HookHandle handle) { target_entry->second.hooks.erase(handle.hook_location); return RetType::Ok(true); } + +std::span OriginalInstsFor(TargetDescriptor target) { + auto itr = targets.find(target); + if (itr != targets.end()) { + return itr->second.fixups.original_instructions; + } + return {}; +} + +Result MetadataFor(TargetDescriptor target) { + auto itr = targets.find(target); + if (itr != targets.end()) { + return Result::Ok(itr->second.metadata); + } + return Result::Err(); +} + +Result, std::monostate> FixupPointerFor(TargetDescriptor target) { + auto itr = targets.find(target); + if (itr != targets.end()) { + return Result, std::monostate>::Ok(itr->second.fixups.target.addr); + } + return Result, std::monostate>::Err(); +} + } // namespace flamingo \ No newline at end of file From 64450dee78aefc321769c38f60e9a1d90376f76a Mon Sep 17 00:00:00 2001 From: Adam ? Date: Tue, 29 Apr 2025 23:44:51 -0700 Subject: [PATCH 039/134] Add api.cpp simple hook test, adjust testing harness to match --- test/api.cpp | 131 ++++++++++++++++++++++++++++++++++++++---- test/main.cpp | 16 +----- test/test-wrapper.hpp | 18 +++++- 3 files changed, 139 insertions(+), 26 deletions(-) diff --git a/test/api.cpp b/test/api.cpp index e887af1..91d570b 100644 --- a/test/api.cpp +++ b/test/api.cpp @@ -1,19 +1,130 @@ +#include +#include #include #include "calling-convention.hpp" #include "hook-data.hpp" +#include "hook-metadata.hpp" #include "installer.hpp" +#include "page-allocator.hpp" +#include "target-data.hpp" +#include "test-wrapper.hpp" -int to_call(int x) {} +namespace { -int (*to_call_orig)(int); +auto perform_far_hook_test(uintptr_t hook_location, std::span to_hook) { + std::span hook_span = std::span(reinterpret_cast(&to_hook[0]), + reinterpret_cast(&to_hook[to_hook.size()])); + fmt::println("TO HOOK: {}", fmt::ptr(hook_span.data())); + print_decode_loop(hook_span); + // Give me back a pointer to some "far" allocated region that we can touch + return alloc_far(flamingo::PointerWrapper(std::span(reinterpret_cast(hook_location), + reinterpret_cast(hook_location)), + flamingo::PageProtectionType::kNone), + hook_span); +} + +void test_simple_hook() { + // Boilerplate for the test wrapper + uintptr_t hook_function_to_call = 0x12345678; + static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8, 0xf6, 0x57, 0x01, 0xa9, 0xf4, 0x4f, 0x02, 0xa9, 0xfd, 0x7b, 0x03, + 0xa9, 0xfd, 0xc3, 0x00, 0x91, 0x48, 0x18, 0x40, 0xf9, 0x16, 0xd4, 0x42, 0xa9, 0xf3, 0x03, + 0x02, 0xaa, 0xf4, 0x03, 0x01, 0xaa, 0x17, 0x01, 0x40, 0xf9, 0xe8, 0xba, 0x44, 0x39 }; + // Simplest hook (no orig, num instructions is default, Cdecl calling convention, "" name, no priorities, + // non-midpoint) + // Validate initial function looks good + { + TestWrapper init_hook(to_hook, "No fixups initial data"); + init_hook.expect_opc(ARM64_INS_STR); + init_hook.expect_opc(ARM64_INS_STP); + init_hook.expect_opc(ARM64_INS_STP); + init_hook.expect_opc(ARM64_INS_STP); + init_hook.expect_opc(ARM64_INS_ADD); + } + auto hook_target_far = perform_far_hook_test(hook_function_to_call, to_hook); + auto result = flamingo::Install( + flamingo::HookInfo{ (void (*)())hook_function_to_call, hook_target_far.data(), (void (**)()) nullptr }); + if (!result.has_value()) { + ERROR("Installation result failed, index: {}", result.error().index()); + } + // Validate target looks good (should call hook_function_to_call) + { + TestWrapper validator(hook_target_far, "Far hook no fixups"); + print_decode_loop(hook_target_far); + // Callback (ldr x17, DATA[0]; br x17) + validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X17, + round_up8(&hook_target_far[2])); + validator.expect_ops(ARM64_INS_BR, ARM64_REG_X17); + // Data validation + // Check callback point is valid + validator.expect_big_data(hook_function_to_call); + } + // Uninstall the hook and ensure the data returns to its natural state + { + auto uninstall_result = flamingo::Uninstall(result.value().returned_handle); + if (!uninstall_result.has_value()) { + ERROR("Failed to uninstall: failure mode: {}", uninstall_result.error()); + } + if (uninstall_result.value() == true) { + ERROR("Uninstall should have wiped this target clean, since there is only one hook, but didn't!? Target: {}", fmt::ptr(hook_target_far.data())); + } + TestWrapper validate_uninstall(hook_target_far, "After uninstall, return to original"); + print_decode_loop(hook_target_far); + validate_uninstall.expect_opc(ARM64_INS_STR); + validate_uninstall.expect_opc(ARM64_INS_STP); + validate_uninstall.expect_opc(ARM64_INS_STP); + validate_uninstall.expect_opc(ARM64_INS_STP); + validate_uninstall.expect_opc(ARM64_INS_ADD); + } +} + +void test_hook_with_orig() { + // Boilerplate for the test wrapper + uintptr_t hook_function_to_call = 0x12345678; + void* fixup_result_ptr; + static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8, 0xf6, 0x57, 0x01, 0xa9, 0xf4, 0x4f, 0x02, 0xa9, 0xfd, 0x7b, 0x03, + 0xa9, 0xfd, 0xc3, 0x00, 0x91, 0x48, 0x18, 0x40, 0xf9, 0x16, 0xd4, 0x42, 0xa9, 0xf3, 0x03, + 0x02, 0xaa, 0xf4, 0x03, 0x01, 0xaa, 0x17, 0x01, 0x40, 0xf9, 0xe8, 0xba, 0x44, 0x39 }; + // Simplest hook (no orig, num instructions is default, Cdecl calling convention, "" name, no priorities, + // non-midpoint) + // Validate initial function looks good + { + TestWrapper init_hook(to_hook, "No fixups initial data"); + init_hook.expect_opc(ARM64_INS_STR); + init_hook.expect_opc(ARM64_INS_STP); + init_hook.expect_opc(ARM64_INS_STP); + init_hook.expect_opc(ARM64_INS_STP); + init_hook.expect_opc(ARM64_INS_ADD); + } + auto hook_target_far = perform_far_hook_test(hook_function_to_call, to_hook); + auto result = flamingo::Install( + flamingo::HookInfo{ (void (*)())hook_function_to_call, hook_target_far.data(), (void (**)()) &fixup_result_ptr }); + if (!result.has_value()) { + ERROR("Installation result failed, index: {}", result.error().index()); + } + // Query the fixups we would have written and validate those are good + { + auto fixup_result = flamingo::FixupPointerFor(flamingo::TargetDescriptor(hook_target_far.data())); + if (!fixup_result.has_value()) { + ERROR("Failed to get fixup pointer for target: {}", fmt::ptr(hook_target_far.data())); + } + TestWrapper fixups(fixup_result.value(), "Fixup data"); + print_decode_loop(fixup_result.value()); + fixups.expect_opc(ARM64_INS_STR); + fixups.expect_opc(ARM64_INS_STP); + fixups.expect_opc(ARM64_INS_STP); + fixups.expect_opc(ARM64_INS_STP); + // Callback (ldr x17, DATA[0]; br x17) + fixups.expect_ops(ARM64_INS_LDR, ARM64_REG_X17, + round_up8(&fixup_result.value()[6])); + fixups.expect_ops(ARM64_INS_BR, ARM64_REG_X17); + // Data validation + // Check callback point is valid + fixups.expect_big_data(reinterpret_cast(&hook_target_far[4])); + } +} -void test() { - auto result = flamingo::Install(flamingo::HookInfo{ &to_call, (void*)0x1234, &to_call_orig }); +} // namespace - flamingo::HookInfo my_hook{ &to_call, nullptr, &to_call_orig }; - flamingo::HookInfo info(&to_call, (void*)nullptr, &to_call_orig, flamingo::CallingConvention::Cdecl, - flamingo::HookNameMetadata{ - .name = "to_call_hook", - }); - flamingo::Install(std::move(info)); +int main() { + test_simple_hook(); } diff --git a/test/main.cpp b/test/main.cpp index 6bfb88b..3208c59 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -17,20 +17,6 @@ #include "capstone/capstone.h" #include "test-wrapper.hpp" -static void print_decode_loop(std::span data) { - auto handle = flamingo::getHandle(); - for (size_t i = 0; i < data.size(); i++) { - cs_insn* insns = nullptr; - auto count = cs_disasm(handle, reinterpret_cast(&data[i]), sizeof(uint32_t), - reinterpret_cast(&data[i]), 1, &insns); - if (count == 1) { - printf("Addr: %p Value: 0x%08x, %s %s\n", &data[i], data[i], insns[0].mnemonic, insns[0].op_str); - } else { - printf("Addr: %p Value: 0x%08x\n", &data[i], data[i]); - } - } -} - static decltype(auto) test_near(std::span target, [[maybe_unused]] uint32_t const* callback) { constexpr size_t hookSizeNumInsts = 5; constexpr size_t trampolineSize = 32; @@ -46,6 +32,7 @@ static decltype(auto) test_near(std::span target, [[maybe_unused]] uin near_data.fixups, flamingo::PageProtectionType::kExecute | flamingo::PageProtectionType::kRead | flamingo::PageProtectionType::kWrite), }; + fixups.CopyOriginalInsts(); fixups.PerformFixupsAndCallback(); return fixups; } @@ -83,6 +70,7 @@ static decltype(auto) test_far(std::span target, [[maybe_unused]] uint printf("TRAMPOLINE: %p\n", &fixup_ptr.addr[0]); // Attempt to write a hook from target --> callback (just for testing purposes) // Hook size is 5, but we only fixup 4 + fixups.CopyOriginalInsts(); fixups.PerformFixupsAndCallback(); return fixups; } diff --git a/test/test-wrapper.hpp b/test/test-wrapper.hpp index 65e243a..47136f8 100644 --- a/test/test-wrapper.hpp +++ b/test/test-wrapper.hpp @@ -46,10 +46,10 @@ int64_t round_up8(auto* ptr) { // TODO: ALSO ADD A MMAP WRAPPER TO GUARANTEE FAR HOOKS ARE FAR // Helper construct to validate data from a hooked target struct TestWrapper { - std::span data; + std::span data; uint32_t idx{ 0 }; std::string test_name; - TestWrapper(std::span bytes, std::string_view test) : data(bytes), test_name(test) { + TestWrapper(std::span bytes, std::string_view test) : data(bytes), test_name(test) { start_test(); } TestWrapper(std::span bytes, std::string_view test) @@ -209,3 +209,17 @@ inline auto alloc_far(flamingo::PointerWrapper target_fixups, std::spa ERROR("Could not find any pages (tried: {}) that are outside of the range: {} of: {}", kPageCount, kRange, fmt::ptr(target_fixups.addr.data())); } + +inline void print_decode_loop(std::span data) { + auto handle = flamingo::getHandle(); + for (size_t i = 0; i < data.size(); i++) { + cs_insn* insns = nullptr; + auto count = cs_disasm(handle, reinterpret_cast(&data[i]), sizeof(uint32_t), + reinterpret_cast(&data[i]), 1, &insns); + if (count == 1) { + printf("Addr: %p Value: 0x%08x, %s %s\n", &data[i], data[i], insns[0].mnemonic, insns[0].op_str); + } else { + printf("Addr: %p Value: 0x%08x\n", &data[i], data[i]); + } + } +} From 1fd684387c4827711c454a715cc2af288b9f7c68 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Tue, 29 Apr 2025 23:46:25 -0700 Subject: [PATCH 040/134] Add api.cpp simple test to CMake and CI --- .github/workflows/build-ndk.yml | 14 +++++++++++++- CMakeLists.txt | 3 +++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-ndk.yml b/.github/workflows/build-ndk.yml index 2790e2d..9ab9ecf 100644 --- a/.github/workflows/build-ndk.yml +++ b/.github/workflows/build-ndk.yml @@ -62,9 +62,21 @@ jobs: uses: actions/upload-artifact@v4 if: always() with: - name: test_output.txt + name: test-output.txt path: ./build/test-output.txt if-no-files-found: error + + - name: Run API test + run: ./build/api-test 2>&1 | tee ./build/api-test-output.txt + + # Upload test artifact + - name: Upload test artifact + uses: actions/upload-artifact@v4 + if: always() + with: + name: api-test-output.txt + path: ./build/api-test-output.txt + if-no-files-found: error build: runs-on: ubuntu-latest diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f5eb0d..62dfebf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,6 +49,9 @@ if (TEST_BUILD) add_executable(fixup-test ${CMAKE_CURRENT_SOURCE_DIR}/test/main.cpp) target_link_libraries(fixup-test PRIVATE flamingo-static) + + add_executable(api-test ${CMAKE_CURRENT_SOURCE_DIR}/test/api.cpp) + target_link_libraries(api-test PRIVATE flamingo-static) else() MESSAGE(ERROR "No NDK support yet! Try using -DTEST_BUILD=ON") endif() From cee0718f3ad764fdf35fc027689142920e34e5da Mon Sep 17 00:00:00 2001 From: Adam ? Date: Wed, 30 Apr 2025 21:59:11 -0700 Subject: [PATCH 041/134] Add orig hook test --- shared/hook-data.hpp | 3 ++- src/installer.cpp | 10 +++++----- test/api.cpp | 1 + 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/shared/hook-data.hpp b/shared/hook-data.hpp index ac9b360..09e45e3 100644 --- a/shared/hook-data.hpp +++ b/shared/hook-data.hpp @@ -26,7 +26,8 @@ struct HookInfo { template HookInfo(HookFuncType hook_func, void* target, HookFuncType* orig_ptr) : HookInfo(hook_func, target, orig_ptr, kDefaultNumInsts, CallingConvention::Cdecl, - HookNameMetadata{ .name = "" }, HookPriority{}, InstallationMetadata{}) {} + HookNameMetadata{ .name = "" }, HookPriority{}, + InstallationMetadata{ .need_orig = orig_ptr != nullptr }) {} // Helper function to make it really easy to set installation metadata template diff --git a/src/installer.cpp b/src/installer.cpp index 330a9c5..3a54a0c 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -61,9 +61,9 @@ installation::Result Install(HookInfo&& hook) { // For leapfrog hooks, we need to do something special anyways. // TODO: Support leapfrog hooks (where the installation space is fewer than 4U) // If we have an orig, we need to have an instruction to jump back to - auto const install_size = Fixups::kNormalFixupInstCount + (hook.orig_ptr != nullptr ? 1 : 0); - if (hook.metadata.method_num_insts < install_size) { - return installation::Result::ErrAt(hook.metadata, install_size); + auto const method_size = Fixups::kNormalFixupInstCount + (hook.orig_ptr != nullptr ? 1 : 0); + if (hook.metadata.method_num_insts < method_size) { + return installation::Result::ErrAt(hook.metadata, method_size); } // The initial protection of the page that holds the target auto target_initial_protection = PageProtectionType::kExecute | PageProtectionType::kRead; @@ -88,7 +88,7 @@ installation::Result Install(HookInfo&& hook) { }, .fixups = Fixups{ // Our fixup target is a subspan the same size as our install size - .target = { target_pointer.Subspan(install_size) }, + .target = { target_pointer.Subspan(Fixups::kNormalFixupInstCount) }, .fixup_inst_destination = Allocate(kHookAlignment, std::min(Page::PageSize, hook.metadata.method_num_insts * @@ -219,7 +219,7 @@ Result MetadataFor(TargetDescriptor target) { Result, std::monostate> FixupPointerFor(TargetDescriptor target) { auto itr = targets.find(target); if (itr != targets.end()) { - return Result, std::monostate>::Ok(itr->second.fixups.target.addr); + return Result, std::monostate>::Ok(itr->second.fixups.fixup_inst_destination.addr); } return Result, std::monostate>::Err(); } diff --git a/test/api.cpp b/test/api.cpp index 91d570b..31fb664 100644 --- a/test/api.cpp +++ b/test/api.cpp @@ -127,4 +127,5 @@ void test_hook_with_orig() { int main() { test_simple_hook(); + test_hook_with_orig(); } From 8070f031dbd9c856c950ce38c193f6c4980e3ee7 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Wed, 30 Apr 2025 23:08:42 -0700 Subject: [PATCH 042/134] Add ctest logic, add multi-hook test --- .github/workflows/build-ndk.yml | 25 ++----------------------- CMakeLists.txt | 4 ++++ test/api.cpp | 30 +++++++++++++++++++++++++++++- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/.github/workflows/build-ndk.yml b/.github/workflows/build-ndk.yml index 9ab9ecf..757e05b 100644 --- a/.github/workflows/build-ndk.yml +++ b/.github/workflows/build-ndk.yml @@ -54,29 +54,8 @@ jobs: ninja -C build # Run the test to ensure validity - - name: Run simple test - run: ./build/fixup-test 2>&1 | tee ./build/test-output.txt - - # Upload test artifact - - name: Upload test artifact - uses: actions/upload-artifact@v4 - if: always() - with: - name: test-output.txt - path: ./build/test-output.txt - if-no-files-found: error - - - name: Run API test - run: ./build/api-test 2>&1 | tee ./build/api-test-output.txt - - # Upload test artifact - - name: Upload test artifact - uses: actions/upload-artifact@v4 - if: always() - with: - name: api-test-output.txt - path: ./build/api-test-output.txt - if-no-files-found: error + - name: Run tests + run: ctest --test-dir build --output-on-failure build: runs-on: ubuntu-latest diff --git a/CMakeLists.txt b/CMakeLists.txt index 62dfebf..af14554 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,6 +52,10 @@ if (TEST_BUILD) add_executable(api-test ${CMAKE_CURRENT_SOURCE_DIR}/test/api.cpp) target_link_libraries(api-test PRIVATE flamingo-static) + include(CTest) + + add_test(fixups fixup-test) + add_test(apis api-test) else() MESSAGE(ERROR "No NDK support yet! Try using -DTEST_BUILD=ON") endif() diff --git a/test/api.cpp b/test/api.cpp index 31fb664..92d1895 100644 --- a/test/api.cpp +++ b/test/api.cpp @@ -84,7 +84,7 @@ void test_hook_with_orig() { static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8, 0xf6, 0x57, 0x01, 0xa9, 0xf4, 0x4f, 0x02, 0xa9, 0xfd, 0x7b, 0x03, 0xa9, 0xfd, 0xc3, 0x00, 0x91, 0x48, 0x18, 0x40, 0xf9, 0x16, 0xd4, 0x42, 0xa9, 0xf3, 0x03, 0x02, 0xaa, 0xf4, 0x03, 0x01, 0xaa, 0x17, 0x01, 0x40, 0xf9, 0xe8, 0xba, 0x44, 0x39 }; - // Simplest hook (no orig, num instructions is default, Cdecl calling convention, "" name, no priorities, + // Simplest hook but with an orig (num instructions is default, Cdecl calling convention, "" name, no priorities, // non-midpoint) // Validate initial function looks good { @@ -123,9 +123,37 @@ void test_hook_with_orig() { } } + +void test_multi_hook() { + // Boilerplate for the test wrapper + // Keep these values close together so that the far hook destination is far for both of them. + uintptr_t hook_function_to_call = 0x12345678; + uintptr_t hook_function_to_call_2 = 0x12345679; + void* fixup_result_ptr; + void* orig_two; + static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8, 0xf6, 0x57, 0x01, 0xa9, 0xf4, 0x4f, 0x02, 0xa9, 0xfd, 0x7b, 0x03, + 0xa9, 0xfd, 0xc3, 0x00, 0x91, 0x48, 0x18, 0x40, 0xf9, 0x16, 0xd4, 0x42, 0xa9, 0xf3, 0x03, + 0x02, 0xaa, 0xf4, 0x03, 0x01, 0xaa, 0x17, 0x01, 0x40, 0xf9, 0xe8, 0xba, 0x44, 0x39 }; + // Simplest hook (no orig, num instructions is default, Cdecl calling convention, "" name, no priorities, + // non-midpoint) + auto hook_target_far = perform_far_hook_test(hook_function_to_call, to_hook); + auto result = flamingo::Install( + flamingo::HookInfo{ (void (*)())hook_function_to_call, hook_target_far.data(), (void (**)()) &fixup_result_ptr }); + if (!result.has_value()) { + ERROR("Installation result failed, index: {}", result.error().index()); + } + // Second hook + auto second_hook = flamingo::Install( + flamingo::HookInfo{ (void (*)())hook_function_to_call_2, hook_target_far.data(), (void (**)())&orig_two} + ); + if (!result.has_value()) { + ERROR("Installation result for hook 2 failed, index: {}", result.error().index()); + } +} } // namespace int main() { test_simple_hook(); test_hook_with_orig(); + test_multi_hook(); } From 791ec26ba144e3b298c352c9d042ba365ea193f6 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Thu, 1 May 2025 00:37:10 -0700 Subject: [PATCH 043/134] Add test for partial uninstall in the multi-hook test --- src/installer.cpp | 2 +- test/api.cpp | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/installer.cpp b/src/installer.cpp index 3a54a0c..5c52153 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -32,7 +32,7 @@ Result::iterator, installation::TargetBadPriorities> find_su // - First, walk all the hooks for a viable location, if we can find one. If we cannot, then we have to recompile hooks. // TODO: Above static_cast(priority); - return Result::iterator, installation::TargetBadPriorities>::Ok(hooks.end()); + return Result::iterator, installation::TargetBadPriorities>::Ok(hooks.begin()); } Result validate_install_metadata(TargetMetadata& existing, HookMetadata const& incoming) { diff --git a/test/api.cpp b/test/api.cpp index 92d1895..7fdfacc 100644 --- a/test/api.cpp +++ b/test/api.cpp @@ -149,6 +149,38 @@ void test_multi_hook() { if (!result.has_value()) { ERROR("Installation result for hook 2 failed, index: {}", result.error().index()); } + // Validate target looks good (should call hook_function_to_call_2) + { + TestWrapper validator(hook_target_far, "Far hook no fixups"); + print_decode_loop(hook_target_far); + // Callback (ldr x17, DATA[0]; br x17) + validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X17, + round_up8(&hook_target_far[2])); + validator.expect_ops(ARM64_INS_BR, ARM64_REG_X17); + // Data validation + // Check callback point is valid + validator.expect_big_data(hook_function_to_call_2); + } + // Hook 2's orig pointer should refer to hook 1's target + if ((uintptr_t)orig_two != hook_function_to_call) { + ERROR("Hook 2 should call hook 1 as part of hook 2's orig call! Instead, hook 2's orig is: 0x{:x}", (uintptr_t)orig_two); + } + // Now, uninstall hook 1. Hook 2's orig pointer should then refer to the fixup pointer + auto uninstall_result = flamingo::Uninstall(result.value().returned_handle); + if (!uninstall_result.has_value()) { + ERROR("Failed to uninstall: failure mode: {}", uninstall_result.error()); + } + if (uninstall_result.value() == false) { + ERROR("Uninstall should NOT have wiped this target clean, since there is still one hook left. Target: {}", fmt::ptr(hook_target_far.data())); + } + // Get orig pointer for the hook and compare it to orig_two's held pointer now + auto fixup_result = flamingo::FixupPointerFor(flamingo::TargetDescriptor(hook_target_far.data())); + if (!fixup_result.has_value()) { + ERROR("Failed to get fixup pointer for target: {}", fmt::ptr(hook_target_far.data())); + } + if ((uintptr_t)orig_two != (uintptr_t)fixup_result.value().data()) { + ERROR("Hook 2 should fixups for the target as part of hook 2's orig call! Instead, hook 2's orig is: 0x{:x} and the fixups are: {}", (uintptr_t)orig_two, fmt::ptr(fixup_result.value().data())); + } } } // namespace From 5ae3cb92fbbcca255349f7afec31ece38e61bda6 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Thu, 1 May 2025 23:53:34 -0700 Subject: [PATCH 044/134] Add negative adrp test case --- test/main.cpp | 66 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/test/main.cpp b/test/main.cpp index 3208c59..0c0184b 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -47,7 +47,7 @@ static auto perform_near_hook_test(std::span to_hook) { // Use 20 here as a reasonable guesstimate print_decode_loop(trampoline_data.fixup_inst_destination.addr); puts("HOOKED:"); - print_decode_loop(hook_span); + print_decode_loop(trampoline_data.target.addr); return trampoline_data; } @@ -85,7 +85,7 @@ static auto perform_far_hook_test(std::span to_hook) { // Use 20 here as a reasonable guesstimate print_decode_loop(fixups.fixup_inst_destination.addr); puts("HOOKED:"); - print_decode_loop(hook_span); + print_decode_loop(fixups.target.addr); return fixups; } @@ -281,6 +281,67 @@ static void test_adrp() { } } +static void test_neg_adrp() { + puts("Testing negative adrp"); + static uint8_t to_hook[]{ 0x1f, 0x20, 0x00, 0x71, 0xa8, 0x00, 0x00, 0x54, 0xc8, 0x58, 0xff, 0x90, + 0x08, 0xc1, 0x29, 0x91, 0x00, 0xd9, 0x60, 0xb8, 0xc0, 0x03, 0x5f, 0xd6 }; + { + TestWrapper init_hook(to_hook, "negative adrp"); + init_hook.expect_opc(ARM64_INS_CMP); + init_hook.expect_opc(ARM64_INS_B); + init_hook.expect_ops(ARM64_INS_ADRP, ARM64_REG_X8, + ((int64_t)(&to_hook[0]) - 0x14E8000) & ~0xfff); + init_hook.expect_opc(ARM64_INS_ADD); + init_hook.expect_opc(ARM64_INS_LDR); + } + { + auto results = perform_near_hook_test(to_hook); + TestWrapper fixup_validator(results.fixup_inst_destination.addr, "Near hook negative adrp"); + fixup_validator.expect_opc(ARM64_INS_CMP); + // B is a near branch in this case + fixup_validator.expect_opc(ARM64_INS_B); + // ADRP is replaced with an ldr to load the data directly + // LDR x8, DATA[0] + fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X8, + round_up8(&results.fixup_inst_destination.addr[6])); + fixup_validator.expect_opc(ARM64_INS_ADD); + // Callback + fixup_validator.expect_b(&results.target.addr[4]); + // Data validation + // ADRP result must match + fixup_validator.expect_big_data(((int64_t)(results.target.addr.data()) - 0x14E8000) & ~0xfff); + } + { + auto results = perform_far_hook_test(to_hook); + TestWrapper fixup_validator(results.fixup_inst_destination.addr, "Far hook negative adrp"); + fixup_validator.expect_opc(ARM64_INS_CMP); + // b.hi past following b to ldr + br pair + fixup_validator.expect_b(&results.fixup_inst_destination.addr[3]); + // b over ldr + br pair + fixup_validator.expect_b(&results.fixup_inst_destination.addr[5]); + // LDR x17, DATA[0] + fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X17, + round_up8(&results.fixup_inst_destination.addr[10])); + fixup_validator.expect_ops(ARM64_INS_BR, ARM64_REG_X17); + // ADRP is replaced with an ldr to load the data directly + // LDR x9, DATA[1] + fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X8, + round_up8(&results.fixup_inst_destination.addr[12])); + fixup_validator.expect_opc(ARM64_INS_ADD); + // Callback + fixup_validator.expect_ops(ARM64_INS_LDR, ARM64_REG_X17, + round_up8(&results.fixup_inst_destination.addr[14])); + fixup_validator.expect_ops(ARM64_INS_BR, ARM64_REG_X17); + // Data validation + // B.hi destination should match + fixup_validator.expect_big_data((uint64_t)&results.target.addr[6]); + // ADRP result must match + fixup_validator.expect_big_data(((int64_t)(results.target.addr.data()) - 0x14E8000) & ~0xfff); + // Check callback point is valid + fixup_validator.expect_big_data(reinterpret_cast(&results.target.addr[4])); + } +} + // TODO: Test a case where we have a loop in the first 4 instructions // TODO: Test a case where we have an ldr literal that loads from within fixup range // TODO: Test a case with a negative adrp offset @@ -290,5 +351,6 @@ int main() { test_bls_tbzs_within_hook(); test_ldr_ldrb_tbnz_bl(); test_adrp(); + test_neg_adrp(); puts("ALL GOOD!"); } From 733f2c89cede889beef6fa8b179d5c55384fcc76 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Fri, 2 May 2025 21:47:03 -0700 Subject: [PATCH 045/134] Cleanup old files, format new ones --- shared/And64InlineHook.hpp | 48 --- shared/capi.h | 6 +- shared/hook-installer.hpp | 64 ---- shared/hooking.hpp | 226 -------------- shared/installer.hpp | 9 +- shared/more_stuff.hpp | 28 -- shared/type-info.hpp | 4 + shared/util.hpp | 58 ++-- src/And64InlineHook.cpp | 591 ------------------------------------- src/hook-installer.cpp | 139 --------- src/hook-target.cpp | 55 ---- src/main.cpp | 55 ---- src/things.cpp | 12 - 13 files changed, 40 insertions(+), 1255 deletions(-) delete mode 100644 shared/And64InlineHook.hpp delete mode 100644 shared/hook-installer.hpp delete mode 100644 shared/hooking.hpp delete mode 100644 shared/more_stuff.hpp delete mode 100644 src/And64InlineHook.cpp delete mode 100644 src/hook-installer.cpp delete mode 100644 src/hook-target.cpp delete mode 100644 src/main.cpp delete mode 100644 src/things.cpp diff --git a/shared/And64InlineHook.hpp b/shared/And64InlineHook.hpp deleted file mode 100644 index d370210..0000000 --- a/shared/And64InlineHook.hpp +++ /dev/null @@ -1,48 +0,0 @@ -/* - * @date : 2018/04/18 - * @author : Rprop (r_prop@outlook.com) - * https://github.com/Rprop/And64InlineHook - */ -/* - MIT License - - Copyright (c) 2018 Rprop (r_prop@outlook.com) - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -#pragma once -#include -#define A64_MAX_BACKUPS 1024 -#ifdef __aarch64__ -#ifdef __cplusplus -extern "C" { -#endif - - - - void A64HookFunction(void* const symbol, void* const replace, void **result); - void* A64HookFunctionV(void* const symbol, void* const replace, void *const rwx, const uintptr_t rwx_size); - -#ifdef __cplusplus -} -#endif -#else -#warning "Cannot use Android64 Hooking!" -#endif diff --git a/shared/capi.h b/shared/capi.h index 6a5dfd4..f729c2c 100644 --- a/shared/capi.h +++ b/shared/capi.h @@ -1,6 +1,8 @@ #pragma once #include + +#define FLAMINGO_C_EXPORT __attribute__((visibility("default"))) // The flamingo C API #ifdef __cplusplus @@ -30,7 +32,9 @@ typedef struct { /// The returned original_instructions pointer is safe to read in the range [0..hook_size). /// This function is commonly used for ensuring correct results when going through an xref trace, or validating real /// instructions. -FlamingoOriginalInstructionsResult flamingo_orig_for(uint32_t const* addr); +FLAMINGO_C_EXPORT FlamingoOriginalInstructionsResult flamingo_orig_for(uint32_t const* addr); + + #ifdef __cplusplus } diff --git a/shared/hook-installer.hpp b/shared/hook-installer.hpp deleted file mode 100644 index 64efa24..0000000 --- a/shared/hook-installer.hpp +++ /dev/null @@ -1,64 +0,0 @@ -#pragma once -#include "calling_convention.hpp" -#include -#include -#include -#include "hook-data.hpp" -#include "trampoline-allocator.hpp" - -struct HookTargetInstallation { - friend struct HookInstaller; - - #ifndef FLAMINGO_NO_REGISTRATION_CHECKS - enum struct RegistrationStatus { - None = 0, - Ok = 1, - MismatchTargetConv = 2, - MismatchTargetParamSizes = 4, - MismatchReturnSize = 8, - MismatchMetadataMidpoint = 16, - }; - private: - RegistrationStatus registration_status; - std::vector parameter_sizes; - std::size_t return_size; - public: - HookTargetInstallation(void* target_, HookData& from, uint16_t size) : parameter_sizes(from.parameter_sizes), return_size(from.return_size), target(target_), calling_convention(from.convention), method_size(size), metadata(from.metadata) { - installation_info.push_back(&from); - } - #else - HookTargetInstallation(void* target_, HookData& from, uint16_t size) : target(target_), calling_convention(from.convention), method_size(size), metadata(from.metadata) { - installation_info.push_back(&from); - } - #endif - void TryAddHook(HookData& toAdd, TargetData const& target); - - private: - void* target; - std::vector installation_info; - CallingConvention calling_convention; - uint16_t method_size; - InstallationMetadata metadata; - std::optional orig_trampoline; -}; - -struct HookInstaller { - static void CollectHooks(); - static void CreateAdjacencyMap(); - static void InstallHooks(); - - private: - static void InstallConventionalHook(HookTargetInstallation& toInstall); - - // In bytes - constexpr static uint16_t MinimumMethodSize = 20; - // In bytes - constexpr static uint16_t LeapfrogSize = 16; - // In bytes - constexpr static uint16_t ConventionalHookSize = 16; - static std::unordered_map collected_hooks; - #ifndef FLAMINGO_NO_LEAPFROG - static std::unordered_map> adjacency_map; - static void InstallLeapfrogHook(HookTargetInstallation const& toInstall, std::list const& adjacencies); - #endif -}; diff --git a/shared/hooking.hpp b/shared/hooking.hpp deleted file mode 100644 index 750bf17..0000000 --- a/shared/hooking.hpp +++ /dev/null @@ -1,226 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include "util.hpp" -#include - -// What do we want the syntax to look like? -// Remember, we need to provide a way of having priorities/before/after -// -// We could make something like: DelayHook([](){address to install to}) -// And also have something like: ImmediateHook -// And also: ImmediateHook(address to install to immediately) -// Would love to also provide information for before/after calls -// Templates for that would be _ideal_ but not _mandatory_ -// Ex: Before<"one", "two">, After<"three", "four"> -// Because we can't promote string literals to types (hecc you ndk) we may have some more trouble (as in, we would have to do runtime conversions to compare) -// Turns out clang sucks absolute ass and won't even let us use ANY type of non-template classes that aren't integral types. -// Sooooooo we are SOL from a constexpr perspective, without a ton of macros and all constexpr+virtual - -// Before and After types SHOULD use string literals to avoid ambiguity. - -template -requires ((std::is_convertible_v && ...)) -constexpr auto Before(TArgs... strs) { - return make_array(strs...); -} -template -requires ((std::is_convertible_v && ...)) -constexpr auto After(TArgs... strs) { - return make_array(strs...); -} - -struct Priority { - constexpr explicit Priority(uint32_t p) : priority(p) {} - uint32_t priority; -}; - -struct HookCreatorIl2Cpp { - auto getData() { - // Check the function signature make sure it is valid for our target installation - return Data(functionPtr); - } -}; - -struct TargetType { - void* dst; - std::size_t methodSize; -}; - -struct Data { - // This should be a function pointer - // Which should return a Target type - void* targetInstallation; - // To perform checks on hook installs, make sure everything matches - // TODO: Maybe unnecessary - - std::vector paramSizes; - std::size_t returnSize; - void* hookAddress; - private: - TargetType target; -}; - -/* - -Hook1 ---> Hook2 - --> Hook3 -Uninstall Hook2: -- Take hook1's orig, assign it to Hook3 -- ACTUALLY uninstall hook2: --- actually is done, we just move the pointers and call it a day. --- IF hook2 was the ONLY OR LAST hook, things are a bit trickier: ---- Last hook is the one that calls actual orig ---- Last hook removal: ---- Take orig from prev, assign it to true orig pointer ---- great! --- Else, hook2 is ONLY: ---- Replace hooked asm with original ---- deallocate fixups/true orig -- Update the tracking metadata -*/ - -// Uninstall hooks: -// - Pass in a method pointer to MY hooked method that I want uninstalled, perhaps also dst? - -std::vector hooks; - -struct Hook { - void* target; - Data install() { - auto lambda = [](Hook* self) { - self->target = (void*)0x12345678; - return TargetType{(void*)0x12345678, 0}; - }; - return Data{&std::bind(lambda, this)}; - } - static auto hook(int x, int y) {} - void uninstall() { - // Travel through hook collection - // Look at hook addresses - // Find a match to hook function - auto found = std::find_if(hooks.begin(), hooks.end(), [](Data& h) {return h.hookAddress == &hook;}); - if (found != hooks.end()) { - // Uninstall the hook - found->target - } - } -}; - -// Make sure hooks don't try to install within each other -// If target is at 0x0, cannot install until 0x14 (probably) - -struct HookInstaller { - // Holds the collection of hooks that need to be installed, specifically their metadata and where to install them (where applicable) - // When we install a hook, we should also provide a known method size - // Installing hooks should assume that MOST hooks are already provided for us - // We need to install in order, we may even want to force prefix/postfix - // Handler should be: - // Prefixes in correct order - // orig --> ret to temp - // Postfixes in correct order - // ret with temp -}; - -// Holds the metadata required for a hook -struct HookMetadata { - // The address to install this particular hook - virtual void* addr() = 0; - // The function to jump to during fire - virtual void* function() = 0; - virtual std::span befores() = 0; - virtual std::span afters() = 0; - virtual Priority priority() = 0; - virtual std::string id() = 0; -}; - -template -concept convertible_to = std::is_convertible_v; - -template -concept is_valid_metadata_getter = requires (T t) { - {t()} -> convertible_to; -}; - -// Pass in a metadata getter function, which we invoke (either early, or on each pass) to determine metadata -template -requires (is_valid_metadata_getter) -struct HookHandler { - // This will be what we jump to when we perform any hook. - // However, we need to properly have data about WHERE this hook is, what hooks we have, etc. - // We could push something to the top of stack that tells us our hook address/any other metadata we need - // But it is a bit unfortunate, since we could probably get the metadata in a more clever way. - - // The overall hook handler should get the metadata, then invoke each hook with metadata at the top of stack - // Each hook will have a prologue to take the top thing off of stack and put it into a non-volatile reg - // Then we take that value and assume it is metadata, which it is. - // That way, when we go to invoke our "orig" we actually just call the next thing in line. - // We could also solve this by dynamically changing what our orig is depending on hook installation - static R handler(TArgs... args) { - HookMetadata* metadata = metadataGetterFunc(); - // Iterate hooks for this particular section, fire all prefixes - // Then fire for orig, save ret to temp if non-void - // Then fire postfixes, provide ret as reference - // return ret, if needed - } - // We could also perform this differently depending on if we actually NEED our hook to be a b, though it makes the most immediate sense - // We could also just perform another level of indirection, though that doesn't really solve anything, per-se. -}; - -struct HookManager { - - static std::unordered_map> hooks; - - static void sort(std::vector& hooks) { - - std::sort(hooks.begin(), hooks.end(), [](HookMetadata* first, HookMetadata* second) { - // Return true if first should come before second - auto& firstId = first->id(); - auto& secondId = second->id(); - return first->id() == second->befores(). - }); - } - static void recompile(uint32_t* target, uint32_t methodSize = -1) { - // Recompiles all hooks that are at this location. - // A recompile consists of the following: - // - Collection of all hooks at this location - auto targetHooksItr = hooks.find(target); - if (targetHooksItr == hooks.end()) { - // No hooks means we just exit - return; - } - auto& targetHooks = targetHooksItr->second; - // - Sorted while respecting priorities - std::sort(targetHooks.begin(), targetHooks.end(), [](HookMetadata* first, HookMetadata* second) { - return first- - }); - // - Trampoline origs are recompiled in sorted order - // - Actual target is recompiled with jump to first hook, last hook's orig refers back to target - // - Optimizations may take place-- ex, leapfrog if known hooks are close enough - std::multiset::iterator - } -}; - -// Hooks could hold their own trampoline, though what we put in it should be handled by the thing that orders the various hooks - -struct Hook { - // Before/After hooks apply BEFORE priority - template - explicit Hook(std::string_view name, std::array before = std::array(), std::array after = std::array()) { - - } - // Priority specific hooks apply AFTER before/after hooks, are also unique from before/after construction - explicit Hook(std::string_view name, Priority p) { - - } -}; - -Hook q("Hi There!", Before("you", "him", "her"), After("Me")); - -Hook two("Test", Priority(10)); - diff --git a/shared/installer.hpp b/shared/installer.hpp index cc0497c..df8d659 100644 --- a/shared/installer.hpp +++ b/shared/installer.hpp @@ -1,14 +1,8 @@ - - -#include #include #include -#include #include -#include "fixups.hpp" #include "hook-data.hpp" #include "hook-installation-result.hpp" -#include "page-allocator.hpp" #include "target-data.hpp" #include "util.hpp" @@ -59,6 +53,7 @@ std::span FLAMINGO_EXPORT OriginalInstsFor(TargetDescriptor target); /// @brief Returns the Fixups span for a provided TargetDescriptor. /// If the target is not hooked, returns an error Result. -[[nodiscard]] FLAMINGO_EXPORT Result, std::monostate> FixupPointerFor(TargetDescriptor target); +[[nodiscard]] FLAMINGO_EXPORT Result, std::monostate> FixupPointerFor( + TargetDescriptor target); } // namespace flamingo \ No newline at end of file diff --git a/shared/more_stuff.hpp b/shared/more_stuff.hpp deleted file mode 100644 index b124232..0000000 --- a/shared/more_stuff.hpp +++ /dev/null @@ -1,28 +0,0 @@ -/* -Okay, so more stuff - -We want to have all hooks be preinstalled early -Then, we take all hooks that are to be installed at a given location -And, well, we install them - -We can worry about hooks that need to be installed specifically later, we just need to have hooks have a way of: --- where to install (function call, maybe address?) ---- should resolve to an address --- target pointer/functor (ex, contextual lambdas should be permissible, albeit hard to do early enough) --- metadata (ex, orig hook, trampoline or no, etc.) --- trampoline pointer to write to --- additional asm to write? ---- somehow we should try to make transpilers possible, though that can be figured out later - -Once we have all of the hooks: -- resolve all hook targets -- resolve metadata (ex, only one orig hook per target) -- hook (and optionally allocate trampoline) once per target --- (potentially) make use of target method size to determine if optimizations need to take place/if hook cannot be made -- create innard prologue with identical signature (size of parameters matter, not types) -- forward parameters to each method call --- calling the orig no longer is applicable due to ordering, prefix/postfix only. --- alternatively, innard prologue only forwards to most specific hook, orig calls forward to next in chain --- however, it must be a requirement that a given hook calls orig for this to be the case (or if it does not, fallthrough to call it on return) -- profit? -*/ diff --git a/shared/type-info.hpp b/shared/type-info.hpp index df113e9..931492b 100644 --- a/shared/type-info.hpp +++ b/shared/type-info.hpp @@ -28,4 +28,8 @@ struct TypeInfo { } } }; + +inline bool operator==(TypeInfo const& lhs, TypeInfo const& rhs) { + return lhs.size == rhs.size; +} } // namespace flamingo diff --git a/shared/util.hpp b/shared/util.hpp index fd9d479..109ff88 100644 --- a/shared/util.hpp +++ b/shared/util.hpp @@ -13,29 +13,29 @@ #include #include -#define LOGA(lvl, ...) \ - do { \ - std::string __ss = fmt::format(__VA_ARGS__); \ - __android_log_print(lvl, FLAMINGO_ID "|v" FLAMINGO_VERSION, "%s", __ss.c_str()); \ - } while (0) +#define LOGA(lvl, ...) \ + do { \ + std::string __ss = fmt::format(__VA_ARGS__); \ + __android_log_print(lvl, FLAMINGO_ID "|v" FLAMINGO_VERSION, "%s", __ss.c_str()); \ + } while (0) #ifndef NO_DEBUG_LOGS #define FLAMINGO_DEBUG(...) LOGA(ANDROID_LOG_DEBUG, __VA_ARGS__) -#define FLAMINGO_ASSERT(...) \ - do { \ - if (!(__VA_ARGS__)) FLAMINGO_ABORT("Failed condition: " #__VA_ARGS__); \ - } while (0) +#define FLAMINGO_ASSERT(...) \ + do { \ + if (!(__VA_ARGS__)) FLAMINGO_ABORT("Failed condition: " #__VA_ARGS__); \ + } while (0) #else #define FLAMINGO_DEBUG(...) #define FLAMINGO_ASSERT(...) __builtin_assume(__VA_ARGS__) #endif #define FLAMINGO_CRITICAL(...) LOGA(ANDROID_LOG_FATAL, __VA_ARGS__) -#define FLAMINGO_ABORT(...) \ - do { \ - FLAMINGO_CRITICAL(__VA_ARGS__); \ - std::abort(); \ - } while (0) +#define FLAMINGO_ABORT(...) \ + do { \ + FLAMINGO_CRITICAL(__VA_ARGS__); \ + std::abort(); \ + } while (0) #else // ANDROID #error "Need logging definitions here, for non-ANDROID, header only support!" #endif @@ -56,28 +56,28 @@ #endif #define FLAMINGO_CRITICAL(...) Paper::Logger::fmtLog(__VA_ARGS__) -#define FLAMINGO_ABORT(...) \ - do { \ - FLAMINGO_CRITICAL(__VA_ARGS__); \ - Paper::Logger::WaitForFlush(); \ - SAFE_ABORT(); \ - } while (0) +#define FLAMINGO_ABORT(...) \ + do { \ + FLAMINGO_CRITICAL(__VA_ARGS__); \ + Paper::Logger::WaitForFlush(); \ + SAFE_ABORT(); \ + } while (0) #else #include #include #define FLAMINGO_ASSERT(...) assert(__VA_ARGS__) -#define FLAMINGO_DEBUG(...) \ - fmt::print(__VA_ARGS__); \ - puts("") +#define FLAMINGO_DEBUG(...) \ + fmt::print(__VA_ARGS__); \ + puts("") #define FLAMINGO_CRITICAL(...) \ - fmt::print(__VA_ARGS__); \ - puts("") -#define FLAMINGO_ABORT(...) \ - fmt::print(__VA_ARGS__); \ - puts(""); \ - std::abort() + fmt::print(__VA_ARGS__); \ + puts("") +#define FLAMINGO_ABORT(...) \ + fmt::print(__VA_ARGS__); \ + puts(""); \ + std::abort() #endif #endif \ No newline at end of file diff --git a/src/And64InlineHook.cpp b/src/And64InlineHook.cpp deleted file mode 100644 index b6876da..0000000 --- a/src/And64InlineHook.cpp +++ /dev/null @@ -1,591 +0,0 @@ -/* - * @date : 2018/04/18 - * @author : Rprop (r_prop@outlook.com) - * https://github.com/Rprop/And64InlineHook - */ -/* - MIT License - - Copyright (c) 2018 Rprop (r_prop@outlook.com) - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ -#include -#include -#include -#include -#include -#include -#ifdef __aarch64__ - -#include "And64InlineHook.hpp" -#define A64_MAX_INSTRUCTIONS 5 -#define A64_MAX_REFERENCES (A64_MAX_INSTRUCTIONS * 2) -#define A64_NOP 0xd503201fu -#define A64_JNIEXPORT __attribute__((visibility("default"))) -#define A64_LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, "A64_HOOK", __VA_ARGS__)) -#ifndef NDEBUG -# define A64_LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "A64_HOOK", __VA_ARGS__)) -#else -# define A64_LOGI(...) ((void)0) -#endif // NDEBUG -using instruction = uint32_t *__restrict *__restrict; -struct context -{ - struct fix_info - { - uint32_t *bp; - uint32_t ls; // left-shift counts - uint32_t ad; // & operand - }; - struct insns_info - { - union - { - uint64_t insu; - int64_t ins; - void *insp; - }; - fix_info fmap[A64_MAX_REFERENCES]; - }; - int64_t basep; - int64_t endp; - insns_info dat[A64_MAX_INSTRUCTIONS]; - - inline bool is_in_fixing_range(const int64_t absolute_addr) { - return absolute_addr >= this->basep && absolute_addr < this->endp; - } - inline intptr_t get_ref_ins_index(const int64_t absolute_addr) { - return static_cast((absolute_addr - this->basep) / sizeof(uint32_t)); - } - inline intptr_t get_and_set_current_index(uint32_t *__restrict inp, uint32_t *__restrict outp) { - intptr_t current_idx = this->get_ref_ins_index(reinterpret_cast(inp)); - this->dat[current_idx].insp = outp; - return current_idx; - } - inline void reset_current_ins(const intptr_t idx, uint32_t *__restrict outp) { - this->dat[idx].insp = outp; - } - void insert_fix_map(const intptr_t idx, uint32_t *bp, uint32_t ls = 0u, uint32_t ad = 0xffffffffu) { - for (auto &f : this->dat[idx].fmap) { - if (f.bp == NULL) { - f.bp = bp; - f.ls = ls; - f.ad = ad; - return; - } //if - } - // What? GGing.. - } - void process_fix_map(const intptr_t idx) { - for (auto &f : this->dat[idx].fmap) { - if (f.bp == NULL) break; - *(f.bp) = *(f.bp) | (((int32_t(this->dat[idx].ins - reinterpret_cast(f.bp)) >> 2) << f.ls) & f.ad); - f.bp = NULL; - } - } -}; - -//------------------------------------------------------------------------- - -static bool __fix_branch_imm(instruction inpp, instruction outpp, context *ctxp) -{ - static constexpr uint32_t mbits = 6u; - static constexpr uint32_t mask = 0xfc000000u; // 0b11111100000000000000000000000000 - static constexpr uint32_t rmask = 0x03ffffffu; // 0b00000011111111111111111111111111 - static constexpr uint32_t op_b = 0x14000000u; // "b" ADDR_PCREL26 - static constexpr uint32_t op_bl = 0x94000000u; // "bl" ADDR_PCREL26 - - const uint32_t ins = *(*inpp); - const uint32_t opc = ins & mask; - switch (opc) { - case op_b: - case op_bl: - { - intptr_t current_idx = ctxp->get_and_set_current_index(*inpp, *outpp); - int64_t absolute_addr = reinterpret_cast(*inpp) + (static_cast(ins << mbits) >> (mbits - 2u)); // sign-extended - int64_t new_pc_offset = static_cast(absolute_addr - reinterpret_cast(*outpp)) >> 2; // shifted - bool special_fix_type = ctxp->is_in_fixing_range(absolute_addr); - // whether the branch should be converted to absolute jump - if (!special_fix_type && llabs(new_pc_offset) >= (rmask >> 1)) { - bool b_aligned = (reinterpret_cast(*outpp + 2) & 7u) == 0u; - if (opc == op_b) { - if (b_aligned != true) { - (*outpp)[0] = A64_NOP; - ctxp->reset_current_ins(current_idx, ++(*outpp)); - } //if - (*outpp)[0] = 0x58000051u; // LDR X17, #0x8 - (*outpp)[1] = 0xd61f0220u; // BR X17 - memcpy(*outpp + 2, &absolute_addr, sizeof(absolute_addr)); - *outpp += 4; - } else { - if (b_aligned == true) { - (*outpp)[0] = A64_NOP; - ctxp->reset_current_ins(current_idx, ++(*outpp)); - } //if - (*outpp)[0] = 0x58000071u; // LDR X17, #12 - (*outpp)[1] = 0x1000009eu; // ADR X30, #16 - (*outpp)[2] = 0xd61f0220u; // BR X17 - memcpy(*outpp + 3, &absolute_addr, sizeof(absolute_addr)); - *outpp += 5; - } //if - } else { - if (special_fix_type) { - intptr_t ref_idx = ctxp->get_ref_ins_index(absolute_addr); - if (ref_idx <= current_idx) { - new_pc_offset = static_cast(ctxp->dat[ref_idx].ins - reinterpret_cast(*outpp)) >> 2; - } else { - ctxp->insert_fix_map(ref_idx, *outpp, 0u, rmask); - new_pc_offset = 0; - } //if - } //if - - (*outpp)[0] = opc | (new_pc_offset & ~mask); - ++(*outpp); - } //if - - ++(*inpp); - return ctxp->process_fix_map(current_idx), true; - } - } - return false; -} - -//------------------------------------------------------------------------- - -static bool __fix_cond_comp_test_branch(instruction inpp, instruction outpp, context *ctxp) -{ - static constexpr uint32_t lsb = 5u; - static constexpr uint32_t lmask01 = 0xff00001fu; // 0b11111111000000000000000000011111 - static constexpr uint32_t mask0 = 0xff000010u; // 0b11111111000000000000000000010000 - static constexpr uint32_t op_bc = 0x54000000u; // "b.c" ADDR_PCREL19 - static constexpr uint32_t mask1 = 0x7f000000u; // 0b01111111000000000000000000000000 - static constexpr uint32_t op_cbz = 0x34000000u; // "cbz" Rt, ADDR_PCREL19 - static constexpr uint32_t op_cbnz = 0x35000000u; // "cbnz" Rt, ADDR_PCREL19 - static constexpr uint32_t lmask2 = 0xfff8001fu; // 0b11111111111110000000000000011111 - static constexpr uint32_t mask2 = 0x7f000000u; // 0b01111111000000000000000000000000 - static constexpr uint32_t op_tbz = 0x36000000u; // 0b00110110000000000000000000000000 "tbz" Rt, BIT_NUM, ADDR_PCREL14 - static constexpr uint32_t op_tbnz = 0x37000000u; // 0b00110111000000000000000000000000 "tbnz" Rt, BIT_NUM, ADDR_PCREL14 - - const uint32_t ins = *(*inpp); - uint32_t lmask = lmask01; - if ((ins & mask0) != op_bc) { - uint32_t opc = ins & mask1; - if (opc != op_cbz && opc != op_cbnz) { - opc = ins & mask2; - if (opc != op_tbz && opc != op_tbnz) { - return false; - } //if - lmask = lmask2; - } //if - } //if - - intptr_t current_idx = ctxp->get_and_set_current_index(*inpp, *outpp); - int64_t absolute_addr = reinterpret_cast(*inpp) + ((ins & ~lmask) >> (lsb - 2u)); - int64_t new_pc_offset = static_cast(absolute_addr - reinterpret_cast(*outpp)) >> 2; // shifted - bool special_fix_type = ctxp->is_in_fixing_range(absolute_addr); - if (!special_fix_type && llabs(new_pc_offset) >= (~lmask >> (lsb + 1))) { - if ((reinterpret_cast(*outpp + 4) & 7u) != 0u) { - (*outpp)[0] = A64_NOP; - ctxp->reset_current_ins(current_idx, ++(*outpp)); - } //if - (*outpp)[0] = (((8u >> 2u) << lsb) & ~lmask) | (ins & lmask); // B.C #0x8 - (*outpp)[1] = 0x14000005u; // B #0x14 - (*outpp)[2] = 0x58000051u; // LDR X17, #0x8 - (*outpp)[3] = 0xd61f0220u; // BR X17 - memcpy(*outpp + 4, &absolute_addr, sizeof(absolute_addr)); - *outpp += 6; - } else { - if (special_fix_type) { - intptr_t ref_idx = ctxp->get_ref_ins_index(absolute_addr); - if (ref_idx <= current_idx) { - new_pc_offset = static_cast(ctxp->dat[ref_idx].ins - reinterpret_cast(*outpp)) >> 2; - } else { - ctxp->insert_fix_map(ref_idx, *outpp, lsb, ~lmask); - new_pc_offset = 0; - } //if - } //if - - (*outpp)[0] = (static_cast(new_pc_offset << lsb) & ~lmask) | (ins & lmask); - ++(*outpp); - } //if - - ++(*inpp); - return ctxp->process_fix_map(current_idx), true; -} - -//------------------------------------------------------------------------- - -static bool __fix_loadlit(instruction inpp, instruction outpp, context *ctxp) -{ - const uint32_t ins = *(*inpp); - - // memory prefetch("prfm"), just skip it - // http://infocenter.arm.com/help/topic/com.arm.doc.100069_0608_00_en/pge1427897420050.html - if ((ins & 0xff000000u) == 0xd8000000u) { - ctxp->process_fix_map(ctxp->get_and_set_current_index(*inpp, *outpp)); - ++(*inpp); - return true; - } //if - - static constexpr uint32_t msb = 8u; - static constexpr uint32_t lsb = 5u; - static constexpr uint32_t mask_30 = 0x40000000u; // 0b01000000000000000000000000000000 - static constexpr uint32_t mask_31 = 0x80000000u; // 0b10000000000000000000000000000000 - static constexpr uint32_t lmask = 0xff00001fu; // 0b11111111000000000000000000011111 - static constexpr uint32_t mask_ldr = 0xbf000000u; // 0b10111111000000000000000000000000 - static constexpr uint32_t op_ldr = 0x18000000u; // 0b00011000000000000000000000000000 "LDR Wt/Xt, label" | ADDR_PCREL19 - static constexpr uint32_t mask_ldrv = 0x3f000000u; // 0b00111111000000000000000000000000 - static constexpr uint32_t op_ldrv = 0x1c000000u; // 0b00011100000000000000000000000000 "LDR St/Dt/Qt, label" | ADDR_PCREL19 - static constexpr uint32_t mask_ldrsw = 0xff000000u; // 0b11111111000000000000000000000000 - static constexpr uint32_t op_ldrsw = 0x98000000u; // "LDRSW Xt, label" | ADDR_PCREL19 | load register signed word - // LDR S0, #0 | 0b00011100000000000000000000000000 | 32-bit - // LDR D0, #0 | 0b01011100000000000000000000000000 | 64-bit - // LDR Q0, #0 | 0b10011100000000000000000000000000 | 128-bit - // INVALID | 0b11011100000000000000000000000000 | may be 256-bit - - uint32_t mask = mask_ldr; - uintptr_t faligned = (ins & mask_30) ? 7u : 3u; - if ((ins & mask_ldr) != op_ldr) { - mask = mask_ldrv; - if (faligned != 7u) - faligned = (ins & mask_31) ? 15u : 3u; - if ((ins & mask_ldrv) != op_ldrv) { - if ((ins & mask_ldrsw) != op_ldrsw) { - return false; - } //if - mask = mask_ldrsw; - faligned = 7u; - } //if - } //if - - intptr_t current_idx = ctxp->get_and_set_current_index(*inpp, *outpp); - int64_t absolute_addr = reinterpret_cast(*inpp) + ((static_cast(ins << msb) >> (msb + lsb - 2u)) & ~3u); - int64_t new_pc_offset = static_cast(absolute_addr - reinterpret_cast(*outpp)) >> 2; // shifted - bool special_fix_type = ctxp->is_in_fixing_range(absolute_addr); - // special_fix_type may encounter issue when there are mixed data and code - if (special_fix_type || (llabs(new_pc_offset) + (faligned + 1u - 4u) / 4u) >= (~lmask >> (lsb + 1))) { // inaccurate, but it works - while ((reinterpret_cast(*outpp + 2) & faligned) != 0u) { - *(*outpp)++ = A64_NOP; - } - ctxp->reset_current_ins(current_idx, *outpp); - - // Note that if memory at absolute_addr is writeable (non-const), we will fail to fetch it. - // And what's worse, we may unexpectedly overwrite something if special_fix_type is true... - uint32_t ns = static_cast((faligned + 1) / sizeof(uint32_t)); - (*outpp)[0] = (((8u >> 2u) << lsb) & ~mask) | (ins & lmask); // LDR #0x8 - (*outpp)[1] = 0x14000001u + ns; // B #0xc - memcpy(*outpp + 2, reinterpret_cast(absolute_addr), faligned + 1); - *outpp += 2 + ns; - } else { - faligned >>= 2; // new_pc_offset is shifted and 4-byte aligned - while ((new_pc_offset & faligned) != 0) { - *(*outpp)++ = A64_NOP; - new_pc_offset = static_cast(absolute_addr - reinterpret_cast(*outpp)) >> 2; - } - ctxp->reset_current_ins(current_idx, *outpp); - - (*outpp)[0] = (static_cast(new_pc_offset << lsb) & ~mask) | (ins & lmask); - ++(*outpp); - } //if - - ++(*inpp); - return ctxp->process_fix_map(current_idx), true; -} - -//------------------------------------------------------------------------- - -static bool __fix_pcreladdr(instruction inpp, instruction outpp, context *ctxp) -{ - // Load a PC-relative address into a register - // http://infocenter.arm.com/help/topic/com.arm.doc.100069_0608_00_en/pge1427897645644.html - static constexpr uint32_t msb = 8u; - static constexpr uint32_t lsb = 5u; - static constexpr uint32_t mask = 0x9f000000u; // 0b10011111000000000000000000000000 - static constexpr uint32_t rmask = 0x0000001fu; // 0b00000000000000000000000000011111 - static constexpr uint32_t lmask = 0xff00001fu; // 0b11111111000000000000000000011111 - static constexpr uint32_t fmask = 0x00ffffffu; // 0b00000000111111111111111111111111 - static constexpr uint32_t max_val = 0x001fffffu; // 0b00000000000111111111111111111111 - static constexpr uint32_t op_adr = 0x10000000u; // "adr" Rd, ADDR_PCREL21 - static constexpr uint32_t op_adrp = 0x90000000u; // "adrp" Rd, ADDR_ADRP - - const uint32_t ins = *(*inpp); - intptr_t current_idx; - switch (ins & mask) { - case op_adr: - { - current_idx = ctxp->get_and_set_current_index(*inpp, *outpp); - int64_t lsb_bytes = static_cast(ins << 1u) >> 30u; - int64_t absolute_addr = reinterpret_cast(*inpp) + (((static_cast(ins << msb) >> (msb + lsb - 2u)) & ~3u) | lsb_bytes); - int64_t new_pc_offset = static_cast(absolute_addr - reinterpret_cast(*outpp)); - bool special_fix_type = ctxp->is_in_fixing_range(absolute_addr); - if (!special_fix_type && llabs(new_pc_offset) >= (max_val >> 1)) { - if ((reinterpret_cast(*outpp + 2) & 7u) != 0u) { - (*outpp)[0] = A64_NOP; - ctxp->reset_current_ins(current_idx, ++(*outpp)); - } //if - - (*outpp)[0] = 0x58000000u | (((8u >> 2u) << lsb) & ~mask) | (ins & rmask); // LDR #0x8 - (*outpp)[1] = 0x14000003u; // B #0xc - memcpy(*outpp + 2, &absolute_addr, sizeof(absolute_addr)); - *outpp += 4; - } else { - if (special_fix_type) { - intptr_t ref_idx = ctxp->get_ref_ins_index(absolute_addr & ~3ull); - if (ref_idx <= current_idx) { - new_pc_offset = static_cast(ctxp->dat[ref_idx].ins - reinterpret_cast(*outpp)); - } else { - ctxp->insert_fix_map(ref_idx, *outpp, lsb, fmask); - new_pc_offset = 0; - } //if - } //if - - // the lsb_bytes will never be changed, so we can use lmask to keep it - (*outpp)[0] = (static_cast(new_pc_offset << (lsb - 2u)) & fmask) | (ins & lmask); - ++(*outpp); - } //if - } - break; - case op_adrp: - { - current_idx = ctxp->get_and_set_current_index(*inpp, *outpp); - int32_t lsb_bytes = static_cast(ins << 1u) >> 30u; - int64_t absolute_addr = (reinterpret_cast(*inpp) & ~0xfffll) + ((((static_cast(ins << msb) >> (msb + lsb - 2u)) & ~3u) | lsb_bytes) << 12); - A64_LOGI("ins = 0x%.8X, pc = %p, abs_addr = %p", - ins, *inpp, reinterpret_cast(absolute_addr)); - if (ctxp->is_in_fixing_range(absolute_addr)) { - intptr_t ref_idx = ctxp->get_ref_ins_index(absolute_addr/* & ~3ull*/); - if (ref_idx > current_idx) { - // the bottom 12 bits of absolute_addr are masked out, - // so ref_idx must be less than or equal to current_idx! - A64_LOGE("ref_idx must be less than or equal to current_idx!"); - } //if - - // *absolute_addr may be changed due to relocation fixing - A64_LOGI("What is the correct way to fix this?"); - *(*outpp)++ = ins; // 0x90000000u; - } else { - if ((reinterpret_cast(*outpp + 2) & 7u) != 0u) { - (*outpp)[0] = A64_NOP; - ctxp->reset_current_ins(current_idx, ++(*outpp)); - } //if - - (*outpp)[0] = 0x58000000u | (((8u >> 2u) << lsb) & ~mask) | (ins & rmask); // LDR #0x8 - (*outpp)[1] = 0x14000003u; // B #0xc - memcpy(*outpp + 2, &absolute_addr, sizeof(absolute_addr)); // potential overflow? - *outpp += 4; - } //if - } - break; - default: - return false; - } - - ctxp->process_fix_map(current_idx); - ++(*inpp); - return true; -} - -//------------------------------------------------------------------------- -#define __flush_cache(c, n) __builtin___clear_cache(reinterpret_cast(c), reinterpret_cast(c) + n) -static void __fix_instructions(uint32_t *__restrict inp, int32_t count, uint32_t *__restrict outp) -{ - context ctx; - ctx.basep = reinterpret_cast(inp); - ctx.endp = reinterpret_cast(inp + count); - memset(ctx.dat, 0, sizeof(ctx.dat)); - static_assert(sizeof(ctx.dat) / sizeof(ctx.dat[0]) == A64_MAX_INSTRUCTIONS, - "please use A64_MAX_INSTRUCTIONS!"); -#ifndef NDEBUG - if (count > A64_MAX_INSTRUCTIONS) { - A64_LOGE("too many fixing instructions!"); - } //if -#endif // NDEBUG - - uint32_t *const outp_base = outp; - - while (--count >= 0) { - if (__fix_branch_imm(&inp, &outp, &ctx)) continue; - if (__fix_cond_comp_test_branch(&inp, &outp, &ctx)) continue; - if (__fix_loadlit(&inp, &outp, &ctx)) continue; - if (__fix_pcreladdr(&inp, &outp, &ctx)) continue; - - // without PC-relative offset - ctx.process_fix_map(ctx.get_and_set_current_index(inp, outp)); - *(outp++) = *(inp++); - } - - static constexpr uint_fast64_t mask = 0x03ffffffu; // 0b00000011111111111111111111111111 - auto callback = reinterpret_cast(inp); - auto pc_offset = static_cast(callback - reinterpret_cast(outp)) >> 2; - if (llabs(pc_offset) >= (mask >> 1)) { - if ((reinterpret_cast(outp + 2) & 7u) != 0u) { - outp[0] = A64_NOP; - ++outp; - } //if - outp[0] = 0x58000051u; // LDR X17, #0x8 - outp[1] = 0xd61f0220u; // BR X17 - *reinterpret_cast(outp + 2) = callback; - outp += 4; - } else { - outp[0] = 0x14000000u | (pc_offset & mask); // "B" ADDR_PCREL26 - ++outp; - } //if - - const uintptr_t total = (outp - outp_base) * sizeof(uint32_t); - __flush_cache(outp_base, total); // necessary -} - -//------------------------------------------------------------------------- - -extern "C" { -#define __attribute __attribute__ -#define aligned(x) __aligned__(x) -#define __intval(p) reinterpret_cast(p) -#define __uintval(p) reinterpret_cast(p) -#define __ptr(p) reinterpret_cast(p) -#define __page_size 4096 -#define __page_align(n) __align_up(static_cast(n), __page_size) -#define __ptr_align(x) __ptr(__align_down(reinterpret_cast(x), __page_size)) -#define __align_up(x, n) (((x) + ((n) - 1)) & ~((n) - 1)) -#define __align_down(x, n) ((x) & -(n)) -#define __countof(x) static_cast(sizeof(x) / sizeof((x)[0])) // must be signed -#define __atomic_increase(p) __sync_add_and_fetch(p, 1) -#define __sync_cmpswap(p, v, n) __sync_bool_compare_and_swap(p, v, n) -#define __predict_true(exp) __builtin_expect((exp) != 0, 1) - -#define __make_rwx(p, n) ::mprotect(__ptr_align(p), \ - __page_align(__uintval(p) + n) != __page_align(__uintval(p)) ? __page_align(n) + __page_size : __page_align(n), \ - PROT_READ | PROT_WRITE | PROT_EXEC) - - //------------------------------------------------------------------------- - - static __attribute((aligned(__page_size))) uint32_t __insns_pool[A64_MAX_BACKUPS][A64_MAX_INSTRUCTIONS * 10]; - - //------------------------------------------------------------------------- - - class A64HookInit - { - public: - A64HookInit() - { - __make_rwx(__insns_pool, sizeof(__insns_pool)); - A64_LOGI("insns pool initialized."); - } - }; - static A64HookInit __init; - - //------------------------------------------------------------------------- - - static uint32_t *FastAllocateTrampoline() - { - static_assert((A64_MAX_INSTRUCTIONS * 10 * sizeof(uint32_t)) % 8 == 0, "8-byte align"); - static volatile int32_t __index = -1; - - int32_t i = __atomic_increase(&__index); - if (__predict_true(i >= 0 && i < __countof(__insns_pool))) { - return __insns_pool[i]; - } //if - - A64_LOGE("failed to allocate trampoline!"); - return NULL; - } - - //------------------------------------------------------------------------- - - A64_JNIEXPORT void *A64HookFunctionV(void *const symbol, void *const replace, - void *const rwx, const uintptr_t rwx_size) - { - static constexpr uint_fast64_t mask = 0x03ffffffu; // 0b00000011111111111111111111111111 - - uint32_t *trampoline = static_cast(rwx), *original = static_cast(symbol); - - static_assert(A64_MAX_INSTRUCTIONS >= 5, "please fix A64_MAX_INSTRUCTIONS!"); - auto pc_offset = static_cast(__intval(replace) - __intval(symbol)) >> 2; - if (llabs(pc_offset) >= (mask >>1)) { - int32_t count = (reinterpret_cast(original + 2) & 7u) != 0u ? 5 : 4; - if (trampoline) { - if (rwx_size < count * 10u) { - // LOGW("rwx size is too small to hold %u bytes backup instructions!", count * 10u); - return NULL; - } //if - __fix_instructions(original, count, trampoline); - } //if - - if (__make_rwx(original, 5 * sizeof(uint32_t)) == 0) { - if (count == 5) { - original[0] = A64_NOP; - ++original; - } //if - original[0] = 0x58000051u; // LDR X17, #0x8 - original[1] = 0xd61f0220u; // BR X17 - *reinterpret_cast(original + 2) = __intval(replace); - __flush_cache(symbol, 5 * sizeof(uint32_t)); - - A64_LOGI("inline hook %p->%p successfully! %zu bytes overwritten", - symbol, replace, 5 * sizeof(uint32_t)); - } else { - A64_LOGE("mprotect failed with errno = %d, p = %p, size = %zu", - errno, original, 5 * sizeof(uint32_t)); - trampoline = NULL; - } //if - } else { - if (trampoline) { - if (rwx_size < 1u * 10u) { - // LOGW("rwx size is too small to hold %u bytes backup instructions!", 1u * 10u); - return NULL; - } //if - __fix_instructions(original, 1, trampoline); - } //if - - if (__make_rwx(original, 1 * sizeof(uint32_t)) == 0) { - __sync_cmpswap(original, *original, 0x14000000u | (pc_offset & mask)); // "B" ADDR_PCREL26 - __flush_cache(symbol, 1 * sizeof(uint32_t)); - - A64_LOGI("inline hook %p->%p successfully! %zu bytes overwritten", - symbol, replace, 1 * sizeof(uint32_t)); - } else { - A64_LOGE("mprotect failed with errno = %d, p = %p, size = %zu", - errno, original, 1 * sizeof(uint32_t)); - trampoline = NULL; - } //if - } //if - - return trampoline; - } - - //------------------------------------------------------------------------- - - A64_JNIEXPORT void A64HookFunction(void *const symbol, void *const replace, void **result) - { - void *trampoline = NULL; - if (result != NULL) { - trampoline = FastAllocateTrampoline(); - *result = trampoline; - if (trampoline == NULL) return; - } //if - - trampoline = A64HookFunctionV(symbol, replace, trampoline, A64_MAX_INSTRUCTIONS * 10u); - if (trampoline == NULL && result != NULL) { - *result = NULL; - } //if - } -} - -#endif // defined(__aarch64__) \ No newline at end of file diff --git a/src/hook-installer.cpp b/src/hook-installer.cpp deleted file mode 100644 index 595406a..0000000 --- a/src/hook-installer.cpp +++ /dev/null @@ -1,139 +0,0 @@ -#include "hook-installer.hpp" -#include "enum-helpers.hpp" -#include -#include "trampoline-allocator.hpp" - -std::unordered_map HookInstaller::collected_hooks; -std::unordered_map> HookInstaller::adjacency_map; - -void HookInstaller::CreateAdjacencyMap() { - // After we collected all hooks, create an adjacency map so we can do leapfrog hooks. - // This is O(N^2) N being hooks that are small. - // Log begin - for (auto [target, hook] : collected_hooks) { - if (hook.method_size < MinimumMethodSize) { - // Oh no! We have a method that needs to be leapfrog hooked. - // For now, we shall add an entry to the adjacency map. - auto lst_pair = adjacency_map.insert({target, std::list()}); - for (auto [target2, hook2] : collected_hooks) { - if (hook2.method_size > MinimumMethodSize + LeapfrogSize) { - // Space for a POTENTIAL leapfrog hook - // Need to make sure it's actually within an acceptable range, of course - // Nested leapfrogs may also need to apply - // Log additions here - lst_pair.first->second.push_back(target2); - } - } - } - } - // Log completion -} - -void HookInstaller::CollectHooks() { - // Beginning collection! - for (auto& hook : HookData::hooks_to_install) { - // For each hook, resolve target - auto target = hook.resolution_function(); - auto insertion = collected_hooks.try_emplace(target.target_method, target.target_method, hook, target.method_size); - if (!insertion.second) { - // Already exists, try to add the hook - insertion.first->second.TryAddHook(hook, target); - } - // TODO: Consider if we need to add more metadata for hook installation smoothness? - // Log what we are doing: - // Collecting hook at: target, with target method size: size, with signature: retSize(paramSizes...), and metadata: metadata, with hook: ptr, trampoline: ptr - } - // Collection of hooks complete! -} - -void HookInstaller::InstallConventionalHook(HookTargetInstallation& toInstall) { - #ifndef FLAMINGO_NO_REGISTRATION_CHECKS - assert(toInstall.registration_status == HookTargetInstallation::RegistrationStatus::Ok); - #endif - // The metadata for is_midpoint is actually not necessary here-- only whether we want an orig - // If we do, we should allocate space for one. The space we allocate should be based off of two factors: - // 1. The size of our hook in instructions - // 2. If we need to perform a longer trampoline + fixups because we are a leapfrog destination - // TODO: For now, we shall assume the first case alone. - auto hookSize = ConventionalHookSize; - // First hook is the one that is closest to the game code, that is, last executed - // The last hook is the one that is actually written to the target destination, with each subsequent earlier hook - // being the one that is called next, until the first hook is reached. - // The first hook's orig either calls the allocated trampoline OR it does nothing - - std::size_t trampolineSize = hookSize; - // Write actual hook to be a callback - Trampoline targetHook(reinterpret_cast(toInstall.target), hookSize, trampolineSize); - targetHook.WriteCallback(reinterpret_cast(toInstall.installation_info.back()->hook_ptr)); - // TODO: Write our remaining callbacks for leapfrog hooks - targetHook.Finish(); - // Flush icache to update hook - __builtin___clear_cache(reinterpret_cast(toInstall.target), reinterpret_cast(toInstall.target) + hookSize); - // Log what we have done so far and also the rest of this - for (auto revItr = toInstall.installation_info.rbegin(); revItr != toInstall.installation_info.rend();) { - // hook_ptr is where we want to jump to FIRST. - auto hook = *revItr; - // orig ptr for this should forward to the hook_ptr for the next - if (revItr++ != toInstall.installation_info.rend()) { - *hook->orig_ptr = (*revItr)->hook_ptr; - } else { - // hook is the final hook. - // Check to see if we want an orig, and if so, assign it - if (toInstall.metadata.need_orig) { - auto trampoline = TrampolineAllocator::Allocate(Trampoline::MaximumFixupSize * hookSize); - // Write our fixups to the trampoline from our target address - trampoline.WriteFixups(reinterpret_cast(toInstall.target), ConventionalHookSize / sizeof(uint32_t)); - *hook->orig_ptr = trampoline.address; - toInstall.orig_trampoline.emplace(trampoline); - } else { - // TODO: Set the last hook's orig to some value that should never be called, to ensure nothing ever calls this. - // This might also be handled by the macros. - } - } - } - // At this point, our hooks have all been installed! - // We should mark this hook as installed -} - -void HookInstaller::InstallHooks() { - // Now, we are done collecting all hooks and have them in a better IR form. - // We should first check our leapfrog hooks (adjacency map) - #ifndef FLAMINGO_NO_LEAPFROG - if (!adjacency_map.empty()) { - // We have a NONZERO quantity of hooks that need to be leapfrogged. - // All hooks that are in the dst of this list need to be managed. - // TODO: For now, simply say that we can't handle this circumstance. - // Log this - } - #endif - // For each, we are going to have to do some stuff. - for (auto [target, hook] : collected_hooks) { - #ifndef FLAMINGO_NO_REGISTRATION_CHECKS - if (hook.registration_status == HookTargetInstallation::RegistrationStatus::Ok) { - // Installed OK! - // Log what we are doing - } - else { - if (flamingo::enum_helpers::HasFlag(hook.registration_status)) { - // Log mismatch of target convention - } if (flamingo::enum_helpers::HasFlag(hook.registration_status)) { - // Log mismatch of sizes - } if (flamingo::enum_helpers::HasFlag(hook.registration_status)) { - // Log mismatch of param sizes - } - // Continue even after noting this. - // Log this - } - #endif - - if (hook.method_size >= MinimumMethodSize) { - // This is a trivial example, we can just install :) - // TODO: However, this should only be the case if we ALSO are not going to allocate space for a leapfrog destination. - InstallConventionalHook(hook); - } else { - // TODO: Leapfrog install and determine validity - // Log and skip this install - } - } - // Log all hooks are installed! -} \ No newline at end of file diff --git a/src/hook-target.cpp b/src/hook-target.cpp deleted file mode 100644 index d4cd796..0000000 --- a/src/hook-target.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include "hook-installer.hpp" -#include -#include "enum-helpers.hpp" - -void HookTargetInstallation::TryAddHook(HookData& toAdd, TargetData const& target) -{ - assert(!installation_info.empty()); - // Only need to check the back of the collection to ensure validity. - installation_info.push_back(&toAdd); - - #ifndef FLAMINGO_NO_REGISTRATION_CHECKS - HookTargetInstallation::RegistrationStatus status = HookTargetInstallation::RegistrationStatus::None; - if (calling_convention != target.calling_convention) { - status = flamingo::enum_helpers::AddFlag(status); - } - if (return_size != toAdd.return_size) { - status = flamingo::enum_helpers::AddFlag(status); - } - if (parameter_sizes.size() == toAdd.parameter_sizes.size()) { - auto itr1 = parameter_sizes.begin(); - auto itr2 = toAdd.parameter_sizes.begin(); - for (; itr2 != toAdd.parameter_sizes.end(); itr1++, itr2++) { - if (*itr1 != *itr2) { - status = flamingo::enum_helpers::AddFlag(status); - break; - } - } - } else { - status = flamingo::enum_helpers::AddFlag(status); - } - // If we think of ourselves as being a midpoint hook, yet someone else disagrees, this is a problem - if (metadata.is_midpoint != toAdd.metadata.is_midpoint) { - status = flamingo::enum_helpers::AddFlag(status); - } - if (metadata.need_orig != toAdd.metadata.need_orig) { - // TODO: Determine how we want to handle this case, specifically where someone wants orig but someone else doesn't (is a rewrite) - // Should we have the non-orig be the top level and call it a day? - // - Thus should we only complain if we have TWO cases where that is not the case? - // For now, we will just not handle this any differently and probably just excessively write an orig - } - - // Use the most restrictive set of fixup registers possible - // metadata.permissible_fixup_registers &= toAdd.metadata.permissible_fixup_registers; - - if (status == HookTargetInstallation::RegistrationStatus::None) { - // Success until this point - status = HookTargetInstallation::RegistrationStatus::Ok; - } - registration_status = status; - if (target.method_size < method_size) { - // Method size is the minimum known method size for this location - method_size = target.method_size; - } - #endif -} diff --git a/src/main.cpp b/src/main.cpp deleted file mode 100644 index 71e827e..0000000 --- a/src/main.cpp +++ /dev/null @@ -1,55 +0,0 @@ -// On dlopen, we should basically just construct our vectors and everything else -// As well as our analytics data -#include "more_stuff.hpp" -#include "hook-installer.hpp" -// #include "hook-installer.hpp" -#include "modloader/shared/modloader.hpp" -// #include -#include -#include "enum-helpers.hpp" - -// Error reporter for pseudo-enum types -namespace { - template - struct ErrorReporter; - - #define REPORT_ERROR(Type, Value, msg) \ - template<> \ - struct ErrorReporter { \ - static void ReportError(Type value, HookTargetInstallation& existing, HookData& incoming) { \ - if ((static_cast(value) & static_cast(Type::Value)) != 0) { \ - /* TODO: printf("Error registering hook! Error: " msg "\n"); */ \ - } \ - } \ - } - - REPORT_ERROR(HookTargetInstallation::RegistrationStatus, MismatchTargetConv, "Mismatched target calling convention!"); - REPORT_ERROR(HookTargetInstallation::RegistrationStatus, MismatchTargetParamSizes, "Mismatched target parameter sizes!"); - REPORT_ERROR(HookTargetInstallation::RegistrationStatus, MismatchReturnSize, "Mismatched return size!"); - - template - concept has_error_reporter = requires (HookTargetInstallation& existing, HookData& incoming) { - ErrorReporter::ReportError(value, existing, incoming); - }; - - template - requires (has_error_reporter && ...) - void ReportErrors(HookTargetInstallation::RegistrationStatus value, HookTargetInstallation& existing, HookData& incoming) { - (ErrorReporter::ReportError(value, existing, incoming), ...); - } -} - -std::list HookData::hooks_to_install; - -void HookData::RegisterHook(HookData&& data) { - hooks_to_install.emplace_back(std::forward(data)); -} - -extern "C" void load() { - // Here's where we will INSTALL all of our hooks! - HookInstaller::CollectHooks(); - #ifndef FLAMINGO_NO_LEAPFROG - HookInstaller::CreateAdjacencyMap(); - #endif - HookInstaller::InstallHooks(); -} \ No newline at end of file diff --git a/src/things.cpp b/src/things.cpp deleted file mode 100644 index ecd8b7d..0000000 --- a/src/things.cpp +++ /dev/null @@ -1,12 +0,0 @@ -// OK, so, we wanna uninstall properly -// We also wanna allocate pages dynamically -// We also wanna come up with cool ways to do things -// Properly create not shit trampolines -// Dissassembler? - Binja -// Support transpilers -// Proper tracking of all hooks known -// Support for manual install/automatic install -// Grouping of hooks/before/after for installation timing - -// #include "hooking.hpp" - From 31d6fc8a1eef316f89ed110a759d7b07a5e78371 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Fri, 2 May 2025 21:47:21 -0700 Subject: [PATCH 046/134] Add validation logic to installs --- shared/hook-installation-result.hpp | 101 +++++++++++----------------- src/installer.cpp | 47 ++++++++++--- 2 files changed, 77 insertions(+), 71 deletions(-) diff --git a/shared/hook-installation-result.hpp b/shared/hook-installation-result.hpp index f675c8d..de287c3 100644 --- a/shared/hook-installation-result.hpp +++ b/shared/hook-installation-result.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include #include #include @@ -12,12 +13,12 @@ namespace flamingo { -template +template struct is_variant { constexpr static bool value = false; }; -template +template struct is_variant> { constexpr static bool value = true; }; @@ -34,10 +35,11 @@ struct Result { return Result{ std::variant(std::in_place_index_t<0>{}, std::forward(args)...) }; } // Helper function for if E is a variant (we have multiple errors and need to construct one) - template - requires (is_variant::value) + template + requires(is_variant::value) static Result ErrAt(TArgs&&... args) { - return Result{ std::variant(std::in_place_index_t<0>{}, E(std::in_place_type_t{}, std::forward(args)...))}; + return Result{ std::variant(std::in_place_index_t<0>{}, + E(std::in_place_type_t{}, std::forward(args)...)) }; } T const& value() const { return std::get<1>(data); @@ -74,89 +76,66 @@ struct TargetTooSmall : HookErrorInfo { uint_fast16_t actual_num_insts; uint_fast16_t needed_num_insts; }; -/// @brief An error when the target method is impossible to install given its priorities and other hooks to install it onto. +/// @brief An error when the target method is impossible to install given its priorities and other hooks to install it +/// onto. struct TargetBadPriorities : HookErrorInfo { // TODO: Add a bunch of stuff here TargetBadPriorities(HookMetadata const& m) : HookErrorInfo(m.name_info) {} }; -/// @brief An error when the target method has some validation failure with respect to the metadata it holds. -struct TargetMismatch : HookErrorInfo { - TargetMismatch(HookMetadata const& m) : HookErrorInfo(m.name_info) {} -}; - // TODO: Should we add the incoming hook IDs? -struct MismatchReturn { - TypeInfo existing{}; - TypeInfo incoming{}; +#ifndef FLAMINGO_NO_REGISTRATION_CHECKS +struct MismatchReturn : HookErrorInfo { + MismatchReturn(HookMetadata const& m, TypeInfo existing) + : HookErrorInfo(m.name_info), existing(existing), incoming(m.return_info) {} + TypeInfo existing; + TypeInfo incoming; }; -struct MismatchParam { +struct MismatchParam : HookErrorInfo { + MismatchParam(HookMetadata const& m, size_t idx, TypeInfo existing) + : HookErrorInfo(m.name_info), idx(idx), existing(existing), incoming(m.parameter_info[idx]) {} size_t idx{}; TypeInfo existing{}; TypeInfo incoming{}; }; -struct MismatchTargetConv { +struct MismatchParamCount : HookErrorInfo { + MismatchParamCount(HookMetadata const& m, size_t existing) + : HookErrorInfo(m.name_info), existing(existing), incoming(m.parameter_info.size()) {} + size_t existing; + size_t incoming; +}; + +#endif + +struct MismatchTargetConv : HookErrorInfo { + MismatchTargetConv(HookMetadata const& m, CallingConvention existing) + : HookErrorInfo(m.name_info), existing(existing), incoming(m.convention) {} CallingConvention existing{}; CallingConvention incoming{}; }; -struct MismatchMidpoint { +struct MismatchMidpoint : HookErrorInfo { + MismatchMidpoint(HookMetadata const& m, bool existing) + : HookErrorInfo(m.name_info), existing(existing), incoming(m.installation_metadata.is_midpoint) {} bool existing{}; bool incoming{}; }; -namespace util { - -template -consteval static bool all_unique() { - if constexpr (sizeof...(TArgs) == 0ULL) { - return true; - } else { - return (!std::is_same_v && ...) && all_unique(); - } -} - -template - requires(all_unique()) -using OptionTuple = std::tuple...>; - -template - requires(all_unique()) -struct OptionalErrors { - private: - OptionTuple maybe_errors; - - public: - [[nodiscard]] constexpr bool has_error() const { - return std::apply([](auto&&... opts) { return (opts || ...); }, maybe_errors); - } - template - void assign(T&& val) { - std::get>(maybe_errors) = std::forward(val); - } -}; - -} // namespace util +#ifndef FLAMINGO_NO_REGISTRATION_CHECKS +/// @brief An error when the target method has some validation failure with respect to the metadata it holds. +using TargetMismatch = std::variant; +#else +/// @brief An error when the target method has some validation failure with respect to the metadata it holds. +using TargetMismatch = std::variant; +#endif // Can be one of many cases. using Error = std::variant; using Result = flamingo::Result; -template -auto& get(Error const& obj) { - return std::get>(obj); -} - -template -void merge_optional(std::optional& lhs, std::optional const& rhs) { - if (rhs) { - lhs.emplace(*rhs); - } -} - } // namespace installation } // namespace flamingo diff --git a/src/installer.cpp b/src/installer.cpp index 5c52153..489ffa2 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -1,4 +1,5 @@ #include "installer.hpp" +#include #include #include #include @@ -24,25 +25,49 @@ using namespace flamingo; /// @brief The set of all targets hooked. An ordered map so we can perform large-scale walks by doing binary search. inline static std::map targets; -Result::iterator, installation::TargetBadPriorities> find_suitable_priority_location_for(std::list& hooks, HookPriority priority) { +Result::iterator, installation::TargetBadPriorities> find_suitable_priority_location_for( + std::list& hooks, HookPriority priority) { // Install onto the target, respecting priorities. // Note that we may need to recompile some callbacks/fixups to change things // 1. Topological sort on our hooks that exist here by priority - // - Find a suitable location where we can fit (note that we MAY need to recompile and move hooks around in order to do this) - // - First, walk all the hooks for a viable location, if we can find one. If we cannot, then we have to recompile hooks. + // - Find a suitable location where we can fit (note that we MAY need to recompile and move hooks around in order to + // do this) + // - First, walk all the hooks for a viable location, if we can find one. If we cannot, then we have to recompile + // hooks. // TODO: Above static_cast(priority); return Result::iterator, installation::TargetBadPriorities>::Ok(hooks.begin()); } -Result validate_install_metadata(TargetMetadata& existing, HookMetadata const& incoming) { +Result validate_install_metadata(TargetMetadata& existing, + HookMetadata const& incoming) { // TODO: At this point, we should be double checking metadata. + using ResultT = Result; // 1. Take the min of num_insts/verify they are equivalent + existing.method_num_insts = std::min(existing.method_num_insts, incoming.method_num_insts); // 2. Validate calling convention matches - // 3. Ensure parameter_info and return_info are matching (ifdef guarded) - static_cast(existing); - static_cast(incoming); - return Result::Ok(); + if (existing.convention != incoming.convention) { + return ResultT::ErrAt(incoming, existing.convention); + } + // 3. Validate midpoint matches + if (existing.metadata.is_midpoint != incoming.installation_metadata.is_midpoint) { + return ResultT::ErrAt(incoming, existing.metadata.is_midpoint); + } +// 3. Ensure parameter_info and return_info are matching (ifdef guarded) +#ifndef FLAMINGO_NO_REGISTRATION_CHECKS + if (existing.return_info != incoming.return_info) { + return ResultT::ErrAt(incoming, existing.return_info); + } + if (existing.parameter_info.size() != incoming.parameter_info.size()) { + return ResultT::ErrAt(incoming, existing.parameter_info.size()); + } + for (size_t i = 0; i < existing.parameter_info.size(); i++) { + if (existing.parameter_info[i] != incoming.parameter_info[i]) { + return ResultT::ErrAt(incoming, i, existing.parameter_info[i]); + } + } +#endif + return ResultT::Ok(); } } // namespace @@ -120,7 +145,8 @@ installation::Result Install(HookInfo&& hook) { return installation::Result::ErrAt(location_or_err.error()); } auto const location = location_or_err.value(); - // 2. Assuming we found a reasonable location to install, insert our new hook before this location, and then adjust those around us to match. + // 2. Assuming we found a reasonable location to install, insert our new hook before this location, and then adjust + // those around us to match. auto const hook_data_result = hooked_target->second.hooks.emplace(location, std::move(hook)); // - This is done by looking to the left and right of our target iterator to insert at: // -- If left does not exist: Rewrite the jump from the target to us; else rewrite the left's orig final jump to us @@ -153,7 +179,8 @@ Result Reinstall(TargetDescriptor target) { // Perform the write of the jump to the first hook itr->second.fixups.target.WriteJump(itr->second.hooks.begin()->hook_ptr); // Note that we do NOT reconstruct all of the inner hook pointers between each hook. - // This is done as a partial optimization, but at some point we should revisit this (and adjust the docstring comment to match) + // This is done as a partial optimization, but at some point we should revisit this (and adjust the docstring comment + // to match) // TODO: Above return RetType::Ok(true); } From d8306d7be6aa1625babc96a2febdabafaef6c5de Mon Sep 17 00:00:00 2001 From: Adam ? Date: Fri, 2 May 2025 21:50:50 -0700 Subject: [PATCH 047/134] Add shim capi file, ensure we build with visibility=hidden --- CMakeLists.txt | 2 +- src/capi.cpp | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 src/capi.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index af14554..cdf8bae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,7 +21,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Flamingo static build always exists add_library(flamingo-static ${SOURCE_DIR}/fixups.cpp ${SOURCE_DIR}/installer.cpp ${SOURCE_DIR}/page-allocator.cpp) -target_compile_options(flamingo-static PRIVATE -Wall -Wextra -Werror -Wpedantic -DFMT_HEADER_ONLY) +target_compile_options(flamingo-static PRIVATE -Wall -Wextra -Werror -Wpedantic -DFMT_HEADER_ONLY -fvisibility=hidden) option(TEST_BUILD "Enable local executable test builds for sanity checks" ON) diff --git a/src/capi.cpp b/src/capi.cpp new file mode 100644 index 0000000..138fcda --- /dev/null +++ b/src/capi.cpp @@ -0,0 +1,3 @@ +// TODO: Stub file for now. Ideally, we want to implement all of the CAPI functions here. +#include "capi.h" + From 368208ed780ebf8485c99412da31dd375772a807 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Fri, 2 May 2025 22:45:49 -0700 Subject: [PATCH 048/134] Add android install, fixup cmakelists a bit Currently no actual logic is done in flamingo, though, so probably want to add some logic to at least have it do some test hooks Also need to add this logic to CI Tests are still getting built that run locally, fortunately --- CMakeLists.txt | 89 ++++++++++++++++++++++++++++++++++++++---- build.ps1 | 26 +----------- include/git_info.h | 5 +++ qpm.json | 30 +++++++++----- qpm.shared.json | 53 +++++++++++++++++++------ shared/hook-data.hpp | 2 +- shared/util.hpp | 5 +-- src/flamingo-stamp.cpp | 2 + 8 files changed, 155 insertions(+), 57 deletions(-) create mode 100644 include/git_info.h create mode 100644 src/flamingo-stamp.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index cdf8bae..82aa760 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,14 +14,47 @@ if (NOT DEFINED CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "Debug") endif() -add_compile_options(-fno-rtti -fPIE -fPIC -fno-exceptions -fcolor-diagnostics) +add_compile_options(-fno-rtti -fPIE -fPIC -fno-exceptions -fcolor-diagnostics -DFMT_HEADER_ONLY) add_link_options(-lstdc++ -lm) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -# Flamingo static build always exists -add_library(flamingo-static ${SOURCE_DIR}/fixups.cpp ${SOURCE_DIR}/installer.cpp ${SOURCE_DIR}/page-allocator.cpp) -target_compile_options(flamingo-static PRIVATE -Wall -Wextra -Werror -Wpedantic -DFMT_HEADER_ONLY -fvisibility=hidden) +# get git info +execute_process(COMMAND git config user.name OUTPUT_VARIABLE GIT_USER) +execute_process(COMMAND git branch --show-current OUTPUT_VARIABLE GIT_BRANCH) +execute_process(COMMAND git rev-parse --short HEAD OUTPUT_VARIABLE GIT_COMMIT) +execute_process(COMMAND git diff-index --quiet HEAD RESULT_VARIABLE GIT_MODIFIED) + +string(STRIP "${GIT_USER}" GIT_USER) +string(STRIP "${GIT_BRANCH}" GIT_BRANCH) +string(STRIP "${GIT_COMMIT}" GIT_COMMIT) +string(STRIP "${GIT_MODIFIED}" GIT_MODIFIED) + +message(STATUS "GIT_USER: ${GIT_USER}") +message(STATUS "GIT_BRANCH: ${GIT_BRANCH}") +message(STATUS "GIT_COMMIT: 0x${GIT_COMMIT}") +message(STATUS "GIT_MODIFIED: ${GIT_MODIFIED}") + +# Check for file presence and read current contents +set(GIT_INFO_H_PATH "${CMAKE_CURRENT_SOURCE_DIR}/include/git_info.h") +if(EXISTS "${GIT_INFO_H_PATH}") + file(READ "${GIT_INFO_H_PATH}" GIT_INFO_H_CURRENT) +else() + set(GIT_INFO_H_CURRENT "") +endif() + +# Define new git info content +set(GIT_INFO_H "#pragma once +#define GIT_USER \"${GIT_USER}\" +#define GIT_BRANCH \"${GIT_BRANCH}\" +#define GIT_COMMIT 0x${GIT_COMMIT} +#define GIT_MODIFIED ${GIT_MODIFIED} +") + +# Write git info to file if the contents have changed +if(NOT "${GIT_INFO_H}" STREQUAL "${GIT_INFO_H_CURRENT}") + file(WRITE "${GIT_INFO_H_PATH}" "${GIT_INFO_H}") +endif() option(TEST_BUILD "Enable local executable test builds for sanity checks" ON) @@ -29,7 +62,13 @@ option(TEST_BUILD "Enable local executable test builds for sanity checks" ON) if (TEST_BUILD) project(flamingo-test) - # Fetch capstone (v5.0.1) via: pip install capstone==5.0.1 -U --user + # Static library of flamingo to link against for tests. + # TODO: We may want to test a dynamic version at some point too + add_library(flamingo-static ${SOURCE_DIR}/fixups.cpp ${SOURCE_DIR}/installer.cpp ${SOURCE_DIR}/page-allocator.cpp ${SOURCE_DIR}/capi.cpp) + target_compile_options(flamingo-static PRIVATE -Wall -Wextra -Werror -Wpedantic -fvisibility=hidden) + target_include_directories(flamingo-static PUBLIC ${SHARED_DIR}) + + # Fetch capstone (v5.0.1) include(FetchContent) FetchContent_Declare( capstone @@ -42,7 +81,7 @@ if (TEST_BUILD) FetchContent_MakeAvailable(capstone) target_link_libraries(flamingo-static PUBLIC capstone) - target_include_directories(flamingo-static PUBLIC ${SHARED_DIR} ${capstone_SOURCE_DIR}/include ${EXTERN_DIR}/includes/fmt/fmt/include) + target_include_directories(flamingo-static PUBLIC ${capstone_SOURCE_DIR}/include ${EXTERN_DIR}/includes/fmt/fmt/include) add_compile_definitions(TEST_BUILD) MESSAGE(STATUS "Compiling test build") @@ -57,5 +96,41 @@ if (TEST_BUILD) add_test(fixups fixup-test) add_test(apis api-test) else() - MESSAGE(ERROR "No NDK support yet! Try using -DTEST_BUILD=ON") + include(qpm_defines.cmake) + project(${COMPILE_ID}) + + add_library(${COMPILE_ID} SHARED) + + target_link_libraries(${COMPILE_ID} PRIVATE capstone -llog) + target_include_directories(${COMPILE_ID} PRIVATE ${EXTERN_DIR}/includes ${EXTERN_DIR}/includes/capstone/shared ${EXTERN_DIR}/includes/fmt/fmt/include) + # add shared dir as include dir + target_include_directories(${COMPILE_ID} PUBLIC ${SHARED_DIR}) + target_include_directories(${COMPILE_ID} PRIVATE ${SOURCE_DIR} ${INCLUDE_DIR}) + + # TODO: Until paper lets us build without exceptions, we don't use paper. + target_link_options(${COMPILE_ID} PRIVATE -Wl,--exclude-libs,ALL) + target_compile_options(${COMPILE_ID} PRIVATE -DFLAMINGO_HEADER_ONLY -fvisibility=hidden) + + target_sources(${COMPILE_ID} PUBLIC ${SOURCE_DIR}/capi.cpp ${SOURCE_DIR}/flamingo-stamp.cpp ${SOURCE_DIR}/fixups.cpp ${SOURCE_DIR}/installer.cpp ${SOURCE_DIR}/page-allocator.cpp) + + include(extern.cmake) + + add_custom_command(TARGET ${COMPILE_ID} POST_BUILD + COMMAND ${CMAKE_STRIP} -S -d --strip-all + "lib${COMPILE_ID}.so" -o "stripped_lib${COMPILE_ID}.so" + COMMENT "Strip debug symbols done on final binary.") + + add_custom_command(TARGET ${COMPILE_ID} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory debug + COMMENT "Create the debug dir" + ) + add_custom_command(TARGET ${COMPILE_ID} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E rename lib${COMPILE_ID}.so debug/lib${COMPILE_ID}.so + COMMENT "Rename the lib to debug_ since it has debug symbols" + ) + + add_custom_command(TARGET ${COMPILE_ID} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E rename stripped_lib${COMPILE_ID}.so lib${COMPILE_ID}.so + COMMENT "Rename the stripped lib to regular" + ) endif() diff --git a/build.ps1 b/build.ps1 index 4559903..f57003a 100644 --- a/build.ps1 +++ b/build.ps1 @@ -12,36 +12,14 @@ function Clean-Build-Folder { $NDKPath = Get-Content $PSScriptRoot/ndkpath.txt # Clean-Build-Folder -# build tests - -& cmake -G "Ninja" -DCMAKE_BUILD_TYPE="RelWithDebInfo" -DTEST_BUILD=1 -B build +# Build flamingo (not as tests) as tests are built via different cmake line (-DTEST_BUILD=1, or unspecified) +& cmake -G "Ninja" -DCMAKE_BUILD_TYPE="RelWithDebInfo" -DTEST_BUILD=0 -B build & cmake --build ./build $ExitCode = $LastExitCode - if (-not ($ExitCode -eq 0)) { $msg = "ExitCode: " + $ExitCode Write-Output $msg exit $ExitCode } - -# # clean folder -# Clean-Build-Folder -# # build mod - -# & cmake -G "Ninja" -DCMAKE_BUILD_TYPE="RelWithDebInfo" -B build -# & cmake --build ./build - -# $ExitCode = $LastExitCode - -# Post build, we actually want to transform the compile_commands.json file such that it has no \\ characters and ONLY has / characters -# (Get-Content -Path build/compile_commands.json) | -# ForEach-Object {$_ -Replace '\\\\', '/'} | Set-Content -Path build/compile_commands.json - -# To build tests, we just compile with our local clang++ into an executable -# Kind of wacky but will work on linux -# Requires libcapstone-dev installed -# sudo apt install -# clang++ test/main.cpp src/trampoline.cpp src/trampoline-allocator.cpp -o build/test -std=c++20 -Ishared -Iextern/includes -lcapstone -Iextern/includes/fmt/fmt/include -L/usr/lib/x86_64-linux-gnu -DFMT_HEADER_ONLY -Wall -Wextra -Werror -g -exit $ExitCode \ No newline at end of file diff --git a/include/git_info.h b/include/git_info.h new file mode 100644 index 0000000..fc9410a --- /dev/null +++ b/include/git_info.h @@ -0,0 +1,5 @@ +#pragma once +#define GIT_USER "Adam ?" +#define GIT_BRANCH "dev/sc2ad/v1.0.0-milestone" +#define GIT_COMMIT 0x2d2a000 +#define GIT_MODIFIED 1 diff --git a/qpm.json b/qpm.json index a9f5881..9875a04 100644 --- a/qpm.json +++ b/qpm.json @@ -1,15 +1,27 @@ { + "$schema": "https://raw.githubusercontent.com/QuestPackageManager/QPM.Package/refs/heads/main/qpm.schema.json", + "version": "0.4.0", "sharedDir": "shared", "dependenciesDir": "extern", "info": { "name": "flamingo", "id": "flamingo", - "version": "0.1.0", + "version": "0.2.0", "url": "https://github.com/sc2ad/Flamingo", "additionalData": { "overrideSoName": "libflamingo.so" } }, + "workspace": { + "scripts": { + "build": [ + "pwsh ./build.ps1" + ] + }, + "qmodIncludeDirs": [], + "qmodIncludeFiles": [], + "qmodOutput": null + }, "dependencies": [ { "id": "capstone", @@ -20,17 +32,15 @@ }, { "id": "fmt", - "versionRange": "^10.0.0", + "versionRange": "^11.0.0", "additionalData": { "private": true } + }, + { + "id": "paper2_scotland2", + "versionRange": "^4.6.1", + "additionalData": {} } - ], - "workspace": { - "scripts": { - "build": [ - "pwsh ./build.ps1" - ] - } - } + ] } \ No newline at end of file diff --git a/qpm.shared.json b/qpm.shared.json index 34f89ed..5d26ab6 100644 --- a/qpm.shared.json +++ b/qpm.shared.json @@ -1,5 +1,7 @@ { + "$schema": "https://raw.githubusercontent.com/QuestPackageManager/QPM.Package/refs/heads/main/qpm.shared.schema.json", "config": { + "version": "0.4.0", "sharedDir": "shared", "dependenciesDir": "extern", "info": { @@ -11,6 +13,16 @@ "overrideSoName": "libflamingo.so" } }, + "workspace": { + "scripts": { + "build": [ + "pwsh ./build.ps1" + ] + }, + "qmodIncludeDirs": [], + "qmodIncludeFiles": [], + "qmodOutput": null + }, "dependencies": [ { "id": "capstone", @@ -21,28 +33,45 @@ }, { "id": "fmt", - "versionRange": "^10.0.0", + "versionRange": "^11.0.0", "additionalData": { "private": true } + }, + { + "id": "paper2_scotland2", + "versionRange": "^4.6.1", + "additionalData": {} } - ], - "workspace": { - "scripts": { - "build": [ - "pwsh ./build.ps1" - ] - } - } + ] }, "restoredDependencies": [ + { + "dependency": { + "id": "paper2_scotland2", + "versionRange": "=4.6.1", + "additionalData": { + "soLink": "https://github.com/Fernthedev/paperlog/releases/download/v4.6.1/libpaper2_scotland2.so", + "overrideSoName": "libpaper2_scotland2.so", + "modLink": "https://github.com/Fernthedev/paperlog/releases/download/v4.6.1/paper2_scotland2.qmod", + "branchName": "version/v4_6_1", + "compileOptions": { + "systemIncludes": [ + "shared/utfcpp/source" + ] + }, + "cmake": false + } + }, + "version": "4.6.1" + }, { "dependency": { "id": "fmt", - "versionRange": "=10.0.0", + "versionRange": "=11.0.2", "additionalData": { "headersOnly": true, - "branchName": "version/v10_0_0", + "branchName": "version/v11_0_2", "compileOptions": { "systemIncludes": [ "fmt/include/" @@ -53,7 +82,7 @@ } } }, - "version": "10.0.0" + "version": "11.0.2" }, { "dependency": { diff --git a/shared/hook-data.hpp b/shared/hook-data.hpp index 09e45e3..a8b5504 100644 --- a/shared/hook-data.hpp +++ b/shared/hook-data.hpp @@ -27,7 +27,7 @@ struct HookInfo { HookInfo(HookFuncType hook_func, void* target, HookFuncType* orig_ptr) : HookInfo(hook_func, target, orig_ptr, kDefaultNumInsts, CallingConvention::Cdecl, HookNameMetadata{ .name = "" }, HookPriority{}, - InstallationMetadata{ .need_orig = orig_ptr != nullptr }) {} + InstallationMetadata{ .need_orig = orig_ptr != nullptr, .is_midpoint = false, .write_prot = false }) {} // Helper function to make it really easy to set installation metadata template diff --git a/shared/util.hpp b/shared/util.hpp index 109ff88..824bcbe 100644 --- a/shared/util.hpp +++ b/shared/util.hpp @@ -44,8 +44,7 @@ #include #ifdef ANDROID -#include "beatsaber-hook/shared/utils/utils-functions.h" -#include "paper/shared/logger.hpp" +#include "paper2_scotland2/shared/logger.hpp" #ifndef NDEBUG #define FLAMINGO_ASSERT(...) assert(__VA_ARGS__) @@ -60,7 +59,7 @@ do { \ FLAMINGO_CRITICAL(__VA_ARGS__); \ Paper::Logger::WaitForFlush(); \ - SAFE_ABORT(); \ + std::abort(); \ } while (0) #else diff --git a/src/flamingo-stamp.cpp b/src/flamingo-stamp.cpp new file mode 100644 index 0000000..05b495e --- /dev/null +++ b/src/flamingo-stamp.cpp @@ -0,0 +1,2 @@ +// Stamp file used when building for Android (to ensure we pull in the static library correctly) +#include "git_info.h" From 7fe1eeda5a969ed299aa0b882fce8196fbba3003 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Fri, 2 May 2025 22:50:34 -0700 Subject: [PATCH 049/134] Remove PR run events, only do them for push events May revist in the future, but for now this should save some duplicate work --- .github/workflows/build-ndk.yml | 6 +----- mod.json | 9 ++++++--- qpm.json | 5 ----- qpm.shared.json | 26 +------------------------- 4 files changed, 8 insertions(+), 38 deletions(-) diff --git a/.github/workflows/build-ndk.yml b/.github/workflows/build-ndk.yml index 757e05b..45911bd 100644 --- a/.github/workflows/build-ndk.yml +++ b/.github/workflows/build-ndk.yml @@ -5,9 +5,6 @@ on: push: branches-ignore: - "version-*" - pull_request: - branches-ignore: - - "version-*" env: module_id: flamingo @@ -95,7 +92,6 @@ jobs: - name: Create Qmod run: | - qpm-rust qmod build pwsh -Command ./createqmod.ps1 ${{env.qmodName}} - name: Get Library Name @@ -117,7 +113,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: debug_${{ steps.libname.outputs.NAME }} - path: ./build/debug_${{ steps.libname.outputs.NAME }} + path: ./build/debug/${{ steps.libname.outputs.NAME }} if-no-files-found: error - name: Upload qmod artifact diff --git a/mod.json b/mod.json index fe2b7b3..bec3e84 100644 --- a/mod.json +++ b/mod.json @@ -1,15 +1,18 @@ { + "$schema": "https://raw.githubusercontent.com/Lauriethefish/QuestPatcher.QMod/refs/heads/main/QuestPatcher.QMod/Resources/qmod.schema.json", "_QPVersion": "1.0.0", "name": "flamingo", "id": "flamingo", + "modloader": "Scotland2", "author": "Sc2ad", - "version": "0.1.0", + "version": "0.2.0", "description": "General purpose Android hook library", "dependencies": [], - "modFiles": [ + "modFiles": [], + "lateModFiles": [], + "libraryFiles": [ "libflamingo.so" ], - "libraryFiles": [], "fileCopies": [], "copyExtensions": [] } \ No newline at end of file diff --git a/qpm.json b/qpm.json index 9875a04..a00f871 100644 --- a/qpm.json +++ b/qpm.json @@ -36,11 +36,6 @@ "additionalData": { "private": true } - }, - { - "id": "paper2_scotland2", - "versionRange": "^4.6.1", - "additionalData": {} } ] } \ No newline at end of file diff --git a/qpm.shared.json b/qpm.shared.json index 5d26ab6..b5d8128 100644 --- a/qpm.shared.json +++ b/qpm.shared.json @@ -7,7 +7,7 @@ "info": { "name": "flamingo", "id": "flamingo", - "version": "0.1.0", + "version": "0.2.0", "url": "https://github.com/sc2ad/Flamingo", "additionalData": { "overrideSoName": "libflamingo.so" @@ -37,34 +37,10 @@ "additionalData": { "private": true } - }, - { - "id": "paper2_scotland2", - "versionRange": "^4.6.1", - "additionalData": {} } ] }, "restoredDependencies": [ - { - "dependency": { - "id": "paper2_scotland2", - "versionRange": "=4.6.1", - "additionalData": { - "soLink": "https://github.com/Fernthedev/paperlog/releases/download/v4.6.1/libpaper2_scotland2.so", - "overrideSoName": "libpaper2_scotland2.so", - "modLink": "https://github.com/Fernthedev/paperlog/releases/download/v4.6.1/paper2_scotland2.qmod", - "branchName": "version/v4_6_1", - "compileOptions": { - "systemIncludes": [ - "shared/utfcpp/source" - ] - }, - "cmake": false - } - }, - "version": "4.6.1" - }, { "dependency": { "id": "fmt", From 1115579c3f532213b5c4c4012a87eca808228310 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Fri, 2 May 2025 23:34:38 -0700 Subject: [PATCH 050/134] Add android test code if dropped into mods Currently always applies, but eventually it will be ifdef'd out Also add an installation result formatter --- include/git_info.h | 2 +- shared/hook-installation-result.hpp | 27 ++++++++++++++ shared/util.hpp | 13 ++++++- src/flamingo-stamp.cpp | 57 +++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 2 deletions(-) diff --git a/include/git_info.h b/include/git_info.h index fc9410a..db03231 100644 --- a/include/git_info.h +++ b/include/git_info.h @@ -1,5 +1,5 @@ #pragma once #define GIT_USER "Adam ?" #define GIT_BRANCH "dev/sc2ad/v1.0.0-milestone" -#define GIT_COMMIT 0x2d2a000 +#define GIT_COMMIT 0x9873624 #define GIT_MODIFIED 1 diff --git a/shared/hook-installation-result.hpp b/shared/hook-installation-result.hpp index de287c3..fce4563 100644 --- a/shared/hook-installation-result.hpp +++ b/shared/hook-installation-result.hpp @@ -8,8 +8,10 @@ #include "calling-convention.hpp" #include "hook-metadata.hpp" +#include "page-allocator.hpp" #include "target-data.hpp" #include "type-info.hpp" +#include "util.hpp" namespace flamingo { @@ -139,3 +141,28 @@ using Result = flamingo::Result; } // namespace installation } // namespace flamingo + +// Custom formatter for flamingo::Error +template <> class fmt::formatter { +public: + constexpr auto parse (format_parse_context& ctx) { return ctx.begin(); } + template + constexpr auto format (flamingo::installation::Error const& error, Context& ctx) const { + using namespace flamingo::installation; + return std::visit(flamingo::util::overload { + [&](TargetIsNull const& null_target) { + return format_to(ctx.out(), "Null target, for hook with name: {}", null_target.installing_hook.name); + }, + [&](TargetBadPriorities const& bad_priorities) { + return format_to(ctx.out(), "Bad priorities, for hook with name: {}", bad_priorities.installing_hook.name); + }, + [&](TargetMismatch const& mismatch) { + // TODO: Actually print this out + return format_to(ctx.out(), "Target mismatch type: {}", mismatch.index()); + }, + [&](TargetTooSmall const& small_target) { + return format_to(ctx.out(), "Target too small, needed: {} instructions, but have: {} instructions for hook with name: {}", small_target.needed_num_insts, small_target.actual_num_insts, small_target.installing_hook.name); + } + }, error); + } +}; diff --git a/shared/util.hpp b/shared/util.hpp index 824bcbe..da49fb5 100644 --- a/shared/util.hpp +++ b/shared/util.hpp @@ -79,4 +79,15 @@ std::abort() #endif -#endif \ No newline at end of file +#endif + +namespace flamingo::util { + +template +struct overload : Ts... { + using Ts::operator()...; +}; +template +overload(Ts...) -> overload; + +} // namespace flamingo::util \ No newline at end of file diff --git a/src/flamingo-stamp.cpp b/src/flamingo-stamp.cpp index 05b495e..38823df 100644 --- a/src/flamingo-stamp.cpp +++ b/src/flamingo-stamp.cpp @@ -1,2 +1,59 @@ // Stamp file used when building for Android (to ensure we pull in the static library correctly) +#include +#include +#include #include "git_info.h" +#include "hook-data.hpp" +#include "target-data.hpp" +#include "util.hpp" +#include "installer.hpp" + +extern void* modloader_libil2cpp_handle; + +void* (*orig_runtime_invoke)(void*, void*, void*, void*); + +void* wrap_runtime_invoke(void* method_info, void* obj, void* p, void* e) { + FLAMINGO_DEBUG("Runtime Invoke: {}, {}, {}", method_info, obj, p); + return orig_runtime_invoke(method_info, obj, p, e); +} + +static void print_decode_loop(void* ptr, size_t size) { + auto handle = flamingo::getHandle(); + uint32_t* data = (uint32_t*)ptr; + for (size_t i = 0; i < size; i++) { + cs_insn* insns = nullptr; + auto count = cs_disasm(handle, reinterpret_cast(&data[i]), sizeof(uint32_t), + reinterpret_cast(&data[i]), 1, &insns); + if (count == 1) { + FLAMINGO_DEBUG("Addr: {} Value: 0x{:08x}, {} {}", fmt::ptr(&data[i]), data[i], insns[0].mnemonic, insns[0].op_str); + } else { + FLAMINGO_DEBUG("Addr: {} Value: 0x{:08x}", fmt::ptr(&data[i]), data[i]); + } + } +} + +extern "C" void late_load() { + FLAMINGO_DEBUG("GIT COMMIT: 0x{:08x}", GIT_COMMIT); + void* (*runtime_invoke)(void*, void*, void*, void*); + runtime_invoke = (decltype(runtime_invoke))dlsym(modloader_libil2cpp_handle, "il2cpp_runtime_invoke"); + FLAMINGO_DEBUG("Found runtime_invoke: {}", fmt::ptr(runtime_invoke)); + print_decode_loop((void*)runtime_invoke, 10); + // Install the hook to it + auto result = flamingo::Install(flamingo::HookInfo(&wrap_runtime_invoke, (void*)runtime_invoke, &orig_runtime_invoke)); + if (!result.has_value()) { + FLAMINGO_ABORT("Hook installation error! Error is of type: {}", result.error()); + } + // After hook install, log the hook + FLAMINGO_DEBUG("runtime_invoke again: {}", fmt::ptr(runtime_invoke)); + FLAMINGO_DEBUG("Target hook addr: {}", fmt::ptr(&wrap_runtime_invoke)); + FLAMINGO_DEBUG("Orig callback (should match runtime_invoke): {}", fmt::ptr(orig_runtime_invoke)); + print_decode_loop((void*)runtime_invoke, 10); + // Ask for the fixups location and dump that + auto fixups = flamingo::FixupPointerFor(flamingo::TargetDescriptor {.target = (void*)runtime_invoke }); + if (!result.has_value()) { + FLAMINGO_ABORT("Fixup lookup failed for target: {}", (void*)runtime_invoke); + } + FLAMINGO_DEBUG("Fixups pointer: {}", fmt::ptr(fixups.value().data())); + print_decode_loop((void*)fixups.value().data(), fixups.value().size()); + FLAMINGO_DEBUG("ALL SET!"); +} \ No newline at end of file From 277ea3b8722c6029b1be20923b73ea2a27059880 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Wed, 7 May 2025 20:06:42 -0700 Subject: [PATCH 051/134] Ensure correct partial mod passes tests and builds --- CMakeLists.txt | 6 +++--- include/git_info.h | 2 +- qpm.json | 5 +++++ qpm.shared.json | 18 ++++++++++++++++++ shared/util.hpp | 2 +- src/flamingo-stamp.cpp | 4 ++-- 6 files changed, 30 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 82aa760..4a687de 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,7 +15,7 @@ if (NOT DEFINED CMAKE_BUILD_TYPE) endif() add_compile_options(-fno-rtti -fPIE -fPIC -fno-exceptions -fcolor-diagnostics -DFMT_HEADER_ONLY) -add_link_options(-lstdc++ -lm) +add_link_options(-lm) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) @@ -80,7 +80,7 @@ if (TEST_BUILD) set(CAPSTONE_ARM64_SUPPORT ON) FetchContent_MakeAvailable(capstone) - target_link_libraries(flamingo-static PUBLIC capstone) + target_link_libraries(flamingo-static PUBLIC capstone -lstdc++) target_include_directories(flamingo-static PUBLIC ${capstone_SOURCE_DIR}/include ${EXTERN_DIR}/includes/fmt/fmt/include) add_compile_definitions(TEST_BUILD) @@ -109,7 +109,7 @@ else() # TODO: Until paper lets us build without exceptions, we don't use paper. target_link_options(${COMPILE_ID} PRIVATE -Wl,--exclude-libs,ALL) - target_compile_options(${COMPILE_ID} PRIVATE -DFLAMINGO_HEADER_ONLY -fvisibility=hidden) + target_compile_options(${COMPILE_ID} PRIVATE -DFLAMINGO_HEADER_ONLY -fvisibility=hidden -Wall -Wextra -Werror -Wpedantic) target_sources(${COMPILE_ID} PUBLIC ${SOURCE_DIR}/capi.cpp ${SOURCE_DIR}/flamingo-stamp.cpp ${SOURCE_DIR}/fixups.cpp ${SOURCE_DIR}/installer.cpp ${SOURCE_DIR}/page-allocator.cpp) diff --git a/include/git_info.h b/include/git_info.h index db03231..c5d6384 100644 --- a/include/git_info.h +++ b/include/git_info.h @@ -1,5 +1,5 @@ #pragma once #define GIT_USER "Adam ?" #define GIT_BRANCH "dev/sc2ad/v1.0.0-milestone" -#define GIT_COMMIT 0x9873624 +#define GIT_COMMIT 0xbf77603 #define GIT_MODIFIED 1 diff --git a/qpm.json b/qpm.json index a00f871..66770b7 100644 --- a/qpm.json +++ b/qpm.json @@ -36,6 +36,11 @@ "additionalData": { "private": true } + }, + { + "id": "scotland2", + "versionRange": "^0.1.6", + "additionalData": {} } ] } \ No newline at end of file diff --git a/qpm.shared.json b/qpm.shared.json index b5d8128..05418cd 100644 --- a/qpm.shared.json +++ b/qpm.shared.json @@ -37,10 +37,28 @@ "additionalData": { "private": true } + }, + { + "id": "scotland2", + "versionRange": "^0.1.6", + "additionalData": {} } ] }, "restoredDependencies": [ + { + "dependency": { + "id": "scotland2", + "versionRange": "=0.1.6", + "additionalData": { + "soLink": "https://github.com/sc2ad/scotland2/releases/download/v0.1.6/libsl2.so", + "debugSoLink": "https://github.com/sc2ad/scotland2/releases/download/v0.1.6/debug_libsl2.so", + "overrideSoName": "libsl2.so", + "branchName": "version/v0_1_6" + } + }, + "version": "0.1.6" + }, { "dependency": { "id": "fmt", diff --git a/shared/util.hpp b/shared/util.hpp index da49fb5..5a0a188 100644 --- a/shared/util.hpp +++ b/shared/util.hpp @@ -1,7 +1,7 @@ #pragma once #define FLAMINGO_ID "flamingo" -#define FLAMINGO_VERSION "0.1.0" +#define FLAMINGO_VERSION "0.2.0" #define FLAMINGO_EXPORT __attribute__((visibility("default"))) diff --git a/src/flamingo-stamp.cpp b/src/flamingo-stamp.cpp index 38823df..1b0d3d0 100644 --- a/src/flamingo-stamp.cpp +++ b/src/flamingo-stamp.cpp @@ -32,7 +32,7 @@ static void print_decode_loop(void* ptr, size_t size) { } } -extern "C" void late_load() { +FLAMINGO_EXPORT extern "C" void late_load() { FLAMINGO_DEBUG("GIT COMMIT: 0x{:08x}", GIT_COMMIT); void* (*runtime_invoke)(void*, void*, void*, void*); runtime_invoke = (decltype(runtime_invoke))dlsym(modloader_libil2cpp_handle, "il2cpp_runtime_invoke"); @@ -56,4 +56,4 @@ extern "C" void late_load() { FLAMINGO_DEBUG("Fixups pointer: {}", fmt::ptr(fixups.value().data())); print_decode_loop((void*)fixups.value().data(), fixups.value().size()); FLAMINGO_DEBUG("ALL SET!"); -} \ No newline at end of file +} From a2ce927940bf348dbfb534a1581087199db14612 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Wed, 7 May 2025 23:19:01 -0700 Subject: [PATCH 052/134] Add custom formatters for various hook info --- shared/calling-convention.hpp | 24 ++++++++ shared/hook-installation-result.hpp | 91 ++++++++++++++++++++++------- shared/hook-metadata.hpp | 17 +++++- shared/type-info.hpp | 17 +++++- 4 files changed, 127 insertions(+), 22 deletions(-) diff --git a/shared/calling-convention.hpp b/shared/calling-convention.hpp index 9d114f9..b2aa564 100644 --- a/shared/calling-convention.hpp +++ b/shared/calling-convention.hpp @@ -1,7 +1,31 @@ #pragma once +#include +#include + namespace flamingo { /// @brief Represents the calling convention for a given hook. /// Used primarily for type checking enum struct CallingConvention { Cdecl, Fastcall, Thiscall }; } // namespace flamingo + +// Custom formatter for flamingo::CallingConvention +// TODO: Maybe consider pulling in magic_enum? +template <> +class fmt::formatter { + public: + constexpr auto parse(format_parse_context& ctx) { + return ctx.begin(); + } + template + constexpr auto format(flamingo::CallingConvention const& conv, Context& ctx) const { + switch (conv) { + case flamingo::CallingConvention::Cdecl: + return format_to(ctx.out(), "Cdecl"); + case flamingo::CallingConvention::Fastcall: + return format_to(ctx.out(), "Fastcall"); + case flamingo::CallingConvention::Thiscall: + return format_to(ctx.out(), "Thiscall"); + } + } +}; diff --git a/shared/hook-installation-result.hpp b/shared/hook-installation-result.hpp index fce4563..fba0dd7 100644 --- a/shared/hook-installation-result.hpp +++ b/shared/hook-installation-result.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include #include #include @@ -127,7 +128,8 @@ struct MismatchMidpoint : HookErrorInfo { #ifndef FLAMINGO_NO_REGISTRATION_CHECKS /// @brief An error when the target method has some validation failure with respect to the metadata it holds. -using TargetMismatch = std::variant; +using TargetMismatch = + std::variant; #else /// @brief An error when the target method has some validation failure with respect to the metadata it holds. using TargetMismatch = std::variant; @@ -143,26 +145,75 @@ using Result = flamingo::Result; } // namespace flamingo // Custom formatter for flamingo::Error -template <> class fmt::formatter { -public: - constexpr auto parse (format_parse_context& ctx) { return ctx.begin(); } +template <> +class fmt::formatter { + public: + constexpr auto parse(format_parse_context& ctx) { + return ctx.begin(); + } template - constexpr auto format (flamingo::installation::Error const& error, Context& ctx) const { + constexpr auto format(flamingo::installation::Error const& error, Context& ctx) const { using namespace flamingo::installation; - return std::visit(flamingo::util::overload { - [&](TargetIsNull const& null_target) { - return format_to(ctx.out(), "Null target, for hook with name: {}", null_target.installing_hook.name); - }, - [&](TargetBadPriorities const& bad_priorities) { - return format_to(ctx.out(), "Bad priorities, for hook with name: {}", bad_priorities.installing_hook.name); - }, - [&](TargetMismatch const& mismatch) { - // TODO: Actually print this out - return format_to(ctx.out(), "Target mismatch type: {}", mismatch.index()); - }, - [&](TargetTooSmall const& small_target) { - return format_to(ctx.out(), "Target too small, needed: {} instructions, but have: {} instructions for hook with name: {}", small_target.needed_num_insts, small_target.actual_num_insts, small_target.installing_hook.name); - } - }, error); + return std::visit( + flamingo::util::overload{ + [&](TargetIsNull const& null_target) { + return format_to(ctx.out(), FMT_COMPILE("Null target, for hook: {}"), null_target.installing_hook); + }, + [&](TargetBadPriorities const& bad_priorities) { + return format_to(ctx.out(), FMT_COMPILE("Bad priorities, for hook: {}"), bad_priorities.installing_hook); + }, + [&](TargetMismatch const& mismatch) { + return format_to(ctx.out(), FMT_COMPILE("Target mismatch: {}"), mismatch); + }, + [&](TargetTooSmall const& small_target) { + return format_to( + ctx.out(), + FMT_COMPILE("Target too small, needed: {} instructions, but have: {} instructions for hook: {}"), + small_target.needed_num_insts, small_target.actual_num_insts, small_target.installing_hook); + } }, + error); + } +}; + +// Custom formatter for flamingo::installation::TargetMismatch +template <> +class fmt::formatter { + public: + constexpr auto parse(format_parse_context& ctx) { + return ctx.begin(); + } + template + constexpr auto format(flamingo::installation::TargetMismatch const& mismatch, Context& ctx) const { + using namespace flamingo::installation; + return std::visit( + flamingo::util::overload{ + [&](MismatchTargetConv const& mismatch_conv) { + return format_to(ctx.out(), FMT_COMPILE("Target has calling convention: {} but specified: {} for hook: {}"), + mismatch_conv.existing, mismatch_conv.incoming, mismatch_conv.installing_hook); + }, + [&](MismatchMidpoint const& mismatch_midpoint) { + return format_to(ctx.out(), + FMT_COMPILE("Target has midpoint specified as: {} but specified: {} for hook: {}"), + mismatch_midpoint.existing, mismatch_midpoint.incoming, mismatch_midpoint.installing_hook); + }, +#ifndef FLAMINGO_NO_REGISTRATION_CHECKS + [&](MismatchReturn const& mismatch_return) { + return format_to(ctx.out(), + FMT_COMPILE("Target has return type specified as: {} but specified: {} for hook: {}"), + mismatch_return.existing, mismatch_return.incoming, mismatch_return.installing_hook); + }, + [&](MismatchParam const& mismatch_param) { + return format_to( + ctx.out(), FMT_COMPILE("Target has parameter {} type specified as: {} but specified: {} for hook: {}"), + mismatch_param.idx, mismatch_param.existing, mismatch_param.incoming, mismatch_param.installing_hook); + }, + [&](MismatchParamCount const& mismatch_param_count) { + return format_to(ctx.out(), FMT_COMPILE("Target has {} parameters but specified: {} for hook: {}"), + mismatch_param_count.existing, mismatch_param_count.incoming, + mismatch_param_count.installing_hook); + }, +#endif + }, + mismatch); } }; diff --git a/shared/hook-metadata.hpp b/shared/hook-metadata.hpp index 4bae430..a9ef86d 100644 --- a/shared/hook-metadata.hpp +++ b/shared/hook-metadata.hpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include "calling-convention.hpp" #include "type-info.hpp" @@ -45,4 +47,17 @@ struct HookMetadata { #endif }; -} // namespace flamingo \ No newline at end of file +} // namespace flamingo + +// Custom formatter for flamingo::CallingConvention +template <> +class fmt::formatter { + public: + constexpr auto parse(format_parse_context& ctx) { + return ctx.begin(); + } + template + constexpr auto format(flamingo::HookNameMetadata const& metadata, Context& ctx) const { + return format_to(ctx.out(), FMT_COMPILE("name: {}"), metadata.name); + } +}; diff --git a/shared/type-info.hpp b/shared/type-info.hpp index 931492b..c7525f8 100644 --- a/shared/type-info.hpp +++ b/shared/type-info.hpp @@ -2,12 +2,14 @@ #include #include +#include +#include namespace flamingo { /// @brief Represents the type info for representing a type in a hook struct TypeInfo { friend struct HookInfo; - // TODO: Add more members here. Ideally some form of relaxed type checking? + // TODO: Add more members here, like name. Ideally some form of relaxed type checking? std::size_t size{}; private: @@ -33,3 +35,16 @@ inline bool operator==(TypeInfo const& lhs, TypeInfo const& rhs) { return lhs.size == rhs.size; } } // namespace flamingo + +// Custom formatter for flamingo::TypeInfo +template <> +class fmt::formatter { + public: + constexpr auto parse(format_parse_context& ctx) { + return ctx.begin(); + } + template + constexpr auto format(flamingo::TypeInfo const& info, Context& ctx) const { + return format_to(ctx.out(), FMT_COMPILE("(size={})"), info.size); + } +}; From 04597e3e60814fe5b483b212635a688b826edd5e Mon Sep 17 00:00:00 2001 From: Adam ? Date: Wed, 7 May 2025 23:23:33 -0700 Subject: [PATCH 053/134] Add CAPI header, implementation to follow --- shared/capi.h | 216 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 206 insertions(+), 10 deletions(-) diff --git a/shared/capi.h b/shared/capi.h index f729c2c..2b82e49 100644 --- a/shared/capi.h +++ b/shared/capi.h @@ -1,32 +1,128 @@ #pragma once +#include #include -#define FLAMINGO_C_EXPORT __attribute__((visibility("default"))) +// Most flamingo API calls also require the result to be used in some way. +#define FLAMINGO_C_EXPORT __attribute__((visibility("default"))) __attribute__((warn_unused_result)) +#define FLAMINGO_C_EXPORT_VOID __attribute__((visibility("default"))) // The flamingo C API #ifdef __cplusplus extern "C" { #endif -// General C API: -// Create a hook (get back a handle) -// Uninstall/remove a hook (with the handle) -// Creation of the hook is actually a non-trivial process. -// We require lots of meta information, potentially. +/// @brief The installation result types +typedef enum { + FLAMINGO_INSTALL_OK, + FLAMINGO_INSTALL_TARGET_NULL, + FLAMINGO_INSTALL_BAD_PRIORITIES, + FLAMINGO_INSTALL_MISMATCH_CALLING_CONVENTION, + FLAMINGO_INSTALL_MISMATCH_MIDPOINT, + FLAMINGO_INSTALL_TOO_SMALL, +#ifndef FLAMINGO_NO_REGISTRATION_CHECKS + FLAMINGO_INSTALL_MISMATCH_RETURN, + FLAMINGO_INSTALL_MISMATCH_PARAM, + FLAMINGO_INSTALL_MISMATCH_PARAM_COUNT, +#endif +} FlamingoInstallationType; + +/// @brief Opaque pointer around a flamingo::HookHandle +typedef struct FlamingoHookHandle FlamingoHookHandle; + +/// @brief Opaque pointer around a flamingo::installation::Error +typedef struct FlamingoInstallErrorData FlamingoInstallErrorData; + +/// @brief Returned from an installation. +/// The handle of the union is legal only when result == FLAMINGO_INSTALL_OK, otherwise it holds an error type. +/// The error type can be formatted to a string using flamingo_format_error. Getting the actual failure info is +/// currently unsupported. The lifetime of the handle is until flamingo_uninstall_hook is called. The lifetime of the +/// error data is until flamingo_format_error is called. +typedef struct { + FlamingoInstallationType result; + union { + FlamingoHookHandle* handle; + FlamingoInstallErrorData* data; + } value; +} FlamingoInstallationResult; + +/// @brief Returned from a reinstallation. If success is false, the value of any_hooks_reinstalled is undefined. +/// If success is true, the value describes if any hooks were present at the specified target for a reinstall. +typedef struct { + bool success; + bool any_hooks_reinstalled; +} FlamingoReinstallResult; + +/// @brief Returned from an uninstall. If success is false, the value of any_hooks_remain is undefined. +/// If success is true, the value describes if any hooks remain after the uninstallation at this target. +typedef struct { + bool success; + bool any_hooks_remain; +} FlamingoUninstallResult; + +/// @brief The calling conventions of the target used for hook validation. +/// It is assumed that the hook function you provide has the same calling convention. +typedef enum { + FLAMINGO_CDECL, + FLAMINGO_FASTCALL, + FLAMINGO_THISCALL, +} FlamingoCallingConvention; + +/// @brief Opaque pointer around a flamingo::HookNameMetadata +typedef struct FlamingoNameInfo FlamingoNameInfo; + +/// @brief Opaque pointer around a flamingo::HookPriority +typedef struct FlamingoHookPriority FlamingoHookPriority; -// Check if a location is hooked -// Additionally return a window over the original data (to allow for xref traces) -// Reinstall all hooks at a target, or a range +/// @brief Opaque pointer around a flamingo::InstallationMetadata +typedef struct FlamingoInstallationMetadata FlamingoInstallationMetadata; + +/// @brief Opaque pointer around a flamingo::TypeInfo +typedef struct FlamingoTypeInfo FlamingoTypeInfo; /// @brief Returned from a call to query if a region is hooked, and what the original instructions at that location are. +/// Should not be stored for long-term use, since the lifetime of the result is tied to the lifetime of the hooks at +/// this location. typedef struct { /// @brief The size of the hook present at this address, in number of instructions. uint32_t hook_size; - /// @brief A pointer to the hook's original instructions. Safe to dereference up to 'hook_size'. + /// @brief A non-owning pointer to the hook's original instructions. + /// Safe to dereference up to 'hook_size' for as long as there is at least one hook at this location that is not + /// uninstalled, and no reinstall takes place. If no hook is present at this target, this pointer will exactly equal + /// the addr pointer and size will be 0. uint32_t const* original_instructions; } FlamingoOriginalInstructionsResult; +/// @brief Creates a flamingo::HookNameMetadata from the provided parameters. The return is an opaque pointer. +/// The returned pointer's lifetime is until a different flamingo API call is made that CONSUMES the FlamingoNameInfo*. +/// This is primarily used to give hooks names and to describe priorities for installation. +/// The lifetime of the result is until it is consumed by a call to flamingo_install_hook*, or flamingo_make_priority. +FLAMINGO_C_EXPORT FlamingoNameInfo* flamingo_make_name(char const* name_str); + +/// @brief Creates a flamingo::HookMetadata from the provided parameters. +/// The parameters are arrays of FlamingoNameInfo that must be dereferencable up to num_befores and num_afters +/// respectively. The parameters are CONSUMED, that is, the pointers are no longer valid after this API call. This is +/// used to give hooks priority information in flamingo_install_hook_full* +/// The lifetime of the result is until it is consumed by a call to flamingo_install_hook*. +FLAMINGO_C_EXPORT FlamingoHookPriority* flamingo_make_priority(FlamingoNameInfo** before_names, size_t num_befores, + FlamingoNameInfo** after_names, size_t num_afters); + +/// @brief Creates a flamingo::InstallationMetadata from the provided parameters. +/// This is used to describe metadata that should hint to flamingo to install correctly. +/// Note that these are HINTS and are not strictly required for flamingo to follow, though in practice it will. This +/// will be changed to strong guarantees in a future version of flamingo. +/// @param is_midpoint Whether this hook is in the middle of a function call instead of at the beginning. Note that this +/// will usually mean a different scratch register should be used, and that the branching logic may be incorrect. +/// @param write_prot Whether to also mark the page where the target is as writable. +/// The lifetime of the result is until it is consumed by a call to flamingo_install_hook*. +FLAMINGO_C_EXPORT FlamingoInstallationMetadata* flamingo_make_install_metadata(bool is_midpoint, bool write_prot); + +/// @brief Creates a flamingo::TypeInfo from the provided parameters. +/// This is used for type checking hook installs to ensure multiple installs over the same target agree upon the +/// parameters provided. For void types, it is expected to provide a type size of 0. +/// The lifetime of the result is until it is consumed by a call to flamingo_install_hook*. +FLAMINGO_C_EXPORT FlamingoTypeInfo* flamingo_make_type_info(char const* name, size_t size); + /// @brief Returns a FlamingoOriginalInstructionsResult. /// The hook_size of the returned instance will be 0 if the provided address is not the START of an installed hook. /// The returned original_instructions pointer is safe to read in the range [0..hook_size). @@ -34,7 +130,107 @@ typedef struct { /// instructions. FLAMINGO_C_EXPORT FlamingoOriginalInstructionsResult flamingo_orig_for(uint32_t const* addr); +/// @brief Install a hook at the provided target to call the provided hook function, with an optionally non-null +/// orig_pointer that will be assigned to the fixups region, and a name. Returns the installation result to be used in +/// uninstalls or for errors. +/// @param hook_function The function to call from the hook. If this is null, will branch to 0. +/// @param target The target to install the hook to. +/// @param orig_pointer A pointer to a function to populate after the install with fixups to call for a trampoline. If +/// null, no fixups will be generated. +/// @param convention The calling convention of the target function. The hook_function must match the calling +/// convention. +/// @param name_info The name of the hook, made through flamingo_make_name. +/// @param priority The priorities to respect for the hook, made through flamingo_make_priority. +/// @param install_metadata Extra installation metadata to specifiy. +/// The lifetime of the result is until it is consumed by a call to flamingo_uninstall_hook or flamingo_format_error, +/// depending on the type of the result. +FLAMINGO_C_EXPORT FlamingoInstallationResult flamingo_install_hook_full(void* hook_function, uint32_t const* target, + void** orig_pointer, uint16_t num_insts, + FlamingoCallingConvention convention, + FlamingoNameInfo* name_info, + FlamingoHookPriority* priority, + FlamingoInstallationMetadata* install_metadata); + +/// @brief Exactly the same as flamingo_install_hook_full, except: The number of instructions is 10, calling convention +/// is Cdecl, and there are no flags set in installation_metadata. +/// @param hook_function The function to call from the hook. If this is null, will branch to 0. +/// @param target The target to install the hook to. +/// @param orig_pointer A pointer to a function to populate after the install with fixups to call for a trampoline. If +/// null, no fixups will be generated. +/// @param name_info The name of the hook, made through flamingo_make_name. +/// The lifetime of the result is until it is consumed by a call to flamingo_uninstall_hook or flamingo_format_error, +/// depending on the type of the result. +FLAMINGO_C_EXPORT FlamingoInstallationResult flamingo_install_hook(void* hook_function, uint32_t const* target, + void** orig_pointer, FlamingoNameInfo* name_info); + +/// @brief Exactly the same as flamingo_install_hook_full, except: The number of instructions is 10, calling convention +/// is Cdecl, there are no flags set in installation_metadata, and there is no name for the hook (the empty string will +/// be used instead). +/// @param hook_function The function to call from the hook. If this is null, will branch to 0. +/// @param target The target to install the hook to. +/// @param orig_pointer A pointer to a function to populate after the install with fixups to call for a trampoline. If +/// null, no fixups will be generated. +/// The lifetime of the result is until it is consumed by a call to flamingo_uninstall_hook or flamingo_format_error, +/// depending on the type of the result. +FLAMINGO_C_EXPORT FlamingoInstallationResult flamingo_install_hook_no_name(void* hook_function, uint32_t const* target, + void** orig_pointer); + +#ifndef FLAMINGO_NO_REGISTRATION_CHECKS +/// @brief Install a hook at the provided target to call the provided hook function, with an optionally non-null +/// orig_pointer that will be assigned to the fixups region, and a name. Returns the installation result to be used in +/// uninstalls or for errors. +/// Additionally checks the provided return_info and parameter_info, made from calls to flamingo_make_type_info. +/// @param hook_function The function to call from the hook. If this is null, will branch to 0. +/// @param target The target to install the hook to. +/// @param orig_pointer A pointer to a function to populate after the install with fixups to call for a trampoline. If +/// null, no fixups will be generated. +/// @param convention The calling convention of the target function. The hook_function must match the calling +/// convention. +/// @param name_info The name of the hook, made through flamingo_make_name. +/// @param priority The priorities to respect for the hook, made through flamingo_make_priority. +/// @param install_metadata Extra installation metadata to specifiy. +/// @param return_info The return type info to check, made through flamingo_make_type_info. +/// @param parameter_info An array of type infos to check, made through flamingo_make_type_info. +/// @param num_params The length of the parameter_info array. +/// The lifetime of the result is until it is consumed by a call to flamingo_uninstall_hook or flamingo_format_error, +/// depending on the type of the result. +FLAMINGO_C_EXPORT FlamingoInstallationResult +flamingo_install_hook_full_checked(void* hook_function, uint32_t const* target, void** orig_pointer, uint16_t num_insts, + FlamingoCallingConvention convention, FlamingoNameInfo* name_info, + FlamingoHookPriority* priority, FlamingoInstallationMetadata* install_metadata, + FlamingoTypeInfo* return_info, FlamingoTypeInfo** parameter_info, size_t num_params); + +/// @brief Exactly the same as flamingo_install_hook_full_checked, except: The number of instructions is 10, calling +/// convention is Cdecl, and there are no flags set in installation_metadata. Additionally checks the provided +/// return_info and parameter_info, made from calls to flamingo_make_type_info. +/// @param hook_function The function to call from the hook. If this is null, will branch to 0. +/// @param target The target to install the hook to. +/// @param orig_pointer A pointer to a function to populate after the install with fixups to call for a trampoline. If +/// null, no fixups will be generated. +/// @param name_info The name of the hook, made through flamingo_make_name. +/// @param return_info The return type info to check, made through flamingo_make_type_info. +/// @param parameter_info An array of type infos to check, made through flamingo_make_type_info. +/// @param num_params The length of the parameter_info array. +/// The lifetime of the result is until it is consumed by a call to flamingo_uninstall_hook or flamingo_format_error, +/// depending on the type of the result. +FLAMINGO_C_EXPORT FlamingoInstallationResult flamingo_install_hook_checked( + void* hook_function, uint32_t const* target, void** orig_pointer, FlamingoNameInfo* name_info, + FlamingoTypeInfo* return_info, FlamingoTypeInfo** parameter_info, size_t num_params); +#endif + +/// @brief Reinstall the top hook onto the target again. Used if the original function changed for any reason (ex, it +/// was re-JIT'd). If no hooks are present on the target, returns success: true, any_hooks_reinstalled: false. +FLAMINGO_C_EXPORT FlamingoReinstallResult flamingo_reinstall_hook(uint32_t const* target); + +/// @brief Given a handle to a successfully installed hook, uninstalls this hook at that location, returning if it +/// succeeded and if there are other hooks left at that target. After this call, the provided FlamingoHookHandle is +/// invalid. +FLAMINGO_C_EXPORT FlamingoUninstallResult flamingo_uninstall_hook(FlamingoHookHandle* handle); +/// @brief Given an installation error, formats a human-readable error message and writes it to the provided string, not +/// exceeding the size provided. +FLAMINGO_C_EXPORT_VOID void flamingo_format_error(FlamingoInstallErrorData* error, char* buffer, + size_t buffer_size); #ifdef __cplusplus } From 69357fa01d35dbb575e8d3e4c0f7b0144c5a58aa Mon Sep 17 00:00:00 2001 From: Adam ? Date: Wed, 7 May 2025 23:23:33 -0700 Subject: [PATCH 054/134] Add CAPI header, implementation to follow --- shared/capi.h | 1 + 1 file changed, 1 insertion(+) diff --git a/shared/capi.h b/shared/capi.h index 2b82e49..5d1ed77 100644 --- a/shared/capi.h +++ b/shared/capi.h @@ -2,6 +2,7 @@ #include #include +#include // Most flamingo API calls also require the result to be used in some way. #define FLAMINGO_C_EXPORT __attribute__((visibility("default"))) __attribute__((warn_unused_result)) From 9a0a163d6564dec9eb155bc46ac469d7c07dff35 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Sun, 11 May 2025 20:33:28 -0700 Subject: [PATCH 055/134] Update .clang-tidy a bit --- .clang-tidy | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 6cfdd11..ae51d81 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -2,10 +2,8 @@ Checks: "clang-diagnostic-*, clang-analyzer-*, fuchsia-statically-constructed-objects, *, - -llvmlibc-callee-namespace, -readability-identifier-length, - -llvmlibc-implementation-in-namespace, - -llvmlibc-restrict-system-libc-headers, + -llvmlibc-*, -llvm-namespace-comment, -llvm-include-order, -altera-*, @@ -22,5 +20,4 @@ Checks: "clang-diagnostic-*, -abseil-*" WarningsAsErrors: true HeaderFilterRegex: "" -AnalyzeTemporaryDtors: false FormatStyle: file From 9a86013170a54e3ede9c4924224ad51b54b8bb54 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Tue, 22 Jul 2025 19:38:39 -0700 Subject: [PATCH 056/134] Add CAPI implementation, adjust comments to match Also fix some incorrect build cases and add some ctors --- shared/capi.h | 44 +++-- shared/hook-data.hpp | 57 ++++++- shared/hook-installation-result.hpp | 18 +- shared/type-info.hpp | 2 +- src/capi.cpp | 245 +++++++++++++++++++++++++++- 5 files changed, 337 insertions(+), 29 deletions(-) diff --git a/shared/capi.h b/shared/capi.h index 5d1ed77..9278f6a 100644 --- a/shared/capi.h +++ b/shared/capi.h @@ -1,8 +1,8 @@ #pragma once #include -#include #include +#include // Most flamingo API calls also require the result to be used in some way. #define FLAMINGO_C_EXPORT __attribute__((visibility("default"))) __attribute__((warn_unused_result)) @@ -48,17 +48,27 @@ typedef struct { } FlamingoInstallationResult; /// @brief Returned from a reinstallation. If success is false, the value of any_hooks_reinstalled is undefined. -/// If success is true, the value describes if any hooks were present at the specified target for a reinstall. +/// If success is true, the union describes if any hooks were present at the specified target for a reinstall. +/// If success is false, the union describes the error that occurred during the reinstall. The lifetime of the error +/// data is until flamingo_format_error is called. typedef struct { bool success; - bool any_hooks_reinstalled; + union { + bool any_hooks_reinstalled; + FlamingoInstallErrorData* data; + } value; } FlamingoReinstallResult; /// @brief Returned from an uninstall. If success is false, the value of any_hooks_remain is undefined. -/// If success is true, the value describes if any hooks remain after the uninstallation at this target. +/// If success is true, the union describes if any hooks remain after the uninstallation at this target. +/// If success is false, the union describes the error that occurred during the uninstall. A false value indicates that +/// no hook was found with the provided handle. A true value indicates that a remapping failure occurred. typedef struct { bool success; - bool any_hooks_remain; + union { + bool any_hooks_remain; + bool remap_failure; + } value; } FlamingoUninstallResult; /// @brief The calling conventions of the target used for hook validation. @@ -86,7 +96,7 @@ typedef struct FlamingoTypeInfo FlamingoTypeInfo; /// this location. typedef struct { /// @brief The size of the hook present at this address, in number of instructions. - uint32_t hook_size; + size_t hook_size; /// @brief A non-owning pointer to the hook's original instructions. /// Safe to dereference up to 'hook_size' for as long as there is at least one hook at this location that is not /// uninstalled, and no reinstall takes place. If no hook is present at this target, this pointer will exactly equal @@ -116,7 +126,8 @@ FLAMINGO_C_EXPORT FlamingoHookPriority* flamingo_make_priority(FlamingoNameInfo* /// will usually mean a different scratch register should be used, and that the branching logic may be incorrect. /// @param write_prot Whether to also mark the page where the target is as writable. /// The lifetime of the result is until it is consumed by a call to flamingo_install_hook*. -FLAMINGO_C_EXPORT FlamingoInstallationMetadata* flamingo_make_install_metadata(bool is_midpoint, bool write_prot); +FLAMINGO_C_EXPORT FlamingoInstallationMetadata* flamingo_make_install_metadata(bool make_fixups, bool is_midpoint, + bool write_prot); /// @brief Creates a flamingo::TypeInfo from the provided parameters. /// This is used for type checking hook installs to ensure multiple installs over the same target agree upon the @@ -145,7 +156,7 @@ FLAMINGO_C_EXPORT FlamingoOriginalInstructionsResult flamingo_orig_for(uint32_t /// @param install_metadata Extra installation metadata to specifiy. /// The lifetime of the result is until it is consumed by a call to flamingo_uninstall_hook or flamingo_format_error, /// depending on the type of the result. -FLAMINGO_C_EXPORT FlamingoInstallationResult flamingo_install_hook_full(void* hook_function, uint32_t const* target, +FLAMINGO_C_EXPORT FlamingoInstallationResult flamingo_install_hook_full(void* hook_function, uint32_t* target, void** orig_pointer, uint16_t num_insts, FlamingoCallingConvention convention, FlamingoNameInfo* name_info, @@ -161,7 +172,7 @@ FLAMINGO_C_EXPORT FlamingoInstallationResult flamingo_install_hook_full(void* ho /// @param name_info The name of the hook, made through flamingo_make_name. /// The lifetime of the result is until it is consumed by a call to flamingo_uninstall_hook or flamingo_format_error, /// depending on the type of the result. -FLAMINGO_C_EXPORT FlamingoInstallationResult flamingo_install_hook(void* hook_function, uint32_t const* target, +FLAMINGO_C_EXPORT FlamingoInstallationResult flamingo_install_hook(void* hook_function, uint32_t* target, void** orig_pointer, FlamingoNameInfo* name_info); /// @brief Exactly the same as flamingo_install_hook_full, except: The number of instructions is 10, calling convention @@ -173,7 +184,7 @@ FLAMINGO_C_EXPORT FlamingoInstallationResult flamingo_install_hook(void* hook_fu /// null, no fixups will be generated. /// The lifetime of the result is until it is consumed by a call to flamingo_uninstall_hook or flamingo_format_error, /// depending on the type of the result. -FLAMINGO_C_EXPORT FlamingoInstallationResult flamingo_install_hook_no_name(void* hook_function, uint32_t const* target, +FLAMINGO_C_EXPORT FlamingoInstallationResult flamingo_install_hook_no_name(void* hook_function, uint32_t* target, void** orig_pointer); #ifndef FLAMINGO_NO_REGISTRATION_CHECKS @@ -196,7 +207,7 @@ FLAMINGO_C_EXPORT FlamingoInstallationResult flamingo_install_hook_no_name(void* /// The lifetime of the result is until it is consumed by a call to flamingo_uninstall_hook or flamingo_format_error, /// depending on the type of the result. FLAMINGO_C_EXPORT FlamingoInstallationResult -flamingo_install_hook_full_checked(void* hook_function, uint32_t const* target, void** orig_pointer, uint16_t num_insts, +flamingo_install_hook_full_checked(void* hook_function, uint32_t* target, void** orig_pointer, uint16_t num_insts, FlamingoCallingConvention convention, FlamingoNameInfo* name_info, FlamingoHookPriority* priority, FlamingoInstallationMetadata* install_metadata, FlamingoTypeInfo* return_info, FlamingoTypeInfo** parameter_info, size_t num_params); @@ -214,14 +225,14 @@ flamingo_install_hook_full_checked(void* hook_function, uint32_t const* target, /// @param num_params The length of the parameter_info array. /// The lifetime of the result is until it is consumed by a call to flamingo_uninstall_hook or flamingo_format_error, /// depending on the type of the result. -FLAMINGO_C_EXPORT FlamingoInstallationResult flamingo_install_hook_checked( - void* hook_function, uint32_t const* target, void** orig_pointer, FlamingoNameInfo* name_info, - FlamingoTypeInfo* return_info, FlamingoTypeInfo** parameter_info, size_t num_params); +FLAMINGO_C_EXPORT FlamingoInstallationResult +flamingo_install_hook_checked(void* hook_function, uint32_t* target, void** orig_pointer, FlamingoNameInfo* name_info, + FlamingoTypeInfo* return_info, FlamingoTypeInfo** parameter_info, size_t num_params); #endif /// @brief Reinstall the top hook onto the target again. Used if the original function changed for any reason (ex, it /// was re-JIT'd). If no hooks are present on the target, returns success: true, any_hooks_reinstalled: false. -FLAMINGO_C_EXPORT FlamingoReinstallResult flamingo_reinstall_hook(uint32_t const* target); +FLAMINGO_C_EXPORT FlamingoReinstallResult flamingo_reinstall_hook(uint32_t* target); /// @brief Given a handle to a successfully installed hook, uninstalls this hook at that location, returning if it /// succeeded and if there are other hooks left at that target. After this call, the provided FlamingoHookHandle is @@ -230,8 +241,7 @@ FLAMINGO_C_EXPORT FlamingoUninstallResult flamingo_uninstall_hook(FlamingoHookHa /// @brief Given an installation error, formats a human-readable error message and writes it to the provided string, not /// exceeding the size provided. -FLAMINGO_C_EXPORT_VOID void flamingo_format_error(FlamingoInstallErrorData* error, char* buffer, - size_t buffer_size); +FLAMINGO_C_EXPORT_VOID void flamingo_format_error(FlamingoInstallErrorData* error, char* buffer, size_t buffer_size); #ifdef __cplusplus } diff --git a/shared/hook-data.hpp b/shared/hook-data.hpp index a8b5504..7be4149 100644 --- a/shared/hook-data.hpp +++ b/shared/hook-data.hpp @@ -1,10 +1,13 @@ #pragma once +#include #include #include #include +#include #include "calling-convention.hpp" #include "hook-metadata.hpp" +#include "type-info.hpp" namespace flamingo { @@ -40,7 +43,7 @@ struct HookInfo { template HookInfo(HookFuncType hook_func, void* target, HookFuncType* orig_ptr, uint16_t num_insts, CallingConvention conv, HookNameMetadata&& name_info, HookPriority&& priority, - InstallationMetadata&& install_metadata) + InstallationMetadata const& install_metadata) : target(target), orig_ptr(reinterpret_cast(orig_ptr)), hook_ptr(reinterpret_cast(hook_func)), @@ -57,6 +60,58 @@ struct HookInfo { }) { } + HookInfo(void* hook_func, void* target, void** orig_ptr, uint16_t num_insts, CallingConvention conv, + HookNameMetadata&& name_info, HookPriority&& priority, InstallationMetadata const& install_metadata) + : target(target), + orig_ptr(orig_ptr), + hook_ptr(hook_func), + metadata(HookMetadata{ + .convention = conv, + .installation_metadata = install_metadata, + .method_num_insts = num_insts, + .name_info = name_info, + .priority = priority, +#ifndef FLAMINGO_NO_REGISTRATION_CHECKS + .parameter_info = {}, + .return_info = {}, +#endif + }) { + } + + HookInfo(void* hook_func, void* target, void** orig_ptr, uint16_t num_insts, CallingConvention conv, + HookNameMetadata&& name_info, HookPriority&& priority, InstallationMetadata const& install_metadata, std::vector&& params, TypeInfo&& return_info) +: target(target), + orig_ptr(orig_ptr), + hook_ptr(hook_func), + metadata(HookMetadata{ + .convention = conv, + .installation_metadata = install_metadata, + .method_num_insts = num_insts, + .name_info = name_info, + .priority = priority, +#ifndef FLAMINGO_NO_REGISTRATION_CHECKS + .parameter_info = std::move(params), + .return_info = return_info, +#endif + }) { +} + +HookInfo(void* hook_func, void* target, void** orig_ptr, HookNameMetadata&& name_info, std::vector&& params, TypeInfo&& return_info) + : HookInfo(hook_func, target, orig_ptr, kDefaultNumInsts, CallingConvention::Cdecl, std::move(name_info), + HookPriority{}, + InstallationMetadata{ .need_orig = orig_ptr != nullptr, .is_midpoint = false, .write_prot = false }, std::move(params), std::move(return_info)) {} + + + HookInfo(void* hook_func, void* target, void** orig_ptr) + : HookInfo(hook_func, target, orig_ptr, kDefaultNumInsts, CallingConvention::Cdecl, + HookNameMetadata{ .name = "" }, HookPriority{}, + InstallationMetadata{ .need_orig = orig_ptr != nullptr, .is_midpoint = false, .write_prot = false }) {} + + HookInfo(void* hook_func, void* target, void** orig_ptr, HookNameMetadata&& name_info) + : HookInfo(hook_func, target, orig_ptr, kDefaultNumInsts, CallingConvention::Cdecl, std::move(name_info), + HookPriority{}, + InstallationMetadata{ .need_orig = orig_ptr != nullptr, .is_midpoint = false, .write_prot = false }) {} + void assign_orig(void* ptr) { if (orig_ptr != nullptr) *orig_ptr = ptr; } diff --git a/shared/hook-installation-result.hpp b/shared/hook-installation-result.hpp index fba0dd7..971e4d7 100644 --- a/shared/hook-installation-result.hpp +++ b/shared/hook-installation-result.hpp @@ -157,18 +157,18 @@ class fmt::formatter { return std::visit( flamingo::util::overload{ [&](TargetIsNull const& null_target) { - return format_to(ctx.out(), FMT_COMPILE("Null target, for hook: {}"), null_target.installing_hook); + return format_to(ctx.out(), "Null target, for hook: {}", null_target.installing_hook); }, [&](TargetBadPriorities const& bad_priorities) { - return format_to(ctx.out(), FMT_COMPILE("Bad priorities, for hook: {}"), bad_priorities.installing_hook); + return format_to(ctx.out(), "Bad priorities, for hook: {}", bad_priorities.installing_hook); }, [&](TargetMismatch const& mismatch) { - return format_to(ctx.out(), FMT_COMPILE("Target mismatch: {}"), mismatch); + return format_to(ctx.out(), "Target mismatch: {}", mismatch); }, [&](TargetTooSmall const& small_target) { return format_to( ctx.out(), - FMT_COMPILE("Target too small, needed: {} instructions, but have: {} instructions for hook: {}"), + "Target too small, needed: {} instructions, but have: {} instructions for hook: {}", small_target.needed_num_insts, small_target.actual_num_insts, small_target.installing_hook); } }, error); @@ -188,27 +188,27 @@ class fmt::formatter { return std::visit( flamingo::util::overload{ [&](MismatchTargetConv const& mismatch_conv) { - return format_to(ctx.out(), FMT_COMPILE("Target has calling convention: {} but specified: {} for hook: {}"), + return format_to(ctx.out(), "Target has calling convention: {} but specified: {} for hook: {}", mismatch_conv.existing, mismatch_conv.incoming, mismatch_conv.installing_hook); }, [&](MismatchMidpoint const& mismatch_midpoint) { return format_to(ctx.out(), - FMT_COMPILE("Target has midpoint specified as: {} but specified: {} for hook: {}"), + "Target has midpoint specified as: {} but specified: {} for hook: {}", mismatch_midpoint.existing, mismatch_midpoint.incoming, mismatch_midpoint.installing_hook); }, #ifndef FLAMINGO_NO_REGISTRATION_CHECKS [&](MismatchReturn const& mismatch_return) { return format_to(ctx.out(), - FMT_COMPILE("Target has return type specified as: {} but specified: {} for hook: {}"), + "Target has return type specified as: {} but specified: {} for hook: {}", mismatch_return.existing, mismatch_return.incoming, mismatch_return.installing_hook); }, [&](MismatchParam const& mismatch_param) { return format_to( - ctx.out(), FMT_COMPILE("Target has parameter {} type specified as: {} but specified: {} for hook: {}"), + ctx.out(), "Target has parameter {} type specified as: {} but specified: {} for hook: {}", mismatch_param.idx, mismatch_param.existing, mismatch_param.incoming, mismatch_param.installing_hook); }, [&](MismatchParamCount const& mismatch_param_count) { - return format_to(ctx.out(), FMT_COMPILE("Target has {} parameters but specified: {} for hook: {}"), + return format_to(ctx.out(), "Target has {} parameters but specified: {} for hook: {}", mismatch_param_count.existing, mismatch_param_count.incoming, mismatch_param_count.installing_hook); }, diff --git a/shared/type-info.hpp b/shared/type-info.hpp index c7525f8..70f34fe 100644 --- a/shared/type-info.hpp +++ b/shared/type-info.hpp @@ -45,6 +45,6 @@ class fmt::formatter { } template constexpr auto format(flamingo::TypeInfo const& info, Context& ctx) const { - return format_to(ctx.out(), FMT_COMPILE("(size={})"), info.size); + return format_to(ctx.out(), "(size={})", info.size); } }; diff --git a/src/capi.cpp b/src/capi.cpp index 138fcda..7823e22 100644 --- a/src/capi.cpp +++ b/src/capi.cpp @@ -1,3 +1,246 @@ -// TODO: Stub file for now. Ideally, we want to implement all of the CAPI functions here. #include "capi.h" +#include +#include +#include +#include +#include "calling-convention.hpp" +#include "hook-data.hpp" +#include "hook-installation-result.hpp" +#include "hook-metadata.hpp" +#include "installer.hpp" +#include "target-data.hpp" +#include "type-info.hpp" +#include "util.hpp" +namespace { + +flamingo::CallingConvention convert_calling_conv(FlamingoCallingConvention conv) { + switch (conv) { + case FlamingoCallingConvention::FLAMINGO_CDECL: + return flamingo::CallingConvention::Cdecl; + case FLAMINGO_FASTCALL: + return flamingo::CallingConvention::Fastcall; + case FLAMINGO_THISCALL: + return flamingo::CallingConvention::Thiscall; + } +} + +FlamingoInstallationType type_from_mismatch(flamingo::installation::TargetMismatch const& mismatch) { + using namespace flamingo::installation; + return std::visit(flamingo::util::overload{ + [](MismatchTargetConv const&) { return FLAMINGO_INSTALL_MISMATCH_CALLING_CONVENTION; }, + [](MismatchMidpoint const&) { return FLAMINGO_INSTALL_MISMATCH_MIDPOINT; }, +#ifndef FLAMINGO_NO_REGISTRATION_CHECKS + [](MismatchReturn const&) { return FLAMINGO_INSTALL_MISMATCH_RETURN; }, + [](MismatchParam const&) { return FLAMINGO_INSTALL_MISMATCH_PARAM; }, + [](MismatchParamCount const&) { return FLAMINGO_INSTALL_MISMATCH_PARAM_COUNT; }, +#endif + }, + mismatch); +} + +FlamingoInstallErrorData* make_error_data(flamingo::installation::Error const& error) { + return reinterpret_cast(new flamingo::installation::Error{ error }); +} + +FlamingoInstallationResult convert_install_result(flamingo::installation::Result const& result) { + using namespace flamingo::installation; + if (result.has_value()) { + return FlamingoInstallationResult{ + .result = FLAMINGO_INSTALL_OK, + .value = { .handle = + reinterpret_cast(new flamingo::HookHandle(result.value().returned_handle)) }, + }; + } + auto const& error = result.error(); + // Convert the error to a FlamingoInstallationType + auto result_type = + std::visit(flamingo::util::overload{ + [](TargetIsNull const&) { return FLAMINGO_INSTALL_TARGET_NULL; }, + [](TargetBadPriorities const&) { return FLAMINGO_INSTALL_BAD_PRIORITIES; }, + [](TargetMismatch const& mismatch) { return type_from_mismatch(mismatch); }, + [](TargetTooSmall const&) { return FLAMINGO_INSTALL_TOO_SMALL; }, + }, + error); + return FlamingoInstallationResult{ + .result = result_type, + .value = { .data = make_error_data(error) }, + }; +} + +FlamingoReinstallResult convert_reinstall_result(flamingo::Result const& result) { + if (result.has_value()) { + return FlamingoReinstallResult{ + .success = true, + .value = {.any_hooks_reinstalled = result.value()}, + }; + } + return FlamingoReinstallResult{ + .success = result.has_value(), + .value {.data = make_error_data(result.error())}, + }; +} + +FlamingoUninstallResult convert_uninstall_result(flamingo::Result const& result) { + if (result.has_value()) { + return FlamingoUninstallResult{ + .success = true, + .value = {.any_hooks_remain = result.value()}, + }; + } + return FlamingoUninstallResult{ + .success = result.has_value(), + .value {.remap_failure = result.error()}, + }; +} +} // namespace + +FLAMINGO_C_EXPORT FlamingoNameInfo* flamingo_make_name(char const* name_str) { + return reinterpret_cast(new flamingo::HookNameMetadata{ .name = name_str }); +} + +FLAMINGO_C_EXPORT FlamingoHookPriority* flamingo_make_priority(FlamingoNameInfo** before_names, size_t num_befores, + FlamingoNameInfo** after_names, size_t num_afters) { + // Iterate the befores and afters, consume their pointers to make new instances for the before set + auto result = new flamingo::HookPriority(); + result->befores.resize(num_befores); + for (size_t i = 0; i < num_befores; i++) { + auto value = reinterpret_cast(before_names[i]); + new (&result->befores[i]) flamingo::HookNameMetadata(*value); + delete value; + } + result->afters.resize(num_afters); + for (size_t i = 0; i < num_afters; i++) { + auto value = reinterpret_cast(after_names[i]); + new (&result->afters[i]) flamingo::HookNameMetadata(*value); + delete value; + } + return reinterpret_cast(result); +} + +FLAMINGO_C_EXPORT FlamingoInstallationMetadata* flamingo_make_install_metadata(bool make_fixups, bool is_midpoint, + bool write_prot) { + return reinterpret_cast(new flamingo::InstallationMetadata{ + // This value is overriden by the install itself + .need_orig = make_fixups, + .is_midpoint = is_midpoint, + .write_prot = write_prot, + }); +} + +FLAMINGO_C_EXPORT FlamingoTypeInfo* flamingo_make_type_info(char const* name, size_t size) { + // TODO: Implement usage of name + static_cast(name); + return reinterpret_cast(new flamingo::TypeInfo{ .size = size }); +} + +FLAMINGO_C_EXPORT FlamingoOriginalInstructionsResult flamingo_orig_for(uint32_t const* addr) { + auto result = flamingo::OriginalInstsFor(flamingo::TargetDescriptor{ const_cast(addr) }); + return FlamingoOriginalInstructionsResult{ + .hook_size = result.size(), + .original_instructions = result.empty() ? addr : result.data(), + }; +} + +FLAMINGO_C_EXPORT FlamingoInstallationResult +flamingo_install_hook_full(void* hook_function, uint32_t* target, void** orig_pointer, uint16_t num_insts, + FlamingoCallingConvention convention, FlamingoNameInfo* name_info, + FlamingoHookPriority* priority, FlamingoInstallationMetadata* install_metadata) { + auto flamingo_name_info = reinterpret_cast(name_info); + auto flamingo_priority = reinterpret_cast(priority); + auto flamingo_install_metadata = reinterpret_cast(install_metadata); + auto result = flamingo::Install(flamingo::HookInfo( + hook_function, target, orig_pointer, num_insts, convert_calling_conv(convention), std::move(*flamingo_name_info), + std::move(*flamingo_priority), std::move(*flamingo_install_metadata))); + // Delete the otherwise dangling pointers + delete flamingo_name_info; + delete flamingo_priority; + delete flamingo_install_metadata; + return convert_install_result(result); +} + +FLAMINGO_C_EXPORT FlamingoInstallationResult flamingo_install_hook(void* hook_function, uint32_t* target, + void** orig_pointer, FlamingoNameInfo* name_info) { + auto flamingo_name_info = reinterpret_cast(name_info); + auto result = + flamingo::Install(flamingo::HookInfo(hook_function, target, orig_pointer, std::move(*flamingo_name_info))); + // Delete the otherwise dangling pointers + delete flamingo_name_info; + return convert_install_result(result); +} + +FLAMINGO_C_EXPORT FlamingoInstallationResult flamingo_install_hook_no_name(void* hook_function, uint32_t* target, + void** orig_pointer) { + auto result = flamingo::Install(flamingo::HookInfo(hook_function, target, orig_pointer)); + return convert_install_result(result); +} + +#ifndef FLAMINGO_NO_REGISTRATION_CHECKS + +FLAMINGO_C_EXPORT FlamingoInstallationResult flamingo_install_hook_full_checked( + void* hook_function, uint32_t* target, void** orig_pointer, uint16_t num_insts, + FlamingoCallingConvention convention, FlamingoNameInfo* name_info, FlamingoHookPriority* priority, + FlamingoInstallationMetadata* install_metadata, FlamingoTypeInfo* return_info, FlamingoTypeInfo** parameter_info, + size_t num_params) { + auto flamingo_name_info = reinterpret_cast(name_info); + auto flamingo_priority = reinterpret_cast(priority); + auto flamingo_install_metadata = reinterpret_cast(install_metadata); + auto flamingo_return_info = reinterpret_cast(return_info); + std::vector flamingo_parameter_info{}; + flamingo_parameter_info.resize(num_params); + for (size_t i = 0; i < num_params; i++) { + auto p = reinterpret_cast(parameter_info[i]); + new (&flamingo_parameter_info[i]) flamingo::TypeInfo(*p); + delete p; + } + auto result = flamingo::Install( + flamingo::HookInfo(hook_function, target, orig_pointer, num_insts, convert_calling_conv(convention), + std::move(*flamingo_name_info), std::move(*flamingo_priority), *flamingo_install_metadata, + std::move(flamingo_parameter_info), std::move(*flamingo_return_info))); + // Delete the otherwise dangling pointers + delete flamingo_name_info; + delete flamingo_priority; + delete flamingo_install_metadata; + delete flamingo_return_info; + return convert_install_result(result); +} + +FLAMINGO_C_EXPORT FlamingoInstallationResult +flamingo_install_hook_checked(void* hook_function, uint32_t* target, void** orig_pointer, FlamingoNameInfo* name_info, + FlamingoTypeInfo* return_info, FlamingoTypeInfo** parameter_info, size_t num_params) { + auto flamingo_name_info = reinterpret_cast(name_info); + auto flamingo_return_info = reinterpret_cast(return_info); + std::vector flamingo_parameter_info{}; + flamingo_parameter_info.resize(num_params); + for (size_t i = 0; i < num_params; i++) { + auto p = reinterpret_cast(parameter_info[i]); + new (&flamingo_parameter_info[i]) flamingo::TypeInfo(*p); + delete p; + } + auto result = + flamingo::Install(flamingo::HookInfo(hook_function, target, orig_pointer, std::move(*flamingo_name_info), + std::move(flamingo_parameter_info), std::move(*flamingo_return_info))); + // Delete the otherwise dangling pointers + delete flamingo_name_info; + delete flamingo_return_info; + return convert_install_result(result); +} +#endif + +FLAMINGO_C_EXPORT FlamingoReinstallResult flamingo_reinstall_hook(uint32_t* target) { + return convert_reinstall_result(flamingo::Reinstall(flamingo::TargetDescriptor{ .target = target })); +} + +FLAMINGO_C_EXPORT FlamingoUninstallResult flamingo_uninstall_hook(FlamingoHookHandle* handle) { + auto value = reinterpret_cast(handle); + auto result = flamingo::Uninstall(value->returned_handle); + delete value; + return convert_uninstall_result(result); +} + +FLAMINGO_C_EXPORT_VOID void flamingo_format_error(FlamingoInstallErrorData* error, char* buffer, size_t buffer_size) { + auto install_error = reinterpret_cast(error); + auto [out, _] = fmt::format_to_n(buffer, buffer_size - 1, FMT_COMPILE("{}"), *install_error); + *out = '\0'; // Suffix with a null + delete install_error; +} From e47d8da28a630b3fde589ca166da27bea66bd626 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Tue, 22 Jul 2025 20:49:24 -0700 Subject: [PATCH 057/134] Update flamingo test to handle b cases Ideally, remove this logic once SHO is implemented --- src/flamingo-stamp.cpp | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/flamingo-stamp.cpp b/src/flamingo-stamp.cpp index 1b0d3d0..7c6635d 100644 --- a/src/flamingo-stamp.cpp +++ b/src/flamingo-stamp.cpp @@ -2,11 +2,12 @@ #include #include #include +#include "fixups.hpp" #include "git_info.h" #include "hook-data.hpp" +#include "installer.hpp" #include "target-data.hpp" #include "util.hpp" -#include "installer.hpp" extern void* modloader_libil2cpp_handle; @@ -23,9 +24,10 @@ static void print_decode_loop(void* ptr, size_t size) { for (size_t i = 0; i < size; i++) { cs_insn* insns = nullptr; auto count = cs_disasm(handle, reinterpret_cast(&data[i]), sizeof(uint32_t), - reinterpret_cast(&data[i]), 1, &insns); + reinterpret_cast(&data[i]), 1, &insns); if (count == 1) { - FLAMINGO_DEBUG("Addr: {} Value: 0x{:08x}, {} {}", fmt::ptr(&data[i]), data[i], insns[0].mnemonic, insns[0].op_str); + FLAMINGO_DEBUG("Addr: {} Value: 0x{:08x}, {} {}", fmt::ptr(&data[i]), data[i], insns[0].mnemonic, + insns[0].op_str); } else { FLAMINGO_DEBUG("Addr: {} Value: 0x{:08x}", fmt::ptr(&data[i]), data[i]); } @@ -37,9 +39,22 @@ FLAMINGO_EXPORT extern "C" void late_load() { void* (*runtime_invoke)(void*, void*, void*, void*); runtime_invoke = (decltype(runtime_invoke))dlsym(modloader_libil2cpp_handle, "il2cpp_runtime_invoke"); FLAMINGO_DEBUG("Found runtime_invoke: {}", fmt::ptr(runtime_invoke)); + print_decode_loop((void*)runtime_invoke, 10); + // Read the b at this location and get it back out + { + constexpr uint64_t mask = ~(0xFFULL << (64U - 8U)); + auto pc = static_cast(reinterpret_cast(runtime_invoke) & mask); + cs_insn* insns = nullptr; + cs_disasm(flamingo::getHandle(), reinterpret_cast(runtime_invoke), 4, pc, 1, &insns); + FLAMINGO_DEBUG("inst ops: {}", (int)insns->detail->arm64.op_count); + runtime_invoke = (decltype(runtime_invoke))insns->detail->arm64.operands[0].imm; + FLAMINGO_DEBUG("runtime_invoke after b resolved: {}", fmt::ptr(runtime_invoke)); + } + print_decode_loop((void*)runtime_invoke, 10); // Install the hook to it - auto result = flamingo::Install(flamingo::HookInfo(&wrap_runtime_invoke, (void*)runtime_invoke, &orig_runtime_invoke)); + auto result = + flamingo::Install(flamingo::HookInfo(&wrap_runtime_invoke, (void*)runtime_invoke, &orig_runtime_invoke)); if (!result.has_value()) { FLAMINGO_ABORT("Hook installation error! Error is of type: {}", result.error()); } @@ -49,7 +64,7 @@ FLAMINGO_EXPORT extern "C" void late_load() { FLAMINGO_DEBUG("Orig callback (should match runtime_invoke): {}", fmt::ptr(orig_runtime_invoke)); print_decode_loop((void*)runtime_invoke, 10); // Ask for the fixups location and dump that - auto fixups = flamingo::FixupPointerFor(flamingo::TargetDescriptor {.target = (void*)runtime_invoke }); + auto fixups = flamingo::FixupPointerFor(flamingo::TargetDescriptor{ .target = (void*)runtime_invoke }); if (!result.has_value()) { FLAMINGO_ABORT("Fixup lookup failed for target: {}", (void*)runtime_invoke); } From e397f703015fbb6ccc8a1bda7b6614de800f48a1 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Mon, 25 Aug 2025 22:19:21 -0700 Subject: [PATCH 058/134] Add final priority level for flamingo hooks Also implements an extremely small priority installer --- include/git_info.h | 2 +- shared/hook-installation-result.hpp | 11 +++++------ shared/hook-metadata.hpp | 2 ++ src/capi.cpp | 3 ++- src/installer.cpp | 23 +++++++++++++++++------ 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/include/git_info.h b/include/git_info.h index c5d6384..20a36d5 100644 --- a/include/git_info.h +++ b/include/git_info.h @@ -1,5 +1,5 @@ #pragma once #define GIT_USER "Adam ?" #define GIT_BRANCH "dev/sc2ad/v1.0.0-milestone" -#define GIT_COMMIT 0xbf77603 +#define GIT_COMMIT 0xcee43c1 #define GIT_MODIFIED 1 diff --git a/shared/hook-installation-result.hpp b/shared/hook-installation-result.hpp index 971e4d7..7f5875f 100644 --- a/shared/hook-installation-result.hpp +++ b/shared/hook-installation-result.hpp @@ -1,15 +1,13 @@ #pragma once #include #include -#include -#include -#include +#include +#include #include #include #include "calling-convention.hpp" #include "hook-metadata.hpp" -#include "page-allocator.hpp" #include "target-data.hpp" #include "type-info.hpp" #include "util.hpp" @@ -83,7 +81,8 @@ struct TargetTooSmall : HookErrorInfo { /// onto. struct TargetBadPriorities : HookErrorInfo { // TODO: Add a bunch of stuff here - TargetBadPriorities(HookMetadata const& m) : HookErrorInfo(m.name_info) {} + TargetBadPriorities(HookMetadata const& m, std::string_view message) : HookErrorInfo(m.name_info), message(message) {} + std::string message; }; // TODO: Should we add the incoming hook IDs? @@ -160,7 +159,7 @@ class fmt::formatter { return format_to(ctx.out(), "Null target, for hook: {}", null_target.installing_hook); }, [&](TargetBadPriorities const& bad_priorities) { - return format_to(ctx.out(), "Bad priorities, for hook: {}", bad_priorities.installing_hook); + return format_to(ctx.out(), "Bad priorities, for hook: {}, with message: {}", bad_priorities.installing_hook, bad_priorities.message); }, [&](TargetMismatch const& mismatch) { return format_to(ctx.out(), "Target mismatch: {}", mismatch); diff --git a/shared/hook-metadata.hpp b/shared/hook-metadata.hpp index a9ef86d..68fed90 100644 --- a/shared/hook-metadata.hpp +++ b/shared/hook-metadata.hpp @@ -33,6 +33,8 @@ struct HookPriority { std::vector befores{}; /// @brief The set of constraints for this hook to be installed after (called later than) std::vector afters{}; + /// @brief Set to true if this hook should be the final hook (closest to the original function) + bool is_final{false}; }; struct HookMetadata { diff --git a/src/capi.cpp b/src/capi.cpp index 7823e22..5bfc703 100644 --- a/src/capi.cpp +++ b/src/capi.cpp @@ -100,7 +100,7 @@ FLAMINGO_C_EXPORT FlamingoNameInfo* flamingo_make_name(char const* name_str) { } FLAMINGO_C_EXPORT FlamingoHookPriority* flamingo_make_priority(FlamingoNameInfo** before_names, size_t num_befores, - FlamingoNameInfo** after_names, size_t num_afters) { + FlamingoNameInfo** after_names, size_t num_afters, bool is_final) { // Iterate the befores and afters, consume their pointers to make new instances for the before set auto result = new flamingo::HookPriority(); result->befores.resize(num_befores); @@ -115,6 +115,7 @@ FLAMINGO_C_EXPORT FlamingoHookPriority* flamingo_make_priority(FlamingoNameInfo* new (&result->afters[i]) flamingo::HookNameMetadata(*value); delete value; } + result->is_final = is_final; return reinterpret_cast(result); } diff --git a/src/installer.cpp b/src/installer.cpp index 489ffa2..e0e80d7 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -26,7 +26,8 @@ using namespace flamingo; inline static std::map targets; Result::iterator, installation::TargetBadPriorities> find_suitable_priority_location_for( - std::list& hooks, HookPriority priority) { + std::list& hooks, HookMetadata const& hook_to_install) { + using ResultT = Result::iterator, installation::TargetBadPriorities>; // Install onto the target, respecting priorities. // Note that we may need to recompile some callbacks/fixups to change things // 1. Topological sort on our hooks that exist here by priority @@ -35,13 +36,23 @@ Result::iterator, installation::TargetBadPriorities> find_su // - First, walk all the hooks for a viable location, if we can find one. If we cannot, then we have to recompile // hooks. // TODO: Above - static_cast(priority); - return Result::iterator, installation::TargetBadPriorities>::Ok(hooks.begin()); + // Also, if we have a final priority, we need to be the final hook, unless that hook is itself already marked as + // final. + if (hook_to_install.priority.is_final) { + if (!hooks.empty() && hooks.back().metadata.priority.is_final) { + // We cannot install here, we have a conflict + return ResultT::Err(installation::TargetBadPriorities{ + hook_to_install, fmt::format("Cannot install a 'final' hook after another 'final' hook with name: {}", hooks.back().metadata.name_info) }); + } + // Select the end to install at + return ResultT::Ok(hooks.end()); + } + // Otherwise, just install it at the front. + return ResultT::Ok(hooks.begin()); } Result validate_install_metadata(TargetMetadata& existing, HookMetadata const& incoming) { - // TODO: At this point, we should be double checking metadata. using ResultT = Result; // 1. Take the min of num_insts/verify they are equivalent existing.method_num_insts = std::min(existing.method_num_insts, incoming.method_num_insts); @@ -53,7 +64,7 @@ Result validate_install_metadata(T if (existing.metadata.is_midpoint != incoming.installation_metadata.is_midpoint) { return ResultT::ErrAt(incoming, existing.metadata.is_midpoint); } -// 3. Ensure parameter_info and return_info are matching (ifdef guarded) + // 4. Ensure parameter_info and return_info are matching (ifdef guarded) #ifndef FLAMINGO_NO_REGISTRATION_CHECKS if (existing.return_info != incoming.return_info) { return ResultT::ErrAt(incoming, existing.return_info); @@ -140,7 +151,7 @@ installation::Result Install(HookInfo&& hook) { return installation::Result::ErrAt(installation_checks.error()); } - auto location_or_err = find_suitable_priority_location_for(hooked_target->second.hooks, hook.metadata.priority); + auto location_or_err = find_suitable_priority_location_for(hooked_target->second.hooks, hook.metadata); if (!location_or_err.has_value()) { return installation::Result::ErrAt(location_or_err.error()); } From 21759269e7923e016d09022b08f907ae5886bda4 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Mon, 25 Aug 2025 22:25:23 -0700 Subject: [PATCH 059/134] Remove autogenerated git_info.h file --- .gitignore | 1 + CMakeLists.txt | 2 +- include/git_info.h | 5 ----- src/fixups.cpp | 2 ++ src/flamingo-stamp.cpp | 2 +- 5 files changed, 5 insertions(+), 7 deletions(-) delete mode 100644 include/git_info.h diff --git a/.gitignore b/.gitignore index 1e4329e..5c0a81e 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,4 @@ qpm_defines.cmake build/ build-linux/ +*.inc diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a687de..e04a53f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,7 +36,7 @@ message(STATUS "GIT_COMMIT: 0x${GIT_COMMIT}") message(STATUS "GIT_MODIFIED: ${GIT_MODIFIED}") # Check for file presence and read current contents -set(GIT_INFO_H_PATH "${CMAKE_CURRENT_SOURCE_DIR}/include/git_info.h") +set(GIT_INFO_H_PATH "${CMAKE_CURRENT_SOURCE_DIR}/include/git_info.inc") if(EXISTS "${GIT_INFO_H_PATH}") file(READ "${GIT_INFO_H_PATH}" GIT_INFO_H_CURRENT) else() diff --git a/include/git_info.h b/include/git_info.h deleted file mode 100644 index 20a36d5..0000000 --- a/include/git_info.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once -#define GIT_USER "Adam ?" -#define GIT_BRANCH "dev/sc2ad/v1.0.0-milestone" -#define GIT_COMMIT 0xcee43c1 -#define GIT_MODIFIED 1 diff --git a/src/fixups.cpp b/src/fixups.cpp index 08f9928..fb8cccc 100644 --- a/src/fixups.cpp +++ b/src/fixups.cpp @@ -9,6 +9,7 @@ #include #include "capstone/capstone.h" #include "capstone/platform.h" +#include "git_info.inc" #include "util.hpp" namespace { @@ -508,6 +509,7 @@ csh getHandle() { if (e1 != CS_ERR_OK || e2 != CS_ERR_OK) { FLAMINGO_ABORT("Capstone initialization failed: {}, {}", static_cast(e1), static_cast(e2)); } + FLAMINGO_DEBUG("Hello from flamingo! Commit: {:#08x}", GIT_COMMIT); init = true; } return handle; diff --git a/src/flamingo-stamp.cpp b/src/flamingo-stamp.cpp index 7c6635d..460aa2f 100644 --- a/src/flamingo-stamp.cpp +++ b/src/flamingo-stamp.cpp @@ -3,7 +3,7 @@ #include #include #include "fixups.hpp" -#include "git_info.h" +#include "git_info.inc" #include "hook-data.hpp" #include "installer.hpp" #include "target-data.hpp" From 6ba514da202e1e05edf796fa516ed28bd88df33e Mon Sep 17 00:00:00 2001 From: Adam ? Date: Mon, 25 Aug 2025 22:44:21 -0700 Subject: [PATCH 060/134] Add build for android test to CI and add script for it Could have been shelved into the same script, but this is fine enough --- .github/workflows/build-android-test.yml | 66 +++++++++++++++++++++++ .github/workflows/build-ndk.yml | 69 ++++++++++++++++++------ CMakeLists.txt | 7 ++- build-android-test.ps1 | 25 +++++++++ qpm.json | 5 +- qpm.shared.json | 5 +- 6 files changed, 157 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/build-android-test.yml create mode 100644 build-android-test.ps1 diff --git a/.github/workflows/build-android-test.yml b/.github/workflows/build-android-test.yml new file mode 100644 index 0000000..f1a88ac --- /dev/null +++ b/.github/workflows/build-android-test.yml @@ -0,0 +1,66 @@ +on: + workflow_dispatch: + push: + branches-ignore: + - "version-*" + +env: + module_id: flamingo + qmodName: flamingo + +jobs: + build-android-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + name: Checkout + with: + submodules: true + lfs: true + + - uses: seanmiddleditch/gha-setup-ninja@v3 + + - name: Create ndkpath.txt + run: | + echo "$ANDROID_NDK_LATEST_HOME" > ${GITHUB_WORKSPACE}/ndkpath.txt + cat ${GITHUB_WORKSPACE}/ndkpath.txt + + - name: Setup qpm + if: ${{ !startsWith(github.ref, 'refs/tags/v') }} + uses: Fernthedev/qpm-action@v1 + with: + workflow_token: ${{ secrets.GITHUB_TOKEN }} + restore: true + cache: true + publish: false + + - name: QPM Collapse + run: qpm-rust collapse + + - name: Build + run: | + cd ${GITHUB_WORKSPACE} + qpm-rust s build-android-test + + - name: Get Library Name + id: libname + run: | + cd ./build/ + pattern="lib${module_id}*.so" + files=( $pattern ) + echo ::set-output name=NAME::"${files[0]}" + + - name: Upload non-debug artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.libname.outputs.NAME }} + path: ./build/${{ steps.libname.outputs.NAME }} + if-no-files-found: error + + - name: Upload debug artifact + uses: actions/upload-artifact@v4 + with: + name: debug_${{ steps.libname.outputs.NAME }} + path: ./build/debug/${{ steps.libname.outputs.NAME }} + if-no-files-found: error \ No newline at end of file diff --git a/.github/workflows/build-ndk.yml b/.github/workflows/build-ndk.yml index 45911bd..85c693d 100644 --- a/.github/workflows/build-ndk.yml +++ b/.github/workflows/build-ndk.yml @@ -8,7 +8,6 @@ on: env: module_id: flamingo - cache-name: flamingo_cache qmodName: flamingo jobs: @@ -30,14 +29,14 @@ jobs: platform: x64 # Get QPM so we can bring in dependencies - - name: QPM Rust Action + - name: Setup qpm + if: ${{ !startsWith(github.ref, 'refs/tags/v') }} uses: Fernthedev/qpm-action@v1 with: - #required - workflow_token: ${{secrets.GITHUB_TOKEN}} - - restore: true # will run restore on download - cache: true #will cache dependencies + workflow_token: ${{ secrets.GITHUB_TOKEN }} + restore: true + cache: true + publish: false - name: QPM Collapse run: qpm-rust collapse @@ -53,6 +52,7 @@ jobs: # Run the test to ensure validity - name: Run tests run: ctest --test-dir build --output-on-failure + build: runs-on: ubuntu-latest @@ -70,17 +70,38 @@ jobs: echo "$ANDROID_NDK_LATEST_HOME" > ${GITHUB_WORKSPACE}/ndkpath.txt cat ${GITHUB_WORKSPACE}/ndkpath.txt - - name: QPM Rust Action - uses: Fernthedev/qpm-rust-action@main - with: - #required - workflow_token: ${{secrets.GITHUB_TOKEN}} - - restore: true # will run restore on download - cache: true #will cache dependencies + # get version from pushed tag + - name: Extract version + if: startsWith(github.ref, 'refs/tags/v') + id: version + run: | + echo "TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_OUTPUT} + echo "VERSION=${GITHUB_REF#refs/tags/v}" >> ${GITHUB_OUTPUT} - # Name of qmod in release asset. Assumes exists, same as prior - qpm_qmod: ${{env.qmodName}}.qmod + - name: Setup qpm + if: ${{ !startsWith(github.ref, 'refs/tags/v') }} + uses: Fernthedev/qpm-action@v1 + with: + workflow_token: ${{ secrets.GITHUB_TOKEN }} + restore: true + cache: true + publish: false + + # if we have a tag, we are making a qpm release + - name: Setup qpm for release + if: startsWith(github.ref, 'refs/tags/v') + uses: Fernthedev/qpm-action@v1 + with: + workflow_token: ${{ secrets.GITHUB_TOKEN }} + restore: true + cache: true + publish: late + publish_token: ${{ secrets.QPM_TOKEN }} + version: ${{ steps.version.outputs.VERSION }} + tag: ${{ steps.version.outputs.TAG }} + qpm_release_bin: true + qpm_debug_bin: true + qpm_qmod: ${{ env.qmodName }}.qmod - name: QPM Collapse run: qpm-rust collapse @@ -122,3 +143,17 @@ jobs: name: ${{env.qmodName}}.qmod path: ./${{ env.qmodName }}.qmod if-no-files-found: error + + # if we had a tag, we should make a release + - name: Upload release artifacts + if: startsWith(github.ref, 'refs/tags/v') + id: upload_file_release + uses: softprops/action-gh-release@v0.1.15 + with: + tag_name: ${{ github.event.inputs.version }} + files: | + ./build/${{ steps.libname.outputs.NAME }} + ./build/debug_${{ steps.libname.outputs.NAME }} + ./${{ env.qmodName }}.qmod + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CMakeLists.txt b/CMakeLists.txt index e04a53f..74ec4db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,6 +67,7 @@ if (TEST_BUILD) add_library(flamingo-static ${SOURCE_DIR}/fixups.cpp ${SOURCE_DIR}/installer.cpp ${SOURCE_DIR}/page-allocator.cpp ${SOURCE_DIR}/capi.cpp) target_compile_options(flamingo-static PRIVATE -Wall -Wextra -Werror -Wpedantic -fvisibility=hidden) target_include_directories(flamingo-static PUBLIC ${SHARED_DIR}) + target_include_directories(flamingo-static PRIVATE ${SOURCE_DIR} ${INCLUDE_DIR}) # Fetch capstone (v5.0.1) include(FetchContent) @@ -111,7 +112,11 @@ else() target_link_options(${COMPILE_ID} PRIVATE -Wl,--exclude-libs,ALL) target_compile_options(${COMPILE_ID} PRIVATE -DFLAMINGO_HEADER_ONLY -fvisibility=hidden -Wall -Wextra -Werror -Wpedantic) - target_sources(${COMPILE_ID} PUBLIC ${SOURCE_DIR}/capi.cpp ${SOURCE_DIR}/flamingo-stamp.cpp ${SOURCE_DIR}/fixups.cpp ${SOURCE_DIR}/installer.cpp ${SOURCE_DIR}/page-allocator.cpp) + target_sources(${COMPILE_ID} PUBLIC ${SOURCE_DIR}/capi.cpp ${SOURCE_DIR}/fixups.cpp ${SOURCE_DIR}/installer.cpp ${SOURCE_DIR}/page-allocator.cpp) + + if (TEST_ON_ANDROID) + target_sources(${COMPILE_ID} PUBLIC ${SOURCE_DIR}/flamingo-stamp.cpp) + endif() include(extern.cmake) diff --git a/build-android-test.ps1 b/build-android-test.ps1 new file mode 100644 index 0000000..27e727c --- /dev/null +++ b/build-android-test.ps1 @@ -0,0 +1,25 @@ + +function Clean-Build-Folder { + if (Test-Path -Path "build") { + remove-item build -R + new-item -Path build -ItemType Directory + } + else { + new-item -Path build -ItemType Directory + } +} + +$NDKPath = Get-Content $PSScriptRoot/ndkpath.txt + +# Clean-Build-Folder +# Build flamingo local Android test +& cmake -G "Ninja" -DCMAKE_BUILD_TYPE="Debug" -DTEST_BUILD=0 -DTEST_ON_ANDROID=1 -B build +& cmake --build ./build + +$ExitCode = $LastExitCode + +if (-not ($ExitCode -eq 0)) { + $msg = "ExitCode: " + $ExitCode + Write-Output $msg + exit $ExitCode +} diff --git a/qpm.json b/qpm.json index 66770b7..e943b2c 100644 --- a/qpm.json +++ b/qpm.json @@ -6,7 +6,7 @@ "info": { "name": "flamingo", "id": "flamingo", - "version": "0.2.0", + "version": "1.0.0", "url": "https://github.com/sc2ad/Flamingo", "additionalData": { "overrideSoName": "libflamingo.so" @@ -16,6 +16,9 @@ "scripts": { "build": [ "pwsh ./build.ps1" + ], + "build-android-test": [ + "pwsh ./build-android-test.ps1" ] }, "qmodIncludeDirs": [], diff --git a/qpm.shared.json b/qpm.shared.json index 05418cd..3c2987b 100644 --- a/qpm.shared.json +++ b/qpm.shared.json @@ -7,7 +7,7 @@ "info": { "name": "flamingo", "id": "flamingo", - "version": "0.2.0", + "version": "1.0.0", "url": "https://github.com/sc2ad/Flamingo", "additionalData": { "overrideSoName": "libflamingo.so" @@ -17,6 +17,9 @@ "scripts": { "build": [ "pwsh ./build.ps1" + ], + "build-android-test": [ + "pwsh ./build-android-test.ps1" ] }, "qmodIncludeDirs": [], From 94f1cfb34d38d23b30e3d78d4ee23a8b0f11bc42 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Mon, 1 Sep 2025 00:32:43 -0700 Subject: [PATCH 061/134] Remove mod.json, fix qpm*.json --- .github/workflows/build-ndk.yml | 4 ++++ mod.json | 18 ------------------ mod.template.json | 4 +++- qpm.json | 6 ++++-- qpm.shared.json | 6 ++++-- 5 files changed, 15 insertions(+), 23 deletions(-) delete mode 100644 mod.json diff --git a/.github/workflows/build-ndk.yml b/.github/workflows/build-ndk.yml index 85c693d..ab937b1 100644 --- a/.github/workflows/build-ndk.yml +++ b/.github/workflows/build-ndk.yml @@ -111,6 +111,10 @@ jobs: cd ${GITHUB_WORKSPACE} qpm-rust s build + - name: Make mod.json + run: | + qpm-rust qmod manifest + - name: Create Qmod run: | pwsh -Command ./createqmod.ps1 ${{env.qmodName}} diff --git a/mod.json b/mod.json deleted file mode 100644 index bec3e84..0000000 --- a/mod.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/Lauriethefish/QuestPatcher.QMod/refs/heads/main/QuestPatcher.QMod/Resources/qmod.schema.json", - "_QPVersion": "1.0.0", - "name": "flamingo", - "id": "flamingo", - "modloader": "Scotland2", - "author": "Sc2ad", - "version": "0.2.0", - "description": "General purpose Android hook library", - "dependencies": [], - "modFiles": [], - "lateModFiles": [], - "libraryFiles": [ - "libflamingo.so" - ], - "fileCopies": [], - "copyExtensions": [] -} \ No newline at end of file diff --git a/mod.template.json b/mod.template.json index 7b672ef..e185226 100644 --- a/mod.template.json +++ b/mod.template.json @@ -7,7 +7,9 @@ "description": "General purpose Android hook library", "dependencies": [], "modFiles": [], - "libraryFiles": [], + "libraryFiles": [ + "libflamingo.so" + ], "fileCopies": [], "copyExtensions": [] } \ No newline at end of file diff --git a/qpm.json b/qpm.json index e943b2c..2934c1c 100644 --- a/qpm.json +++ b/qpm.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/QuestPackageManager/QPM.Package/refs/heads/main/qpm.schema.json", - "version": "0.4.0", + "version": "1.0.0", "sharedDir": "shared", "dependenciesDir": "extern", "info": { @@ -43,7 +43,9 @@ { "id": "scotland2", "versionRange": "^0.1.6", - "additionalData": {} + "additionalData": { + "includeQmod": false + } } ] } \ No newline at end of file diff --git a/qpm.shared.json b/qpm.shared.json index 3c2987b..457d5e7 100644 --- a/qpm.shared.json +++ b/qpm.shared.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/QuestPackageManager/QPM.Package/refs/heads/main/qpm.shared.schema.json", "config": { - "version": "0.4.0", + "version": "1.0.0", "sharedDir": "shared", "dependenciesDir": "extern", "info": { @@ -44,7 +44,9 @@ { "id": "scotland2", "versionRange": "^0.1.6", - "additionalData": {} + "additionalData": { + "includeQmod": false + } } ] }, From a37bfd46e1030a18dc009a78a61779b4d881b6b1 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Mon, 1 Sep 2025 00:32:52 -0700 Subject: [PATCH 062/134] Fix some docs in capi --- shared/capi.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/shared/capi.h b/shared/capi.h index 9278f6a..da05ffb 100644 --- a/shared/capi.h +++ b/shared/capi.h @@ -114,14 +114,18 @@ FLAMINGO_C_EXPORT FlamingoNameInfo* flamingo_make_name(char const* name_str); /// The parameters are arrays of FlamingoNameInfo that must be dereferencable up to num_befores and num_afters /// respectively. The parameters are CONSUMED, that is, the pointers are no longer valid after this API call. This is /// used to give hooks priority information in flamingo_install_hook_full* +/// @param is_final Whether this hook should be the final hook (closest to the original function). This takes precedence +/// over all other priorities. /// The lifetime of the result is until it is consumed by a call to flamingo_install_hook*. FLAMINGO_C_EXPORT FlamingoHookPriority* flamingo_make_priority(FlamingoNameInfo** before_names, size_t num_befores, - FlamingoNameInfo** after_names, size_t num_afters); + FlamingoNameInfo** after_names, size_t num_afters, + bool is_final); /// @brief Creates a flamingo::InstallationMetadata from the provided parameters. /// This is used to describe metadata that should hint to flamingo to install correctly. /// Note that these are HINTS and are not strictly required for flamingo to follow, though in practice it will. This /// will be changed to strong guarantees in a future version of flamingo. +/// @param make_fixups Whether fixups should be generated for this hook. If false, orig cannot be called safely. /// @param is_midpoint Whether this hook is in the middle of a function call instead of at the beginning. Note that this /// will usually mean a different scratch register should be used, and that the branching logic may be incorrect. /// @param write_prot Whether to also mark the page where the target is as writable. @@ -149,6 +153,7 @@ FLAMINGO_C_EXPORT FlamingoOriginalInstructionsResult flamingo_orig_for(uint32_t /// @param target The target to install the hook to. /// @param orig_pointer A pointer to a function to populate after the install with fixups to call for a trampoline. If /// null, no fixups will be generated. +/// @param num_insts The number of instructions of the target function that are safe to mutate. /// @param convention The calling convention of the target function. The hook_function must match the calling /// convention. /// @param name_info The name of the hook, made through flamingo_make_name. @@ -196,6 +201,7 @@ FLAMINGO_C_EXPORT FlamingoInstallationResult flamingo_install_hook_no_name(void* /// @param target The target to install the hook to. /// @param orig_pointer A pointer to a function to populate after the install with fixups to call for a trampoline. If /// null, no fixups will be generated. +/// @param num_insts The number of instructions of the target function that are safe to mutate. /// @param convention The calling convention of the target function. The hook_function must match the calling /// convention. /// @param name_info The name of the hook, made through flamingo_make_name. From 1ff3f510794f528f78da97b2bc50e0b21ed297ee Mon Sep 17 00:00:00 2001 From: Adam ? Date: Mon, 1 Sep 2025 00:33:01 -0700 Subject: [PATCH 063/134] Version bump in util --- shared/util.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/util.hpp b/shared/util.hpp index 5a0a188..593509f 100644 --- a/shared/util.hpp +++ b/shared/util.hpp @@ -1,7 +1,7 @@ #pragma once #define FLAMINGO_ID "flamingo" -#define FLAMINGO_VERSION "0.2.0" +#define FLAMINGO_VERSION "1.0.0" #define FLAMINGO_EXPORT __attribute__((visibility("default"))) From 3da53aad1b05dce568e32f9b2633ce2c588a58d9 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Mon, 1 Sep 2025 12:42:03 -0700 Subject: [PATCH 064/134] Update CI to support proper builds --- .github/workflows/build-ndk.yml | 13 +++++++------ qpm.json | 6 ++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-ndk.yml b/.github/workflows/build-ndk.yml index ab937b1..3b7f747 100644 --- a/.github/workflows/build-ndk.yml +++ b/.github/workflows/build-ndk.yml @@ -39,7 +39,7 @@ jobs: publish: false - name: QPM Collapse - run: qpm-rust collapse + run: qpm collapse - name: CMake with LOCAL_TEST enabled run: | @@ -104,20 +104,21 @@ jobs: qpm_qmod: ${{ env.qmodName }}.qmod - name: QPM Collapse - run: qpm-rust collapse + run: qpm collapse - name: Build run: | cd ${GITHUB_WORKSPACE} - qpm-rust s build + qpm s build - name: Make mod.json run: | qpm-rust qmod manifest - - name: Create Qmod + - name: Build & Create Qmod run: | - pwsh -Command ./createqmod.ps1 ${{env.qmodName}} + cd ${GITHUB_WORKSPACE} + qpm qmod zip - name: Get Library Name id: libname @@ -125,7 +126,7 @@ jobs: cd ./build/ pattern="lib${module_id}*.so" files=( $pattern ) - echo ::set-output name=NAME::"${files[0]}" + echo "NAME=${files[0]}" >> ${GITHUB_OUTPUT} - name: Upload non-debug artifact uses: actions/upload-artifact@v4 diff --git a/qpm.json b/qpm.json index 2934c1c..ca3f7fa 100644 --- a/qpm.json +++ b/qpm.json @@ -21,9 +21,11 @@ "pwsh ./build-android-test.ps1" ] }, - "qmodIncludeDirs": [], + "qmodIncludeDirs": [ + "./build" + ], "qmodIncludeFiles": [], - "qmodOutput": null + "qmodOutput": "flamingo.qmod" }, "dependencies": [ { From 05988d8c9bed3dfd53524ce724451dde6a4cfba0 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Mon, 1 Sep 2025 12:45:58 -0700 Subject: [PATCH 065/134] Remove old publish script, use single source of truth --- .github/workflows/build-ndk.yml | 4 +- .github/workflows/publish.yml | 89 --------------------------------- 2 files changed, 3 insertions(+), 90 deletions(-) delete mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/build-ndk.yml b/.github/workflows/build-ndk.yml index 3b7f747..5fb3c24 100644 --- a/.github/workflows/build-ndk.yml +++ b/.github/workflows/build-ndk.yml @@ -5,6 +5,8 @@ on: push: branches-ignore: - "version-*" + tags: + - "v*" env: module_id: flamingo @@ -113,7 +115,7 @@ jobs: - name: Make mod.json run: | - qpm-rust qmod manifest + qpm qmod manifest - name: Build & Create Qmod run: | diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 5de2c18..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: Publish QPM Package - -env: - module_id: flamingo - qmodName: flamingo - cache-name: flamingo_cache - -on: - push: - tags: - - "v*" - -jobs: - publish: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - name: Checkout - with: - submodules: true - lfs: true - - - uses: seanmiddleditch/gha-setup-ninja@v3 - - - name: Create ndkpath.txt - run: | - echo "$ANDROID_NDK_LATEST_HOME" > ${GITHUB_WORKSPACE}/ndkpath.txt - cat ${GITHUB_WORKSPACE}/ndkpath.txt - - - name: Get Tag Version - id: get_tag_version - run: | - echo ${GITHUB_REF#refs/tags/} - echo ::set-output name=TAG::${GITHUB_REF#refs/tags/} - echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v} - - - name: QPM Rust Action - uses: Fernthedev/qpm-rust-action@main - with: - #required - workflow_token: ${{secrets.GITHUB_TOKEN}} - - restore: true # will run restore on download - cache: true #will cache dependencies - - publish: true - publish_token: ${{secrets.QPM_TOKEN}} - - version: ${{ steps.get_tag_version.outputs.VERSION }} - tag: ${{ steps.get_tag_version.outputs.TAG }} - - # set to true if applicable, ASSUMES the file is already a release asset - qpm_release_bin: true - qpm_debug_bin: true - - # Name of qmod in release asset. Assumes exists, same as prior - qpm_qmod: ${{env.qmodName}}.qmod - - - name: Build - run: | - cd ${GITHUB_WORKSPACE} - qpm-rust s build - qpm-rust qmod build - - - name: Get Library Name - id: libname - run: | - cd ./build/ - pattern="lib${module_id}*.so" - files=( $pattern ) - echo ::set-output name=NAME::"${files[0]}" - - - name: Create Qmod - run: | - pwsh -Command ./createqmod.ps1 ${{env.qmodName}} - - - name: Upload to Release - id: upload_file_release - uses: softprops/action-gh-release@v0.1.15 - with: - name: ${{ github.event.inputs.release_msg }} - tag_name: ${{ github.event.inputs.version }} - files: | - ./build/${{ steps.libname.outputs.NAME }} - ./build/debug_${{ steps.libname.outputs.NAME }} - ./${{ env.qmodName }}.qmod - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From d8a6b428a8c2454a5d719856725a1cd4f3a92fbe Mon Sep 17 00:00:00 2001 From: Adam ? Date: Mon, 1 Sep 2025 13:05:14 -0700 Subject: [PATCH 066/134] Fixup publish CI --- .github/workflows/build-ndk.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-ndk.yml b/.github/workflows/build-ndk.yml index 5fb3c24..f5f7b50 100644 --- a/.github/workflows/build-ndk.yml +++ b/.github/workflows/build-ndk.yml @@ -32,7 +32,6 @@ jobs: # Get QPM so we can bring in dependencies - name: Setup qpm - if: ${{ !startsWith(github.ref, 'refs/tags/v') }} uses: Fernthedev/qpm-action@v1 with: workflow_token: ${{ secrets.GITHUB_TOKEN }} @@ -130,6 +129,9 @@ jobs: files=( $pattern ) echo "NAME=${files[0]}" >> ${GITHUB_OUTPUT} + - name: Rename debug artifact + run: mv "./build/debug/${{ steps.libname.outputs.NAME }}" "./build/debug_${{steps.libname.outputs.NAME }}" + - name: Upload non-debug artifact uses: actions/upload-artifact@v4 with: @@ -141,7 +143,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: debug_${{ steps.libname.outputs.NAME }} - path: ./build/debug/${{ steps.libname.outputs.NAME }} + path: ./build/debug_${{ steps.libname.outputs.NAME }} if-no-files-found: error - name: Upload qmod artifact From 4cdde79217f909d3cfc0abf78f7f35a96766aef9 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Mon, 1 Sep 2025 13:07:10 -0700 Subject: [PATCH 067/134] Remove implicit capstone dependency --- shared/fixups.hpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/shared/fixups.hpp b/shared/fixups.hpp index 0d43fc7..068c191 100644 --- a/shared/fixups.hpp +++ b/shared/fixups.hpp @@ -1,12 +1,15 @@ #pragma once -#include #include #include #include #include "page-allocator.hpp" #include "util.hpp" +#if __has_include() +#include +#endif + namespace flamingo { template @@ -82,7 +85,9 @@ struct Fixups { void Uninstall(); }; +#if __has_include() // TODO: DO NOT EXPOSE THIS SYMBOL (USE IT FOR TESTING ONLY) csh getHandle(); +#endif } // namespace flamingo \ No newline at end of file From fe6b658ffba06a238c3940f330e0d5cc8dc32bd9 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Tue, 2 Sep 2025 17:51:08 -0700 Subject: [PATCH 068/134] Fix usage of GIT_COMMIT --- src/fixups.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/fixups.cpp b/src/fixups.cpp index fb8cccc..79e596f 100644 --- a/src/fixups.cpp +++ b/src/fixups.cpp @@ -9,7 +9,9 @@ #include #include "capstone/capstone.h" #include "capstone/platform.h" +#if !defined(GIT_COMMIT) && __has_include("git_info.inc") #include "git_info.inc" +#endif #include "util.hpp" namespace { @@ -509,7 +511,11 @@ csh getHandle() { if (e1 != CS_ERR_OK || e2 != CS_ERR_OK) { FLAMINGO_ABORT("Capstone initialization failed: {}, {}", static_cast(e1), static_cast(e2)); } + #ifdef GIT_COMMIT FLAMINGO_DEBUG("Hello from flamingo! Commit: {:#08x}", GIT_COMMIT); + #else + FLAMINGO_DEBUG("Hello from flamingo! Unknown build commit hash"); + #endif init = true; } return handle; From 61bfa18d381907ea9997dca7efd55d282baf2bd3 Mon Sep 17 00:00:00 2001 From: Lillie <75392499+iLillie@users.noreply.github.com> Date: Sun, 30 Nov 2025 16:56:10 +0100 Subject: [PATCH 069/134] fix: add pragma once --- shared/installer.hpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shared/installer.hpp b/shared/installer.hpp index df8d659..6b75a9d 100644 --- a/shared/installer.hpp +++ b/shared/installer.hpp @@ -1,3 +1,5 @@ +#pragma once + #include #include #include @@ -56,4 +58,4 @@ std::span FLAMINGO_EXPORT OriginalInstsFor(TargetDescriptor target); [[nodiscard]] FLAMINGO_EXPORT Result, std::monostate> FixupPointerFor( TargetDescriptor target); -} // namespace flamingo \ No newline at end of file +} // namespace flamingo From 284f901c720cca3da704756502b179273c594367 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Sun, 30 Nov 2025 13:26:04 -0800 Subject: [PATCH 070/134] HookInfo: Add converting constructor for name and priorities only --- shared/hook-data.hpp | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/shared/hook-data.hpp b/shared/hook-data.hpp index 7be4149..b114c63 100644 --- a/shared/hook-data.hpp +++ b/shared/hook-data.hpp @@ -79,28 +79,30 @@ struct HookInfo { } HookInfo(void* hook_func, void* target, void** orig_ptr, uint16_t num_insts, CallingConvention conv, - HookNameMetadata&& name_info, HookPriority&& priority, InstallationMetadata const& install_metadata, std::vector&& params, TypeInfo&& return_info) -: target(target), - orig_ptr(orig_ptr), - hook_ptr(hook_func), - metadata(HookMetadata{ - .convention = conv, - .installation_metadata = install_metadata, - .method_num_insts = num_insts, - .name_info = name_info, - .priority = priority, + HookNameMetadata&& name_info, HookPriority&& priority, InstallationMetadata const& install_metadata, + std::vector&& params, TypeInfo&& return_info) + : target(target), + orig_ptr(orig_ptr), + hook_ptr(hook_func), + metadata(HookMetadata{ + .convention = conv, + .installation_metadata = install_metadata, + .method_num_insts = num_insts, + .name_info = name_info, + .priority = priority, #ifndef FLAMINGO_NO_REGISTRATION_CHECKS - .parameter_info = std::move(params), - .return_info = return_info, + .parameter_info = std::move(params), + .return_info = return_info, #endif - }) { -} + }) { + } -HookInfo(void* hook_func, void* target, void** orig_ptr, HookNameMetadata&& name_info, std::vector&& params, TypeInfo&& return_info) + HookInfo(void* hook_func, void* target, void** orig_ptr, HookNameMetadata&& name_info, std::vector&& params, + TypeInfo&& return_info) : HookInfo(hook_func, target, orig_ptr, kDefaultNumInsts, CallingConvention::Cdecl, std::move(name_info), HookPriority{}, - InstallationMetadata{ .need_orig = orig_ptr != nullptr, .is_midpoint = false, .write_prot = false }, std::move(params), std::move(return_info)) {} - + InstallationMetadata{ .need_orig = orig_ptr != nullptr, .is_midpoint = false, .write_prot = false }, + std::move(params), std::move(return_info)) {} HookInfo(void* hook_func, void* target, void** orig_ptr) : HookInfo(hook_func, target, orig_ptr, kDefaultNumInsts, CallingConvention::Cdecl, @@ -112,6 +114,11 @@ HookInfo(void* hook_func, void* target, void** orig_ptr, HookNameMetadata&& name HookPriority{}, InstallationMetadata{ .need_orig = orig_ptr != nullptr, .is_midpoint = false, .write_prot = false }) {} + HookInfo(void* hook_func, void* target, void** orig_ptr, HookNameMetadata&& name_info, HookPriority&& priority) + : HookInfo(hook_func, target, orig_ptr, kDefaultNumInsts, CallingConvention::Cdecl, std::move(name_info), + std::move(priority), + InstallationMetadata{ .need_orig = orig_ptr != nullptr, .is_midpoint = false, .write_prot = false }) {} + void assign_orig(void* ptr) { if (orig_ptr != nullptr) *orig_ptr = ptr; } From d0cb4d7b18eecb8d5c4deb2376640739e5bde517 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Sun, 30 Nov 2025 13:26:39 -0800 Subject: [PATCH 071/134] Properly qualify fmt:: usage for format_to calls --- shared/calling-convention.hpp | 6 ++--- shared/hook-installation-result.hpp | 40 +++++++++++++---------------- shared/hook-metadata.hpp | 4 +-- shared/type-info.hpp | 2 +- 4 files changed, 24 insertions(+), 28 deletions(-) diff --git a/shared/calling-convention.hpp b/shared/calling-convention.hpp index b2aa564..b5c2b27 100644 --- a/shared/calling-convention.hpp +++ b/shared/calling-convention.hpp @@ -21,11 +21,11 @@ class fmt::formatter { constexpr auto format(flamingo::CallingConvention const& conv, Context& ctx) const { switch (conv) { case flamingo::CallingConvention::Cdecl: - return format_to(ctx.out(), "Cdecl"); + return fmt::format_to(ctx.out(), "Cdecl"); case flamingo::CallingConvention::Fastcall: - return format_to(ctx.out(), "Fastcall"); + return fmt::format_to(ctx.out(), "Fastcall"); case flamingo::CallingConvention::Thiscall: - return format_to(ctx.out(), "Thiscall"); + return fmt::format_to(ctx.out(), "Thiscall"); } } }; diff --git a/shared/hook-installation-result.hpp b/shared/hook-installation-result.hpp index 7f5875f..5e89c45 100644 --- a/shared/hook-installation-result.hpp +++ b/shared/hook-installation-result.hpp @@ -147,27 +147,25 @@ using Result = flamingo::Result; template <> class fmt::formatter { public: - constexpr auto parse(format_parse_context& ctx) { + constexpr static auto parse(format_parse_context& ctx) { return ctx.begin(); } template - constexpr auto format(flamingo::installation::Error const& error, Context& ctx) const { + constexpr static auto format(flamingo::installation::Error const& error, Context& ctx) { using namespace flamingo::installation; return std::visit( flamingo::util::overload{ - [&](TargetIsNull const& null_target) { - return format_to(ctx.out(), "Null target, for hook: {}", null_target.installing_hook); - }, - [&](TargetBadPriorities const& bad_priorities) { - return format_to(ctx.out(), "Bad priorities, for hook: {}, with message: {}", bad_priorities.installing_hook, bad_priorities.message); + [&ctx](TargetIsNull const& null_target) { + return fmt::format_to(ctx.out(), "Null target, for hook: {}", null_target.installing_hook); }, - [&](TargetMismatch const& mismatch) { - return format_to(ctx.out(), "Target mismatch: {}", mismatch); + [&ctx](TargetBadPriorities const& bad_priorities) { + return fmt::format_to(ctx.out(), "Bad priorities, for hook: {}, with message: {}", + bad_priorities.installing_hook, bad_priorities.message); }, - [&](TargetTooSmall const& small_target) { - return format_to( - ctx.out(), - "Target too small, needed: {} instructions, but have: {} instructions for hook: {}", + [&ctx](TargetMismatch const& mismatch) { return fmt::format_to(ctx.out(), "Target mismatch: {}", mismatch); }, + [&ctx](TargetTooSmall const& small_target) { + return fmt::format_to( + ctx.out(), "Target too small, needed: {} instructions, but have: {} instructions for hook: {}", small_target.needed_num_insts, small_target.actual_num_insts, small_target.installing_hook); } }, error); @@ -187,27 +185,25 @@ class fmt::formatter { return std::visit( flamingo::util::overload{ [&](MismatchTargetConv const& mismatch_conv) { - return format_to(ctx.out(), "Target has calling convention: {} but specified: {} for hook: {}", + return fmt::format_to(ctx.out(), "Target has calling convention: {} but specified: {} for hook: {}", mismatch_conv.existing, mismatch_conv.incoming, mismatch_conv.installing_hook); }, [&](MismatchMidpoint const& mismatch_midpoint) { - return format_to(ctx.out(), - "Target has midpoint specified as: {} but specified: {} for hook: {}", + return fmt::format_to(ctx.out(), "Target has midpoint specified as: {} but specified: {} for hook: {}", mismatch_midpoint.existing, mismatch_midpoint.incoming, mismatch_midpoint.installing_hook); }, #ifndef FLAMINGO_NO_REGISTRATION_CHECKS [&](MismatchReturn const& mismatch_return) { - return format_to(ctx.out(), - "Target has return type specified as: {} but specified: {} for hook: {}", + return fmt::format_to(ctx.out(), "Target has return type specified as: {} but specified: {} for hook: {}", mismatch_return.existing, mismatch_return.incoming, mismatch_return.installing_hook); }, [&](MismatchParam const& mismatch_param) { - return format_to( - ctx.out(), "Target has parameter {} type specified as: {} but specified: {} for hook: {}", - mismatch_param.idx, mismatch_param.existing, mismatch_param.incoming, mismatch_param.installing_hook); + return fmt::format_to(ctx.out(), "Target has parameter {} type specified as: {} but specified: {} for hook: {}", + mismatch_param.idx, mismatch_param.existing, mismatch_param.incoming, + mismatch_param.installing_hook); }, [&](MismatchParamCount const& mismatch_param_count) { - return format_to(ctx.out(), "Target has {} parameters but specified: {} for hook: {}", + return fmt::format_to(ctx.out(), "Target has {} parameters but specified: {} for hook: {}", mismatch_param_count.existing, mismatch_param_count.incoming, mismatch_param_count.installing_hook); }, diff --git a/shared/hook-metadata.hpp b/shared/hook-metadata.hpp index 68fed90..1c5c1c5 100644 --- a/shared/hook-metadata.hpp +++ b/shared/hook-metadata.hpp @@ -51,7 +51,7 @@ struct HookMetadata { } // namespace flamingo -// Custom formatter for flamingo::CallingConvention +// Custom formatter for flamingo::HookNameMetadata template <> class fmt::formatter { public: @@ -60,6 +60,6 @@ class fmt::formatter { } template constexpr auto format(flamingo::HookNameMetadata const& metadata, Context& ctx) const { - return format_to(ctx.out(), FMT_COMPILE("name: {}"), metadata.name); + return fmt::format_to(ctx.out(), "name: {}", metadata.name); } }; diff --git a/shared/type-info.hpp b/shared/type-info.hpp index 70f34fe..77bd187 100644 --- a/shared/type-info.hpp +++ b/shared/type-info.hpp @@ -45,6 +45,6 @@ class fmt::formatter { } template constexpr auto format(flamingo::TypeInfo const& info, Context& ctx) const { - return format_to(ctx.out(), "(size={})", info.size); + return fmt::format_to(ctx.out(), "(size={})", info.size); } }; From 4cee52d0e8ed6fb7773391395e04ef3c9f3046f6 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Sun, 30 Nov 2025 13:27:19 -0800 Subject: [PATCH 072/134] Workaround fmt bug on 11.0.2 for FMT_COMPILE nesting, test fmt logic for installs --- src/capi.cpp | 2 +- test/api.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/capi.cpp b/src/capi.cpp index 5bfc703..d4beae8 100644 --- a/src/capi.cpp +++ b/src/capi.cpp @@ -241,7 +241,7 @@ FLAMINGO_C_EXPORT FlamingoUninstallResult flamingo_uninstall_hook(FlamingoHookHa FLAMINGO_C_EXPORT_VOID void flamingo_format_error(FlamingoInstallErrorData* error, char* buffer, size_t buffer_size) { auto install_error = reinterpret_cast(error); - auto [out, _] = fmt::format_to_n(buffer, buffer_size - 1, FMT_COMPILE("{}"), *install_error); + auto [out, _] = fmt::format_to_n(buffer, buffer_size - 1, "{}", *install_error); *out = '\0'; // Suffix with a null delete install_error; } diff --git a/test/api.cpp b/test/api.cpp index 7fdfacc..42c6276 100644 --- a/test/api.cpp +++ b/test/api.cpp @@ -44,7 +44,7 @@ void test_simple_hook() { auto result = flamingo::Install( flamingo::HookInfo{ (void (*)())hook_function_to_call, hook_target_far.data(), (void (**)()) nullptr }); if (!result.has_value()) { - ERROR("Installation result failed, index: {}", result.error().index()); + ERROR("Installation result failed, index: {}", result.error()); } // Validate target looks good (should call hook_function_to_call) { From a60d8e672403597129054a4c8aa0b29c94861cbd Mon Sep 17 00:00:00 2001 From: Adam ? Date: Sun, 30 Nov 2025 13:31:40 -0800 Subject: [PATCH 073/134] util: Ensure header only flamingo builds do not attempt to export flamingo symbols --- shared/util.hpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/shared/util.hpp b/shared/util.hpp index 593509f..a2e5281 100644 --- a/shared/util.hpp +++ b/shared/util.hpp @@ -1,12 +1,12 @@ #pragma once #define FLAMINGO_ID "flamingo" -#define FLAMINGO_VERSION "1.0.0" - -#define FLAMINGO_EXPORT __attribute__((visibility("default"))) +#define FLAMINGO_VERSION "1.1.1" #ifdef FLAMINGO_HEADER_ONLY +#define FLAMINGO_EXPORT __attribute__((visibility("hidden"))) + #ifdef ANDROID #include #include @@ -41,6 +41,8 @@ #endif #else // FLAMINGO_HEADER_ONLY +#define FLAMINGO_EXPORT __attribute__((visibility("default"))) + #include #ifdef ANDROID From c066de0de2113a04650eb8efcf33e1d03d5c78d0 Mon Sep 17 00:00:00 2001 From: Adam ? Date: Mon, 1 Dec 2025 00:47:09 -0800 Subject: [PATCH 074/134] util: Fix FLAMINGO_HEADER_ONLY symbol exposal Also fix qpm private dependency on sl2 --- CMakeLists.txt | 2 +- qpm.json | 3 ++- shared/util.hpp | 9 ++++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 74ec4db..80e495b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -110,7 +110,7 @@ else() # TODO: Until paper lets us build without exceptions, we don't use paper. target_link_options(${COMPILE_ID} PRIVATE -Wl,--exclude-libs,ALL) - target_compile_options(${COMPILE_ID} PRIVATE -DFLAMINGO_HEADER_ONLY -fvisibility=hidden -Wall -Wextra -Werror -Wpedantic) + target_compile_options(${COMPILE_ID} PRIVATE -DFLAMINGO_LOG_STANDALONE -fvisibility=hidden -Wall -Wextra -Werror -Wpedantic) target_sources(${COMPILE_ID} PUBLIC ${SOURCE_DIR}/capi.cpp ${SOURCE_DIR}/fixups.cpp ${SOURCE_DIR}/installer.cpp ${SOURCE_DIR}/page-allocator.cpp) diff --git a/qpm.json b/qpm.json index ca3f7fa..96aec8c 100644 --- a/qpm.json +++ b/qpm.json @@ -46,7 +46,8 @@ "id": "scotland2", "versionRange": "^0.1.6", "additionalData": { - "includeQmod": false + "includeQmod": false, + "private": true } } ] diff --git a/shared/util.hpp b/shared/util.hpp index a2e5281..31f7192 100644 --- a/shared/util.hpp +++ b/shared/util.hpp @@ -4,8 +4,12 @@ #define FLAMINGO_VERSION "1.1.1" #ifdef FLAMINGO_HEADER_ONLY - #define FLAMINGO_EXPORT __attribute__((visibility("hidden"))) +#else // FLAMINGO_HEADER_ONLY +#define FLAMINGO_EXPORT __attribute__((visibility("default"))) +#endif + +#if defined(FLAMINGO_LOG_STANDALONE) || defined(FLAMINGO_HEADER_ONLY) #ifdef ANDROID #include @@ -40,8 +44,7 @@ #error "Need logging definitions here, for non-ANDROID, header only support!" #endif -#else // FLAMINGO_HEADER_ONLY -#define FLAMINGO_EXPORT __attribute__((visibility("default"))) +#else // FLAMINGO_LOG_STANDALONE #include From c1631e9f7b025308331f2cc782dbcd789aedb5f1 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Thu, 25 Dec 2025 23:23:42 -0400 Subject: [PATCH 075/134] metadata: Add namespaze field and matching function to HookNameMetadata --- shared/hook-metadata.hpp | 6 ++++++ src/capi.cpp | 3 +++ 2 files changed, 9 insertions(+) diff --git a/shared/hook-metadata.hpp b/shared/hook-metadata.hpp index 1c5c1c5..4b2475b 100644 --- a/shared/hook-metadata.hpp +++ b/shared/hook-metadata.hpp @@ -24,6 +24,12 @@ struct InstallationMetadata { /// Lookups are described using userdata when the HookInfo is made at first. struct HookNameMetadata { std::string name{}; + std::string namespaze{}; + + /// @brief Checks if this name metadata matches another (either by name or namespace) + [[nodiscard]] bool matches(HookNameMetadata const& other) const { + return (name == other.name) || (namespaze == other.namespaze); + } }; /// @brief Represents a priority for how to align hook orderings. Note that a change in priority MAY require a full list diff --git a/src/capi.cpp b/src/capi.cpp index d4beae8..207e482 100644 --- a/src/capi.cpp +++ b/src/capi.cpp @@ -98,6 +98,9 @@ FlamingoUninstallResult convert_uninstall_result(flamingo::Result co FLAMINGO_C_EXPORT FlamingoNameInfo* flamingo_make_name(char const* name_str) { return reinterpret_cast(new flamingo::HookNameMetadata{ .name = name_str }); } +FLAMINGO_C_EXPORT FlamingoNameInfo* flamingo_make_name_namespaced(char const* name_str, char const* namespaze_str) { + return reinterpret_cast(new flamingo::HookNameMetadata{ .name = name_str, .namespaze = namespaze_str }); +} FLAMINGO_C_EXPORT FlamingoHookPriority* flamingo_make_priority(FlamingoNameInfo** before_names, size_t num_befores, FlamingoNameInfo** after_names, size_t num_afters, bool is_final) { From d19e3da8f677119aa8e80cd10ea500cfe21c6217 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Thu, 25 Dec 2025 23:23:54 -0400 Subject: [PATCH 076/134] Basic sorting, time to rewrite to topological graph --- src/installer.cpp | 60 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/src/installer.cpp b/src/installer.cpp index e0e80d7..b085c63 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include "fixups.hpp" @@ -25,6 +26,62 @@ using namespace flamingo; /// @brief The set of all targets hooked. An ordered map so we can perform large-scale walks by doing binary search. inline static std::map targets; +std::priority_queue prioritizeHooks(std::list& hooks) { + auto comp = [](HookInfo& a, HookInfo& b) { + // Simple heuristic: Hooks with more 'befores' have higher priority + return a.metadata.priority.befores.size() < b.metadata.priority.befores.size(); + }; + std::priority_queue, decltype(comp)> pq(comp); + for (auto& hook : hooks) { + pq.push(hook); + } + return pq; +} + +/// @brief Topologically sorts the provided hooks by their priority constraints. +void topological_sort_hooks_by_priority(std::list& hooks) { + if (hooks.empty()) { + return; + } + // Ensure any "final" priority hooks are placed at the end while preserving relative order. + std::vector::iterator> finals; + finals.reserve(std::distance(hooks.begin(), hooks.end())); + for (auto it = hooks.begin(); it != hooks.end(); ++it) { + if (it->metadata.priority.is_final) { + finals.push_back(it); + } + } + for (auto& it : finals) { + hooks.splice(hooks.end(), hooks, it); + } + + // now sort the non-final hooks based on their after/before constraints + std::stable_sort(hooks.begin(), hooks.end(), [](HookInfo const& a, HookInfo const& b) { + // Check if 'a' should come before 'b' + bool a_before_b = std::ranges::any_of(a.metadata.priority.befores, [&](HookNameMetadata const& name) { + return name.matches(b.metadata.name_info); + }); + // Check if 'b' should come before 'a' + bool b_before_a = std::ranges::any_of(b.metadata.priority.befores, [&](HookNameMetadata const& name) { + return name.matches(a.metadata.name_info); + }); + if (a_before_b && !b_before_a) { + return true; + } + if (b_before_a && !a_before_b) { + return false; + } + + if (a_before_b && b_before_a) { + // Conflict detected + FLAMINGO_DEBUG("Conflict in hook priorities between '{}' and '{}'. Maintaining original order.", + a.metadata.name_info, b.metadata.name_info); + } + // If neither or both are true, maintain original order + return false; + }); +} + Result::iterator, installation::TargetBadPriorities> find_suitable_priority_location_for( std::list& hooks, HookMetadata const& hook_to_install) { using ResultT = Result::iterator, installation::TargetBadPriorities>; @@ -42,7 +99,8 @@ Result::iterator, installation::TargetBadPriorities> find_su if (!hooks.empty() && hooks.back().metadata.priority.is_final) { // We cannot install here, we have a conflict return ResultT::Err(installation::TargetBadPriorities{ - hook_to_install, fmt::format("Cannot install a 'final' hook after another 'final' hook with name: {}", hooks.back().metadata.name_info) }); + hook_to_install, fmt::format("Cannot install a 'final' hook after another 'final' hook with name: {}", + hooks.back().metadata.name_info) }); } // Select the end to install at return ResultT::Ok(hooks.end()); From 4c8213993b4f0ccc562e3bfa92dea55ea8000607 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Fri, 26 Dec 2025 01:35:41 -0400 Subject: [PATCH 077/134] Implement proper topological sort (untested) --- shared/hook-metadata.hpp | 5 ++ src/installer.cpp | 136 ++++++++++++++++++++++++++++++--------- 2 files changed, 109 insertions(+), 32 deletions(-) diff --git a/shared/hook-metadata.hpp b/shared/hook-metadata.hpp index 4b2475b..0f16f94 100644 --- a/shared/hook-metadata.hpp +++ b/shared/hook-metadata.hpp @@ -30,6 +30,11 @@ struct HookNameMetadata { [[nodiscard]] bool matches(HookNameMetadata const& other) const { return (name == other.name) || (namespaze == other.namespaze); } + + [[nodiscard]] + bool operator==(HookNameMetadata const& other) const { + return (name == other.name) && (namespaze == other.namespaze); + } }; /// @brief Represents a priority for how to align hook orderings. Note that a change in priority MAY require a full list diff --git a/src/installer.cpp b/src/installer.cpp index b085c63..02acf1c 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "fixups.hpp" #include "hook-data.hpp" @@ -26,17 +27,11 @@ using namespace flamingo; /// @brief The set of all targets hooked. An ordered map so we can perform large-scale walks by doing binary search. inline static std::map targets; -std::priority_queue prioritizeHooks(std::list& hooks) { - auto comp = [](HookInfo& a, HookInfo& b) { - // Simple heuristic: Hooks with more 'befores' have higher priority - return a.metadata.priority.befores.size() < b.metadata.priority.befores.size(); - }; - std::priority_queue, decltype(comp)> pq(comp); - for (auto& hook : hooks) { - pq.push(hook); - } - return pq; -} +struct HookNameMetadataHash { + std::size_t operator()(HookNameMetadata const& k) const { + return std::hash()(k.name) ^ (std::hash()(k.namespaze) << 1); + } +}; /// @brief Topologically sorts the provided hooks by their priority constraints. void topological_sort_hooks_by_priority(std::list& hooks) { @@ -55,31 +50,107 @@ void topological_sort_hooks_by_priority(std::list& hooks) { hooks.splice(hooks.end(), hooks, it); } - // now sort the non-final hooks based on their after/before constraints - std::stable_sort(hooks.begin(), hooks.end(), [](HookInfo const& a, HookInfo const& b) { - // Check if 'a' should come before 'b' - bool a_before_b = std::ranges::any_of(a.metadata.priority.befores, [&](HookNameMetadata const& name) { - return name.matches(b.metadata.name_info); - }); - // Check if 'b' should come before 'a' - bool b_before_a = std::ranges::any_of(b.metadata.priority.befores, [&](HookNameMetadata const& name) { - return name.matches(a.metadata.name_info); - }); - if (a_before_b && !b_before_a) { - return true; + // now build topological graph + // each hook has a requirement to be after certain other hooks in this graph + // we also convert before requirements to after requirements for easier processing + + // build name to iterator map + std::unordered_map::iterator, HookNameMetadataHash> name_to_iterator; + for (auto it = hooks.begin(); it != hooks.end(); ++it) { + name_to_iterator[it->metadata.name_info] = it; + } + + // A before B == B after A + // HookInfo after strings + std::unordered_map, HookNameMetadataHash> graph; + graph.reserve(hooks.size()); + + // + for (auto const& hook : hooks) { + // build afters first + { + auto& afters = graph[hook.metadata.name_info]; + for (auto const& after : hook.metadata.priority.afters) { + afters.push_back(after); + } + } + + // now build from befores + // iterate all hooks, find the ones that match our befores + for (auto const& before : hook.metadata.priority.befores) { + // If we found it, add ourselves to its after list + graph[before].push_back(hook.metadata.name_info); + } + } + + // now topogologically sort in place + // topological sort should use HookNameMetadata.matches(HookNameMetadata const& other) for matching + // we use Kahn's algorithm (https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm) + // 1. Compute indegree of each node + // 2. Initialize a queue with all nodes of indegree 0 + // 3. While the queue is not empty: + // a. Pop a node from the queue, add it to the sorted list + // b. For each of its neighbors, decrease their indegree by 1 + // c. If any neighbor's indegree becomes 0, add it to the queue + std::unordered_map indegree; + for (auto const& [name, _] : graph) { + indegree[name] = 0; + } + + // count incoming edges + for (auto const& [name, afters] : graph) { + for (auto const& dep : afters) { + auto jt = std::ranges::find_if(graph, [&](auto const& p) { return p.first.matches(dep); }); + + if (jt != graph.end()) { + indegree[jt->first]++; + } } - if (b_before_a && !a_before_b) { - return false; + } + + std::queue q; + + // start with indegree 0 + for (auto const& [name, deg] : indegree) { + if (deg == 0) { + q.push(name); + } + } + + // we sort in place + while (!q.empty()) { + auto u = q.front(); + q.pop(); + auto it = name_to_iterator[u]; + + // splice this node to the end of the list in order + hooks.splice(hooks.end(), hooks, it); + + for (auto const& dep : graph[u]) { + auto jt = std::ranges::find_if(graph, [&](auto const& p) { return p.first.matches(dep); }); + + if (jt == graph.end()) { + continue; + } + + if (--indegree[jt->first] == 0) { + q.push(jt->first); + } } + } - if (a_before_b && b_before_a) { - // Conflict detected - FLAMINGO_DEBUG("Conflict in hook priorities between '{}' and '{}'. Maintaining original order.", - a.metadata.name_info, b.metadata.name_info); + // check cycle + // After processing, skip nodes with indegree > 0 (cycles) + for (auto it = hooks.begin(); it != hooks.end();) { + if (indegree[it->metadata.name_info] > 0) { + FLAMINGO_CRITICAL( + "Cycle detected in hook priorities involving hook: {}. Skipping installation of this hook.", + it->metadata.name_info.name.c_str()); + it = hooks.erase(it); // remove from list + } else { + ++it; } - // If neither or both are true, maintain original order - return false; - }); + } } Result::iterator, installation::TargetBadPriorities> find_suitable_priority_location_for( @@ -93,6 +164,7 @@ Result::iterator, installation::TargetBadPriorities> find_su // - First, walk all the hooks for a viable location, if we can find one. If we cannot, then we have to recompile // hooks. // TODO: Above + topological_sort_hooks_by_priority(hooks); // Also, if we have a final priority, we need to be the final hook, unless that hook is itself already marked as // final. if (hook_to_install.priority.is_final) { From 00c2544995fe3292e3150e5958c54959d63b17b6 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Fri, 26 Dec 2025 01:35:54 -0400 Subject: [PATCH 078/134] Add topological sort tests --- CMakeLists.txt | 4 ++ qpm.json | 1 + qpm.shared.json | 36 ++++++----- test.sh | 5 ++ test/sorted-hooks.cpp | 138 ++++++++++++++++++++++++++++++++++++++++++ test/test-wrapper.hpp | 10 +-- 6 files changed, 174 insertions(+), 20 deletions(-) create mode 100644 test.sh create mode 100644 test/sorted-hooks.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 80e495b..88369ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,10 +92,14 @@ if (TEST_BUILD) add_executable(api-test ${CMAKE_CURRENT_SOURCE_DIR}/test/api.cpp) target_link_libraries(api-test PRIVATE flamingo-static) + + add_executable(sorting-test ${CMAKE_CURRENT_SOURCE_DIR}/test/sorted-hooks.cpp) + target_link_libraries(sorting-test PRIVATE flamingo-static) include(CTest) add_test(fixups fixup-test) add_test(apis api-test) + add_test(sorting sorting-test) else() include(qpm_defines.cmake) project(${COMPILE_ID}) diff --git a/qpm.json b/qpm.json index 96aec8c..6eb5ead 100644 --- a/qpm.json +++ b/qpm.json @@ -21,6 +21,7 @@ "pwsh ./build-android-test.ps1" ] }, + "ndk": "^29.0.14206865", "qmodIncludeDirs": [ "./build" ], diff --git a/qpm.shared.json b/qpm.shared.json index 457d5e7..75e4894 100644 --- a/qpm.shared.json +++ b/qpm.shared.json @@ -22,9 +22,12 @@ "pwsh ./build-android-test.ps1" ] }, - "qmodIncludeDirs": [], + "ndk": "^29.0.14206865", + "qmodIncludeDirs": [ + "./build" + ], "qmodIncludeFiles": [], - "qmodOutput": null + "qmodOutput": "flamingo.qmod" }, "dependencies": [ { @@ -45,25 +48,13 @@ "id": "scotland2", "versionRange": "^0.1.6", "additionalData": { - "includeQmod": false + "includeQmod": false, + "private": true } } ] }, "restoredDependencies": [ - { - "dependency": { - "id": "scotland2", - "versionRange": "=0.1.6", - "additionalData": { - "soLink": "https://github.com/sc2ad/scotland2/releases/download/v0.1.6/libsl2.so", - "debugSoLink": "https://github.com/sc2ad/scotland2/releases/download/v0.1.6/debug_libsl2.so", - "overrideSoName": "libsl2.so", - "branchName": "version/v0_1_6" - } - }, - "version": "0.1.6" - }, { "dependency": { "id": "fmt", @@ -94,6 +85,19 @@ } }, "version": "0.1.0" + }, + { + "dependency": { + "id": "scotland2", + "versionRange": "=0.1.7", + "additionalData": { + "soLink": "https://github.com/sc2ad/scotland2/releases/download/v0.1.7/libsl2.so", + "debugSoLink": "https://github.com/sc2ad/scotland2/releases/download/v0.1.7/debug_libsl2.so", + "overrideSoName": "libsl2.so", + "branchName": "version/v0_1_7" + } + }, + "version": "0.1.7" } ] } \ No newline at end of file diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..3ce525b --- /dev/null +++ b/test.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -euo pipefail +CC=clang CXX=clang cmake -B build -DTEST_BUILD=1 -GNinja +ninja -C build +ctest --test-dir build --output-on-failure \ No newline at end of file diff --git a/test/sorted-hooks.cpp b/test/sorted-hooks.cpp new file mode 100644 index 0000000..810dcf0 --- /dev/null +++ b/test/sorted-hooks.cpp @@ -0,0 +1,138 @@ +#include +#include +#include +#include + +#include "calling-convention.hpp" +#include "hook-data.hpp" +#include "hook-metadata.hpp" +#include "installer.hpp" +#include "page-allocator.hpp" +#include "target-data.hpp" +#include "test-wrapper.hpp" + +using namespace flamingo; + +static std::span perform_far_hook_test(uintptr_t hook_location, std::span to_hook) { + std::span hook_span = std::span(reinterpret_cast(&to_hook[0]), + reinterpret_cast(&to_hook[to_hook.size()])); + return alloc_far(flamingo::PointerWrapper(std::span(reinterpret_cast(hook_location), + reinterpret_cast(hook_location)), + flamingo::PageProtectionType::kNone), + hook_span); +} + +static void test_name_matching() { + puts("Test: name matching"); + // Setup + uintptr_t hook_function_A = 0x11110001; + uintptr_t hook_function_B = 0x22220002; + static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8, 0xf6, 0x57, 0x01, 0xa9, 0xf4, 0x4f, + 0x02, 0xa9, 0xfd, 0x7b, 0x03, 0xa9, 0xfd, 0xc3, 0x00, 0x91 }; + + auto hook_target = perform_far_hook_test(hook_function_A, to_hook); + + void* origA = nullptr; + void* origB = nullptr; + + // Install B first, but request that B is installed after A (i.e., B.afters = {A}) + HookNameMetadata nameA; + nameA.name = "A"; + HookNameMetadata nameB; + nameB.name = "B"; + HookPriority priorityB; + priorityB.afters.push_back(nameA); + + flamingo::HookInfo hB((void*)hook_function_B, hook_target.data(), &origB, std::move(nameB), std::move(priorityB)); + auto resB = flamingo::Install(std::move(hB)); + if (!resB.has_value()) ERROR("Failed to install B: {}", resB.error()); + + // Now install A; final ordering should be A then B + HookNameMetadata nmA; + nmA.name = "A"; + HookPriority pA; + flamingo::HookInfo hA((void*)hook_function_A, hook_target.data(), &origA, std::move(nmA), std::move(pA)); + auto resA = flamingo::Install(std::move(hA)); + if (!resA.has_value()) ERROR("Failed to install A: {}", resA.error()); + + // Validate ordering: A should be first (origA == B.hook_ptr), B should be last (origB == fixups) + auto fixup_res = flamingo::FixupPointerFor(flamingo::TargetDescriptor(hook_target.data())); + if (!fixup_res.has_value()) { + ERROR("Failed to get fixup pointer"); + } + void* fixup_ptr = (void*)fixup_res.value().data(); + + if ((uintptr_t)origA != hook_function_B) { + ERROR("Name-matching: expected A.orig == B.hook_ptr (0x{:x}) but got 0x{:x}", hook_function_B, (uintptr_t)origA); + } + if ((uintptr_t)origB != (uintptr_t)fixup_ptr) { + ERROR("Name-matching: expected B.orig == fixups (ptr {}) but got 0x{:x}", fmt::ptr(fixup_ptr), (uintptr_t)origB); + } +} + +static void test_namespaze_matching() { + puts("Test: namespaze matching"); + // Setup three hooks: two in the same namespaze and one that must come before that namespaze + uintptr_t hf1 = 0x33330001; + uintptr_t hf2 = 0x33330002; + uintptr_t prior = 0x44440004; + static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8, 0xf6, 0x57, 0x01, 0xa9, 0xf4, 0x4f }; + + auto hook_target = perform_far_hook_test(hf1, to_hook); + + void* orig1 = nullptr; + void* orig2 = nullptr; + void* orig_prior = nullptr; + + HookNameMetadata n1; + n1.name = "one"; + n1.namespaze = "common"; + HookNameMetadata n2; + n2.name = "two"; + n2.namespaze = "common"; + + // Install first two in the same namespaze + flamingo::HookInfo h1((void*)hf1, hook_target.data(), &orig1, std::move(n1), HookPriority{}); + auto r1 = flamingo::Install(std::move(h1)); + if (!r1.has_value()) ERROR("Failed to install h1: {}", r1.error()); + + HookNameMetadata tmp2; + tmp2.name = "two"; + tmp2.namespaze = "common"; + flamingo::HookInfo h2((void*)hf2, hook_target.data(), &orig2, std::move(tmp2), HookPriority{}); + auto r2 = flamingo::Install(std::move(h2)); + if (!r2.has_value()) ERROR("Failed to install h2: {}", r2.error()); + + // Now install prior that requests to be before the entire namespaze "common" + HookNameMetadata prior_name; + prior_name.name = "prior"; + HookPriority prior_prio; + HookNameMetadata match_ns; + match_ns.namespaze = "common"; + prior_prio.befores.push_back(match_ns); + flamingo::HookInfo hprior((void*)prior, hook_target.data(), &orig_prior, std::move(prior_name), + std::move(prior_prio)); + auto rp = flamingo::Install(std::move(hprior)); + if (!rp.has_value()) ERROR("Failed to install prior: {}", rp.error()); + + // Validate ordering: prior -> hf1 -> hf2 (hf1 and hf2 preserve relative order) + auto fixup_res = flamingo::FixupPointerFor(flamingo::TargetDescriptor(hook_target.data())); + if (!fixup_res.has_value()) ERROR("Failed to get fixup pointer"); + void* fixup_ptr = (void*)fixup_res.value().data(); + + if ((uintptr_t)orig_prior != hf1) { + ERROR("Namespaze-matching: expected prior.orig == hf1 (0x{:x}) but got 0x{:x}", hf1, (uintptr_t)orig_prior); + } + if ((uintptr_t)orig1 != hf2) { + ERROR("Namespaze-matching: expected hf1.orig == hf2 (0x{:x}) but got 0x{:x}", hf2, (uintptr_t)orig1); + } + if ((uintptr_t)orig2 != (uintptr_t)fixup_ptr) { + ERROR("Namespaze-matching: expected hf2.orig == fixup but got 0x{:x}", (uintptr_t)orig2); + } +} + +int main() { + test_name_matching(); + test_namespaze_matching(); + puts("SORTED HOOKS TESTS PASSED"); +} diff --git a/test/test-wrapper.hpp b/test/test-wrapper.hpp index 47136f8..3bba8f4 100644 --- a/test/test-wrapper.hpp +++ b/test/test-wrapper.hpp @@ -29,10 +29,12 @@ // - Dump the trampoline // - Dump the new hook // Perhaps we make TestWrapper take a Fixups instance for this? That way we can do all three of those. -#define ERROR(S, ...) \ - fmt::print(stderr, FMT_COMPILE(S), __VA_ARGS__); \ - fmt::print(stderr, "\n"); \ - std::exit(1); +#define ERROR(S, ...) \ + do { \ + fmt::print(stderr, S, ##__VA_ARGS__); \ + fmt::print(stderr, "\n"); \ + std::exit(1); \ + } while (0) // Converts a pointer to data into the next 64b multiple for use with tests with differing alignments. int64_t round_up8(auto* ptr) { From 3cf8d688423085be5d03861eff3f68dd89131cbc Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Fri, 26 Dec 2025 17:30:28 -0400 Subject: [PATCH 079/134] Redo topological sort based on before ordering --- shared/hook-metadata.hpp | 9 ++- src/installer.cpp | 147 ++++++++++++++++++++++++--------------- 2 files changed, 97 insertions(+), 59 deletions(-) diff --git a/shared/hook-metadata.hpp b/shared/hook-metadata.hpp index 0f16f94..4905cc8 100644 --- a/shared/hook-metadata.hpp +++ b/shared/hook-metadata.hpp @@ -27,6 +27,8 @@ struct HookNameMetadata { std::string namespaze{}; /// @brief Checks if this name metadata matches another (either by name or namespace) + /// @param other The other metadata to check against + /// @return True if either the name or namespace matches [[nodiscard]] bool matches(HookNameMetadata const& other) const { return (name == other.name) || (namespaze == other.namespaze); } @@ -37,13 +39,16 @@ struct HookNameMetadata { } }; +/// Specifies the filter type for hook names in priorities +using HookNameFilter = HookNameMetadata; + /// @brief Represents a priority for how to align hook orderings. Note that a change in priority MAY require a full list /// recreation. But SHOULD NOT require a hook recompile or a trampoline recompile. struct HookPriority { /// @brief The set of constraints for this hook to be installed before (called earlier than) - std::vector befores{}; + std::vector befores{}; /// @brief The set of constraints for this hook to be installed after (called later than) - std::vector afters{}; + std::vector afters{}; /// @brief Set to true if this hook should be the final hook (closest to the original function) bool is_final{false}; }; diff --git a/src/installer.cpp b/src/installer.cpp index 02acf1c..74e3875 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -8,6 +8,9 @@ #include #include #include + +#include + #include "fixups.hpp" #include "hook-data.hpp" #include "hook-installation-result.hpp" @@ -62,95 +65,125 @@ void topological_sort_hooks_by_priority(std::list& hooks) { // A before B == B after A // HookInfo after strings + // this graph represents all the hooks and their before dependencies + // key must be before values std::unordered_map, HookNameMetadataHash> graph; graph.reserve(hooks.size()); + // finds all hooks that match the given filter + auto findMatches = [&](HookNameFilter const& filter, HookNameMetadata self) { + std::vector matches; + matches.reserve(1); + for (auto const& hook : hooks) { + auto const& name = hook.metadata.name_info; + // exclude self + if (name == self) continue; + if (name.matches(filter)) matches.push_back(name); + } + return matches; + }; + // for (auto const& hook : hooks) { // build afters first - { - auto& afters = graph[hook.metadata.name_info]; - for (auto const& after : hook.metadata.priority.afters) { - afters.push_back(after); + // If this hook specifies that it must come after some other hooks (A), + // then we must add edges A -> this_hook in the graph (so that A precedes this). + for (auto const& afterFilter : hook.metadata.priority.afters) { + auto matches = findMatches(afterFilter, hook.metadata.name_info); + for (auto const& matched : matches) { + graph[matched].push_back(hook.metadata.name_info); } } // now build from befores - // iterate all hooks, find the ones that match our befores - for (auto const& before : hook.metadata.priority.befores) { - // If we found it, add ourselves to its after list - graph[before].push_back(hook.metadata.name_info); + // If this hook requests to be before some matched hooks, add edges current -> matched + for (auto const& beforeFilter : hook.metadata.priority.befores) { + auto matches = findMatches(beforeFilter, hook.metadata.name_info); + for (auto const& matched_before : matches) { + graph[hook.metadata.name_info].push_back(matched_before); + } } } // now topogologically sort in place // topological sort should use HookNameMetadata.matches(HookNameMetadata const& other) for matching - // we use Kahn's algorithm (https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm) - // 1. Compute indegree of each node - // 2. Initialize a queue with all nodes of indegree 0 - // 3. While the queue is not empty: - // a. Pop a node from the queue, add it to the sorted list - // b. For each of its neighbors, decrease their indegree by 1 - // c. If any neighbor's indegree becomes 0, add it to the queue - std::unordered_map indegree; - for (auto const& [name, _] : graph) { - indegree[name] = 0; - } - - // count incoming edges + // we cannot invalidate list iterators + // for hooks with cycle, we keep them in their original order + // we use Kahn's algorithm for topological sorting + // https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm + std::list sorted_hooks; + std::unordered_map in_degree; for (auto const& [name, afters] : graph) { - for (auto const& dep : afters) { - auto jt = std::ranges::find_if(graph, [&](auto const& p) { return p.first.matches(dep); }); + // ensure in_degree entry exists with at least 0 + in_degree.try_emplace(name, 0); - if (jt != graph.end()) { - indegree[jt->first]++; - } + for (auto const& after : afters) { + in_degree[after]++; } } - std::queue q; + // Ensure every hook is represented in in_degree (isolated nodes should have degree 0) + for (auto const& [name, itr] : name_to_iterator) { + in_degree.try_emplace(name, 0); + } - // start with indegree 0 - for (auto const& [name, deg] : indegree) { - if (deg == 0) { - q.push(name); + // find all nodes with in_degree 0, preserving the original hooks list order + std::queue zero_in_degree; + for (auto const& hook : hooks) { + auto it_deg = in_degree.find(hook.metadata.name_info); + if (it_deg != in_degree.end() && it_deg->second == 0) { + zero_in_degree.push(hook.metadata.name_info); } } - // we sort in place - while (!q.empty()) { - auto u = q.front(); - q.pop(); - auto it = name_to_iterator[u]; + while (!zero_in_degree.empty()) { + auto current_name = zero_in_degree.front(); + zero_in_degree.pop(); - // splice this node to the end of the list in order - hooks.splice(hooks.end(), hooks, it); - - for (auto const& dep : graph[u]) { - auto jt = std::ranges::find_if(graph, [&](auto const& p) { return p.first.matches(dep); }); - - if (jt == graph.end()) { - continue; - } + // find the iterator for this name + auto it = name_to_iterator.find(current_name); + if (it != name_to_iterator.end()) { + // move to sorted_hooks + sorted_hooks.splice(sorted_hooks.end(), hooks, it->second); + } - if (--indegree[jt->first] == 0) { - q.push(jt->first); + // decrease in_degree of afters + auto const& afters = graph[current_name]; + for (auto const& after : afters) { + in_degree[after]--; + if (in_degree[after] == 0) { + zero_in_degree.push(after); } } } - // check cycle - // After processing, skip nodes with indegree > 0 (cycles) - for (auto it = hooks.begin(); it != hooks.end();) { - if (indegree[it->metadata.name_info] > 0) { - FLAMINGO_CRITICAL( - "Cycle detected in hook priorities involving hook: {}. Skipping installation of this hook.", - it->metadata.name_info.name.c_str()); - it = hooks.erase(it); // remove from list - } else { - ++it; + { + std::vector hook_names; + hook_names.reserve(sorted_hooks.size()); + for (auto const& hook : sorted_hooks) { + hook_names.push_back(hook.metadata.name_info.name); } + FLAMINGO_DEBUG("Flattened hook order after topological sort attempt: {}", fmt::join(hook_names, " -> ")); + } + + // now, any remaining hooks in `hooks` are part of cycles + // append them in their original order and log a warning. Splicing invalidates + // iterators, so consume from the front until empty to preserve original order. + while (!hooks.empty()) { + auto it = hooks.begin(); + auto const& hook = *it; + sorted_hooks.splice(sorted_hooks.end(), hooks, it); + FLAMINGO_CRITICAL( + "Detected cycle in hook priorities involving hook name: {}. Hooks involved in the cycle will remain in their original order.", + hook.metadata.name_info); + FLAMINGO_CRITICAL("After priorities for this hook were: {}", + fmt::join(hook.metadata.priority.afters, ", ")); + FLAMINGO_CRITICAL("Before priorities for this hook were: {}", + fmt::join(hook.metadata.priority.befores, ", ")); } + + // replace original list with the sorted result using swap to avoid reallocation + hooks.swap(sorted_hooks); } Result::iterator, installation::TargetBadPriorities> find_suitable_priority_location_for( From 39c2e86890f154ab8459cea4776c2ddcede9ad02 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Sun, 28 Dec 2025 02:18:23 -0400 Subject: [PATCH 080/134] More hook tests --- test/sorted-hooks.cpp | 203 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 202 insertions(+), 1 deletion(-) diff --git a/test/sorted-hooks.cpp b/test/sorted-hooks.cpp index 810dcf0..9d83043 100644 --- a/test/sorted-hooks.cpp +++ b/test/sorted-hooks.cpp @@ -1,7 +1,15 @@ #include #include +#include +#include +#include #include #include +#include +#include +#include +#include +#include #include "calling-convention.hpp" #include "hook-data.hpp" @@ -103,6 +111,7 @@ static void test_namespaze_matching() { auto r2 = flamingo::Install(std::move(h2)); if (!r2.has_value()) ERROR("Failed to install h2: {}", r2.error()); + fmt::println("Installed hooks hf1=0x{:x}, hf2=0x{:x}\n", hf1, hf2); // Now install prior that requests to be before the entire namespaze "common" HookNameMetadata prior_name; prior_name.name = "prior"; @@ -131,8 +140,200 @@ static void test_namespaze_matching() { } } + + +static void test_priority_cycle() { + puts("Test: priority cycle"); + uintptr_t hx = 0xaaaa0001; + uintptr_t hy = 0xbbbb0002; + static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8, 0xf6, 0x57, 0x01, 0xa9 }; + + auto hook_target = perform_far_hook_test(hx, to_hook); + void* origX = nullptr; + void* origY = nullptr; + + HookNameMetadata nX; nX.name = "X"; + HookNameMetadata nY; nY.name = "Y"; + HookPriority pX; pX.afters.push_back(nY); + HookPriority pY; pY.afters.push_back(nX); + + flamingo::HookInfo hX((void*)hx, hook_target.data(), &origX, std::move(nX), std::move(pX)); + auto rX = flamingo::Install(std::move(hX)); + if (!rX.has_value()) ERROR("Failed to install X: {}", rX.error()); + + flamingo::HookInfo hY((void*)hy, hook_target.data(), &origY, std::move(nY), std::move(pY)); + auto rY = flamingo::Install(std::move(hY)); + if (!rY.has_value()) ERROR("Failed to install Y: {}", rY.error()); + + auto fixup_res = flamingo::FixupPointerFor(flamingo::TargetDescriptor(hook_target.data())); + if (!fixup_res.has_value()) ERROR("Failed to get fixup pointer"); + void* fixup_ptr = (void*)fixup_res.value().data(); + + // Cycle should preserve original install order: X then Y + if ((uintptr_t)origX != hy) { + ERROR("Priority-cycle: expected X.orig == Y.hook_ptr (0x{:x}) but got 0x{:x}", hy, (uintptr_t)origX); + } + if ((uintptr_t)origY != (uintptr_t)fixup_ptr) { + ERROR("Priority-cycle: expected Y.orig == fixups but got 0x{:x}", (uintptr_t)origY); + } +} + +static void test_complex_namespace() { + puts("Test: complex namespace ordering"); + uintptr_t a1 = 0x10010001; + uintptr_t a2 = 0x10010002; + uintptr_t b1 = 0x20020001; + static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8, 0xf6, 0x57 }; + + auto hook_target = perform_far_hook_test(a1, to_hook); + void* origA1 = nullptr; void* origA2 = nullptr; void* origB1 = nullptr; + + HookNameMetadata ma1; ma1.name = "a1"; ma1.namespaze = "alpha"; + HookNameMetadata ma2; ma2.name = "a2"; ma2.namespaze = "alpha"; + HookNameMetadata mb1; mb1.name = "b1"; mb1.namespaze = "beta"; + + flamingo::HookInfo hA1((void*)a1, hook_target.data(), &origA1, std::move(ma1), HookPriority{}); + if (!flamingo::Install(std::move(hA1)).has_value()) ERROR("Failed to install a1"); + + flamingo::HookInfo hA2((void*)a2, hook_target.data(), &origA2, std::move(ma2), HookPriority{}); + if (!flamingo::Install(std::move(hA2)).has_value()) ERROR("Failed to install a2"); + + // b1 requests to be before the entire namespaze "alpha" + HookNameMetadata match_ns; match_ns.namespaze = "alpha"; + HookPriority pB; pB.befores.push_back(match_ns); + flamingo::HookInfo hB1((void*)b1, hook_target.data(), &origB1, std::move(mb1), std::move(pB)); + if (!flamingo::Install(std::move(hB1)).has_value()) ERROR("Failed to install b1"); + + auto fixup_res = flamingo::FixupPointerFor(flamingo::TargetDescriptor(hook_target.data())); + if (!fixup_res.has_value()) ERROR("Failed to get fixup pointer"); + void* fixup_ptr = (void*)fixup_res.value().data(); + + // Expect b1 -> a1 -> a2 + if ((uintptr_t)origB1 != a1) ERROR("Complex-ns: expected b1.orig == a1 (0x{:x}) got 0x{:x}", a1, (uintptr_t)origB1); + if ((uintptr_t)origA1 != a2) ERROR("Complex-ns: expected a1.orig == a2 (0x{:x}) got 0x{:x}", a2, (uintptr_t)origA1); + if ((uintptr_t)origA2 != (uintptr_t)fixup_ptr) ERROR("Complex-ns: expected a2.orig == fixup got 0x{:x}", (uintptr_t)origA2); +} + +static void test_final_conflict() { + puts("Test: final hook conflict"); + uintptr_t f1 = 0x90010001; + uintptr_t f2 = 0x90020002; + static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8 }; + + auto hook_target = perform_far_hook_test(f1, to_hook); + void* origF1 = nullptr; void* origF2 = nullptr; + + HookNameMetadata n1; n1.name = "final1"; + HookPriority p1; p1.is_final = true; + flamingo::HookInfo hF1((void*)f1, hook_target.data(), &origF1, std::move(n1), std::move(p1)); + auto r1 = flamingo::Install(std::move(hF1)); + if (!r1.has_value()) ERROR("Failed to install final1: {}", r1.error()); + + HookNameMetadata n2; n2.name = "final2"; + HookPriority p2; p2.is_final = true; + flamingo::HookInfo hF2((void*)f2, hook_target.data(), &origF2, std::move(n2), std::move(p2)); + auto r2 = flamingo::Install(std::move(hF2)); + if (r2.has_value()) ERROR("Expected second final install to fail but it succeeded"); +} + + +static void test_five_hook_order() { + puts("Test: five-hook priority ordering"); + uintptr_t h1 = 0x50010001; + uintptr_t h2 = 0x50020002; + uintptr_t h3 = 0x50030003; + uintptr_t h4 = 0x50040004; + uintptr_t h5 = 0x50050005; + static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8, 0xf6 }; + + auto hook_target = perform_far_hook_test(h1, to_hook); + void* orig1 = nullptr; void* orig2 = nullptr; void* orig3 = nullptr; void* orig4 = nullptr; void* orig5 = nullptr; + + // Simpler acyclic chain: h1 -> h2 -> h3 -> h4 -> h5 + HookNameMetadata m1; m1.name = "h1"; + HookNameMetadata m2; m2.name = "h2"; + HookNameMetadata m3; m3.name = "h3"; + HookNameMetadata m4; m4.name = "h4"; + HookNameMetadata m5; m5.name = "h5"; + + HookPriority p2; p2.afters.push_back(m1); // h2 after h1 + HookPriority p3; p3.afters.push_back(m2); // h3 after h2 + HookPriority p4; p4.afters.push_back(m3); // h4 after h3 + HookPriority p5; p5.afters.push_back(m4); // h5 after h4 + + // Install in scrambled order to ensure priorities drive final order: 3,5,2,4,1 + flamingo::HookInfo hh3((void*)h3, hook_target.data(), &orig3, std::move(m3), std::move(p3)); + if (!flamingo::Install(std::move(hh3)).has_value()) ERROR("Failed to install h3"); + + flamingo::HookInfo hh5((void*)h5, hook_target.data(), &orig5, std::move(m5), std::move(p5)); + if (!flamingo::Install(std::move(hh5)).has_value()) ERROR("Failed to install h5"); + + flamingo::HookInfo hh2((void*)h2, hook_target.data(), &orig2, std::move(m2), std::move(p2)); + if (!flamingo::Install(std::move(hh2)).has_value()) ERROR("Failed to install h2"); + + flamingo::HookInfo hh4((void*)h4, hook_target.data(), &orig4, std::move(m4), std::move(p4)); + if (!flamingo::Install(std::move(hh4)).has_value()) ERROR("Failed to install h4"); + + flamingo::HookInfo hh1((void*)h1, hook_target.data(), &orig1, std::move(m1), HookPriority{}); + if (!flamingo::Install(std::move(hh1)).has_value()) ERROR("Failed to install h1"); + + auto fixup_res = flamingo::FixupPointerFor(flamingo::TargetDescriptor(hook_target.data())); + if (!fixup_res.has_value()) ERROR("Failed to get fixup pointer"); + void* fixup_ptr = (void*)fixup_res.value().data(); + + // Validate expected chain: reconstruct ordering by following orig pointers and ensure it equals [h1,h2,h3,h4,h5] + std::vector hooks = {h1, h2, h3, h4, h5}; + std::unordered_map orig_map; + orig_map[h1] = (uintptr_t)orig1; + orig_map[h2] = (uintptr_t)orig2; + orig_map[h3] = (uintptr_t)orig3; + orig_map[h4] = (uintptr_t)orig4; + orig_map[h5] = (uintptr_t)orig5; + + // find head: hook address not present in any orig_map values + std::unordered_set pointed; + for (auto const& kv : orig_map) { + if (std::find(hooks.begin(), hooks.end(), kv.second) != hooks.end()) pointed.insert(kv.second); + } + uintptr_t head = 0; + for (auto h : hooks) { + if (pointed.find(h) == pointed.end()) { head = h; break; } + } + if (head == 0) ERROR("5-hook: could not determine head of hook chain"); + + // traverse + std::vector order; + uintptr_t cur = head; + while (true) { + order.push_back(cur); + auto it = orig_map.find(cur); + if (it == orig_map.end()) break; + uintptr_t next = it->second; + if (next == (uintptr_t)fixup_ptr) break; + cur = next; + if (order.size() > hooks.size()) break; + } + + std::vector expected = {h1, h2, h3, h4, h5}; + if (order != expected) { + auto join_hex = [](std::vector const& v){ + std::string out; + for (size_t i=0;i Date: Sun, 28 Dec 2025 02:18:41 -0400 Subject: [PATCH 081/134] More work done for proper hook installations --- src/installer.cpp | 92 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 84 insertions(+), 8 deletions(-) diff --git a/src/installer.cpp b/src/installer.cpp index 74e3875..03890e8 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -53,6 +53,15 @@ void topological_sort_hooks_by_priority(std::list& hooks) { hooks.splice(hooks.end(), hooks, it); } + { + std::vector hook_names; + hook_names.reserve(hooks.size()); + for (auto const& hook : hooks) { + hook_names.push_back(hook.metadata.name_info.name); + } + FLAMINGO_DEBUG("Initial hook order before topological sort: {}", fmt::join(hook_names, " -> ")); + } + // now build topological graph // each hook has a requirement to be after certain other hooks in this graph // we also convert before requirements to after requirements for easier processing @@ -174,16 +183,23 @@ void topological_sort_hooks_by_priority(std::list& hooks) { auto const& hook = *it; sorted_hooks.splice(sorted_hooks.end(), hooks, it); FLAMINGO_CRITICAL( - "Detected cycle in hook priorities involving hook name: {}. Hooks involved in the cycle will remain in their original order.", + "Detected cycle in hook priorities involving hook name: {}. Hooks involved in the cycle will remain in their " + "original order.", hook.metadata.name_info); - FLAMINGO_CRITICAL("After priorities for this hook were: {}", - fmt::join(hook.metadata.priority.afters, ", ")); - FLAMINGO_CRITICAL("Before priorities for this hook were: {}", - fmt::join(hook.metadata.priority.befores, ", ")); + FLAMINGO_CRITICAL("After priorities for this hook were: {}", fmt::join(hook.metadata.priority.afters, ", ")); + FLAMINGO_CRITICAL("Before priorities for this hook were: {}", fmt::join(hook.metadata.priority.befores, ", ")); } // replace original list with the sorted result using swap to avoid reallocation hooks.swap(sorted_hooks); + { + std::vector hook_names; + hook_names.reserve(hooks.size()); + for (auto const& hook : hooks) { + hook_names.push_back(hook.metadata.name_info.name); + } + FLAMINGO_DEBUG("Final hook order after topological sort: {}", fmt::join(hook_names, " -> ")); + } } Result::iterator, installation::TargetBadPriorities> find_suitable_priority_location_for( @@ -197,7 +213,12 @@ Result::iterator, installation::TargetBadPriorities> find_su // - First, walk all the hooks for a viable location, if we can find one. If we cannot, then we have to recompile // hooks. // TODO: Above - topological_sort_hooks_by_priority(hooks); + + // Figure out 3 possible scenarios + // if incoming is final, we must be at the end (unless the end is also final, then error) + // if existing hooks have priority constraints that depend on us, we need to respect those (topologically sort) + // otherwise, we can install at the first suitable location that fits + // Also, if we have a final priority, we need to be the final hook, unless that hook is itself already marked as // final. if (hook_to_install.priority.is_final) { @@ -210,8 +231,63 @@ Result::iterator, installation::TargetBadPriorities> find_su // Select the end to install at return ResultT::Ok(hooks.end()); } - // Otherwise, just install it at the front. - return ResultT::Ok(hooks.begin()); + + // ok now we have a non-final hook + // if existing hooks have priority constraints that depend on us, we need to respect those + // therefore topological + + bool requires_sort = false; + for (auto const& existing_hook : hooks) { + // if existing_hook requests to be after us, we cannot install after it + for (auto const& after_filter : existing_hook.metadata.priority.afters) { + if (after_filter.matches(hook_to_install.name_info)) { + requires_sort = true; + break; + } + // if existing_hook requests to be before us, we cannot install before it + for (auto const& before_filter : existing_hook.metadata.priority.befores) { + if (before_filter.matches(hook_to_install.name_info)) { + requires_sort = true; + break; + } + } + } + } + + // if no priority constraints affect us, we can install at the first suitable location that fits + if (requires_sort) { + // if our hook has priority constraints, we need to topologically sort and find a suitable location + topological_sort_hooks_by_priority(hooks); + + // Otherwise, install at the end to preserve the relative order of previously-installed hooks + FLAMINGO_ABORT("Priority-based installation requiring reordering is not yet implemented"); + } + + // linear search for first suitable location + for (auto it = hooks.begin(); it != hooks.end(); ++it) { + bool can_install_before = true; + // if we want to be after any existing hook, we cannot install before it + for (auto const& after_filter : hook_to_install.priority.afters) { + if (after_filter.matches(hook_to_install.name_info)) { + can_install_before = false; + break; + } + } + if (!can_install_before) { + continue; + } + + // TODO: if we want to be before any existing hook, we cannot install after it + // (this requires lookahead, so we skip for now) + // maybe we can assume this should never happen for now? + + if (can_install_before) { + return ResultT::Ok(it); + } + } + + // If we could not find any suitable location, install at the end + return ResultT::Ok(hooks.end()); } Result validate_install_metadata(TargetMetadata& existing, From c59822629e10dd0604637b700328b64de04d45d3 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 29 Dec 2025 02:20:05 -0400 Subject: [PATCH 082/134] Enhance topological sorting for hook installation by adding priority checks and ensuring relative order is preserved --- src/installer.cpp | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/installer.cpp b/src/installer.cpp index 03890e8..5d2237d 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -37,10 +38,13 @@ struct HookNameMetadataHash { }; /// @brief Topologically sorts the provided hooks by their priority constraints. +/// @param hooks The list of hooks to sort in place. +/// @return An iterator to the place where the new hook can be installed respecting priorities. void topological_sort_hooks_by_priority(std::list& hooks) { if (hooks.empty()) { return; } + // Ensure any "final" priority hooks are placed at the end while preserving relative order. std::vector::iterator> finals; finals.reserve(std::distance(hooks.begin(), hooks.end())); @@ -236,7 +240,8 @@ Result::iterator, installation::TargetBadPriorities> find_su // if existing hooks have priority constraints that depend on us, we need to respect those // therefore topological - bool requires_sort = false; + // If the incoming hook has any priority constraints, we need a topological pass. + bool requires_sort = !hook_to_install.priority.afters.empty() || !hook_to_install.priority.befores.empty(); for (auto const& existing_hook : hooks) { // if existing_hook requests to be after us, we cannot install after it for (auto const& after_filter : existing_hook.metadata.priority.afters) { @@ -256,14 +261,27 @@ Result::iterator, installation::TargetBadPriorities> find_su // if no priority constraints affect us, we can install at the first suitable location that fits if (requires_sort) { + // Insert the new hook first so we can let topo sort place it correctly + HookInfo dumbHook(nullptr, nullptr, nullptr); + dumbHook.metadata = hook_to_install; + auto newIt = hooks.insert(hooks.end(), std::move(dumbHook)); // if our hook has priority constraints, we need to topologically sort and find a suitable location topological_sort_hooks_by_priority(hooks); - // Otherwise, install at the end to preserve the relative order of previously-installed hooks - FLAMINGO_ABORT("Priority-based installation requiring reordering is not yet implemented"); + // find our new location (insert requires the next iterator) + auto newLoc = std::next(newIt); + hooks.erase(newIt); + + return ResultT::Ok(newLoc); + } + + // fast track If the incoming hook has no explicit constraints, append to the end to preserve relative install order. + if (hook_to_install.priority.afters.empty() && hook_to_install.priority.befores.empty()) { + return ResultT::Ok(hooks.end()); } // linear search for first suitable location + // Preserve relative installation order for hooks in the same namespaze: place after last hook in that namespace for (auto it = hooks.begin(); it != hooks.end(); ++it) { bool can_install_before = true; // if we want to be after any existing hook, we cannot install before it From 8dde653a47592fcd337e4d2696d10bb6fe8c83c8 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 29 Dec 2025 13:02:43 -0400 Subject: [PATCH 083/134] Fix namespace hook matching --- shared/hook-metadata.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared/hook-metadata.hpp b/shared/hook-metadata.hpp index 4905cc8..a6ef924 100644 --- a/shared/hook-metadata.hpp +++ b/shared/hook-metadata.hpp @@ -30,7 +30,7 @@ struct HookNameMetadata { /// @param other The other metadata to check against /// @return True if either the name or namespace matches [[nodiscard]] bool matches(HookNameMetadata const& other) const { - return (name == other.name) || (namespaze == other.namespaze); + return *this == other || (!name.empty() && name == other.name) || (!namespaze.empty() && namespaze == other.namespaze); } [[nodiscard]] @@ -76,6 +76,6 @@ class fmt::formatter { } template constexpr auto format(flamingo::HookNameMetadata const& metadata, Context& ctx) const { - return fmt::format_to(ctx.out(), "name: {}", metadata.name); + return fmt::format_to(ctx.out(), "name: {} namespaze {}", metadata.name, metadata.namespaze); } }; From acec361bbd40376f12faa231266f4016b9f396ad Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 29 Dec 2025 13:05:06 -0400 Subject: [PATCH 084/134] Fix topo sort too --- src/installer.cpp | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/installer.cpp b/src/installer.cpp index 5d2237d..209f468 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -90,8 +90,12 @@ void topological_sort_hooks_by_priority(std::list& hooks) { for (auto const& hook : hooks) { auto const& name = hook.metadata.name_info; // exclude self - if (name == self) continue; - if (name.matches(filter)) matches.push_back(name); + if (name == self) { + continue; + } + if (name.matches(filter)) { + matches.push_back(name); + } } return matches; }; @@ -126,18 +130,15 @@ void topological_sort_hooks_by_priority(std::list& hooks) { // https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm std::list sorted_hooks; std::unordered_map in_degree; - for (auto const& [name, afters] : graph) { - // ensure in_degree entry exists with at least 0 - in_degree.try_emplace(name, 0); - - for (auto const& after : afters) { - in_degree[after]++; - } + for (auto const& hook : hooks) { + in_degree[hook.metadata.name_info] = 0; } - // Ensure every hook is represented in in_degree (isolated nodes should have degree 0) - for (auto const& [name, itr] : name_to_iterator) { - in_degree.try_emplace(name, 0); + // compute in-degrees + for (auto const& [name, befores] : graph) { + for (auto const& before : befores) { + in_degree[before]++; + } } // find all nodes with in_degree 0, preserving the original hooks list order @@ -155,17 +156,19 @@ void topological_sort_hooks_by_priority(std::list& hooks) { // find the iterator for this name auto it = name_to_iterator.find(current_name); - if (it != name_to_iterator.end()) { - // move to sorted_hooks - sorted_hooks.splice(sorted_hooks.end(), hooks, it->second); + if (it == name_to_iterator.end()) { + // should not happen + continue; } - + // move to sorted_hooks + sorted_hooks.splice(sorted_hooks.end(), hooks, it->second); + // decrease in_degree of afters - auto const& afters = graph[current_name]; - for (auto const& after : afters) { - in_degree[after]--; - if (in_degree[after] == 0) { - zero_in_degree.push(after); + auto const& befores = graph[current_name]; + for (auto const& before : befores) { + in_degree[before]--; + if (in_degree[before] == 0) { + zero_in_degree.push(before); } } } From 3cf33b217a664bcaa0b67d564266f1af17065486 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 29 Dec 2025 14:31:52 -0400 Subject: [PATCH 085/134] Try to make sense out of this and keep the previous behaviour --- src/installer.cpp | 49 ++++++++++++++++++++--------------------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/src/installer.cpp b/src/installer.cpp index 209f468..6c41713 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -162,7 +162,7 @@ void topological_sort_hooks_by_priority(std::list& hooks) { } // move to sorted_hooks sorted_hooks.splice(sorted_hooks.end(), hooks, it->second); - + // decrease in_degree of afters auto const& befores = graph[current_name]; for (auto const& before : befores) { @@ -243,23 +243,25 @@ Result::iterator, installation::TargetBadPriorities> find_su // if existing hooks have priority constraints that depend on us, we need to respect those // therefore topological - // If the incoming hook has any priority constraints, we need a topological pass. + // If the incoming hook has any priority constraints, we may need a topological pass. bool requires_sort = !hook_to_install.priority.afters.empty() || !hook_to_install.priority.befores.empty(); + + // If any existing hook has constraints that reference the incoming hook, we must sort. for (auto const& existing_hook : hooks) { - // if existing_hook requests to be after us, we cannot install after it for (auto const& after_filter : existing_hook.metadata.priority.afters) { if (after_filter.matches(hook_to_install.name_info)) { requires_sort = true; break; } - // if existing_hook requests to be before us, we cannot install before it - for (auto const& before_filter : existing_hook.metadata.priority.befores) { - if (before_filter.matches(hook_to_install.name_info)) { - requires_sort = true; - break; - } + } + // if existing_hook requests to be before us, we cannot install before it + for (auto const& before_filter : existing_hook.metadata.priority.befores) { + if (before_filter.matches(hook_to_install.name_info)) { + requires_sort = true; + break; } } + if (requires_sort) break; } // if no priority constraints affect us, we can install at the first suitable location that fits @@ -270,6 +272,7 @@ Result::iterator, installation::TargetBadPriorities> find_su auto newIt = hooks.insert(hooks.end(), std::move(dumbHook)); // if our hook has priority constraints, we need to topologically sort and find a suitable location topological_sort_hooks_by_priority(hooks); + // TODO: Recompile hooks after sort // find our new location (insert requires the next iterator) auto newLoc = std::next(newIt); @@ -278,37 +281,27 @@ Result::iterator, installation::TargetBadPriorities> find_su return ResultT::Ok(newLoc); } - // fast track If the incoming hook has no explicit constraints, append to the end to preserve relative install order. + // fast track If the incoming hook has no explicit constraints, insert at the front + // so newer installs are called before earlier ones (preserve expected install semantics). if (hook_to_install.priority.afters.empty() && hook_to_install.priority.befores.empty()) { - return ResultT::Ok(hooks.end()); + return ResultT::Ok(hooks.begin()); } - // linear search for first suitable location - // Preserve relative installation order for hooks in the same namespaze: place after last hook in that namespace + // Linear search for a suitable location: insert before the first existing hook that we should come after. for (auto it = hooks.begin(); it != hooks.end(); ++it) { bool can_install_before = true; - // if we want to be after any existing hook, we cannot install before it for (auto const& after_filter : hook_to_install.priority.afters) { - if (after_filter.matches(hook_to_install.name_info)) { + if (after_filter.matches(it->metadata.name_info)) { can_install_before = false; break; } } - if (!can_install_before) { - continue; - } - - // TODO: if we want to be before any existing hook, we cannot install after it - // (this requires lookahead, so we skip for now) - // maybe we can assume this should never happen for now? - - if (can_install_before) { - return ResultT::Ok(it); - } + if (!can_install_before) continue; + return ResultT::Ok(it); } - // If we could not find any suitable location, install at the end - return ResultT::Ok(hooks.end()); + // If we could not find any suitable location, install at the start + return ResultT::Ok(hooks.begin()); } Result validate_install_metadata(TargetMetadata& existing, From 2e76ab3ae8bf6e40606f042f189aefb037e9d983 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 29 Dec 2025 14:54:55 -0400 Subject: [PATCH 086/134] Implement hook recompilation and fix tests --- src/installer.cpp | 48 ++++++++++++++++++++++++++++++++++++++++--- test/sorted-hooks.cpp | 25 +++++++++++++--------- 2 files changed, 60 insertions(+), 13 deletions(-) diff --git a/src/installer.cpp b/src/installer.cpp index 6c41713..0c4feb3 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -209,8 +209,48 @@ void topological_sort_hooks_by_priority(std::list& hooks) { } } +/// @brief Recompiles the hooks for the given target, updating their orig pointers as needed. +/// @param hooks The list of hooks installed on the target. +/// @param target_info The target descriptor for the target. +void recompile_hooks(std::list& hooks, TargetDescriptor const& target_info) { + // Find the target entry. Note that this assumes the handle is not invalidated. + auto target_entry = targets.find(target_info); + if (target_entry == targets.end()) { + FLAMINGO_ABORT("Recompile hooks called on non-existent target!"); + return; + } + if (hooks.empty()) return; + + // Ensure the target jumps to the first hook. + auto it = hooks.begin(); + if (std::next(it) == hooks.end()) { + // Single hook: target -> hook, hook.orig -> fixups or no_fixups + target_entry->second.fixups.target.WriteJump(it->hook_ptr); + it->assign_orig(target_entry->second.metadata.metadata.need_orig + ? target_entry->second.fixups.fixup_inst_destination.addr.data() + : reinterpret_cast(&no_fixups)); + return; + } + + // Multiple hooks: head, middles, tail + // Head + target_entry->second.fixups.target.WriteJump(it->hook_ptr); + it->assign_orig(std::next(it)->hook_ptr); + + // Middles + for (++it; std::next(it) != hooks.end(); ++it) { + it->assign_orig(std::next(it)->hook_ptr); + } + + // Tail + // 'it' now refers to the last element + it->assign_orig(target_entry->second.metadata.metadata.need_orig + ? target_entry->second.fixups.fixup_inst_destination.addr.data() + : reinterpret_cast(&no_fixups)); +} + Result::iterator, installation::TargetBadPriorities> find_suitable_priority_location_for( - std::list& hooks, HookMetadata const& hook_to_install) { + std::list& hooks, HookMetadata const& hook_to_install, TargetDescriptor const& target) { using ResultT = Result::iterator, installation::TargetBadPriorities>; // Install onto the target, respecting priorities. // Note that we may need to recompile some callbacks/fixups to change things @@ -272,12 +312,14 @@ Result::iterator, installation::TargetBadPriorities> find_su auto newIt = hooks.insert(hooks.end(), std::move(dumbHook)); // if our hook has priority constraints, we need to topologically sort and find a suitable location topological_sort_hooks_by_priority(hooks); - // TODO: Recompile hooks after sort // find our new location (insert requires the next iterator) auto newLoc = std::next(newIt); hooks.erase(newIt); + // now recompile all hooks to ensure orig pointers are correct + recompile_hooks(hooks, target); + return ResultT::Ok(newLoc); } @@ -404,7 +446,7 @@ installation::Result Install(HookInfo&& hook) { return installation::Result::ErrAt(installation_checks.error()); } - auto location_or_err = find_suitable_priority_location_for(hooked_target->second.hooks, hook.metadata); + auto location_or_err = find_suitable_priority_location_for(hooked_target->second.hooks, hook.metadata, target_info); if (!location_or_err.has_value()) { return installation::Result::ErrAt(location_or_err.error()); } diff --git a/test/sorted-hooks.cpp b/test/sorted-hooks.cpp index 9d83043..60238fa 100644 --- a/test/sorted-hooks.cpp +++ b/test/sorted-hooks.cpp @@ -129,14 +129,17 @@ static void test_namespaze_matching() { if (!fixup_res.has_value()) ERROR("Failed to get fixup pointer"); void* fixup_ptr = (void*)fixup_res.value().data(); - if ((uintptr_t)orig_prior != hf1) { - ERROR("Namespaze-matching: expected prior.orig == hf1 (0x{:x}) but got 0x{:x}", hf1, (uintptr_t)orig_prior); + // Newer installs are placed at the front, so when h1 then h2 were installed + // the preserved order is (two -> one). Thus after inserting `prior` before + // the `common` namespace, expected chain is: prior -> two -> one. + if ((uintptr_t)orig_prior != hf2) { + ERROR("Namespaze-matching: expected prior.orig == hf2 (0x{:x}) but got 0x{:x}", hf2, (uintptr_t)orig_prior); } - if ((uintptr_t)orig1 != hf2) { - ERROR("Namespaze-matching: expected hf1.orig == hf2 (0x{:x}) but got 0x{:x}", hf2, (uintptr_t)orig1); + if ((uintptr_t)orig2 != hf1) { + ERROR("Namespaze-matching: expected hf2.orig == hf1 (0x{:x}) but got 0x{:x}", hf1, (uintptr_t)orig2); } - if ((uintptr_t)orig2 != (uintptr_t)fixup_ptr) { - ERROR("Namespaze-matching: expected hf2.orig == fixup but got 0x{:x}", (uintptr_t)orig2); + if ((uintptr_t)orig1 != (uintptr_t)fixup_ptr) { + ERROR("Namespaze-matching: expected hf1.orig == fixup but got 0x{:x}", (uintptr_t)orig1); } } @@ -208,10 +211,12 @@ static void test_complex_namespace() { if (!fixup_res.has_value()) ERROR("Failed to get fixup pointer"); void* fixup_ptr = (void*)fixup_res.value().data(); - // Expect b1 -> a1 -> a2 - if ((uintptr_t)origB1 != a1) ERROR("Complex-ns: expected b1.orig == a1 (0x{:x}) got 0x{:x}", a1, (uintptr_t)origB1); - if ((uintptr_t)origA1 != a2) ERROR("Complex-ns: expected a1.orig == a2 (0x{:x}) got 0x{:x}", a2, (uintptr_t)origA1); - if ((uintptr_t)origA2 != (uintptr_t)fixup_ptr) ERROR("Complex-ns: expected a2.orig == fixup got 0x{:x}", (uintptr_t)origA2); + // Newer installs are at the front; after installing a1 then a2 the preserved + // order is a2 -> a1. Placing b1 before the 'alpha' namespace yields: + // Expect b1 -> a2 -> a1 + if ((uintptr_t)origB1 != a2) ERROR("Complex-ns: expected b1.orig == a2 (0x{:x}) got 0x{:x}", a2, (uintptr_t)origB1); + if ((uintptr_t)origA2 != a1) ERROR("Complex-ns: expected a2.orig == a1 (0x{:x}) got 0x{:x}", a1, (uintptr_t)origA2); + if ((uintptr_t)origA1 != (uintptr_t)fixup_ptr) ERROR("Complex-ns: expected a1.orig == fixup got 0x{:x}", (uintptr_t)origA1); } static void test_final_conflict() { From 38887acc40e8cac9841f34a99071fa3d50f545be Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 29 Dec 2025 15:10:27 -0400 Subject: [PATCH 087/134] Throw error if cycles --- src/installer.cpp | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/installer.cpp b/src/installer.cpp index 0c4feb3..1d19fd4 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -39,10 +39,10 @@ struct HookNameMetadataHash { /// @brief Topologically sorts the provided hooks by their priority constraints. /// @param hooks The list of hooks to sort in place. -/// @return An iterator to the place where the new hook can be installed respecting priorities. -void topological_sort_hooks_by_priority(std::list& hooks) { +/// @return The sorted list of hooks that have cycles. +std::list topological_sort_hooks_by_priority(std::list& hooks) { if (hooks.empty()) { - return; + return {}; } // Ensure any "final" priority hooks are placed at the end while preserving relative order. @@ -185,10 +185,7 @@ void topological_sort_hooks_by_priority(std::list& hooks) { // now, any remaining hooks in `hooks` are part of cycles // append them in their original order and log a warning. Splicing invalidates // iterators, so consume from the front until empty to preserve original order. - while (!hooks.empty()) { - auto it = hooks.begin(); - auto const& hook = *it; - sorted_hooks.splice(sorted_hooks.end(), hooks, it); + for (auto const& hook : hooks) { FLAMINGO_CRITICAL( "Detected cycle in hook priorities involving hook name: {}. Hooks involved in the cycle will remain in their " "original order.", @@ -199,6 +196,7 @@ void topological_sort_hooks_by_priority(std::list& hooks) { // replace original list with the sorted result using swap to avoid reallocation hooks.swap(sorted_hooks); + { std::vector hook_names; hook_names.reserve(hooks.size()); @@ -207,6 +205,9 @@ void topological_sort_hooks_by_priority(std::list& hooks) { } FLAMINGO_DEBUG("Final hook order after topological sort: {}", fmt::join(hook_names, " -> ")); } + + // remaining hooks are cycles + return sorted_hooks; } /// @brief Recompiles the hooks for the given target, updating their orig pointers as needed. @@ -219,7 +220,6 @@ void recompile_hooks(std::list& hooks, TargetDescriptor const& target_ FLAMINGO_ABORT("Recompile hooks called on non-existent target!"); return; } - if (hooks.empty()) return; // Ensure the target jumps to the first hook. auto it = hooks.begin(); @@ -227,8 +227,8 @@ void recompile_hooks(std::list& hooks, TargetDescriptor const& target_ // Single hook: target -> hook, hook.orig -> fixups or no_fixups target_entry->second.fixups.target.WriteJump(it->hook_ptr); it->assign_orig(target_entry->second.metadata.metadata.need_orig - ? target_entry->second.fixups.fixup_inst_destination.addr.data() - : reinterpret_cast(&no_fixups)); + ? target_entry->second.fixups.fixup_inst_destination.addr.data() + : reinterpret_cast(&no_fixups)); return; } @@ -245,8 +245,8 @@ void recompile_hooks(std::list& hooks, TargetDescriptor const& target_ // Tail // 'it' now refers to the last element it->assign_orig(target_entry->second.metadata.metadata.need_orig - ? target_entry->second.fixups.fixup_inst_destination.addr.data() - : reinterpret_cast(&no_fixups)); + ? target_entry->second.fixups.fixup_inst_destination.addr.data() + : reinterpret_cast(&no_fixups)); } Result::iterator, installation::TargetBadPriorities> find_suitable_priority_location_for( @@ -259,7 +259,6 @@ Result::iterator, installation::TargetBadPriorities> find_su // do this) // - First, walk all the hooks for a viable location, if we can find one. If we cannot, then we have to recompile // hooks. - // TODO: Above // Figure out 3 possible scenarios // if incoming is final, we must be at the end (unless the end is also final, then error) @@ -304,19 +303,28 @@ Result::iterator, installation::TargetBadPriorities> find_su if (requires_sort) break; } - // if no priority constraints affect us, we can install at the first suitable location that fits + // if we require a sort, do it then recompile if (requires_sort) { // Insert the new hook first so we can let topo sort place it correctly HookInfo dumbHook(nullptr, nullptr, nullptr); dumbHook.metadata = hook_to_install; auto newIt = hooks.insert(hooks.end(), std::move(dumbHook)); // if our hook has priority constraints, we need to topologically sort and find a suitable location - topological_sort_hooks_by_priority(hooks); + auto cycles = topological_sort_hooks_by_priority(hooks); // find our new location (insert requires the next iterator) auto newLoc = std::next(newIt); hooks.erase(newIt); + if (!cycles.empty()) { + // We have cycles involving our new hook + // Remove our new hook + hooks.erase(newIt); + return ResultT::Err(installation::TargetBadPriorities{ + hook_to_install, fmt::format("Cannot install hook due to cycles in priorities involving hook name: {}", + hooks.back().metadata.name_info) }); + } + // now recompile all hooks to ensure orig pointers are correct recompile_hooks(hooks, target); @@ -329,6 +337,7 @@ Result::iterator, installation::TargetBadPriorities> find_su return ResultT::Ok(hooks.begin()); } + // if no priority constraints affect us, we can install at the first suitable location that fits // Linear search for a suitable location: insert before the first existing hook that we should come after. for (auto it = hooks.begin(); it != hooks.end(); ++it) { bool can_install_before = true; From 97e0197b7bce924c95dfec16f1ce44ebd1c9bfa6 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 29 Dec 2025 15:13:28 -0400 Subject: [PATCH 088/134] Fix segfault --- src/installer.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/installer.cpp b/src/installer.cpp index 1d19fd4..2bf68b5 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -319,10 +319,14 @@ Result::iterator, installation::TargetBadPriorities> find_su if (!cycles.empty()) { // We have cycles involving our new hook // Remove our new hook - hooks.erase(newIt); + std::vector cycle_strings; + for (auto const& hook : cycles) { + cycle_strings.push_back(hook.metadata.name_info.name); + } + return ResultT::Err(installation::TargetBadPriorities{ hook_to_install, fmt::format("Cannot install hook due to cycles in priorities involving hook name: {}", - hooks.back().metadata.name_info) }); + fmt::join(cycle_strings, ",") ) }); } // now recompile all hooks to ensure orig pointers are correct From 8e69adcd0b09a311139c8590f7b30359c79c78a1 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 29 Dec 2025 15:22:25 -0400 Subject: [PATCH 089/134] Fix priority cycle handling in hook installation tests --- test/sorted-hooks.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/test/sorted-hooks.cpp b/test/sorted-hooks.cpp index 60238fa..845fbab 100644 --- a/test/sorted-hooks.cpp +++ b/test/sorted-hooks.cpp @@ -164,20 +164,25 @@ static void test_priority_cycle() { auto rX = flamingo::Install(std::move(hX)); if (!rX.has_value()) ERROR("Failed to install X: {}", rX.error()); + // installing this should trigger a cycle and return an error; the original + // state from after installing X should be retained. flamingo::HookInfo hY((void*)hy, hook_target.data(), &origY, std::move(nY), std::move(pY)); auto rY = flamingo::Install(std::move(hY)); - if (!rY.has_value()) ERROR("Failed to install Y: {}", rY.error()); + if (rY.has_value()) ERROR("Expected Y install to fail due to cycle, but it succeeded"); auto fixup_res = flamingo::FixupPointerFor(flamingo::TargetDescriptor(hook_target.data())); if (!fixup_res.has_value()) ERROR("Failed to get fixup pointer"); void* fixup_ptr = (void*)fixup_res.value().data(); - // Cycle should preserve original install order: X then Y - if ((uintptr_t)origX != hy) { - ERROR("Priority-cycle: expected X.orig == Y.hook_ptr (0x{:x}) but got 0x{:x}", hy, (uintptr_t)origX); + // Since Y failed to install, the original state from after installing X + // must be preserved: X.orig should still point to the fixup pointer, and + // Y should not have been installed (origY stays null). + if ((uintptr_t)origX != (uintptr_t)fixup_ptr) { + ERROR("Priority-cycle: expected X.orig to remain pointing at fixups (0x{:x}) but got 0x{:x}", + (uintptr_t)fixup_ptr, (uintptr_t)origX); } - if ((uintptr_t)origY != (uintptr_t)fixup_ptr) { - ERROR("Priority-cycle: expected Y.orig == fixups but got 0x{:x}", (uintptr_t)origY); + if (origY != nullptr) { + ERROR("Priority-cycle: expected Y not to be installed (origY == nullptr) but got 0x{:x}", (uintptr_t)origY); } } From e6803fec8715f63639a7fb8f0801a09cb79dcf7c Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 29 Dec 2025 15:26:40 -0400 Subject: [PATCH 090/134] Add more tests --- test/sorted-hooks.cpp | 129 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 1 deletion(-) diff --git a/test/sorted-hooks.cpp b/test/sorted-hooks.cpp index 845fbab..c522abe 100644 --- a/test/sorted-hooks.cpp +++ b/test/sorted-hooks.cpp @@ -338,6 +338,130 @@ static void test_five_hook_order() { } } +// Forward declarations for additional edge-case tests + +static void test_no_constraints_multiple() { + puts("Test: no-constraints multiple installs"); + uintptr_t h1 = 0x60010001; + uintptr_t h2 = 0x60020002; + uintptr_t h3 = 0x60030003; + static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8 }; + + auto hook_target = perform_far_hook_test(h1, to_hook); + void* orig1 = nullptr; void* orig2 = nullptr; void* orig3 = nullptr; + + HookNameMetadata m1; m1.name = "h1"; + HookNameMetadata m2; m2.name = "h2"; + HookNameMetadata m3; m3.name = "h3"; + + if (!flamingo::Install(flamingo::HookInfo((void*)h1, hook_target.data(), &orig1, std::move(m1), HookPriority{})).has_value()) + ERROR("Failed to install h1"); + if (!flamingo::Install(flamingo::HookInfo((void*)h2, hook_target.data(), &orig2, std::move(m2), HookPriority{})).has_value()) + ERROR("Failed to install h2"); + if (!flamingo::Install(flamingo::HookInfo((void*)h3, hook_target.data(), &orig3, std::move(m3), HookPriority{})).has_value()) + ERROR("Failed to install h3"); + + auto fixup_res = flamingo::FixupPointerFor(flamingo::TargetDescriptor(hook_target.data())); + if (!fixup_res.has_value()) ERROR("Failed to get fixup pointer"); + void* fixup_ptr = (void*)fixup_res.value().data(); + + std::vector hooks = {h1,h2,h3}; + std::unordered_map orig_map; + orig_map[h1] = (uintptr_t)orig1; + orig_map[h2] = (uintptr_t)orig2; + orig_map[h3] = (uintptr_t)orig3; + + std::unordered_set pointed; + for (auto const& kv : orig_map) { + if (std::find(hooks.begin(), hooks.end(), kv.second) != hooks.end()) pointed.insert(kv.second); + } + uintptr_t head = 0; + for (auto h : hooks) if (pointed.find(h) == pointed.end()) { head = h; break; } + if (head == 0) ERROR("No-constraints: could not determine head"); + + std::vector order; uintptr_t cur = head; + while (true) { + order.push_back(cur); + auto it = orig_map.find(cur); + if (it == orig_map.end()) break; + uintptr_t next = it->second; + if (next == (uintptr_t)fixup_ptr) break; + cur = next; + if (order.size() > hooks.size()) break; + } + + std::vector expected = {h3,h2,h1}; + if (order != expected) ERROR("No-constraints: expected {} but got {}", + fmt::format("0x{:x},0x{:x},0x{:x}", expected[0],expected[1],expected[2]), + fmt::format("0x{:x}", order.empty() ? 0 : order[0])); +} + +static void test_befores_namespace_multiple() { + puts("Test: befores matching multiple in namespace"); + uintptr_t a1 = 0x70010001; + uintptr_t a2 = 0x70020002; + uintptr_t prior = 0x70030003; + static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8 }; + + auto hook_target = perform_far_hook_test(a1, to_hook); + void* origA1 = nullptr; void* origA2 = nullptr; void* origPrior = nullptr; + + HookNameMetadata ma1; ma1.name = "one"; ma1.namespaze = "grp"; + HookNameMetadata ma2; ma2.name = "two"; ma2.namespaze = "grp"; + + if (!flamingo::Install(flamingo::HookInfo((void*)a1, hook_target.data(), &origA1, std::move(ma1), HookPriority{})).has_value()) + ERROR("Failed to install a1"); + if (!flamingo::Install(flamingo::HookInfo((void*)a2, hook_target.data(), &origA2, std::move(ma2), HookPriority{})).has_value()) + ERROR("Failed to install a2"); + + HookNameMetadata prior_name; prior_name.name = "prior"; + HookPriority prior_p; HookNameMetadata match_ns; match_ns.namespaze = "grp"; prior_p.befores.push_back(match_ns); + if (!flamingo::Install(flamingo::HookInfo((void*)prior, hook_target.data(), &origPrior, std::move(prior_name), std::move(prior_p))).has_value()) + ERROR("Failed to install prior"); + + auto fixup_res = flamingo::FixupPointerFor(flamingo::TargetDescriptor(hook_target.data())); + if (!fixup_res.has_value()) ERROR("Failed to get fixup pointer"); + void* fixup_ptr = (void*)fixup_res.value().data(); + + // Expect prior -> a2 -> a1 (newer installs at front) + if ((uintptr_t)origPrior != a2) ERROR("Befores-multi: expected prior.orig == a2 (0x{:x}) got 0x{:x}", a2, (uintptr_t)origPrior); + if ((uintptr_t)origA2 != a1) ERROR("Befores-multi: expected a2.orig == a1 (0x{:x}) got 0x{:x}", a1, (uintptr_t)origA2); + if ((uintptr_t)origA1 != (uintptr_t)fixup_ptr) ERROR("Befores-multi: expected a1.orig == fixup got 0x{:x}", (uintptr_t)origA1); +} + +static void test_afters_namespace_multiple() { + puts("Test: afters matching multiple in namespace"); + uintptr_t g1 = 0x80010001; + uintptr_t g2 = 0x80020002; + uintptr_t late = 0x80030003; + static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8 }; + + auto hook_target = perform_far_hook_test(g1, to_hook); + void* origG1 = nullptr; void* origG2 = nullptr; void* origLate = nullptr; + + HookNameMetadata mg1; mg1.name = "g1"; mg1.namespaze = "grp"; + HookNameMetadata mg2; mg2.name = "g2"; mg2.namespaze = "grp"; + + if (!flamingo::Install(flamingo::HookInfo((void*)g1, hook_target.data(), &origG1, std::move(mg1), HookPriority{})).has_value()) + ERROR("Failed to install g1"); + if (!flamingo::Install(flamingo::HookInfo((void*)g2, hook_target.data(), &origG2, std::move(mg2), HookPriority{})).has_value()) + ERROR("Failed to install g2"); + + HookNameMetadata late_name; late_name.name = "late"; + HookPriority late_p; HookNameMetadata match_ns2; match_ns2.namespaze = "grp"; late_p.afters.push_back(match_ns2); + if (!flamingo::Install(flamingo::HookInfo((void*)late, hook_target.data(), &origLate, std::move(late_name), std::move(late_p))).has_value()) + ERROR("Failed to install late"); + + auto fixup_res = flamingo::FixupPointerFor(flamingo::TargetDescriptor(hook_target.data())); + if (!fixup_res.has_value()) ERROR("Failed to get fixup pointer"); + void* fixup_ptr = (void*)fixup_res.value().data(); + + // Expect g2 -> g1 -> late + if ((uintptr_t)origG2 != g1) ERROR("Afters-multi: expected g2.orig == g1 (0x{:x}) got 0x{:x}", g1, (uintptr_t)origG2); + if ((uintptr_t)origG1 != late) ERROR("Afters-multi: expected g1.orig == late (0x{:x}) got 0x{:x}", late, (uintptr_t)origG1); + if ((uintptr_t)origLate != (uintptr_t)fixup_ptr) ERROR("Afters-multi: expected late.orig == fixup got 0x{:x}", (uintptr_t)origLate); +} + int main() { test_name_matching(); test_namespaze_matching(); @@ -345,5 +469,8 @@ int main() { test_complex_namespace(); test_final_conflict(); test_five_hook_order(); + test_no_constraints_multiple(); + test_befores_namespace_multiple(); + test_afters_namespace_multiple(); puts("SORTED HOOKS TESTS PASSED"); -} \ No newline at end of file +} From 7d5011d9d98451819744e4fd9efd08048be0f293 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 29 Dec 2025 15:26:45 -0400 Subject: [PATCH 091/134] One more test and format --- test/sorted-hooks.cpp | 336 +++++++++++++++++++++++++++++++----------- 1 file changed, 254 insertions(+), 82 deletions(-) diff --git a/test/sorted-hooks.cpp b/test/sorted-hooks.cpp index c522abe..153539c 100644 --- a/test/sorted-hooks.cpp +++ b/test/sorted-hooks.cpp @@ -1,15 +1,15 @@ #include +#include #include #include #include #include #include -#include -#include +#include #include #include -#include -#include +#include +#include #include "calling-convention.hpp" #include "hook-data.hpp" @@ -111,7 +111,7 @@ static void test_namespaze_matching() { auto r2 = flamingo::Install(std::move(h2)); if (!r2.has_value()) ERROR("Failed to install h2: {}", r2.error()); - fmt::println("Installed hooks hf1=0x{:x}, hf2=0x{:x}\n", hf1, hf2); + fmt::println("Installed hooks hf1=0x{:x}, hf2=0x{:x}\n", hf1, hf2); // Now install prior that requests to be before the entire namespaze "common" HookNameMetadata prior_name; prior_name.name = "prior"; @@ -143,8 +143,6 @@ static void test_namespaze_matching() { } } - - static void test_priority_cycle() { puts("Test: priority cycle"); uintptr_t hx = 0xaaaa0001; @@ -155,10 +153,14 @@ static void test_priority_cycle() { void* origX = nullptr; void* origY = nullptr; - HookNameMetadata nX; nX.name = "X"; - HookNameMetadata nY; nY.name = "Y"; - HookPriority pX; pX.afters.push_back(nY); - HookPriority pY; pY.afters.push_back(nX); + HookNameMetadata nX; + nX.name = "X"; + HookNameMetadata nY; + nY.name = "Y"; + HookPriority pX; + pX.afters.push_back(nY); + HookPriority pY; + pY.afters.push_back(nX); flamingo::HookInfo hX((void*)hx, hook_target.data(), &origX, std::move(nX), std::move(pX)); auto rX = flamingo::Install(std::move(hX)); @@ -178,8 +180,8 @@ static void test_priority_cycle() { // must be preserved: X.orig should still point to the fixup pointer, and // Y should not have been installed (origY stays null). if ((uintptr_t)origX != (uintptr_t)fixup_ptr) { - ERROR("Priority-cycle: expected X.orig to remain pointing at fixups (0x{:x}) but got 0x{:x}", - (uintptr_t)fixup_ptr, (uintptr_t)origX); + ERROR("Priority-cycle: expected X.orig to remain pointing at fixups (0x{:x}) but got 0x{:x}", (uintptr_t)fixup_ptr, + (uintptr_t)origX); } if (origY != nullptr) { ERROR("Priority-cycle: expected Y not to be installed (origY == nullptr) but got 0x{:x}", (uintptr_t)origY); @@ -194,11 +196,19 @@ static void test_complex_namespace() { static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8, 0xf6, 0x57 }; auto hook_target = perform_far_hook_test(a1, to_hook); - void* origA1 = nullptr; void* origA2 = nullptr; void* origB1 = nullptr; - - HookNameMetadata ma1; ma1.name = "a1"; ma1.namespaze = "alpha"; - HookNameMetadata ma2; ma2.name = "a2"; ma2.namespaze = "alpha"; - HookNameMetadata mb1; mb1.name = "b1"; mb1.namespaze = "beta"; + void* origA1 = nullptr; + void* origA2 = nullptr; + void* origB1 = nullptr; + + HookNameMetadata ma1; + ma1.name = "a1"; + ma1.namespaze = "alpha"; + HookNameMetadata ma2; + ma2.name = "a2"; + ma2.namespaze = "alpha"; + HookNameMetadata mb1; + mb1.name = "b1"; + mb1.namespaze = "beta"; flamingo::HookInfo hA1((void*)a1, hook_target.data(), &origA1, std::move(ma1), HookPriority{}); if (!flamingo::Install(std::move(hA1)).has_value()) ERROR("Failed to install a1"); @@ -207,8 +217,10 @@ static void test_complex_namespace() { if (!flamingo::Install(std::move(hA2)).has_value()) ERROR("Failed to install a2"); // b1 requests to be before the entire namespaze "alpha" - HookNameMetadata match_ns; match_ns.namespaze = "alpha"; - HookPriority pB; pB.befores.push_back(match_ns); + HookNameMetadata match_ns; + match_ns.namespaze = "alpha"; + HookPriority pB; + pB.befores.push_back(match_ns); flamingo::HookInfo hB1((void*)b1, hook_target.data(), &origB1, std::move(mb1), std::move(pB)); if (!flamingo::Install(std::move(hB1)).has_value()) ERROR("Failed to install b1"); @@ -221,7 +233,8 @@ static void test_complex_namespace() { // Expect b1 -> a2 -> a1 if ((uintptr_t)origB1 != a2) ERROR("Complex-ns: expected b1.orig == a2 (0x{:x}) got 0x{:x}", a2, (uintptr_t)origB1); if ((uintptr_t)origA2 != a1) ERROR("Complex-ns: expected a2.orig == a1 (0x{:x}) got 0x{:x}", a1, (uintptr_t)origA2); - if ((uintptr_t)origA1 != (uintptr_t)fixup_ptr) ERROR("Complex-ns: expected a1.orig == fixup got 0x{:x}", (uintptr_t)origA1); + if ((uintptr_t)origA1 != (uintptr_t)fixup_ptr) + ERROR("Complex-ns: expected a1.orig == fixup got 0x{:x}", (uintptr_t)origA1); } static void test_final_conflict() { @@ -231,22 +244,26 @@ static void test_final_conflict() { static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8 }; auto hook_target = perform_far_hook_test(f1, to_hook); - void* origF1 = nullptr; void* origF2 = nullptr; + void* origF1 = nullptr; + void* origF2 = nullptr; - HookNameMetadata n1; n1.name = "final1"; - HookPriority p1; p1.is_final = true; + HookNameMetadata n1; + n1.name = "final1"; + HookPriority p1; + p1.is_final = true; flamingo::HookInfo hF1((void*)f1, hook_target.data(), &origF1, std::move(n1), std::move(p1)); auto r1 = flamingo::Install(std::move(hF1)); if (!r1.has_value()) ERROR("Failed to install final1: {}", r1.error()); - HookNameMetadata n2; n2.name = "final2"; - HookPriority p2; p2.is_final = true; + HookNameMetadata n2; + n2.name = "final2"; + HookPriority p2; + p2.is_final = true; flamingo::HookInfo hF2((void*)f2, hook_target.data(), &origF2, std::move(n2), std::move(p2)); auto r2 = flamingo::Install(std::move(hF2)); if (r2.has_value()) ERROR("Expected second final install to fail but it succeeded"); } - static void test_five_hook_order() { puts("Test: five-hook priority ordering"); uintptr_t h1 = 0x50010001; @@ -257,19 +274,32 @@ static void test_five_hook_order() { static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8, 0xf6 }; auto hook_target = perform_far_hook_test(h1, to_hook); - void* orig1 = nullptr; void* orig2 = nullptr; void* orig3 = nullptr; void* orig4 = nullptr; void* orig5 = nullptr; + void* orig1 = nullptr; + void* orig2 = nullptr; + void* orig3 = nullptr; + void* orig4 = nullptr; + void* orig5 = nullptr; // Simpler acyclic chain: h1 -> h2 -> h3 -> h4 -> h5 - HookNameMetadata m1; m1.name = "h1"; - HookNameMetadata m2; m2.name = "h2"; - HookNameMetadata m3; m3.name = "h3"; - HookNameMetadata m4; m4.name = "h4"; - HookNameMetadata m5; m5.name = "h5"; - - HookPriority p2; p2.afters.push_back(m1); // h2 after h1 - HookPriority p3; p3.afters.push_back(m2); // h3 after h2 - HookPriority p4; p4.afters.push_back(m3); // h4 after h3 - HookPriority p5; p5.afters.push_back(m4); // h5 after h4 + HookNameMetadata m1; + m1.name = "h1"; + HookNameMetadata m2; + m2.name = "h2"; + HookNameMetadata m3; + m3.name = "h3"; + HookNameMetadata m4; + m4.name = "h4"; + HookNameMetadata m5; + m5.name = "h5"; + + HookPriority p2; + p2.afters.push_back(m1); // h2 after h1 + HookPriority p3; + p3.afters.push_back(m2); // h3 after h2 + HookPriority p4; + p4.afters.push_back(m3); // h4 after h3 + HookPriority p5; + p5.afters.push_back(m4); // h5 after h4 // Install in scrambled order to ensure priorities drive final order: 3,5,2,4,1 flamingo::HookInfo hh3((void*)h3, hook_target.data(), &orig3, std::move(m3), std::move(p3)); @@ -292,7 +322,7 @@ static void test_five_hook_order() { void* fixup_ptr = (void*)fixup_res.value().data(); // Validate expected chain: reconstruct ordering by following orig pointers and ensure it equals [h1,h2,h3,h4,h5] - std::vector hooks = {h1, h2, h3, h4, h5}; + std::vector hooks = { h1, h2, h3, h4, h5 }; std::unordered_map orig_map; orig_map[h1] = (uintptr_t)orig1; orig_map[h2] = (uintptr_t)orig2; @@ -307,7 +337,10 @@ static void test_five_hook_order() { } uintptr_t head = 0; for (auto h : hooks) { - if (pointed.find(h) == pointed.end()) { head = h; break; } + if (pointed.find(h) == pointed.end()) { + head = h; + break; + } } if (head == 0) ERROR("5-hook: could not determine head of hook chain"); @@ -324,11 +357,11 @@ static void test_five_hook_order() { if (order.size() > hooks.size()) break; } - std::vector expected = {h1, h2, h3, h4, h5}; + std::vector expected = { h1, h2, h3, h4, h5 }; if (order != expected) { - auto join_hex = [](std::vector const& v){ + auto join_hex = [](std::vector const& v) { std::string out; - for (size_t i=0;i hooks = {h1,h2,h3}; + std::vector hooks = { h1, h2, h3 }; std::unordered_map orig_map; orig_map[h1] = (uintptr_t)orig1; orig_map[h2] = (uintptr_t)orig2; @@ -376,10 +417,15 @@ static void test_no_constraints_multiple() { if (std::find(hooks.begin(), hooks.end(), kv.second) != hooks.end()) pointed.insert(kv.second); } uintptr_t head = 0; - for (auto h : hooks) if (pointed.find(h) == pointed.end()) { head = h; break; } + for (auto h : hooks) + if (pointed.find(h) == pointed.end()) { + head = h; + break; + } if (head == 0) ERROR("No-constraints: could not determine head"); - std::vector order; uintptr_t cur = head; + std::vector order; + uintptr_t cur = head; while (true) { order.push_back(cur); auto it = orig_map.find(cur); @@ -390,10 +436,11 @@ static void test_no_constraints_multiple() { if (order.size() > hooks.size()) break; } - std::vector expected = {h3,h2,h1}; - if (order != expected) ERROR("No-constraints: expected {} but got {}", - fmt::format("0x{:x},0x{:x},0x{:x}", expected[0],expected[1],expected[2]), - fmt::format("0x{:x}", order.empty() ? 0 : order[0])); + std::vector expected = { h3, h2, h1 }; + if (order != expected) + ERROR("No-constraints: expected {} but got {}", + fmt::format("0x{:x},0x{:x},0x{:x}", expected[0], expected[1], expected[2]), + fmt::format("0x{:x}", order.empty() ? 0 : order[0])); } static void test_befores_namespace_multiple() { @@ -404,19 +451,33 @@ static void test_befores_namespace_multiple() { static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8 }; auto hook_target = perform_far_hook_test(a1, to_hook); - void* origA1 = nullptr; void* origA2 = nullptr; void* origPrior = nullptr; - - HookNameMetadata ma1; ma1.name = "one"; ma1.namespaze = "grp"; - HookNameMetadata ma2; ma2.name = "two"; ma2.namespaze = "grp"; - - if (!flamingo::Install(flamingo::HookInfo((void*)a1, hook_target.data(), &origA1, std::move(ma1), HookPriority{})).has_value()) + void* origA1 = nullptr; + void* origA2 = nullptr; + void* origPrior = nullptr; + + HookNameMetadata ma1; + ma1.name = "one"; + ma1.namespaze = "grp"; + HookNameMetadata ma2; + ma2.name = "two"; + ma2.namespaze = "grp"; + + if (!flamingo::Install(flamingo::HookInfo((void*)a1, hook_target.data(), &origA1, std::move(ma1), HookPriority{})) + .has_value()) ERROR("Failed to install a1"); - if (!flamingo::Install(flamingo::HookInfo((void*)a2, hook_target.data(), &origA2, std::move(ma2), HookPriority{})).has_value()) + if (!flamingo::Install(flamingo::HookInfo((void*)a2, hook_target.data(), &origA2, std::move(ma2), HookPriority{})) + .has_value()) ERROR("Failed to install a2"); - HookNameMetadata prior_name; prior_name.name = "prior"; - HookPriority prior_p; HookNameMetadata match_ns; match_ns.namespaze = "grp"; prior_p.befores.push_back(match_ns); - if (!flamingo::Install(flamingo::HookInfo((void*)prior, hook_target.data(), &origPrior, std::move(prior_name), std::move(prior_p))).has_value()) + HookNameMetadata prior_name; + prior_name.name = "prior"; + HookPriority prior_p; + HookNameMetadata match_ns; + match_ns.namespaze = "grp"; + prior_p.befores.push_back(match_ns); + if (!flamingo::Install( + flamingo::HookInfo((void*)prior, hook_target.data(), &origPrior, std::move(prior_name), std::move(prior_p))) + .has_value()) ERROR("Failed to install prior"); auto fixup_res = flamingo::FixupPointerFor(flamingo::TargetDescriptor(hook_target.data())); @@ -424,9 +485,12 @@ static void test_befores_namespace_multiple() { void* fixup_ptr = (void*)fixup_res.value().data(); // Expect prior -> a2 -> a1 (newer installs at front) - if ((uintptr_t)origPrior != a2) ERROR("Befores-multi: expected prior.orig == a2 (0x{:x}) got 0x{:x}", a2, (uintptr_t)origPrior); - if ((uintptr_t)origA2 != a1) ERROR("Befores-multi: expected a2.orig == a1 (0x{:x}) got 0x{:x}", a1, (uintptr_t)origA2); - if ((uintptr_t)origA1 != (uintptr_t)fixup_ptr) ERROR("Befores-multi: expected a1.orig == fixup got 0x{:x}", (uintptr_t)origA1); + if ((uintptr_t)origPrior != a2) + ERROR("Befores-multi: expected prior.orig == a2 (0x{:x}) got 0x{:x}", a2, (uintptr_t)origPrior); + if ((uintptr_t)origA2 != a1) + ERROR("Befores-multi: expected a2.orig == a1 (0x{:x}) got 0x{:x}", a1, (uintptr_t)origA2); + if ((uintptr_t)origA1 != (uintptr_t)fixup_ptr) + ERROR("Befores-multi: expected a1.orig == fixup got 0x{:x}", (uintptr_t)origA1); } static void test_afters_namespace_multiple() { @@ -437,19 +501,33 @@ static void test_afters_namespace_multiple() { static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8 }; auto hook_target = perform_far_hook_test(g1, to_hook); - void* origG1 = nullptr; void* origG2 = nullptr; void* origLate = nullptr; - - HookNameMetadata mg1; mg1.name = "g1"; mg1.namespaze = "grp"; - HookNameMetadata mg2; mg2.name = "g2"; mg2.namespaze = "grp"; - - if (!flamingo::Install(flamingo::HookInfo((void*)g1, hook_target.data(), &origG1, std::move(mg1), HookPriority{})).has_value()) + void* origG1 = nullptr; + void* origG2 = nullptr; + void* origLate = nullptr; + + HookNameMetadata mg1; + mg1.name = "g1"; + mg1.namespaze = "grp"; + HookNameMetadata mg2; + mg2.name = "g2"; + mg2.namespaze = "grp"; + + if (!flamingo::Install(flamingo::HookInfo((void*)g1, hook_target.data(), &origG1, std::move(mg1), HookPriority{})) + .has_value()) ERROR("Failed to install g1"); - if (!flamingo::Install(flamingo::HookInfo((void*)g2, hook_target.data(), &origG2, std::move(mg2), HookPriority{})).has_value()) + if (!flamingo::Install(flamingo::HookInfo((void*)g2, hook_target.data(), &origG2, std::move(mg2), HookPriority{})) + .has_value()) ERROR("Failed to install g2"); - HookNameMetadata late_name; late_name.name = "late"; - HookPriority late_p; HookNameMetadata match_ns2; match_ns2.namespaze = "grp"; late_p.afters.push_back(match_ns2); - if (!flamingo::Install(flamingo::HookInfo((void*)late, hook_target.data(), &origLate, std::move(late_name), std::move(late_p))).has_value()) + HookNameMetadata late_name; + late_name.name = "late"; + HookPriority late_p; + HookNameMetadata match_ns2; + match_ns2.namespaze = "grp"; + late_p.afters.push_back(match_ns2); + if (!flamingo::Install( + flamingo::HookInfo((void*)late, hook_target.data(), &origLate, std::move(late_name), std::move(late_p))) + .has_value()) ERROR("Failed to install late"); auto fixup_res = flamingo::FixupPointerFor(flamingo::TargetDescriptor(hook_target.data())); @@ -458,8 +536,101 @@ static void test_afters_namespace_multiple() { // Expect g2 -> g1 -> late if ((uintptr_t)origG2 != g1) ERROR("Afters-multi: expected g2.orig == g1 (0x{:x}) got 0x{:x}", g1, (uintptr_t)origG2); - if ((uintptr_t)origG1 != late) ERROR("Afters-multi: expected g1.orig == late (0x{:x}) got 0x{:x}", late, (uintptr_t)origG1); - if ((uintptr_t)origLate != (uintptr_t)fixup_ptr) ERROR("Afters-multi: expected late.orig == fixup got 0x{:x}", (uintptr_t)origLate); + if ((uintptr_t)origG1 != late) + ERROR("Afters-multi: expected g1.orig == late (0x{:x}) got 0x{:x}", late, (uintptr_t)origG1); + if ((uintptr_t)origLate != (uintptr_t)fixup_ptr) + ERROR("Afters-multi: expected late.orig == fixup got 0x{:x}", (uintptr_t)origLate); +} + +static void test_preserve_no_priority_relative_order() { + puts("Test: preserve relative order of no-priority hooks during topo sort"); + uintptr_t a1 = 0x91010001; + uintptr_t a2 = 0x91020002; + uintptr_t a3 = 0x91030003; + uintptr_t d = 0x91040004; + static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8 }; + + auto hook_target = perform_far_hook_test(a1, to_hook); + void* origA1 = nullptr; + void* origA2 = nullptr; + void* origA3 = nullptr; + void* origD = nullptr; + + HookNameMetadata ma1; + ma1.name = "a1"; + ma1.namespaze = "grp"; + HookNameMetadata ma2; + ma2.name = "a2"; + ma2.namespaze = "grp"; + HookNameMetadata ma3; + ma3.name = "a3"; + ma3.namespaze = "grp"; + + if (!flamingo::Install(flamingo::HookInfo((void*)a1, hook_target.data(), &origA1, std::move(ma1), HookPriority{})) + .has_value()) + ERROR("Failed to install a1"); + if (!flamingo::Install(flamingo::HookInfo((void*)a2, hook_target.data(), &origA2, std::move(ma2), HookPriority{})) + .has_value()) + ERROR("Failed to install a2"); + if (!flamingo::Install(flamingo::HookInfo((void*)a3, hook_target.data(), &origA3, std::move(ma3), HookPriority{})) + .has_value()) + ERROR("Failed to install a3"); + + // Now insert D that requests to be before the entire namespaze "grp" which will trigger topo sort + HookNameMetadata md; + md.name = "d"; + HookPriority pd; + HookNameMetadata match_ns; + match_ns.namespaze = "grp"; + pd.befores.push_back(match_ns); + if (!flamingo::Install(flamingo::HookInfo((void*)d, hook_target.data(), &origD, std::move(md), std::move(pd))) + .has_value()) + ERROR("Failed to install d"); + + auto fixup_res = flamingo::FixupPointerFor(flamingo::TargetDescriptor(hook_target.data())); + if (!fixup_res.has_value()) ERROR("Failed to get fixup pointer"); + void* fixup_ptr = (void*)fixup_res.value().data(); + + // Reconstruct ordering starting from head + std::unordered_map orig_map; + orig_map[a1] = (uintptr_t)origA1; + orig_map[a2] = (uintptr_t)origA2; + orig_map[a3] = (uintptr_t)origA3; + orig_map[d] = (uintptr_t)origD; + + // find head + std::vector all = { d, a3, a2, a1 }; + std::unordered_set pointed; + for (auto const& kv : orig_map) { + if (std::ranges::find(all, kv.second) != all.end()) pointed.insert(kv.second); + } + uintptr_t head = 0; + for (auto h : all) + if (pointed.find(h) == pointed.end()) { + head = h; + break; + } + if (head == 0) ERROR("Preserve-relative: could not determine head"); + + std::vector order; + uintptr_t cur = head; + while (true) { + order.push_back(cur); + auto it = orig_map.find(cur); + if (it == orig_map.end()) break; + uintptr_t next = it->second; + if (next == (uintptr_t)fixup_ptr) break; + cur = next; + if (order.size() > all.size()) break; + } + + // Expect D followed by a3 -> a2 -> a1 (a3,a2,a1 relative order preserved) + std::vector expected = { d, a3, a2, a1 }; + if (order != expected) { + ERROR("Preserve-relative: expected {} but got {}", + fmt::format("0x{:x},0x{:x},0x{:x},0x{:x}", expected[0], expected[1], expected[2], expected[3]), + fmt::format("0x{:x}", order.empty() ? 0 : order[0])); + } } int main() { @@ -472,5 +643,6 @@ int main() { test_no_constraints_multiple(); test_befores_namespace_multiple(); test_afters_namespace_multiple(); + test_preserve_no_priority_relative_order(); puts("SORTED HOOKS TESTS PASSED"); } From 0001bbbf9d656293de554807d7ebb6338ca2d2ba Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 29 Dec 2025 15:39:48 -0400 Subject: [PATCH 092/134] Refactor hook installation logic to improve priority handling and simplify insertion process --- src/installer.cpp | 75 ++++++++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/src/installer.cpp b/src/installer.cpp index 2bf68b5..85f67ba 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -41,10 +41,6 @@ struct HookNameMetadataHash { /// @param hooks The list of hooks to sort in place. /// @return The sorted list of hooks that have cycles. std::list topological_sort_hooks_by_priority(std::list& hooks) { - if (hooks.empty()) { - return {}; - } - // Ensure any "final" priority hooks are placed at the end while preserving relative order. std::vector::iterator> finals; finals.reserve(std::distance(hooks.begin(), hooks.end())); @@ -249,8 +245,12 @@ void recompile_hooks(std::list& hooks, TargetDescriptor const& target_ : reinterpret_cast(&no_fixups)); } +/// @brief Finds a suitable location to install the given hook on the target, respecting priority constraints. +/// @param hooks The list of hooks currently installed on the target. +/// @param hook_to_install The hook to install. +/// @return An iterator to the location where the hook was installed, or an error if installation is not possible. Result::iterator, installation::TargetBadPriorities> find_suitable_priority_location_for( - std::list& hooks, HookMetadata const& hook_to_install, TargetDescriptor const& target) { + std::list& hooks, HookInfo&& hook_to_install) { using ResultT = Result::iterator, installation::TargetBadPriorities>; // Install onto the target, respecting priorities. // Note that we may need to recompile some callbacks/fixups to change things @@ -267,15 +267,17 @@ Result::iterator, installation::TargetBadPriorities> find_su // Also, if we have a final priority, we need to be the final hook, unless that hook is itself already marked as // final. - if (hook_to_install.priority.is_final) { + if (hook_to_install.metadata.priority.is_final) { if (!hooks.empty() && hooks.back().metadata.priority.is_final) { // We cannot install here, we have a conflict return ResultT::Err(installation::TargetBadPriorities{ - hook_to_install, fmt::format("Cannot install a 'final' hook after another 'final' hook with name: {}", - hooks.back().metadata.name_info) }); + hook_to_install.metadata, fmt::format("Cannot install a 'final' hook after another 'final' hook with name: {}", + hooks.back().metadata.name_info) }); } // Select the end to install at - return ResultT::Ok(hooks.end()); + + auto new_it = hooks.emplace(hooks.end(), std::move(hook_to_install)); + return ResultT::Ok(new_it); } // ok now we have a non-final hook @@ -283,39 +285,37 @@ Result::iterator, installation::TargetBadPriorities> find_su // therefore topological // If the incoming hook has any priority constraints, we may need a topological pass. - bool requires_sort = !hook_to_install.priority.afters.empty() || !hook_to_install.priority.befores.empty(); + bool requires_sort = + !hook_to_install.metadata.priority.afters.empty() || !hook_to_install.metadata.priority.befores.empty(); // If any existing hook has constraints that reference the incoming hook, we must sort. for (auto const& existing_hook : hooks) { for (auto const& after_filter : existing_hook.metadata.priority.afters) { - if (after_filter.matches(hook_to_install.name_info)) { + if (after_filter.matches(hook_to_install.metadata.name_info)) { requires_sort = true; break; } } // if existing_hook requests to be before us, we cannot install before it for (auto const& before_filter : existing_hook.metadata.priority.befores) { - if (before_filter.matches(hook_to_install.name_info)) { + if (before_filter.matches(hook_to_install.metadata.name_info)) { requires_sort = true; break; } } - if (requires_sort) break; + if (requires_sort) { + break; + } } // if we require a sort, do it then recompile if (requires_sort) { + TargetDescriptor target{ hook_to_install.target }; // Insert the new hook first so we can let topo sort place it correctly - HookInfo dumbHook(nullptr, nullptr, nullptr); - dumbHook.metadata = hook_to_install; - auto newIt = hooks.insert(hooks.end(), std::move(dumbHook)); + auto newIt = hooks.insert(hooks.begin(), std::move(hook_to_install)); // if our hook has priority constraints, we need to topologically sort and find a suitable location auto cycles = topological_sort_hooks_by_priority(hooks); - // find our new location (insert requires the next iterator) - auto newLoc = std::next(newIt); - hooks.erase(newIt); - if (!cycles.empty()) { // We have cycles involving our new hook // Remove our new hook @@ -325,38 +325,44 @@ Result::iterator, installation::TargetBadPriorities> find_su } return ResultT::Err(installation::TargetBadPriorities{ - hook_to_install, fmt::format("Cannot install hook due to cycles in priorities involving hook name: {}", - fmt::join(cycle_strings, ",") ) }); + hook_to_install.metadata, fmt::format("Cannot install hook due to cycles in priorities involving hook name: {}", + fmt::join(cycle_strings, ",")) }); } // now recompile all hooks to ensure orig pointers are correct recompile_hooks(hooks, target); - return ResultT::Ok(newLoc); + return ResultT::Ok(newIt); } // fast track If the incoming hook has no explicit constraints, insert at the front // so newer installs are called before earlier ones (preserve expected install semantics). - if (hook_to_install.priority.afters.empty() && hook_to_install.priority.befores.empty()) { - return ResultT::Ok(hooks.begin()); + if (hook_to_install.metadata.priority.afters.empty() && hook_to_install.metadata.priority.befores.empty()) { + auto newIt = hooks.emplace(hooks.begin(), std::move(hook_to_install)); + return ResultT::Ok(newIt); } // if no priority constraints affect us, we can install at the first suitable location that fits // Linear search for a suitable location: insert before the first existing hook that we should come after. for (auto it = hooks.begin(); it != hooks.end(); ++it) { bool can_install_before = true; - for (auto const& after_filter : hook_to_install.priority.afters) { + for (auto const& after_filter : hook_to_install.metadata.priority.afters) { if (after_filter.matches(it->metadata.name_info)) { can_install_before = false; break; } } - if (!can_install_before) continue; - return ResultT::Ok(it); + if (!can_install_before) { + continue; + } + + auto new_it = hooks.emplace(it, std::move(hook_to_install)); + return ResultT::Ok(new_it); } // If we could not find any suitable location, install at the start - return ResultT::Ok(hooks.begin()); + auto new_it = hooks.emplace(hooks.begin(), std::move(hook_to_install)); + return ResultT::Ok(new_it); } Result validate_install_metadata(TargetMetadata& existing, @@ -459,14 +465,15 @@ installation::Result Install(HookInfo&& hook) { return installation::Result::ErrAt(installation_checks.error()); } - auto location_or_err = find_suitable_priority_location_for(hooked_target->second.hooks, hook.metadata, target_info); + auto location_or_err = find_suitable_priority_location_for(hooked_target->second.hooks, std::move(hook)); if (!location_or_err.has_value()) { return installation::Result::ErrAt(location_or_err.error()); } - auto const location = location_or_err.value(); - // 2. Assuming we found a reasonable location to install, insert our new hook before this location, and then adjust - // those around us to match. - auto const hook_data_result = hooked_target->second.hooks.emplace(location, std::move(hook)); + auto const hook_data_result = location_or_err.value(); + + // TODO: Recompile fixups/origs for all hooks on this target or not? + // recompile_hooks(hooked_target->second.hooks, target_info); + // - This is done by looking to the left and right of our target iterator to insert at: // -- If left does not exist: Rewrite the jump from the target to us; else rewrite the left's orig final jump to us if (hook_data_result == hooked_target->second.hooks.begin()) { From 6e40091f806079c41cbb6fe4f40d0481a054bdd5 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 29 Dec 2025 16:29:35 -0400 Subject: [PATCH 093/134] Add hook management functions and memory management for hook info --- shared/capi.h | 25 ++++++++++++++++++ shared/installer.hpp | 5 ++++ src/capi.cpp | 60 ++++++++++++++++++++++++++++++++++++++++++++ src/installer.cpp | 8 ++++++ 4 files changed, 98 insertions(+) diff --git a/shared/capi.h b/shared/capi.h index da05ffb..06c9d3f 100644 --- a/shared/capi.h +++ b/shared/capi.h @@ -91,6 +91,16 @@ typedef struct FlamingoInstallationMetadata FlamingoInstallationMetadata; /// @brief Opaque pointer around a flamingo::TypeInfo typedef struct FlamingoTypeInfo FlamingoTypeInfo; +/// @brief C representation of a hook entry returned by query APIs. +/// Fields owning strings (`name` and `namespaze`) are allocated by the API +/// and must be freed with `flamingo_free_strings` if non-null. +typedef struct { + void* hook_ptr; ///< Pointer to the hook function + void* orig_ptr; ///< Pointer to the original/trampoline function or NULL + char* name; ///< Nullable, malloc'd C string for the hook's name + char* namespaze; ///< Nullable, malloc'd C string for the hook's namespace +} FlamingoHookInfo; + /// @brief Returned from a call to query if a region is hooked, and what the original instructions at that location are. /// Should not be stored for long-term use, since the lifetime of the result is tied to the lifetime of the hooks at /// this location. @@ -249,6 +259,21 @@ FLAMINGO_C_EXPORT FlamingoUninstallResult flamingo_uninstall_hook(FlamingoHookHa /// exceeding the size provided. FLAMINGO_C_EXPORT_VOID void flamingo_format_error(FlamingoInstallErrorData* error, char* buffer, size_t buffer_size); +/// @brief Returns the number of hooks installed at `target`. Returns 0 if none or if target is not hooked. +FLAMINGO_C_EXPORT size_t flamingo_get_hook_count(uint32_t* target); + +/// @brief Fills the provided `hooks` array with `FlamingoHookInfo` entries for `target`. +/// If `capacity` is smaller than the number of hooks, the function returns the required size but does not write +/// beyond `capacity` elements. +/// The `name` and `namespaze` fields inside each written `FlamingoHookInfo` are allocated with `malloc` and must +/// be freed with `flamingo_free_strings` (pass an array of the `name` pointers or `namespaze` pointers respectively). +FLAMINGO_C_EXPORT size_t flamingo_get_hooks(uint32_t* target, FlamingoHookInfo** hooks); + +/// @brief Frees an array of `FlamingoHookInfo` strings allocated by `flamingo_get_hooks`. +/// @param hooks The array of `FlamingoHookInfo` hooks to free. +/// @param length The length of the `hooks` array. +FLAMINGO_C_EXPORT_VOID void flamingo_free_hooks_array(FlamingoHookInfo* hooks, int length); + #ifdef __cplusplus } #endif diff --git a/shared/installer.hpp b/shared/installer.hpp index 6b75a9d..ddcaa19 100644 --- a/shared/installer.hpp +++ b/shared/installer.hpp @@ -13,6 +13,11 @@ namespace flamingo { constexpr static auto kHookAlignment = 16U; constexpr static auto kNumFixupsPerInst = 4U; +/// @brief Returns the target data flamingo has for a specified target. +/// @param target The target descriptor to query. +/// @return The target data if it exists. +[[nodiscard]] FLAMINGO_EXPORT std::optional TargetDataFor(TargetDescriptor target); + /// @brief To install a hook, we require a constructed HookInfo. We want to hold exclusive ownership, so we require an /// rvalue (we may also forward params?). Because a HookInfo is just data, we go find our TargetInfo that matches our /// target. Then, we attempt to install that HookInfo onto the TargetData, mutating the TargetData (but not invalidating diff --git a/src/capi.cpp b/src/capi.cpp index 207e482..c000a49 100644 --- a/src/capi.cpp +++ b/src/capi.cpp @@ -1,6 +1,8 @@ #include "capi.h" #include #include +#include +#include #include #include #include "calling-convention.hpp" @@ -248,3 +250,61 @@ FLAMINGO_C_EXPORT_VOID void flamingo_format_error(FlamingoInstallErrorData* erro *out = '\0'; // Suffix with a null delete install_error; } + +FLAMINGO_C_EXPORT size_t flamingo_get_hook_count(uint32_t* target) { + auto res = flamingo::TargetDataFor(flamingo::TargetDescriptor{ .target = target }); + if (!res.has_value()) return 0; + return res.value().hooks.size(); +} + +FLAMINGO_C_EXPORT size_t flamingo_get_hooks(uint32_t* target, FlamingoHookInfo** hooks) { + auto res = flamingo::TargetDataFor(flamingo::TargetDescriptor{ .target = target }); + if (!res.has_value()) return 0; + if (hooks == nullptr) return 0; + + auto const& list = res.value().hooks; + + *hooks = reinterpret_cast(std::malloc(sizeof(FlamingoHookInfo) * list.size())); + + size_t index = 0; + for (auto const& hook_info : list) { + auto& out_info = (*hooks)[index]; + out_info.hook_ptr = hook_info.hook_ptr; + out_info.orig_ptr = hook_info.orig_ptr; + + auto const& name_info = hook_info.metadata.name_info; + + if (!name_info.name.empty()) { + out_info.name = reinterpret_cast(std::malloc(name_info.name.size() + 1)); + std::strcpy(out_info.name, name_info.name.c_str()); + } else { + out_info.name = nullptr; + } + if (!name_info.namespaze.empty()) { + out_info.namespaze = reinterpret_cast(std::malloc(name_info.namespaze.size() + 1)); + std::strcpy(out_info.namespaze, name_info.namespaze.c_str()); + } else { + out_info.namespaze = nullptr; + } + index++; + } + + return list.size(); +} + +FLAMINGO_C_EXPORT_VOID void flamingo_free_hooks_array(FlamingoHookInfo* hooks, size_t length) { + if (hooks == nullptr) return; + + // free hooks name and namespaze + for (size_t i = 0; i < length; i++) { + auto& hook_info = hooks[i]; + if (hook_info.name != nullptr) { + std::free(hook_info.name); + } + if (hook_info.namespaze != nullptr) { + std::free(hook_info.namespaze); + } + } + + free(hooks); +} \ No newline at end of file diff --git a/src/installer.cpp b/src/installer.cpp index 85f67ba..fc7884a 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -398,6 +398,14 @@ Result validate_install_metadata(T } // namespace namespace flamingo { +std::optional TargetDataFor(TargetDescriptor target) { + auto it = targets.find(target); + if (it == targets.end()) { + return std::nullopt; + } + return it->second; +} + installation::Result Install(HookInfo&& hook) { // Null targets to install to are prohibited, but null hook functions are allowed (and will most likely cause // horrible crashes when called) From 7ebeff3039398c5a286917367aeec25c3c597c1b Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 29 Dec 2025 16:29:43 -0400 Subject: [PATCH 094/134] Flush instruction cache after writing jump in ShimTarget --- src/fixups.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/fixups.cpp b/src/fixups.cpp index 79e596f..4cb772c 100644 --- a/src/fixups.cpp +++ b/src/fixups.cpp @@ -540,6 +540,10 @@ void ShimTarget::WriteJump(void* address) { // The writer for ensuring correct permissions and also performing the write ProtectionWriter writer(*this); WriteCallback(writer, reinterpret_cast(address)); + + // flush instruction cache + __builtin___clear_cache(reinterpret_cast(addr.data()), + reinterpret_cast(addr.data() + addr.size())); } void ShimTarget::WriteCallback(ProtectionWriter& writer, uint32_t const* target) { From 51d7d058960aad2df004ea181d0e95c7babdcd18 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 29 Dec 2025 16:34:43 -0400 Subject: [PATCH 095/134] Add comment --- src/installer.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/installer.cpp b/src/installer.cpp index fc7884a..b10d2d3 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -217,6 +217,8 @@ void recompile_hooks(std::list& hooks, TargetDescriptor const& target_ return; } +// TODO: Do we need to copy Reinstall logic here? + // Ensure the target jumps to the first hook. auto it = hooks.begin(); if (std::next(it) == hooks.end()) { From 39f9315afbc10765c609a4c7c945060380220a5e Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 29 Dec 2025 17:01:41 -0400 Subject: [PATCH 096/134] Refactor flamingo_get_hooks to accept hooks array and capacity, improving memory management --- shared/capi.h | 9 ++++----- src/capi.cpp | 46 +++++++++++++++++++++------------------------- 2 files changed, 25 insertions(+), 30 deletions(-) diff --git a/shared/capi.h b/shared/capi.h index 06c9d3f..f9fe754 100644 --- a/shared/capi.h +++ b/shared/capi.h @@ -267,12 +267,11 @@ FLAMINGO_C_EXPORT size_t flamingo_get_hook_count(uint32_t* target); /// beyond `capacity` elements. /// The `name` and `namespaze` fields inside each written `FlamingoHookInfo` are allocated with `malloc` and must /// be freed with `flamingo_free_strings` (pass an array of the `name` pointers or `namespaze` pointers respectively). -FLAMINGO_C_EXPORT size_t flamingo_get_hooks(uint32_t* target, FlamingoHookInfo** hooks); +FLAMINGO_C_EXPORT size_t flamingo_get_hooks(uint32_t* target, FlamingoHookInfo* hooks, size_t capacity); -/// @brief Frees an array of `FlamingoHookInfo` strings allocated by `flamingo_get_hooks`. -/// @param hooks The array of `FlamingoHookInfo` hooks to free. -/// @param length The length of the `hooks` array. -FLAMINGO_C_EXPORT_VOID void flamingo_free_hooks_array(FlamingoHookInfo* hooks, int length); +/// @brief Frees the `name` and `namespaze` strings inside an array of `FlamingoHookInfo` returned +/// by `flamingo_get_hooks`. Does NOT free the `hooks` array itself; the caller is responsible for that. +FLAMINGO_C_EXPORT_VOID void flamingo_free_hooks_array(FlamingoHookInfo* hooks, size_t length); #ifdef __cplusplus } diff --git a/src/capi.cpp b/src/capi.cpp index c000a49..e8c0ef2 100644 --- a/src/capi.cpp +++ b/src/capi.cpp @@ -257,39 +257,37 @@ FLAMINGO_C_EXPORT size_t flamingo_get_hook_count(uint32_t* target) { return res.value().hooks.size(); } -FLAMINGO_C_EXPORT size_t flamingo_get_hooks(uint32_t* target, FlamingoHookInfo** hooks) { +FLAMINGO_C_EXPORT size_t flamingo_get_hooks(uint32_t* target, FlamingoHookInfo* hooks, size_t capacity) { auto res = flamingo::TargetDataFor(flamingo::TargetDescriptor{ .target = target }); if (!res.has_value()) return 0; if (hooks == nullptr) return 0; - auto const& list = res.value().hooks; - - *hooks = reinterpret_cast(std::malloc(sizeof(FlamingoHookInfo) * list.size())); - - size_t index = 0; - for (auto const& hook_info : list) { - auto& out_info = (*hooks)[index]; - out_info.hook_ptr = hook_info.hook_ptr; - out_info.orig_ptr = hook_info.orig_ptr; - - auto const& name_info = hook_info.metadata.name_info; - - if (!name_info.name.empty()) { - out_info.name = reinterpret_cast(std::malloc(name_info.name.size() + 1)); - std::strcpy(out_info.name, name_info.name.c_str()); + auto const& hook_list = res.value().hooks; + size_t to_copy = std::min(capacity, hook_list.size()); + + auto it = hook_list.begin(); + for (size_t i = 0; i < to_copy; i++) { + auto& dest = hooks[i]; + dest.hook_ptr = it->hook_ptr; + dest.orig_ptr = it->orig_ptr; + // Copy name + if (it->metadata.name_info.name.empty()) { + dest.name = nullptr; } else { - out_info.name = nullptr; + dest.name = static_cast(std::malloc(it->metadata.name_info.name.size() + 1)); + std::strcpy(dest.name, it->metadata.name_info.name.c_str()); } - if (!name_info.namespaze.empty()) { - out_info.namespaze = reinterpret_cast(std::malloc(name_info.namespaze.size() + 1)); - std::strcpy(out_info.namespaze, name_info.namespaze.c_str()); + // Copy namespaze + if (it->metadata.name_info.namespaze.empty()) { + dest.namespaze = nullptr; } else { - out_info.namespaze = nullptr; + dest.namespaze = static_cast(std::malloc(it->metadata.name_info.namespaze.size() + 1)); + std::strcpy(dest.namespaze, it->metadata.name_info.namespaze.c_str()); } - index++; + ++it; } - return list.size(); + return to_copy; } FLAMINGO_C_EXPORT_VOID void flamingo_free_hooks_array(FlamingoHookInfo* hooks, size_t length) { @@ -305,6 +303,4 @@ FLAMINGO_C_EXPORT_VOID void flamingo_free_hooks_array(FlamingoHookInfo* hooks, s std::free(hook_info.namespaze); } } - - free(hooks); } \ No newline at end of file From f6d0d0984d0660066d7aaffd4a455c6baa20d7f7 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 29 Dec 2025 17:01:57 -0400 Subject: [PATCH 097/134] Enhance Reinstall function to topologically sort hooks by priority and recompile them for correct orig pointers --- shared/installer.hpp | 2 +- src/installer.cpp | 32 ++++++++++++++++++-------------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/shared/installer.hpp b/shared/installer.hpp index ddcaa19..a78633f 100644 --- a/shared/installer.hpp +++ b/shared/installer.hpp @@ -28,7 +28,7 @@ constexpr static auto kNumFixupsPerInst = 4U; /// @brief Called on a target to reinstall all targets present at that location. /// A reinstall is done by re-performing orig fixups at the target, and rewriting a jump to the first hook. -/// All other hooks remain unchanged. +/// Then all hooks are topologically sorted by priority, and recompiled to ensure orig pointers are correct. /// This function returns Ok(true) if all hooks were reinstalled correctly, Ok(false) if there were no hooks to /// reinstall, and Error(...) otherwise. [[nodiscard]] FLAMINGO_EXPORT Result Reinstall(TargetDescriptor target); diff --git a/src/installer.cpp b/src/installer.cpp index b10d2d3..97e6d69 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -217,23 +217,27 @@ void recompile_hooks(std::list& hooks, TargetDescriptor const& target_ return; } -// TODO: Do we need to copy Reinstall logic here? + // TODO: Do we need to copy Reinstall logic here? - // Ensure the target jumps to the first hook. + // Reinstall the orig by calling PerformFixupsAndCallback() again (as needed) + // Perform the write of the jump to the first hook + // Head auto it = hooks.begin(); + target_entry->second.fixups.target.WriteJump(it->hook_ptr); + + // Single hook: target -> hook, hook.orig -> fixups or no_fixups if (std::next(it) == hooks.end()) { - // Single hook: target -> hook, hook.orig -> fixups or no_fixups - target_entry->second.fixups.target.WriteJump(it->hook_ptr); it->assign_orig(target_entry->second.metadata.metadata.need_orig ? target_entry->second.fixups.fixup_inst_destination.addr.data() : reinterpret_cast(&no_fixups)); return; - } + } + + // When multiple hooks, orig is next hook + it->assign_orig(std::next(it)->hook_ptr); + // Multiple hooks: head, middles, tail - // Head - target_entry->second.fixups.target.WriteJump(it->hook_ptr); - it->assign_orig(std::next(it)->hook_ptr); // Middles for (++it; std::next(it) != hooks.end(); ++it) { @@ -512,12 +516,12 @@ Result Reinstall(TargetDescriptor target) { if (itr->second.metadata.metadata.need_orig) { itr->second.fixups.PerformFixupsAndCallback(); } - // Perform the write of the jump to the first hook - itr->second.fixups.target.WriteJump(itr->second.hooks.begin()->hook_ptr); - // Note that we do NOT reconstruct all of the inner hook pointers between each hook. - // This is done as a partial optimization, but at some point we should revisit this (and adjust the docstring comment - // to match) - // TODO: Above + // topo sort hooks by priority + topological_sort_hooks_by_priority(itr->second.hooks); + // Recompile all hooks to ensure orig pointers are correct + recompile_hooks(itr->second.hooks, target); + + return RetType::Ok(true); } From 3305b7f18594a813b719ae2addd0308e2333489e Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 29 Dec 2025 18:04:47 -0400 Subject: [PATCH 098/134] Fix double free in Reinstall --- src/fixups.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fixups.cpp b/src/fixups.cpp index 4cb772c..5b30d05 100644 --- a/src/fixups.cpp +++ b/src/fixups.cpp @@ -632,7 +632,7 @@ void Fixups::PerformFixupsAndCallback() { } // Free the disassembled instructions from before the fixups - cs_free(insns, target.addr.size()); + if (insns != nullptr) cs_free(insns, count); // Now, write the callback after all of our fixups. context.WriteCallback(&target.addr[target.addr.size()]); // After we have written ALL of our fixups initially AND our callback, perform our second pass where we inject From e958f3aea976e24854f9e11892dad92920192b34 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 29 Dec 2025 18:07:22 -0400 Subject: [PATCH 099/134] Add tests for reinstall and uninstall functionality in hook management --- test/sorted-hooks.cpp | 65 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/test/sorted-hooks.cpp b/test/sorted-hooks.cpp index 153539c..0881b21 100644 --- a/test/sorted-hooks.cpp +++ b/test/sorted-hooks.cpp @@ -633,6 +633,69 @@ static void test_preserve_no_priority_relative_order() { } } + +static void test_reinstall() { + puts("Test: reinstall"); + uintptr_t h = 0xA0001001; + static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8 }; + auto hook_target = perform_far_hook_test(h, to_hook); + void* orig = nullptr; + + // Install a single hook + HookNameMetadata m; + m.name = "reinst"; + auto r = flamingo::Install(flamingo::HookInfo((void*)h, hook_target.data(), &orig, std::move(m), HookPriority{})); + if (!r.has_value()) ERROR("Failed to install for reinstall test: {}", r.error()); + + // Reinstall should succeed and preserve orig pointer semantics + auto reins = flamingo::Reinstall(flamingo::TargetDescriptor(hook_target.data())); + if (!reins.has_value()) ERROR("Reinstall failed"); + if (!reins.value()) ERROR("Reinstall reported no hooks reinstalled"); + + auto fixup_res = flamingo::FixupPointerFor(flamingo::TargetDescriptor(hook_target.data())); + if (!fixup_res.has_value()) ERROR("Failed to get fixup pointer after reinstall"); + void* fixup_ptr = (void*)fixup_res.value().data(); + if ((uintptr_t)orig != (uintptr_t)fixup_ptr) ERROR("Reinstall: expected orig == fixup after reinstall"); +} + +static void test_uninstall() { + puts("Test: uninstall"); + uintptr_t h1 = 0xB0010001; + uintptr_t h2 = 0xB0020002; + static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8 }; + auto hook_target = perform_far_hook_test(h1, to_hook); + + void* orig1 = nullptr; + void* orig2 = nullptr; + + auto r1 = flamingo::Install(flamingo::HookInfo((void*)h1, hook_target.data(), &orig1, HookNameMetadata{.name = "u1"}, HookPriority{})); + if (!r1.has_value()) ERROR("Failed to install u1: {}", r1.error()); + auto handle1 = r1.value().returned_handle; + + auto r2 = flamingo::Install(flamingo::HookInfo((void*)h2, hook_target.data(), &orig2, HookNameMetadata{.name = "u2"}, HookPriority{})); + if (!r2.has_value()) ERROR("Failed to install u2: {}", r2.error()); + auto handle2 = r2.value().returned_handle; + + // Uninstall one hook; should succeed and leave the other present + auto un1 = flamingo::Uninstall(handle2); + if (!un1.has_value()) ERROR("Uninstall returned error"); + + auto fixup_res = flamingo::FixupPointerFor(flamingo::TargetDescriptor(hook_target.data())); + if (!fixup_res.has_value()) ERROR("Failed to get fixup pointer after first uninstall"); + void* fixup_ptr = (void*)fixup_res.value().data(); + + // The remaining hook's orig should point to fixup + if ((uintptr_t)orig1 != (uintptr_t)fixup_ptr) ERROR("Uninstall: expected remaining orig to point to fixup"); + + // Now uninstall the last hook and ensure the target is removed (no fixup) + auto un2 = flamingo::Uninstall(handle1); + if (!un2.has_value()) ERROR("Uninstall (last) returned error"); + // After removing last, FixupPointerFor should fail + auto fix_final = flamingo::FixupPointerFor(flamingo::TargetDescriptor(hook_target.data())); + if (fix_final.has_value()) ERROR("Expected no fixup pointer after removing last hook"); +} + + int main() { test_name_matching(); test_namespaze_matching(); @@ -644,5 +707,7 @@ int main() { test_befores_namespace_multiple(); test_afters_namespace_multiple(); test_preserve_no_priority_relative_order(); + test_reinstall(); + test_uninstall(); puts("SORTED HOOKS TESTS PASSED"); } From 5f9e4051c114cbf26bb000d74f94d8ac6aa8ee07 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 29 Dec 2025 18:37:28 -0400 Subject: [PATCH 100/134] Optimize Reinstall function by adjusting hook processing and adding TODO for future improvements --- src/installer.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/installer.cpp b/src/installer.cpp index 97e6d69..33926d1 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -516,12 +516,12 @@ Result Reinstall(TargetDescriptor target) { if (itr->second.metadata.metadata.need_orig) { itr->second.fixups.PerformFixupsAndCallback(); } - // topo sort hooks by priority - topological_sort_hooks_by_priority(itr->second.hooks); - // Recompile all hooks to ensure orig pointers are correct - recompile_hooks(itr->second.hooks, target); - - + // Perform the write of the jump to the first hook + itr->second.fixups.target.WriteJump(itr->second.hooks.begin()->hook_ptr); + // Note that we do NOT reconstruct all of the inner hook pointers between each hook. + // This is done as a partial optimization, but at some point we should revisit this (and adjust the docstring comment + // to match) + // TODO: Above return RetType::Ok(true); } From 8f5f1e7b88d93f7d2063a4e7086ffe2846e777be Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 29 Dec 2025 18:41:07 -0400 Subject: [PATCH 101/134] Update build configuration for test environment by adding build_test directory in .gitignore and modifying test script --- .gitignore | 1 + test.sh | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 5c0a81e..6634061 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,7 @@ extern/ extern.cmake qpm_defines.cmake build/ +build_test build-linux/ *.inc diff --git a/test.sh b/test.sh index 3ce525b..38c8a9e 100644 --- a/test.sh +++ b/test.sh @@ -1,5 +1,5 @@ #!/bin/bash set -euo pipefail -CC=clang CXX=clang cmake -B build -DTEST_BUILD=1 -GNinja -ninja -C build -ctest --test-dir build --output-on-failure \ No newline at end of file +CC=clang CXX=clang cmake -B build_test -DTEST_BUILD=1 -GNinja +ninja -C build_test +ctest --test-dir build_test --output-on-failure \ No newline at end of file From e6ab254b575fa309fe0e2a03574880cf7ad5b8de Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 29 Dec 2025 18:41:53 -0400 Subject: [PATCH 102/134] Preserve original hook order when detecting priority cycles in find_suitable_installation --- src/installer.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/installer.cpp b/src/installer.cpp index 33926d1..1a69bd1 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -316,6 +316,9 @@ Result::iterator, installation::TargetBadPriorities> find_su // if we require a sort, do it then recompile if (requires_sort) { + // copy hooks + auto old_hooks = hooks; + TargetDescriptor target{ hook_to_install.target }; // Insert the new hook first so we can let topo sort place it correctly auto newIt = hooks.insert(hooks.begin(), std::move(hook_to_install)); @@ -330,6 +333,9 @@ Result::iterator, installation::TargetBadPriorities> find_su cycle_strings.push_back(hook.metadata.name_info.name); } + // revert hooks (we need to keep original order) + hooks.swap(old_hooks); + return ResultT::Err(installation::TargetBadPriorities{ hook_to_install.metadata, fmt::format("Cannot install hook due to cycles in priorities involving hook name: {}", fmt::join(cycle_strings, ",")) }); From de7a3c03b2010c699dcf31ed858458ec57e7ab28 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 29 Dec 2025 20:21:24 -0400 Subject: [PATCH 103/134] Do not use moved data --- src/installer.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/installer.cpp b/src/installer.cpp index 1a69bd1..a4f39a3 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -320,6 +320,7 @@ Result::iterator, installation::TargetBadPriorities> find_su auto old_hooks = hooks; TargetDescriptor target{ hook_to_install.target }; + auto metadata = hook_to_install.metadata; // Insert the new hook first so we can let topo sort place it correctly auto newIt = hooks.insert(hooks.begin(), std::move(hook_to_install)); // if our hook has priority constraints, we need to topologically sort and find a suitable location @@ -337,7 +338,7 @@ Result::iterator, installation::TargetBadPriorities> find_su hooks.swap(old_hooks); return ResultT::Err(installation::TargetBadPriorities{ - hook_to_install.metadata, fmt::format("Cannot install hook due to cycles in priorities involving hook name: {}", + metadata, fmt::format("Cannot install hook due to cycles in priorities involving hook name: {}", fmt::join(cycle_strings, ",")) }); } From 378f340532f4cf6c8d092baf50f2987b9b7d621a Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Wed, 31 Dec 2025 15:07:04 -0400 Subject: [PATCH 104/134] Revert dsocstring change --- shared/installer.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/installer.hpp b/shared/installer.hpp index a78633f..ddcaa19 100644 --- a/shared/installer.hpp +++ b/shared/installer.hpp @@ -28,7 +28,7 @@ constexpr static auto kNumFixupsPerInst = 4U; /// @brief Called on a target to reinstall all targets present at that location. /// A reinstall is done by re-performing orig fixups at the target, and rewriting a jump to the first hook. -/// Then all hooks are topologically sorted by priority, and recompiled to ensure orig pointers are correct. +/// All other hooks remain unchanged. /// This function returns Ok(true) if all hooks were reinstalled correctly, Ok(false) if there were no hooks to /// reinstall, and Error(...) otherwise. [[nodiscard]] FLAMINGO_EXPORT Result Reinstall(TargetDescriptor target); From c6c14b7c6a3d300fcb2ddbf90ad03efc3d06db63 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:30:46 -0400 Subject: [PATCH 105/134] Update docstrings accordingly --- shared/capi.h | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/shared/capi.h b/shared/capi.h index f9fe754..6c30f79 100644 --- a/shared/capi.h +++ b/shared/capi.h @@ -93,7 +93,8 @@ typedef struct FlamingoTypeInfo FlamingoTypeInfo; /// @brief C representation of a hook entry returned by query APIs. /// Fields owning strings (`name` and `namespaze`) are allocated by the API -/// and must be freed with `flamingo_free_strings` if non-null. +/// and must be freed with `flamingo_free_hooks_array` (pass the `hooks` array +/// and the number of elements to free). typedef struct { void* hook_ptr; ///< Pointer to the hook function void* orig_ptr; ///< Pointer to the original/trampoline function or NULL @@ -263,10 +264,11 @@ FLAMINGO_C_EXPORT_VOID void flamingo_format_error(FlamingoInstallErrorData* erro FLAMINGO_C_EXPORT size_t flamingo_get_hook_count(uint32_t* target); /// @brief Fills the provided `hooks` array with `FlamingoHookInfo` entries for `target`. -/// If `capacity` is smaller than the number of hooks, the function returns the required size but does not write -/// beyond `capacity` elements. -/// The `name` and `namespaze` fields inside each written `FlamingoHookInfo` are allocated with `malloc` and must -/// be freed with `flamingo_free_strings` (pass an array of the `name` pointers or `namespaze` pointers respectively). +/// If `capacity` is smaller than the number of hooks, the function writes up to `capacity` elements and +/// returns the number of elements written (i.e. the number of `FlamingoHookInfo` structures populated). +/// The `name` and `namespaze` fields inside each written `FlamingoHookInfo` are allocated with `malloc`. +/// To free those strings, call `flamingo_free_hooks_array` passing the same `hooks` pointer and the length +/// equal to the number of entries written (the function's return value). FLAMINGO_C_EXPORT size_t flamingo_get_hooks(uint32_t* target, FlamingoHookInfo* hooks, size_t capacity); /// @brief Frees the `name` and `namespaze` strings inside an array of `FlamingoHookInfo` returned From 0cc5b08c7d4a580cb0b9659d8b88716b85551cb6 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:55:34 -0400 Subject: [PATCH 106/134] Update filter logic and CAPI for consistency --- shared/capi.h | 30 ++++++++++++++++++++++++---- shared/hook-metadata.hpp | 42 ++++++++++++++++++++++++++++++++-------- src/capi.cpp | 27 +++++++++++++++++++------- src/installer.cpp | 6 ++++-- test/sorted-hooks.cpp | 8 ++++---- 5 files changed, 88 insertions(+), 25 deletions(-) diff --git a/shared/capi.h b/shared/capi.h index 6c30f79..e7cd55d 100644 --- a/shared/capi.h +++ b/shared/capi.h @@ -82,6 +82,9 @@ typedef enum { /// @brief Opaque pointer around a flamingo::HookNameMetadata typedef struct FlamingoNameInfo FlamingoNameInfo; +/// @brief Opaque pointer around a flamingo::HookNameFilter +typedef struct FlamingoHookFilter FlamingoHookFilter; + /// @brief Opaque pointer around a flamingo::HookPriority typedef struct FlamingoHookPriority FlamingoHookPriority; @@ -116,20 +119,39 @@ typedef struct { } FlamingoOriginalInstructionsResult; /// @brief Creates a flamingo::HookNameMetadata from the provided parameters. The return is an opaque pointer. +/// @param name_str Nullable, C string for the hook's name (may be NULL or empty for no name). +/// @return An opaque pointer to a FlamingoNameInfo. The returned pointer's lifetime is until it is consumed by a call +/// to flamingo_install_hook*, flamingo_make_priority, or any API that takes ownership of FlamingoNameInfo*. /// The returned pointer's lifetime is until a different flamingo API call is made that CONSUMES the FlamingoNameInfo*. -/// This is primarily used to give hooks names and to describe priorities for installation. +/// This is primarily used to give hooks names for installation. /// The lifetime of the result is until it is consumed by a call to flamingo_install_hook*, or flamingo_make_priority. FLAMINGO_C_EXPORT FlamingoNameInfo* flamingo_make_name(char const* name_str); +/// @brief Creates a flamingo::HookNameMetadata from the provided parameters. The return is an opaque pointer. +/// @param namespaze_str Nullable, C string for the hook's namespace (may be NULL or empty for no namespace). +/// @param name_str Nullable, C string for the hook's name (may be NULL or empty for no name). +/// @return An opaque pointer to a FlamingoNameInfo. The returned pointer's lifetime is until it is consumed by a call +/// The returned pointer's lifetime is until a different flamingo API call is made that CONSUMES the FlamingoNameInfo*. +/// This is primarily used to give hooks names for installation. +/// The lifetime of the result is until it is consumed by a call to flamingo_install_hook*, or flamingo_make_priority. +FLAMINGO_C_EXPORT FlamingoNameInfo* flamingo_make_name_namespaced(char const* namespaze_str, char const* name_str); + +/// @brief Creates a flamingo::HookNameFilter from the provided namespace and name strings. +/// @param namespaze_str Nullable, C string for the filter's namespace (may be NULL or empty for no namespace filter). Allows matching any namespace if null or empty. +/// @param name_str Nullable, C string for the filter's name (may be NULL or empty for no name filter). Allows matching any name if null or empty. +/// @return An opaque pointer to a FlamingoHookFilter. The returned pointer's lifetime is until it is consumed by a call to +/// flamingo_make_priority or any API that takes ownership of FlamingoHookFilter*. This is primarily used to describe priorities for installation. +FLAMINGO_C_EXPORT FlamingoHookFilter* flamingo_make_filter(char const* namespaze_str, char const* name_str); + /// @brief Creates a flamingo::HookMetadata from the provided parameters. -/// The parameters are arrays of FlamingoNameInfo that must be dereferencable up to num_befores and num_afters +/// The parameters are arrays of FlamingoHookFilter that must be dereferencable up to num_befores and num_afters /// respectively. The parameters are CONSUMED, that is, the pointers are no longer valid after this API call. This is /// used to give hooks priority information in flamingo_install_hook_full* /// @param is_final Whether this hook should be the final hook (closest to the original function). This takes precedence /// over all other priorities. /// The lifetime of the result is until it is consumed by a call to flamingo_install_hook*. -FLAMINGO_C_EXPORT FlamingoHookPriority* flamingo_make_priority(FlamingoNameInfo** before_names, size_t num_befores, - FlamingoNameInfo** after_names, size_t num_afters, +FLAMINGO_C_EXPORT FlamingoHookPriority* flamingo_make_priority(FlamingoHookFilter** before_names, size_t num_befores, + FlamingoHookFilter** after_names, size_t num_afters, bool is_final); /// @brief Creates a flamingo::InstallationMetadata from the provided parameters. diff --git a/shared/hook-metadata.hpp b/shared/hook-metadata.hpp index a6ef924..be547e1 100644 --- a/shared/hook-metadata.hpp +++ b/shared/hook-metadata.hpp @@ -26,13 +26,6 @@ struct HookNameMetadata { std::string name{}; std::string namespaze{}; - /// @brief Checks if this name metadata matches another (either by name or namespace) - /// @param other The other metadata to check against - /// @return True if either the name or namespace matches - [[nodiscard]] bool matches(HookNameMetadata const& other) const { - return *this == other || (!name.empty() && name == other.name) || (!namespaze.empty() && namespaze == other.namespaze); - } - [[nodiscard]] bool operator==(HookNameMetadata const& other) const { return (name == other.name) && (namespaze == other.namespaze); @@ -40,7 +33,29 @@ struct HookNameMetadata { }; /// Specifies the filter type for hook names in priorities -using HookNameFilter = HookNameMetadata; +struct HookNameFilter { + std::optional namespaze{}; + std::optional name{}; + + explicit HookNameFilter() = default; + explicit HookNameFilter(std::string namespaze) : namespaze(std::move(namespaze)) {} + explicit HookNameFilter(std::string namespaze, std::string name) : namespaze(std::move(namespaze)), name(std::move(name)) {} + + /// @brief Construct a filter that matches the provided metadata. This is used for constructing filters from userdata. + explicit HookNameFilter(HookNameMetadata const& metadata) : namespaze(metadata.namespaze), name(metadata.name) {} + + /// @brief Checks if the provided metadata matches this filter. + // A filter with no fields set matches everything. + [[nodiscard]] bool matches(HookNameMetadata const& metadata) const { + if (name.has_value() && name.value() != metadata.name) { + return false; + } + if (namespaze.has_value() && namespaze.value() != metadata.namespaze) { + return false; + } + return true; + } +}; /// @brief Represents a priority for how to align hook orderings. Note that a change in priority MAY require a full list /// recreation. But SHOULD NOT require a hook recompile or a trampoline recompile. @@ -79,3 +94,14 @@ class fmt::formatter { return fmt::format_to(ctx.out(), "name: {} namespaze {}", metadata.name, metadata.namespaze); } }; +template <> +class fmt::formatter { + public: + constexpr auto parse(format_parse_context& ctx) { + return ctx.begin(); + } + template + constexpr auto format(flamingo::HookNameFilter const& filter, Context& ctx) const { + return fmt::format_to(ctx.out(), "name: {} namespaze {}", filter.name.value_or("*"), filter.namespaze.value_or("*")); + } +}; diff --git a/src/capi.cpp b/src/capi.cpp index e8c0ef2..cbb3327 100644 --- a/src/capi.cpp +++ b/src/capi.cpp @@ -100,24 +100,37 @@ FlamingoUninstallResult convert_uninstall_result(flamingo::Result co FLAMINGO_C_EXPORT FlamingoNameInfo* flamingo_make_name(char const* name_str) { return reinterpret_cast(new flamingo::HookNameMetadata{ .name = name_str }); } -FLAMINGO_C_EXPORT FlamingoNameInfo* flamingo_make_name_namespaced(char const* name_str, char const* namespaze_str) { +FLAMINGO_C_EXPORT FlamingoNameInfo* flamingo_make_name_namespaced(char const* namespaze_str, char const* name_str) { return reinterpret_cast(new flamingo::HookNameMetadata{ .name = name_str, .namespaze = namespaze_str }); } -FLAMINGO_C_EXPORT FlamingoHookPriority* flamingo_make_priority(FlamingoNameInfo** before_names, size_t num_befores, - FlamingoNameInfo** after_names, size_t num_afters, bool is_final) { + +FLAMINGO_C_EXPORT FlamingoHookFilter* flamingo_make_filter(char const* namespaze_str, char const* name_str){ + + auto* result = new flamingo::HookNameFilter(); + if (namespaze_str != nullptr) { + result->namespaze = namespaze_str; + } + if (name_str != nullptr) { + result->name = name_str; + } + return reinterpret_cast(result); +} + +FLAMINGO_C_EXPORT FlamingoHookPriority* flamingo_make_priority(FlamingoHookFilter** before_names, size_t num_befores, + FlamingoHookFilter** after_names, size_t num_afters, bool is_final) { // Iterate the befores and afters, consume their pointers to make new instances for the before set auto result = new flamingo::HookPriority(); result->befores.resize(num_befores); for (size_t i = 0; i < num_befores; i++) { - auto value = reinterpret_cast(before_names[i]); - new (&result->befores[i]) flamingo::HookNameMetadata(*value); + auto value = reinterpret_cast(before_names[i]); + new (&result->befores[i]) flamingo::HookNameFilter(*value); delete value; } result->afters.resize(num_afters); for (size_t i = 0; i < num_afters; i++) { - auto value = reinterpret_cast(after_names[i]); - new (&result->afters[i]) flamingo::HookNameMetadata(*value); + auto value = reinterpret_cast(after_names[i]); + new (&result->afters[i]) flamingo::HookNameFilter(*value); delete value; } result->is_final = is_final; diff --git a/src/installer.cpp b/src/installer.cpp index a4f39a3..1bcf3ac 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -85,11 +85,13 @@ std::list topological_sort_hooks_by_priority(std::list& hook matches.reserve(1); for (auto const& hook : hooks) { auto const& name = hook.metadata.name_info; - // exclude self + // exclude self from matches to avoid self-cycle issues. + // If a hook specifies that it should be before/after itself, + // it's likely a mistake, but we won't consider it a cycle since it's not really a dependency. if (name == self) { continue; } - if (name.matches(filter)) { + if (filter.matches(name)) { matches.push_back(name); } } diff --git a/test/sorted-hooks.cpp b/test/sorted-hooks.cpp index 0881b21..8c1c668 100644 --- a/test/sorted-hooks.cpp +++ b/test/sorted-hooks.cpp @@ -49,7 +49,7 @@ static void test_name_matching() { HookNameMetadata nameB; nameB.name = "B"; HookPriority priorityB; - priorityB.afters.push_back(nameA); + priorityB.afters.push_back(HookNameFilter(nameA)); flamingo::HookInfo hB((void*)hook_function_B, hook_target.data(), &origB, std::move(nameB), std::move(priorityB)); auto resB = flamingo::Install(std::move(hB)); @@ -116,7 +116,7 @@ static void test_namespaze_matching() { HookNameMetadata prior_name; prior_name.name = "prior"; HookPriority prior_prio; - HookNameMetadata match_ns; + HookNameFilter match_ns; match_ns.namespaze = "common"; prior_prio.befores.push_back(match_ns); flamingo::HookInfo hprior((void*)prior, hook_target.data(), &orig_prior, std::move(prior_name), @@ -158,9 +158,9 @@ static void test_priority_cycle() { HookNameMetadata nY; nY.name = "Y"; HookPriority pX; - pX.afters.push_back(nY); + pX.afters.push_back(HookNameFilter(nY)); HookPriority pY; - pY.afters.push_back(nX); + pY.afters.push_back(HookNameFilter(nX)); flamingo::HookInfo hX((void*)hx, hook_target.data(), &origX, std::move(nX), std::move(pX)); auto rX = flamingo::Install(std::move(hX)); From 72ccc11522364d2ed323b53ec788d46d803a50d4 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:56:53 -0400 Subject: [PATCH 107/134] move clear cache to dtor --- shared/fixups.hpp | 4 ++++ src/fixups.cpp | 4 +--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/shared/fixups.hpp b/shared/fixups.hpp index 068c191..490dceb 100644 --- a/shared/fixups.hpp +++ b/shared/fixups.hpp @@ -37,6 +37,10 @@ struct ProtectionWriter { ~ProtectionWriter() { target.protection = original_permissions; target.Protect(); + + // flush instruction cache on finish + auto addr = target.addr; + __builtin___clear_cache(reinterpret_cast(addr.data()), reinterpret_cast(addr.data() + addr.size())); } // Write data to this writer. Returns the index that we wrote to. uint_fast16_t Write(T inst) { diff --git a/src/fixups.cpp b/src/fixups.cpp index 5b30d05..deebcde 100644 --- a/src/fixups.cpp +++ b/src/fixups.cpp @@ -541,9 +541,7 @@ void ShimTarget::WriteJump(void* address) { ProtectionWriter writer(*this); WriteCallback(writer, reinterpret_cast(address)); - // flush instruction cache - __builtin___clear_cache(reinterpret_cast(addr.data()), - reinterpret_cast(addr.data() + addr.size())); + } void ShimTarget::WriteCallback(ProtectionWriter& writer, uint32_t const* target) { From 65eec0a4bbeed6fc9138453d40c4266c28dd8451 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Fri, 27 Mar 2026 16:55:39 -0400 Subject: [PATCH 108/134] Fix error macro --- test/test-wrapper.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test-wrapper.hpp b/test/test-wrapper.hpp index 3bba8f4..1814a7c 100644 --- a/test/test-wrapper.hpp +++ b/test/test-wrapper.hpp @@ -29,9 +29,9 @@ // - Dump the trampoline // - Dump the new hook // Perhaps we make TestWrapper take a Fixups instance for this? That way we can do all three of those. -#define ERROR(S, ...) \ +#define ERROR(...) \ do { \ - fmt::print(stderr, S, ##__VA_ARGS__); \ + fmt::print(stderr, __VA_ARGS__); \ fmt::print(stderr, "\n"); \ std::exit(1); \ } while (0) From 0b7239d0083dae15b033f8b503d82f75696f9b93 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Fri, 27 Mar 2026 16:55:50 -0400 Subject: [PATCH 109/134] Make constructor implicit --- shared/hook-metadata.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shared/hook-metadata.hpp b/shared/hook-metadata.hpp index be547e1..cd1e310 100644 --- a/shared/hook-metadata.hpp +++ b/shared/hook-metadata.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -42,7 +43,7 @@ struct HookNameFilter { explicit HookNameFilter(std::string namespaze, std::string name) : namespaze(std::move(namespaze)), name(std::move(name)) {} /// @brief Construct a filter that matches the provided metadata. This is used for constructing filters from userdata. - explicit HookNameFilter(HookNameMetadata const& metadata) : namespaze(metadata.namespaze), name(metadata.name) {} + HookNameFilter(HookNameMetadata const& metadata) : namespaze(metadata.namespaze), name(metadata.name) {} /// @brief Checks if the provided metadata matches this filter. // A filter with no fields set matches everything. From ca90de05859378735f9ebd9ed407eb1b8e205a60 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:09:11 -0400 Subject: [PATCH 110/134] Fix remaining broken filters in tests --- test/sorted-hooks.cpp | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/test/sorted-hooks.cpp b/test/sorted-hooks.cpp index 8c1c668..7138bd4 100644 --- a/test/sorted-hooks.cpp +++ b/test/sorted-hooks.cpp @@ -116,8 +116,7 @@ static void test_namespaze_matching() { HookNameMetadata prior_name; prior_name.name = "prior"; HookPriority prior_prio; - HookNameFilter match_ns; - match_ns.namespaze = "common"; + HookNameFilter match_ns{"common"}; prior_prio.befores.push_back(match_ns); flamingo::HookInfo hprior((void*)prior, hook_target.data(), &orig_prior, std::move(prior_name), std::move(prior_prio)); @@ -217,8 +216,7 @@ static void test_complex_namespace() { if (!flamingo::Install(std::move(hA2)).has_value()) ERROR("Failed to install a2"); // b1 requests to be before the entire namespaze "alpha" - HookNameMetadata match_ns; - match_ns.namespaze = "alpha"; + HookNameFilter match_ns{"alpha"}; HookPriority pB; pB.befores.push_back(match_ns); flamingo::HookInfo hB1((void*)b1, hook_target.data(), &origB1, std::move(mb1), std::move(pB)); @@ -472,8 +470,7 @@ static void test_befores_namespace_multiple() { HookNameMetadata prior_name; prior_name.name = "prior"; HookPriority prior_p; - HookNameMetadata match_ns; - match_ns.namespaze = "grp"; + HookNameFilter match_ns{"grp"}; prior_p.befores.push_back(match_ns); if (!flamingo::Install( flamingo::HookInfo((void*)prior, hook_target.data(), &origPrior, std::move(prior_name), std::move(prior_p))) @@ -522,9 +519,9 @@ static void test_afters_namespace_multiple() { HookNameMetadata late_name; late_name.name = "late"; HookPriority late_p; - HookNameMetadata match_ns2; + HookNameFilter match_ns2; match_ns2.namespaze = "grp"; - late_p.afters.push_back(match_ns2); + late_p.afters.emplace_back(match_ns2); if (!flamingo::Install( flamingo::HookInfo((void*)late, hook_target.data(), &origLate, std::move(late_name), std::move(late_p))) .has_value()) @@ -580,8 +577,7 @@ static void test_preserve_no_priority_relative_order() { HookNameMetadata md; md.name = "d"; HookPriority pd; - HookNameMetadata match_ns; - match_ns.namespaze = "grp"; + HookNameFilter match_ns{"grp"}; pd.befores.push_back(match_ns); if (!flamingo::Install(flamingo::HookInfo((void*)d, hook_target.data(), &origD, std::move(md), std::move(pd))) .has_value()) From 010f9779948d500838bd3b2e8b492132a36fa809 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:10:31 -0400 Subject: [PATCH 111/134] Use `namespace` --- shared/hook-metadata.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/hook-metadata.hpp b/shared/hook-metadata.hpp index cd1e310..8075b46 100644 --- a/shared/hook-metadata.hpp +++ b/shared/hook-metadata.hpp @@ -103,6 +103,6 @@ class fmt::formatter { } template constexpr auto format(flamingo::HookNameFilter const& filter, Context& ctx) const { - return fmt::format_to(ctx.out(), "name: {} namespaze {}", filter.name.value_or("*"), filter.namespaze.value_or("*")); + return fmt::format_to(ctx.out(), "name: {} namespace {}", filter.name.value_or("*"), filter.namespaze.value_or("*")); } }; From 7cdd42de25a34aa4751906f9bf00f9dcea34239e Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:10:39 -0400 Subject: [PATCH 112/134] Run test script in CI --- .github/workflows/build-ndk.yml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-ndk.yml b/.github/workflows/build-ndk.yml index f5f7b50..b147341 100644 --- a/.github/workflows/build-ndk.yml +++ b/.github/workflows/build-ndk.yml @@ -42,17 +42,22 @@ jobs: - name: QPM Collapse run: qpm collapse - - name: CMake with LOCAL_TEST enabled + - name: Run test script run: | - CC=clang CXX=clang cmake -B build -DTEST_BUILD=1 -GNinja + cd ${GITHUB_WORKSPACE} + ./test.sh - - name: Build - run: | - ninja -C build + # - name: CMake with LOCAL_TEST enabled + # run: | + # CC=clang CXX=clang cmake -B build -DTEST_BUILD=1 -GNinja + + # - name: Build + # run: | + # ninja -C build - # Run the test to ensure validity - - name: Run tests - run: ctest --test-dir build --output-on-failure + # # Run the test to ensure validity + # - name: Run tests + # run: ctest --test-dir build --output-on-failure build: runs-on: ubuntu-latest From 2cc22e30285c670b019a13005928c7edaa2a8acf Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:22:32 -0400 Subject: [PATCH 113/134] Make test script executable --- test.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 test.sh diff --git a/test.sh b/test.sh old mode 100644 new mode 100755 From f7407fe5cb38a8b0e72cf8aee4ed91465dca3400 Mon Sep 17 00:00:00 2001 From: FernTheDev <15272073+Fernthedev@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:23:42 -0400 Subject: [PATCH 114/134] Remove commented --- .github/workflows/build-ndk.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/build-ndk.yml b/.github/workflows/build-ndk.yml index b147341..0d2468b 100644 --- a/.github/workflows/build-ndk.yml +++ b/.github/workflows/build-ndk.yml @@ -47,18 +47,6 @@ jobs: cd ${GITHUB_WORKSPACE} ./test.sh - # - name: CMake with LOCAL_TEST enabled - # run: | - # CC=clang CXX=clang cmake -B build -DTEST_BUILD=1 -GNinja - - # - name: Build - # run: | - # ninja -C build - - # # Run the test to ensure validity - # - name: Run tests - # run: ctest --test-dir build --output-on-failure - build: runs-on: ubuntu-latest From c719ed8596b1af5521909686ab64be8ce2a4786a Mon Sep 17 00:00:00 2001 From: FernTheDev <15272073+Fernthedev@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:24:38 -0400 Subject: [PATCH 115/134] namespace --- shared/hook-metadata.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/hook-metadata.hpp b/shared/hook-metadata.hpp index 8075b46..fa1a36e 100644 --- a/shared/hook-metadata.hpp +++ b/shared/hook-metadata.hpp @@ -92,7 +92,7 @@ class fmt::formatter { } template constexpr auto format(flamingo::HookNameMetadata const& metadata, Context& ctx) const { - return fmt::format_to(ctx.out(), "name: {} namespaze {}", metadata.name, metadata.namespaze); + return fmt::format_to(ctx.out(), "name: {} namespace {}", metadata.name, metadata.namespaze); } }; template <> From e948f802e2e9a8b6120e879107bec2c9d165da91 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:50:44 -0400 Subject: [PATCH 116/134] clear cache before protection is lost --- shared/fixups.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shared/fixups.hpp b/shared/fixups.hpp index 490dceb..150cb4c 100644 --- a/shared/fixups.hpp +++ b/shared/fixups.hpp @@ -35,12 +35,12 @@ struct ProtectionWriter { } ~ProtectionWriter() { - target.protection = original_permissions; - target.Protect(); - // flush instruction cache on finish auto addr = target.addr; __builtin___clear_cache(reinterpret_cast(addr.data()), reinterpret_cast(addr.data() + addr.size())); + + target.protection = original_permissions; + target.Protect(); } // Write data to this writer. Returns the index that we wrote to. uint_fast16_t Write(T inst) { From 282dc5eb5b83ff2a5d49b0ddade5538e9e449e05 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:51:44 -0400 Subject: [PATCH 117/134] Empty space --- src/fixups.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/fixups.cpp b/src/fixups.cpp index deebcde..49b2774 100644 --- a/src/fixups.cpp +++ b/src/fixups.cpp @@ -541,7 +541,6 @@ void ShimTarget::WriteJump(void* address) { ProtectionWriter writer(*this); WriteCallback(writer, reinterpret_cast(address)); - } void ShimTarget::WriteCallback(ProtectionWriter& writer, uint32_t const* target) { From 5ddb006bf8ca77aa393f361bcecfc5436a7f5e50 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:51:55 -0400 Subject: [PATCH 118/134] Empty space --- src/fixups.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/fixups.cpp b/src/fixups.cpp index 49b2774..aa3053b 100644 --- a/src/fixups.cpp +++ b/src/fixups.cpp @@ -540,7 +540,6 @@ void ShimTarget::WriteJump(void* address) { // The writer for ensuring correct permissions and also performing the write ProtectionWriter writer(*this); WriteCallback(writer, reinterpret_cast(address)); - } void ShimTarget::WriteCallback(ProtectionWriter& writer, uint32_t const* target) { From 3325d0105a0aa4a9c9ca098636b54295ffea8f4d Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:16:02 -0400 Subject: [PATCH 119/134] Add debugging symbols and launcher --- .vscode/launch.json | 20 ++++++++++++++++++++ .vscode/tasks.json | 18 ++++++++++++++++-- test.sh | 2 +- 3 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..f4dcea9 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch test", + "type": "lldb", + "request": "launch", + "program": "${workspaceRoot}/build_test/sorting-test", + "args": [], + "cwd": "${workspaceRoot}", + "preLaunchTask": "Build test", + "sourceMap": { + "${workspaceRoot}/libflamingo.so": "${workspaceRoot}/debug/libflamingo.so" + }, + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 56bae3d..c2ce692 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -9,7 +9,11 @@ "windows": { "command": "ndk-build.cmd" }, - "args": ["NDK_PROJECT_PATH=.", "APP_BUILD_SCRIPT=./Android.mk", "NDK_APPLICATION_MK=./Application.mk"], + "args": [ + "NDK_PROJECT_PATH=.", + "APP_BUILD_SCRIPT=./Android.mk", + "NDK_APPLICATION_MK=./Application.mk" + ], "group": "build", "options": { "env": {} @@ -43,6 +47,16 @@ "options": { "env": {} } + }, + { + "label": "Build test", + "detail": "Builds the test executable", + "type": "shell", + "command": "cmake -B build_test -DTEST_BUILD=1 -DCMAKE_BUILD_TYPE=Debug -GNinja && cmake --build build_test", + "group": "build", + "options": { + "env": {} + }, } ] -} +} \ No newline at end of file diff --git a/test.sh b/test.sh index 38c8a9e..d144742 100755 --- a/test.sh +++ b/test.sh @@ -1,5 +1,5 @@ #!/bin/bash set -euo pipefail -CC=clang CXX=clang cmake -B build_test -DTEST_BUILD=1 -GNinja +CC=clang CXX=clang cmake -B build_test -DTEST_BUILD=1 -GNinja -DCMAKE_BUILD_TYPE=Debug ninja -C build_test ctest --test-dir build_test --output-on-failure \ No newline at end of file From 5b59de8cd7cb79d8137a819e59912a9ff09db516 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:16:12 -0400 Subject: [PATCH 120/134] Refactor topological_sort_hooks_by_priority to return Result type and improve cycle detection --- src/installer.cpp | 89 ++++++++++++++++++++++++++++++----------------- 1 file changed, 58 insertions(+), 31 deletions(-) diff --git a/src/installer.cpp b/src/installer.cpp index 1bcf3ac..11787cc 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -40,14 +40,42 @@ struct HookNameMetadataHash { /// @brief Topologically sorts the provided hooks by their priority constraints. /// @param hooks The list of hooks to sort in place. /// @return The sorted list of hooks that have cycles. -std::list topological_sort_hooks_by_priority(std::list& hooks) { +Result, installation::TargetBadPriorities> topological_sort_hooks_by_priority( + std::list& hooks) { + using ResultT = Result, installation::TargetBadPriorities>; // Ensure any "final" priority hooks are placed at the end while preserving relative order. std::vector::iterator> finals; - finals.reserve(std::distance(hooks.begin(), hooks.end())); + finals.reserve(hooks.size()); + + // build name to iterator map + std::unordered_map::iterator, HookNameMetadataHash> name_to_iterator; + for (auto it = hooks.begin(); it != hooks.end(); ++it) { if (it->metadata.priority.is_final) { finals.push_back(it); } + name_to_iterator[it->metadata.name_info] = it; + + // check if any reference self in after/before + for (auto const& after : it->metadata.priority.afters) { + if (after.matches(it->metadata.name_info)) { + FLAMINGO_CRITICAL("Hook {} references itself in after dependencies. This is likely a mistake.", + it->metadata.name_info.name); + + return ResultT::Err(installation::TargetBadPriorities{ + it->metadata, fmt::format("Hook {} references itself in after dependencies. This is likely a mistake.", + it->metadata.name_info.name) }); + } + } + for (auto const& before : it->metadata.priority.befores) { + if (before.matches(it->metadata.name_info)) { + FLAMINGO_CRITICAL("Hook {} references itself in before dependencies. This is likely a mistake.", + it->metadata.name_info.name); + return ResultT::Err(installation::TargetBadPriorities{ + it->metadata, fmt::format("Hook {} references itself in before dependencies. This is likely a mistake.", + it->metadata.name_info.name) }); + } + } } for (auto& it : finals) { hooks.splice(hooks.end(), hooks, it); @@ -66,31 +94,19 @@ std::list topological_sort_hooks_by_priority(std::list& hook // each hook has a requirement to be after certain other hooks in this graph // we also convert before requirements to after requirements for easier processing - // build name to iterator map - std::unordered_map::iterator, HookNameMetadataHash> name_to_iterator; - for (auto it = hooks.begin(); it != hooks.end(); ++it) { - name_to_iterator[it->metadata.name_info] = it; - } - // A before B == B after A // HookInfo after strings // this graph represents all the hooks and their before dependencies - // key must be before values + // if A must be before B, we have an edge A -> B, meaning A must come before B std::unordered_map, HookNameMetadataHash> graph; graph.reserve(hooks.size()); // finds all hooks that match the given filter - auto findMatches = [&](HookNameFilter const& filter, HookNameMetadata self) { + auto findMatches = [&](HookNameFilter const& filter) { std::vector matches; matches.reserve(1); for (auto const& hook : hooks) { auto const& name = hook.metadata.name_info; - // exclude self from matches to avoid self-cycle issues. - // If a hook specifies that it should be before/after itself, - // it's likely a mistake, but we won't consider it a cycle since it's not really a dependency. - if (name == self) { - continue; - } if (filter.matches(name)) { matches.push_back(name); } @@ -104,7 +120,7 @@ std::list topological_sort_hooks_by_priority(std::list& hook // If this hook specifies that it must come after some other hooks (A), // then we must add edges A -> this_hook in the graph (so that A precedes this). for (auto const& afterFilter : hook.metadata.priority.afters) { - auto matches = findMatches(afterFilter, hook.metadata.name_info); + auto matches = findMatches(afterFilter); for (auto const& matched : matches) { graph[matched].push_back(hook.metadata.name_info); } @@ -113,7 +129,7 @@ std::list topological_sort_hooks_by_priority(std::list& hook // now build from befores // If this hook requests to be before some matched hooks, add edges current -> matched for (auto const& beforeFilter : hook.metadata.priority.befores) { - auto matches = findMatches(beforeFilter, hook.metadata.name_info); + auto matches = findMatches(beforeFilter); for (auto const& matched_before : matches) { graph[hook.metadata.name_info].push_back(matched_before); } @@ -183,13 +199,21 @@ std::list topological_sort_hooks_by_priority(std::list& hook // now, any remaining hooks in `hooks` are part of cycles // append them in their original order and log a warning. Splicing invalidates // iterators, so consume from the front until empty to preserve original order. - for (auto const& hook : hooks) { - FLAMINGO_CRITICAL( - "Detected cycle in hook priorities involving hook name: {}. Hooks involved in the cycle will remain in their " - "original order.", - hook.metadata.name_info); - FLAMINGO_CRITICAL("After priorities for this hook were: {}", fmt::join(hook.metadata.priority.afters, ", ")); - FLAMINGO_CRITICAL("Before priorities for this hook were: {}", fmt::join(hook.metadata.priority.befores, ", ")); + + if (!hooks.empty()) { + for (auto const& hook : hooks) { + FLAMINGO_CRITICAL( + "Detected cycle in hook priorities involving hook name: {}. Hooks involved in the cycle will remain in their " + "original order.", + hook.metadata.name_info); + FLAMINGO_CRITICAL("After priorities for this hook were: {}", fmt::join(hook.metadata.priority.afters, ", ")); + FLAMINGO_CRITICAL("Before priorities for this hook were: {}", fmt::join(hook.metadata.priority.befores, ", ")); + } + + // TODO: Restore hooks list to original state? + return ResultT::Err(installation::TargetBadPriorities{ + hooks.front().metadata, fmt::format("Detected cycle in hook priorities involving hooks. Hooks " + "involved in the cycle will remain in their original order.") }); } // replace original list with the sorted result using swap to avoid reallocation @@ -205,7 +229,7 @@ std::list topological_sort_hooks_by_priority(std::list& hook } // remaining hooks are cycles - return sorted_hooks; + return ResultT::Ok(sorted_hooks); } /// @brief Recompiles the hooks for the given target, updating their orig pointers as needed. @@ -233,11 +257,10 @@ void recompile_hooks(std::list& hooks, TargetDescriptor const& target_ ? target_entry->second.fixups.fixup_inst_destination.addr.data() : reinterpret_cast(&no_fixups)); return; - } - + } + // When multiple hooks, orig is next hook it->assign_orig(std::next(it)->hook_ptr); - // Multiple hooks: head, middles, tail @@ -326,7 +349,11 @@ Result::iterator, installation::TargetBadPriorities> find_su // Insert the new hook first so we can let topo sort place it correctly auto newIt = hooks.insert(hooks.begin(), std::move(hook_to_install)); // if our hook has priority constraints, we need to topologically sort and find a suitable location - auto cycles = topological_sort_hooks_by_priority(hooks); + auto cyclesResult = topological_sort_hooks_by_priority(hooks); + if (!cyclesResult.has_value()) { + return ResultT::Err(cyclesResult.error()); + } + auto cycles = cyclesResult.value(); if (!cycles.empty()) { // We have cycles involving our new hook @@ -341,7 +368,7 @@ Result::iterator, installation::TargetBadPriorities> find_su return ResultT::Err(installation::TargetBadPriorities{ metadata, fmt::format("Cannot install hook due to cycles in priorities involving hook name: {}", - fmt::join(cycle_strings, ",")) }); + fmt::join(cycle_strings, ",")) }); } // now recompile all hooks to ensure orig pointers are correct From 3b6c88d0c99af06f1c3a05006d706e08b43e07b7 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 6 Apr 2026 00:48:24 -0400 Subject: [PATCH 121/134] log warnings --- shared/util.hpp | 5 +++++ src/installer.cpp | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/shared/util.hpp b/shared/util.hpp index 31f7192..a12653f 100644 --- a/shared/util.hpp +++ b/shared/util.hpp @@ -34,6 +34,7 @@ #define FLAMINGO_ASSERT(...) __builtin_assume(__VA_ARGS__) #endif +#define FLAMINGO_WARN(...) LOGA(ANDROID_LOG_WARN, __VA_ARGS__) #define FLAMINGO_CRITICAL(...) LOGA(ANDROID_LOG_FATAL, __VA_ARGS__) #define FLAMINGO_ABORT(...) \ do { \ @@ -59,6 +60,7 @@ #define FLAMINGO_DEBUG(...) #endif +#define FLAMINGO_WARN(...) Paper::Logger::fmtLog(__VA_ARGS__) #define FLAMINGO_CRITICAL(...) Paper::Logger::fmtLog(__VA_ARGS__) #define FLAMINGO_ABORT(...) \ do { \ @@ -78,6 +80,9 @@ #define FLAMINGO_CRITICAL(...) \ fmt::print(__VA_ARGS__); \ puts("") +#define FLAMINGO_WARN(...) \ + fmt::print(__VA_ARGS__); \ + puts("") #define FLAMINGO_ABORT(...) \ fmt::print(__VA_ARGS__); \ puts(""); \ diff --git a/src/installer.cpp b/src/installer.cpp index 11787cc..3896375 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -59,7 +59,7 @@ Result, installation::TargetBadPriorities> topological_sort_ // check if any reference self in after/before for (auto const& after : it->metadata.priority.afters) { if (after.matches(it->metadata.name_info)) { - FLAMINGO_CRITICAL("Hook {} references itself in after dependencies. This is likely a mistake.", + FLAMINGO_WARN("Hook {} references itself in after dependencies. This is likely a mistake.", it->metadata.name_info.name); return ResultT::Err(installation::TargetBadPriorities{ @@ -69,7 +69,7 @@ Result, installation::TargetBadPriorities> topological_sort_ } for (auto const& before : it->metadata.priority.befores) { if (before.matches(it->metadata.name_info)) { - FLAMINGO_CRITICAL("Hook {} references itself in before dependencies. This is likely a mistake.", + FLAMINGO_WARN("Hook {} references itself in before dependencies. This is likely a mistake.", it->metadata.name_info.name); return ResultT::Err(installation::TargetBadPriorities{ it->metadata, fmt::format("Hook {} references itself in before dependencies. This is likely a mistake.", From 3c90a7f0067490c85762a48edbf173a54e2ad520 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 6 Apr 2026 00:52:50 -0400 Subject: [PATCH 122/134] Log hooks --- src/installer.cpp | 43 ++++++++++++++++--------------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/src/installer.cpp b/src/installer.cpp index 3896375..de0b6ed 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -47,6 +47,16 @@ Result, installation::TargetBadPriorities> topological_sort_ std::vector::iterator> finals; finals.reserve(hooks.size()); + auto LogHooks = [](auto&& s, std::list const& hooks) { + std::vector hook_names; + hook_names.reserve(hooks.size()); + for (auto const& hook : hooks) { + hook_names.push_back(hook.metadata.name_info.name); + } + // TODO: Can we avoid runtime string here? + FLAMINGO_DEBUG(fmt::runtime(s), fmt::join(hook_names, " -> ")); + }; + // build name to iterator map std::unordered_map::iterator, HookNameMetadataHash> name_to_iterator; @@ -60,7 +70,7 @@ Result, installation::TargetBadPriorities> topological_sort_ for (auto const& after : it->metadata.priority.afters) { if (after.matches(it->metadata.name_info)) { FLAMINGO_WARN("Hook {} references itself in after dependencies. This is likely a mistake.", - it->metadata.name_info.name); + it->metadata.name_info.name); return ResultT::Err(installation::TargetBadPriorities{ it->metadata, fmt::format("Hook {} references itself in after dependencies. This is likely a mistake.", @@ -70,7 +80,7 @@ Result, installation::TargetBadPriorities> topological_sort_ for (auto const& before : it->metadata.priority.befores) { if (before.matches(it->metadata.name_info)) { FLAMINGO_WARN("Hook {} references itself in before dependencies. This is likely a mistake.", - it->metadata.name_info.name); + it->metadata.name_info.name); return ResultT::Err(installation::TargetBadPriorities{ it->metadata, fmt::format("Hook {} references itself in before dependencies. This is likely a mistake.", it->metadata.name_info.name) }); @@ -81,14 +91,7 @@ Result, installation::TargetBadPriorities> topological_sort_ hooks.splice(hooks.end(), hooks, it); } - { - std::vector hook_names; - hook_names.reserve(hooks.size()); - for (auto const& hook : hooks) { - hook_names.push_back(hook.metadata.name_info.name); - } - FLAMINGO_DEBUG("Initial hook order before topological sort: {}", fmt::join(hook_names, " -> ")); - } + LogHooks("Initial hook order before topological sort: {}", hooks); // now build topological graph // each hook has a requirement to be after certain other hooks in this graph @@ -187,14 +190,7 @@ Result, installation::TargetBadPriorities> topological_sort_ } } - { - std::vector hook_names; - hook_names.reserve(sorted_hooks.size()); - for (auto const& hook : sorted_hooks) { - hook_names.push_back(hook.metadata.name_info.name); - } - FLAMINGO_DEBUG("Flattened hook order after topological sort attempt: {}", fmt::join(hook_names, " -> ")); - } + LogHooks("Flattened hook order after topological sort (before cycle processing): {}", sorted_hooks); // now, any remaining hooks in `hooks` are part of cycles // append them in their original order and log a warning. Splicing invalidates @@ -213,20 +209,13 @@ Result, installation::TargetBadPriorities> topological_sort_ // TODO: Restore hooks list to original state? return ResultT::Err(installation::TargetBadPriorities{ hooks.front().metadata, fmt::format("Detected cycle in hook priorities involving hooks. Hooks " - "involved in the cycle will remain in their original order.") }); + "involved in the cycle will remain in their original order.") }); } // replace original list with the sorted result using swap to avoid reallocation hooks.swap(sorted_hooks); - { - std::vector hook_names; - hook_names.reserve(hooks.size()); - for (auto const& hook : hooks) { - hook_names.push_back(hook.metadata.name_info.name); - } - FLAMINGO_DEBUG("Final hook order after topological sort: {}", fmt::join(hook_names, " -> ")); - } + LogHooks("Final hook order after topological sort: {}", hooks); // remaining hooks are cycles return ResultT::Ok(sorted_hooks); From 8c15b8eb8ff6e274035d3bd6ae2ea63eb84e867e Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 6 Apr 2026 00:53:52 -0400 Subject: [PATCH 123/134] Simplify recompiling hooks --- src/installer.cpp | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/installer.cpp b/src/installer.cpp index de0b6ed..d40aaf0 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -240,26 +240,10 @@ void recompile_hooks(std::list& hooks, TargetDescriptor const& target_ auto it = hooks.begin(); target_entry->second.fixups.target.WriteJump(it->hook_ptr); - // Single hook: target -> hook, hook.orig -> fixups or no_fixups - if (std::next(it) == hooks.end()) { - it->assign_orig(target_entry->second.metadata.metadata.need_orig - ? target_entry->second.fixups.fixup_inst_destination.addr.data() - : reinterpret_cast(&no_fixups)); - return; - } - - // When multiple hooks, orig is next hook - it->assign_orig(std::next(it)->hook_ptr); - - // Multiple hooks: head, middles, tail - - // Middles - for (++it; std::next(it) != hooks.end(); ++it) { + while (std::next(it) != hooks.end()) { it->assign_orig(std::next(it)->hook_ptr); + ++it; } - - // Tail - // 'it' now refers to the last element it->assign_orig(target_entry->second.metadata.metadata.need_orig ? target_entry->second.fixups.fixup_inst_destination.addr.data() : reinterpret_cast(&no_fixups)); From 4c2c3c1279a6409bddee9ea98298dc50bbf9fe63 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Mon, 6 Apr 2026 00:54:46 -0400 Subject: [PATCH 124/134] Use emplace_back --- src/installer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/installer.cpp b/src/installer.cpp index d40aaf0..3b0610c 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -280,8 +280,8 @@ Result::iterator, installation::TargetBadPriorities> find_su } // Select the end to install at - auto new_it = hooks.emplace(hooks.end(), std::move(hook_to_install)); - return ResultT::Ok(new_it); + hooks.emplace_back(std::move(hook_to_install)); + return ResultT::Ok(--hooks.end()); } // ok now we have a non-final hook From 1e9b8375b8481d5a22f96c38cad1143395383b06 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Wed, 8 Apr 2026 14:23:36 -0400 Subject: [PATCH 125/134] Add tests for hook order stability and mixed-priority stability --- test/sorted-hooks.cpp | 134 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/test/sorted-hooks.cpp b/test/sorted-hooks.cpp index 7138bd4..8f10c5c 100644 --- a/test/sorted-hooks.cpp +++ b/test/sorted-hooks.cpp @@ -629,6 +629,138 @@ static void test_preserve_no_priority_relative_order() { } } +static void test_order_stability() { + puts("Test: order stability for equal-priority hooks"); + uintptr_t s1 = 0xA1010001; + uintptr_t s2 = 0xA1020002; + uintptr_t s3 = 0xA1030003; + uintptr_t s4 = 0xA1040004; + uintptr_t prior = 0xA1050005; + static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8 }; + + auto hook_target = perform_far_hook_test(s1, to_hook); + void* orig1 = nullptr; + void* orig2 = nullptr; + void* orig3 = nullptr; + void* orig4 = nullptr; + void* origPrior = nullptr; + + HookNameMetadata m1; m1.name = "s1"; m1.namespaze = "stable"; + HookNameMetadata m2; m2.name = "s2"; m2.namespaze = "stable"; + HookNameMetadata m3; m3.name = "s3"; m3.namespaze = "stable"; + HookNameMetadata m4; m4.name = "s4"; m4.namespaze = "stable"; + + if (!flamingo::Install(flamingo::HookInfo((void*)s1, hook_target.data(), &orig1, std::move(m1), HookPriority{})).has_value()) + ERROR("Failed to install s1"); + if (!flamingo::Install(flamingo::HookInfo((void*)s2, hook_target.data(), &orig2, std::move(m2), HookPriority{})).has_value()) + ERROR("Failed to install s2"); + if (!flamingo::Install(flamingo::HookInfo((void*)s3, hook_target.data(), &orig3, std::move(m3), HookPriority{})).has_value()) + ERROR("Failed to install s3"); + if (!flamingo::Install(flamingo::HookInfo((void*)s4, hook_target.data(), &orig4, std::move(m4), HookPriority{})).has_value()) + ERROR("Failed to install s4"); + + // Insert a hook that requests to be before the entire 'stable' namespace, + // forcing a topo-sort while the stable group's relative order must be preserved. + HookNameMetadata mp; + mp.name = "prior"; + HookPriority pp; + HookNameFilter match_ns{"stable"}; + pp.befores.push_back(match_ns); + if (!flamingo::Install(flamingo::HookInfo((void*)prior, hook_target.data(), &origPrior, std::move(mp), std::move(pp))).has_value()) + ERROR("Failed to install prior"); + + auto fixup_res = flamingo::FixupPointerFor(flamingo::TargetDescriptor(hook_target.data())); + if (!fixup_res.has_value()) ERROR("Failed to get fixup pointer"); + void* fixup_ptr = (void*)fixup_res.value().data(); + + // Expect ordering: prior -> s4 -> s3 -> s2 -> s1 (newer installs at front, relative order preserved) + if ((uintptr_t)origPrior != s4) + ERROR("Stability: expected prior.orig == s4 (0x{:x}) got 0x{:x}", s4, (uintptr_t)origPrior); + if ((uintptr_t)orig4 != s3) + ERROR("Stability: expected s4.orig == s3 (0x{:x}) got 0x{:x}", s3, (uintptr_t)orig4); + if ((uintptr_t)orig3 != s2) + ERROR("Stability: expected s3.orig == s2 (0x{:x}) got 0x{:x}", s2, (uintptr_t)orig3); + if ((uintptr_t)orig2 != s1) + ERROR("Stability: expected s2.orig == s1 (0x{:x}) got 0x{:x}", s1, (uintptr_t)orig2); + if ((uintptr_t)orig1 != (uintptr_t)fixup_ptr) + ERROR("Stability: expected s1.orig == fixup got 0x{:x}", (uintptr_t)orig1); +} + +static void test_mixed_priority_stability() { + puts("Test: mixed-priority stability (some prioritized, others stable)"); + uintptr_t m1 = 0xC1010001; + uintptr_t m2 = 0xC1020002; + uintptr_t m3 = 0xC1030003; + uintptr_t before = 0xC1040004; + uintptr_t mid = 0xC1050005; + uintptr_t after = 0xC1060006; + static uint8_t to_hook[]{ 0xf7, 0x0f, 0x1c, 0xf8 }; + + auto hook_target = perform_far_hook_test(m1, to_hook); + void* orig1 = nullptr; + void* orig2 = nullptr; + void* orig3 = nullptr; + void* origBefore = nullptr; + void* origMid = nullptr; + void* origAfter = nullptr; + + HookNameMetadata mm1; mm1.name = "e1"; mm1.namespaze = "mix"; + HookNameMetadata mm2; mm2.name = "e2"; mm2.namespaze = "mix"; + HookNameMetadata mm3; mm3.name = "e3"; mm3.namespaze = "mix"; + + if (!flamingo::Install(flamingo::HookInfo((void*)m1, hook_target.data(), &orig1, std::move(mm1), HookPriority{})).has_value()) + ERROR("Failed to install m1"); + if (!flamingo::Install(flamingo::HookInfo((void*)m2, hook_target.data(), &orig2, std::move(mm2), HookPriority{})).has_value()) + ERROR("Failed to install m2"); + if (!flamingo::Install(flamingo::HookInfo((void*)m3, hook_target.data(), &orig3, std::move(mm3), HookPriority{})).has_value()) + ERROR("Failed to install m3"); + + // Install a hook that should come before the whole 'mix' namespace + HookNameMetadata mb; + mb.name = "before"; + HookPriority pb; + HookNameFilter nm_ns{"mix"}; + pb.befores.push_back(nm_ns); + if (!flamingo::Install(flamingo::HookInfo((void*)before, hook_target.data(), &origBefore, std::move(mb), std::move(pb))).has_value()) + ERROR("Failed to install before hook"); + + // Install a hook that requests to be after the specific hook 'e2' + HookNameMetadata mmid; + mmid.name = "mid"; + HookPriority pmid; + HookNameMetadata ref; ref.name = "e2"; + pmid.afters.emplace_back(ref); + if (!flamingo::Install(flamingo::HookInfo((void*)mid, hook_target.data(), &origMid, std::move(mmid), std::move(pmid))).has_value()) + ERROR("Failed to install mid hook"); + + // Install a hook that should come after the whole 'mix' namespace + HookNameMetadata ma; + ma.name = "after"; + HookPriority pa; + HookNameFilter nm_ns2; nm_ns2.namespaze = "mix"; + pa.afters.emplace_back(nm_ns2); + if (!flamingo::Install(flamingo::HookInfo((void*)after, hook_target.data(), &origAfter, std::move(ma), std::move(pa))).has_value()) + ERROR("Failed to install after hook"); + + auto fixup_res = flamingo::FixupPointerFor(flamingo::TargetDescriptor(hook_target.data())); + if (!fixup_res.has_value()) ERROR("Failed to get fixup pointer"); + void* fixup_ptr = (void*)fixup_res.value().data(); + + // Observed final chain: mid -> before -> m3 -> m2 -> m1 -> after + if ((uintptr_t)origMid != before) + ERROR("Mixed-stability: expected mid.orig == before (0x{:x}) got 0x{:x}", before, (uintptr_t)origMid); + if ((uintptr_t)origBefore != m3) + ERROR("Mixed-stability: expected before.orig == m3 (0x{:x}) got 0x{:x}", m3, (uintptr_t)origBefore); + if ((uintptr_t)orig3 != m2) + ERROR("Mixed-stability: expected m3.orig == m2 (0x{:x}) got 0x{:x}", m2, (uintptr_t)orig3); + if ((uintptr_t)orig2 != m1) + ERROR("Mixed-stability: expected m2.orig == m1 (0x{:x}) got 0x{:x}", m1, (uintptr_t)orig2); + if ((uintptr_t)orig1 != after) + ERROR("Mixed-stability: expected m1.orig == after (0x{:x}) got 0x{:x}", after, (uintptr_t)orig1); + if ((uintptr_t)origAfter != (uintptr_t)fixup_ptr) + ERROR("Mixed-stability: expected after.orig == fixup got 0x{:x}", (uintptr_t)origAfter); +} + static void test_reinstall() { puts("Test: reinstall"); @@ -703,6 +835,8 @@ int main() { test_befores_namespace_multiple(); test_afters_namespace_multiple(); test_preserve_no_priority_relative_order(); + test_order_stability(); + test_mixed_priority_stability(); test_reinstall(); test_uninstall(); puts("SORTED HOOKS TESTS PASSED"); From d62d781f611407529e1cd1b259098163ad34241b Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Wed, 8 Apr 2026 14:36:16 -0400 Subject: [PATCH 126/134] Update comments to include hook order --- test/sorted-hooks.cpp | 137 +++++++++++++++++++++++++++--------------- 1 file changed, 90 insertions(+), 47 deletions(-) diff --git a/test/sorted-hooks.cpp b/test/sorted-hooks.cpp index 8f10c5c..a49527d 100644 --- a/test/sorted-hooks.cpp +++ b/test/sorted-hooks.cpp @@ -30,6 +30,7 @@ static std::span perform_far_hook_test(uintptr_t hook_location, std::s hook_span); } +// Expected chain: A -> B -> fixup static void test_name_matching() { puts("Test: name matching"); // Setup @@ -78,6 +79,7 @@ static void test_name_matching() { } } +// Expected chain: prior -> two -> one -> fixup static void test_namespaze_matching() { puts("Test: namespaze matching"); // Setup three hooks: two in the same namespaze and one that must come before that namespaze @@ -116,7 +118,7 @@ static void test_namespaze_matching() { HookNameMetadata prior_name; prior_name.name = "prior"; HookPriority prior_prio; - HookNameFilter match_ns{"common"}; + HookNameFilter match_ns{ "common" }; prior_prio.befores.push_back(match_ns); flamingo::HookInfo hprior((void*)prior, hook_target.data(), &orig_prior, std::move(prior_name), std::move(prior_prio)); @@ -142,6 +144,7 @@ static void test_namespaze_matching() { } } +// Expected chain after X install: X -> fixup (Y install should fail) static void test_priority_cycle() { puts("Test: priority cycle"); uintptr_t hx = 0xaaaa0001; @@ -157,9 +160,9 @@ static void test_priority_cycle() { HookNameMetadata nY; nY.name = "Y"; HookPriority pX; - pX.afters.push_back(HookNameFilter(nY)); + pX.afters.emplace_back(nY); HookPriority pY; - pY.afters.push_back(HookNameFilter(nX)); + pY.afters.emplace_back(nX); flamingo::HookInfo hX((void*)hx, hook_target.data(), &origX, std::move(nX), std::move(pX)); auto rX = flamingo::Install(std::move(hX)); @@ -187,6 +190,7 @@ static void test_priority_cycle() { } } +// Expected chain: b1 -> a2 -> a1 -> fixup static void test_complex_namespace() { puts("Test: complex namespace ordering"); uintptr_t a1 = 0x10010001; @@ -216,7 +220,7 @@ static void test_complex_namespace() { if (!flamingo::Install(std::move(hA2)).has_value()) ERROR("Failed to install a2"); // b1 requests to be before the entire namespaze "alpha" - HookNameFilter match_ns{"alpha"}; + HookNameFilter match_ns{ "alpha" }; HookPriority pB; pB.befores.push_back(match_ns); flamingo::HookInfo hB1((void*)b1, hook_target.data(), &origB1, std::move(mb1), std::move(pB)); @@ -235,6 +239,7 @@ static void test_complex_namespace() { ERROR("Complex-ns: expected a1.orig == fixup got 0x{:x}", (uintptr_t)origA1); } +// Expected chain: final1 -> fixup (final2 install should fail) static void test_final_conflict() { puts("Test: final hook conflict"); uintptr_t f1 = 0x90010001; @@ -262,6 +267,7 @@ static void test_final_conflict() { if (r2.has_value()) ERROR("Expected second final install to fail but it succeeded"); } +// Expected chain: h1 -> h2 -> h3 -> h4 -> h5 -> fixup static void test_five_hook_order() { puts("Test: five-hook priority ordering"); uintptr_t h1 = 0x50010001; @@ -291,13 +297,13 @@ static void test_five_hook_order() { m5.name = "h5"; HookPriority p2; - p2.afters.push_back(m1); // h2 after h1 + p2.afters.emplace_back(m1); // h2 after h1 HookPriority p3; - p3.afters.push_back(m2); // h3 after h2 + p3.afters.emplace_back(m2); // h3 after h2 HookPriority p4; - p4.afters.push_back(m3); // h4 after h3 + p4.afters.emplace_back(m3); // h4 after h3 HookPriority p5; - p5.afters.push_back(m4); // h5 after h4 + p5.afters.emplace_back(m4); // h5 after h4 // Install in scrambled order to ensure priorities drive final order: 3,5,2,4,1 flamingo::HookInfo hh3((void*)h3, hook_target.data(), &orig3, std::move(m3), std::move(p3)); @@ -331,11 +337,13 @@ static void test_five_hook_order() { // find head: hook address not present in any orig_map values std::unordered_set pointed; for (auto const& kv : orig_map) { - if (std::find(hooks.begin(), hooks.end(), kv.second) != hooks.end()) pointed.insert(kv.second); + if (std::find(hooks.begin(), hooks.end(), kv.second) != hooks.end()) { + pointed.insert(kv.second); + } } uintptr_t head = 0; for (auto h : hooks) { - if (pointed.find(h) == pointed.end()) { + if (!pointed.contains(h)) { head = h; break; } @@ -371,6 +379,7 @@ static void test_five_hook_order() { // Forward declarations for additional edge-case tests +// Expected chain (no constraints): h3 -> h2 -> h1 -> fixup static void test_no_constraints_multiple() { puts("Test: no-constraints multiple installs"); uintptr_t h1 = 0x60010001; @@ -416,7 +425,7 @@ static void test_no_constraints_multiple() { } uintptr_t head = 0; for (auto h : hooks) - if (pointed.find(h) == pointed.end()) { + if (!pointed.contains(h)) { head = h; break; } @@ -441,6 +450,7 @@ static void test_no_constraints_multiple() { fmt::format("0x{:x}", order.empty() ? 0 : order[0])); } +// Expected chain: prior -> a2 -> a1 -> fixup static void test_befores_namespace_multiple() { puts("Test: befores matching multiple in namespace"); uintptr_t a1 = 0x70010001; @@ -470,7 +480,7 @@ static void test_befores_namespace_multiple() { HookNameMetadata prior_name; prior_name.name = "prior"; HookPriority prior_p; - HookNameFilter match_ns{"grp"}; + HookNameFilter match_ns{ "grp" }; prior_p.befores.push_back(match_ns); if (!flamingo::Install( flamingo::HookInfo((void*)prior, hook_target.data(), &origPrior, std::move(prior_name), std::move(prior_p))) @@ -490,6 +500,7 @@ static void test_befores_namespace_multiple() { ERROR("Befores-multi: expected a1.orig == fixup got 0x{:x}", (uintptr_t)origA1); } +// Expected chain: g2 -> g1 -> late -> fixup static void test_afters_namespace_multiple() { puts("Test: afters matching multiple in namespace"); uintptr_t g1 = 0x80010001; @@ -539,6 +550,7 @@ static void test_afters_namespace_multiple() { ERROR("Afters-multi: expected late.orig == fixup got 0x{:x}", (uintptr_t)origLate); } +// Expected chain after inserting D: d -> a3 -> a2 -> a1 -> fixup static void test_preserve_no_priority_relative_order() { puts("Test: preserve relative order of no-priority hooks during topo sort"); uintptr_t a1 = 0x91010001; @@ -577,7 +589,7 @@ static void test_preserve_no_priority_relative_order() { HookNameMetadata md; md.name = "d"; HookPriority pd; - HookNameFilter match_ns{"grp"}; + HookNameFilter match_ns{ "grp" }; pd.befores.push_back(match_ns); if (!flamingo::Install(flamingo::HookInfo((void*)d, hook_target.data(), &origD, std::move(md), std::move(pd))) .has_value()) @@ -602,7 +614,7 @@ static void test_preserve_no_priority_relative_order() { } uintptr_t head = 0; for (auto h : all) - if (pointed.find(h) == pointed.end()) { + if (!pointed.contains(h)) { head = h; break; } @@ -629,6 +641,7 @@ static void test_preserve_no_priority_relative_order() { } } +// Expected chain: prior -> s4 -> s3 -> s2 -> s1 -> fixup static void test_order_stability() { puts("Test: order stability for equal-priority hooks"); uintptr_t s1 = 0xA1010001; @@ -645,18 +658,30 @@ static void test_order_stability() { void* orig4 = nullptr; void* origPrior = nullptr; - HookNameMetadata m1; m1.name = "s1"; m1.namespaze = "stable"; - HookNameMetadata m2; m2.name = "s2"; m2.namespaze = "stable"; - HookNameMetadata m3; m3.name = "s3"; m3.namespaze = "stable"; - HookNameMetadata m4; m4.name = "s4"; m4.namespaze = "stable"; + HookNameMetadata m1; + m1.name = "s1"; + m1.namespaze = "stable"; + HookNameMetadata m2; + m2.name = "s2"; + m2.namespaze = "stable"; + HookNameMetadata m3; + m3.name = "s3"; + m3.namespaze = "stable"; + HookNameMetadata m4; + m4.name = "s4"; + m4.namespaze = "stable"; - if (!flamingo::Install(flamingo::HookInfo((void*)s1, hook_target.data(), &orig1, std::move(m1), HookPriority{})).has_value()) + if (!flamingo::Install(flamingo::HookInfo((void*)s1, hook_target.data(), &orig1, std::move(m1), HookPriority{})) + .has_value()) ERROR("Failed to install s1"); - if (!flamingo::Install(flamingo::HookInfo((void*)s2, hook_target.data(), &orig2, std::move(m2), HookPriority{})).has_value()) + if (!flamingo::Install(flamingo::HookInfo((void*)s2, hook_target.data(), &orig2, std::move(m2), HookPriority{})) + .has_value()) ERROR("Failed to install s2"); - if (!flamingo::Install(flamingo::HookInfo((void*)s3, hook_target.data(), &orig3, std::move(m3), HookPriority{})).has_value()) + if (!flamingo::Install(flamingo::HookInfo((void*)s3, hook_target.data(), &orig3, std::move(m3), HookPriority{})) + .has_value()) ERROR("Failed to install s3"); - if (!flamingo::Install(flamingo::HookInfo((void*)s4, hook_target.data(), &orig4, std::move(m4), HookPriority{})).has_value()) + if (!flamingo::Install(flamingo::HookInfo((void*)s4, hook_target.data(), &orig4, std::move(m4), HookPriority{})) + .has_value()) ERROR("Failed to install s4"); // Insert a hook that requests to be before the entire 'stable' namespace, @@ -664,9 +689,10 @@ static void test_order_stability() { HookNameMetadata mp; mp.name = "prior"; HookPriority pp; - HookNameFilter match_ns{"stable"}; + HookNameFilter match_ns{ "stable" }; pp.befores.push_back(match_ns); - if (!flamingo::Install(flamingo::HookInfo((void*)prior, hook_target.data(), &origPrior, std::move(mp), std::move(pp))).has_value()) + if (!flamingo::Install(flamingo::HookInfo((void*)prior, hook_target.data(), &origPrior, std::move(mp), std::move(pp))) + .has_value()) ERROR("Failed to install prior"); auto fixup_res = flamingo::FixupPointerFor(flamingo::TargetDescriptor(hook_target.data())); @@ -676,16 +702,14 @@ static void test_order_stability() { // Expect ordering: prior -> s4 -> s3 -> s2 -> s1 (newer installs at front, relative order preserved) if ((uintptr_t)origPrior != s4) ERROR("Stability: expected prior.orig == s4 (0x{:x}) got 0x{:x}", s4, (uintptr_t)origPrior); - if ((uintptr_t)orig4 != s3) - ERROR("Stability: expected s4.orig == s3 (0x{:x}) got 0x{:x}", s3, (uintptr_t)orig4); - if ((uintptr_t)orig3 != s2) - ERROR("Stability: expected s3.orig == s2 (0x{:x}) got 0x{:x}", s2, (uintptr_t)orig3); - if ((uintptr_t)orig2 != s1) - ERROR("Stability: expected s2.orig == s1 (0x{:x}) got 0x{:x}", s1, (uintptr_t)orig2); + if ((uintptr_t)orig4 != s3) ERROR("Stability: expected s4.orig == s3 (0x{:x}) got 0x{:x}", s3, (uintptr_t)orig4); + if ((uintptr_t)orig3 != s2) ERROR("Stability: expected s3.orig == s2 (0x{:x}) got 0x{:x}", s2, (uintptr_t)orig3); + if ((uintptr_t)orig2 != s1) ERROR("Stability: expected s2.orig == s1 (0x{:x}) got 0x{:x}", s1, (uintptr_t)orig2); if ((uintptr_t)orig1 != (uintptr_t)fixup_ptr) ERROR("Stability: expected s1.orig == fixup got 0x{:x}", (uintptr_t)orig1); } +// Expected chain (observed): mid -> before -> e3 -> e2 -> e1 -> after -> fixup static void test_mixed_priority_stability() { puts("Test: mixed-priority stability (some prioritized, others stable)"); uintptr_t m1 = 0xC1010001; @@ -704,42 +728,57 @@ static void test_mixed_priority_stability() { void* origMid = nullptr; void* origAfter = nullptr; - HookNameMetadata mm1; mm1.name = "e1"; mm1.namespaze = "mix"; - HookNameMetadata mm2; mm2.name = "e2"; mm2.namespaze = "mix"; - HookNameMetadata mm3; mm3.name = "e3"; mm3.namespaze = "mix"; - - if (!flamingo::Install(flamingo::HookInfo((void*)m1, hook_target.data(), &orig1, std::move(mm1), HookPriority{})).has_value()) + HookNameMetadata mm1; + mm1.name = "e1"; + mm1.namespaze = "mix"; + HookNameMetadata mm2; + mm2.name = "e2"; + mm2.namespaze = "mix"; + HookNameMetadata mm3; + mm3.name = "e3"; + mm3.namespaze = "mix"; + + if (!flamingo::Install(flamingo::HookInfo((void*)m1, hook_target.data(), &orig1, std::move(mm1), HookPriority{})) + .has_value()) ERROR("Failed to install m1"); - if (!flamingo::Install(flamingo::HookInfo((void*)m2, hook_target.data(), &orig2, std::move(mm2), HookPriority{})).has_value()) + if (!flamingo::Install(flamingo::HookInfo((void*)m2, hook_target.data(), &orig2, std::move(mm2), HookPriority{})) + .has_value()) ERROR("Failed to install m2"); - if (!flamingo::Install(flamingo::HookInfo((void*)m3, hook_target.data(), &orig3, std::move(mm3), HookPriority{})).has_value()) + if (!flamingo::Install(flamingo::HookInfo((void*)m3, hook_target.data(), &orig3, std::move(mm3), HookPriority{})) + .has_value()) ERROR("Failed to install m3"); // Install a hook that should come before the whole 'mix' namespace HookNameMetadata mb; mb.name = "before"; HookPriority pb; - HookNameFilter nm_ns{"mix"}; + HookNameFilter nm_ns{ "mix" }; pb.befores.push_back(nm_ns); - if (!flamingo::Install(flamingo::HookInfo((void*)before, hook_target.data(), &origBefore, std::move(mb), std::move(pb))).has_value()) + if (!flamingo::Install( + flamingo::HookInfo((void*)before, hook_target.data(), &origBefore, std::move(mb), std::move(pb))) + .has_value()) ERROR("Failed to install before hook"); // Install a hook that requests to be after the specific hook 'e2' HookNameMetadata mmid; mmid.name = "mid"; HookPriority pmid; - HookNameMetadata ref; ref.name = "e2"; + HookNameMetadata ref; + ref.name = "e2"; pmid.afters.emplace_back(ref); - if (!flamingo::Install(flamingo::HookInfo((void*)mid, hook_target.data(), &origMid, std::move(mmid), std::move(pmid))).has_value()) + if (!flamingo::Install(flamingo::HookInfo((void*)mid, hook_target.data(), &origMid, std::move(mmid), std::move(pmid))) + .has_value()) ERROR("Failed to install mid hook"); // Install a hook that should come after the whole 'mix' namespace HookNameMetadata ma; ma.name = "after"; HookPriority pa; - HookNameFilter nm_ns2; nm_ns2.namespaze = "mix"; + HookNameFilter nm_ns2; + nm_ns2.namespaze = "mix"; pa.afters.emplace_back(nm_ns2); - if (!flamingo::Install(flamingo::HookInfo((void*)after, hook_target.data(), &origAfter, std::move(ma), std::move(pa))).has_value()) + if (!flamingo::Install(flamingo::HookInfo((void*)after, hook_target.data(), &origAfter, std::move(ma), std::move(pa))) + .has_value()) ERROR("Failed to install after hook"); auto fixup_res = flamingo::FixupPointerFor(flamingo::TargetDescriptor(hook_target.data())); @@ -761,7 +800,7 @@ static void test_mixed_priority_stability() { ERROR("Mixed-stability: expected after.orig == fixup got 0x{:x}", (uintptr_t)origAfter); } - +// Expected chain for reinstall: reinst -> fixup static void test_reinstall() { puts("Test: reinstall"); uintptr_t h = 0xA0001001; @@ -786,6 +825,9 @@ static void test_reinstall() { if ((uintptr_t)orig != (uintptr_t)fixup_ptr) ERROR("Reinstall: expected orig == fixup after reinstall"); } +// Expected initial chain: u2 -> u1 -> fixup; +// after removing u2: u1 -> fixup; +// after removing u1: no fixup static void test_uninstall() { puts("Test: uninstall"); uintptr_t h1 = 0xB0010001; @@ -796,11 +838,13 @@ static void test_uninstall() { void* orig1 = nullptr; void* orig2 = nullptr; - auto r1 = flamingo::Install(flamingo::HookInfo((void*)h1, hook_target.data(), &orig1, HookNameMetadata{.name = "u1"}, HookPriority{})); + auto r1 = flamingo::Install( + flamingo::HookInfo((void*)h1, hook_target.data(), &orig1, HookNameMetadata{ .name = "u1" }, HookPriority{})); if (!r1.has_value()) ERROR("Failed to install u1: {}", r1.error()); auto handle1 = r1.value().returned_handle; - auto r2 = flamingo::Install(flamingo::HookInfo((void*)h2, hook_target.data(), &orig2, HookNameMetadata{.name = "u2"}, HookPriority{})); + auto r2 = flamingo::Install( + flamingo::HookInfo((void*)h2, hook_target.data(), &orig2, HookNameMetadata{ .name = "u2" }, HookPriority{})); if (!r2.has_value()) ERROR("Failed to install u2: {}", r2.error()); auto handle2 = r2.value().returned_handle; @@ -823,7 +867,6 @@ static void test_uninstall() { if (fix_final.has_value()) ERROR("Expected no fixup pointer after removing last hook"); } - int main() { test_name_matching(); test_namespaze_matching(); From 88c7a7a6d7924b949aad73bf8d4818d795d0ee0b Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:21:31 -0400 Subject: [PATCH 127/134] Refactor hook priority validation logic and remove redundant self-reference checks --- src/installer.cpp | 73 +++++++++++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/src/installer.cpp b/src/installer.cpp index 3b0610c..14d2e0d 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -65,27 +65,6 @@ Result, installation::TargetBadPriorities> topological_sort_ finals.push_back(it); } name_to_iterator[it->metadata.name_info] = it; - - // check if any reference self in after/before - for (auto const& after : it->metadata.priority.afters) { - if (after.matches(it->metadata.name_info)) { - FLAMINGO_WARN("Hook {} references itself in after dependencies. This is likely a mistake.", - it->metadata.name_info.name); - - return ResultT::Err(installation::TargetBadPriorities{ - it->metadata, fmt::format("Hook {} references itself in after dependencies. This is likely a mistake.", - it->metadata.name_info.name) }); - } - } - for (auto const& before : it->metadata.priority.befores) { - if (before.matches(it->metadata.name_info)) { - FLAMINGO_WARN("Hook {} references itself in before dependencies. This is likely a mistake.", - it->metadata.name_info.name); - return ResultT::Err(installation::TargetBadPriorities{ - it->metadata, fmt::format("Hook {} references itself in before dependencies. This is likely a mistake.", - it->metadata.name_info.name) }); - } - } } for (auto& it : finals) { hooks.splice(hooks.end(), hooks, it); @@ -272,12 +251,7 @@ Result::iterator, installation::TargetBadPriorities> find_su // Also, if we have a final priority, we need to be the final hook, unless that hook is itself already marked as // final. if (hook_to_install.metadata.priority.is_final) { - if (!hooks.empty() && hooks.back().metadata.priority.is_final) { - // We cannot install here, we have a conflict - return ResultT::Err(installation::TargetBadPriorities{ - hook_to_install.metadata, fmt::format("Cannot install a 'final' hook after another 'final' hook with name: {}", - hooks.back().metadata.name_info) }); - } + // we don't validate here since it's done in the Install function // Select the end to install at hooks.emplace_back(std::move(hook_to_install)); @@ -410,6 +384,46 @@ Result validate_install_metadata(T return ResultT::Ok(); } +Result validate_priority_constraints_for_new_hook( + std::list const& existing_hooks, HookMetadata const& incoming) { + using ResultT = Result; + // Validate that the new hook's priorities do not conflict with existing hooks + // For each existing hook, we need to ensure that if the new hook wants to be before it, it is actually before it, and + // if it wants to be after it, it is actually after it. We also need to ensure that if the new hook has a final priority, + // it is the last hook. + + // Check final priority constraints first + if (incoming.priority.is_final) { + // we can just check the end because final hooks must be at the end + if (!existing_hooks.empty() && existing_hooks.back().metadata.priority.is_final) { + return ResultT::Err(installation::TargetBadPriorities{ + incoming, fmt::format("Cannot install a 'final' hook after another 'final' hook with name: {}", + existing_hooks.back().metadata.name_info) }); + } + } + + // Now check before/after constraints + for (auto const& existing : existing_hooks) { + for (auto const& afterFilter : incoming.priority.afters) { + if (afterFilter.matches(existing.metadata.name_info)) { + return ResultT::Err(installation::TargetBadPriorities{ + incoming, fmt::format("Cannot install hook because it requests to be after hook with name: {} but that " + "hook is already installed before it.", + existing.metadata.name_info) }); + } + } + for (auto const& beforeFilter : incoming.priority.befores) { + if (beforeFilter.matches(existing.metadata.name_info)) { + return ResultT::Err(installation::TargetBadPriorities{ + incoming, fmt::format("Cannot install hook because it requests to be before hook with name: {} but that " + "hook is already installed after it.", + existing.metadata.name_info) }); + } + } + } + + return ResultT::Ok(); + } // namespace namespace flamingo { @@ -488,6 +502,11 @@ installation::Result Install(HookInfo&& hook) { return installation::Result::ErrAt(installation_checks.error()); } + auto priority_checks = validate_priority_constraints_for_new_hook(hooked_target->second.hooks, hook.metadata); + if (!priority_checks.has_value()) { + return installation::Result::ErrAt(priority_checks.error()); + } + auto location_or_err = find_suitable_priority_location_for(hooked_target->second.hooks, std::move(hook)); if (!location_or_err.has_value()) { return installation::Result::ErrAt(location_or_err.error()); From cf33ac551765a582135e0cf445710eba96f0de66 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:22:14 -0400 Subject: [PATCH 128/134] Add formatted output for HookInfo and simplify hook sorting logic --- shared/hook-data.hpp | 6 ++++++ src/installer.cpp | 41 ++++++----------------------------------- 2 files changed, 12 insertions(+), 35 deletions(-) diff --git a/shared/hook-data.hpp b/shared/hook-data.hpp index b114c63..7c4cbba 100644 --- a/shared/hook-data.hpp +++ b/shared/hook-data.hpp @@ -129,4 +129,10 @@ struct HookInfo { HookMetadata metadata; }; +inline auto format_as(HookInfo const& hook) { + return fmt::format("HookInfo {{ target: 0x{:x}, hook_ptr: 0x{:x}, name: {}, namespace: {} }}", (uintptr_t)hook.target, + (uintptr_t)hook.hook_ptr, hook.metadata.name_info.name, hook.metadata.name_info.namespaze); +} + + } // namespace flamingo diff --git a/src/installer.cpp b/src/installer.cpp index 14d2e0d..dbce803 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -151,13 +151,10 @@ Result, installation::TargetBadPriorities> topological_sort_ zero_in_degree.pop(); // find the iterator for this name - auto it = name_to_iterator.find(current_name); - if (it == name_to_iterator.end()) { - // should not happen - continue; - } + auto it = name_to_iterator[current_name]; + // move to sorted_hooks - sorted_hooks.splice(sorted_hooks.end(), hooks, it->second); + sorted_hooks.splice(sorted_hooks.end(), hooks, it); // decrease in_degree of afters auto const& befores = graph[current_name]; @@ -262,34 +259,8 @@ Result::iterator, installation::TargetBadPriorities> find_su // if existing hooks have priority constraints that depend on us, we need to respect those // therefore topological - // If the incoming hook has any priority constraints, we may need a topological pass. - bool requires_sort = - !hook_to_install.metadata.priority.afters.empty() || !hook_to_install.metadata.priority.befores.empty(); - - // If any existing hook has constraints that reference the incoming hook, we must sort. - for (auto const& existing_hook : hooks) { - for (auto const& after_filter : existing_hook.metadata.priority.afters) { - if (after_filter.matches(hook_to_install.metadata.name_info)) { - requires_sort = true; - break; - } - } - // if existing_hook requests to be before us, we cannot install before it - for (auto const& before_filter : existing_hook.metadata.priority.befores) { - if (before_filter.matches(hook_to_install.metadata.name_info)) { - requires_sort = true; - break; - } - } - if (requires_sort) { - break; - } - } - - // if we require a sort, do it then recompile - if (requires_sort) { // copy hooks - auto old_hooks = hooks; + auto old_hooks = std::list(hooks); TargetDescriptor target{ hook_to_install.target }; auto metadata = hook_to_install.metadata; @@ -314,8 +285,8 @@ Result::iterator, installation::TargetBadPriorities> find_su hooks.swap(old_hooks); return ResultT::Err(installation::TargetBadPriorities{ - metadata, fmt::format("Cannot install hook due to cycles in priorities involving hook name: {}", - fmt::join(cycle_strings, ",")) }); + metadata, fmt::format("Cannot install hook due to cycles in priorities involving hook name:\n\t{}", + fmt::join(cycles, "\n\t")) }); } // now recompile all hooks to ensure orig pointers are correct From 4dbf909562bf05355af763d9206ba9a87632d78f Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:27:56 -0400 Subject: [PATCH 129/134] Fix accident --- src/installer.cpp | 54 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/src/installer.cpp b/src/installer.cpp index dbce803..0d22154 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -151,10 +151,13 @@ Result, installation::TargetBadPriorities> topological_sort_ zero_in_degree.pop(); // find the iterator for this name - auto it = name_to_iterator[current_name]; - + auto it = name_to_iterator.find(current_name); + if (it == name_to_iterator.end()) { + // should not happen + continue; + } // move to sorted_hooks - sorted_hooks.splice(sorted_hooks.end(), hooks, it); + sorted_hooks.splice(sorted_hooks.end(), hooks, it->second); // decrease in_degree of afters auto const& befores = graph[current_name]; @@ -259,8 +262,34 @@ Result::iterator, installation::TargetBadPriorities> find_su // if existing hooks have priority constraints that depend on us, we need to respect those // therefore topological + // If the incoming hook has any priority constraints, we may need a topological pass. + bool requires_sort = + !hook_to_install.metadata.priority.afters.empty() || !hook_to_install.metadata.priority.befores.empty(); + + // If any existing hook has constraints that reference the incoming hook, we must sort. + for (auto const& existing_hook : hooks) { + for (auto const& after_filter : existing_hook.metadata.priority.afters) { + if (after_filter.matches(hook_to_install.metadata.name_info)) { + requires_sort = true; + break; + } + } + // if existing_hook requests to be before us, we cannot install before it + for (auto const& before_filter : existing_hook.metadata.priority.befores) { + if (before_filter.matches(hook_to_install.metadata.name_info)) { + requires_sort = true; + break; + } + } + if (requires_sort) { + break; + } + } + + // if we require a sort, do it then recompile + if (requires_sort) { // copy hooks - auto old_hooks = std::list(hooks); + auto old_hooks = hooks; TargetDescriptor target{ hook_to_install.target }; auto metadata = hook_to_install.metadata; @@ -275,18 +304,12 @@ Result::iterator, installation::TargetBadPriorities> find_su if (!cycles.empty()) { // We have cycles involving our new hook - // Remove our new hook - std::vector cycle_strings; - for (auto const& hook : cycles) { - cycle_strings.push_back(hook.metadata.name_info.name); - } - // revert hooks (we need to keep original order) hooks.swap(old_hooks); return ResultT::Err(installation::TargetBadPriorities{ - metadata, fmt::format("Cannot install hook due to cycles in priorities involving hook name:\n\t{}", - fmt::join(cycles, "\n\t")) }); + metadata, fmt::format("Cannot install hook due to cycles in priorities involving hook name:\n\t{}", + fmt::join(cycles, "\n\t")) }); } // now recompile all hooks to ensure orig pointers are correct @@ -360,8 +383,8 @@ Result validate_priority_cons using ResultT = Result; // Validate that the new hook's priorities do not conflict with existing hooks // For each existing hook, we need to ensure that if the new hook wants to be before it, it is actually before it, and - // if it wants to be after it, it is actually after it. We also need to ensure that if the new hook has a final priority, - // it is the last hook. + // if it wants to be after it, it is actually after it. We also need to ensure that if the new hook has a final + // priority, it is the last hook. // Check final priority constraints first if (incoming.priority.is_final) { @@ -394,9 +417,8 @@ Result validate_priority_cons } return ResultT::Ok(); - +} } // namespace - namespace flamingo { std::optional TargetDataFor(TargetDescriptor target) { auto it = targets.find(target); From 1036a36f327de87d97a07aa2ea3abb61c6954041 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:36:49 -0400 Subject: [PATCH 130/134] Change check to self priority. --- src/installer.cpp | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/installer.cpp b/src/installer.cpp index 0d22154..b929bfd 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -396,23 +396,19 @@ Result validate_priority_cons } } - // Now check before/after constraints - for (auto const& existing : existing_hooks) { - for (auto const& afterFilter : incoming.priority.afters) { - if (afterFilter.matches(existing.metadata.name_info)) { - return ResultT::Err(installation::TargetBadPriorities{ - incoming, fmt::format("Cannot install hook because it requests to be after hook with name: {} but that " - "hook is already installed before it.", - existing.metadata.name_info) }); - } + // Prevent a hook from declaring priorities that reference itself (self-priority). + for (auto const& afterFilter : incoming.priority.afters) { + if (afterFilter.matches(incoming.name_info)) { + return ResultT::Err(installation::TargetBadPriorities{ + incoming, + fmt::format("Cannot install hook because it requests to be after itself: {}", incoming.name_info)}); } - for (auto const& beforeFilter : incoming.priority.befores) { - if (beforeFilter.matches(existing.metadata.name_info)) { - return ResultT::Err(installation::TargetBadPriorities{ - incoming, fmt::format("Cannot install hook because it requests to be before hook with name: {} but that " - "hook is already installed after it.", - existing.metadata.name_info) }); - } + } + for (auto const& beforeFilter : incoming.priority.befores) { + if (beforeFilter.matches(incoming.name_info)) { + return ResultT::Err(installation::TargetBadPriorities{ + incoming, + fmt::format("Cannot install hook because it requests to be before itself: {}", incoming.name_info)}); } } From 79b6303df15f06de34781f4cc5ac9b6bd47f4dc5 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:40:08 -0400 Subject: [PATCH 131/134] Add filtered hook retrieval functions and corresponding memory management --- shared/capi.h | 11 +++++++++ shared/installer.hpp | 10 ++++++++ src/capi.cpp | 57 ++++++++++++++++++++++++++++++++++++++++++++ src/installer.cpp | 21 ++++++++++++++++ 4 files changed, 99 insertions(+) diff --git a/shared/capi.h b/shared/capi.h index e7cd55d..dd52a9d 100644 --- a/shared/capi.h +++ b/shared/capi.h @@ -297,6 +297,17 @@ FLAMINGO_C_EXPORT size_t flamingo_get_hooks(uint32_t* target, FlamingoHookInfo* /// by `flamingo_get_hooks`. Does NOT free the `hooks` array itself; the caller is responsible for that. FLAMINGO_C_EXPORT_VOID void flamingo_free_hooks_array(FlamingoHookInfo* hooks, size_t length); +/// @brief Allocates and returns an array of `FlamingoHookInfo` matching `filter` and `target`. +/// If `filter` is NULL, no name/namespace filtering is applied. If `target` is NULL, hooks across all targets +/// are considered. +/// The returned pointer is malloc'd and must be freed with `flamingo_free_hooks_info_array`. +/// The actual number of entries is written to `out_count` (may be NULL if caller doesn't need it). +FLAMINGO_C_EXPORT FlamingoHookInfo* flamingo_get_hooks_filtered(FlamingoHookFilter* filter, + uint32_t* target, size_t* out_count); + +/// @brief Frees an array returned by `flamingo_get_hooks_filtered`, including per-entry strings. +FLAMINGO_C_EXPORT_VOID void flamingo_free_hooks_info_array(FlamingoHookInfo* hooks, size_t length); + #ifdef __cplusplus } #endif diff --git a/shared/installer.hpp b/shared/installer.hpp index ddcaa19..90142dd 100644 --- a/shared/installer.hpp +++ b/shared/installer.hpp @@ -3,6 +3,9 @@ #include #include #include +#include +#include +#include #include "hook-data.hpp" #include "hook-installation-result.hpp" #include "target-data.hpp" @@ -63,4 +66,11 @@ std::span FLAMINGO_EXPORT OriginalInstsFor(TargetDescriptor target); [[nodiscard]] FLAMINGO_EXPORT Result, std::monostate> FixupPointerFor( TargetDescriptor target); +/// @brief Returns a list of installed hooks. +/// @param filter Optional name filter; if provided only hooks matching the filter are returned. +/// @param target Optional target filter; if provided only hooks installed on that target are returned. +[[nodiscard]] FLAMINGO_EXPORT std::vector Hooks( + std::optional const& filter = std::nullopt, + std::optional const& target = std::nullopt); + } // namespace flamingo diff --git a/src/capi.cpp b/src/capi.cpp index cbb3327..273c3f8 100644 --- a/src/capi.cpp +++ b/src/capi.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include "calling-convention.hpp" @@ -303,6 +305,61 @@ FLAMINGO_C_EXPORT size_t flamingo_get_hooks(uint32_t* target, FlamingoHookInfo* return to_copy; } +// Provide a single allocation-style filtered query and a matching free function below. + +FLAMINGO_C_EXPORT FlamingoHookInfo* flamingo_get_hooks_filtered(FlamingoHookFilter* filter, uint32_t* target, + size_t* out_count) { + std::optional opt_filter = std::nullopt; + if (filter != nullptr) { + auto const& f = *reinterpret_cast(filter); + opt_filter = flamingo::HookNameFilter{ f }; + } + std::optional td = std::nullopt; + if (target != nullptr) { + td = flamingo::TargetDescriptor{ .target = target }; + } + + auto v = flamingo::Hooks(opt_filter, td); + size_t count = v.size(); + if (out_count != nullptr) *out_count = count; + if (count == 0) { + return nullptr; + } + + auto* arr = static_cast(std::malloc(sizeof(FlamingoHookInfo) * count)); + + // populate + for (size_t i = 0; i < count; ++i) { + auto const& orig = v[i]; + auto& c = arr[i]; + + c.hook_ptr = orig.hook_ptr; + c.orig_ptr = orig.orig_ptr; + if (orig.metadata.name_info.name.empty()) { + c.name = nullptr; + } else { + c.name = static_cast(std::malloc(orig.metadata.name_info.name.size() + 1)); + std::strcpy(c.name, orig.metadata.name_info.name.c_str()); + } + if (orig.metadata.name_info.namespaze.empty()) { + c.namespaze = nullptr; + } else { + c.namespaze = static_cast(std::malloc(orig.metadata.name_info.namespaze.size() + 1)); + std::strcpy(c.namespaze, orig.metadata.name_info.namespaze.c_str()); + } + } + return arr; +} + +FLAMINGO_C_EXPORT_VOID void flamingo_free_hooks_info_array(FlamingoHookInfo* hooks, size_t length) { + if (hooks == nullptr) return; + for (size_t i = 0; i < length; ++i) { + if (hooks[i].name != nullptr) std::free(hooks[i].name); + if (hooks[i].namespaze != nullptr) std::free(hooks[i].namespaze); + } + std::free(hooks); +} + FLAMINGO_C_EXPORT_VOID void flamingo_free_hooks_array(FlamingoHookInfo* hooks, size_t length) { if (hooks == nullptr) return; diff --git a/src/installer.cpp b/src/installer.cpp index b929bfd..387b9eb 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -608,4 +608,25 @@ Result, std::monostate> FixupPointerFor(TargetDescript return Result, std::monostate>::Err(); } +std::vector Hooks(std::optional const& filter, + std::optional const& targetFilter) { + std::vector out; + out.reserve(16); + for (auto const& target_pair : targets) { + // If a target filter is provided, skip other targets + if (targetFilter.has_value()) { + if (target_pair.first.target != targetFilter->target) { + continue; + } + } + for (auto const& hook : target_pair.second.hooks) { + if (filter.has_value()) { + if (!filter->matches(hook.metadata.name_info)) continue; + } + out.push_back(hook); + } + } + return out; +} + } // namespace flamingo \ No newline at end of file From c9ca51c19d5f328de0898b6e90109d5e7e59699c Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:40:37 -0400 Subject: [PATCH 132/134] use void* for target --- shared/capi.h | 2 +- src/capi.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shared/capi.h b/shared/capi.h index dd52a9d..91dc405 100644 --- a/shared/capi.h +++ b/shared/capi.h @@ -303,7 +303,7 @@ FLAMINGO_C_EXPORT_VOID void flamingo_free_hooks_array(FlamingoHookInfo* hooks, s /// The returned pointer is malloc'd and must be freed with `flamingo_free_hooks_info_array`. /// The actual number of entries is written to `out_count` (may be NULL if caller doesn't need it). FLAMINGO_C_EXPORT FlamingoHookInfo* flamingo_get_hooks_filtered(FlamingoHookFilter* filter, - uint32_t* target, size_t* out_count); + void* target, size_t* out_count); /// @brief Frees an array returned by `flamingo_get_hooks_filtered`, including per-entry strings. FLAMINGO_C_EXPORT_VOID void flamingo_free_hooks_info_array(FlamingoHookInfo* hooks, size_t length); diff --git a/src/capi.cpp b/src/capi.cpp index 273c3f8..8ee3df1 100644 --- a/src/capi.cpp +++ b/src/capi.cpp @@ -307,7 +307,7 @@ FLAMINGO_C_EXPORT size_t flamingo_get_hooks(uint32_t* target, FlamingoHookInfo* // Provide a single allocation-style filtered query and a matching free function below. -FLAMINGO_C_EXPORT FlamingoHookInfo* flamingo_get_hooks_filtered(FlamingoHookFilter* filter, uint32_t* target, +FLAMINGO_C_EXPORT FlamingoHookInfo* flamingo_get_hooks_filtered(FlamingoHookFilter* filter, void* target, size_t* out_count) { std::optional opt_filter = std::nullopt; if (filter != nullptr) { From 0600c9a4535f0a9a3aaa9d144646485087d8eb42 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:43:28 -0400 Subject: [PATCH 133/134] Change logs to debug --- src/installer.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/installer.cpp b/src/installer.cpp index 387b9eb..60e8b13 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -177,12 +177,12 @@ Result, installation::TargetBadPriorities> topological_sort_ if (!hooks.empty()) { for (auto const& hook : hooks) { - FLAMINGO_CRITICAL( + FLAMINGO_DEBUG( "Detected cycle in hook priorities involving hook name: {}. Hooks involved in the cycle will remain in their " "original order.", hook.metadata.name_info); - FLAMINGO_CRITICAL("After priorities for this hook were: {}", fmt::join(hook.metadata.priority.afters, ", ")); - FLAMINGO_CRITICAL("Before priorities for this hook were: {}", fmt::join(hook.metadata.priority.befores, ", ")); + FLAMINGO_DEBUG("After priorities for this hook were: {}", fmt::join(hook.metadata.priority.afters, ", ")); + FLAMINGO_DEBUG("Before priorities for this hook were: {}", fmt::join(hook.metadata.priority.befores, ", ")); } // TODO: Restore hooks list to original state? From d71a7318253bc7767273f074388780ca64b97ec7 Mon Sep 17 00:00:00 2001 From: Fernthedev <15272073+Fernthedev@users.noreply.github.com> Date: Wed, 6 May 2026 01:40:57 -0400 Subject: [PATCH 134/134] Seperate graph building and topo sorting as per scad's instructions (done with AI) --- shared/hook-metadata.hpp | 14 ++- shared/target-data.hpp | 11 +++ src/installer.cpp | 197 +++++++++++++++------------------------ 3 files changed, 100 insertions(+), 122 deletions(-) diff --git a/shared/hook-metadata.hpp b/shared/hook-metadata.hpp index fa1a36e..ab4f5e0 100644 --- a/shared/hook-metadata.hpp +++ b/shared/hook-metadata.hpp @@ -103,6 +103,18 @@ class fmt::formatter { } template constexpr auto format(flamingo::HookNameFilter const& filter, Context& ctx) const { - return fmt::format_to(ctx.out(), "name: {} namespace {}", filter.name.value_or("*"), filter.namespaze.value_or("*")); + return fmt::format_to(ctx.out(), "name: {} namespace {}", filter.name.value_or("*"), + filter.namespaze.value_or("*")); } }; + +// HookNameMetadata hash +namespace std { +template <> +struct hash { + std::size_t operator()(flamingo::HookNameMetadata const& k) const { + return std::hash()(k.name) ^ (std::hash()(k.namespaze) << 1); + } +}; + +} // namespace std \ No newline at end of file diff --git a/shared/target-data.hpp b/shared/target-data.hpp index d79cf96..046c986 100644 --- a/shared/target-data.hpp +++ b/shared/target-data.hpp @@ -3,6 +3,9 @@ #include #include #include +#include +#include +#include #include "fixups.hpp" #include "hook-data.hpp" @@ -37,6 +40,14 @@ struct TargetData { TargetMetadata metadata; Fixups fixups; std::list hooks{}; + struct GraphNode { + // represents the hook's iterator in the TargetData::hooks + std::list::iterator hook_it; + // adjacency list of nodes that should come after this node (edges: this -> after) + std::vector afters; + }; + + std::unordered_map priority_graph; }; /// @brief A handle to an installed hook. Used for uninstalls. diff --git a/src/installer.cpp b/src/installer.cpp index 60e8b13..9b53406 100644 --- a/src/installer.cpp +++ b/src/installer.cpp @@ -31,115 +31,70 @@ using namespace flamingo; /// @brief The set of all targets hooked. An ordered map so we can perform large-scale walks by doing binary search. inline static std::map targets; -struct HookNameMetadataHash { - std::size_t operator()(HookNameMetadata const& k) const { - return std::hash()(k.name) ^ (std::hash()(k.namespaze) << 1); - } -}; - -/// @brief Topologically sorts the provided hooks by their priority constraints. -/// @param hooks The list of hooks to sort in place. -/// @return The sorted list of hooks that have cycles. -Result, installation::TargetBadPriorities> topological_sort_hooks_by_priority( - std::list& hooks) { - using ResultT = Result, installation::TargetBadPriorities>; - // Ensure any "final" priority hooks are placed at the end while preserving relative order. - std::vector::iterator> finals; - finals.reserve(hooks.size()); - - auto LogHooks = [](auto&& s, std::list const& hooks) { - std::vector hook_names; - hook_names.reserve(hooks.size()); - for (auto const& hook : hooks) { - hook_names.push_back(hook.metadata.name_info.name); - } - // TODO: Can we avoid runtime string here? - FLAMINGO_DEBUG(fmt::runtime(s), fmt::join(hook_names, " -> ")); - }; +// Rebuild the per-target priority graph from the current hooks list. +void rebuild_priority_graph(TargetData& target_data) { + target_data.priority_graph.clear(); - // build name to iterator map - std::unordered_map::iterator, HookNameMetadataHash> name_to_iterator; - - for (auto it = hooks.begin(); it != hooks.end(); ++it) { - if (it->metadata.priority.is_final) { - finals.push_back(it); - } - name_to_iterator[it->metadata.name_info] = it; - } - for (auto& it : finals) { - hooks.splice(hooks.end(), hooks, it); + // First, create nodes for every hook with iterator + for (auto it = target_data.hooks.begin(); it != target_data.hooks.end(); ++it) { + target_data.priority_graph[it->metadata.name_info] = TargetData::GraphNode{ .hook_it = it, .afters = {} }; } - LogHooks("Initial hook order before topological sort: {}", hooks); - - // now build topological graph - // each hook has a requirement to be after certain other hooks in this graph - // we also convert before requirements to after requirements for easier processing - - // A before B == B after A - // HookInfo after strings - // this graph represents all the hooks and their before dependencies - // if A must be before B, we have an edge A -> B, meaning A must come before B - std::unordered_map, HookNameMetadataHash> graph; - graph.reserve(hooks.size()); - - // finds all hooks that match the given filter + // Helper to find matches for a filter among current hooks auto findMatches = [&](HookNameFilter const& filter) { std::vector matches; - matches.reserve(1); - for (auto const& hook : hooks) { - auto const& name = hook.metadata.name_info; - if (filter.matches(name)) { - matches.push_back(name); + for (auto const& hook : target_data.hooks) { + if (filter.matches(hook.metadata.name_info)) { + matches.push_back(hook.metadata.name_info); } } return matches; }; - // - for (auto const& hook : hooks) { - // build afters first - // If this hook specifies that it must come after some other hooks (A), - // then we must add edges A -> this_hook in the graph (so that A precedes this). + // Build adjacency (afters) using same semantics as previous implementation + for (auto const& hook : target_data.hooks) { + // afters: for each afterFilter, matched -> this_hook for (auto const& afterFilter : hook.metadata.priority.afters) { auto matches = findMatches(afterFilter); for (auto const& matched : matches) { - graph[matched].push_back(hook.metadata.name_info); + target_data.priority_graph[matched].afters.push_back(hook.metadata.name_info); } } - - // now build from befores - // If this hook requests to be before some matched hooks, add edges current -> matched + // befores: current -> matched_before for (auto const& beforeFilter : hook.metadata.priority.befores) { auto matches = findMatches(beforeFilter); for (auto const& matched_before : matches) { - graph[hook.metadata.name_info].push_back(matched_before); + target_data.priority_graph[hook.metadata.name_info].afters.push_back(matched_before); + } } } } - // now topogologically sort in place - // topological sort should use HookNameMetadata.matches(HookNameMetadata const& other) for matching - // we cannot invalidate list iterators - // for hooks with cycle, we keep them in their original order - // we use Kahn's algorithm for topological sorting - // https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm +// Topologically sort hooks for a single target using its persisted graph. +static Result, installation::TargetBadPriorities> topological_sort_target(TargetData& target_data) { + using ResultT = Result, installation::TargetBadPriorities>; + std::list sorted_hooks; - std::unordered_map in_degree; - for (auto const& hook : hooks) { - in_degree[hook.metadata.name_info] = 0; + + // build in-degree map + std::unordered_map in_degree; + for (auto const& pair : target_data.priority_graph) { + in_degree[pair.first] = 0; } - // compute in-degrees - for (auto const& [name, befores] : graph) { - for (auto const& before : befores) { - in_degree[before]++; + // calculate in-degrees + // Note that we only consider nodes in the graph for topological sorting, + // which means that hooks without priority constraints will not be affected by this process and will remain in their original order at the end of the sorted list. + // This is intentional, as it allows us to preserve the original order of hooks that don't have any priority constraints, while still ensuring that hooks with constraints are ordered correctly. + for (auto const& [name, node] : target_data.priority_graph) { + for (auto const& after : node.afters) { + in_degree[after]++; } } - // find all nodes with in_degree 0, preserving the original hooks list order + // queue of zero in-degree nodes, preserve original hooks list order std::queue zero_in_degree; - for (auto const& hook : hooks) { + for (auto const& hook : target_data.hooks) { auto it_deg = in_degree.find(hook.metadata.name_info); if (it_deg != in_degree.end() && it_deg->second == 0) { zero_in_degree.push(hook.metadata.name_info); @@ -150,53 +105,47 @@ Result, installation::TargetBadPriorities> topological_sort_ auto current_name = zero_in_degree.front(); zero_in_degree.pop(); - // find the iterator for this name - auto it = name_to_iterator.find(current_name); - if (it == name_to_iterator.end()) { - // should not happen - continue; + auto it_node = target_data.priority_graph.find(current_name); + if (it_node == target_data.priority_graph.end()) { + continue; // shouldn't happen } - // move to sorted_hooks - sorted_hooks.splice(sorted_hooks.end(), hooks, it->second); - - // decrease in_degree of afters - auto const& befores = graph[current_name]; - for (auto const& before : befores) { - in_degree[before]--; - if (in_degree[before] == 0) { - zero_in_degree.push(before); + + // move node's hook iterator into sorted_hooks + sorted_hooks.splice(sorted_hooks.end(), target_data.hooks, it_node->second.hook_it); + + for (auto const& after : it_node->second.afters) { + in_degree[after]--; + if (in_degree[after] == 0) { + zero_in_degree.push(after); } } } - LogHooks("Flattened hook order after topological sort (before cycle processing): {}", sorted_hooks); - - // now, any remaining hooks in `hooks` are part of cycles - // append them in their original order and log a warning. Splicing invalidates - // iterators, so consume from the front until empty to preserve original order. + // if any hooks remain, they are part of cycles + if (!target_data.hooks.empty()) { + std::vector cycles; + for (auto const& h : target_data.hooks) cycles.push_back(h); - if (!hooks.empty()) { - for (auto const& hook : hooks) { + if (!target_data.hooks.empty()) { + for (auto const& hook : target_data.hooks) { FLAMINGO_DEBUG( - "Detected cycle in hook priorities involving hook name: {}. Hooks involved in the cycle will remain in their " + "Detected cycle in hook priorities involving hook name: {}. Hooks involved in the cycle will remain in " + "their " "original order.", hook.metadata.name_info); FLAMINGO_DEBUG("After priorities for this hook were: {}", fmt::join(hook.metadata.priority.afters, ", ")); FLAMINGO_DEBUG("Before priorities for this hook were: {}", fmt::join(hook.metadata.priority.befores, ", ")); + } } - // TODO: Restore hooks list to original state? return ResultT::Err(installation::TargetBadPriorities{ - hooks.front().metadata, fmt::format("Detected cycle in hook priorities involving hooks. Hooks " + target_data.hooks.front().metadata, fmt::format("Detected cycle in hook priorities involving hooks. Hooks " "involved in the cycle will remain in their original order.") }); } - // replace original list with the sorted result using swap to avoid reallocation - hooks.swap(sorted_hooks); - - LogHooks("Final hook order after topological sort: {}", hooks); + // swap sorted back into target_data.hooks + target_data.hooks.swap(sorted_hooks); - // remaining hooks are cycles return ResultT::Ok(sorted_hooks); } @@ -296,20 +245,19 @@ Result::iterator, installation::TargetBadPriorities> find_su // Insert the new hook first so we can let topo sort place it correctly auto newIt = hooks.insert(hooks.begin(), std::move(hook_to_install)); // if our hook has priority constraints, we need to topologically sort and find a suitable location - auto cyclesResult = topological_sort_hooks_by_priority(hooks); - if (!cyclesResult.has_value()) { - return ResultT::Err(cyclesResult.error()); + auto trg_it = targets.find(target); + if (trg_it == targets.end()) { + // should not happen + hooks.swap(old_hooks); + return ResultT::Err(installation::TargetBadPriorities{ metadata, "Target missing during priority sorting" }); } - auto cycles = cyclesResult.value(); - - if (!cycles.empty()) { - // We have cycles involving our new hook + auto& target_data = trg_it->second; + rebuild_priority_graph(target_data); + auto cyclesResult = topological_sort_target(target_data); + if (!cyclesResult.has_value()) { // revert hooks (we need to keep original order) hooks.swap(old_hooks); - - return ResultT::Err(installation::TargetBadPriorities{ - metadata, fmt::format("Cannot install hook due to cycles in priorities involving hook name:\n\t{}", - fmt::join(cycles, "\n\t")) }); + return ResultT::Err(cyclesResult.error()); } // now recompile all hooks to ensure orig pointers are correct @@ -470,7 +418,10 @@ installation::Result Install(HookInfo&& hook) { std::min(Page::PageSize, hook.metadata.method_num_insts * sizeof(uint32_t) * kNumFixupsPerInst), PageProtectionType::kExecute | PageProtectionType::kRead), - } }); + + }, + .priority_graph = {}, + }); auto& target_data = result.first->second; hook.assign_orig(reinterpret_cast(&no_fixups)); // Always copy over our original instructions to our .fixups instance @@ -482,6 +433,8 @@ installation::Result Install(HookInfo&& hook) { } // Add the hook itself to the set of hooks we have, taking ownership auto const hook_data_result = target_data.hooks.emplace(target_data.hooks.end(), std::move(hook)); + // Build the initial priority graph for this target now that we have a hook + rebuild_priority_graph(target_data); // Now actually INSTALL the hook at target to point to the first hook in target_data.hooks target_data.fixups.target.WriteJump(hook_data_result->hook_ptr); return installation::Result::Ok(flamingo::installation::Ok{ HookHandle{ .hook_location = hook_data_result } }); @@ -581,6 +534,8 @@ Result Uninstall(HookHandle handle) { // targets map is destroyed. Note that this invalidates all other held HookHandles to the SAME entry. Other entries // will not be invalidated. target_entry->second.hooks.erase(handle.hook_location); + // Rebuild the priority graph to reflect the removed hook + rebuild_priority_graph(target_entry->second); return RetType::Ok(true); }