diff --git a/.clang-format b/.clang-format index 1e59dce..e0a8a36 100644 --- a/.clang-format +++ b/.clang-format @@ -2,12 +2,14 @@ BasedOnStyle: Google AllowShortBlocksOnASingleLine: false AllowShortFunctionsOnASingleLine: Empty AllowShortIfStatementsOnASingleLine: true -ColumnLimit: 200 +ColumnLimit: 120 CommentPragmas: NOLINT:.* DerivePointerAlignment: false IncludeBlocks: Preserve -IndentWidth: 4 +IndentWidth: 2 PointerAlignment: Left -TabWidth: 4 +TabWidth: 2 UseTab: Never -Cpp11BracedListStyle: false \ No newline at end of file +Cpp11BracedListStyle: false +QualifierAlignment: Right +BracedInitializerIndentWidth: 2 diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..ae51d81 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,23 @@ +Checks: "clang-diagnostic-*, + clang-analyzer-*, + fuchsia-statically-constructed-objects, + *, + -readability-identifier-length, + -llvmlibc-*, + -llvm-namespace-comment, + -llvm-include-order, + -altera-*, + -fuchsia-*, + -modernize-use-trailing-return-type, + -cppcoreguidelines-avoid-non-const-global-variables, + -llvm-header-guard, + -cppcoreguidelines-macro-usage, + -cppcoreguidelines-pro-type-vararg, + -cppcoreguidelines-avoid-do-while, + -hicpp-vararg, + -concurrency-mt-unsafe, + -modernize-use-using, + -abseil-*" +WarningsAsErrors: true +HeaderFilterRegex: "" +FormatStyle: file 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 14f5ddf..0d2468b 100644 --- a/.github/workflows/build-ndk.yml +++ b/.github/workflows/build-ndk.yml @@ -4,110 +4,158 @@ on: workflow_dispatch: push: branches-ignore: - - 'version-*' - pull_request: - branches-ignore: - - 'version-*' + - "version-*" + tags: + - "v*" env: - module_id: flamingo + module_id: flamingo + qmodName: flamingo jobs: + test-local: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + name: Checkout + with: + submodules: true + + - uses: seanmiddleditch/gha-setup-ninja@v3 + + - name: Set up Clang + uses: egor-tensin/setup-clang@v1 + with: + version: latest + platform: x64 + + # Get QPM so we can bring in dependencies + - name: Setup qpm + uses: Fernthedev/qpm-action@v1 + with: + workflow_token: ${{ secrets.GITHUB_TOKEN }} + restore: true + cache: true + publish: false + + - name: QPM Collapse + run: qpm collapse + + - name: Run test script + run: | + cd ${GITHUB_WORKSPACE} + ./test.sh + 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 + + # 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: 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 collapse + + - name: Build + run: | + cd ${GITHUB_WORKSPACE} + qpm s build + + - name: Make mod.json + run: | + qpm qmod manifest + + - name: Build & Create Qmod + run: | + cd ${GITHUB_WORKSPACE} + qpm qmod zip + + - name: Get Library Name + id: libname + run: | + cd ./build/ + pattern="lib${module_id}*.so" + 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: + 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 + + - name: Upload qmod artifact + uses: actions/upload-artifact@v4 + with: + 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/.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 585ea17..6634061 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ # VSCode config stuff !.vscode/c_cpp_properties.json !.vscode/tasks.json +.cache/ # NDK stuff out/ @@ -43,6 +44,7 @@ out/ [Oo]bjs/ ndkpath.txt *.zip +*.txt # Log files/backups *.log @@ -52,8 +54,11 @@ ndkpath.txt libs/ obj/ extern/ -qpm.shared.json *.backup extern.cmake qpm_defines.cmake -build/ \ No newline at end of file +build/ +build_test + +build-linux/ +*.inc 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/.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/CMakeLists.txt b/CMakeLists.txt index 3787220..88369ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,4 @@ -# include some defines automatically made by qpm -include(qpm_defines.cmake) - cmake_minimum_required(VERSION 3.22) -project(${COMPILE_ID}) # c++ standard set(CMAKE_CXX_STANDARD 20) @@ -11,63 +7,139 @@ 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) + +if (NOT DEFINED CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Debug") +endif() -# 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(-Wall -Wextra -Werror -Wpedantic) -# compile definitions used +add_compile_options(-fno-rtti -fPIE -fPIC -fno-exceptions -fcolor-diagnostics -DFMT_HEADER_ONLY) +add_link_options(-lm) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -if (DEFINED TEST_BUILD) - MESSAGE(STATUS "Compiling with test defines") +# 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.inc") +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() -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 ${cpp_file_list_utils}) -# target_sources(${COMPILE_ID} PRIVATE ${c_file_list_utils}) - -# 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() - -# 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) - -include(extern.cmake) -MESSAGE(STATUS "extern added!") - -add_custom_command(TARGET ${COMPILE_ID} POST_BUILD - COMMAND ${CMAKE_STRIP} -g -S -d --strip-all +option(TEST_BUILD "Enable local executable test builds for sanity checks" ON) + +# If we are making a test build, go into this block instead +if (TEST_BUILD) + project(flamingo-test) + + # 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}) + target_include_directories(flamingo-static PRIVATE ${SOURCE_DIR} ${INCLUDE_DIR}) + + # Fetch capstone (v5.0.1) + include(FetchContent) + FetchContent_Declare( + capstone + GIT_REPOSITORY https://github.com/capstone-engine/capstone.git + GIT_TAG 5.0.1 + ) + + set(CAPSTONE_ARCHITECTURE_DEFAULT OFF) + set(CAPSTONE_ARM64_SUPPORT ON) + + FetchContent_MakeAvailable(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) + MESSAGE(STATUS "Compiling 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) + + 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}) + + 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_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) + + if (TEST_ON_ANDROID) + target_sources(${COMPILE_ID} PUBLIC ${SOURCE_DIR}/flamingo-stamp.cpp) + endif() + + 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 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 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" - ) + 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-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/build.ps1 b/build.ps1 index 5d4f438..f57003a 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 } } @@ -12,32 +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 - - -exit $ExitCode \ No newline at end of file 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 diff --git a/mod.template.json b/mod.template.json new file mode 100644 index 0000000..e185226 --- /dev/null +++ b/mod.template.json @@ -0,0 +1,15 @@ +{ + "_QPVersion": "1.0.0", + "name": "${mod_name}", + "id": "${mod_id}", + "author": "Sc2ad", + "version": "${version}", + "description": "General purpose Android hook library", + "dependencies": [], + "modFiles": [], + "libraryFiles": [ + "libflamingo.so" + ], + "fileCopies": [], + "copyExtensions": [] +} \ No newline at end of file diff --git a/qpm.json b/qpm.json index 662c8bc..6eb5ead 100644 --- a/qpm.json +++ b/qpm.json @@ -1,36 +1,55 @@ { + "$schema": "https://raw.githubusercontent.com/QuestPackageManager/QPM.Package/refs/heads/main/qpm.schema.json", + "version": "1.0.0", "sharedDir": "shared", "dependenciesDir": "extern", "info": { "name": "flamingo", "id": "flamingo", - "version": "0.1.0", + "version": "1.0.0", "url": "https://github.com/sc2ad/Flamingo", "additionalData": { "overrideSoName": "libflamingo.so" } }, + "workspace": { + "scripts": { + "build": [ + "pwsh ./build.ps1" + ], + "build-android-test": [ + "pwsh ./build-android-test.ps1" + ] + }, + "ndk": "^29.0.14206865", + "qmodIncludeDirs": [ + "./build" + ], + "qmodIncludeFiles": [], + "qmodOutput": "flamingo.qmod" + }, "dependencies": [ { "id": "capstone", "versionRange": "^0.1.0", - "additionalData": {} - }, - { - "id": "paper", - "versionRange": "^1.0.0", - "additionalData": {} + "additionalData": { + "private": true + } }, { - "id": "modloader", - "versionRange": "^1.0.2", - "additionalData": {} + "id": "fmt", + "versionRange": "^11.0.0", + "additionalData": { + "private": true + } }, { - "id": "beatsaber-hook", - "versionRange": "^3.8.0", - "additionalData": {} + "id": "scotland2", + "versionRange": "^0.1.6", + "additionalData": { + "includeQmod": false, + "private": true + } } - ], - "additionalData": {} + ] } \ No newline at end of file diff --git a/qpm.shared.json b/qpm.shared.json new file mode 100644 index 0000000..75e4894 --- /dev/null +++ b/qpm.shared.json @@ -0,0 +1,103 @@ +{ + "$schema": "https://raw.githubusercontent.com/QuestPackageManager/QPM.Package/refs/heads/main/qpm.shared.schema.json", + "config": { + "version": "1.0.0", + "sharedDir": "shared", + "dependenciesDir": "extern", + "info": { + "name": "flamingo", + "id": "flamingo", + "version": "1.0.0", + "url": "https://github.com/sc2ad/Flamingo", + "additionalData": { + "overrideSoName": "libflamingo.so" + } + }, + "workspace": { + "scripts": { + "build": [ + "pwsh ./build.ps1" + ], + "build-android-test": [ + "pwsh ./build-android-test.ps1" + ] + }, + "ndk": "^29.0.14206865", + "qmodIncludeDirs": [ + "./build" + ], + "qmodIncludeFiles": [], + "qmodOutput": "flamingo.qmod" + }, + "dependencies": [ + { + "id": "capstone", + "versionRange": "^0.1.0", + "additionalData": { + "private": true + } + }, + { + "id": "fmt", + "versionRange": "^11.0.0", + "additionalData": { + "private": true + } + }, + { + "id": "scotland2", + "versionRange": "^0.1.6", + "additionalData": { + "includeQmod": false, + "private": true + } + } + ] + }, + "restoredDependencies": [ + { + "dependency": { + "id": "fmt", + "versionRange": "=11.0.2", + "additionalData": { + "headersOnly": true, + "branchName": "version/v11_0_2", + "compileOptions": { + "systemIncludes": [ + "fmt/include/" + ], + "cppFlags": [ + "-DFMT_HEADER_ONLY" + ] + } + } + }, + "version": "11.0.2" + }, + { + "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" + }, + { + "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/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/calling-convention.hpp b/shared/calling-convention.hpp new file mode 100644 index 0000000..b5c2b27 --- /dev/null +++ b/shared/calling-convention.hpp @@ -0,0 +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 fmt::format_to(ctx.out(), "Cdecl"); + case flamingo::CallingConvention::Fastcall: + return fmt::format_to(ctx.out(), "Fastcall"); + case flamingo::CallingConvention::Thiscall: + return fmt::format_to(ctx.out(), "Thiscall"); + } + } +}; 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/capi.h b/shared/capi.h new file mode 100644 index 0000000..91dc405 --- /dev/null +++ b/shared/capi.h @@ -0,0 +1,313 @@ +#pragma once + +#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)) +#define FLAMINGO_C_EXPORT_VOID __attribute__((visibility("default"))) +// The flamingo C API + +#ifdef __cplusplus +extern "C" { +#endif + +/// @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 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; + 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 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; + union { + bool any_hooks_remain; + bool remap_failure; + } value; +} 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::HookNameFilter +typedef struct FlamingoHookFilter FlamingoHookFilter; + +/// @brief Opaque pointer around a flamingo::HookPriority +typedef struct FlamingoHookPriority FlamingoHookPriority; + +/// @brief Opaque pointer around a flamingo::InstallationMetadata +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_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 + 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. +typedef struct { + /// @brief The size of the hook present at this address, in number of instructions. + 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 + /// 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. +/// @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 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 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(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. +/// 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. +/// 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 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 +/// 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). +/// This function is commonly used for ensuring correct results when going through an xref trace, or validating real +/// 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 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. +/// @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* 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* 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* 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 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. +/// @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* 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* 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* 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); + +/// @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 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 +/// 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, + 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); + +#ifdef __cplusplus +} +#endif 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/fixups.hpp b/shared/fixups.hpp new file mode 100644 index 0000000..150cb4c --- /dev/null +++ b/shared/fixups.hpp @@ -0,0 +1,97 @@ +#pragma once + +#include +#include +#include +#include "page-allocator.hpp" +#include "util.hpp" + +#if __has_include() +#include +#endif + +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() { + // 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) { + 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 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); + + private: + void WriteCallback(ProtectionWriter& writer, uint32_t const* target); +}; + +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 + ShimTarget target; + // The location to write fixups to + 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; + // 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(); +}; + +#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 diff --git a/shared/hook-data.hpp b/shared/hook-data.hpp index 12412d5..7c4cbba 100644 --- a/shared/hook-data.hpp +++ b/shared/hook-data.hpp @@ -1,34 +1,138 @@ #pragma once +#include +#include #include -#include "calling_convention.hpp" -#include +#include #include -struct TargetData { - void* target_method; - uint16_t method_size; - CallingConvention calling_convention; -}; +#include "calling-convention.hpp" +#include "hook-metadata.hpp" +#include "type-info.hpp" + +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 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 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) + : HookInfo(hook_func, target, orig_ptr, kDefaultNumInsts, CallingConvention::Cdecl, + HookNameMetadata{ .name = "" }, HookPriority{}, + InstallationMetadata{ .need_orig = orig_ptr != nullptr, .is_midpoint = false, .write_prot = false }) {} + + // 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 const& install_metadata) + : target(target), + orig_ptr(reinterpret_cast(orig_ptr)), + hook_ptr(reinterpret_cast(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 = { TypeInfo::from()... }, + .return_info = TypeInfo::from(), +#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) + : 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 + }) { + } -struct InstallationMetadata { - uint8_t need_orig : 1; - uint8_t is_midpoint : 1; - // uint32_t permissible_fixup_registers; + 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 }) {} + + 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; + } + + void* target; + void** orig_ptr; + void* hook_ptr; + HookMetadata metadata; }; -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 +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/shared/hook-installation-result.hpp b/shared/hook-installation-result.hpp new file mode 100644 index 0000000..5e89c45 --- /dev/null +++ b/shared/hook-installation-result.hpp @@ -0,0 +1,214 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +#include "calling-convention.hpp" +#include "hook-metadata.hpp" +#include "target-data.hpp" +#include "type-info.hpp" +#include "util.hpp" + +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::variant(std::in_place_index_t<1>{}, std::forward(args)...) }; + } + template + static Result Err(TArgs&&... 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); + } + 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; +}; + +/// @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; +}; +/// @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, std::string_view message) : HookErrorInfo(m.name_info), message(message) {} + std::string message; +}; +// TODO: Should we add the incoming hook IDs? + +#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 : 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 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 : HookErrorInfo { + MismatchMidpoint(HookMetadata const& m, bool existing) + : HookErrorInfo(m.name_info), existing(existing), incoming(m.installation_metadata.is_midpoint) {} + bool existing{}; + bool incoming{}; +}; + +#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; + +} // namespace installation + +} // namespace flamingo + +// Custom formatter for flamingo::Error +template <> +class fmt::formatter { + public: + constexpr static auto parse(format_parse_context& ctx) { + return ctx.begin(); + } + template + constexpr static auto format(flamingo::installation::Error const& error, Context& ctx) { + using namespace flamingo::installation; + return std::visit( + flamingo::util::overload{ + [&ctx](TargetIsNull const& null_target) { + return fmt::format_to(ctx.out(), "Null target, for hook: {}", null_target.installing_hook); + }, + [&ctx](TargetBadPriorities const& bad_priorities) { + return fmt::format_to(ctx.out(), "Bad priorities, for hook: {}, with message: {}", + bad_priorities.installing_hook, bad_priorities.message); + }, + [&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); + } +}; + +// 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 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 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 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 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 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); + }, +#endif + }, + mismatch); + } +}; 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/hook-metadata.hpp b/shared/hook-metadata.hpp new file mode 100644 index 0000000..ab4f5e0 --- /dev/null +++ b/shared/hook-metadata.hpp @@ -0,0 +1,120 @@ +#pragma once + +#include +#include +#include +#include +#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 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. +/// Lookups are described using userdata when the HookInfo is made at first. +struct HookNameMetadata { + std::string name{}; + std::string namespaze{}; + + [[nodiscard]] + bool operator==(HookNameMetadata const& other) const { + return (name == other.name) && (namespaze == other.namespaze); + } +}; + +/// Specifies the filter type for hook names in priorities +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. + 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. +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{}; + /// @brief Set to true if this hook should be the final hook (closest to the original function) + bool is_final{false}; +}; + +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 + +// Custom formatter for flamingo::HookNameMetadata +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 fmt::format_to(ctx.out(), "name: {} namespace {}", 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: {} 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/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 new file mode 100644 index 0000000..90142dd --- /dev/null +++ b/shared/installer.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "hook-data.hpp" +#include "hook-installation-result.hpp" +#include "target-data.hpp" +#include "util.hpp" + +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 +/// 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]] 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]] 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. +/// 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]] 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); + +/// @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/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/page-allocator.hpp b/shared/page-allocator.hpp new file mode 100644 index 0000000..dd92025 --- /dev/null +++ b/shared/page-allocator.hpp @@ -0,0 +1,71 @@ +#pragma once +#include +#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) {} + PointerWrapper(PointerWrapper const&) = default; + + 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)); + } + } + /// @brief Returns a subspan that is of the (potentially shrunken) size. + PointerWrapper Subspan(size_t n) const { + return PointerWrapper(addr.first(std::min(n, addr.size())), protection); + } +}; + +PointerWrapper Allocate(uint_fast16_t alignment, uint_fast16_t size, PageProtectionType protection); + +} // namespace flamingo diff --git a/shared/target-data.hpp b/shared/target-data.hpp new file mode 100644 index 0000000..046c986 --- /dev/null +++ b/shared/target-data.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include +#include +#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 +}; + +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 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. +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/trampoline-allocator.hpp b/shared/trampoline-allocator.hpp deleted file mode 100644 index 19ea05a..0000000 --- a/shared/trampoline-allocator.hpp +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include -#include -#include -#include -// #include "beatsaber-hook/shared/utils/capstone-utils.hpp" - -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; - 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* address); - 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 fixupSize); - /// @brief A TRAMPOLINE IS NOT COMPLETE UNTIL FINISH IS CALLED! - void Finish(); -}; - -struct TrampolineAllocator { - static Trampoline Allocate(std::size_t trampolineSize); - static void Free(Trampoline const& toFree); -}; \ No newline at end of file diff --git a/shared/type-info.hpp b/shared/type-info.hpp new file mode 100644 index 0000000..77bd187 --- /dev/null +++ b/shared/type-info.hpp @@ -0,0 +1,50 @@ +#pragma once + +#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, like name. 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 if constexpr (std::is_void_v) { + return TypeInfo{ + .size = 0, + }; + } else { + return TypeInfo{ + .size = sizeof(T), + }; + } + } +}; + +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 fmt::format_to(ctx.out(), "(size={})", info.size); + } +}; diff --git a/shared/util.hpp b/shared/util.hpp index 32fc66d..a12653f 100644 --- a/shared/util.hpp +++ b/shared/util.hpp @@ -1,28 +1,103 @@ #pragma once -#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 + +#define FLAMINGO_ID "flamingo" +#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 +#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_WARN(...) LOGA(ANDROID_LOG_WARN, __VA_ARGS__) +#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_LOG_STANDALONE + +#include + +#ifdef ANDROID +#include "paper2_scotland2/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_WARN(...) Paper::Logger::fmtLog(__VA_ARGS__) +#define FLAMINGO_CRITICAL(...) Paper::Logger::fmtLog(__VA_ARGS__) +#define FLAMINGO_ABORT(...) \ + do { \ + FLAMINGO_CRITICAL(__VA_ARGS__); \ + Paper::Logger::WaitForFlush(); \ + std::abort(); \ + } while (0) + +#else +#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_WARN(...) \ + fmt::print(__VA_ARGS__); \ + puts("") +#define FLAMINGO_ABORT(...) \ + fmt::print(__VA_ARGS__); \ + puts(""); \ + std::abort() +#endif + +#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/And64InlineHook.cpp b/src/And64InlineHook.cpp deleted file mode 100644 index ca0e0c8..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 - } __attribute__((aligned(16))); - 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/capi.cpp b/src/capi.cpp new file mode 100644 index 0000000..8ee3df1 --- /dev/null +++ b/src/capi.cpp @@ -0,0 +1,376 @@ +#include "capi.h" +#include +#include +#include +#include +#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 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 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::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::HookNameFilter(*value); + delete value; + } + result->is_final = is_final; + 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, "{}", *install_error); + *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, size_t capacity) { + auto res = flamingo::TargetDataFor(flamingo::TargetDescriptor{ .target = target }); + if (!res.has_value()) return 0; + if (hooks == nullptr) return 0; + + 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 { + dest.name = static_cast(std::malloc(it->metadata.name_info.name.size() + 1)); + std::strcpy(dest.name, it->metadata.name_info.name.c_str()); + } + // Copy namespaze + if (it->metadata.name_info.namespaze.empty()) { + dest.namespaze = nullptr; + } else { + dest.namespaze = static_cast(std::malloc(it->metadata.name_info.namespaze.size() + 1)); + std::strcpy(dest.namespaze, it->metadata.name_info.namespaze.c_str()); + } + ++it; + } + + 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, void* 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; + + // 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); + } + } +} \ No newline at end of file diff --git a/src/fixups.cpp b/src/fixups.cpp new file mode 100644 index 0000000..aa3053b --- /dev/null +++ b/src/fixups.cpp @@ -0,0 +1,693 @@ +#include "fixups.hpp" +#include +#include +#include +#include +#include +#include +#include +#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 { +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; +}; +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{}; + 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, .alignment = 1 }); + 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])); + // 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, + .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)); + } + #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; +} + +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 ShimTarget::WriteJump(void* address) { + FLAMINGO_ASSERT(!addr.empty()); + // 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::CopyOriginalInsts() { + FLAMINGO_ASSERT(!target.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. + + // 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 + 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 + // 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& 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) { + 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)); + } + // 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 +} + +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 +// 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 + +// 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 diff --git a/src/flamingo-stamp.cpp b/src/flamingo-stamp.cpp new file mode 100644 index 0000000..460aa2f --- /dev/null +++ b/src/flamingo-stamp.cpp @@ -0,0 +1,74 @@ +// Stamp file used when building for Android (to ensure we pull in the static library correctly) +#include +#include +#include +#include "fixups.hpp" +#include "git_info.inc" +#include "hook-data.hpp" +#include "installer.hpp" +#include "target-data.hpp" +#include "util.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]); + } + } +} + +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"); + 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)); + 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!"); +} 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/installer.cpp b/src/installer.cpp new file mode 100644 index 0000000..9b53406 --- /dev/null +++ b/src/installer.cpp @@ -0,0 +1,587 @@ +#include "installer.hpp" +#include +#include +#include +#include +#include +#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" + +/// @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 { +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; + +// Rebuild the per-target priority graph from the current hooks list. +void rebuild_priority_graph(TargetData& target_data) { + target_data.priority_graph.clear(); + + // 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 = {} }; + } + + // Helper to find matches for a filter among current hooks + auto findMatches = [&](HookNameFilter const& filter) { + std::vector matches; + for (auto const& hook : target_data.hooks) { + if (filter.matches(hook.metadata.name_info)) { + matches.push_back(hook.metadata.name_info); + } + } + return matches; + }; + + // 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) { + target_data.priority_graph[matched].afters.push_back(hook.metadata.name_info); + } + } + // befores: current -> matched_before + for (auto const& beforeFilter : hook.metadata.priority.befores) { + auto matches = findMatches(beforeFilter); + for (auto const& matched_before : matches) { + target_data.priority_graph[hook.metadata.name_info].afters.push_back(matched_before); + } + } + } + } + +// 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; + + // build in-degree map + std::unordered_map in_degree; + for (auto const& pair : target_data.priority_graph) { + in_degree[pair.first] = 0; + } + + // 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]++; + } + } + + // queue of zero in-degree nodes, preserve original hooks list order + std::queue zero_in_degree; + 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); + } + } + + while (!zero_in_degree.empty()) { + auto current_name = zero_in_degree.front(); + zero_in_degree.pop(); + + auto it_node = target_data.priority_graph.find(current_name); + if (it_node == target_data.priority_graph.end()) { + continue; // shouldn't happen + } + + // 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); + } + } + } + + // 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 (!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 " + "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, ", ")); + } + } + + return ResultT::Err(installation::TargetBadPriorities{ + 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.") }); + } + + // swap sorted back into target_data.hooks + target_data.hooks.swap(sorted_hooks); + + return ResultT::Ok(sorted_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; + } + + // TODO: Do we need to copy Reinstall logic here? + + // 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); + + while (std::next(it) != hooks.end()) { + it->assign_orig(std::next(it)->hook_ptr); + ++it; + } + it->assign_orig(target_entry->second.metadata.metadata.need_orig + ? target_entry->second.fixups.fixup_inst_destination.addr.data() + : 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, 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 + // 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. + + // 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.metadata.priority.is_final) { + // 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)); + return ResultT::Ok(--hooks.end()); + } + + // 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 + + // 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; + + 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 + 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& 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(cyclesResult.error()); + } + + // now recompile all hooks to ensure orig pointers are correct + recompile_hooks(hooks, target); + + 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.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.metadata.priority.afters) { + if (after_filter.matches(it->metadata.name_info)) { + can_install_before = false; + break; + } + } + 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 + auto new_it = hooks.emplace(hooks.begin(), std::move(hook_to_install)); + return ResultT::Ok(new_it); +} + +Result validate_install_metadata(TargetMetadata& existing, + HookMetadata const& incoming) { + 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 + 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); + } + // 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); + } + 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(); +} + +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) }); + } + } + + // 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(incoming.name_info)) { + return ResultT::Err(installation::TargetBadPriorities{ + incoming, + fmt::format("Cannot install hook because it requests to be before itself: {}", incoming.name_info)}); + } + } + + return ResultT::Ok(); +} +} // 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) + 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()) { + // 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) + // If we have an orig, we need to have an instruction to jump back to + 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; + 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), + target_initial_protection); + 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 + }, + .fixups = Fixups{ + // Our fixup target is a subspan the same size as our install size + .target = { target_pointer.Subspan(Fixups::kNormalFixupInstCount) }, + .fixup_inst_destination = + Allocate(kHookAlignment, + 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 + 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(); + 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)); + // 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 } }); + } + 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 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()); + } + 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()) { + 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 { + 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) { + 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) + itr->second.fixups.CopyOriginalInsts(); + 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 + 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.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 + // 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.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.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. + else { + 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 + // 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); +} + +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.fixup_inst_destination.addr); + } + 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 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/page-allocator.cpp b/src/page-allocator.cpp new file mode 100644 index 0000000..8dc782b --- /dev/null +++ b/src/page-allocator.cpp @@ -0,0 +1,82 @@ +// 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 +#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 }); + 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]) }, + protection); +} + +} // namespace flamingo 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" - diff --git a/src/trampoline-allocator.cpp b/src/trampoline-allocator.cpp deleted file mode 100644 index f2b6d70..0000000 --- a/src/trampoline-allocator.cpp +++ /dev/null @@ -1,370 +0,0 @@ -#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 - -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) { - if constexpr (imm_19) { - // Imm 19 - constexpr static uint32_t imm_mask = 0b00000000111111111111111111100000; - } else { - // Imm 14 - constexpr static uint32_t imm_mask = 0b00000000000001111111111111100000; - } - 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); -} - -/// @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; -} - -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; - 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: - case ARM64_INS_LDRSW: - - // 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; - int64_t pc = reinterpret_cast(address + instruction_count); - int64_t 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)); -} - -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; - -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)) { - // Log error on memalign allocation! - SAFE_ABORT_MSG("Failed to allocate trampoline page of size: %zu for size: %zu", 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); - } - auto& page = pages.emplace_back(ptr, trampolineSize); - return {static_cast(ptr), trampolineSize, 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) & ~(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)) { - // Log error on mprotect - SAFE_ABORT_MSG("Failed to mark page at: %p as read only!", p.ptr); - } - ::free(p.ptr); - } - return; - } - } - // 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 diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..d144742 --- /dev/null +++ b/test.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -euo pipefail +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 diff --git a/test/api.cpp b/test/api.cpp new file mode 100644 index 0000000..42c6276 --- /dev/null +++ b/test/api.cpp @@ -0,0 +1,191 @@ +#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" + +namespace { + +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()); + } + // 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 but with an 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_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()); + } + // 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 + +int main() { + test_simple_hook(); + test_hook_with_orig(); + test_multi_hook(); +} diff --git a/test/main.cpp b/test/main.cpp new file mode 100644 index 0000000..0c0184b --- /dev/null +++ b/test/main.cpp @@ -0,0 +1,356 @@ +// 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" + +static decltype(auto) test_near(std::span target, [[maybe_unused]] uint32_t const* callback) { + constexpr size_t hookSizeNumInsts = 5; + 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(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), + }; + fixups.CopyOriginalInsts(); + fixups.PerformFixupsAndCallback(); + return fixups; +} + +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()); + print_decode_loop(hook_span); + puts("TEST NEAR..."); + 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:"); + print_decode_loop(trampoline_data.target.addr); + return trampoline_data; +} + +static decltype(auto) test_far(std::span target, [[maybe_unused]] uint32_t const* callback) { + constexpr size_t hookSizeNumInsts = 5; + 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(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]); + // 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; +} + +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 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:"); + print_decode_loop(fixups.target.addr); + return 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 }; + { + 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, + 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 + fixup_validator.expect_big_data(reinterpret_cast(&results.target.addr[4])); + } +} + +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 }; + { + 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, + 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, + 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. + // 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])); + } +} + +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 }; + { + 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 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.target.addr[5]); + fixup_validator.expect_opc(ARM64_INS_MOV); + // Callback + fixup_validator.expect_b(&results.target.addr[4]); + // Data validation + } + { + 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, + 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, + 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] + 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])); + } +} + +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 }; + { + 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, + 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); + // 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()) & ~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, + 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, + 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)(results.target.addr.data()) & ~0xfff); + // Check callback point is valid + fixup_validator.expect_big_data(reinterpret_cast(&results.target.addr[4])); + } +} + +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 + +int main() { + test_no_fixups(); + test_bls_tbzs_within_hook(); + test_ldr_ldrb_tbnz_bl(); + test_adrp(); + test_neg_adrp(); + puts("ALL GOOD!"); +} diff --git a/test/sorted-hooks.cpp b/test/sorted-hooks.cpp new file mode 100644 index 0000000..a49527d --- /dev/null +++ b/test/sorted-hooks.cpp @@ -0,0 +1,886 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#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); +} + +// Expected chain: A -> B -> fixup +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(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)); + 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); + } +} + +// 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 + 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()); + + 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"; + HookPriority prior_prio; + 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)); + 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(); + + // 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)orig2 != hf1) { + ERROR("Namespaze-matching: expected hf2.orig == hf1 (0x{:x}) but got 0x{:x}", hf1, (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); + } +} + +// Expected chain after X install: X -> fixup (Y install should fail) +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.emplace_back(nY); + HookPriority pY; + 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)); + 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("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(); + + // 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 (origY != nullptr) { + ERROR("Priority-cycle: expected Y not to be installed (origY == nullptr) but got 0x{:x}", (uintptr_t)origY); + } +} + +// Expected chain: b1 -> a2 -> a1 -> fixup +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" + 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)); + 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(); + + // 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); +} + +// Expected chain: final1 -> fixup (final2 install should fail) +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"); +} + +// Expected chain: h1 -> h2 -> h3 -> h4 -> h5 -> fixup +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.emplace_back(m1); // h2 after h1 + HookPriority p3; + p3.afters.emplace_back(m2); // h3 after h2 + HookPriority p4; + p4.afters.emplace_back(m3); // h4 after h3 + HookPriority p5; + 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)); + 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.contains(h)) { + 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 < v.size(); ++i) { + if (i) out += ", "; + out += fmt::format("0x{:x}", v[i]); + } + return out; + }; + ERROR("5-hook: ordering mismatch; expected {} but got {}", join_hex(expected), join_hex(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; + 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.contains(h)) { + 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])); +} + +// Expected chain: prior -> a2 -> a1 -> fixup +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; + 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))) + .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); +} + +// Expected chain: g2 -> g1 -> late -> fixup +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; + HookNameFilter match_ns2; + match_ns2.namespaze = "grp"; + 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()) + 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); +} + +// 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; + 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; + 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()) + 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.contains(h)) { + 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])); + } +} + +// 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; + 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); +} + +// 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; + 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); +} + +// Expected chain for reinstall: reinst -> fixup +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"); +} + +// 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; + 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(); + test_priority_cycle(); + test_complex_namespace(); + test_final_conflict(); + test_five_hook_order(); + test_no_constraints_multiple(); + 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"); +} diff --git a/test/test-wrapper.hpp b/test/test-wrapper.hpp new file mode 100644 index 0000000..1814a7c --- /dev/null +++ b/test/test-wrapper.hpp @@ -0,0 +1,227 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#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(...) \ + do { \ + fmt::print(stderr, __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) { + 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; + 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() { + // 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; + } + + 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); + } + } +}; + +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())); +} + +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]); + } + } +}