diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..6f9aacc --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = ['i686-pc-windows-msvc','x86_64-pc-windows-msvc'] \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c676424..3fef1bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,10 @@ name: CI on: push: - branches: [ master ] + branches: [ master,add-c-exports ] pull_request: branches: [ master ] + workflow_dispatch: env: CARGO_TERM_COLOR: always @@ -17,49 +18,134 @@ jobs: strategy: fail-fast: false matrix: - toolchain: ["nightly"] os: ["windows-latest"] target: ["x86_64-pc-windows-msvc", "i686-pc-windows-msvc"] - include: - - target: x86_64-pc-windows-msvc - target32: i686-pc-windows-msvc steps: - - uses: actions/checkout@v2 - - - name: Install latest rust ${{ matrix.toolchain }} for ${{ matrix.target }} - uses: actions-rs/toolchain@v1 + - uses: actions/checkout@v4 + + - name: Install Nightly Rust + uses: dtolnay/rust-toolchain@nightly with: - target: ${{ matrix.target }} - toolchain: ${{ matrix.toolchain }} - override: true - - - name: Install latest rust ${{ matrix.toolchain }} for x86 version of target - if: matrix.target32 - uses: actions-rs/toolchain@v1 - with: - target: ${{ matrix.target32 }} - toolchain: ${{ matrix.toolchain }} - override: true - - - name: Build + targets: ${{ matrix.target }} + + # We need this because we also text x64-to-x86 injection. + - name: Add i686 Rust Target + run: rustup target add i686-pc-windows-msvc --toolchain nightly + + - name: Can Build run: cargo build --target ${{ matrix.target }} - - name: Build test target - run: cargo build --target ${{ matrix.target }} --manifest-path "tests/helpers/test_target/Cargo.toml" --all-targets - - name: Build test payload - run: cargo build --target ${{ matrix.target }} --manifest-path "tests/helpers/test_payload/Cargo.toml" --all-targets + - name: Can Build test target + run: cargo build --target ${{ matrix.target }} --manifest-path "tests/helpers/test_target/Cargo.toml" + - name: Can Build test payload + run: cargo build --target ${{ matrix.target }} --manifest-path "tests/helpers/test_payload/Cargo.toml" - name: Test - run: cargo test --target ${{ matrix.target }} --all-targets --all-features -- --nocapture + run: cargo test --target ${{ matrix.target }} --all-features -- --nocapture - - name: Build (default features) - run: cargo build --target ${{ matrix.target }} --all-targets - - name: Build (no features) - run: cargo build --target ${{ matrix.target }} --no-default-features --all-targets - - name: Build (feature syringe) - run: cargo build --target ${{ matrix.target }} --features syringe --all-targets - - name: Build (feature rpc) - run: cargo build --target ${{ matrix.target }} --features rpc --all-targets + - name: Can Build (no features) + run: cargo build --target ${{ matrix.target }} --no-default-features + - name: Can Build (feature syringe) + run: cargo build --target ${{ matrix.target }} --no-default-features --features syringe + - name: Can Build (feature rpc) + run: cargo build --target ${{ matrix.target }} --no-default-features --features rpc + + test-on-wine: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + # Install Rust nightly toolchain + - name: Install Nightly Rust + uses: dtolnay/rust-toolchain@nightly + + # Add Windows MSVC targets + - name: Add Rust targets + run: | + rustup target add x86_64-pc-windows-msvc --toolchain nightly + rustup target add i686-pc-windows-msvc --toolchain nightly + + # Install cargo-xwin + - name: Install cargo-xwin + run: cargo install cargo-xwin + + # We can't use 'cross' due to using xwin, so we have to manually install Wine + - name: Install Wine + run: | + sudo apt-get install ppa-purge && sudo ppa-purge -y ppa:ubuntu-toolchain-r/test + sudo dpkg --add-architecture i386 + sudo mkdir -pm755 /etc/apt/keyrings && sudo wget -O /etc/apt/keyrings/winehq-archive.key https://dl.winehq.org/wine-builds/winehq.key + sudo wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/jammy/winehq-jammy.sources + sudo apt-get update + sudo apt install --install-recommends winehq-stable + + # Run tests with Wine + - name: Run tests on Wine + run: ./scripts/test-wine.ps1 + shell: pwsh + + build-c-libs: + strategy: + matrix: + include: + - os: windows-latest + target: x86_64-pc-windows-msvc + use_cross: false + features: "c-exports,into-x86-from-x64" + - os: windows-latest + target: i686-pc-windows-msvc + use_cross: false + features: "c-exports" + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - id: build-libs + uses: Reloaded-Project/reloaded-project-configurations-rust/.github/actions/build-c-library@v1 # upgrade if needed + with: + target: ${{ matrix.target }} + use_pgo: false + use_cross: ${{ matrix.use_cross }} + features: ${{ matrix.features }} + no_default_features: true + + build-c-headers: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Generate C++ bindings + uses: Reloaded-Project/reloaded-project-configurations-rust/.github/actions/generate-bindings@v1 + with: + config_file: cbindgen_cpp.toml + header_file: bindings_cpp.hpp + + - name: Generate C bindings + uses: Reloaded-Project/reloaded-project-configurations-rust/.github/actions/generate-bindings@v1 + with: + config_file: cbindgen_c.toml + header_file: bindings_c.h + + build-dotnet-library: + needs: build-c-libs + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Build and Package .NET Library + uses: Reloaded-Project/reloaded-project-configurations-rust/.github/actions/build-dotnet-library@dotnet-bindings + with: + targets: 'x86_64-pc-windows-msvc,i686-pc-windows-msvc' documentation: runs-on: ${{ matrix.os }} @@ -67,19 +153,25 @@ jobs: matrix: os: ["windows-latest"] steps: - - uses: actions/checkout@v2 - - name: Install latest nightly - uses: actions-rs/toolchain@v1 + - uses: actions/checkout@v4 with: - profile: minimal - toolchain: nightly - override: true + submodules: recursive + + - name: Install Nightly Rust + uses: dtolnay/rust-toolchain@nightly + + # Add Windows MSVC targets + - name: Add Rust targets + run: | + rustup target add x86_64-pc-windows-msvc --toolchain nightly + rustup target add i686-pc-windows-msvc --toolchain nightly + - name: Generate documentation - run: cargo doc --all-features + run: cargo +nightly doc --all-features - name: Install cargo-deadlinks - run: cargo install cargo-deadlinks + run: cargo +nightly install cargo-deadlinks - name: Check dead links in doc - run: cargo deadlinks + run: cargo +nightly deadlinks clippy: runs-on: ${{ matrix.os }} @@ -87,13 +179,81 @@ jobs: matrix: os: ["windows-latest"] steps: - - uses: actions/checkout@v1 - - uses: actions-rs/toolchain@v1 + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Nightly Rust + uses: dtolnay/rust-toolchain@nightly + with: + components: clippy + + - uses: giraffate/clippy-action@v1 with: - toolchain: nightly - components: clippy - override: true - - uses: actions-rs/clippy-check@v1 + reporter: 'github-pr-review' + github_token: ${{ secrets.GITHUB_TOKEN }} + + publish-crate: + permissions: + contents: write + + needs: [test,build-c-libs,build-c-headers,test-on-wine,build-dotnet-library] + # Publish only on tags + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: ↙️ Download Artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Upload to NuGet + shell: pwsh + run: | + $items = Get-ChildItem -Path "artifacts/**.nupkg" -Recurse + Foreach ($item in $items) + { + Write-Host "Pushing $item" + dotnet nuget push "$item" -k "${{ secrets.NUGET_KEY }}" -s "https://api.nuget.org/v3/index.json" --skip-duplicate + } + + $items = Get-ChildItem -Path "artifacts/**.snupkg" -Recurse + Foreach ($item in $items) + { + Write-Host "Pushing Symbol Package $item" + dotnet nuget push "$item" -k "${{ secrets.NUGET_KEY }}" -s "https://api.nuget.org/v3/index.json" --skip-duplicate + } + + - name: Compress Artifacts + shell: bash + run: | + dir="artifacts" + if [ ! -d "$dir" ]; then + echo "Directory $dir does not exist. No artifacts found." + exit 0 + fi + + for subdir in "$dir"/*; do + if [ -d "$subdir" ]; then + base=$(basename "$subdir") + zip -r "$dir/$base.zip" "$subdir" + rm -r "$subdir" + fi + done + ls -A ./artifacts + + - name: GitHub Release Artifacts + uses: softprops/action-gh-release@v1 + with: + files: | + artifacts/* + + + - name: Publish to crates.io + uses: Reloaded-Project/reloaded-project-configurations-rust/.github/actions/publish-crate@v1 with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features + token: ${{ secrets.CRATES_IO_TOKEN }} diff --git a/.gitignore b/.gitignore index c640ca5..96ef6c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ /target Cargo.lock -.vscode diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9c2c2b4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "rust-analyzer.checkOnSave.command": "clippy", + "[rust]": { + "editor.formatOnSave": true + }, + "rust-analyzer.linkedProjects": [ + "Cargo.toml", + "tests/helpers/test_payload/Cargo.toml", + "tests/helpers/test_target/Cargo.toml", + ], + "rust-analyzer.cargo.features": "all" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..5b576a4 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,23 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Test on Windows", + "type": "shell", + "command": "pwsh ./scripts/test.ps1", + "problemMatcher": [] + }, + { + "label": "Clean", + "type": "shell", + "command": "pwsh ./scripts/clean.ps1", + "problemMatcher": [] + }, + { + "label": "Test on Wine (See Readme)", + "type": "shell", + "command": "pwsh ./scripts/test-wine.ps1", + "problemMatcher": [] + }, + ] +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 700730b..a9c6688 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,10 @@ iced-x86 = { version = "1.19", features = ["std", "code_asm"], default-features bincode = { version = "1.3", default-features = false, optional = true } serde = { version = "1.0", default-features = false, optional = true } +# C# Bindings +[build-dependencies] +csbindgen = "1.9.0" + [target.'cfg(target_arch = "x86")'.dependencies] [target.'cfg(target_arch = "x86_64")'.dependencies] @@ -50,7 +54,23 @@ payload-utils = ["bincode", "serde"] syringe = ["iced-x86"] full = ["into-x86-from-x64", "rpc", "process-memory", "payload-utils"] doc-cfg = ["full"] +c-exports = ["syringe"] [package.metadata.docs.rs] targets = ["x86_64-pc-windows-msvc", "i686-pc-windows-msvc"] features = ["doc-cfg"] + +# Profile Build +[profile.profile] +inherits = "release" +debug = true +codegen-units = 1 +lto = true +strip = false # No stripping!! + +# Optimized Release Build +[profile.release] +codegen-units = 1 +lto = true +strip = true # Automatically strip symbols from the binary. +panic = "abort" \ No newline at end of file diff --git a/README.md b/README.md index b233341..031e2ee 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ A windows dll injection library written in Rust. ## Supported scenarios + | Injector Process | Target Process | Supported? | | ---------------- | -------------- | ------------------------------------------ | | 32-bit | 32-bit | Yes | @@ -42,11 +43,11 @@ syringe.eject(injected_payload).unwrap(); ## Remote Procedure Calls (RPC) This crate supports two mechanisms for rpc. Both only work one-way for calling exported functions in the target process and are only intended for one-time initialization usage. For extended communication a dedicated rpc library should be used. -| | `RemotePayloadProcedure` | `RemoteRawProcedure` | -| ---------------- | ------------------------------ | ------------------------------------------ | -| Feature | `rpc-payload` | `rpc-raw` | -| Argument and Return Requirements | `Serialize + DeserializeOwned` | `Copy`, Argument size has to be smaller than `usize` in target process | -| Function Definition | Using macro `payload_procedure!` | Any `extern "system"` or `extern "C"` with `#[no_mangle]` | +| | `RemotePayloadProcedure` | `RemoteRawProcedure` | +| -------------------------------- | -------------------------------- | ---------------------------------------------------------------------- | +| Feature | `rpc-payload` | `rpc-raw` | +| Argument and Return Requirements | `Serialize + DeserializeOwned` | `Copy`, Argument size has to be smaller than `usize` in target process | +| Function Definition | Using macro `payload_procedure!` | Any `extern "system"` or `extern "C"` with `#[no_mangle]` | ### RemotePayloadProcedure A rpc mechanism based on [`bincode`](https://crates.io/crates/bincode). @@ -119,6 +120,36 @@ syringe.eject(injected_payload).unwrap(); ## License Licensed under MIT license ([LICENSE](https://github.com/OpenByteDev/dll-syringe/blob/master/LICENSE) or http://opensource.org/licenses/MIT) +## Instructions for Contributors + +### Prerequisites + +You will need the nightly toolchains of Rust and Cargo to build/test this project. + +``` +rustup target add x86_64-pc-windows-msvc --toolchain nightly +rustup target add i686-pc-windows-msvc --toolchain nightly +``` + +> [!NOTE] +> Also applies to developing on Linux, you'll need it for your IDE (i.e. rust-analyzer or RustRover) to work properly. + +### Run Tests + +Run the `./scripts/test.ps1` script from PowerShell. + +### Running Tests on Linux + +You'll need `cargo xwin` to build the MSVC targets on Linux: + +``` +cargo install cargo-xwin +``` + +After that, you can run the tests with `./scripts/test-wine.ps1` PowerShell script. +(As opposed to `./scripts/test.ps1`) + +Make sure you have Wine installed! + ## Attribution Inspired by [Reloaded.Injector](https://github.com/Reloaded-Project/Reloaded.Injector) from [Sewer](https://github.com/Sewer56). - diff --git a/bindings/csharp/Init.cs b/bindings/csharp/Init.cs new file mode 100644 index 0000000..d7b2313 --- /dev/null +++ b/bindings/csharp/Init.cs @@ -0,0 +1,13 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using dll_syringe.Net.Sys; + +// ReSharper disable once CheckNamespace +class Init +{ + [ModuleInitializer] + internal static void RegisterImportResolver() + { + NativeLibrary.SetDllImportResolver(typeof(NativeMethods).Assembly, NativeMethods.DllImportResolver); + } +} \ No newline at end of file diff --git a/bindings/csharp/NativeMethods.cs b/bindings/csharp/NativeMethods.cs new file mode 100644 index 0000000..4675da8 --- /dev/null +++ b/bindings/csharp/NativeMethods.cs @@ -0,0 +1,70 @@ +using System; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace dll_syringe.Net.Sys; + +public static unsafe partial class NativeMethods +{ + // https://docs.microsoft.com/en-us/dotnet/standard/native-interop/cross-platform + // Library path will search + // win => __DllName, __DllName.dll + // linux, osx => __DllName.so, __DllName.dylib + internal static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) + { + if (libraryName == __DllName) + { + var dllName = __DllName; + var path = "runtimes/"; + var extension = ""; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + path += "win-"; + extension = ".dll"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + path += "osx-"; + extension = ".dylib"; + dllName = "lib" + dllName; + } + else + { + path += "linux-"; + extension = ".so"; + dllName = "lib" + dllName; + } + + if (RuntimeInformation.ProcessArchitecture == Architecture.X86) + { + path += "x86"; + } + else if (RuntimeInformation.ProcessArchitecture == Architecture.X64) + { + path += "x64"; + } + else if (RuntimeInformation.ProcessArchitecture == Architecture.Arm) + { + path += "arm"; + } + else if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) + { + path += "arm64"; + } + + path += "/native/" + dllName + extension; + try + { + return NativeLibrary.Load(Path.Combine(AppContext.BaseDirectory, path), assembly, searchPath); + } + catch (DllNotFoundException) + { + return NativeLibrary.Load(Path.Combine(AppContext.BaseDirectory, dllName + extension)); + } + } + + return IntPtr.Zero; + } +} \ No newline at end of file diff --git a/bindings/csharp/NativeMethods.g.cs b/bindings/csharp/NativeMethods.g.cs new file mode 100644 index 0000000..f4b2d8e --- /dev/null +++ b/bindings/csharp/NativeMethods.g.cs @@ -0,0 +1,65 @@ +// +// This code is generated by csbindgen. +// DON'T CHANGE THIS DIRECTLY. +// +#pragma warning disable CS8500 +#pragma warning disable CS8981 +using System; +using System.Runtime.InteropServices; + + +namespace dll_syringe.Net.Sys +{ + public static unsafe partial class NativeMethods + { + const string __DllName = "dll_syringe"; + + + + /// Creates a new `Syringe` instance for a process identified by PID. # Arguments * `pid` - The PID of the target process. # Returns A pointer to a `CSyringe` instance, or null if the process could not be opened. + [DllImport(__DllName, EntryPoint = "syringe_for_process", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern CSyringe* syringe_for_process(uint pid); + + /// Creates a new `Syringe` instance for a suspended process identified by PID. # Arguments * `pid` - The PID of the target suspended process. # Returns A pointer to a `CSyringe` instance, or null if the process could not be opened or initialized. + [DllImport(__DllName, EntryPoint = "syringe_for_suspended_process", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern CSyringe* syringe_for_suspended_process(uint pid); + + /// Injects a DLL into the target process associated with the given `Syringe`. # Safety This function is unsafe because it dereferences raw pointers. # Arguments * `c_syringe` - A pointer to the `CSyringe` instance. * `dll_path` - A C string path to the DLL to be injected. # Returns `true` if injection succeeded, otherwise `false`. + [DllImport(__DllName, EntryPoint = "syringe_inject", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool syringe_inject(CSyringe* c_syringe, byte* dll_path); + + /// Finds or injects a DLL into the target process. If the DLL is already present in the target process, it returns the existing module. Otherwise, it injects the DLL. # Safety This function is unsafe because it dereferences raw pointers. # Arguments * `c_syringe` - A pointer to the `CSyringe` instance. * `dll_path` - A C string path to the DLL to be injected. # Returns A pointer to a `CProcessModule`, or null if the operation failed. + [DllImport(__DllName, EntryPoint = "syringe_find_or_inject", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern CProcessModule* syringe_find_or_inject(CSyringe* c_syringe, byte* dll_path); + + /// Ejects a module from the target process. # Arguments * `c_syringe` - A pointer to the `CSyringe` instance. * `c_module` - A pointer to the `CProcessModule` to be ejected. # Returns `true` if ejection succeeded, otherwise `false`. # Safety This is safe as long as it has a valid pointer to a Syringe and Module. + [DllImport(__DllName, EntryPoint = "syringe_eject", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool syringe_eject(CSyringe* c_syringe, CProcessModule* c_module); + + /// Frees a `CSyringe` instance. # Arguments * `c_syringe` - A pointer to the `CSyringe` instance to be freed. # Safety This is safe as long as it has a valid pointer to a Syringe instance. + [DllImport(__DllName, EntryPoint = "syringe_free", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void syringe_free(CSyringe* c_syringe); + + /// Frees a `CProcessModule` instance. # Arguments * `c_module` - A pointer to the `CProcessModule` to be freed. # Safety This is safe as long as it has a valid pointer to a module created by this Syringe instance. + [DllImport(__DllName, EntryPoint = "syringe_module_free", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void syringe_module_free(CProcessModule* c_module); + + + } + + [StructLayout(LayoutKind.Sequential)] + public unsafe partial struct CSyringe + { + } + + [StructLayout(LayoutKind.Sequential)] + public unsafe partial struct CProcessModule + { + } + + + +} + \ No newline at end of file diff --git a/bindings/csharp/csharp.csproj b/bindings/csharp/csharp.csproj new file mode 100644 index 0000000..6a2290b --- /dev/null +++ b/bindings/csharp/csharp.csproj @@ -0,0 +1,42 @@ + + + + net5.0 + dll_syringe.Net.Sys + dll_syringe.Net.Sys + https://github.com/OpenByteDev/dll-syringe + A windows dll injection library written in Rust. (Raw C# Bindings). + 0.16.0 + + dll-syringe + + + preview + true + true + snupkg + true + true + true + + LICENSE + true + true + true + + + + + True + / + + + + + + diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..bffac40 --- /dev/null +++ b/build.rs @@ -0,0 +1,9 @@ +fn main() { + csbindgen::Builder::default() + .input_extern_file("src/c_exports.rs") + .csharp_dll_name("dll_syringe") + .csharp_class_accessibility("public") + .csharp_namespace("dll_syringe.Net.Sys") + .generate_csharp_file("bindings/csharp/NativeMethods.g.cs") + .unwrap(); +} diff --git a/cbindgen_c.toml b/cbindgen_c.toml new file mode 100644 index 0000000..d76b608 --- /dev/null +++ b/cbindgen_c.toml @@ -0,0 +1,486 @@ +# The language to output bindings in +# +# possible values: "C", "C++" +# +# default: "C++" +language = "C" + +# Options for wrapping the contents of the header: + +# An optional string of text to output at the beginning of the generated file +# default: doesn't emit anything +header = """ +#ifdef _MSC_VER +#define PACKED + #pragma pack(push, 1) +#else + #define PACKED __attribute__((packed)) +#endif +""" + +# An optional string of text to output at the end of the generated file +# default: doesn't emit anything +trailer = "/* Text to put at the end of the generated file */" + +# An optional name to use as an include guard +# default: doesn't emit an include guard +include_guard = "dll_syringe" + +# An optional string of text to output between major sections of the generated +# file as a warning against manual editing +# +# default: doesn't emit anything +autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */" + +# Whether to include a comment with the version of cbindgen used to generate the file +# default: false +include_version = true + +# An optional namespace to output around the generated bindings +# default: doesn't emit a namespace +namespace = "dll_syringe" + +# An optional list of namespaces to output around the generated bindings +# default: [] +namespaces = [ ] + +# An optional list of namespaces to declare as using with "using namespace" +# default: [] +using_namespaces = [ ] + +# A list of sys headers to #include (with angle brackets) +# default: [] +sys_includes = [] + +# A list of headers to #include (with quotes) +# default: [] +includes = [] + +# Whether cbindgen's default C/C++ standard imports should be suppressed. These +# imports are included by default because our generated headers tend to require +# them (e.g. for uint32_t). Currently, the generated imports are: +# +# * for C: , , , , +# +# * for C++: , , , , (depending on config) +# +# default: false +no_includes = false + +# Code Style Options + +# The style to use for curly braces +# +# possible values: "SameLine", "NextLine" +# +# default: "SameLine" +braces = "SameLine" + +# The desired length of a line to use when formatting lines +# default: 100 +line_length = 100 + +# The amount of spaces to indent by +# default: 2 +tab_width = 2 + +# How the generated documentation should be commented. +# +# possible values: +# * "c": /* like this */ +# * "c99": // like this +# * "c++": /// like this +# * "doxy": like C, but with leading *'s on each line +# * "auto": "c++" if that's the language, "doxy" otherwise +# +# default: "auto" +documentation_style = "auto" + +# Codegen Options + +# When generating a C header, the kind of declaration style to use for structs +# or enums. +# +# possible values: +# * "type": typedef struct { ... } MyType; +# * "tag": struct MyType { ... }; +# * "both": typedef struct MyType { ... } MyType; +# +# default: "both" +style = "both" + +# A list of substitutions for converting cfg's to ifdefs. cfgs which aren't +# defined here will just be discarded. +# +# e.g. +# `#[cfg(target = "freebsd")] ...` +# becomes +# `#if defined(DEFINE_FREEBSD) ... #endif` +[defines] +#"target_os = freebsd" = "DEFINE_FREEBSD" +#"feature = serde" = "DEFINE_SERDE" + +[export] +# A list of additional items to always include in the generated bindings if they're +# found but otherwise don't appear to be used by the public API. +# +# default: [] +include = [] + +# A list of items to not include in the generated bindings +# default: [] +exclude = [] + +# A prefix to add before the name of every item +# default: no prefix is added +prefix = "" + +# Types of items that we'll generate. If empty, then all types of item are emitted. +# +# possible items: (TODO: explain these in detail) +# * "constants": +# * "globals": +# * "enums": +# * "structs": +# * "unions": +# * "typedefs": +# * "opaque": +# * "functions": +# +# default: [] +item_types = [] + +# Whether applying rules in export.rename prevents export.prefix from applying. +# +# e.g. given this toml: +# +# [export] +# prefix = "capi_" +# [export.rename] +# "MyType" = "my_cool_type" +# +# You get the following results: +# +# renaming_overrides_prefixing = true: +# "MyType" => "my_cool_type" +# +# renaming_overrides_prefixing = false: +# "MyType => capi_my_cool_type" +# +# default: false +renaming_overrides_prefixing = true + +# Table of name conversions to apply to item names (lhs becomes rhs) +[export.rename] +"AtomicI32" = "int" + +# Table of things to add to the body of any struct, union, or enum that has the +# given name. This can be used to add things like methods which don't change ABI. +[export.body] +#"MyType" = """ +# void cppMethod() const; +#""" + +[layout] +# A string that should come before the name of any type which has been marked +# as `#[repr(packed)]`. For instance, "__attribute__((packed))" would be a +# reasonable value if targeting gcc/clang. A more portable solution would +# involve emitting the name of a macro which you define in a platform-specific +# way. e.g. "PACKED" +# +# default: `#[repr(packed)]` types will be treated as opaque, since it would +# be unsafe for C callers to use a incorrectly laid-out union. +packed = "PACKED" + +# A string that should come before the name of any type which has been marked +# as `#[repr(align(n))]`. This string must be a function-like macro which takes +# a single argument (the requested alignment, `n`). For instance, a macro +# `#define`d as `ALIGNED(n)` in `header` which translates to +# `__attribute__((aligned(n)))` would be a reasonable value if targeting +# gcc/clang. +# +# default: `#[repr(align(n))]` types will be treated as opaque, since it +# could be unsafe for C callers to use a incorrectly-aligned union. +aligned_n = "ALIGNED" + + +[fn] +# An optional prefix to put before every function declaration +# default: no prefix added +prefix = "" + +# An optional postfix to put after any function declaration +# default: no postix added +postfix = "" + +# How to format function arguments +# +# possible values: +# * "horizontal": place all arguments on the same line +# * "vertical": place each argument on its own line +# * "auto": only use vertical if horizontal would exceed line_length +# +# default: "auto" +args = "horizontal" + +# An optional string that should prefix function declarations which have been +# marked as `#[must_use]`. For instance, "__attribute__((warn_unused_result))" +# would be a reasonable value if targeting gcc/clang. A more portable solution +# would involve emitting the name of a macro which you define in a +# platform-specific way. e.g. "MUST_USE_FUNC" +# default: nothing is emitted for must_use functions +must_use = "MUST_USE_FUNC" + +# A rule to use to rename function argument names. The renaming assumes the input +# is the Rust standard snake_case, however it accepts all the different rename_args +# inputs. This means many options here are no-ops or redundant. +# +# possible values (that actually do something): +# * "CamelCase": my_arg => myArg +# * "PascalCase": my_arg => MyArg +# * "GeckoCase": my_arg => aMyArg +# * "ScreamingSnakeCase": my_arg => MY_ARG +# * "None": apply no renaming +# +# technically possible values (that shouldn't have a purpose here): +# * "SnakeCase": apply no renaming +# * "LowerCase": apply no renaming (actually applies to_lowercase, is this bug?) +# * "UpperCase": same as ScreamingSnakeCase in this context +# * "QualifiedScreamingSnakeCase" => same as ScreamingSnakeCase in this context +# +# default: "None" +rename_args = "CamelCase" + +[struct] +# A rule to use to rename struct field names. The renaming assumes the input is +# the Rust standard snake_case, however it acccepts all the different rename_args +# inputs. This means many options here are no-ops or redundant. +# +# possible values (that actually do something): +# * "CamelCase": my_arg => myArg +# * "PascalCase": my_arg => MyArg +# * "GeckoCase": my_arg => mMyArg +# * "ScreamingSnakeCase": my_arg => MY_ARG +# * "None": apply no renaming +# +# technically possible values (that shouldn't have a purpose here): +# * "SnakeCase": apply no renaming +# * "LowerCase": apply no renaming (actually applies to_lowercase, is this bug?) +# * "UpperCase": same as ScreamingSnakeCase in this context +# * "QualifiedScreamingSnakeCase" => same as ScreamingSnakeCase in this context +# +# default: "None" +rename_fields = "PascalCase" + +# An optional string that should come before the name of any struct which has been +# marked as `#[must_use]`. For instance, "__attribute__((warn_unused))" +# would be a reasonable value if targeting gcc/clang. A more portable solution +# would involve emitting the name of a macro which you define in a +# platform-specific way. e.g. "MUST_USE_STRUCT" +# +# default: nothing is emitted for must_use structs +must_use = "MUST_USE_STRUCT" + +# Whether a Rust type with associated consts should emit those consts inside the +# type's body. Otherwise they will be emitted trailing and with the type's name +# prefixed. This does nothing if the target is C, or if +# [const]allow_static_const = false +# +# default: false +# associated_constants_in_body: false + +# Whether to derive a simple constructor that takes a value for every field. +# default: false +derive_constructor = true + +# Whether to derive an operator== for all structs +# default: false +derive_eq = false + +# Whether to derive an operator!= for all structs +# default: false +derive_neq = false + +# Whether to derive an operator< for all structs +# default: false +derive_lt = false + +# Whether to derive an operator<= for all structs +# default: false +derive_lte = false + +# Whether to derive an operator> for all structs +# default: false +derive_gt = false + +# Whether to derive an operator>= for all structs +# default: false +derive_gte = false + +[enum] +# A rule to use to rename enum variants, and the names of any fields those +# variants have. This should probably be split up into two separate options, but +# for now, they're the same! See the documentation for `[struct]rename_fields` +# for how this applies to fields. Renaming of the variant assumes that the input +# is the Rust standard PascalCase. In the case of QualifiedScreamingSnakeCase, +# it also assumed that the enum's name is PascalCase. +# +# possible values (that actually do something): +# * "CamelCase": MyVariant => myVariant +# * "SnakeCase": MyVariant => my_variant +# * "ScreamingSnakeCase": MyVariant => MY_VARIANT +# * "QualifiedScreamingSnakeCase": MyVariant => ENUM_NAME_MY_VARIANT +# * "LowerCase": MyVariant => myvariant +# * "UpperCase": MyVariant => MYVARIANT +# * "None": apply no renaming +# +# technically possible values (that shouldn't have a purpose for the variants): +# * "PascalCase": apply no renaming +# * "GeckoCase": apply no renaming +# +# default: "None" +rename_variants = "None" + +# Whether an extra "sentinel" enum variant should be added to all generated enums. +# Firefox uses this for their IPC serialization library. +# +# WARNING: if the sentinel is ever passed into Rust, behaviour will be Undefined. +# Rust does not know about this value, and will assume it cannot happen. +# +# default: false +add_sentinel = false + +# Whether enum variant names should be prefixed with the name of the enum. +# default: false +prefix_with_name = false + +# Whether to generate static `::MyVariant(..)` constructors and `bool IsMyVariant()` +# methods for enums with fields. +# +# default: false +derive_helper_methods = false + +# Whether to generate `const MyVariant& AsMyVariant() const` methods for enums with fields. +# default: false +derive_const_casts = false + +# Whether to generate `MyVariant& AsMyVariant()` methods for enums with fields +# default: false +derive_mut_casts = false + +# The name of the macro/function to use for asserting `IsMyVariant()` in the body of +# derived `AsMyVariant()` cast methods. +# +# default: "assert" (but also causes `` to be included by default) +cast_assert_name = "MOZ_RELEASE_ASSERT" + +# An optional string that should come before the name of any enum which has been +# marked as `#[must_use]`. For instance, "__attribute__((warn_unused))" +# would be a reasonable value if targeting gcc/clang. A more portable solution +# would involve emitting the name of a macro which you define in a +# platform-specific way. e.g. "MUST_USE_ENUM" +# +# Note that this refers to the *output* type. That means this will not apply to an enum +# with fields, as it will be emitted as a struct. `[struct]must_use` will apply there. +# +# default: nothing is emitted for must_use enums +must_use = "MUST_USE_ENUM" + +# Whether enums with fields should generate destructors. This exists so that generic +# enums can be properly instantiated with payloads that are C++ types with +# destructors. This isn't necessary for structs because C++ has rules to +# automatically derive the correct constructors and destructors for those types. +# +# Care should be taken with this option, as Rust and C++ cannot +# properly interoperate with eachother's notions of destructors. Also, this may +# change the ABI for the type. Either your destructor-full enums must live +# exclusively within C++, or they must only be passed by-reference between +# C++ and Rust. +# +# default: false +derive_tagged_enum_destructor = false + +# Whether enums with fields should generate copy-constructor. See the discussion on +# derive_tagged_enum_destructor for why this is both useful and very dangerous. +# +# default: false +derive_tagged_enum_copy_constructor = false + +# Whether enums with fields should generate an empty, private destructor. +# This allows the auto-generated constructor functions to compile, if there are +# non-trivially constructible members. This falls in the same family of +# dangerousness as `derive_tagged_enum_copy_constructor` and co. +# +# default: false +private_default_tagged_enum_constructor = false + +[const] +# Whether a generated constant can be a static const in C++ mode. I have no +# idea why you would turn this off. +# +# default: true +allow_static_const = true + +[macro_expansion] +# Whether bindings should be generated for instances of the bitflags! macro. +# default: false +bitflags = false + +# Options for how your Rust library should be parsed + +[parse] +# Whether to parse dependent crates and include their types in the output +# default: false +parse_deps = false + +# A white list of crate names that are allowed to be parsed. If this is defined, +# only crates found in this list will ever be parsed. +# +# default: there is no whitelist (NOTE: this is the opposite of []) +include = [] + +# A black list of crate names that are not allowed to be parsed. +# default: [] +exclude = [] + +# Whether to use a new temporary target directory when running `rustc --pretty=expanded`. +# This may be required for some build processes. +# +# default: false +clean = false + +# Which crates other than the top-level binding crate we should generate +# bindings for. +# +# default: [] +extra_bindings = [] + +[parse.expand] +# A list of crate names that should be run through `cargo expand` before +# parsing to expand any macros. Note that if a crate is named here, it +# will always be parsed, even if the blacklist/whitelist says it shouldn't be. +# +# default: [] +crates = [] + +# If enabled, use the `--all-features` option when expanding. Ignored when +# `features` is set. For backwards-compatibility, this is forced on if +# `expand = ["euclid"]` shorthand is used. +# +# default: false +all_features = false + +# When `all_features` is disabled and this is also disabled, use the +# `--no-default-features` option when expanding. +# +# default: true +default_features = true + +# A list of feature names that should be used when running `cargo expand`. This +# combines with `default_features` like in your `Cargo.toml`. Note that the features +# listed here are features for the current crate being built, *not* the crates +# being expanded. The crate's `Cargo.toml` must take care of enabling the +# appropriate features in its dependencies +# +# default: [] +features = ["cbindgen"] \ No newline at end of file diff --git a/cbindgen_cpp.toml b/cbindgen_cpp.toml new file mode 100644 index 0000000..3864569 --- /dev/null +++ b/cbindgen_cpp.toml @@ -0,0 +1,486 @@ +# The language to output bindings in +# +# possible values: "C", "C++" +# +# default: "C++" +language = "C++" + +# Options for wrapping the contents of the header: + +# An optional string of text to output at the beginning of the generated file +# default: doesn't emit anything +header = """ +#ifdef _MSC_VER +#define PACKED + #pragma pack(push, 1) +#else + #define PACKED __attribute__((packed)) +#endif +""" + +# An optional string of text to output at the end of the generated file +# default: doesn't emit anything +trailer = "/* Text to put at the end of the generated file */" + +# An optional name to use as an include guard +# default: doesn't emit an include guard +include_guard = "dll_syringe" + +# An optional string of text to output between major sections of the generated +# file as a warning against manual editing +# +# default: doesn't emit anything +autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */" + +# Whether to include a comment with the version of cbindgen used to generate the file +# default: false +include_version = true + +# An optional namespace to output around the generated bindings +# default: doesn't emit a namespace +namespace = "dll_syringe" + +# An optional list of namespaces to output around the generated bindings +# default: [] +namespaces = [ ] + +# An optional list of namespaces to declare as using with "using namespace" +# default: [] +using_namespaces = [ ] + +# A list of sys headers to #include (with angle brackets) +# default: [] +sys_includes = [] + +# A list of headers to #include (with quotes) +# default: [] +includes = [] + +# Whether cbindgen's default C/C++ standard imports should be suppressed. These +# imports are included by default because our generated headers tend to require +# them (e.g. for uint32_t). Currently, the generated imports are: +# +# * for C: , , , , +# +# * for C++: , , , , (depending on config) +# +# default: false +no_includes = false + +# Code Style Options + +# The style to use for curly braces +# +# possible values: "SameLine", "NextLine" +# +# default: "SameLine" +braces = "SameLine" + +# The desired length of a line to use when formatting lines +# default: 100 +line_length = 100 + +# The amount of spaces to indent by +# default: 2 +tab_width = 2 + +# How the generated documentation should be commented. +# +# possible values: +# * "c": /* like this */ +# * "c99": // like this +# * "c++": /// like this +# * "doxy": like C, but with leading *'s on each line +# * "auto": "c++" if that's the language, "doxy" otherwise +# +# default: "auto" +documentation_style = "auto" + +# Codegen Options + +# When generating a C header, the kind of declaration style to use for structs +# or enums. +# +# possible values: +# * "type": typedef struct { ... } MyType; +# * "tag": struct MyType { ... }; +# * "both": typedef struct MyType { ... } MyType; +# +# default: "both" +style = "both" + +# A list of substitutions for converting cfg's to ifdefs. cfgs which aren't +# defined here will just be discarded. +# +# e.g. +# `#[cfg(target = "freebsd")] ...` +# becomes +# `#if defined(DEFINE_FREEBSD) ... #endif` +[defines] +#"target_os = freebsd" = "DEFINE_FREEBSD" +#"feature = serde" = "DEFINE_SERDE" + +[export] +# A list of additional items to always include in the generated bindings if they're +# found but otherwise don't appear to be used by the public API. +# +# default: [] +include = [] + +# A list of items to not include in the generated bindings +# default: [] +exclude = [] + +# A prefix to add before the name of every item +# default: no prefix is added +prefix = "" + +# Types of items that we'll generate. If empty, then all types of item are emitted. +# +# possible items: (TODO: explain these in detail) +# * "constants": +# * "globals": +# * "enums": +# * "structs": +# * "unions": +# * "typedefs": +# * "opaque": +# * "functions": +# +# default: [] +item_types = [] + +# Whether applying rules in export.rename prevents export.prefix from applying. +# +# e.g. given this toml: +# +# [export] +# prefix = "capi_" +# [export.rename] +# "MyType" = "my_cool_type" +# +# You get the following results: +# +# renaming_overrides_prefixing = true: +# "MyType" => "my_cool_type" +# +# renaming_overrides_prefixing = false: +# "MyType => capi_my_cool_type" +# +# default: false +renaming_overrides_prefixing = true + +# Table of name conversions to apply to item names (lhs becomes rhs) +[export.rename] +"AtomicI32" = "int" + +# Table of things to add to the body of any struct, union, or enum that has the +# given name. This can be used to add things like methods which don't change ABI. +[export.body] +#"MyType" = """ +# void cppMethod() const; +#""" + +[layout] +# A string that should come before the name of any type which has been marked +# as `#[repr(packed)]`. For instance, "__attribute__((packed))" would be a +# reasonable value if targeting gcc/clang. A more portable solution would +# involve emitting the name of a macro which you define in a platform-specific +# way. e.g. "PACKED" +# +# default: `#[repr(packed)]` types will be treated as opaque, since it would +# be unsafe for C callers to use a incorrectly laid-out union. +packed = "PACKED" + +# A string that should come before the name of any type which has been marked +# as `#[repr(align(n))]`. This string must be a function-like macro which takes +# a single argument (the requested alignment, `n`). For instance, a macro +# `#define`d as `ALIGNED(n)` in `header` which translates to +# `__attribute__((aligned(n)))` would be a reasonable value if targeting +# gcc/clang. +# +# default: `#[repr(align(n))]` types will be treated as opaque, since it +# could be unsafe for C callers to use a incorrectly-aligned union. +aligned_n = "ALIGNED" + + +[fn] +# An optional prefix to put before every function declaration +# default: no prefix added +prefix = "" + +# An optional postfix to put after any function declaration +# default: no postix added +postfix = "" + +# How to format function arguments +# +# possible values: +# * "horizontal": place all arguments on the same line +# * "vertical": place each argument on its own line +# * "auto": only use vertical if horizontal would exceed line_length +# +# default: "auto" +args = "horizontal" + +# An optional string that should prefix function declarations which have been +# marked as `#[must_use]`. For instance, "__attribute__((warn_unused_result))" +# would be a reasonable value if targeting gcc/clang. A more portable solution +# would involve emitting the name of a macro which you define in a +# platform-specific way. e.g. "MUST_USE_FUNC" +# default: nothing is emitted for must_use functions +must_use = "MUST_USE_FUNC" + +# A rule to use to rename function argument names. The renaming assumes the input +# is the Rust standard snake_case, however it accepts all the different rename_args +# inputs. This means many options here are no-ops or redundant. +# +# possible values (that actually do something): +# * "CamelCase": my_arg => myArg +# * "PascalCase": my_arg => MyArg +# * "GeckoCase": my_arg => aMyArg +# * "ScreamingSnakeCase": my_arg => MY_ARG +# * "None": apply no renaming +# +# technically possible values (that shouldn't have a purpose here): +# * "SnakeCase": apply no renaming +# * "LowerCase": apply no renaming (actually applies to_lowercase, is this bug?) +# * "UpperCase": same as ScreamingSnakeCase in this context +# * "QualifiedScreamingSnakeCase" => same as ScreamingSnakeCase in this context +# +# default: "None" +rename_args = "CamelCase" + +[struct] +# A rule to use to rename struct field names. The renaming assumes the input is +# the Rust standard snake_case, however it acccepts all the different rename_args +# inputs. This means many options here are no-ops or redundant. +# +# possible values (that actually do something): +# * "CamelCase": my_arg => myArg +# * "PascalCase": my_arg => MyArg +# * "GeckoCase": my_arg => mMyArg +# * "ScreamingSnakeCase": my_arg => MY_ARG +# * "None": apply no renaming +# +# technically possible values (that shouldn't have a purpose here): +# * "SnakeCase": apply no renaming +# * "LowerCase": apply no renaming (actually applies to_lowercase, is this bug?) +# * "UpperCase": same as ScreamingSnakeCase in this context +# * "QualifiedScreamingSnakeCase" => same as ScreamingSnakeCase in this context +# +# default: "None" +rename_fields = "PascalCase" + +# An optional string that should come before the name of any struct which has been +# marked as `#[must_use]`. For instance, "__attribute__((warn_unused))" +# would be a reasonable value if targeting gcc/clang. A more portable solution +# would involve emitting the name of a macro which you define in a +# platform-specific way. e.g. "MUST_USE_STRUCT" +# +# default: nothing is emitted for must_use structs +must_use = "MUST_USE_STRUCT" + +# Whether a Rust type with associated consts should emit those consts inside the +# type's body. Otherwise they will be emitted trailing and with the type's name +# prefixed. This does nothing if the target is C, or if +# [const]allow_static_const = false +# +# default: false +# associated_constants_in_body: false + +# Whether to derive a simple constructor that takes a value for every field. +# default: false +derive_constructor = true + +# Whether to derive an operator== for all structs +# default: false +derive_eq = false + +# Whether to derive an operator!= for all structs +# default: false +derive_neq = false + +# Whether to derive an operator< for all structs +# default: false +derive_lt = false + +# Whether to derive an operator<= for all structs +# default: false +derive_lte = false + +# Whether to derive an operator> for all structs +# default: false +derive_gt = false + +# Whether to derive an operator>= for all structs +# default: false +derive_gte = false + +[enum] +# A rule to use to rename enum variants, and the names of any fields those +# variants have. This should probably be split up into two separate options, but +# for now, they're the same! See the documentation for `[struct]rename_fields` +# for how this applies to fields. Renaming of the variant assumes that the input +# is the Rust standard PascalCase. In the case of QualifiedScreamingSnakeCase, +# it also assumed that the enum's name is PascalCase. +# +# possible values (that actually do something): +# * "CamelCase": MyVariant => myVariant +# * "SnakeCase": MyVariant => my_variant +# * "ScreamingSnakeCase": MyVariant => MY_VARIANT +# * "QualifiedScreamingSnakeCase": MyVariant => ENUM_NAME_MY_VARIANT +# * "LowerCase": MyVariant => myvariant +# * "UpperCase": MyVariant => MYVARIANT +# * "None": apply no renaming +# +# technically possible values (that shouldn't have a purpose for the variants): +# * "PascalCase": apply no renaming +# * "GeckoCase": apply no renaming +# +# default: "None" +rename_variants = "None" + +# Whether an extra "sentinel" enum variant should be added to all generated enums. +# Firefox uses this for their IPC serialization library. +# +# WARNING: if the sentinel is ever passed into Rust, behaviour will be Undefined. +# Rust does not know about this value, and will assume it cannot happen. +# +# default: false +add_sentinel = false + +# Whether enum variant names should be prefixed with the name of the enum. +# default: false +prefix_with_name = false + +# Whether to generate static `::MyVariant(..)` constructors and `bool IsMyVariant()` +# methods for enums with fields. +# +# default: false +derive_helper_methods = false + +# Whether to generate `const MyVariant& AsMyVariant() const` methods for enums with fields. +# default: false +derive_const_casts = false + +# Whether to generate `MyVariant& AsMyVariant()` methods for enums with fields +# default: false +derive_mut_casts = false + +# The name of the macro/function to use for asserting `IsMyVariant()` in the body of +# derived `AsMyVariant()` cast methods. +# +# default: "assert" (but also causes `` to be included by default) +cast_assert_name = "MOZ_RELEASE_ASSERT" + +# An optional string that should come before the name of any enum which has been +# marked as `#[must_use]`. For instance, "__attribute__((warn_unused))" +# would be a reasonable value if targeting gcc/clang. A more portable solution +# would involve emitting the name of a macro which you define in a +# platform-specific way. e.g. "MUST_USE_ENUM" +# +# Note that this refers to the *output* type. That means this will not apply to an enum +# with fields, as it will be emitted as a struct. `[struct]must_use` will apply there. +# +# default: nothing is emitted for must_use enums +must_use = "MUST_USE_ENUM" + +# Whether enums with fields should generate destructors. This exists so that generic +# enums can be properly instantiated with payloads that are C++ types with +# destructors. This isn't necessary for structs because C++ has rules to +# automatically derive the correct constructors and destructors for those types. +# +# Care should be taken with this option, as Rust and C++ cannot +# properly interoperate with eachother's notions of destructors. Also, this may +# change the ABI for the type. Either your destructor-full enums must live +# exclusively within C++, or they must only be passed by-reference between +# C++ and Rust. +# +# default: false +derive_tagged_enum_destructor = false + +# Whether enums with fields should generate copy-constructor. See the discussion on +# derive_tagged_enum_destructor for why this is both useful and very dangerous. +# +# default: false +derive_tagged_enum_copy_constructor = false + +# Whether enums with fields should generate an empty, private destructor. +# This allows the auto-generated constructor functions to compile, if there are +# non-trivially constructible members. This falls in the same family of +# dangerousness as `derive_tagged_enum_copy_constructor` and co. +# +# default: false +private_default_tagged_enum_constructor = false + +[const] +# Whether a generated constant can be a static const in C++ mode. I have no +# idea why you would turn this off. +# +# default: true +allow_static_const = true + +[macro_expansion] +# Whether bindings should be generated for instances of the bitflags! macro. +# default: false +bitflags = false + +# Options for how your Rust library should be parsed + +[parse] +# Whether to parse dependent crates and include their types in the output +# default: false +parse_deps = false + +# A white list of crate names that are allowed to be parsed. If this is defined, +# only crates found in this list will ever be parsed. +# +# default: there is no whitelist (NOTE: this is the opposite of []) +include = [] + +# A black list of crate names that are not allowed to be parsed. +# default: [] +exclude = [] + +# Whether to use a new temporary target directory when running `rustc --pretty=expanded`. +# This may be required for some build processes. +# +# default: false +clean = false + +# Which crates other than the top-level binding crate we should generate +# bindings for. +# +# default: [] +extra_bindings = [] + +[parse.expand] +# A list of crate names that should be run through `cargo expand` before +# parsing to expand any macros. Note that if a crate is named here, it +# will always be parsed, even if the blacklist/whitelist says it shouldn't be. +# +# default: [] +crates = [] + +# If enabled, use the `--all-features` option when expanding. Ignored when +# `features` is set. For backwards-compatibility, this is forced on if +# `expand = ["euclid"]` shorthand is used. +# +# default: false +all_features = false + +# When `all_features` is disabled and this is also disabled, use the +# `--no-default-features` option when expanding. +# +# default: true +default_features = true + +# A list of feature names that should be used when running `cargo expand`. This +# combines with `default_features` like in your `Cargo.toml`. Note that the features +# listed here are features for the current crate being built, *not* the crates +# being expanded. The crate's `Cargo.toml` must take care of enabling the +# appropriate features in its dependencies +# +# default: [] +features = ["cbindgen"] \ No newline at end of file diff --git a/clean.ps1 b/clean.ps1 deleted file mode 100644 index b5e7270..0000000 --- a/clean.ps1 +++ /dev/null @@ -1,9 +0,0 @@ -cargo clean - -Set-Location .\tests\helpers\test_payload -cargo clean -Set-Location ..\..\.. - -Set-Location .\tests\helpers\test_target -cargo clean -Set-Location ..\..\.. diff --git a/dll-syringe.sln b/dll-syringe.sln new file mode 100644 index 0000000..b910aba --- /dev/null +++ b/dll-syringe.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "csharp", "csharp\csharp.csproj", "{F984ECFF-FE18-456A-9F11-789454DFCB87}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F984ECFF-FE18-456A-9F11-789454DFCB87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F984ECFF-FE18-456A-9F11-789454DFCB87}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F984ECFF-FE18-456A-9F11-789454DFCB87}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F984ECFF-FE18-456A-9F11-789454DFCB87}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C0C6BF2E-7E97-457D-8547-69772834230A} + EndGlobalSection +EndGlobal diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..271800c --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" \ No newline at end of file diff --git a/scripts/clean.ps1 b/scripts/clean.ps1 new file mode 100644 index 0000000..4c6df25 --- /dev/null +++ b/scripts/clean.ps1 @@ -0,0 +1,11 @@ +# Navigate up one folder from the current script location +Set-Location "$PSScriptRoot\.." +cargo clean + +Set-Location "./tests/helpers/test_payload" +cargo clean +Set-Location "../../.." + +Set-Location "./tests/helpers/test_target" +cargo clean +Set-Location "../../.." diff --git a/scripts/test-wine.ps1 b/scripts/test-wine.ps1 new file mode 100644 index 0000000..2518492 --- /dev/null +++ b/scripts/test-wine.ps1 @@ -0,0 +1,17 @@ +# Navigate up one folder from the current script location +Set-Location "$PSScriptRoot\.." + +# Testing +$env:CROSS_SYSROOT = "." # pretend we cross + +# Prebuild dummy projects. +cargo +nightly xwin rustc --manifest-path "tests/helpers/test_target/Cargo.toml" --target i686-pc-windows-msvc --xwin-arch x86 --xwin-cache-dir "target/cache/x86" +cargo +nightly xwin rustc --manifest-path "tests/helpers/test_payload/Cargo.toml" --target i686-pc-windows-msvc --xwin-arch x86 --xwin-cache-dir "target/cache/x86" +cargo +nightly xwin rustc --manifest-path "tests/helpers/test_target/Cargo.toml" --target x86_64-pc-windows-msvc --xwin-arch x86_64 --xwin-cache-dir "target/cache/x64" +cargo +nightly xwin rustc --manifest-path "tests/helpers/test_payload/Cargo.toml" --target x86_64-pc-windows-msvc --xwin-arch x86_64 --xwin-cache-dir "target/cache/x64" + +# Windows/MSVC x86 +cargo +nightly xwin test --target i686-pc-windows-msvc --xwin-arch x86 --xwin-cache-dir "target/cache/x86" + +# Windows/MSVC x64 +cargo +nightly xwin test --target x86_64-pc-windows-msvc --xwin-arch x86_64 --xwin-cache-dir "target/cache/x64" \ No newline at end of file diff --git a/scripts/test.ps1 b/scripts/test.ps1 new file mode 100644 index 0000000..a6c1e6c --- /dev/null +++ b/scripts/test.ps1 @@ -0,0 +1,8 @@ +# Navigate up one folder from the current script location +Set-Location "$PSScriptRoot\.." + +# Windows/MSVC x86 +cargo test --target i686-pc-windows-msvc -- --nocapture + +# Windows/MSVC x64 +cargo test --target x86_64-pc-windows-msvc -- --nocapture \ No newline at end of file diff --git a/src/c_exports.rs b/src/c_exports.rs new file mode 100644 index 0000000..0a8deaf --- /dev/null +++ b/src/c_exports.rs @@ -0,0 +1,178 @@ +use crate::process::{BorrowedProcessModule, OwnedProcess}; +use crate::Syringe; +use std::ffi::CStr; +use std::os::raw::c_char; +use std::path::Path; +use std::ptr::null_mut; + +// Note: Don't specify [repr(C)] for these structs, as they are not passed to C code. +// The C code only interacts with pointers to these structs. + +/// Represents an instance of a Syringe for a target process. +#[derive(Debug)] +pub struct CSyringe { + syringe: Syringe, +} + +/// Represents a module within a process, allowing for ejection of a previously injected module. +#[derive(Debug)] +pub struct CProcessModule<'a> { + module: BorrowedProcessModule<'a>, +} + +/// Creates a new `Syringe` instance for a process identified by PID. +/// +/// # Arguments +/// +/// * `pid` - The PID of the target process. +/// +/// # Returns +/// +/// A pointer to a `CSyringe` instance, or null if the process could not be opened. +#[no_mangle] +pub extern "C" fn syringe_for_process(pid: u32) -> *mut CSyringe { + let process = match OwnedProcess::from_pid(pid) { + Ok(process) => process, + Err(_) => return null_mut(), + }; + let syringe = Syringe::for_process(process); + let boxed = Box::new(CSyringe { syringe }); + Box::into_raw(boxed) // Directly return a pointer to the boxed CSyringe +} + +/// Creates a new `Syringe` instance for a suspended process identified by PID. +/// +/// # Arguments +/// +/// * `pid` - The PID of the target suspended process. +/// +/// # Returns +/// +/// A pointer to a `CSyringe` instance, or null if the process could not be opened or initialized. +#[no_mangle] +pub extern "C" fn syringe_for_suspended_process(pid: u32) -> *mut CSyringe { + let process = match OwnedProcess::from_pid(pid) { + Ok(process) => process, + Err(_) => return null_mut(), + }; + + match Syringe::for_suspended_process(process) { + Ok(syringe) => { + let boxed = Box::new(CSyringe { syringe }); + Box::into_raw(boxed) // Return a pointer to the boxed CSyringe + } + Err(_) => null_mut(), + } +} + +/// Injects a DLL into the target process associated with the given `Syringe`. +/// +/// # Safety +/// +/// This function is unsafe because it dereferences raw pointers. +/// +/// # Arguments +/// +/// * `c_syringe` - A pointer to the `CSyringe` instance. +/// * `dll_path` - A C string path to the DLL to be injected. +/// +/// # Returns +/// +/// `true` if injection succeeded, otherwise `false`. +#[no_mangle] +pub unsafe extern "C" fn syringe_inject(c_syringe: *mut CSyringe, dll_path: *const c_char) -> bool { + let c_syringe = unsafe { &mut *c_syringe }; + let path_str = unsafe { CStr::from_ptr(dll_path).to_str().unwrap() }; + c_syringe.syringe.inject(Path::new(path_str)).is_ok() +} + +/// Finds or injects a DLL into the target process. +/// +/// If the DLL is already present in the target process, it returns the existing module. +/// Otherwise, it injects the DLL. +/// +/// # Safety +/// +/// This function is unsafe because it dereferences raw pointers. +/// +/// # Arguments +/// +/// * `c_syringe` - A pointer to the `CSyringe` instance. +/// * `dll_path` - A C string path to the DLL to be injected. +/// +/// # Returns +/// +/// A pointer to a `CProcessModule`, or null if the operation failed. +#[no_mangle] +pub unsafe extern "C" fn syringe_find_or_inject<'a>( + c_syringe: *mut CSyringe, + dll_path: *const c_char, +) -> *mut CProcessModule<'a> { + let syringe = unsafe { &mut (*c_syringe).syringe }; + let path_str = unsafe { CStr::from_ptr(dll_path).to_str().unwrap() }; + match syringe.find_or_inject(Path::new(path_str)) { + Ok(module) => { + let c_module = Box::new(CProcessModule { module }); + Box::into_raw(c_module) + } + Err(_) => null_mut(), + } +} + +/// Ejects a module from the target process. +/// +/// # Arguments +/// +/// * `c_syringe` - A pointer to the `CSyringe` instance. +/// * `c_module` - A pointer to the `CProcessModule` to be ejected. +/// +/// # Returns +/// +/// `true` if ejection succeeded, otherwise `false`. +/// +/// # Safety +/// This is safe as long as it has a valid pointer to a Syringe and Module. +#[no_mangle] +pub unsafe extern "C" fn syringe_eject( + c_syringe: *mut CSyringe, + c_module: *mut CProcessModule<'_>, +) -> bool { + let syringe = unsafe { &mut (*c_syringe).syringe }; + let module = unsafe { &mut (*c_module).module }; + syringe.eject(*module).is_ok() +} + +/// Frees a `CSyringe` instance. +/// +/// # Arguments +/// +/// * `c_syringe` - A pointer to the `CSyringe` instance to be freed. +/// +/// # Safety +/// This is safe as long as it has a valid pointer to a Syringe instance. +#[no_mangle] +pub unsafe extern "C" fn syringe_free(c_syringe: *mut CSyringe) { + if !c_syringe.is_null() { + unsafe { + let _ = Box::from_raw(c_syringe); // drop + } + } +} + +/// Frees a `CProcessModule` instance. +/// +/// # Arguments +/// +/// * `c_module` - A pointer to the `CProcessModule` to be freed. +/// +/// # Safety +/// This is safe as long as it has a valid pointer to a module +/// created by this Syringe instance. +#[no_mangle] +pub unsafe extern "C" fn syringe_module_free(c_module: *mut CProcessModule<'_>) { + if !c_module.is_null() { + unsafe { + let _ = Box::from_raw(c_module); // drop + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 8aaa914..b697940 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,10 @@ pub mod error; /// Module containing traits and types for working with function pointers. pub mod function; +/// C exports for the library. +#[cfg(feature = "c-exports")] +pub mod c_exports; + #[cfg(feature = "payload-utils")] #[doc(hidden)] pub mod payload_utils; diff --git a/src/process/memory/buffer.rs b/src/process/memory/buffer.rs index 5ebb334..be746f8 100644 --- a/src/process/memory/buffer.rs +++ b/src/process/memory/buffer.rs @@ -338,6 +338,12 @@ impl<'a> ProcessMemorySlice<'a> { } let mut bytes_written = 0; + if buf.is_empty() { + // This works around a discrepancy between Wine and actual Windows. + // On Wine, a 0 sized write fails, on Windows this suceeds. Will file as bug soon. + return Ok(()); + } + let result = unsafe { WriteProcessMemory( self.process.as_raw_handle(), diff --git a/src/process/memory/mod.rs b/src/process/memory/mod.rs index b7c91c3..77f7d2b 100644 --- a/src/process/memory/mod.rs +++ b/src/process/memory/mod.rs @@ -1,4 +1,5 @@ mod buffer; +#[allow(unused_imports)] pub use buffer::*; #[cfg(feature = "syringe")] diff --git a/src/process/memory/raw_allocator.rs b/src/process/memory/raw_allocator.rs index e274472..797876a 100644 --- a/src/process/memory/raw_allocator.rs +++ b/src/process/memory/raw_allocator.rs @@ -1,6 +1,5 @@ -use std::{collections::LinkedList, io, mem, ptr::NonNull}; - use crate::process::{memory::ProcessMemoryBuffer, BorrowedProcess, Process}; +use std::{collections::LinkedList, io, mem, ptr::NonNull}; pub trait RawAllocator { type Error; @@ -224,12 +223,17 @@ pub enum AllocError { } #[cfg(test)] -mod tests { - use std::mem; - - use crate::process::memory::ProcessMemorySlice; +struct AlignTestStruct { + _a: u8, + _b: u16, + _c: u32, + _d: u64, +} +#[cfg(test)] +mod tests { use super::*; + use crate::process::memory::ProcessMemorySlice; #[test] fn single_alloc() { @@ -367,11 +371,3 @@ mod tests { assert!(alloc.len > page_size); } } - -#[cfg(test)] -struct AlignTestStruct { - _a: u8, - _b: u16, - _c: u32, - _d: u64, -} diff --git a/src/rpc/raw.rs b/src/rpc/raw.rs index b6abdff..767ab97 100644 --- a/src/rpc/raw.rs +++ b/src/rpc/raw.rs @@ -1,5 +1,4 @@ -use iced_x86::{code_asm::*, IcedError}; - +use iced_x86::code_asm::*; use std::{ any::{self, TypeId}, cell::OnceCell, @@ -165,6 +164,7 @@ where Self::build_call_stub_x64(self.ptr, result.as_ptr().as_ptr(), float_mask).unwrap() }; let code = self.remote_allocator.alloc_and_copy_buf(code.as_slice())?; + #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] code.memory().flush_instruction_cache()?; Ok(RemoteRawProcedureStub { diff --git a/src/rpc/rpc_core.rs b/src/rpc/rpc_core.rs index 3d33e67..f1cf76f 100644 --- a/src/rpc/rpc_core.rs +++ b/src/rpc/rpc_core.rs @@ -1,5 +1,4 @@ -use iced_x86::{code_asm::*, IcedError}; - +use iced_x86::code_asm::*; use std::{ ffi::CString, mem, @@ -83,6 +82,7 @@ impl Syringe { .unwrap() }; let function_stub = self.remote_allocator.alloc_and_copy_buf(code.as_slice())?; + #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] function_stub.memory().flush_instruction_cache()?; Ok(RemoteProcedureStub { diff --git a/src/syringe.rs b/src/syringe.rs index 4351c01..d713a4e 100644 --- a/src/syringe.rs +++ b/src/syringe.rs @@ -27,7 +27,7 @@ use crate::{ #[cfg(all(target_arch = "x86_64", feature = "into-x86-from-x64"))] use { goblin::pe::PE, - std::{convert::TryInto, fs, mem::MaybeUninit, path::PathBuf, time::Duration}, + std::{fs, mem::MaybeUninit, path::PathBuf, time::Duration}, widestring::U16Str, winapi::{shared::minwindef::MAX_PATH, um::wow64apiset::GetSystemWow64DirectoryW}, }; @@ -116,6 +116,28 @@ impl Syringe { } } + /// Creates a new syringe for the given suspended target process. + pub fn for_suspended_process(process: OwnedProcess) -> Result { + let syringe = Self::for_process(process); + + // If we are injecting into a 'suspended' process, then said process is said to not be fully + // initialized. This means: + // - We can't use `EnumProcessModulesEx` and friends. + // - So we can't locate Kernel32.dll in 32-bit process (from 64-bit process) + // - And therefore calling LoadLibrary is not possible. + + // Thankfully we can 'initialize' this suspended process without running any end user logic + // (e.g. a game's entry point) by creating a dummy method and invoking it. + let ret: u8 = 0xC3; + let bx = syringe.remote_allocator.alloc_and_copy(&ret)?; + syringe.process().run_remote_thread( + unsafe { mem::transmute(bx.as_raw_ptr()) }, + std::ptr::null::() as *mut u8, + )?; + + Ok(syringe) + } + /// Returns the target process for this syringe. pub fn process(&self) -> BorrowedProcess<'_> { self.remote_allocator.process() diff --git a/test.ps1 b/test.ps1 deleted file mode 100644 index bf8ab5c..0000000 --- a/test.ps1 +++ /dev/null @@ -1,2 +0,0 @@ -cargo test --target i686-pc-windows-msvc -- --nocapture -cargo test --target x86_64-pc-windows-msvc -- --nocapture diff --git a/tests/common/mod.rs b/tests/common/mod.rs index d666d51..657172c 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,44 +1,41 @@ +use core::iter::once; +use dll_syringe::process::OwnedProcess; +use dll_syringe::process::Process; +use std::ffi::CString; +use std::ffi::OsStr; +use std::mem::zeroed; +use std::os::windows::io::FromRawHandle; +use std::os::windows::io::OwnedHandle; +use std::os::windows::prelude::OsStrExt; +use std::path::Path; +use std::ptr::null_mut; use std::{ + env::{current_dir, var}, error::Error, + fs::{canonicalize, remove_file, File}, + io::{copy, ErrorKind}, path::PathBuf, process::{Command, Stdio}, str::FromStr, + sync::Mutex, }; +use winapi::um::processthreadsapi::{CreateProcessW, PROCESS_INFORMATION, STARTUPINFOW}; +use winapi::um::winbase::CREATE_SUSPENDED; pub fn build_test_payload_x86() -> Result> { - build_helper_crate( - "test_payload", - Some(&find_x86_variant_of_target()), - false, - "dll", - ) + build_helper_crate("test_payload", &find_x86_variant_of_target(), false, "dll") } pub fn build_test_target_x86() -> Result> { - build_helper_crate( - "test_target", - Some(&find_x86_variant_of_target()), - false, - "exe", - ) + build_helper_crate("test_target", &find_x86_variant_of_target(), false, "exe") } pub fn build_test_payload_x64() -> Result> { - build_helper_crate( - "test_payload", - Some(&find_x64_variant_of_target()), - false, - "dll", - ) + build_helper_crate("test_payload", &find_x64_variant_of_target(), false, "dll") } pub fn build_test_target_x64() -> Result> { - build_helper_crate( - "test_target", - Some(&find_x64_variant_of_target()), - false, - "exe", - ) + build_helper_crate("test_target", &find_x64_variant_of_target(), false, "exe") } fn find_x64_variant_of_target() -> String { @@ -51,7 +48,7 @@ fn find_x86_variant_of_target() -> String { pub fn build_helper_crate( crate_name: &str, - target: Option<&str>, + target: &str, release: bool, ext: &str, ) -> Result> { @@ -59,37 +56,84 @@ pub fn build_helper_crate( .join(crate_name) .canonicalize()?; - let mut command = Command::new("cargo"); - command - .arg("build") - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()); - if let Some(target) = target { - command.arg("--target").arg(target); + // For cross/wine testing, we precompile in external script. + if !is_cross() { + let mut command = Command::new("cargo"); + command + .arg("build") + .arg("--target") + .arg(target) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + + let exit_code = command.current_dir(&payload_crate_path).spawn()?.wait()?; + assert!( + exit_code.success(), + "Failed to build helper crate {} for target {}", + crate_name, + target + ); } - let exit_code = command.current_dir(&payload_crate_path).spawn()?.wait()?; - assert!( - exit_code.success(), - "Failed to build helper crate {} for target {}", - crate_name, - target.unwrap_or("default") - ); let mut payload_artifact_path = payload_crate_path; payload_artifact_path.push("target"); - - if let Some(target) = target { - payload_artifact_path.push(target); - } - + payload_artifact_path.push(target); payload_artifact_path.push(if release { "release" } else { "debug" }); payload_artifact_path.push(format!("{crate_name}.{ext}")); - assert!(&payload_artifact_path.exists()); + assert!( + &payload_artifact_path.exists(), + "Artifact doesn't exist! {:?}", + &payload_artifact_path + ); Ok(payload_artifact_path) } +/// Detects cross-rs. +/// +/// Remarks: +/// +/// I wish I could install Rust itself via `Rustup` here, but the Ubuntu image that ships with +/// `cross` doesn't have the right packages to support encryption, thus we can't download toolchains +/// (I tried). And I also didn't have good luck with pre-build step and downloading extra packages. +/// +/// So as a compromise, we build the test binaries outside for testing from Linux. +fn is_cross() -> bool { + var("CROSS_SYSROOT").is_ok() +} + +pub(crate) fn start_suspended_process(path: &Path) -> OwnedProcess { + unsafe { + let mut startup_info: STARTUPINFOW = zeroed(); + let mut process_info: PROCESS_INFORMATION = zeroed(); + startup_info.cb = std::mem::size_of::() as u32; + + let target_path_wide: Vec = OsStr::new(path.to_str().unwrap()) + .encode_wide() + .chain(once(0)) // null terminator + .collect(); + + if CreateProcessW( + target_path_wide.as_ptr(), + null_mut(), // Command line + null_mut(), // Process security attributes + null_mut(), // Thread security attributes + 0, // Inherit handles + CREATE_SUSPENDED, // Creation flags + null_mut(), // Environment + null_mut(), // Current directory + &mut startup_info, + &mut process_info, + ) == 0 + { + panic!("Failed to create suspended process"); + } else { + OwnedProcess::from_handle_unchecked(OwnedHandle::from_raw_handle(process_info.hProcess)) + } + } +} + #[macro_export] macro_rules! syringe_test { (fn $test_name:ident ($process:ident : OwnedProcess, $payload_path:ident : &Path $(,)?) $body:block) => { @@ -98,8 +142,8 @@ macro_rules! syringe_test { use dll_syringe::process::OwnedProcess; use std::{ path::Path, - process::{Command, Stdio}, }; + use $crate::common::start_suspended_process; #[test] #[cfg(any( @@ -126,21 +170,59 @@ macro_rules! syringe_test { payload_path: impl AsRef, target_path: impl AsRef, ) { - let dummy_process: OwnedProcess = Command::new(target_path.as_ref()) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn().unwrap() - .into(); + let proc = start_suspended_process(target_path.as_ref()); + let _guard = proc.try_clone().unwrap().kill_on_drop(); + test(proc, payload_path.as_ref()); + } - let _guard = dummy_process.try_clone().unwrap().kill_on_drop(); + fn test( + $process : OwnedProcess, + $payload_path : &Path, + ) $body + } + }; +} + +#[macro_export] +macro_rules! suspended_process_test { + (fn $test_name:ident ($process:ident : OwnedProcess $(,)?) $body:block) => { + mod $test_name { + use super::*; + use dll_syringe::process::OwnedProcess; + use std::{ + path::Path + }; + use $crate::common::start_suspended_process; - test(dummy_process, payload_path.as_ref()) + #[test] + #[cfg(any( + target_arch = "x86", + all(target_arch = "x86_64", feature = "into-x86-from-x64") + ))] + fn x86() { + test_with_setup( + common::build_test_target_x86().unwrap(), + ) + } + + #[test] + #[cfg(target_arch = "x86_64")] + fn x86_64() { + test_with_setup( + common::build_test_target_x64().unwrap(), + ) + } + + fn test_with_setup( + target_path: impl AsRef, + ) { + let proc = start_suspended_process(target_path.as_ref()); + let _guard = proc.try_clone().unwrap().kill_on_drop(); + test(proc) } fn test( $process : OwnedProcess, - $payload_path : &Path, ) $body } }; @@ -154,7 +236,8 @@ macro_rules! process_test { use dll_syringe::process::OwnedProcess; use std::{ path::Path, - process::{Command, Stdio}, + process::Command, + process::Stdio, }; #[test] @@ -188,7 +271,6 @@ macro_rules! process_test { .into(); let _guard = dummy_process.try_clone().unwrap().kill_on_drop(); - test(dummy_process) } diff --git a/tests/eject.rs b/tests/eject.rs index e3f8ffd..5e7f3b5 100644 --- a/tests/eject.rs +++ b/tests/eject.rs @@ -10,7 +10,7 @@ syringe_test! { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); syringe.eject(module).unwrap(); } @@ -21,7 +21,7 @@ syringe_test! { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); syringe.process().kill().unwrap(); diff --git a/tests/exports.rs b/tests/exports.rs new file mode 100644 index 0000000..dadab16 --- /dev/null +++ b/tests/exports.rs @@ -0,0 +1,98 @@ +#![cfg(feature = "c-exports")] +#![cfg(feature = "syringe")] + +#[allow(unused)] +mod common; + +use dll_syringe::c_exports::*; +use std::ffi::CString; +use std::os::windows::io::IntoRawHandle; +use winapi::um::processthreadsapi::GetProcessId; + +syringe_test! { + fn test_csyringe_for_process( + process: OwnedProcess, + _payload_path: &Path, + ) { + let handle = process.into_raw_handle(); + let pid = unsafe { GetProcessId(handle) }; + let c_syringe = syringe_for_suspended_process(pid); + assert!(!c_syringe.is_null()); + + unsafe { syringe_free(c_syringe) }; + } +} + +suspended_process_test! { + fn test_csyringe_for_suspended_process( + process: OwnedProcess, + ) { + let handle = process.into_raw_handle(); + let pid = unsafe { GetProcessId(handle) }; + let c_syringe = syringe_for_suspended_process(pid); + assert!(!c_syringe.is_null()); + + unsafe { syringe_free(c_syringe) }; + } +} + +syringe_test! { + fn test_csyringe_inject( + process: OwnedProcess, + payload_path: &Path, + ) { + let handle = process.into_raw_handle(); + let pid = unsafe { GetProcessId(handle) }; + let c_syringe = syringe_for_suspended_process(pid); + assert!(!c_syringe.is_null()); + + let dll_path = CString::new(payload_path.to_str().unwrap()).unwrap(); + let injected = unsafe { syringe_inject(c_syringe, dll_path.as_ptr()) }; + assert!(injected); + + unsafe { syringe_free(c_syringe) }; + } +} + +syringe_test! { + fn test_csyringe_find_or_inject( + process: OwnedProcess, + payload_path: &Path, + ) { + let handle = process.into_raw_handle(); + let pid = unsafe { GetProcessId(handle) }; + let c_syringe = syringe_for_suspended_process(pid); + assert!(!c_syringe.is_null()); + + let dll_path = CString::new(payload_path.to_str().unwrap()).unwrap(); + let c_module = unsafe { syringe_find_or_inject(c_syringe, dll_path.as_ptr()) }; + assert!(!c_module.is_null()); + + unsafe { + syringe_module_free(c_module); + syringe_free(c_syringe); + } + } +} + +syringe_test! { + fn test_csyringe_eject( + process: OwnedProcess, + payload_path: &Path, + ) { + let handle = process.into_raw_handle(); + let pid = unsafe { GetProcessId(handle) }; + let c_syringe = syringe_for_suspended_process(pid); + assert!(!c_syringe.is_null()); + + let dll_path = CString::new(payload_path.to_str().unwrap()).unwrap(); + let c_module = unsafe { syringe_find_or_inject(c_syringe, dll_path.as_ptr()) }; + assert!(!c_module.is_null()); + + let ejected = unsafe { syringe_eject(c_syringe, c_module) }; + assert!(ejected); + + unsafe { syringe_module_free(c_module); } + unsafe { syringe_free(c_syringe) }; + } +} diff --git a/tests/inject.rs b/tests/inject.rs index 3ad6b69..86db93b 100644 --- a/tests/inject.rs +++ b/tests/inject.rs @@ -10,16 +10,16 @@ syringe_test! { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); syringe.inject(payload_path).unwrap(); } } -process_test! { +suspended_process_test! { fn inject_with_invalid_path_fails_with_remote_io( process: OwnedProcess, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let result = syringe.inject("invalid path"); assert!(result.is_err()); let err = result.unwrap_err(); @@ -37,7 +37,7 @@ syringe_test! { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); syringe.process().kill().unwrap(); let result = syringe.inject(payload_path); diff --git a/tests/module.rs b/tests/module.rs index a424263..190a0e1 100644 --- a/tests/module.rs +++ b/tests/module.rs @@ -33,7 +33,7 @@ syringe_test! { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); assert!(module.guess_is_loaded()); } @@ -45,7 +45,7 @@ syringe_test! { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); syringe.eject(module).unwrap(); assert!(!module.try_guess_is_loaded().unwrap()); @@ -58,7 +58,7 @@ syringe_test! { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); syringe.process().kill().unwrap(); assert!(!module.try_guess_is_loaded().unwrap()); diff --git a/tests/process.rs b/tests/process.rs index a190905..abf7858 100644 --- a/tests/process.rs +++ b/tests/process.rs @@ -1,5 +1,10 @@ +use core::mem::{size_of, zeroed}; use dll_syringe::process::{BorrowedProcess, OwnedProcess, Process}; -use std::{fs, mem, time::Duration}; +use std::{ffi::CString, fs, mem, process::Command, time::Duration}; +use winapi::um::{ + libloaderapi::{GetProcAddress, LoadLibraryA}, + winnt::OSVERSIONINFOW, +}; #[allow(unused)] mod common; @@ -28,7 +33,7 @@ process_test! { } } -process_test! { +suspended_process_test! { fn list_module_handles_on_crashed_does_not_hang( process: OwnedProcess ) { @@ -38,7 +43,7 @@ process_test! { } } -process_test! { +suspended_process_test! { fn is_alive_is_true_for_running( process: OwnedProcess ) { @@ -48,7 +53,7 @@ process_test! { } } -process_test! { +suspended_process_test! { fn is_alive_is_false_for_killed( process: OwnedProcess ) { @@ -76,7 +81,7 @@ process_test! { } } -process_test! { +suspended_process_test! { fn kill_guard_kills_process_on_drop( process: OwnedProcess ) { @@ -87,10 +92,15 @@ process_test! { } } -process_test! { +suspended_process_test! { fn long_process_paths_are_supported( process: OwnedProcess ) { + if is_running_under_wine() || is_older_than_windows_10() { + println!("Test skipped due to running under an environment with unsupported long paths. (Wine or older than Windows 10)."); + return; + } + let process_path = process.path().unwrap(); process.kill().unwrap(); @@ -138,3 +148,32 @@ fn current_pseudo_process_eq_current_process() { assert_eq!(pseudo.try_to_owned().unwrap(), normal); assert_eq!(pseudo, normal.try_clone().unwrap()); } + +fn is_running_under_wine() -> bool { + unsafe { + let ntdll = CString::new("ntdll.dll").unwrap(); + let lib = LoadLibraryA(ntdll.as_ptr()); + if !lib.is_null() { + let func_name = CString::new("wine_get_version").unwrap(); + let func = GetProcAddress(lib, func_name.as_ptr()); + !func.is_null() + } else { + false + } + } +} + +// winapi crate doesn't have this. +// This is in ntdll, so already loaded for every Windows process. +extern "system" { + fn RtlGetVersion(lpVersionInformation: &mut OSVERSIONINFOW) -> u32; +} + +fn is_older_than_windows_10() -> bool { + unsafe { + let mut os_info: OSVERSIONINFOW = zeroed(); + os_info.dwOSVersionInfoSize = size_of::() as u32; + RtlGetVersion(&mut os_info); + os_info.dwMajorVersion < 10 + } +} diff --git a/tests/rpc.rs b/tests/rpc.rs index d96b026..f7dd569 100644 --- a/tests/rpc.rs +++ b/tests/rpc.rs @@ -9,11 +9,11 @@ mod core { pub use super::*; use std::time::Duration; - process_test! { + suspended_process_test! { fn get_procedure_address_of_win32_fn( process: OwnedProcess, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.process().wait_for_module_by_name("kernel32.dll", Duration::from_secs(1)).unwrap().unwrap(); let open_process = syringe.get_procedure_address( @@ -29,7 +29,7 @@ mod core { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let dll_main = syringe.get_procedure_address(module, "DllMain").unwrap(); @@ -37,11 +37,11 @@ mod core { } } - process_test! { + suspended_process_test! { fn get_procedure_address_of_invalid( process: OwnedProcess, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.process().wait_for_module_by_name("kernel32.dll", Duration::from_secs(1)).unwrap().unwrap(); let invalid = syringe.get_procedure_address(module, "ProcedureThatDoesNotExist").unwrap(); assert!(invalid.is_none()); @@ -59,7 +59,7 @@ mod payload { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_add = unsafe { syringe.get_payload_procedure:: u32>(module, "add") }.unwrap().unwrap(); @@ -73,7 +73,7 @@ mod payload { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_sum = unsafe { syringe.get_payload_procedure::) -> u64>(module, "sum") }.unwrap().unwrap(); @@ -87,7 +87,7 @@ mod payload { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_does_panic = unsafe { syringe.get_payload_procedure::(module, "does_panic") }.unwrap().unwrap(); @@ -114,7 +114,7 @@ mod raw { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_add = unsafe { syringe.get_raw_procedure:: u32>(module, "add_raw") }.unwrap().unwrap(); @@ -128,7 +128,7 @@ mod raw { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_sub = unsafe { syringe.get_raw_procedure:: u32>(module, "sub_raw") }.unwrap().unwrap(); @@ -142,7 +142,7 @@ mod raw { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_add = unsafe { syringe.get_raw_procedure:: u16>(module, "add_smol_raw") }.unwrap().unwrap(); @@ -156,7 +156,7 @@ mod raw { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_sum = unsafe { syringe.get_raw_procedure:: u32>(module, "sum_5_raw") }.unwrap().unwrap(); @@ -170,7 +170,7 @@ mod raw { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_sum = unsafe { syringe.get_raw_procedure:: u32>(module, "sum_10_raw") }.unwrap().unwrap(); @@ -184,7 +184,7 @@ mod raw { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_sub = unsafe { syringe.get_raw_procedure:: f32>(module, "sub_float_raw") }.unwrap().unwrap(); @@ -198,7 +198,7 @@ mod raw { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_add = unsafe { syringe.get_raw_procedure:: u32>(module, "add_raw_c") }.unwrap().unwrap(); @@ -212,7 +212,7 @@ mod raw { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_sum = unsafe { syringe.get_raw_procedure:: u32>(module, "sum_10_raw_c") }.unwrap().unwrap(); @@ -226,7 +226,7 @@ mod raw { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_add = unsafe { syringe.get_raw_procedure:: u32>(module, "add_raw_c") }.unwrap().unwrap(); syringe.eject(module).unwrap(); @@ -240,7 +240,7 @@ mod raw { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_add = unsafe { syringe.get_raw_procedure:: u32>(module, "add_raw_c") }.unwrap().unwrap(); syringe.process().kill().unwrap(); @@ -254,7 +254,7 @@ mod raw { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_add = unsafe { syringe.get_raw_procedure::(module, "crash") }.unwrap().unwrap(); let add_err = remote_add.call().unwrap_err();