diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2472c6a1..7dbbf974 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,7 +5,6 @@ updates: # Rust dependencies - package-ecosystem: "cargo" directory: "/" - target-branch: "develop" schedule: interval: "weekly" day: "monday" @@ -32,7 +31,6 @@ updates: # Fuzzing dependencies - package-ecosystem: "cargo" directory: "/fuzz" - target-branch: "develop" schedule: interval: "weekly" day: "monday" @@ -47,7 +45,6 @@ updates: # GitHub Actions - package-ecosystem: "github-actions" directory: "/" - target-branch: "develop" schedule: interval: "weekly" day: "monday" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0accdca6..038c1656 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ master, develop ] + branches: [ master ] pull_request: - branches: [ master, develop ] + branches: [ master ] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f0124d47..f80de51a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,26 +1,60 @@ name: Release and Publish +# Publishing is driven by publishing a GitHub release, not by pushing to a +# branch. Create the release and its tag when you intend to ship; until then +# nothing reaches crates.io, so merging to any branch is always safe. on: - push: - branches: [ main, master ] - paths-ignore: - - '**.md' - - 'docs/**' - - '.github/**' - - '!.github/workflows/release.yml' + release: + types: [published] env: CARGO_TERM_COLOR: always jobs: - # First ensure all tests pass + # Derive the version from the release tag and hold it against Cargo.toml, so a + # mistyped tag fails here rather than shipping under the wrong version. + version: + name: Resolve Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + ref: ${{ github.event.release.tag_name }} + + - name: Resolve and verify version + id: version + run: | + TAG="${{ github.event.release.tag_name }}" + VERSION="${TAG#v}" + FAILED=0 + for MANIFEST in dotscope/Cargo.toml dotscope-cli/Cargo.toml; do + DECLARED=$(grep '^version = ' "$MANIFEST" | head -1 | cut -d '"' -f 2) + if [ "$VERSION" != "$DECLARED" ]; then + echo "::error::Release tag ${TAG} implies version ${VERSION}, but ${MANIFEST} declares ${DECLARED}" + FAILED=1 + fi + done + if [ "$FAILED" -ne 0 ]; then + exit 1 + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Releasing v${VERSION}" + + # Then ensure all tests pass test: name: Pre-Release Tests runs-on: ubuntu-latest + needs: version steps: - name: Checkout code uses: actions/checkout@v7 + with: + ref: ${{ github.event.release.tag_name }} - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable @@ -57,111 +91,11 @@ jobs: - name: Check documentation run: cargo doc --no-deps - # Create GitHub release - create-release: - name: Create Release - runs-on: ubuntu-latest - needs: test - outputs: - version: ${{ steps.version.outputs.version }} - release_created: ${{ steps.check_release.outputs.created }} - - steps: - - name: Checkout code - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Get version from Cargo.toml - id: version - run: | - VERSION=$(grep '^version = ' dotscope/Cargo.toml | head -1 | cut -d '"' -f 2) - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Version: $VERSION" - - - name: Check if release exists - id: check_release - run: | - if gh release view "v${{ steps.version.outputs.version }}" >/dev/null 2>&1; then - echo "created=false" >> $GITHUB_OUTPUT - echo "Release v${{ steps.version.outputs.version }} already exists, skipping" - else - echo "created=true" >> $GITHUB_OUTPUT - fi - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Generate changelog - id: changelog - if: steps.check_release.outputs.created == 'true' - run: | - LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") - - if [ -z "$LATEST_TAG" ]; then - CHANGELOG=$(git log --pretty=format:"- %s (%h)" --no-merges) - else - CHANGELOG=$(git log ${LATEST_TAG}..HEAD --pretty=format:"- %s (%h)" --no-merges) - fi - - if [ -z "$CHANGELOG" ]; then - CHANGELOG="- Initial release" - fi - - echo "CHANGELOG<> $GITHUB_OUTPUT - echo "$CHANGELOG" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - - name: Create Release - if: steps.check_release.outputs.created == 'true' - run: | - VERSION="v${{ steps.version.outputs.version }}" - cat < release_body.md - ## Changes in ${VERSION} - - ${{ steps.changelog.outputs.CHANGELOG }} - - ## Installation - - ### Library - - Add this to your \`Cargo.toml\`: - - \`\`\`toml - [dependencies] - dotscope = "${{ steps.version.outputs.version }}" - \`\`\` - - Or install via cargo: - - \`\`\`bash - cargo add dotscope - \`\`\` - - ### CLI Tool - - Download the pre-built binary for your platform from the assets below and extract it. - - | Platform | Asset | - |----------|-------| - | Linux (x86_64) | \`dotscope-${VERSION}-x86_64-unknown-linux-gnu.zip\` | - | macOS (Apple Silicon) | \`dotscope-${VERSION}-aarch64-apple-darwin.zip\` | - | Windows (x86_64) | \`dotscope-${VERSION}-x86_64-pc-windows-msvc.zip\` | - - > **Note:** Z3 is an optional compile-time dependency used only for the \`z3\` feature (CFF reconstruction). The pre-built CLI binaries do **not** require Z3 at runtime. - BODY - - gh release create "$VERSION" \ - --title "Release ${VERSION}" \ - --notes-file release_body.md - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # Build CLI binaries for all platforms + # Build CLI binaries for all platforms and attach them to the release build-cli: name: Build CLI (${{ matrix.name }}) runs-on: ${{ matrix.os }} - needs: create-release - if: needs.create-release.outputs.release_created == 'true' + needs: [version, test] strategy: fail-fast: false matrix: @@ -182,6 +116,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v7 + with: + ref: ${{ github.event.release.tag_name }} - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable @@ -200,18 +136,18 @@ jobs: if: runner.os != 'Windows' run: | cd target/${{ matrix.target }}/release - zip dotscope-v${{ needs.create-release.outputs.version }}-${{ matrix.target }}.zip ${{ matrix.artifact }} - mv dotscope-v${{ needs.create-release.outputs.version }}-${{ matrix.target }}.zip ${{ github.workspace }}/ + zip dotscope-v${{ needs.version.outputs.version }}-${{ matrix.target }}.zip ${{ matrix.artifact }} + mv dotscope-v${{ needs.version.outputs.version }}-${{ matrix.target }}.zip ${{ github.workspace }}/ - name: Package binary (Windows) if: runner.os == 'Windows' run: | cd target/${{ matrix.target }}/release - Compress-Archive -Path ${{ matrix.artifact }} -DestinationPath "${{ github.workspace }}\dotscope-v${{ needs.create-release.outputs.version }}-${{ matrix.target }}.zip" + Compress-Archive -Path ${{ matrix.artifact }} -DestinationPath "${{ github.workspace }}\dotscope-v${{ needs.version.outputs.version }}-${{ matrix.target }}.zip" shell: pwsh - name: Upload release asset - run: gh release upload "v${{ needs.create-release.outputs.version }}" "dotscope-v${{ needs.create-release.outputs.version }}-${{ matrix.target }}.zip" --clobber + run: gh release upload "${{ github.event.release.tag_name }}" "dotscope-v${{ needs.version.outputs.version }}-${{ matrix.target }}.zip" --clobber env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -219,12 +155,13 @@ jobs: publish: name: Publish to crates.io runs-on: ubuntu-latest - needs: [test, create-release] - if: needs.create-release.outputs.release_created == 'true' + needs: [version, test] steps: - name: Checkout code uses: actions/checkout@v7 + with: + ref: ${{ github.event.release.tag_name }} - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable @@ -244,12 +181,13 @@ jobs: verify-docs: name: Verify Documentation runs-on: ubuntu-latest - needs: [test, create-release] - if: needs.create-release.outputs.release_created == 'true' + needs: [version, test] steps: - name: Checkout code uses: actions/checkout@v7 + with: + ref: ${{ github.event.release.tag_name }} - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dfc8c804..b6ba52cc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -94,4 +94,3 @@ pages: - public only: - master - - develop diff --git a/CHANGELOG.md b/CHANGELOG.md index ef4288d7..0da0c059 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,46 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.8.4] - 2026-07-26 + +### Added + +- **Memory optimization pass** (`compiler::MemoryOptimizationPass`): store-to-load forwarding, redundant load elimination, and block-local dead store elimination, every rewrite gated on a Memory SSA alias proof. Registered in the deobfuscation pipeline's normalize phase and enabled by default; disable with `PassConfig::memory_optimization = false`. This reaches the field and array traffic obfuscators use to keep values out of SSA registers, which the register-level passes cannot see through +- **Field-sensitive points-to for CIL** (`CilTarget::field_member_index`): the field's metadata token supplies the stable per-field cell identity Andersen's analysis keys on, so `&o.a` and `&o.b` no longer alias. An unresolved (null) field token falls back to the sound whole-object approximation +- **x86 segment overrides reach the IR**: `X86Memory` gained a `segment` field, decoded from the instruction's explicit prefix, and `fs:`/`gs:`-qualified accesses now lower to `LoadIndirect`/`StoreIndirect` with a distinct `address_space` (257/256, following LLVM's numbering). Alias analysis treats the spaces as disjoint, so TEB/PEB and stack-cookie accesses stop colliding with flat memory at the same displacement. `cs:`/`ds:`/`es:`/`ss:` deliberately stay in the flat default — in flat user mode they share a base, and marking them would let alias analysis prove two names for one cell disjoint +- **Re-exports for analyssa's new alias machinery**: `analysis::{pointsto, address}` modules plus `MemorySsa`, `IndirectLocation`, `ArrayIndex`, `AliasResult`, `MemoryDefSite`, `MemoryPhiOperand`, and `MemorySsaStats` +- **Cross-block value promotion before CFF restructuring** (`deobfuscation::passes::unflattening::spill`): unflattening rewires the CFG, so the SSA has to be reconstructed afterwards — and reconstruction is a reaching-definition problem that can only be solved for values with a storage location. Arguments and locals have one; a stack temporary produced in one block and consumed in another exists only as an SSA name, and the sole record that two names denote the same value is the phi that merges them. Rewiring discards exactly that record, and for an edge the patch *creates* no phi ever described it. Every value crossing a block boundary is now promoted to a local slot before any terminator is touched — the classical "spill temporaries before restructuring" step — so rebuild can recover versions and phi placement for all of them by the ordinary algorithm. This is what makes .NET Reactor NecroBit samples reconstruct at all, and it is correct for any rewiring rather than only the shapes whose phis happen to survive +- **Zero-initialization for undefined SSA definitions** (`SsaConverter`): a new construction phase materializes definitions for variables that had none, following ECMA-335 §I.12.3.2.2 — a typed zero `Const` for primitives and references, and `LoadLocalAddr; InitObj; LoadLocal` for value types. Previously such variables were registered with no defining instruction, so they survived initial construction but vanished the moment the variable table was rebuilt from real definitions +- **Emulation and CFF smoke tests** (`tests/emulation_smoke.rs`): five samples whose deobfuscation depends on emulation or the unflattening tracer producing byte-identical results, each checked for semantic preservation against `original.exe`. The full packer suites take hours; this runs in seconds, so a change to the emulation layer or the tracer is validated against real output rather than only wall-clock time +- **Deobfuscation benchmark** (`benches/deobfuscation.rs`): ConfuserEx and .NET Reactor groups plus a detection-only control, behind `--features deobfuscation` + +### Fixed + +- **Taint-driven neutralization could produce IR with dangling reads**: `SentinelTaintRemovalPass` and `NeutralizationPass` rewrote every tainted instruction to `Nop` and dropped every tainted phi, destroying definitions that surviving code still read. `PhiTaintMode::NoPropagation` makes phis taint barriers by design, so a phi routinely merges a tainted definition into code the analysis never marks. Both passes now shrink the removal set to a fixpoint (`utils::retain_removable`) — a candidate whose result still has a reader is kept rather than the removal widening into legitimate code. `NeutralizationPass` also excludes its protected `DecryptedString` constants from the candidate set rather than at rewrite time, so branch-target selection sees what is actually removed +- **`AssemblyDependencyGraph::find_cycles` reports participants, not a closed walk**: following analyssa's switch to a single deterministic Tarjan pass, a self-dependency now names the assembly once rather than twice. The stale "modified DFS with three-color marking" documentation was corrected to match +- **CIL emulation fetched instructions in O(n)** (`emulation::engine::context::MethodCode`): every executed instruction cloned the method's entire instruction vector and scanned it linearly for the current offset. A 3-million-instruction run performed 3.2 billion `Instruction` clones — about 1052 per step. Method bodies are now cached per token with an offset-to-index map, and the execution loop borrows the instruction instead of cloning it. Synthetic bodies bypass the cache, since `ILGenerator` can mutate them +- **Emulation rebuilt the assembly context on every instruction**: `loaded_assembly_context` constructed a fresh `EmulationContext` and took an `RwLock` per executed instruction; contexts are now memoized per assembly index +- **`optimize_locals` split values it renumbered** (`SsaFunctionCilExt`): renumbering a local moved its origin but left its rename group pointing at the old slot. `rebuild_ssa` groups variables by rename group while resolving argument/local representatives by origin, so the two views disagreed about which names denote the same local and the value ended up with no reaching definition. Both now move together +- **CFF tracer could escape its visit budget**: entering an expression-switch false arm deliberately resets `total_visits` so each arm gets its own budget. On heavily nested methods that reset fired often enough to make the budget unbounded — a 348-block ConfuserEx method reached 3.3 million block visits against a 50,000 cap. A monotonic counter now bounds total work per trace without changing the per-arm semantics +- **CFF reconstruction produced invalid SSA after block cloning**: cloned blocks duplicated every definition they contained, phi operands survived pointing at predecessors the patched CFG no longer has, and operands were left naming redirected blocks. Clones now allocate fresh variable ids (after state-tainted filtering, which keys on the original ids), phi operands are pruned against the patched predecessor sets and definedness, and redirected operands are resolved through a bounded hop limit +- **Cleanup deleted live types reachable only through another deletion candidate** (#249, fix contributed in #248 by [@agski331](https://github.com/agski331)): `find_unreferenced_types` computed a single step of what is a transitive reachability problem. A candidate rescued partway through a pass never propagated that liveness onwards, so any cluster reachable solely through it was still read as isolated infrastructure — on `reactor_virtualization` that cut the assembly from 854 methods to 45, deleting a VM interpreter whose stubs remain live because nothing devirtualizes it. Reachability is now a worklist drain over a type-level call graph, in `O(V+E)` rather than `O(depth × edges)`. CustomAttribute constructor types are seeded as roots before propagation rather than filtered afterwards, so the types those constructors call survive too, and liveness propagates from a nested type to its enclosing type — deleting an enclosing type cascades to its children through `expand_type_tokens`, which was dropping nested types that live code still called. The deletion set is sorted for a reproducible order +- **Technique cleanup ran whether or not the transformation it implies succeeded**: a technique builds its cleanup request from detection findings alone, so it scheduled the decryptor, its infrastructure type, the initializer and the encrypted data for deletion even when no call site was reversed, leaving those call sites pointing at metadata that no longer exists and emptying the methods holding them. Removal now requires that nothing still calls the decryptor — a successful decryption rewrites its call site to the constant, so the absence of remaining callers is the evidence of reversal. Absence of recorded failures is not, since a decryptor that was never exercised has none either. Note that the caller check reads the SSA call graph, which covers only methods that converted successfully +- **ConfuserEx constant decryption missed builds that differ from stock 1.6.0** (#249): four defects, each masking the next. The blob index was matched against `int32` alone, so builds emitting `T Get(uint32)` yielded no decryptor at all — any integer width is now accepted, the surrounding constraints carrying the selectivity. `stobj` rejected reference types, though ECMA-335 §III.4.29 defines it over any `typeTok` and makes it equivalent to `stind.ref` for reference types, which is what `stobj !!T` becomes at `T = string`. `ldelema` hand-rolled its index match to `I32`/`NativeInt` while `ldelem`/`stelem` share a helper accepting every width, so a `native uint` index aborted emulation. And `stfld` through a pointer to a value-type array element replaced the whole element with the field's value instead of updating the field inside it, which is the path LZMA's bit-decoder struct arrays take +- **ConfuserEx LZMA blobs were rejected or misparsed** (#249): the sniffer required the compressed payload to be smaller than its declared output, but LZMA expands small high-entropy input and the constants blob is XOR-encrypted before compression — a 44-byte blob compresses to 51 and was refused. The header was also modelled as 5 property bytes plus a 4-byte size, while builds calling the LZMA SDK's stream API write the standard 13-byte header with an 8-byte size; reading that as 9 bytes shifts the payload and corrupts the range coder. Both layouts are now attempted and held to the size each declares, and the size ceiling is documented as an allocation guard rather than a property of the format + +### Changed + +- **Deobfuscating heavily flattened methods is roughly 25× faster**, through changes that leave what the tracer computes unchanged: + - Forks mark the evaluator instead of copying it, using analyssa 0.4.1's new `checkpoint`/`rollback`. A flattened method forks millions of times and the evaluator's state grows with everything the trace has learned, so copying it per fork was measured at 99% of tracer time. The same journal treatment is applied to the tracer's own visited-state set, whose sole writer only ever inserts + - Per-block structural facts — dispatcher-target and foreign-dispatcher membership, constant-producer targets, and the overflow-dispatch-site predecessor walk — are computed once per trace rather than per block visit + - The cross-scope local bridge indexes variables by local slot instead of scanning the whole variable table inside the per-instruction loop + - `PatchPlan` keeps membership and redirect-target indexes beside its ordered vectors, replacing linear scans that ran once per block of every node in the trace tree + - Trace nodes store their visited blocks inline (`SmallVec`); a 1200-block method produced 92 million nodes averaging about one block each, so the per-node heap allocation dominated the allocator + - The per-node instruction log was removed: it recorded operand values nothing read, and its only consumer needed opcodes that are available from the SSA +- **Dependencies**: bumped `analyssa` (0.3.0 → 0.4.1) and added `smallvec` (1.15) + +Measured end-to-end on the packer samples, deobfuscating the whole assembly: ConfuserEx `maximum` 63.6 s → 2.0 s, and .NET Reactor `necrobit` — which previously failed to reconstruct at all — 385 s → 15.1 s. Part of that comes from analyssa 0.4.1 rather than from dotscope: alongside the evaluator's `checkpoint`/`rollback`, it batches the rebuild-mode substitution in `eliminate_trivial_phis`, which had been applying one whole-function scan per trivial phi. A rebuild produces trivial phis in proportion to the function, so the round was quadratic — on a 1200-block method it was the single largest cost in the pipeline. + ## [0.8.3] - 2026-07-16 ### Changed diff --git a/Cargo.lock b/Cargo.lock index e8dc6fb6..0f25dc92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,16 +99,16 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "analyssa" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf6eaaf643f25047ba272c9bbd3246ca7a75a3f1db908fe98577c325b4ed9ebf" +checksum = "79845a7f52583759a36e031fef550604c1d8d83b66fdeb7e3e0d31677bad144c" dependencies = [ "boxcar", "dashmap", "log", "num_enum", "rayon", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -189,9 +189,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "apodize" @@ -260,13 +260,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -296,7 +296,7 @@ dependencies = [ "num-traits", "pastey", "rayon", - "thiserror 2.0.18", + "thiserror 2.0.19", "v_frame", "y4m", ] @@ -326,9 +326,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.17.1" +version = "1.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" dependencies = [ "aws-lc-sys", "zeroize", @@ -336,9 +336,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.42.0" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" dependencies = [ "cc", "cmake", @@ -518,9 +518,9 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" -version = "1.25.1" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" dependencies = [ "bytemuck_derive", ] @@ -565,7 +565,7 @@ dependencies = [ "cached_proc_macro_types", "hashbrown 0.15.5", "once_cell", - "thiserror 2.0.18", + "thiserror 2.0.19", "web-time", ] @@ -609,7 +609,7 @@ dependencies = [ "rand_distr 0.5.1", "rayon", "safetensors 0.7.0", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokenizers 0.22.2", "yoke 0.8.3", "zip", @@ -626,7 +626,7 @@ dependencies = [ "objc2-foundation", "objc2-metal", "once_cell", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", ] @@ -645,7 +645,7 @@ dependencies = [ "rayon", "safetensors 0.7.0", "serde", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -684,9 +684,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.67" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" dependencies = [ "find-msvc-tools", "jobserver", @@ -784,9 +784,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.2" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011" +checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" dependencies = [ "clap_builder", "clap_derive", @@ -807,14 +807,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -956,12 +956,12 @@ dependencies = [ [[package]] name = "cowfile" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b0c9a99dda5d60063c8daece5d8c7dc1a7b1cd8a3695fb0f4be1df2193ed138" +checksum = "59677f27c443ba67b4ee6af37659eaed4b7a1e68d28c4169167e550f17b8718e" dependencies = [ "memmap2", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -1377,7 +1377,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -1564,7 +1564,7 @@ dependencies = [ [[package]] name = "dotscope" -version = "0.8.3" +version = "0.8.4" dependencies = [ "aes", "analyssa", @@ -1598,9 +1598,10 @@ dependencies = [ "rustc-hash 2.1.3", "sha1 0.11.0", "sha2 0.11.0", + "smallvec 1.15.2", "strum 0.28.0", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "uguid", "widestring", @@ -1609,7 +1610,7 @@ dependencies = [ [[package]] name = "dotscope-cli" -version = "0.8.3" +version = "0.8.4" dependencies = [ "anyhow", "clap", @@ -1844,9 +1845,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "fax" @@ -1963,9 +1964,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" dependencies = [ "futures-channel", "futures-core", @@ -1978,9 +1979,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", "futures-sink", @@ -1988,15 +1989,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -2005,15 +2006,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", @@ -2022,21 +2023,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-channel", "futures-core", @@ -2504,7 +2505,7 @@ dependencies = [ "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "ureq", "windows-sys 0.60.2", @@ -2533,7 +2534,7 @@ checksum = "12d23156ea4dbe6b37ad48fab2da56ff27b0f6192fb5db210c44eb07bfe6e787" dependencies = [ "html5ever 0.38.0", "tendril 0.5.1", - "thiserror 2.0.18", + "thiserror 2.0.19", "unicode-width", ] @@ -2607,9 +2608,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" dependencies = [ "atomic-waker", "bytes", @@ -2639,7 +2640,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.8", + "webpki-roots 1.0.9", ] [[package]] @@ -2845,12 +2846,13 @@ dependencies = [ [[package]] name = "imbl" -version = "7.0.0" +version = "7.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e525189e5f603908d0c6e0d402cb5de9c4b2c8866151fabc4ebd771ed2630a2e" +checksum = "43ea8d4c37ee560727e824d62804183624d371e632019b0e9e3532bce64a33e5" dependencies = [ "archery", "bitmaps", + "equivalent", "imbl-sized-chunks", "rand_core 0.9.5", "rand_xoshiro", @@ -2995,11 +2997,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.32" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "961d16382652bfdd8c6f68b223b26a8c93e0d475c672f414411db31c6c5c900e" +checksum = "e184d09547b80eb7e20d141ba2fb1fbac843ca53f4cf1b31210adc4c1adc6e16" dependencies = [ "defmt", + "jiff-core", "jiff-static", "log", "portable-atomic", @@ -3007,12 +3010,22 @@ dependencies = [ "serde_core", ] +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + [[package]] name = "jiff-static" -version = "0.2.32" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0879bd39df99c4c5e2c6615ccc026391a423dde10532c573e6086eb94a802cc" +checksum = "323da076b7a6faf914dc677cb05a4b907742ff7375c8322c9e7f5061e5e0e9de" dependencies = [ + "jiff-core", "proc-macro2", "quote", "syn 2.0.119", @@ -3030,7 +3043,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror 2.0.18", + "thiserror 2.0.19", "walkdir", "windows-link 0.2.1", ] @@ -3102,9 +3115,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libfuzzer-sys" @@ -3453,7 +3466,7 @@ dependencies = [ "schemars 1.2.1", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", "tracing-subscriber", @@ -3553,7 +3566,7 @@ dependencies = [ "strum 0.27.2", "symphonia", "sysinfo", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokenizers 0.21.4", "tokio", "tokio-rayon", @@ -3617,7 +3630,7 @@ dependencies = [ "half", "objc2-foundation", "objc2-metal", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -3644,7 +3657,7 @@ dependencies = [ "safetensors 0.7.0", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", "yoke 0.8.3", @@ -4067,7 +4080,7 @@ dependencies = [ "serde_with", "sha1 0.10.7", "sha2 0.10.9", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -4299,9 +4312,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" [[package]] name = "portable-atomic-util" @@ -4362,9 +4375,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -4482,7 +4495,7 @@ dependencies = [ "rustc-hash 2.1.3", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", "web-time", @@ -4505,7 +4518,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror 2.0.19", "tinyvec", "tracing", "web-time", @@ -4527,9 +4540,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -4709,7 +4722,7 @@ dependencies = [ "rand 0.9.5", "rand_chacha 0.9.0", "simd_helpers", - "thiserror 2.0.18", + "thiserror 2.0.19", "v_frame", "wasm-bindgen", ] @@ -4813,27 +4826,27 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] name = "ref-cast" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -4908,7 +4921,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams 0.4.2", "web-sys", - "webpki-roots 1.0.8", + "webpki-roots 1.0.9", ] [[package]] @@ -5191,7 +5204,7 @@ checksum = "5e1aee7486406df3541b5a657204a11be97175a467d77bc98e6d94a66289fb80" dependencies = [ "arraydeque", "smallvec 2.0.0-alpha.12", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -5337,9 +5350,9 @@ checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -5376,22 +5389,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -5407,9 +5420,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "indexmap 2.14.0", "itoa", @@ -5972,6 +5985,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -6095,11 +6119,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.19", ] [[package]] @@ -6115,13 +6139,13 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -6149,9 +6173,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.53" +version = "0.3.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" dependencies = [ "deranged", "num-conv", @@ -6169,9 +6193,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -6239,7 +6263,7 @@ dependencies = [ "serde", "serde_json", "spm_precompiled", - "thiserror 2.0.18", + "thiserror 2.0.19", "unicode-normalization-alignments", "unicode-segmentation", "unicode_categories", @@ -6272,7 +6296,7 @@ dependencies = [ "serde", "serde_json", "spm_precompiled", - "thiserror 2.0.18", + "thiserror 2.0.19", "unicode-normalization-alignments", "unicode-segmentation", "unicode_categories", @@ -6280,9 +6304,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.52.4" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317fafbbe3f02fc663dad00ea6186197de963cd4190e86a26d8d0fae095539af" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -6297,9 +6321,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" dependencies = [ "proc-macro2", "quote", @@ -6340,13 +6364,14 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-sink", + "libc", "pin-project-lite", "tokio", ] @@ -6584,7 +6609,7 @@ dependencies = [ "log", "rand 0.9.5", "sha1 0.10.7", - "thiserror 2.0.18", + "thiserror 2.0.19", "utf-8", ] @@ -6992,9 +7017,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" dependencies = [ "rustls-pki-types", ] @@ -7005,14 +7030,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.8", + "webpki-roots 1.0.9", ] [[package]] name = "webpki-roots" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] @@ -7567,18 +7592,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.54" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.54" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" dependencies = [ "proc-macro2", "quote", diff --git a/README.md b/README.md index 017a140e..8c950092 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Add `dotscope` to your `Cargo.toml`: ```toml [dependencies] -dotscope = "0.8.2" +dotscope = "0.8.4" ``` ### Raw Access Example diff --git a/dotscope-cli/Cargo.toml b/dotscope-cli/Cargo.toml index de10eaea..830c3ddb 100644 --- a/dotscope-cli/Cargo.toml +++ b/dotscope-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dotscope-cli" -version = "0.8.3" +version = "0.8.4" authors = ["Johann Kempter "] edition.workspace = true license.workspace = true @@ -18,10 +18,10 @@ smart-rename = ["dotscope/smart-rename"] [dependencies] dotscope = { path = "../dotscope", features = ["deobfuscation"] } -clap = { version = "4.6.2", features = ["derive", "env", "wrap_help"] } -serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.150" -anyhow = "1.0.103" +clap = { version = "4.6.4", features = ["derive", "env", "wrap_help"] } +serde = { version = "1.0.229", features = ["derive"] } +serde_json = "1.0.151" +anyhow = "1.0.104" comfy-table = "7.2.2" widestring = "1.2.1" ctrlc = "3.5.2" diff --git a/dotscope-cli/src/commands/heaps.rs b/dotscope-cli/src/commands/heaps.rs index ae895134..ee8f88c4 100644 --- a/dotscope-cli/src/commands/heaps.rs +++ b/dotscope-cli/src/commands/heaps.rs @@ -1,5 +1,4 @@ -use std::fmt::Write; -use std::path::Path; +use std::{fmt::Write, path::Path}; use anyhow::{bail, Context}; use dotscope::CilObject; diff --git a/dotscope-cli/src/commands/info.rs b/dotscope-cli/src/commands/info.rs index b45f9658..bb93fa3e 100644 --- a/dotscope-cli/src/commands/info.rs +++ b/dotscope-cli/src/commands/info.rs @@ -1,5 +1,4 @@ -use std::fmt::Write; -use std::path::Path; +use std::{fmt::Write, path::Path}; use serde::Serialize; diff --git a/dotscope/Cargo.toml b/dotscope/Cargo.toml index c3164dc4..50d063e7 100644 --- a/dotscope/Cargo.toml +++ b/dotscope/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dotscope" -version = "0.8.3" +version = "0.8.4" authors = ["Johann Kempter "] edition.workspace = true description = "A high-performance, cross-platform framework for analyzing and reverse engineering .NET PE executables" @@ -40,11 +40,11 @@ arithmetic_side_effects = "deny" indexing_slicing = "deny" [dependencies] -thiserror = "2.0.18" +thiserror = "2.0.19" uguid = "2.2.1" widestring = "1.2.1" strum = { version = "0.28.0", features = ["derive"]} -cowfile = "0.2.2" +cowfile = "0.2.3" memmap2 = "0.9.11" tempfile = "3.27.0" goblin = { version = "0.10.7", default-features = false, features = ["pe32", "pe64", "std"] } @@ -53,7 +53,7 @@ sha1 = { version = "0.11.0", optional = true, features = ["oid"] } sha2 = { version = "0.11.0", features = ["oid"] } md-5 = { version = "0.11.0", optional = true } hmac = "0.13.0" -imbl = { version = "7.0.0", optional = true } +imbl = { version = "7.0.1", optional = true } pbkdf2 = "0.13.0" aes = "0.9.1" des = { version = "0.9.0", optional = true } @@ -63,17 +63,18 @@ dashmap = "6.2.1" crossbeam-skiplist = "0.1.3" rayon = "1.12.0" rustc-hash = "2.1.3" +smallvec = "1.15" boxcar = "0.2.14" quick-xml = "0.41.0" hex = "0.4.3" num-bigint = { version = "0.5.1", optional = true } log = "0.4.33" flate2 = "1.1.9" -analyssa = "0.3.0" +analyssa = "0.4.1" lzma-rs = "0.3.0" z3 = { version = "0.20.2", optional = true } iced-x86 = { version = "1.21.0", default-features = false, features = ["std", "decoder", "instr_info"], optional = true } -tokio = { version = "1.52.4", optional = true, features = ["rt-multi-thread"] } +tokio = { version = "1.53.1", optional = true, features = ["rt-multi-thread"] } # Metal GPU acceleration for LLM inference on macOS (Apple Silicon / AMD GPU). [target.'cfg(target_os = "macos")'.dependencies] @@ -86,7 +87,7 @@ mistralrs = { version = "0.8.1", optional = true } [dev-dependencies] criterion = "0.8.2" env_logger = "0.11.11" -clap = { version = "4.6.2", features = ["derive"] } +clap = { version = "4.6.4", features = ["derive"] } ctrlc = "3.5.2" [features] @@ -175,3 +176,8 @@ harness = false [[bench]] name = "security" harness = false + +[[bench]] +name = "deobfuscation" +harness = false +required-features = ["deobfuscation"] diff --git a/dotscope/benches/assembly.rs b/dotscope/benches/assembly.rs index bc415802..8b7b6422 100644 --- a/dotscope/benches/assembly.rs +++ b/dotscope/benches/assembly.rs @@ -6,12 +6,15 @@ extern crate dotscope; -use criterion::{criterion_group, criterion_main, Criterion}; -use dotscope::assembly::{decode_stream, InstructionAssembler, InstructionEncoder}; -use dotscope::metadata::token::Token; -use dotscope::Result; use std::hint::black_box; +use criterion::{criterion_group, criterion_main, Criterion}; +use dotscope::{ + assembly::{decode_stream, InstructionAssembler, InstructionEncoder}, + metadata::token::Token, + Result, +}; + fn assemble_simple() -> Result<( Vec, u16, diff --git a/dotscope/benches/cilassemblyview.rs b/dotscope/benches/cilassemblyview.rs index a25e52c0..deb03ee3 100644 --- a/dotscope/benches/cilassemblyview.rs +++ b/dotscope/benches/cilassemblyview.rs @@ -2,9 +2,10 @@ extern crate dotscope; +use std::path::PathBuf; + use criterion::{criterion_group, criterion_main, Criterion}; use dotscope::{CilAssemblyView, ValidationConfig}; -use std::path::PathBuf; /// Benchmark loading a `CilAssemblyView` with and without metadata validation. pub fn criterion_benchmark(c: &mut Criterion) { diff --git a/dotscope/benches/cilobject.rs b/dotscope/benches/cilobject.rs index 3aae6236..0436aea7 100644 --- a/dotscope/benches/cilobject.rs +++ b/dotscope/benches/cilobject.rs @@ -2,9 +2,10 @@ extern crate dotscope; +use std::path::PathBuf; + use criterion::{criterion_group, criterion_main, Criterion}; use dotscope::{metadata::cilobject::CilObject, ValidationConfig}; -use std::path::PathBuf; /// Benchmark loading a `CilObject` with and without metadata validation. pub fn criterion_benchmark(c: &mut Criterion) { diff --git a/dotscope/benches/cor20.rs b/dotscope/benches/cor20.rs index 338f1c2f..a3f6a295 100644 --- a/dotscope/benches/cor20.rs +++ b/dotscope/benches/cor20.rs @@ -6,9 +6,10 @@ extern crate dotscope; +use std::{fs, hint::black_box, path::PathBuf}; + use criterion::{criterion_group, criterion_main, Criterion, Throughput}; use dotscope::metadata::cor20header::Cor20Header; -use std::{fs, hint::black_box, path::PathBuf}; /// Benchmark parsing the CLI header from a real assembly. /// diff --git a/dotscope/benches/deobfuscation.rs b/dotscope/benches/deobfuscation.rs new file mode 100644 index 00000000..b518d9eb --- /dev/null +++ b/dotscope/benches/deobfuscation.rs @@ -0,0 +1,173 @@ +//! End-to-end deobfuscation benchmarks. +//! +//! Measures the full deobfuscation pipeline (detection -> emulation-driven +//! decryption -> SSA passes -> cleanup -> rewrite) on protected samples. +//! +//! The ConfuserEx samples are the primary workload: they exercise the CIL +//! emulation layer heavily via constant/string decryptor execution and +//! anti-tamper unpacking, which makes this the best proxy for emulation +//! throughput in a realistic pipeline. +//! +//! Run with: +//! ```sh +//! cargo bench --bench deobfuscation --features deobfuscation +//! ``` + +#![cfg(feature = "deobfuscation")] +#![allow(clippy::unwrap_used, clippy::expect_used, missing_docs)] + +use std::{hint::black_box, path::PathBuf, time::Duration}; + +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use dotscope::deobfuscation::{DeobfuscationEngine, EngineConfig}; + +const CONFUSEREX_DIR: &str = "tests/samples/packers/confuserex/1.6.0"; +const NETREACTOR_DIR: &str = "tests/samples/packers/netreactor/7.5.0"; + +/// NetReactor samples, which lean much harder on the CIL emulation layer than +/// ConfuserEx does — NecroBit method-body decryption and string decryption both +/// run large amounts of emulated CIL. +const NETREACTOR_SAMPLES: &[(&str, &str)] = &[ + // String encryption only — emulation-dominated. + ("reactor_strings.exe", "strings"), + // NecroBit: encrypted method bodies recovered via emulation. + ("reactor_necrobit.exe", "necrobit"), + // NecroBit + strings + control flow: the heaviest emulation workload. + ("reactor_necrobit_strings_cff.exe", "necrobit_strings_cff"), + // Everything on. + ("reactor_full.exe", "full"), +]; + +/// Samples chosen to isolate individual protection costs plus the combined worst case. +/// +/// Each entry is `(filename, label)`. The protections named in the filename map +/// directly onto pipeline stages, so a regression in one stage shows up as a +/// regression in the samples that use it. +const CONFUSEREX_SAMPLES: &[(&str, &str)] = &[ + // Baseline: marker attributes only. Measures fixed pipeline overhead + // (load, detect, rewrite) with essentially no real work. + ("mkaring_minimal.exe", "minimal"), + // Constant/string decryption — the heaviest pure-emulation workload. + ("mkaring_constants.exe", "constants"), + // Control-flow flattening — the heaviest pure-SSA workload. + ("mkaring_controlflow.exe", "controlflow"), + // Anti-tamper — emulation-driven method body decryption. + ("mkaring_antitamper.exe", "antitamper"), + // Combined constants + CFG + control flow. + ("mkaring_constants_cfg_controlflow.exe", "constants_cfg_cf"), + // Everything on. The realistic worst case and the headline number. + ("mkaring_maximum.exe", "maximum"), +]; + +/// Resolves a sample path relative to the crate root. +fn sample_path(dir: &str, filename: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join(dir) + .join(filename) +} + +/// Benchmarks the full `process_file` pipeline across ConfuserEx samples. +fn bench_confuserex(c: &mut Criterion) { + let mut group = c.benchmark_group("confuserex_deobfuscation"); + + // Deobfuscation runs are measured in seconds, not microseconds. Keep the + // sample count low enough that the suite finishes in reasonable time while + // still giving criterion enough data for a stable estimate. + group.sample_size(10); + group.measurement_time(Duration::from_secs(30)); + group.warm_up_time(Duration::from_secs(3)); + + for (filename, label) in CONFUSEREX_SAMPLES { + let path = sample_path(CONFUSEREX_DIR, filename); + if !path.exists() { + eprintln!("skipping missing sample: {}", path.display()); + continue; + } + + group.bench_with_input(BenchmarkId::from_parameter(label), &path, |b, path| { + b.iter(|| { + let engine = DeobfuscationEngine::new(EngineConfig::default()); + // Errors are expected on some protected samples depending on + // pipeline state; we measure the work, not the verdict. + let result = engine.process_file(black_box(path)); + black_box(result.is_ok()) + }); + }); + } + + group.finish(); +} + +/// Benchmarks the full `process_file` pipeline across NetReactor samples. +fn bench_netreactor(c: &mut Criterion) { + let mut group = c.benchmark_group("netreactor_deobfuscation"); + + group.sample_size(10); + group.measurement_time(Duration::from_secs(30)); + group.warm_up_time(Duration::from_secs(3)); + + for (filename, label) in NETREACTOR_SAMPLES { + let path = sample_path(NETREACTOR_DIR, filename); + if !path.exists() { + eprintln!("skipping missing sample: {}", path.display()); + continue; + } + + group.bench_with_input(BenchmarkId::from_parameter(label), &path, |b, path| { + b.iter(|| { + let engine = DeobfuscationEngine::new(EngineConfig::default()); + let result = engine.process_file(black_box(path)); + black_box(result.is_ok()) + }); + }); + } + + group.finish(); +} + +/// Benchmarks detection only, to separate detection cost from transformation cost. +/// +/// Detection walks every method and runs the SSA-based pattern matchers, so it +/// is a useful control: if `process_file` regresses but this does not, the +/// regression is in the transformation/emulation stages. +fn bench_detection_only(c: &mut Criterion) { + let mut group = c.benchmark_group("confuserex_detection"); + group.sample_size(10); + group.measurement_time(Duration::from_secs(20)); + + for (filename, label) in CONFUSEREX_SAMPLES { + let path = sample_path(CONFUSEREX_DIR, filename); + if !path.exists() { + continue; + } + + // Load once outside the measured closure — we are timing detection, + // not PE parsing. + let assembly = match dotscope::CilObject::from_path(&path) { + Ok(a) => a, + Err(e) => { + eprintln!("skipping unloadable sample {}: {e}", path.display()); + continue; + } + }; + + group.bench_with_input( + BenchmarkId::from_parameter(label), + &assembly, + |b, assembly| { + let engine = DeobfuscationEngine::default(); + b.iter(|| black_box(engine.detect(black_box(assembly)))); + }, + ); + } + + group.finish(); +} + +criterion_group!( + benches, + bench_confuserex, + bench_netreactor, + bench_detection_only +); +criterion_main!(benches); diff --git a/dotscope/benches/method_body.rs b/dotscope/benches/method_body.rs index a182c25f..b3d60bff 100644 --- a/dotscope/benches/method_body.rs +++ b/dotscope/benches/method_body.rs @@ -7,9 +7,10 @@ extern crate dotscope; +use std::{fs, hint::black_box, path::PathBuf}; + use criterion::{criterion_group, criterion_main, Criterion, Throughput}; use dotscope::metadata::method::MethodBody; -use std::{fs, hint::black_box, path::PathBuf}; /// Run a method-body parsing benchmark over a sample file. Skips with a /// diagnostic message when the file cannot be read. diff --git a/dotscope/benches/resources.rs b/dotscope/benches/resources.rs index e8c57987..7099d7f1 100644 --- a/dotscope/benches/resources.rs +++ b/dotscope/benches/resources.rs @@ -2,9 +2,10 @@ extern crate dotscope; +use std::{fs, hint::black_box, path::PathBuf}; + use criterion::{criterion_group, criterion_main, Criterion, Throughput}; use dotscope::metadata::resources::{parse_dotnet_resource, parse_dotnet_resource_ref}; -use std::{fs, hint::black_box, path::PathBuf}; /// Benchmark parsing from standalone .resources file (WindowsBase resources) /// diff --git a/dotscope/benches/security.rs b/dotscope/benches/security.rs index c5831d27..bec1a8a4 100644 --- a/dotscope/benches/security.rs +++ b/dotscope/benches/security.rs @@ -5,9 +5,10 @@ extern crate dotscope; +use std::{fs, hint::black_box, path::PathBuf}; + use criterion::{criterion_group, criterion_main, Criterion, Throughput}; use dotscope::metadata::security::PermissionSet; -use std::{fs, hint::black_box, path::PathBuf}; /// Benchmark parsing a real declarative security blob from WindowsBase.dll. /// diff --git a/dotscope/benches/signatures.rs b/dotscope/benches/signatures.rs index 710d0383..8d10750c 100644 --- a/dotscope/benches/signatures.rs +++ b/dotscope/benches/signatures.rs @@ -10,12 +10,13 @@ extern crate dotscope; +use std::hint::black_box; + use criterion::{criterion_group, criterion_main, Criterion}; use dotscope::metadata::signatures::{ parse_field_signature, parse_local_var_signature, parse_method_signature, parse_method_spec_signature, parse_property_signature, parse_type_spec_signature, }; -use std::hint::black_box; /// Benchmark parsing a simple void method with no parameters. /// Signature: void Method() diff --git a/dotscope/benches/streams.rs b/dotscope/benches/streams.rs index 61870e57..872e36a0 100644 --- a/dotscope/benches/streams.rs +++ b/dotscope/benches/streams.rs @@ -10,9 +10,10 @@ extern crate dotscope; +use std::{fs, hint::black_box, path::PathBuf}; + use criterion::{criterion_group, criterion_main, Criterion, Throughput}; use dotscope::metadata::streams::{Blob, Guid, Strings, UserStrings}; -use std::{fs, hint::black_box, path::PathBuf}; /// Read a sample file from the workspace samples directory. Returns `None` /// (with a stderr diagnostic) if the file is missing, so benchmarks can be diff --git a/dotscope/examples/basic.rs b/dotscope/examples/basic.rs index 24edf5fa..2b2e4978 100644 --- a/dotscope/examples/basic.rs +++ b/dotscope/examples/basic.rs @@ -15,9 +15,10 @@ //! - Basic understanding of .NET assemblies //! - Familiarity with Rust error handling -use dotscope::prelude::*; use std::{env, path::Path}; +use dotscope::prelude::*; + fn main() -> Result<()> { // Get the path from command line arguments or use a default let args: Vec = env::args().collect(); diff --git a/dotscope/examples/comprehensive.rs b/dotscope/examples/comprehensive.rs index 7e11bccf..ca90613c 100644 --- a/dotscope/examples/comprehensive.rs +++ b/dotscope/examples/comprehensive.rs @@ -18,9 +18,10 @@ //! - Understanding of .NET metadata structures //! - Familiarity with CIL instructions -use dotscope::prelude::*; use std::{env, path::Path}; +use dotscope::prelude::*; + fn main() -> Result<()> { let args: Vec = env::args().collect(); let prog = args.first().map_or("comprehensive", String::as_str); diff --git a/dotscope/examples/disassembly.rs b/dotscope/examples/disassembly.rs index 531f9cbd..aefae61b 100644 --- a/dotscope/examples/disassembly.rs +++ b/dotscope/examples/disassembly.rs @@ -18,9 +18,10 @@ //! - Understanding of CIL instruction set //! - Familiarity with control flow concepts -use dotscope::prelude::*; use std::{env, path::Path}; +use dotscope::prelude::*; + fn main() -> Result<()> { let args: Vec = env::args().collect(); let prog = args.first().map_or("disassembly", String::as_str); diff --git a/dotscope/examples/injectcode.rs b/dotscope/examples/injectcode.rs index 95007e05..006cdee9 100644 --- a/dotscope/examples/injectcode.rs +++ b/dotscope/examples/injectcode.rs @@ -22,6 +22,8 @@ //! - Basic knowledge of CIL (Common Intermediate Language) //! - Familiarity with method signatures and calling conventions +use std::{env, path::Path}; + use dotscope::{ metadata::{ signatures::{encode_method_signature, SignatureMethod, SignatureParameter, TypeSignature}, @@ -30,7 +32,6 @@ use dotscope::{ }, prelude::*, }; -use std::{env, path::Path}; fn main() -> Result<()> { let args: Vec = env::args().collect(); diff --git a/dotscope/examples/lowlevel.rs b/dotscope/examples/lowlevel.rs index 1bf4d680..d5d40911 100644 --- a/dotscope/examples/lowlevel.rs +++ b/dotscope/examples/lowlevel.rs @@ -18,9 +18,10 @@ //! - Familiarity with ECMA-335 metadata structures //! - Experience with binary parsing concepts -use dotscope::prelude::*; use std::{env, fs, path::Path}; +use dotscope::prelude::*; + fn main() -> Result<()> { let args: Vec = env::args().collect(); let prog = args.first().map_or("lowlevel", String::as_str); diff --git a/dotscope/examples/metadata.rs b/dotscope/examples/metadata.rs index d0969693..f6476e88 100644 --- a/dotscope/examples/metadata.rs +++ b/dotscope/examples/metadata.rs @@ -18,10 +18,10 @@ //! - Familiarity with ECMA-335 specification //! - Experience with basic dotscope operations -use dotscope::metadata::customattributes::CustomAttributeValueRc; -use dotscope::prelude::*; use std::{collections::HashMap, env, path::Path}; +use dotscope::{metadata::customattributes::CustomAttributeValueRc, prelude::*}; + fn main() -> Result<()> { let args: Vec = env::args().collect(); let prog = args.first().map_or("metadata", String::as_str); diff --git a/dotscope/examples/method_analysis.rs b/dotscope/examples/method_analysis.rs index a865febb..5ebc21fb 100644 --- a/dotscope/examples/method_analysis.rs +++ b/dotscope/examples/method_analysis.rs @@ -23,9 +23,10 @@ //! - Understanding of method signatures //! - Familiarity with CIL instruction formats -use dotscope::prelude::*; use std::{env, path::Path}; +use dotscope::prelude::*; + // Helper functions to format implementation attributes fn format_impl_code_type(code_type: &MethodImplCodeType) -> String { match code_type.bits() { diff --git a/dotscope/examples/modify.rs b/dotscope/examples/modify.rs index e1524cc8..881dfc5e 100644 --- a/dotscope/examples/modify.rs +++ b/dotscope/examples/modify.rs @@ -20,6 +20,8 @@ //! - Familiarity with ECMA-335 specification concepts //! - Basic knowledge of P/Invoke and native interoperability +use std::{env, path::Path}; + use dotscope::{ metadata::{ signatures::TypeSignature, @@ -28,7 +30,6 @@ use dotscope::{ prelude::*, CilAssembly, CilAssemblyView, }; -use std::{env, path::Path}; fn main() -> Result<()> { let args: Vec = env::args().collect(); diff --git a/dotscope/examples/project_loader.rs b/dotscope/examples/project_loader.rs index bde1e1c5..fa033b18 100644 --- a/dotscope/examples/project_loader.rs +++ b/dotscope/examples/project_loader.rs @@ -25,9 +25,10 @@ //! cargo run --example project_loader -- tests/samples/crafted_2.exe --search-path tests/samples/mono_4.8 //! ``` -use dotscope::project::ProjectLoader; use std::env; +use dotscope::project::ProjectLoader; + fn main() -> dotscope::Result<()> { let args: Vec = env::args().collect(); let prog = args.first().map_or("project_loader", String::as_str); diff --git a/dotscope/examples/raw_assembly_view.rs b/dotscope/examples/raw_assembly_view.rs index 668a5084..5e34260d 100644 --- a/dotscope/examples/raw_assembly_view.rs +++ b/dotscope/examples/raw_assembly_view.rs @@ -5,9 +5,10 @@ //! and resolved metadata, `CilAssemblyView` gives you raw access to the file //! structure - perfect for building editing tools. -use dotscope::prelude::*; use std::env; +use dotscope::prelude::*; + fn main() -> Result<()> { // Get assembly path from command line or use default let args: Vec = env::args().collect(); diff --git a/dotscope/examples/types.rs b/dotscope/examples/types.rs index 49222c9c..f03a9104 100644 --- a/dotscope/examples/types.rs +++ b/dotscope/examples/types.rs @@ -18,9 +18,10 @@ //! - Familiarity with generics and inheritance //! - Experience with metadata analysis -use dotscope::prelude::*; use std::{collections::HashMap, env, path::Path}; +use dotscope::prelude::*; + fn main() -> Result<()> { let args: Vec = env::args().collect(); let prog = args.first().map_or("types", String::as_str); diff --git a/dotscope/fuzz/Cargo.toml b/dotscope/fuzz/Cargo.toml index 29825d69..3b515c22 100644 --- a/dotscope/fuzz/Cargo.toml +++ b/dotscope/fuzz/Cargo.toml @@ -12,7 +12,7 @@ cargo-fuzz = true [workspace] [dependencies] -libfuzzer-sys = "0.4.12" +libfuzzer-sys = "0.4.13" [dependencies.dotscope] path = ".." diff --git a/dotscope/src/analysis/callgraph/graph.rs b/dotscope/src/analysis/callgraph/graph.rs index 03d0b900..e707c092 100644 --- a/dotscope/src/analysis/callgraph/graph.rs +++ b/dotscope/src/analysis/callgraph/graph.rs @@ -8,9 +8,16 @@ //! The implementation leverages generic graph infrastructure, providing access //! to standard graph algorithms. -use std::collections::{HashMap, HashSet}; -use std::fmt::Write; -use std::sync::OnceLock; +use std::{ + collections::{HashMap, HashSet}, + fmt::Write, + sync::OnceLock, +}; + +use analyssa::graph::{ + algorithms::{self, strongly_connected_components}, + DirectedGraph, NodeId, +}; use crate::{ analysis::callgraph::{CallGraphNode, CallResolver, CallSite, CallTarget, CallType}, @@ -24,10 +31,6 @@ use crate::{ utils::escape_dot, CilObject, Result, }; -use analyssa::graph::{ - algorithms::{self, strongly_connected_components}, - DirectedGraph, NodeId, -}; /// Inter-procedural call graph for a .NET assembly. /// diff --git a/dotscope/src/analysis/callgraph/node.rs b/dotscope/src/analysis/callgraph/node.rs index 86fa0e8b..841d53bd 100644 --- a/dotscope/src/analysis/callgraph/node.rs +++ b/dotscope/src/analysis/callgraph/node.rs @@ -154,8 +154,10 @@ impl CallGraphNode { #[cfg(test)] mod tests { - use crate::analysis::callgraph::{CallGraphNode, CallSite, CallTarget, CallType}; - use crate::metadata::token::Token; + use crate::{ + analysis::callgraph::{CallGraphNode, CallSite, CallTarget, CallType}, + metadata::token::Token, + }; #[test] fn test_node_creation() { diff --git a/dotscope/src/analysis/callgraph/site.rs b/dotscope/src/analysis/callgraph/site.rs index 0e676f21..a336e377 100644 --- a/dotscope/src/analysis/callgraph/site.rs +++ b/dotscope/src/analysis/callgraph/site.rs @@ -237,8 +237,10 @@ impl CallSite { #[cfg(test)] mod tests { - use crate::analysis::callgraph::{CallSite, CallTarget, CallType}; - use crate::metadata::token::Token; + use crate::{ + analysis::callgraph::{CallSite, CallTarget, CallType}, + metadata::token::Token, + }; #[test] fn test_call_type_properties() { diff --git a/dotscope/src/analysis/cfg/mod.rs b/dotscope/src/analysis/cfg/mod.rs index bd983df7..9dc005b3 100644 --- a/dotscope/src/analysis/cfg/mod.rs +++ b/dotscope/src/analysis/cfg/mod.rs @@ -89,6 +89,11 @@ mod edge; mod graph; mod semantics; +#[cfg(feature = "compiler")] +pub use analyssa::analysis::loop_analyzer::SsaLoopAnalysis; +#[cfg(feature = "x86")] +pub use analyssa::analysis::loops::has_back_edges; +pub use analyssa::analysis::loops::{detect_loops, InductionVar, LoopForest, LoopInfo}; pub use edge::{CfgEdge, CfgEdgeKind}; pub use graph::ControlFlowGraph; pub use semantics::{BlockSemantics, LoopSemantics, SemanticAnalyzer}; @@ -97,11 +102,5 @@ pub use semantics::{BlockSemantics, LoopSemantics, SemanticAnalyzer}; // callers reach them through these aliases / re-exports. use crate::analysis::ssa::CilTarget; -#[cfg(feature = "compiler")] -pub use analyssa::analysis::loop_analyzer::SsaLoopAnalysis; -#[cfg(feature = "x86")] -pub use analyssa::analysis::loops::has_back_edges; -pub use analyssa::analysis::loops::{detect_loops, InductionVar, LoopForest, LoopInfo}; - /// CIL-defaulted alias of [`analyssa::analysis::loop_analyzer::LoopAnalyzer`]. pub type LoopAnalyzer<'a, T = CilTarget> = analyssa::analysis::loop_analyzer::LoopAnalyzer<'a, T>; diff --git a/dotscope/src/analysis/cfg/semantics.rs b/dotscope/src/analysis/cfg/semantics.rs index a92834cd..419b1491 100644 --- a/dotscope/src/analysis/cfg/semantics.rs +++ b/dotscope/src/analysis/cfg/semantics.rs @@ -730,7 +730,6 @@ impl<'a, T: Target> SemanticAnalyzer<'a, T> { #[cfg(test)] mod tests { use super::*; - use crate::analysis::{SsaFunctionBuilder, SsaType}; #[test] diff --git a/dotscope/src/analysis/dataflow/mod.rs b/dotscope/src/analysis/dataflow/mod.rs index 391aff7b..d0758434 100644 --- a/dotscope/src/analysis/dataflow/mod.rs +++ b/dotscope/src/analysis/dataflow/mod.rs @@ -2,15 +2,14 @@ //! `analyssa::analysis::dataflow`. CIL-defaulted aliases preserve historical //! `dotscope::analysis::*` API. -use analyssa::graph::{NodeId, RootedGraph}; - -use crate::analysis::{ssa::CilTarget, ControlFlowGraph}; - pub use analyssa::analysis::dataflow::{ framework::{AnalysisResults, DataFlowAnalysis, DataFlowCfg, Direction}, liveness::{LiveVariables, LivenessResult}, reaching::ReachingDefinitions, }; +use analyssa::graph::{NodeId, RootedGraph}; + +use crate::analysis::{ssa::CilTarget, ControlFlowGraph}; /// CIL-defaulted alias of [`analyssa::analysis::dataflow::sccp::ConstantPropagation`]. pub type ConstantPropagation = @@ -45,7 +44,6 @@ impl DataFlowCfg for ControlFlowGraph<'_> { #[cfg(test)] mod tests { use super::*; - use crate::{ analysis::{cfg::ControlFlowGraph, ssa::SsaConverter, SsaFunction}, assembly::{decode_blocks, InstructionAssembler}, diff --git a/dotscope/src/analysis/mod.rs b/dotscope/src/analysis/mod.rs index 5d80abca..f5bd23f2 100644 --- a/dotscope/src/analysis/mod.rs +++ b/dotscope/src/analysis/mod.rs @@ -79,6 +79,8 @@ mod taint; mod x86; // Re-export primary public types at module level +// Direct re-exports from analyssa for the formerly-shimmed analyses. +pub use analyssa::analysis::{algebraic::simplify_op, defuse::Location}; pub use callgraph::{ CallGraph, CallGraphNode, CallGraphStats, CallResolver, CallSite, CallTarget, CallType, ResolverStats, @@ -89,10 +91,6 @@ pub use dataflow::{ LiveVariables, LivenessResult, ReachingDefinitions, ScalarValue, SccpResult, }; -// Direct re-exports from analyssa for the formerly-shimmed analyses. -pub use analyssa::analysis::algebraic::simplify_op; -pub use analyssa::analysis::defuse::Location; - /// CIL-defaulted alias of [`analyssa::analysis::algebraic::SimplifyResult`]. pub type SimplifyResult = analyssa::analysis::algebraic::SimplifyResult; /// CIL-defaulted alias of [`analyssa::analysis::defuse::DefUseIndex`]. @@ -100,30 +98,29 @@ pub type DefUseIndex = analyssa::analysis::defuse::DefUseInd /// CIL-defaulted alias of [`analyssa::analysis::range::ValueRange`]. pub type ValueRange = analyssa::analysis::range::ValueRange; pub use analyssa::graph::NodeId; +// Re-export crate-internal types (used by other crate modules via crate::analysis::X) +#[cfg(feature = "compiler")] +#[allow(unused_imports)] +pub(crate) use cfg::SsaLoopAnalysis; +#[cfg(feature = "compiler")] +pub(crate) use ssa::conv_op_for_target; #[cfg(feature = "z3")] pub use ssa::Z3Solver; pub use ssa::{ - resolve_corelib_valuetype, AbstractValue, BinaryOpKind, CilTarget, CmpKind, ConstEvaluator, - ConstValue, ConstValueCilExt, ControlFlow, DefSite, FieldRef, MethodPurity, MethodRef, + address, pointsto, resolve_corelib_valuetype, AbstractValue, AliasResult, ArrayIndex, + BinaryOpKind, CilTarget, CmpKind, ConstEvaluator, ConstValue, ConstValueCilExt, ControlFlow, + DefSite, EvaluatorMark, FieldRef, IndirectLocation, MemoryDefSite, MemoryLocation, MemoryOp, + MemoryPhi, MemoryPhiOperand, MemorySsa, MemorySsaStats, MemoryVersion, MethodPurity, MethodRef, PhiAnalyzer, PhiNode, PhiOperand, ReturnInfo, SsaBlock, SsaCfg, SsaConverter, SsaEvaluator, SsaExceptionHandler, SsaExceptionHandlerCilExt, SsaFunction, SsaFunctionBuilder, - SsaInstruction, SsaOp, SsaOpCilExt, SsaType, SsaVarId, SsaVariable, SymbolicEvaluator, - SymbolicExpr, SymbolicOp, Target, TypeClass, TypeContext, TypeProvider, TypeRef, UnaryOpKind, - UseSite, ValueResolver, VariableOrigin, + SsaFunctionCilExt, SsaFunctionSemanticsExt, SsaInstruction, SsaOp, SsaOpCilExt, SsaType, + SsaVarId, SsaVariable, SymbolicEvaluator, SymbolicExpr, SymbolicOp, Target, TypeClass, + TypeContext, TypeProvider, TypeRef, UnaryOpKind, UseSite, ValueResolver, VariableOrigin, }; -pub use ssa::{SsaFunctionCilExt, SsaFunctionSemanticsExt}; pub use taint::{ cff_taint_config, find_token_dependencies, PhiTaintMode, TaintAnalysis, TaintConfig, TokenTaintBuilder, }; - -// Re-export crate-internal types (used by other crate modules via crate::analysis::X) -#[cfg(feature = "compiler")] -#[allow(unused_imports)] -pub(crate) use cfg::SsaLoopAnalysis; -#[cfg(feature = "compiler")] -pub(crate) use ssa::conv_op_for_target; - #[cfg(feature = "x86")] pub use x86::{ x86_decode_all, x86_decode_single, x86_decode_traversal, x86_detect_epilogue, diff --git a/dotscope/src/analysis/ssa/converter.rs b/dotscope/src/analysis/ssa/converter.rs index 6fef54d7..fc6c3afc 100644 --- a/dotscope/src/analysis/ssa/converter.rs +++ b/dotscope/src/analysis/ssa/converter.rs @@ -36,6 +36,11 @@ use std::collections::BTreeMap; +use analyssa::{ + graph::{algorithms::DominatorTree, NodeId}, + BitSet, +}; + use crate::{ analysis::{ cfg::ControlFlowGraph, @@ -43,7 +48,8 @@ use crate::{ decompose::decompose_instruction, liveness, place_pruned_phis, resolve_corelib_valuetype, ConstValue, DefSite, PhiNode, PhiPlacementConfig, SimulationResult, SsaBlock, SsaFunction, SsaInstruction, SsaOp, SsaType, SsaVarId, - StackSimulator, StackSlot, StackSlotSource, TypeProvider, UseSite, VariableOrigin, + StackSimulator, StackSlot, StackSlotSource, TypeProvider, TypeRef, UseSite, + VariableOrigin, }, }, assembly::{opcodes, Immediate, Instruction, Operand}, @@ -55,10 +61,28 @@ use crate::{ }, CilObject, Error, Result, }; -use analyssa::{ - graph::{algorithms::DominatorTree, NodeId}, - BitSet, -}; + +/// How a read-but-never-written variable gets its default value. +/// +/// See [`SsaConverter::materialize_undefined_definitions`]. +enum DefaultInit { + /// A constant zero or null, emitted as a single `Const`. + Const { + /// Variable to define. + dest: SsaVarId, + /// The zero value for the variable's type. + value: ConstValue, + }, + /// A zeroed struct, emitted as `ldloca; initobj; ldloc`. + ZeroStruct { + /// Variable to define. + dest: SsaVarId, + /// The value type to zero. + type_ref: TypeRef, + /// CIL local slot backing the variable. + local_index: u16, + }, +} /// A variable definition record during SSA construction. #[derive(Debug, Clone)] @@ -239,7 +263,7 @@ impl<'a, 'cfg> SsaConverter<'a, 'cfg> { match origin { VariableOrigin::Argument(idx) => self.type_provider.arg_type(idx), VariableOrigin::Local(idx) => self.type_provider.local_type(idx), - VariableOrigin::Phi => SsaType::Unknown, + VariableOrigin::Phi | VariableOrigin::EntryLiveIn => SsaType::Unknown, } } @@ -554,6 +578,212 @@ impl<'a, 'cfg> SsaConverter<'a, 'cfg> { .create_variable(origin, 0, DefSite::entry(), var_type) } + /// Returns the constant zero for a type, or `None` if it has no constant form. + /// + /// Mirrors CIL's `.locals init` semantics: integers and floats zero, references + /// null. Value types are not covered here — a zeroed struct is not `ldnull` — + /// and are handled by [`zero_init_value_type`](Self::zero_init_value_type). + fn zero_value_for(ty: &SsaType) -> Option { + Some(match ty { + SsaType::Bool => ConstValue::False, + SsaType::I8 => ConstValue::I8(0), + SsaType::U8 => ConstValue::U8(0), + SsaType::I16 => ConstValue::I16(0), + SsaType::U16 => ConstValue::U16(0), + SsaType::Char => ConstValue::U16(0), + SsaType::I32 => ConstValue::I32(0), + SsaType::U32 => ConstValue::U32(0), + SsaType::I64 => ConstValue::I64(0), + SsaType::U64 => ConstValue::U64(0), + SsaType::NativeInt => ConstValue::NativeInt(0), + SsaType::NativeUInt => ConstValue::NativeUInt(0), + SsaType::F32 => ConstValue::F32(0.0), + SsaType::F64 => ConstValue::F64(0.0), + SsaType::Object + | SsaType::String + | SsaType::Null + | SsaType::Class(_) + | SsaType::Array(_, _) + | SsaType::GenericInst(_, _) + | SsaType::Pointer(_) + | SsaType::ByRef(_) + | SsaType::FnPtr(_) => ConstValue::Null, + _ => return None, + }) + } + + /// How a read-but-never-written variable should be given its default value. + /// + /// Named rather than expressed as a tuple because the two cases produce a + /// different number of instructions, which the insertion accounting depends on. + fn default_init_plan(&self, var_id: SsaVarId) -> Option { + let variable = self.function.variable(var_id)?; + // Arguments arrive from the caller — never synthesize a value. + if matches!(variable.origin(), VariableOrigin::Argument(_)) { + return None; + } + + let var_type = variable.var_type(); + if let Some(value) = Self::zero_value_for(var_type) { + return Some(DefaultInit::Const { + dest: var_id, + value, + }); + } + + // A struct is zeroed with `ldloca; initobj`, then read back. That needs a + // real local slot to take the address of, so it only works for variables + // that correspond to a CIL local. Stack temporaries of value type have no + // slot and are left undefined. + if let (SsaType::ValueType(type_ref), VariableOrigin::Local(local_index)) = + (var_type, variable.origin()) + { + return Some(DefaultInit::ZeroStruct { + dest: var_id, + type_ref: *type_ref, + local_index, + }); + } + + None + } + + /// Materializes explicit default definitions for variables that are read but + /// never written. + /// + /// SSA construction registers a placeholder variable whenever no reaching + /// definition can be found — an uninitialized local, or a stack slot that no + /// predecessor supplies (see [`create_undefined_var`](Self::create_undefined_var)). + /// The placeholder is entered in the variable table with `DefSite::entry()` + /// but no instruction ever defines it. + /// + /// That is stable only for as long as nobody rebuilds the function. + /// `SsaFunction::rebuild_ssa` reconstructs the variable table from actual + /// definitions, so the placeholders disappear and every remaining read of one + /// becomes an undefined use that fails verification. Passes that legitimately + /// promote a phi operand into an instruction operand — CFF unflattening + /// materializing dispatcher phis, for example — turn a tolerated dangling phi + /// operand into exactly such a read. + /// + /// Emitting a real definition in the entry block, which dominates everything, + /// makes the invariant hold by construction. The values match what the runtime + /// would supply for a zero-initialized local, so behaviour is unchanged: zero + /// for primitives, null for references, and an `initobj`-zeroed struct for + /// value types. Only variables that are actually read get a definition, so + /// unused entry seeds do not accumulate dead code. + fn materialize_undefined_definitions(&mut self) { + let entry_block = self.cfg.entry().index(); + + let mut defined: BTreeMap = BTreeMap::new(); + let mut used: BTreeMap = BTreeMap::new(); + for block in self.function.blocks() { + for phi in block.phi_nodes() { + defined.insert(phi.result(), ()); + for operand in phi.operands() { + used.insert(operand.value(), ()); + } + } + for instr in block.instructions() { + if let Some(dest) = instr.def() { + defined.insert(dest, ()); + } + for use_var in instr.uses() { + used.insert(use_var, ()); + } + } + } + + let plans: Vec = used + .keys() + .filter(|var_id| !defined.contains_key(*var_id)) + .filter_map(|var_id| self.default_init_plan(*var_id)) + .collect(); + + if plans.is_empty() { + return; + } + + // Build the instruction sequence first: a struct default needs an extra + // address temporary, so the instruction count is not the plan count. + let mut prologue: Vec = Vec::new(); + for plan in plans { + match plan { + DefaultInit::Const { dest, value } => { + prologue.push(SsaInstruction::synthetic(SsaOp::Const { dest, value })); + } + DefaultInit::ZeroStruct { + dest, + type_ref, + local_index, + } => { + // ldloca local_index; initobj T; ldloc local_index + let addr = self.function.create_variable( + VariableOrigin::Phi, + 0, + DefSite::instruction(entry_block, prologue.len()), + SsaType::ByRef(Box::new(SsaType::ValueType(type_ref))), + ); + prologue.push(SsaInstruction::synthetic(SsaOp::LoadLocalAddr { + dest: addr, + local_index, + })); + prologue.push(SsaInstruction::synthetic(SsaOp::InitObj { + dest_addr: addr, + value_type: type_ref, + })); + prologue.push(SsaInstruction::synthetic(SsaOp::LoadLocal { + dest, + local_index, + })); + } + } + } + + // Inserting at the front of the entry block shifts every instruction + // already there, so def sites recorded against it must move too. The + // address temporaries created above are corrected afterwards. + let inserted = prologue.len(); + for variable in self.function.variables_mut() { + let site = variable.def_site(); + if site.block == entry_block { + if let Some(idx) = site.instruction { + variable.set_def_site(DefSite::instruction( + entry_block, + idx.saturating_add(inserted), + )); + } + } + } + + let Some(entry) = self.function.block_mut(entry_block) else { + return; + }; + for (offset, instr) in prologue.into_iter().enumerate() { + entry.instructions_mut().insert(offset, instr); + } + + // Record where each prologue instruction actually defines its variable. + // This runs after the shift above so it is not displaced by it. + let defs: Vec<(usize, SsaVarId)> = self + .function + .block(entry_block) + .map(|block| { + block + .instructions() + .iter() + .take(inserted) + .enumerate() + .filter_map(|(idx, instr)| instr.def().map(|dest| (idx, dest))) + .collect() + }) + .unwrap_or_default(); + for (idx, dest) in defs { + if let Some(variable) = self.function.variable_mut(dest) { + variable.set_def_site(DefSite::instruction(entry_block, idx)); + } + } + } + /// Tries to map a placeholder variable to a value from a predecessor's exit stack. /// /// This is used when the immediate dominator's exit stack doesn't have the needed @@ -683,6 +913,11 @@ impl<'a, 'cfg> SsaConverter<'a, 'cfg> { // keeps the initial SSA clean and avoids stale instruction indices. builder.strip_resolved_loads(); + // Phase 3c: Give every read-but-never-written variable a real definition. + // Must run after `strip_resolved_loads` so entry-block instruction + // indices are final, and before `reindex_variables`. + builder.materialize_undefined_definitions(); + // Set original local type signatures from type provider for code generation if let Some(local_types) = type_provider.local_type_signatures() { builder.function.set_original_local_types(local_types); @@ -1051,7 +1286,7 @@ impl<'a, 'cfg> SsaConverter<'a, 'cfg> { let group = match origin { VariableOrigin::Argument(idx) => self.arg_group(idx), VariableOrigin::Local(idx) => self.local_group(idx), - VariableOrigin::Phi => continue, + VariableOrigin::Phi | VariableOrigin::EntryLiveIn => continue, }; self.defs .entry(group) @@ -1064,7 +1299,7 @@ impl<'a, 'cfg> SsaConverter<'a, 'cfg> { let use_group = match use_origin { VariableOrigin::Argument(idx) => self.arg_group(idx), VariableOrigin::Local(idx) => self.local_group(idx), - VariableOrigin::Phi => continue, + VariableOrigin::Phi | VariableOrigin::EntryLiveIn => continue, }; self.uses .entry(use_group) @@ -1079,7 +1314,7 @@ impl<'a, 'cfg> SsaConverter<'a, 'cfg> { let store_group = match store_target { VariableOrigin::Argument(idx) => self.arg_group(idx), VariableOrigin::Local(idx) => self.local_group(idx), - VariableOrigin::Phi => continue, + VariableOrigin::Phi | VariableOrigin::EntryLiveIn => continue, }; self.defs .entry(store_group) @@ -2056,7 +2291,7 @@ impl<'a, 'cfg> SsaConverter<'a, 'cfg> { let group = match origin { VariableOrigin::Argument(idx) => self.arg_group(idx), VariableOrigin::Local(idx) => self.local_group(idx), - VariableOrigin::Phi => return None, + VariableOrigin::Phi | VariableOrigin::EntryLiveIn => return None, }; self.current_def(group) } @@ -2239,7 +2474,7 @@ impl<'a, 'cfg> SsaConverter<'a, 'cfg> { mapped } } - VariableOrigin::Phi => { + VariableOrigin::Phi | VariableOrigin::EntryLiveIn => { // Stack temps have Phi origin — resolve via rename group or // var_stack_positions (simulation variables may not have groups yet) let group = self.function.rename_group(mapped); @@ -2279,7 +2514,7 @@ impl<'a, 'cfg> SsaConverter<'a, 'cfg> { use_var } } - VariableOrigin::Phi => { + VariableOrigin::Phi | VariableOrigin::EntryLiveIn => { // Stack temps have Phi origin — resolve via rename group or // var_stack_positions (simulation variables may not have groups yet) let group = self.function.rename_group(use_var); @@ -2459,7 +2694,7 @@ impl<'a, 'cfg> SsaConverter<'a, 'cfg> { match origin { VariableOrigin::Argument(idx) => self.arg_group(idx), VariableOrigin::Local(idx) => self.local_group(idx), - VariableOrigin::Phi => { + VariableOrigin::Phi | VariableOrigin::EntryLiveIn => { // Stack phi — find slot from entry stack self.stack_group(0) } @@ -2741,7 +2976,7 @@ impl<'a, 'cfg> SsaConverter<'a, 'cfg> { let group = match origin { VariableOrigin::Argument(idx) => self.arg_group(idx), VariableOrigin::Local(idx) => self.local_group(idx), - VariableOrigin::Phi => u32::MAX, // shouldn't happen for infer_origin + VariableOrigin::Phi | VariableOrigin::EntryLiveIn => u32::MAX, // shouldn't happen for infer_origin }; let var_type = self.type_for_origin(origin); let v = self.new_def(origin, group, block_idx, Some(instr_idx), var_type); @@ -2780,7 +3015,7 @@ impl<'a, 'cfg> SsaConverter<'a, 'cfg> { let store_group = match store_target { VariableOrigin::Argument(idx) => self.arg_group(idx), VariableOrigin::Local(idx) => self.local_group(idx), - VariableOrigin::Phi => u32::MAX, + VariableOrigin::Phi | VariableOrigin::EntryLiveIn => u32::MAX, }; let var_type = self.type_for_origin(store_target); let _new_version = self.new_def( @@ -2822,7 +3057,7 @@ impl<'a, 'cfg> SsaConverter<'a, 'cfg> { group = match phi.origin() { VariableOrigin::Argument(idx) => self.arg_group(idx), VariableOrigin::Local(idx) => self.local_group(idx), - VariableOrigin::Phi => u32::MAX, + VariableOrigin::Phi | VariableOrigin::EntryLiveIn => u32::MAX, }; } (phi.result(), group) @@ -2863,10 +3098,9 @@ impl<'a, 'cfg> SsaConverter<'a, 'cfg> { #[cfg(test)] mod tests { - use super::*; - use std::collections::BTreeSet; + use super::*; use crate::{ assembly::{decode_blocks, InstructionAssembler}, test::TestTypeProvider, diff --git a/dotscope/src/analysis/ssa/decompose.rs b/dotscope/src/analysis/ssa/decompose.rs index 00877080..b249b527 100644 --- a/dotscope/src/analysis/ssa/decompose.rs +++ b/dotscope/src/analysis/ssa/decompose.rs @@ -1208,6 +1208,8 @@ fn decompose_fe_instruction( dest_addr, src_addr, size, + // CIL `cpblk` is always forward (no direction flag). + reverse: false, }) } else { None @@ -1222,6 +1224,8 @@ fn decompose_fe_instruction( dest_addr, value, size, + // CIL `initblk` is always forward (no direction flag). + reverse: false, }) } else { None @@ -1412,6 +1416,8 @@ fn ldind_typed(uses: &[SsaVarId], def: Option, value_type: SsaType) -> dest, addr, value_type, + // CIL `ldind` has no segment/address-space qualifier. + address_space: None, }) } else { None @@ -1424,6 +1430,8 @@ fn stind_typed(uses: &[SsaVarId], value_type: SsaType) -> Option { addr, value, value_type, + // CIL `stind` has no segment/address-space qualifier. + address_space: None, }) } else { None diff --git a/dotscope/src/analysis/ssa/exception.rs b/dotscope/src/analysis/ssa/exception.rs index 422748b5..b0b7ec12 100644 --- a/dotscope/src/analysis/ssa/exception.rs +++ b/dotscope/src/analysis/ssa/exception.rs @@ -67,9 +67,8 @@ impl SsaExceptionHandlerCilExt for AnalyssaSsaExceptionHandler { #[cfg(test)] mod tests { - use crate::metadata::method::ExceptionHandlerFlags; - use super::*; + use crate::metadata::method::ExceptionHandlerFlags; // Lock T to CilTarget for tests; they construct `SsaExceptionHandler` with // CIL flags directly. diff --git a/dotscope/src/analysis/ssa/function.rs b/dotscope/src/analysis/ssa/function.rs index de677930..24fa583d 100644 --- a/dotscope/src/analysis/ssa/function.rs +++ b/dotscope/src/analysis/ssa/function.rs @@ -117,10 +117,21 @@ impl SsaFunctionCilExt for SsaFunction { } } } + // Renumbering a local moves its rename group too. `rebuild_ssa` groups + // variables by rename group but resolves argument/local representatives + // by origin, so leaving the group pointing at the old slot silently + // splits the value: the two views disagree about which names denote the + // same local, and the value ends up with no reaching definition. The + // group for a local is `num_args + index`, per the assignment rules + // documented on `SsaFunction::rename_groups`. + let num_args = u32::try_from(self.num_args()).unwrap_or(0); for (id, origin) in new_origins { if let Some(v) = self.variable_mut(id) { v.set_origin(origin); } + if let VariableOrigin::Local(new) = origin { + self.set_rename_group(id, num_args.saturating_add(u32::from(new))); + } } // Apply remap to phi nodes diff --git a/dotscope/src/analysis/ssa/mod.rs b/dotscope/src/analysis/ssa/mod.rs index cf06df18..c22e1cbe 100644 --- a/dotscope/src/analysis/ssa/mod.rs +++ b/dotscope/src/analysis/ssa/mod.rs @@ -93,19 +93,34 @@ mod value; // shims that used to mediate via `mod cfg/consts/phi/...` collapsed into // the `pub use` block below. +// `SsaFunction`/`ReturnInfo`/`MethodPurity` live in `analyssa::ir::function`. +pub use analyssa::ir::function::MethodPurity; pub(crate) use builder::conv_op_for_target; pub use builder::SsaFunctionBuilder; pub use converter::SsaConverter; pub use exception::{SsaExceptionHandler, SsaExceptionHandlerCilExt}; pub use function::{SsaFunctionCilExt, SsaFunctionSemanticsExt}; pub use ops::{BinaryOpKind, CmpKind, SsaOp, SsaOpCilExt, UnaryOpKind}; - -// `SsaFunction`/`ReturnInfo`/`MethodPurity` live in `analyssa::ir::function`. -pub use analyssa::ir::function::MethodPurity; /// CIL-defaulted alias of [`analyssa::ir::function::SsaFunction`]. pub type SsaFunction = analyssa::ir::function::SsaFunction; /// CIL-defaulted alias of [`analyssa::ir::function::ReturnInfo`]. pub type ReturnInfo = analyssa::ir::function::ReturnInfo; +#[allow(unused_imports)] +pub use analyssa::analysis::consts::evaluate_const_op; +pub use analyssa::{ + analysis::{ + address, + evaluator::ControlFlow, + memory::{AliasResult, ArrayIndex, IndirectLocation, MemoryDefSite, MemoryPhiOperand}, + phis::{place_pruned_phis, PhiAnalyzer, PhiPlacementConfig}, + pointsto, + }, + ir::{ + phi::{PhiNode, PhiOperand}, + variable::{DefSite, FunctionVarAllocator, SsaVarId, UseSite, VariableOrigin}, + }, + Target, +}; pub use resolver::ValueResolver; pub use stack::{SimulationResult, StackSimulator, StackSlot, StackSlotSource}; #[cfg(feature = "z3")] @@ -118,19 +133,6 @@ pub use types::{ }; pub use value::{AbstractValue, ConstValue, ConstValueCilExt}; -// Direct re-exports from analyssa for the now-collapsed shim files. Each line -// here used to be a one-line module file in `dotscope/src/analysis/ssa/`. -pub use analyssa::ir::phi::{PhiNode, PhiOperand}; -pub use analyssa::ir::variable::{ - DefSite, FunctionVarAllocator, SsaVarId, UseSite, VariableOrigin, -}; -pub use analyssa::Target; - -#[allow(unused_imports)] -pub use analyssa::analysis::consts::evaluate_const_op; -pub use analyssa::analysis::evaluator::ControlFlow; -pub use analyssa::analysis::phis::{place_pruned_phis, PhiAnalyzer, PhiPlacementConfig}; - /// CIL-defaulted alias of [`analyssa::ir::block::SsaBlock`]. pub type SsaBlock = analyssa::ir::block::SsaBlock; /// CIL-defaulted alias of [`analyssa::ir::instruction::SsaInstruction`]. @@ -143,6 +145,8 @@ pub type SsaCfg<'a, T = CilTarget> = analyssa::analysis::cfg::SsaCfg<'a, T>; pub type ConstEvaluator<'a, T = CilTarget> = analyssa::analysis::consts::ConstEvaluator<'a, T>; /// CIL-defaulted alias of [`analyssa::analysis::evaluator::SsaEvaluator`]. pub type SsaEvaluator<'a, T = CilTarget> = analyssa::analysis::evaluator::SsaEvaluator<'a, T>; +/// CIL-defaulted alias of [`analyssa::analysis::evaluator::EvaluatorMark`]. +pub type EvaluatorMark = analyssa::analysis::evaluator::EvaluatorMark; /// CIL-defaulted alias of [`analyssa::analysis::evaluator::ExecutionTrace`]. pub type ExecutionTrace = analyssa::analysis::evaluator::ExecutionTrace; /// CIL-defaulted alias of [`analyssa::analysis::patterns::PatternDetector`]. @@ -168,6 +172,10 @@ pub type MemoryOp = analyssa::analysis::memory::MemoryOp; pub type MemoryPhi = analyssa::analysis::memory::MemoryPhi; /// CIL-defaulted alias of [`analyssa::analysis::memory::MemoryVersion`]. pub type MemoryVersion = analyssa::analysis::memory::MemoryVersion; +/// CIL-defaulted alias of [`analyssa::analysis::memory::MemorySsa`]. +pub type MemorySsa = analyssa::analysis::memory::MemorySsa; +/// CIL-defaulted alias of [`analyssa::analysis::memory::MemorySsaStats`]. +pub type MemorySsaStats = analyssa::analysis::memory::MemorySsaStats; /// CIL-defaulted alias of [`analyssa::analysis::verifier::SsaVerifier`]. pub type SsaVerifier<'a, T = CilTarget> = analyssa::analysis::verifier::SsaVerifier<'a, T>; diff --git a/dotscope/src/analysis/ssa/ops.rs b/dotscope/src/analysis/ssa/ops.rs index bb65f85c..94ccc0ad 100644 --- a/dotscope/src/analysis/ssa/ops.rs +++ b/dotscope/src/analysis/ssa/ops.rs @@ -5,14 +5,13 @@ //! foreign types). use analyssa::ir::ops::SsaOp as AnalyssaSsaOp; - -use crate::{analysis::ssa::target::CilTarget, metadata::token::Token}; - // `BinaryOpInfo`/`UnaryOpInfo` aren't re-exported (the original dotscope // `ops.rs` didn't surface them either; direct callers go through // `analyssa::ir::ops` if they need them). pub use analyssa::ir::ops::{BinaryOpKind, CmpKind, UnaryOpKind}; +use crate::{analysis::ssa::target::CilTarget, metadata::token::Token}; + /// CIL-defaulted alias of `analyssa::ir::ops::SsaOp`. pub type SsaOp = AnalyssaSsaOp; diff --git a/dotscope/src/analysis/ssa/resolver.rs b/dotscope/src/analysis/ssa/resolver.rs index bcd8d2e7..c20d5bbf 100644 --- a/dotscope/src/analysis/ssa/resolver.rs +++ b/dotscope/src/analysis/ssa/resolver.rs @@ -148,11 +148,10 @@ impl<'a> ValueResolver<'a, CilTarget> { #[cfg(test)] mod tests { - use super::*; - #[cfg(feature = "compiler")] use std::sync::Arc; + use super::*; use crate::{ analysis::ssa::{ ConstValue, DefSite, PhiNode, PhiOperand, SsaBlock, SsaFunction, SsaInstruction, SsaOp, diff --git a/dotscope/src/analysis/ssa/symbolic/solver.rs b/dotscope/src/analysis/ssa/symbolic/solver.rs index f0e83b84..100b316b 100644 --- a/dotscope/src/analysis/ssa/symbolic/solver.rs +++ b/dotscope/src/analysis/ssa/symbolic/solver.rs @@ -6,9 +6,10 @@ use std::collections::HashMap; -use crate::{analysis::ssa::CilTarget, metadata::typesystem::PointerSize}; use analyssa::analysis::symbolic::{expr::SymbolicExpr as GenericSymbolicExpr, ops::SymbolicOp}; +use crate::{analysis::ssa::CilTarget, metadata::typesystem::PointerSize}; + type SymbolicExpr = GenericSymbolicExpr; /// Z3-based constraint solver for symbolic expressions. @@ -533,9 +534,10 @@ impl Z3Solver { #[cfg(test)] mod tests { - use crate::{analysis::ssa::symbolic::Z3Solver, metadata::typesystem::PointerSize}; use analyssa::analysis::symbolic::{expr::SymbolicExpr, ops::SymbolicOp}; + use crate::{analysis::ssa::symbolic::Z3Solver, metadata::typesystem::PointerSize}; + #[test] fn test_z3_simple_solve() { let solver = Z3Solver::new(); diff --git a/dotscope/src/analysis/ssa/target.rs b/dotscope/src/analysis/ssa/target.rs index 60bf7cfc..d8476bf0 100644 --- a/dotscope/src/analysis/ssa/target.rs +++ b/dotscope/src/analysis/ssa/target.rs @@ -9,6 +9,10 @@ //! delegate without forming an `impl ConstValue` ↔ `impl Target for //! CilTarget` cycle. +// Re-export so existing `crate::analysis::ssa::target::Target` import paths +// in the rest of dotscope continue to resolve. The trait itself lives in +// `analyssa::target`. +pub use analyssa::target::Target; use analyssa::{ir::value::ConstValue, PointerSize}; #[cfg(feature = "compiler")] @@ -19,11 +23,6 @@ use crate::{ metadata::{method::ExceptionHandlerFlags, signatures::SignatureLocalVariable}, }; -// Re-export so existing `crate::analysis::ssa::target::Target` import paths -// in the rest of dotscope continue to resolve. The trait itself lives in -// `analyssa::target`. -pub use analyssa::target::Target; - /// `Target` impl for .NET CIL. /// /// Instances carry the pointer width chosen at construction (4 for 32-bit @@ -154,6 +153,21 @@ impl Target for CilTarget { *flags == ExceptionHandlerFlags::FILTER } + fn field_member_index(field: &Self::FieldRef) -> Option { + // Field-sensitive points-to keys its abstract cells on + // `(object, member_index)`, so all this needs to be is a *stable and + // distinct* identity per field — the CIL metadata token already is one, + // and it is the same identity `MemoryLocation::InstanceField` compares + // on. A declaration-order index within the parent type would need the + // assembly, which this associated function has no access to. + // + // A null token means the field never resolved; returning `None` there + // takes the sound field-insensitive whole-object fallback rather than + // collapsing every unresolved field onto one cell. + let token = field.token().value(); + (token != 0).then_some(token) + } + fn result_type_for_const(value: &ConstValue) -> Option { Some(match value { ConstValue::I8(_) => SsaType::I8, @@ -432,13 +446,15 @@ fn cil_evaluator_apply_conversion( #[cfg(test)] mod tests { - use super::*; - use analyssa::{MockTarget, MockType}; - use crate::analysis::ssa::{ - value::ConstValue, DefSite, SsaBlock, SsaFunction, SsaInstruction, SsaOp, SsaVarId, - VariableOrigin, + use super::*; + use crate::{ + analysis::ssa::{ + value::ConstValue, DefSite, SsaBlock, SsaFunction, SsaInstruction, SsaOp, SsaVarId, + VariableOrigin, + }, + metadata::token::Token, }; #[test] @@ -484,6 +500,34 @@ mod tests { assert_eq!(CilTarget::bit_width(&SsaType::NativeInt), None); } + #[test] + fn cil_target_field_member_index() { + // Distinct fields must get distinct cell identities, or field-sensitive + // points-to collapses them onto one abstract cell. + let a = FieldRef::new(Token::new(0x0400_0001)); + let b = FieldRef::new(Token::new(0x0400_0002)); + assert_ne!( + CilTarget::field_member_index(&a), + CilTarget::field_member_index(&b) + ); + // Same field, asked twice, must be stable. + assert_eq!( + CilTarget::field_member_index(&a), + CilTarget::field_member_index(&FieldRef::new(Token::new(0x0400_0001))) + ); + // A MemberRef-sourced field is a different token, so it never collides + // with a FieldDef of the same row. + assert_ne!( + CilTarget::field_member_index(&a), + CilTarget::field_member_index(&FieldRef::new(Token::new(0x0A00_0001))) + ); + // An unresolved (null) field takes the field-insensitive fallback. + assert_eq!( + CilTarget::field_member_index(&FieldRef::new(Token::new(0))), + None + ); + } + #[test] fn cil_target_unknown() { assert_eq!(CilTarget::unknown_type(), SsaType::Unknown); diff --git a/dotscope/src/analysis/ssa/value.rs b/dotscope/src/analysis/ssa/value.rs index 6513f33f..bd0390f8 100644 --- a/dotscope/src/analysis/ssa/value.rs +++ b/dotscope/src/analysis/ssa/value.rs @@ -131,11 +131,12 @@ impl TryFrom<&AnalyssaConstValue> for Immediate { #[cfg(test)] mod tests { - use super::*; - - use analyssa::ir::value::{ComputedOp, ComputedValue}; - use analyssa::ir::variable::SsaVarId; + use analyssa::ir::{ + value::{ComputedOp, ComputedValue}, + variable::SsaVarId, + }; + use super::*; use crate::metadata::typesystem::PointerSize; // Tests construct unit/numeric variants like `ConstValue::I32(_)` that diff --git a/dotscope/src/analysis/taint.rs b/dotscope/src/analysis/taint.rs index d3c9e068..5f546d3d 100644 --- a/dotscope/src/analysis/taint.rs +++ b/dotscope/src/analysis/taint.rs @@ -87,7 +87,6 @@ mod tests { use std::collections::HashSet; use super::*; - use crate::analysis::{ ConstValue, PhiNode, PhiOperand, SsaBlock, SsaFunction, SsaInstruction, SsaOp, SsaVarId, VariableOrigin, diff --git a/dotscope/src/analysis/x86/cfg.rs b/dotscope/src/analysis/x86/cfg.rs index 4b92a729..df01db9b 100644 --- a/dotscope/src/analysis/x86/cfg.rs +++ b/dotscope/src/analysis/x86/cfg.rs @@ -4,17 +4,18 @@ //! and their successor relationships. It leverages the existing graph infrastructure //! from [`crate::utils::graph`] for efficient analysis. +use std::{collections::BTreeSet, sync::OnceLock}; + use analyssa::graph::{ algorithms::{compute_dominators, DominatorTree}, DirectedGraph, GraphBase, NodeId, Predecessors, RootedGraph, Successors, }; +use rustc_hash::FxHashMap; use crate::analysis::{ cfg::has_back_edges, x86::types::{X86DecodedInstruction, X86EdgeKind, X86Instruction}, }; -use rustc_hash::FxHashMap; -use std::{collections::BTreeSet, sync::OnceLock}; /// A basic block in the x86 CFG. #[derive(Debug, Clone)] diff --git a/dotscope/src/analysis/x86/decoder.rs b/dotscope/src/analysis/x86/decoder.rs index 7581edb6..d4aedd17 100644 --- a/dotscope/src/analysis/x86/decoder.rs +++ b/dotscope/src/analysis/x86/decoder.rs @@ -3,6 +3,11 @@ //! This module provides a thin wrapper around iced-x86 that converts its //! instruction representation to our simplified [`X86Instruction`] types. +use std::collections::VecDeque; + +use iced_x86::{Decoder, DecoderOptions, Instruction, Mnemonic, OpKind, Register}; +use rustc_hash::FxHashSet; + use crate::{ analysis::x86::types::{ X86Condition, X86DecodedInstruction, X86EpilogueInfo, X86Instruction, X86Memory, @@ -10,9 +15,6 @@ use crate::{ }, Error, Result, }; -use iced_x86::{Decoder, DecoderOptions, Instruction, Mnemonic, OpKind, Register}; -use rustc_hash::FxHashSet; -use std::collections::VecDeque; /// Decode x86/x64 bytes into our instruction representation. /// @@ -914,12 +916,22 @@ fn convert_memory_operand(instr: &Instruction, _index: u32) -> Result #[allow(clippy::cast_possible_truncation)] let size = instr.memory_size().size() as u8; + // `segment_prefix()` is the *explicit* override and is `Register::None` + // otherwise — which is what we want. `memory_segment()` would resolve the + // default (`ds`, or `ss` for an rbp/rsp base) and make every ordinary + // access look segment-qualified. + let segment = match instr.segment_prefix() { + Register::None => None, + reg => Some(convert_register(reg)?), + }; + Ok(X86Memory { base, index, scale, displacement, size, + segment, }) } @@ -1236,6 +1248,32 @@ mod tests { assert!(matches!(result[1].instruction, X86Instruction::Ret)); } + /// A segment override prefix must survive decoding, and an unprefixed + /// access must not pick up the default segment: the SSA lowering keys an + /// address space off this, and a spurious `ds:`/`ss:` there would make two + /// names for one flat cell look disjoint. + #[test] + fn test_decode_segment_prefix() { + // mov eax, fs:[0x30] / ret + let bytes = [0x64, 0xa1, 0x30, 0x00, 0x00, 0x00, 0xc3]; + let result = x86_decode_all(&bytes, 32, 0).unwrap(); + let X86Instruction::Mov { src, .. } = &result[0].instruction else { + panic!("Expected Mov instruction"); + }; + assert_eq!( + src.as_memory().and_then(|m| m.segment), + Some(X86Register::Fs) + ); + + // mov eax, [ebp-4] / ret — defaults to ss:, which must stay `None`. + let bytes = [0x8b, 0x45, 0xfc, 0xc3]; + let result = x86_decode_all(&bytes, 32, 0).unwrap(); + let X86Instruction::Mov { src, .. } = &result[0].instruction else { + panic!("Expected Mov instruction"); + }; + assert_eq!(src.as_memory().and_then(|m| m.segment), None); + } + #[test] fn test_decode_add_reg_imm() { // add eax, 5 diff --git a/dotscope/src/analysis/x86/mod.rs b/dotscope/src/analysis/x86/mod.rs index 83e1f770..305f42ba 100644 --- a/dotscope/src/analysis/x86/mod.rs +++ b/dotscope/src/analysis/x86/mod.rs @@ -145,24 +145,25 @@ pub use decoder::{ x86_decode_all, x86_decode_single, x86_decode_traversal, x86_detect_epilogue, x86_detect_prologue, x86_native_body_size, X86TraversalDecodeResult, }; +// Re-export SSA translation types +pub use ssa::X86ToSsaTranslator; pub use types::{ X86Condition, X86DecodedInstruction, X86EdgeKind, X86EpilogueInfo, X86Instruction, X86Memory, X86Operand, X86PrologueInfo, X86PrologueKind, X86Register, }; -// Re-export SSA translation types -pub use ssa::X86ToSsaTranslator; - #[cfg(test)] mod tests { - use crate::analysis::{ - x86::{ - x86_decode_all, x86_detect_prologue, X86Function, X86Instruction, X86Operand, - X86PrologueKind, X86Register, X86ToSsaTranslator, + use crate::{ + analysis::{ + x86::{ + x86_decode_all, x86_detect_prologue, X86Function, X86Instruction, X86Operand, + X86PrologueKind, X86Register, X86ToSsaTranslator, + }, + ConstValue, SsaEvaluator, SsaOp, }, - ConstValue, SsaEvaluator, SsaOp, + metadata::typesystem::PointerSize, }; - use crate::metadata::typesystem::PointerSize; /// Test the full pipeline: decode -> build CFG #[test] diff --git a/dotscope/src/analysis/x86/ssa.rs b/dotscope/src/analysis/x86/ssa.rs index 8e858bea..22012605 100644 --- a/dotscope/src/analysis/x86/ssa.rs +++ b/dotscope/src/analysis/x86/ssa.rs @@ -63,6 +63,32 @@ use crate::{ /// Number of registers tracked (0-15 GPRs for x64, 16-21 segment registers). const MAX_REGISTERS: usize = 22; +/// Address space id for `gs:`-qualified accesses. +/// +/// The numbering follows LLVM's x86 convention (256 = `gs`, 257 = `fs`) so a +/// dump is readable against existing tooling. The values themselves are opaque +/// to analyssa — it only ever compares them for equality. +const ADDRESS_SPACE_GS: u16 = 256; + +/// Address space id for `fs:`-qualified accesses. See [`ADDRESS_SPACE_GS`]. +const ADDRESS_SPACE_FS: u16 = 257; + +/// Maps a memory operand's segment override to an SSA address space. +/// +/// Only `fs:` and `gs:` produce one. In flat 32-bit and 64-bit user mode the +/// remaining segments share a base with the unprefixed default, so reporting a +/// distinct address space for an explicit `ss:` or `ds:` would let alias +/// analysis prove two names for the *same* cell disjoint — the one direction +/// that is unsound. `fs:`/`gs:` genuinely have their own bases (TEB/PEB access, +/// stack cookies, TLS), which is exactly the distinction worth keeping. +fn address_space_for(mem: &X86Memory) -> Option { + match mem.segment { + Some(X86Register::Fs) => Some(ADDRESS_SPACE_FS), + Some(X86Register::Gs) => Some(ADDRESS_SPACE_GS), + _ => None, + } +} + /// Tracks the current SSA variable for each x86 register. /// /// This is the core of register versioning - each time a register is written, @@ -1606,6 +1632,7 @@ impl<'a> X86ToSsaTranslator<'a> { dest: result, addr, value_type, + address_space: address_space_for(mem), })); Ok(result) @@ -1633,6 +1660,7 @@ impl<'a> X86ToSsaTranslator<'a> { addr, value, value_type, + address_space: address_space_for(mem), })); Ok(()) @@ -2538,9 +2566,58 @@ fn index_to_register(index: usize, bitness: u32) -> Option { #[cfg(test)] mod tests { use super::*; - use crate::{analysis::x86::decoder::x86_decode_all, compiler::SsaCodeGenerator}; + /// `fs:`/`gs:` become distinct address spaces; every other segment stays in + /// the flat default. Marking `ss:`/`ds:` would let alias analysis prove two + /// names for one cell disjoint, which is the unsound direction. + #[test] + fn test_address_space_for_segment() { + let flat = X86Memory::base_disp(X86Register::Ebp, -4, 4); + assert_eq!(address_space_for(&flat), None); + assert_eq!( + address_space_for(&flat.clone().with_segment(X86Register::Fs)), + Some(ADDRESS_SPACE_FS) + ); + assert_eq!( + address_space_for(&flat.clone().with_segment(X86Register::Gs)), + Some(ADDRESS_SPACE_GS) + ); + for flat_segment in [ + X86Register::Cs, + X86Register::Ds, + X86Register::Es, + X86Register::Ss, + ] { + assert_eq!( + address_space_for(&flat.clone().with_segment(flat_segment)), + None, + "{flat_segment:?} shares the flat address space" + ); + } + assert_ne!(ADDRESS_SPACE_FS, ADDRESS_SPACE_GS); + } + + /// The segment override must reach the emitted `LoadIndirect`, not just the + /// decoded operand. + #[test] + fn test_translate_segment_qualified_load() { + // mov eax, fs:[0x30]; ret + let bytes = [0x64, 0xa1, 0x30, 0x00, 0x00, 0x00, 0xc3]; + let instructions = x86_decode_all(&bytes, 32, 0x1000).unwrap(); + let cfg = X86Function::new(&instructions, 32, 0x1000); + let ssa = X86ToSsaTranslator::new(&cfg).translate().unwrap(); + + let spaces: Vec> = ssa + .iter_instructions() + .filter_map(|(_, _, instr)| match instr.op() { + SsaOp::LoadIndirect { address_space, .. } => Some(*address_space), + _ => None, + }) + .collect(); + assert_eq!(spaces, vec![Some(ADDRESS_SPACE_FS)]); + } + #[test] fn test_translate_linear_code() { // pop eax; add eax, 5; ret diff --git a/dotscope/src/analysis/x86/types.rs b/dotscope/src/analysis/x86/types.rs index 4876d144..e829ae1e 100644 --- a/dotscope/src/analysis/x86/types.rs +++ b/dotscope/src/analysis/x86/types.rs @@ -294,6 +294,18 @@ pub struct X86Memory { pub displacement: i64, /// Size of the memory access in bytes (1, 2, 4, or 8). pub size: u8, + /// Explicit segment override prefix on the access, or `None` when the + /// instruction uses the default segment for its base register. + /// + /// Only the *prefix* is recorded, never the effective segment: in flat + /// 32-bit and 64-bit user mode `cs`/`ds`/`es`/`ss` all describe the same + /// address space, so treating an explicit `ss:` as distinct from an + /// unprefixed access would make two names for one cell look disjoint. + /// `fs:` and `gs:` do have their own bases, which is why they are carried + /// through to [`SsaOp::LoadIndirect`]'s address space. + /// + /// [`SsaOp::LoadIndirect`]: crate::analysis::SsaOp::LoadIndirect + pub segment: Option, } impl X86Memory { @@ -306,6 +318,7 @@ impl X86Memory { scale: 1, displacement, size, + segment: None, } } @@ -324,6 +337,7 @@ impl X86Memory { scale, displacement, size, + segment: None, } } @@ -336,8 +350,16 @@ impl X86Memory { scale: 1, displacement, size, + segment: None, } } + + /// Returns this operand qualified by an explicit segment override prefix. + #[must_use] + pub fn with_segment(mut self, segment: X86Register) -> Self { + self.segment = Some(segment); + self + } } /// Operand for an x86 instruction. diff --git a/dotscope/src/assembly/encoder.rs b/dotscope/src/assembly/encoder.rs index 42a8af99..1694fce3 100644 --- a/dotscope/src/assembly/encoder.rs +++ b/dotscope/src/assembly/encoder.rs @@ -51,6 +51,11 @@ //! # Ok::<(), dotscope::Error>(()) //! ``` +use std::{ + collections::{HashMap, HashSet}, + sync::OnceLock, +}; + use crate::{ assembly::{ instruction::{FlowType, Immediate, Instruction, Operand, OperandType}, @@ -58,10 +63,6 @@ use crate::{ }, Error, Result, }; -use std::{ - collections::{HashMap, HashSet}, - sync::OnceLock, -}; /// Reverse lookup table mapping mnemonics to opcode information. /// diff --git a/dotscope/src/assembly/instruction.rs b/dotscope/src/assembly/instruction.rs index 58303bb3..a89a20c5 100644 --- a/dotscope/src/assembly/instruction.rs +++ b/dotscope/src/assembly/instruction.rs @@ -47,9 +47,10 @@ //! - [`crate::assembly::block`] - Uses instructions to build basic block sequences //! - [`crate::metadata::token`] - References metadata tokens in operands -use crate::metadata::token::Token; use std::fmt::{self, Display, UpperHex}; +use crate::metadata::token::Token; + /// Types of operands for CIL instructions. /// /// This enum defines the different types of operands that CIL instructions can accept. diff --git a/dotscope/src/cilassembly/builders/class.rs b/dotscope/src/cilassembly/builders/class.rs index d840127e..a5ce138d 100644 --- a/dotscope/src/cilassembly/builders/class.rs +++ b/dotscope/src/cilassembly/builders/class.rs @@ -680,12 +680,13 @@ impl Default for ClassBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{changes::ChangeRefKind, CilAssembly}, metadata::{cilassemblyview::CilAssemblyView, signatures::TypeSignature, tables::TableId}, }; - use std::path::PathBuf; fn get_test_assembly() -> Result { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/samples/WindowsBase.dll"); diff --git a/dotscope/src/cilassembly/builders/enums.rs b/dotscope/src/cilassembly/builders/enums.rs index 08e8c32c..866cd4ee 100644 --- a/dotscope/src/cilassembly/builders/enums.rs +++ b/dotscope/src/cilassembly/builders/enums.rs @@ -441,12 +441,13 @@ impl Default for EnumBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{changes::ChangeRefKind, CilAssembly}, metadata::{cilassemblyview::CilAssemblyView, signatures::TypeSignature, tables::TableId}, }; - use std::path::PathBuf; fn get_test_assembly() -> Result { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/samples/WindowsBase.dll"); diff --git a/dotscope/src/cilassembly/builders/event.rs b/dotscope/src/cilassembly/builders/event.rs index db725396..e900389e 100644 --- a/dotscope/src/cilassembly/builders/event.rs +++ b/dotscope/src/cilassembly/builders/event.rs @@ -660,12 +660,13 @@ impl Default for EventBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{changes::ChangeRefKind, CilAssembly}, metadata::{cilassemblyview::CilAssemblyView, signatures::TypeSignature, tables::TableId}, }; - use std::path::PathBuf; fn get_test_assembly() -> Result { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/samples/WindowsBase.dll"); diff --git a/dotscope/src/cilassembly/builders/interface.rs b/dotscope/src/cilassembly/builders/interface.rs index e53057f3..552c3d68 100644 --- a/dotscope/src/cilassembly/builders/interface.rs +++ b/dotscope/src/cilassembly/builders/interface.rs @@ -618,12 +618,13 @@ impl Default for InterfaceBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{ChangeRefKind, CilAssembly}, metadata::{cilassemblyview::CilAssemblyView, signatures::TypeSignature, tables::TableId}, }; - use std::path::PathBuf; fn get_test_assembly() -> Result { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/samples/WindowsBase.dll"); diff --git a/dotscope/src/cilassembly/builders/method.rs b/dotscope/src/cilassembly/builders/method.rs index 4ef774d1..e89242fa 100644 --- a/dotscope/src/cilassembly/builders/method.rs +++ b/dotscope/src/cilassembly/builders/method.rs @@ -891,12 +891,13 @@ impl Default for MethodBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{ChangeRefKind, CilAssembly}, metadata::{cilassemblyview::CilAssemblyView, signatures::TypeSignature, tables::TableId}, }; - use std::path::PathBuf; fn get_test_assembly() -> Result { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/samples/WindowsBase.dll"); diff --git a/dotscope/src/cilassembly/builders/method_body.rs b/dotscope/src/cilassembly/builders/method_body.rs index d59ba6bd..53718a5b 100644 --- a/dotscope/src/cilassembly/builders/method_body.rs +++ b/dotscope/src/cilassembly/builders/method_body.rs @@ -902,11 +902,11 @@ impl Default for MethodBodyBuilder { #[cfg(test)] mod tests { - use super::*; - use crate::cilassembly::CilAssembly; - use crate::metadata::cilassemblyview::CilAssemblyView; use std::path::PathBuf; + use super::*; + use crate::{cilassembly::CilAssembly, metadata::cilassemblyview::CilAssemblyView}; + fn get_test_assembly() -> Result { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/samples/WindowsBase.dll"); let view = CilAssemblyView::from_path(&path)?; diff --git a/dotscope/src/cilassembly/builders/property.rs b/dotscope/src/cilassembly/builders/property.rs index 585fb60d..0dd68197 100644 --- a/dotscope/src/cilassembly/builders/property.rs +++ b/dotscope/src/cilassembly/builders/property.rs @@ -781,12 +781,13 @@ impl Default for PropertyBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{ChangeRefKind, CilAssembly}, metadata::{cilassemblyview::CilAssemblyView, signatures::TypeSignature, tables::TableId}, }; - use std::path::PathBuf; fn get_test_assembly() -> Result { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/samples/WindowsBase.dll"); diff --git a/dotscope/src/cilassembly/changes/changeref.rs b/dotscope/src/cilassembly/changes/changeref.rs index 0fb28d31..bcbf53b8 100644 --- a/dotscope/src/cilassembly/changes/changeref.rs +++ b/dotscope/src/cilassembly/changes/changeref.rs @@ -49,14 +49,13 @@ use std::sync::{ Arc, }; +// Re-export hash functions from utils for backwards compatibility +pub use crate::utils::{hash_blob, hash_guid, hash_string}; use crate::{ metadata::{tables::TableId, token::Token}, Error, Result, }; -// Re-export hash functions from utils for backwards compatibility -pub use crate::utils::{hash_blob, hash_guid, hash_string}; - /// Counter for generating unique IDs static NEXT_CHANGE_ID: AtomicU64 = AtomicU64::new(1); @@ -539,9 +538,10 @@ impl ChangeRef { #[cfg(test)] mod tests { - use super::*; use std::thread; + use super::*; + #[test] fn test_changeref_heap_creation() { let ref1 = ChangeRef::new_string(12345); diff --git a/dotscope/src/cilassembly/cleanup/analysis.rs b/dotscope/src/cilassembly/cleanup/analysis.rs index 8a2f1b73..3c567aca 100644 --- a/dotscope/src/cilassembly/cleanup/analysis.rs +++ b/dotscope/src/cilassembly/cleanup/analysis.rs @@ -79,8 +79,12 @@ pub fn expand_type_tokens(request: &CleanupRequest, assembly: &CilObject) -> Has /// in the provided method call graph. /// /// These are self-contained infrastructure types that can be safely deleted. -/// A type is considered unreferenced when no method outside the type (and -/// not already scheduled for deletion) calls any of its methods. +/// Reachability is computed transitively: candidate types are rooted at every +/// live non-candidate type, and a candidate reached from a root is itself +/// promoted to a root. A type is considered unreferenced only when no chain of +/// calls from live code arrives at it. Stopping after a single step would +/// delete clusters that are reachable solely through another candidate — a VM +/// dispatcher or decryptor facade that turns out to be live. /// /// This handles two key scenarios: /// - **Proxy devirtualization**: wrapper types whose methods are @@ -175,9 +179,11 @@ pub fn find_unreferenced_types( } } - // Compute which candidates have live external callers - let mut has_external_caller: HashSet = HashSet::new(); - + // Collapse the method call graph to type granularity, keeping only edges + // whose caller is still live. Doing this once lets reachability be answered + // with a single worklist drain instead of re-scanning every call edge per + // round. + let mut type_calls: HashMap> = HashMap::new(); for (caller_token, callees) in method_call_graph { // Skip callers that are deleted if deleted_methods.contains(caller_token) { @@ -190,35 +196,67 @@ pub fn find_unreferenced_types( if deleted_types.contains(&caller_type) { continue; } - // Skip callers that are themselves candidates — their edges are - // intra-cluster and don't constitute external references - let caller_is_candidate = candidates.contains(&caller_type); for callee_token in callees { let Some(callee_type) = method_to_type.get(callee_token).copied() else { continue; }; + // Intra-type calls say nothing about whether the type is reachable if caller_type == callee_type { continue; } - // Only mark as externally referenced if the caller is NOT a candidate - if !caller_is_candidate && candidates.contains(&callee_type) { - has_external_caller.insert(callee_type); + type_calls + .entry(caller_type) + .or_default() + .insert(callee_type); + } + } + + // A nested type cannot outlive its enclosing type: deleting an enclosing + // type cascades to its nested types via [`expand_type_tokens`]. Model that + // as an edge from nested to enclosing so a live nested type keeps its + // parent alive, rather than being silently dropped along with it. + for type_entry in registry.iter() { + let type_token = *type_entry.key(); + if type_token.table() != 0x02 { + continue; + } + // A type already scheduled for deletion is not live and must never + // become a reachability root + if deleted_types.contains(&type_token) { + continue; + } + if let Some(enclosing) = type_entry.value().enclosing_type() { + if enclosing.token != type_token { + type_calls + .entry(type_token) + .or_default() + .insert(enclosing.token); } } } + // Seed the roots: every live type that is not itself a candidate. Candidacy + // is what reachability has to resolve, so a candidate cannot serve as proof + // that another candidate is referenced. + let mut worklist: Vec = type_calls + .keys() + .filter(|type_token| !candidates.contains(type_token)) + .copied() + .collect(); + // CustomAttribute constructors reference types that the call graph misses. // Types like EmbeddedAttribute and RefSafetyRulesAttribute are only referenced // from CustomAttribute rows (their .ctors are attribute constructors), never // from IL code. Without this check they appear unreferenced and get deleted. + // They join the roots so that whatever they call stays alive as well. if let Some(tables) = assembly.tables() { if let Some(attr_table) = tables.table::() { for row in attr_table { if row.constructor.token.is_table(TableId::MethodDef) { if let Some(&ctor_type) = method_to_type.get(&row.constructor.token) { - if candidates.contains(&ctor_type) { - has_external_caller.insert(ctor_type); + if candidates.remove(&ctor_type) { + worklist.push(ctor_type); } } } @@ -226,11 +264,24 @@ pub fn find_unreferenced_types( } } - // All candidates without external callers are unreferenced infrastructure - candidates - .into_iter() - .filter(|t| !has_external_caller.contains(t)) - .collect() + // Propagate liveness transitively. A candidate rescued here becomes a root + // in turn — without that, a cluster reachable only through another candidate + // (a VM dispatcher, a decryptor facade) is misread as isolated infrastructure. + while let Some(live_type) = worklist.pop() { + let Some(callees) = type_calls.get(&live_type) else { + continue; + }; + for callee_type in callees { + if candidates.remove(callee_type) { + worklist.push(*callee_type); + } + } + } + + // Whatever is still a candidate is unreachable from live code + let mut unreferenced: Vec = candidates.into_iter().collect(); + unreferenced.sort_unstable(); + unreferenced } /// Pre-computes the set of entry-point method tokens that should not be removed. diff --git a/dotscope/src/cilassembly/cleanup/orphans.rs b/dotscope/src/cilassembly/cleanup/orphans.rs index 225a979f..f3b47c80 100644 --- a/dotscope/src/cilassembly/cleanup/orphans.rs +++ b/dotscope/src/cilassembly/cleanup/orphans.rs @@ -22,11 +22,10 @@ use std::collections::{BTreeSet, HashSet}; use crate::{ cilassembly::{ cleanup::{ - references::PreDeletionRefs, references::{ collect_referenced_standalonesig_rids, collect_typerefs_from_deleted_memberref_sigs, remove_unreferenced_memberrefs, - remove_unreferenced_typerefs, remove_unreferenced_typespecs, + remove_unreferenced_typerefs, remove_unreferenced_typespecs, PreDeletionRefs, }, utils::{list_range, remove_candidates_not_alive, try_remove}, CleanupStats, diff --git a/dotscope/src/cilassembly/mod.rs b/dotscope/src/cilassembly/mod.rs index 3b52191f..c7088cda 100644 --- a/dotscope/src/cilassembly/mod.rs +++ b/dotscope/src/cilassembly/mod.rs @@ -172,9 +172,8 @@ pub use modifications::TableModifications; pub use operation::{Operation, TableOperation}; pub use resolver::LastWriteWinsResolver; pub use writer::GeneratorConfig; -pub(crate) use writer::ResolvePlaceholders; - use writer::PeGenerator; +pub(crate) use writer::ResolvePlaceholders; /// A mutable view of a .NET assembly that tracks changes for editing operations. /// @@ -1870,8 +1869,7 @@ mod tests { use std::path::PathBuf; use super::*; - use crate::test::factories::table::cilassembly::create_test_typedef_row; - use crate::Error; + use crate::{test::factories::table::cilassembly::create_test_typedef_row, Error}; #[test] fn test_convert_from_view() { @@ -1994,14 +1992,16 @@ mod tests { /// Mono runtime compatibility tests for assembly modification and execution mod mono_tests { use super::*; - use crate::metadata::signatures::{ - encode_method_signature, SignatureMethod, SignatureParameter, TypeSignature, - }; - use crate::metadata::tables::{ - CodedIndex, CodedIndexType, MemberRefBuilder, TableId, TypeRefBuilder, + use crate::{ + metadata::{ + signatures::{ + encode_method_signature, SignatureMethod, SignatureParameter, TypeSignature, + }, + tables::{CodedIndex, CodedIndexType, MemberRefBuilder, TableId, TypeRefBuilder}, + token::Token, + }, + test::mono::*, }; - use crate::metadata::token::Token; - use crate::test::mono::*; #[test] fn test_mono_runtime_compatibility() -> Result<()> { diff --git a/dotscope/src/cilassembly/modifications.rs b/dotscope/src/cilassembly/modifications.rs index 60bc8dd8..cf2b4656 100644 --- a/dotscope/src/cilassembly/modifications.rs +++ b/dotscope/src/cilassembly/modifications.rs @@ -595,9 +595,13 @@ impl TableModifications { #[cfg(test)] mod tests { use super::*; - use crate::cilassembly::{Operation, TableOperation}; - use crate::metadata::tables::{ModuleRaw, TableDataOwned}; - use crate::metadata::token::Token; + use crate::{ + cilassembly::{Operation, TableOperation}, + metadata::{ + tables::{ModuleRaw, TableDataOwned}, + token::Token, + }, + }; /// Helper to create a simple ModuleRaw for testing fn make_test_row(name_idx: u32) -> TableDataOwned { diff --git a/dotscope/src/cilassembly/operation.rs b/dotscope/src/cilassembly/operation.rs index 76b4b35b..e0d117f9 100644 --- a/dotscope/src/cilassembly/operation.rs +++ b/dotscope/src/cilassembly/operation.rs @@ -59,9 +59,10 @@ //! - Assembly validation - Operation validation and conflict detection //! - [`crate::metadata::tables`] - Table data structures and row types -use crate::metadata::tables::TableDataOwned; use std::time::{SystemTime, UNIX_EPOCH}; +use crate::metadata::tables::TableDataOwned; + /// Specific operation types that can be applied to table rows. /// /// This enum defines the three fundamental operations supported by the assembly modification diff --git a/dotscope/src/cilassembly/resolver.rs b/dotscope/src/cilassembly/resolver.rs index 6d01e214..4e852246 100644 --- a/dotscope/src/cilassembly/resolver.rs +++ b/dotscope/src/cilassembly/resolver.rs @@ -43,9 +43,10 @@ //! This type is [`Send`] and [`Sync`] as it contains no mutable state and operates //! purely on the input data. -use crate::{cilassembly::TableOperation, Error, Result}; use std::collections::HashMap; +use crate::{cilassembly::TableOperation, Error, Result}; + /// Trait for conflict resolution strategies. /// /// Different applications may need different conflict resolution strategies: diff --git a/dotscope/src/cilassembly/writer/fixups.rs b/dotscope/src/cilassembly/writer/fixups.rs index 79d612aa..8b3e4a78 100644 --- a/dotscope/src/cilassembly/writer/fixups.rs +++ b/dotscope/src/cilassembly/writer/fixups.rs @@ -877,13 +877,16 @@ fn calculate_pe_checksum(output: &Output, checksum_offset: u64, actual_size: usi #[cfg(test)] mod tests { - use super::*; - use crate::cilassembly::writer::generator::PeGenerator; - use crate::cilassembly::CilAssembly; - use crate::CilAssemblyView; use std::path::Path; + use tempfile::NamedTempFile; + use super::*; + use crate::{ + cilassembly::{writer::generator::PeGenerator, CilAssembly}, + CilAssemblyView, + }; + #[test] fn test_checksum_excludes_checksum_field() { // Create a small test output diff --git a/dotscope/src/cilassembly/writer/generator.rs b/dotscope/src/cilassembly/writer/generator.rs index d58bc025..ee491995 100644 --- a/dotscope/src/cilassembly/writer/generator.rs +++ b/dotscope/src/cilassembly/writer/generator.rs @@ -30,6 +30,16 @@ //! - ECMA-335 §II.25 - File format extensions to PE //! - [PE Format Specification](https://docs.microsoft.com/en-us/windows/win32/debug/pe-format) +use std::{ + collections::{HashMap, HashSet}, + io::Write, + path::Path, + sync::Arc, +}; + +use log::debug; +use strum::IntoEnumIterator; + #[cfg(feature = "x86")] use crate::analysis::x86_native_body_size; use crate::{ @@ -71,14 +81,6 @@ use crate::{ utils::{align_to, calculate_table_row_size}, Error, Result, }; -use log::debug; -use std::{ - collections::{HashMap, HashSet}, - io::Write, - path::Path, - sync::Arc, -}; -use strum::IntoEnumIterator; /// IAT (Import Address Table) size for .NET executables (8 bytes). const IAT_SIZE: u64 = 8; @@ -3007,12 +3009,13 @@ impl<'a> PeGenerator<'a> { #[cfg(test)] mod tests { + use tempfile::NamedTempFile; + use crate::{ cilassembly::{writer::generator::PeGenerator, CilAssembly}, metadata::{signatures::TypeSignature, tables::TableId}, CilAssemblyView, MethodBuilder, }; - use tempfile::NamedTempFile; #[test] fn test_pe_generator_basic() { diff --git a/dotscope/src/cilassembly/writer/heaps/mod.rs b/dotscope/src/cilassembly/writer/heaps/mod.rs index ba328782..23699287 100644 --- a/dotscope/src/cilassembly/writer/heaps/mod.rs +++ b/dotscope/src/cilassembly/writer/heaps/mod.rs @@ -38,17 +38,16 @@ mod rowpatch; mod streaming; // Streaming writers (primary API) +use std::collections::HashMap; + +// Row patching utilities +pub(crate) use rowpatch::patch_row_heap_refs; pub use streaming::{ compute_blob_heap_offsets, compute_guid_heap_offsets, compute_strings_heap_offsets, compute_userstring_heap_offsets, stream_blob_heap, stream_guid_heap, stream_strings_heap, stream_userstring_heap, }; -// Row patching utilities -pub(crate) use rowpatch::patch_row_heap_refs; - -use std::collections::HashMap; - use crate::{ cilassembly::{changes::AssemblyChanges, writer::context::WriteContext}, CilAssemblyView, Result, diff --git a/dotscope/src/cilassembly/writer/heaps/streaming.rs b/dotscope/src/cilassembly/writer/heaps/streaming.rs index c44fba98..9b40ba4a 100644 --- a/dotscope/src/cilassembly/writer/heaps/streaming.rs +++ b/dotscope/src/cilassembly/writer/heaps/streaming.rs @@ -1114,9 +1114,10 @@ fn write_userstring_entry(output: &mut Output, pos: u64, s: &str) -> Result<()> #[cfg(test)] mod tests { - use super::*; use tempfile::NamedTempFile; + use super::*; + #[test] fn test_stream_strings_heap_empty() { let temp_file = NamedTempFile::new().unwrap(); diff --git a/dotscope/src/cilassembly/writer/methods.rs b/dotscope/src/cilassembly/writer/methods.rs index 22d42c3a..bc5996e9 100644 --- a/dotscope/src/cilassembly/writer/methods.rs +++ b/dotscope/src/cilassembly/writer/methods.rs @@ -284,11 +284,12 @@ pub fn rebuild_method_body( #[cfg(test)] mod tests { - use super::*; - use std::fs::File; - use std::io::Read; + use std::{fs::File, io::Read}; + use tempfile::tempdir; + use super::*; + #[test] fn test_write_tiny_method() { let temp_dir = tempdir().unwrap(); diff --git a/dotscope/src/cilassembly/writer/mod.rs b/dotscope/src/cilassembly/writer/mod.rs index 205f8e3e..67afe340 100644 --- a/dotscope/src/cilassembly/writer/mod.rs +++ b/dotscope/src/cilassembly/writer/mod.rs @@ -60,13 +60,11 @@ mod sizes; mod tables; // Re-export for use by other crate modules -pub(crate) use tables::ResolvePlaceholders; - -// Internal use by CilAssembly -pub(super) use generator::PeGenerator; - // Public re-exports pub use generator::GeneratorConfig; +// Internal use by CilAssembly +pub(super) use generator::PeGenerator; +pub(crate) use tables::ResolvePlaceholders; #[cfg(test)] mod tests { diff --git a/dotscope/src/cilassembly/writer/output.rs b/dotscope/src/cilassembly/writer/output.rs index 39d7ab95..a48cd369 100644 --- a/dotscope/src/cilassembly/writer/output.rs +++ b/dotscope/src/cilassembly/writer/output.rs @@ -1032,14 +1032,16 @@ impl Output { #[cfg(test)] mod tests { - use super::*; - use crate::file::pe::DosHeader; use std::{ fs::File, io::{Read, Write}, }; + use tempfile::tempdir; + use super::*; + use crate::file::pe::DosHeader; + #[test] fn test_mmap_file_creation() { let temp_dir = tempdir().unwrap(); diff --git a/dotscope/src/cilassembly/writer/sizes.rs b/dotscope/src/cilassembly/writer/sizes.rs index d266c290..d9a0d903 100644 --- a/dotscope/src/cilassembly/writer/sizes.rs +++ b/dotscope/src/cilassembly/writer/sizes.rs @@ -81,9 +81,10 @@ pub fn calculate_table_stream_expansion(assembly: &CilAssembly) -> Result { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::prelude::*; - use std::path::PathBuf; fn get_test_assembly() -> Result { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/samples/WindowsBase.dll"); diff --git a/dotscope/src/cilassembly/writer/tables.rs b/dotscope/src/cilassembly/writer/tables.rs index 8a4433e3..c8cabdaa 100644 --- a/dotscope/src/cilassembly/writer/tables.rs +++ b/dotscope/src/cilassembly/writer/tables.rs @@ -655,8 +655,10 @@ mod tests { use super::*; use crate::{ cilassembly::changes::{AssemblyChanges, HeapChanges}, - metadata::tables::{CodedIndex, CodedIndexType}, - metadata::token::Token, + metadata::{ + tables::{CodedIndex, CodedIndexType}, + token::Token, + }, }; #[test] diff --git a/dotscope/src/compiler/codegen/coalescing.rs b/dotscope/src/compiler/codegen/coalescing.rs index c03c2f25..40472bf1 100644 --- a/dotscope/src/compiler/codegen/coalescing.rs +++ b/dotscope/src/compiler/codegen/coalescing.rs @@ -31,9 +31,8 @@ use std::{ collections::{BTreeMap, BinaryHeap}, }; -use rayon::prelude::*; - use analyssa::BitSet; +use rayon::prelude::*; use crate::{ analysis::{ @@ -306,7 +305,12 @@ impl LocalCoalescer { .iter() .filter_map(|v| match v.origin() { VariableOrigin::Phi => Some(v.id()), - VariableOrigin::Argument(_) | VariableOrigin::Local(_) => None, + // An entry live-in is caller-supplied like an argument, so it + // keeps fixed storage rather than being freely coalescable. + // Unreachable from the CIL front end, which never builds one. + VariableOrigin::Argument(_) + | VariableOrigin::Local(_) + | VariableOrigin::EntryLiveIn => None, }) .collect(); @@ -994,10 +998,9 @@ fn types_compatible(t1: Option<&SsaType>, t2: Option<&SsaType>) -> bool { #[cfg(test)] mod tests { - use super::*; - use std::collections::BTreeSet; + use super::*; use crate::analysis::{SsaFunctionBuilder, SsaType, SsaVarId}; /// Helper to create N unique SsaVarIds diff --git a/dotscope/src/compiler/codegen/mod.rs b/dotscope/src/compiler/codegen/mod.rs index 941977a6..75db38f1 100644 --- a/dotscope/src/compiler/codegen/mod.rs +++ b/dotscope/src/compiler/codegen/mod.rs @@ -2152,7 +2152,9 @@ impl SsaCodeGenerator { // If not in the compacted mapping, fall through to allocate // (this handles edge cases where a Local var wasn't seen during coalescing) } - VariableOrigin::Phi => { + // `EntryLiveIn` is unreachable here: it is produced only by + // machine-code front ends, and a CIL method has no such storage. + VariableOrigin::Phi | VariableOrigin::EntryLiveIn => { // Fall through to allocate a new local } } @@ -4303,11 +4305,13 @@ impl SsaCodeGenerator { dest_addr, value, size, + reverse: _, } => vec![*dest_addr, *value, *size], SsaOp::CopyBlk { dest_addr, src_addr, size, + reverse: _, } => vec![*dest_addr, *src_addr, *size], // Exception handling diff --git a/dotscope/src/compiler/host.rs b/dotscope/src/compiler/host.rs index b7b9c81d..d978cc24 100644 --- a/dotscope/src/compiler/host.rs +++ b/dotscope/src/compiler/host.rs @@ -13,7 +13,7 @@ use std::sync::Arc; use analyssa::{ - events::EventLog, + events::EventListener, host::{DirtySet, SsaStore}, ir::function::SsaFunction, scheduling::SsaPassHost, @@ -81,12 +81,12 @@ impl World for CompilerContext { self.dead_methods.insert(method.0); } - fn methods_reverse_topological(&self) -> Vec { - CompilerContext::methods_reverse_topological(self) - .into_iter() - .map(MethodRef::new) - .collect() - } + // `methods_reverse_topological` is deliberately not overridden: analyssa's + // default derives the call graph's strongly-connected components from + // `callees`, which groups mutually recursive methods together. The inherent + // `CompilerContext::methods_reverse_topological` returns a flat order that + // cannot express that grouping, so overriding with it would lose + // information the interprocedural driver needs. } impl SsaStore for CompilerContext { @@ -145,7 +145,7 @@ impl DirtySet for CompilerContext { } impl SsaPassHost for CompilerContext { - fn events(&self) -> &EventLog { + fn events(&self) -> &dyn EventListener { &self.events } diff --git a/dotscope/src/compiler/mod.rs b/dotscope/src/compiler/mod.rs index 06ee592a..2332496b 100644 --- a/dotscope/src/compiler/mod.rs +++ b/dotscope/src/compiler/mod.rs @@ -57,12 +57,12 @@ mod scheduler; mod state; mod summary; -use crate::analysis::CilTarget; - pub use analyssa::events::{DerivedStats, EventKind, EventListener, NullListener}; pub use codegen::{CompilationResult, SsaCodeGenerator}; pub use context::CompilerContext; +use crate::analysis::CilTarget; + /// CIL-defaulted alias of [`analyssa::events::Event`]. pub type Event = analyssa::events::Event; /// CIL-defaulted alias of [`analyssa::events::EventLog`]. @@ -79,8 +79,8 @@ pub use passes::{ AlgebraicSimplificationPass, BlockMergingPass, ConstantPropagationPass, ControlFlowSimplificationPass, CopyPropagationPass, DeadCodeEliminationPass, DeadMethodEliminationPass, GlobalValueNumberingPass, InliningPass, JumpThreadingPass, LicmPass, - LoopCanonicalizationPass, OpaquePredicatePass, PredicateResult, ProxyDevirtualizationPass, - ReassociationPass, StrengthReductionPass, ValueRangePropagationPass, + LoopCanonicalizationPass, MemoryOptimizationPass, OpaquePredicatePass, PredicateResult, + ProxyDevirtualizationPass, ReassociationPass, StrengthReductionPass, ValueRangePropagationPass, }; pub use scheduler::PassScheduler; pub use state::ProcessingState; diff --git a/dotscope/src/compiler/passes/constants/mod.rs b/dotscope/src/compiler/passes/constants/mod.rs index 44b44d5f..ded14f1e 100644 --- a/dotscope/src/compiler/passes/constants/mod.rs +++ b/dotscope/src/compiler/passes/constants/mod.rs @@ -301,7 +301,18 @@ impl ConstantPropagationPass { for (block_idx, block) in ssa.iter_blocks() { for (instr_idx, instr) in block.instructions().iter().enumerate() { let op = instr.op(); - if let Some(result) = Self::check_algebraic_identity(op, constants) { + // The *operand* type decides whether a self-cancelling identity + // holds (`x - x` is NaN for a NaN float) and at what width its + // constant should be materialised. A comparison's destination is + // a boolean, so the destination type would answer neither. + let operand_type = op + .uses() + .first() + .and_then(|operand| ssa.variable(*operand)) + .map(|var| var.var_type().clone()); + if let Some(result) = + Self::check_algebraic_identity(op, constants, operand_type.as_ref()) + { transformations.push((block_idx, instr_idx, result)); } } @@ -359,9 +370,10 @@ impl ConstantPropagationPass { fn check_algebraic_identity( op: &SsaOp, constants: &BTreeMap, + operand_type: Option<&SsaType>, ) -> Option { let dest = op.dest()?; - match simplify_op(op, constants) { + match simplify_op(op, constants, operand_type) { SimplifyResult::Constant(value) => Some(AlgebraicResult::Constant { dest, value }), SimplifyResult::Copy(src) => Some(AlgebraicResult::Copy { dest, src }), SimplifyResult::None => None, diff --git a/dotscope/src/compiler/passes/constants/tests.rs b/dotscope/src/compiler/passes/constants/tests.rs index 108b6e46..be4171ca 100644 --- a/dotscope/src/compiler/passes/constants/tests.rs +++ b/dotscope/src/compiler/passes/constants/tests.rs @@ -195,7 +195,8 @@ fn test_identity_add_zero() { flags: None, }; - let result = ConstantPropagationPass::check_algebraic_identity(&op, &constants); + let result = + ConstantPropagationPass::check_algebraic_identity(&op, &constants, Some(&SsaType::I32)); // Result should be a Copy to v0 (since v0 has value 42, not a zero) match result { Some(AlgebraicResult::Copy { dest, src }) => { @@ -222,7 +223,8 @@ fn test_identity_mul_one() { flags: None, }; - let result = ConstantPropagationPass::check_algebraic_identity(&op, &constants); + let result = + ConstantPropagationPass::check_algebraic_identity(&op, &constants, Some(&SsaType::I32)); match result { Some(AlgebraicResult::Copy { dest, src }) => { assert_eq!(dest, v2); @@ -248,7 +250,8 @@ fn test_identity_and_minus_one() { flags: None, }; - let result = ConstantPropagationPass::check_algebraic_identity(&op, &constants); + let result = + ConstantPropagationPass::check_algebraic_identity(&op, &constants, Some(&SsaType::I32)); match result { Some(AlgebraicResult::Copy { dest, src }) => { assert_eq!(dest, v2); @@ -274,7 +277,8 @@ fn test_absorbing_mul_zero() { flags: None, }; - let result = ConstantPropagationPass::check_algebraic_identity(&op, &constants); + let result = + ConstantPropagationPass::check_algebraic_identity(&op, &constants, Some(&SsaType::I32)); match result { Some(AlgebraicResult::Constant { dest, value }) => { assert_eq!(dest, v2); @@ -300,7 +304,8 @@ fn test_absorbing_and_zero() { flags: None, }; - let result = ConstantPropagationPass::check_algebraic_identity(&op, &constants); + let result = + ConstantPropagationPass::check_algebraic_identity(&op, &constants, Some(&SsaType::I32)); match result { Some(AlgebraicResult::Constant { dest, value }) => { assert_eq!(dest, v2); @@ -326,7 +331,8 @@ fn test_absorbing_or_minus_one() { flags: None, }; - let result = ConstantPropagationPass::check_algebraic_identity(&op, &constants); + let result = + ConstantPropagationPass::check_algebraic_identity(&op, &constants, Some(&SsaType::I32)); match result { Some(AlgebraicResult::Constant { dest, value }) => { assert_eq!(dest, v2); @@ -532,7 +538,8 @@ fn test_identity_shl_zero() { flags: None, }; - let result = ConstantPropagationPass::check_algebraic_identity(&op, &constants); + let result = + ConstantPropagationPass::check_algebraic_identity(&op, &constants, Some(&SsaType::I32)); match result { Some(AlgebraicResult::Copy { dest, src }) => { assert_eq!(dest, v2); @@ -559,7 +566,8 @@ fn test_identity_shr_zero() { flags: None, }; - let result = ConstantPropagationPass::check_algebraic_identity(&op, &constants); + let result = + ConstantPropagationPass::check_algebraic_identity(&op, &constants, Some(&SsaType::I32)); match result { Some(AlgebraicResult::Copy { dest, src }) => { assert_eq!(dest, v2); @@ -585,7 +593,8 @@ fn test_identity_xor_zero() { flags: None, }; - let result = ConstantPropagationPass::check_algebraic_identity(&op, &constants); + let result = + ConstantPropagationPass::check_algebraic_identity(&op, &constants, Some(&SsaType::I32)); match result { Some(AlgebraicResult::Copy { dest, src }) => { assert_eq!(dest, v2); @@ -612,7 +621,8 @@ fn test_identity_div_one() { flags: None, }; - let result = ConstantPropagationPass::check_algebraic_identity(&op, &constants); + let result = + ConstantPropagationPass::check_algebraic_identity(&op, &constants, Some(&SsaType::I32)); match result { Some(AlgebraicResult::Copy { dest, src }) => { assert_eq!(dest, v2); @@ -638,7 +648,8 @@ fn test_identity_sub_zero() { flags: None, }; - let result = ConstantPropagationPass::check_algebraic_identity(&op, &constants); + let result = + ConstantPropagationPass::check_algebraic_identity(&op, &constants, Some(&SsaType::I32)); match result { Some(AlgebraicResult::Copy { dest, src }) => { assert_eq!(dest, v2); diff --git a/dotscope/src/compiler/passes/mod.rs b/dotscope/src/compiler/passes/mod.rs index ba0bb4a7..26cd1d1d 100644 --- a/dotscope/src/compiler/passes/mod.rs +++ b/dotscope/src/compiler/passes/mod.rs @@ -21,6 +21,7 @@ //! | [`ConstantPropagationPass`] | Propagates and folds constant values using SCCP | //! | [`GlobalValueNumberingPass`] | Eliminates redundant computations via value numbering | //! | [`CopyPropagationPass`] | Eliminates redundant copy operations and phi nodes | +//! | [`MemoryOptimizationPass`] | Forwards stores to loads, drops redundant loads, removes dead stores | //! | [`StrengthReductionPass`] | Replaces expensive operations with cheaper equivalents | //! | [`AlgebraicSimplificationPass`] | Simplifies algebraic expressions (x + 0 → x, etc.) | //! | [`ReassociationPass`] | Reorders associative operations for optimization | @@ -90,8 +91,8 @@ mod strength; pub use analyssa::passes::{ AlgebraicSimplificationPass, BlockMergingPass, ControlFlowSimplificationPass, DeadCodeEliminationPass, GlobalValueNumberingPass, JumpThreadingPass, LicmPass, - LoopCanonicalizationPass, OpaquePredicatePass, PredicateResult, ReassociationPass, - ValueRangePropagationPass, + LoopCanonicalizationPass, MemoryOptimizationPass, OpaquePredicatePass, PredicateResult, + ReassociationPass, ValueRangePropagationPass, }; // CIL-specific pass impls remain dotscope-side. `copying` and `strength` @@ -100,8 +101,7 @@ pub use analyssa::passes::{ // `DeadMethodEliminationPass`; `constants`, `inlining`, `proxy` are // CIL-specific in their entirety. pub use self::constants::ConstantPropagationPass; -pub use self::copying::CopyPropagationPass; -pub use self::deadcode::DeadMethodEliminationPass; -pub use self::inlining::InliningPass; -pub use self::proxy::ProxyDevirtualizationPass; -pub use self::strength::StrengthReductionPass; +pub use self::{ + copying::CopyPropagationPass, deadcode::DeadMethodEliminationPass, inlining::InliningPass, + proxy::ProxyDevirtualizationPass, strength::StrengthReductionPass, +}; diff --git a/dotscope/src/deobfuscation/cleanup.rs b/dotscope/src/deobfuscation/cleanup.rs index 72add578..ead26594 100644 --- a/dotscope/src/deobfuscation/cleanup.rs +++ b/dotscope/src/deobfuscation/cleanup.rs @@ -55,6 +55,32 @@ use crate::{ CilObject, Result, }; +/// Returns the registered decryptors that still have call sites in the +/// post-pass call graph. +/// +/// Decryption rewrites a call site into the constant it produced, so a +/// decryptor nothing calls any more has been fully reversed and its +/// infrastructure can go. One that is still called has not been reversed — +/// whether because emulation failed, or because the call site was never +/// reached — and neither it nor the type that owns it may be deleted. +/// +/// Generic decryptors are invoked through `MethodSpec` tokens rather than the +/// `MethodDef` directly, so each callee is resolved back to its base decryptor. +fn decryptors_still_called( + ctx: &AnalysisContext, + ssa_call_graph: &BTreeMap>, +) -> HashSet { + let mut still_called = HashSet::new(); + for callees in ssa_call_graph.values() { + for callee in callees { + if let Some(decryptor) = ctx.decryptors.resolve_decryptor(*callee) { + still_called.insert(decryptor); + } + } + } + still_called +} + /// Builds a complete cleanup request from detection results and analysis state. /// /// This consolidates all cleanup sources into a single request: @@ -85,6 +111,7 @@ pub(crate) fn build_cleanup_request( // Start with technique-merged cleanup let registry = engine.technique_registry(); let mut request = detections.merged_cleanup(); + let still_called = decryptors_still_called(ctx, ssa_call_graph); for tech in registry.sorted_techniques(detections) { if !detections.is_detected(tech.id()) { continue; @@ -93,27 +120,47 @@ pub(crate) fn build_cleanup_request( continue; }; if let Some(tech_cleanup) = tech.cleanup(detection) { - request.merge(&tech_cleanup); + // A technique builds its cleanup request from detection findings + // alone — it cannot see whether the transformation it implies + // actually succeeded. Merging it unconditionally deletes the + // decryptor, its owning infrastructure type and the encrypted data + // even when not a single call site was reversed, leaving live call + // sites pointing at methods that no longer exist. Withhold the whole + // request when a decryptor it removes is still called: deleting what + // was never replaced is strictly worse than leaving the obfuscation + // in place. + let unreversed: Vec = tech_cleanup + .methods() + .filter(|t| still_called.contains(t)) + .copied() + .collect(); + if unreversed.is_empty() { + request.merge(&tech_cleanup); + } else { + log::warn!( + "Skipping cleanup for {}: {} decryptor(s) still have live call sites \ + ({} decryption failures); their infrastructure must survive", + tech.id(), + unreversed.len(), + ctx.decryptors.total_failed(), + ); + } } } - // Protect registered decryptors that were NOT fully decrypted. - // Techniques may unconditionally mark decryptor methods for cleanup, but - // if emulation failed for any call site, the method must survive — deleting - // it would leave broken call sites that reference a now-missing method. - // Only fully-decrypted methods are safe to remove. - let removable = ctx.decryptors.removable_decryptors(); + // Protect decryptors that anything still calls, and remove the rest. + // A successful decryption rewrites its call site to the decrypted constant, + // so a decryptor with no remaining callers has been fully reversed and is + // safe to delete. One that is still called has not been — deleting it would + // leave those call sites referencing a method that no longer exists. for token in ctx.decryptors.registered_tokens() { - if !removable.contains(&token) { + if still_called.contains(&token) { request.protect_token(token); + } else { + request.add_method(token); } } - // Add decryptors that were fully emulated and are now safe to remove. - for token in removable { - request.add_method(token); - } - // Pre-scan: mark methods as dead if they have no live callers. sweep_dead_module_methods(assembly, &mut request, ssa_call_graph, ctx); diff --git a/dotscope/src/deobfuscation/config.rs b/dotscope/src/deobfuscation/config.rs index c8e9a4e7..e8cdbd29 100644 --- a/dotscope/src/deobfuscation/config.rs +++ b/dotscope/src/deobfuscation/config.rs @@ -54,6 +54,14 @@ pub struct PassConfig { pub opaque_predicate_removal: bool, /// Enable copy propagation pass. pub copy_propagation: bool, + /// Enable the memory optimization pass (store-to-load forwarding, redundant + /// load elimination, dead store elimination). + /// + /// Every rewrite is gated on a Memory SSA alias proof, so this reaches the + /// field and array traffic that obfuscators use to keep values out of SSA + /// registers — ConfuserEx state fields, array-backed string tables — which + /// the register-level passes cannot see through. + pub memory_optimization: bool, /// Enable strength reduction pass (mul->shl, div->shr, rem->and for powers of 2). pub strength_reduction: bool, /// Enable control flow simplification pass. @@ -86,6 +94,7 @@ impl Default for PassConfig { dead_code_elimination: true, opaque_predicate_removal: true, copy_propagation: true, + memory_optimization: true, strength_reduction: true, control_flow_simplification: true, interprocedural: true, diff --git a/dotscope/src/deobfuscation/context.rs b/dotscope/src/deobfuscation/context.rs index de9b9e7d..25ae1251 100644 --- a/dotscope/src/deobfuscation/context.rs +++ b/dotscope/src/deobfuscation/context.rs @@ -309,7 +309,6 @@ mod tests { use std::{sync::Arc, thread}; use super::*; - use crate::{ analysis::{CallGraph, ConstValue, SsaVarId}, compiler::CallSiteInfo, diff --git a/dotscope/src/deobfuscation/decryptors.rs b/dotscope/src/deobfuscation/decryptors.rs index 01b5c937..80676d43 100644 --- a/dotscope/src/deobfuscation/decryptors.rs +++ b/dotscope/src/deobfuscation/decryptors.rs @@ -544,8 +544,12 @@ impl DecryptorContext { /// Checks if all calls to a decryptor were successfully handled. /// - /// Returns `true` if there are no failed calls for this decryptor, - /// meaning it's safe to remove. + /// Returns `true` if there are no failed calls for this decryptor. + /// + /// Note that this is not on its own sufficient to justify deleting the + /// method: a decryptor that was never exercised also has no failures. + /// Removal additionally requires that nothing still calls it — see + /// `decryptors_still_called` in the cleanup builder. /// /// # Arguments /// @@ -553,7 +557,7 @@ impl DecryptorContext { /// /// # Returns /// - /// `true` if safe to remove (no failed calls). + /// `true` if no call site failed to decrypt. #[must_use] pub fn is_fully_decrypted(&self, decryptor: Token) -> bool { self.failed.get(&decryptor).is_none_or(|r| r.count() == 0) @@ -626,8 +630,7 @@ impl DecryptorContext { #[cfg(test)] mod tests { - use std::sync::Arc; - use std::thread; + use std::{sync::Arc, thread}; use super::*; diff --git a/dotscope/src/deobfuscation/engine/mod.rs b/dotscope/src/deobfuscation/engine/mod.rs index 8f817265..8950d71d 100644 --- a/dotscope/src/deobfuscation/engine/mod.rs +++ b/dotscope/src/deobfuscation/engine/mod.rs @@ -19,9 +19,10 @@ use crate::{ compiler::{ AlgebraicSimplificationPass, BlockMergingPass, ConstantPropagationPass, ControlFlowSimplificationPass, CopyPropagationPass, DeadCodeEliminationPass, - GlobalValueNumberingPass, InliningPass, JumpThreadingPass, LicmPass, OpaquePredicatePass, - PassPhase, PassScheduler, ProxyDevirtualizationPass, ReassociationPass, - StrengthReductionPass, ValueRangePropagationPass, + GlobalValueNumberingPass, InliningPass, JumpThreadingPass, LicmPass, + MemoryOptimizationPass, OpaquePredicatePass, PassPhase, PassScheduler, + ProxyDevirtualizationPass, ReassociationPass, StrengthReductionPass, + ValueRangePropagationPass, }, deobfuscation::{ config::EngineConfig, @@ -228,6 +229,13 @@ impl DeobfuscationEngine { PassPhase::Normalize, ); } + if self.config.passes.memory_optimization { + // Runs after copy propagation so the address expressions Memory SSA + // decodes are already canonicalised, and before strength reduction / + // algebraic simplification so the values it forwards out of memory + // get folded in the same fixpoint iteration. + scheduler.add(Box::new(MemoryOptimizationPass), PassPhase::Normalize); + } if self.config.passes.strength_reduction { scheduler.add(Box::new(StrengthReductionPass::new()), PassPhase::Normalize); scheduler.add( diff --git a/dotscope/src/deobfuscation/engine/pipeline.rs b/dotscope/src/deobfuscation/engine/pipeline.rs index 583c6559..eaaa77bc 100644 --- a/dotscope/src/deobfuscation/engine/pipeline.rs +++ b/dotscope/src/deobfuscation/engine/pipeline.rs @@ -9,9 +9,8 @@ use std::{ time::{Duration, Instant}, }; -use log::{debug, info, warn}; - use analyssa::scheduling::SsaPass as AnalyssaSsaPass; +use log::{debug, info, warn}; use crate::{ analysis::{CilTarget, MethodRef}, diff --git a/dotscope/src/deobfuscation/engine/tests.rs b/dotscope/src/deobfuscation/engine/tests.rs index d2a6aea0..31a690de 100644 --- a/dotscope/src/deobfuscation/engine/tests.rs +++ b/dotscope/src/deobfuscation/engine/tests.rs @@ -62,6 +62,7 @@ fn test_pipeline_passes_selective() { dead_code_elimination: false, string_decryption: false, strength_reduction: false, + memory_optimization: false, ..Default::default() }, ..Default::default() @@ -76,6 +77,45 @@ fn test_pipeline_passes_selective() { assert_eq!(scheduler.pass_count(), 0); } +/// `memory_optimization` is the only toggle that differs between these two +/// configs, so it must account for exactly one normalize pass. +#[test] +fn test_pipeline_memory_optimization_toggle() { + let base = PassConfig { + constant_propagation: false, + copy_propagation: false, + opaque_predicate_removal: false, + control_flow_simplification: false, + dead_code_elimination: false, + string_decryption: false, + strength_reduction: false, + memory_optimization: false, + ..Default::default() + }; + + let without = DeobfuscationEngine::new(EngineConfig { + passes: base.clone(), + ..Default::default() + }) + .create_scheduler(); + + let with = DeobfuscationEngine::new(EngineConfig { + passes: PassConfig { + memory_optimization: true, + ..base + }, + ..Default::default() + }) + .create_scheduler(); + + assert_eq!( + with.normalize_count(), + without.normalize_count() + 1, + "enabling memory_optimization should register exactly one normalize pass" + ); + assert_eq!(with.pass_count(), without.pass_count()); +} + #[test] fn test_analyze_return_void() { // Create SSA with void return diff --git a/dotscope/src/deobfuscation/mod.rs b/dotscope/src/deobfuscation/mod.rs index 916b87fe..c645f2a2 100644 --- a/dotscope/src/deobfuscation/mod.rs +++ b/dotscope/src/deobfuscation/mod.rs @@ -122,19 +122,18 @@ pub use passes::{ CffReconstructionPass, DecryptionPass, DelegateProxyResolutionPass, DelegateTypeInfo, NativeMethodConversionPass, NeutralizationPass, OpaqueFieldPredicatePass, UnflattenConfig, }; -pub use renamer::SmartRenameConfig; -pub use result::DeobfuscationResult; -pub use techniques::{ - AttributionResult, Detection, Detections, Evidence, ObfuscatorMatcher, ObfuscatorSignature, - Technique, TechniqueCategory, TechniqueRegistry, TechniqueResult, WorkingAssembly, -}; - // Crate-internal re-exports (only items that are actually imported via this path) pub(crate) use processcell::ProcessCell; +pub use renamer::SmartRenameConfig; +pub use result::DeobfuscationResult; pub(crate) use statemachine::{ CfgInfo, StateMachineCallSite, StateMachineProvider, StateMachineSemantics, StateMachineState, StateSlotOperation, StateUpdateCall, }; +pub use techniques::{ + AttributionResult, Detection, Detections, Evidence, ObfuscatorMatcher, ObfuscatorSignature, + Technique, TechniqueCategory, TechniqueRegistry, TechniqueResult, WorkingAssembly, +}; pub(crate) use template::EmulationTemplatePool; #[cfg(test)] diff --git a/dotscope/src/deobfuscation/passes/antidebug.rs b/dotscope/src/deobfuscation/passes/antidebug.rs index a6c88db9..dd8ecde5 100644 --- a/dotscope/src/deobfuscation/passes/antidebug.rs +++ b/dotscope/src/deobfuscation/passes/antidebug.rs @@ -57,7 +57,7 @@ use crate::{ CilTarget, MethodRef, PhiTaintMode, SsaFunction, SsaOp, TaintConfig, TokenTaintBuilder, }, compiler::{CompilerContext, EventKind, ModificationScope, SsaPass}, - deobfuscation::utils::resolve_qualified_method_name, + deobfuscation::utils::{resolve_qualified_method_name, retain_removable}, metadata::token::Token, CilObject, }; @@ -212,13 +212,30 @@ impl SsaPass for SentinelTaintRemovalPass { return Ok(false); } - let tainted_instrs: HashSet<(usize, usize)> = + let mut tainted_instrs: HashSet<(usize, usize)> = taint.tainted_instructions().iter().copied().collect(); + let mut tainted_phis: HashSet<(usize, usize)> = + taint.tainted_phis().iter().copied().collect(); + + // Step 2b: Refuse to destroy a definition something still reads. + // `PhiTaintMode::NoPropagation` makes phis taint barriers, so a phi can + // merge a tainted definition into code the analysis never marks — and + // NOPing the definition would leave that read dangling, which the SSA + // verifier rejects for the whole method. + retain_removable(ssa, &mut tainted_instrs, &mut tainted_phis); + if tainted_instrs.is_empty() && tainted_phis.is_empty() { + return Ok(false); + } + + // Sorted so the event log and the rewrite order do not depend on hash + // iteration order. + let mut removals: Vec<(usize, usize)> = tainted_instrs.iter().copied().collect(); + removals.sort_unstable(); // Step 3: Pre-compute branch redirect targets (immutable borrow) // before mutating instructions (mutable borrow). let mut branch_redirects: Vec<(usize, usize, usize)> = Vec::new(); - for &(block_idx, instr_idx) in taint.tainted_instructions() { + for &(block_idx, instr_idx) in &removals { if let Some(block) = ssa.block(block_idx) { if let Some(instr) = block.instruction(instr_idx) { if instr.is_terminator() { @@ -251,7 +268,7 @@ impl SsaPass for SentinelTaintRemovalPass { // Step 4: Apply mutations. let mut neutralized = 0usize; - for &(block_idx, instr_idx) in taint.tainted_instructions() { + for &(block_idx, instr_idx) in &removals { if let Some(block) = ssa.block_mut(block_idx) { if let Some(instr) = block.instruction_mut(instr_idx) { // Record metadata tokens before neutralizing @@ -279,9 +296,9 @@ impl SsaPass for SentinelTaintRemovalPass { } // Step 5: Remove tainted PHI nodes (reverse order for safe removal). - let mut tainted_phis: Vec<(usize, usize)> = taint.tainted_phis().iter().copied().collect(); - tainted_phis.sort_by(|a, b| b.cmp(a)); - for (block_idx, phi_idx) in tainted_phis { + let mut removable_phis: Vec<(usize, usize)> = tainted_phis.into_iter().collect(); + removable_phis.sort_by(|a, b| b.cmp(a)); + for (block_idx, phi_idx) in removable_phis { if let Some(block) = ssa.block_mut(block_idx) { if phi_idx < block.phi_nodes().len() { block.phi_nodes_mut().remove(phi_idx); diff --git a/dotscope/src/deobfuscation/passes/bitmono/mod.rs b/dotscope/src/deobfuscation/passes/bitmono/mod.rs index 4ee5d7bd..45a1c11e 100644 --- a/dotscope/src/deobfuscation/passes/bitmono/mod.rs +++ b/dotscope/src/deobfuscation/passes/bitmono/mod.rs @@ -16,7 +16,6 @@ mod strings; mod unmanaged; -pub use self::unmanaged::UnmanagedStringReversalPass; - #[cfg(feature = "legacy-crypto")] pub use self::strings::StringDecryptionPass; +pub use self::unmanaged::UnmanagedStringReversalPass; diff --git a/dotscope/src/deobfuscation/passes/bitmono/strings.rs b/dotscope/src/deobfuscation/passes/bitmono/strings.rs index 9454270c..e65f79ca 100644 --- a/dotscope/src/deobfuscation/passes/bitmono/strings.rs +++ b/dotscope/src/deobfuscation/passes/bitmono/strings.rs @@ -494,9 +494,8 @@ fn decrypt_string(encrypted: &[u8], key: &[u8], iv: &[u8]) -> Result { #[cfg(all(test, feature = "legacy-crypto"))] mod tests { - use crate::utils::{apply_crypto_transform, derive_key_iv, CryptoParameters}; - use super::decrypt_string; + use crate::utils::{apply_crypto_transform, derive_key_iv, CryptoParameters}; #[test] fn test_derive_key_iv_zeros() { diff --git a/dotscope/src/deobfuscation/passes/decryption.rs b/dotscope/src/deobfuscation/passes/decryption.rs index 0288a6fe..c4072084 100644 --- a/dotscope/src/deobfuscation/passes/decryption.rs +++ b/dotscope/src/deobfuscation/passes/decryption.rs @@ -64,6 +64,13 @@ use std::{ sync::{Arc, Mutex}, }; +use analyssa::{ + graph::{ + algorithms::{compute_dominators, DominatorTree}, + GraphBase, NodeId, RootedGraph, + }, + ir::value::DecryptedArrayData, +}; use rayon::prelude::*; use crate::{ @@ -87,11 +94,6 @@ use crate::{ }, CilObject, Error, Result, }; -use analyssa::graph::{ - algorithms::{compute_dominators, DominatorTree}, - GraphBase, NodeId, RootedGraph, -}; -use analyssa::ir::value::DecryptedArrayData; /// Decryption pass for obfuscated constants and strings. /// diff --git a/dotscope/src/deobfuscation/passes/jiejienet/mod.rs b/dotscope/src/deobfuscation/passes/jiejienet/mod.rs index 240257b2..035e1559 100644 --- a/dotscope/src/deobfuscation/passes/jiejienet/mod.rs +++ b/dotscope/src/deobfuscation/passes/jiejienet/mod.rs @@ -18,7 +18,8 @@ mod arrays; mod resources; mod typeofs; -pub use self::arrays::ArrayInitRestorationPass; -pub use self::resources::ResourceRestorationPass; pub(crate) use self::resources::ResourceTarget; -pub use self::typeofs::TypeOfRestorationPass; +pub use self::{ + arrays::ArrayInitRestorationPass, resources::ResourceRestorationPass, + typeofs::TypeOfRestorationPass, +}; diff --git a/dotscope/src/deobfuscation/passes/mod.rs b/dotscope/src/deobfuscation/passes/mod.rs index 7a449fe0..6611e407 100644 --- a/dotscope/src/deobfuscation/passes/mod.rs +++ b/dotscope/src/deobfuscation/passes/mod.rs @@ -48,12 +48,14 @@ mod reflection; mod staticfields; mod unflattening; -pub use self::antidebug::{SentinelCondition, SentinelTaintRemovalPass}; -pub use self::decryption::DecryptionPass; -pub use self::delegates::{DelegateProxyResolutionPass, DelegateTypeInfo}; -pub use self::native::NativeMethodConversionPass; -pub use self::neutralize::NeutralizationPass; -pub use self::opaquefields::OpaqueFieldPredicatePass; -pub use self::reflection::{count_resolve_method_calli_sites, ReflectionDevirtualizationPass}; -pub use self::staticfields::{I32Extractor, StaticFieldResolutionPass, StringExtractor}; -pub use self::unflattening::{CffDetector, CffReconstructionPass, Dispatcher, UnflattenConfig}; +pub use self::{ + antidebug::{SentinelCondition, SentinelTaintRemovalPass}, + decryption::DecryptionPass, + delegates::{DelegateProxyResolutionPass, DelegateTypeInfo}, + native::NativeMethodConversionPass, + neutralize::NeutralizationPass, + opaquefields::OpaqueFieldPredicatePass, + reflection::{count_resolve_method_calli_sites, ReflectionDevirtualizationPass}, + staticfields::{I32Extractor, StaticFieldResolutionPass, StringExtractor}, + unflattening::{CffDetector, CffReconstructionPass, Dispatcher, UnflattenConfig}, +}; diff --git a/dotscope/src/deobfuscation/passes/netreactor/mod.rs b/dotscope/src/deobfuscation/passes/netreactor/mod.rs index 05470d56..3598db5d 100644 --- a/dotscope/src/deobfuscation/passes/netreactor/mod.rs +++ b/dotscope/src/deobfuscation/passes/netreactor/mod.rs @@ -16,5 +16,4 @@ mod resolver; mod rewrite; -pub use self::resolver::TokenResolverPass; -pub use self::rewrite::ResourceShimRewritePass; +pub use self::{resolver::TokenResolverPass, rewrite::ResourceShimRewritePass}; diff --git a/dotscope/src/deobfuscation/passes/neutralize.rs b/dotscope/src/deobfuscation/passes/neutralize.rs index efd21bef..37912396 100644 --- a/dotscope/src/deobfuscation/passes/neutralize.rs +++ b/dotscope/src/deobfuscation/passes/neutralize.rs @@ -41,6 +41,7 @@ use std::collections::{HashMap, HashSet}; use crate::{ analysis::{find_token_dependencies, CilTarget, ConstValue, MethodRef, SsaFunction, SsaOp}, compiler::{CompilerContext, EventKind, SsaPass}, + deobfuscation::utils::retain_removable, metadata::token::Token, }; @@ -249,12 +250,44 @@ impl<'a> NeutralizationPass<'a> { return 0; } - // Collect tainted sets for block analysis - let tainted_instrs: HashSet<(usize, usize)> = - taint.tainted_instructions().iter().copied().collect(); - let tainted_phis_set: HashSet<(usize, usize)> = + // Collect tainted sets for block analysis. Never neutralize + // `DecryptedString` constants — they are recovered user data, not + // protection infrastructure. Bidirectional taint reaches them through + // shared data flow (e.g. a `String.Concat` over both a decrypted string + // and a value from a removed decryptor), but the string itself is + // legitimate. Excluding them here rather than at rewrite time keeps the + // candidate set equal to what is actually removed, which both the + // branch-target choice and the liveness closure below depend on. + let mut tainted_instrs: HashSet<(usize, usize)> = taint + .tainted_instructions() + .iter() + .copied() + .filter(|&(block_idx, instr_idx)| { + !ssa.block(block_idx) + .and_then(|b| b.instructions().get(instr_idx)) + .is_some_and(|instr| { + matches!( + instr.op(), + SsaOp::Const { + value: ConstValue::DecryptedString(_), + .. + } + ) + }) + }) + .collect(); + let mut tainted_phis_set: HashSet<(usize, usize)> = taint.tainted_phis().iter().copied().collect(); + // Refuse to destroy a definition something still reads. Dropping a phi + // makes its result undefined, and NOPing an instruction makes its + // destination undefined; either leaves a read the SSA verifier rejects, + // failing the whole method rather than the one rewrite. + retain_removable(ssa, &mut tainted_instrs, &mut tainted_phis_set); + if tainted_instrs.is_empty() && tainted_phis_set.is_empty() { + return 0; + } + // Find blocks that can reach exit (for fallback target selection) let can_reach_exit = Self::find_blocks_reaching_exit(ssa); @@ -263,7 +296,7 @@ impl<'a> NeutralizationPass<'a> { // 1. Remove tainted PHI nodes // Collect PHIs to remove (block_idx, phi_idx) sorted in reverse order // so we can remove them without invalidating indices - let mut tainted_phis: Vec<(usize, usize)> = taint.tainted_phis().iter().copied().collect(); + let mut tainted_phis: Vec<(usize, usize)> = tainted_phis_set.iter().copied().collect(); tainted_phis.sort_by(|a, b| b.cmp(a)); // Sort descending for (block_idx, phi_idx) in tainted_phis { @@ -279,32 +312,13 @@ impl<'a> NeutralizationPass<'a> { // First pass: collect what operations to apply (to avoid borrow conflicts) let mut actions: Vec<(usize, usize, InstrAction)> = Vec::new(); - for &(block_idx, instr_idx) in taint.tainted_instructions() { + // Sorted so the rewrite order does not depend on hash iteration order. + let mut removals: Vec<(usize, usize)> = tainted_instrs.iter().copied().collect(); + removals.sort_unstable(); + + for &(block_idx, instr_idx) in &removals { if let Some(block) = ssa.block(block_idx) { if let Some(instr) = block.instructions().get(instr_idx) { - // Never neutralize DecryptedString constants — they are recovered - // user data, not protection infrastructure. The taint may reach them - // via bidirectional propagation through shared data flow (e.g., a - // String.Concat that uses both a decrypted string and an undecrypted - // value from a removed decryptor), but the string itself is legitimate. - if matches!( - instr.op(), - SsaOp::Const { - value: ConstValue::DecryptedString(_), - .. - } - ) { - continue; - } - - // Also protect instructions whose ONLY tainted inputs are - // DecryptedString variables — they're consumers of user data, - // not protection code. For example, String.Concat(decrypted, tainted) - // should not be neutralized if the decrypted side is legitimate. - // We skip this for now and only protect the constants themselves, - // since the second pipeline run (after neutralization) will re-run - // DCE which correctly handles liveness. - let action = if instr.is_terminator() { match instr.op() { SsaOp::Branch { diff --git a/dotscope/src/deobfuscation/passes/unflattening/detection.rs b/dotscope/src/deobfuscation/passes/unflattening/detection.rs index a23c09ab..1a91b391 100644 --- a/dotscope/src/deobfuscation/passes/unflattening/detection.rs +++ b/dotscope/src/deobfuscation/passes/unflattening/detection.rs @@ -22,6 +22,13 @@ use std::{ collections::{HashSet, VecDeque}, }; +use analyssa::{ + graph::{ + algorithms::{compute_dominators, DominatorTree}, + GraphBase, NodeId, Successors, + }, + BitSet, +}; use rayon::prelude::*; use crate::{ @@ -32,13 +39,6 @@ use crate::{ UnflattenConfig, }, }; -use analyssa::{ - graph::{ - algorithms::{compute_dominators, DominatorTree}, - GraphBase, NodeId, Successors, - }, - BitSet, -}; /// Entry point into a CFF region. /// @@ -1185,13 +1185,12 @@ fn can_reach_dispatcher( mod tests { use analyssa::BitSet; + use super::{CffPattern, EntryCondition, EntryPoint}; use crate::{ analysis::SsaVarId, deobfuscation::passes::unflattening::dispatcher::{DispatcherInfo, StateTransform}, }; - use super::{CffPattern, EntryCondition, EntryPoint}; - #[test] fn test_cff_pattern_case_count() { // Basic pattern structure test diff --git a/dotscope/src/deobfuscation/passes/unflattening/mod.rs b/dotscope/src/deobfuscation/passes/unflattening/mod.rs index 8f4031a5..216f35c0 100644 --- a/dotscope/src/deobfuscation/passes/unflattening/mod.rs +++ b/dotscope/src/deobfuscation/passes/unflattening/mod.rs @@ -38,19 +38,17 @@ mod detection; mod dispatcher; mod reconstruction; +mod spill; mod statevar; mod tracer; -pub use detection::CffDetector; -pub use dispatcher::Dispatcher; -pub use reconstruction::{apply_patch_plan, extract_patch_plan, merge_patch_plans}; - -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; use dashmap::DashSet; +pub use detection::CffDetector; +pub use dispatcher::Dispatcher; use rayon::prelude::*; - -use std::collections::HashMap; +pub use reconstruction::{apply_patch_plan, extract_patch_plan, merge_patch_plans}; use crate::{ analysis::{CilTarget, MethodRef, SsaFunction}, @@ -170,8 +168,22 @@ pub fn unflatten_with_dispatchers( return None; } - // Step 4: Clone the SSA and apply the combined patches once + // Step 4: Clone the SSA, give every cross-block value a storage location, + // then apply the combined patches once. + // + // The promotion must happen before any terminator is rewired. Patching + // invalidates the phi nodes that record which SSA names denote the same + // value across a merge, and for edges the patch creates no phi ever recorded + // anything at all — so a value carried between blocks in a stack temporary + // becomes unreconstructible the moment the CFG changes. Promoting those + // values to local slots first gives `rebuild_ssa` a storage location to + // resolve them against, which is what makes the rebuild well-defined for an + // arbitrarily rewired CFG. let mut patched = ssa.clone(); + let spilled = spill::promote_cross_block_values(&mut patched); + if spilled > 0 { + log::debug!("Promoted {spilled} cross-block value(s) to locals before unflattening"); + } let _result = apply_patch_plan(&mut patched, &merged); // Note: we do NOT reject based on dispatcher_still_needed. With multiple @@ -482,8 +494,10 @@ mod tests { SsaInstruction, SsaOp, SsaVarId, VariableOrigin, }, assembly::{decode_blocks, InstructionAssembler}, - deobfuscation::passes::unflattening::{detection::CffDetector, dispatcher::Dispatcher}, - deobfuscation::{DeobfuscationEngine, EngineConfig}, + deobfuscation::{ + passes::unflattening::{detection::CffDetector, dispatcher::Dispatcher}, + DeobfuscationEngine, EngineConfig, + }, metadata::token::Token, test::TestTypeProvider, CilObject, diff --git a/dotscope/src/deobfuscation/passes/unflattening/reconstruction.rs b/dotscope/src/deobfuscation/passes/unflattening/reconstruction.rs index a0a73e03..b32fa25b 100644 --- a/dotscope/src/deobfuscation/passes/unflattening/reconstruction.rs +++ b/dotscope/src/deobfuscation/passes/unflattening/reconstruction.rs @@ -49,13 +49,26 @@ use std::collections::{BTreeMap, BTreeSet}; use analyssa::BitSet; +use rustc_hash::{FxHashMap, FxHashSet}; use crate::{ - analysis::{SsaBlock, SsaFunction, SsaInstruction, SsaOp, SsaVarId}, + analysis::{DefSite, PhiOperand, SsaBlock, SsaFunction, SsaInstruction, SsaOp, SsaVarId}, deobfuscation::passes::unflattening::tracer::{TraceNode, TraceTerminator, TraceTree}, }; +/// Maximum dispatcher-phi hops followed when recovering a redirected edge's value. +/// +/// Chains longer than this are pathological; giving up is safe because the caller +/// then leaves the operand absent rather than guessing. +const MAX_PHI_RESOLUTION_HOPS: usize = 8; + type PhiOperands = Vec<(usize, SsaVarId)>; + +/// Snapshot of every block's phi nodes: `block -> (phi result -> operands)`. +/// +/// Captured before the CFG is rewired, so edges the patch removes can still be +/// consulted when recovering values for the edges it creates. +type PhiSnapshot = BTreeMap>; type BlockPhiData = Vec<(usize, Vec<(SsaVarId, PhiOperands)>)>; /// Result of unflattening a CFG. @@ -104,6 +117,21 @@ pub struct PatchPlan { /// Blocks in execution order (for debugging/verification). pub execution_order: Vec, + /// Membership index for [`execution_order`](Self::execution_order). + /// + /// The order itself has to stay a `Vec`, but it is appended to once per + /// block of every node in the trace tree — millions of times on a + /// flattened method — and each append asked whether the block was already + /// present by scanning the whole list. + execution_order_seen: FxHashSet, + + /// Source block to redirect target, mirroring + /// [`redirects`](Self::redirects). + /// + /// Conflict detection looks up a source on every redirect, which is the + /// same per-node cost as the execution order above. + redirect_targets: FxHashMap, + /// Branch-collapse requests: `source_block -> new_target`. /// /// When a state transition's path enters the dispatcher through a @@ -132,6 +160,8 @@ impl PatchPlan { state_transition_sources: BTreeSet::new(), clone_requests: BTreeMap::new(), execution_order: Vec::new(), + execution_order_seen: FxHashSet::default(), + redirect_targets: FxHashMap::default(), branch_collapses: BTreeMap::new(), state_transitions_removed: 0, user_branches_preserved: 0, @@ -162,7 +192,7 @@ impl PatchPlan { return; } // Check for conflict: same source with different target - if let Some(&(_, existing_target)) = self.redirects.iter().find(|&&(s, _)| s == source) { + if let Some(&existing_target) = self.redirect_targets.get(&source) { if existing_target != target { // Conflict! This source is a merge point needing cloning. // Add both the existing and new paths to clone requests. @@ -193,6 +223,7 @@ impl PatchPlan { } // First time seeing this source - record it and track predecessor for potential cloning + self.redirect_targets.insert(source, target); self.redirects.push((source, target)); // Also record in clone_requests in case we need it later @@ -224,7 +255,8 @@ impl PatchPlan { } fn add_to_execution_order(&mut self, block: usize) { - if !self.execution_order.contains(&block) && !self.is_dispatcher_block(block) { + if !self.execution_order_seen.contains(&block) && !self.is_dispatcher_block(block) { + self.execution_order_seen.insert(block); self.execution_order.push(block); } } @@ -301,6 +333,8 @@ pub fn merge_patch_plans(plans: Vec) -> PatchPlan { state_transition_sources: BTreeSet::new(), clone_requests: BTreeMap::new(), execution_order: Vec::new(), + execution_order_seen: FxHashSet::default(), + redirect_targets: FxHashMap::default(), branch_collapses: BTreeMap::new(), state_transitions_removed: 0, user_branches_preserved: 0, @@ -319,6 +353,8 @@ pub fn merge_patch_plans(plans: Vec) -> PatchPlan { state_transition_sources: BTreeSet::new(), clone_requests: BTreeMap::new(), execution_order: Vec::new(), + execution_order_seen: FxHashSet::default(), + redirect_targets: FxHashMap::default(), branch_collapses: BTreeMap::new(), state_transitions_removed: 0, user_branches_preserved: 0, @@ -338,6 +374,8 @@ pub fn merge_patch_plans(plans: Vec) -> PatchPlan { state_transition_sources: BTreeSet::new(), clone_requests: BTreeMap::new(), execution_order: Vec::new(), + execution_order_seen: FxHashSet::default(), + redirect_targets: FxHashMap::default(), branch_collapses: BTreeMap::new(), state_transitions_removed: 0, user_branches_preserved: 0, @@ -352,9 +390,7 @@ pub fn merge_patch_plans(plans: Vec) -> PatchPlan { // Merge redirects, checking for conflicts for (source, target) in plan.redirects { - if let Some(&(_, existing_target)) = - merged.redirects.iter().find(|&&(s, _)| s == source) - { + if let Some(&existing_target) = merged.redirect_targets.get(&source) { if existing_target != target { log::warn!( "CFF merge: redirect conflict for block {} (target {} vs {}), keeping first", @@ -367,6 +403,7 @@ pub fn merge_patch_plans(plans: Vec) -> PatchPlan { // Duplicate (same source, same target) — skip continue; } + merged.redirect_targets.insert(source, target); merged.redirects.push((source, target)); } @@ -381,7 +418,7 @@ pub fn merge_patch_plans(plans: Vec) -> PatchPlan { // Merge execution order (deduplicated) for block in plan.execution_order { - if !merged.execution_order.contains(&block) { + if merged.execution_order_seen.insert(block) { merged.execution_order.push(block); } } @@ -559,11 +596,19 @@ fn extract_redirects_from_node( .get(interior_start..end_idx) .map(|s| s.iter().copied().collect()) .unwrap_or_default(); - for iwv in &node.instructions { - if !intermediate_blocks.contains(&iwv.block_idx) { - continue; - } - if !is_pure_prep_op(iwv.instruction.op()) { + // Purity is a property of the blocks themselves, so + // read it from the SSA rather than from a per-node + // instruction log. The tracer walks every + // instruction of a block it passes through, and + // these blocks lie strictly between `first` and + // `last` in the visit order, so they were traversed + // in full — the two formulations see the same + // instructions. + for &block_idx in &intermediate_blocks { + let all_pure = ssa.block(block_idx).is_some_and(|block| { + block.instructions().iter().all(|i| is_pure_prep_op(i.op())) + }); + if !all_pure { intermediates_are_pure = false; break; } @@ -738,6 +783,31 @@ fn extract_redirects_from_node( /// - Number of user branches preserved /// - Final block count pub fn apply_patch_plan(ssa: &mut SsaFunction, plan: &PatchPlan) -> ReconstructionResult { + // Snapshot the phi graph before any rewiring. Redirects bypass the + // dispatcher, and the dispatcher's phis — which record what each case block + // contributed — are cleared later in this function. Recovering the value for + // a redirected edge means reading those phis, so they must be captured while + // they still exist. + let original_phis: PhiSnapshot = ssa + .blocks() + .iter() + .map(|block| { + let phis = block + .phi_nodes() + .iter() + .map(|phi| { + let operands = phi + .operands() + .iter() + .map(|op| (op.predecessor(), op.value())) + .collect(); + (phi.result(), operands) + }) + .collect(); + (block.id(), phis) + }) + .collect(); + // Apply only safe redirects (skip conflicting merge points that need cloning) let safe = plan.safe_redirects(); let to_clone = plan.blocks_to_clone(); @@ -880,9 +950,63 @@ pub fn apply_patch_plan(ssa: &mut SsaFunction, plan: &PatchPlan) -> Reconstructi pred_block.redirect_target(original_merge, new_block_idx); } - // Filter state instructions from the clone + // Filter state instructions from the clone. + // + // This must happen before the clone's definitions are renamed below: + // taint is recorded against the original variable ids, so renaming + // first would stop every state instruction from matching. filter_state_instructions(&mut cloned, &plan.state_tainted, &plan.dispatcher_blocks); + // Give the clone its own definitions. + // + // Copying a block verbatim defines every one of its variables a second + // time, which breaks SSA's single-definition property. The original code + // relied on `rebuild_ssa()` to sort this out, but rebuild cannot recover + // from duplicate definitions — it produces a function whose uses resolve + // to neither definition, surfacing later as an undefined-variable + // verification failure in an unrelated block. + // + // Uses that refer to definitions *outside* this block are left alone: + // they still resolve to the original definition, which dominates both + // the original block and the clone. + let mut defs_to_rename: Vec = Vec::new(); + for phi in cloned.phi_nodes() { + defs_to_rename.push(phi.result()); + } + for instr in cloned.instructions() { + if let Some(dest) = instr.def() { + defs_to_rename.push(dest); + } + } + + let mut renames: BTreeMap = BTreeMap::new(); + for old_var in defs_to_rename { + let Some(var) = ssa.variable(old_var) else { + continue; + }; + let (origin, var_type) = (var.origin(), var.var_type().clone()); + let new_var = ssa.create_variable(origin, 0, DefSite::phi(new_block_idx), var_type); + renames.insert(old_var, new_var); + } + + if !renames.is_empty() { + for phi in cloned.phi_nodes_mut() { + if let Some(&new_var) = renames.get(&phi.result()) { + phi.set_result(new_var); + } + } + for instr in cloned.instructions_mut() { + instr + .op_mut() + .replace_uses_with(|v| renames.get(&v).copied()); + if let Some(dest) = instr.def() { + if let Some(&new_var) = renames.get(&dest) { + instr.op_mut().replace_def(dest, new_var); + } + } + } + } + // Add the clone to SSA ssa.blocks_mut().push(cloned); } @@ -982,6 +1106,100 @@ pub fn apply_patch_plan(ssa: &mut SsaFunction, plan: &PatchPlan) -> Reconstructi } } + // Drop phi operands whose value no longer has a definition. + // + // Clearing the dispatcher and dead case blocks, and filtering state + // instructions out of patched blocks, removes definitions that successor + // phis may still reference. `rebuild_ssa()` resolves phi operands into real + // values, so a stale operand becomes an undefined instruction operand and + // fails verification — reported far from here, as an undefined use in + // whichever block the value was propagated into. + // + // Pruning is safe because `rebuild_ssa()` reconstructs phi nodes from the + // patched CFG and the surviving definitions; a stale operand carries no + // information it could use. + { + let mut defined: BTreeSet = BTreeSet::new(); + for block in ssa.blocks() { + for phi in block.phi_nodes() { + defined.insert(phi.result()); + } + for instr in block.instructions() { + if let Some(dest) = instr.def() { + defined.insert(dest); + } + } + } + + // Predecessor sets for the *patched* CFG. Redirects, branch collapses + // and clone edges all rewire terminators without touching phi operands, + // so a phi can still carry an operand for a block that no longer reaches + // it — 53k such operands on a large NETReactor method. `rebuild_ssa()` + // resolves phi operands into concrete values, and a stale operand names + // a value that does not reach the block, so it propagates a definition + // that no longer dominates its uses. + let mut predecessors: BTreeMap> = BTreeMap::new(); + for block in ssa.blocks() { + let id = block.id(); + block.for_each_successor(|succ| { + predecessors.entry(succ).or_default().insert(id); + }); + } + + // Work out, per block, which phi operands survive and which edges need a + // value recovered. Done as a read-only pass first so `resolve_redirected_operand` + // can consult the snapshot without holding a mutable borrow of `ssa`. + let mut repairs: BTreeMap> = BTreeMap::new(); + for block in ssa.blocks() { + let block_id = block.id(); + let Some(preds) = predecessors.get(&block_id) else { + continue; + }; + for phi in block.phi_nodes() { + let result = phi.result(); + for &pred in preds { + if phi.operands().iter().any(|op| op.predecessor() == pred) { + continue; + } + if let Some(value) = resolve_redirected_operand( + &original_phis, + &defined, + block_id, + result, + pred, + preds, + ) { + repairs + .entry(block_id) + .or_default() + .push((result, pred, value)); + } + } + } + } + + for block in ssa.blocks_mut() { + let block_id = block.id(); + let preds = predecessors.remove(&block_id).unwrap_or_default(); + let block_repairs = repairs.remove(&block_id).unwrap_or_default(); + for phi in block.phi_nodes_mut() { + phi.operands_mut().retain(|op| { + preds.contains(&op.predecessor()) && defined.contains(&op.value()) + }); + for &(result, pred, value) in &block_repairs { + if result == phi.result() + && !phi.operands().iter().any(|op| op.predecessor() == pred) + { + phi.add_operand(PhiOperand::new(value, pred)); + } + } + } + block + .phi_nodes_mut() + .retain(|phi| !phi.operands().is_empty()); + } + } + // NOTE: PHI propagation and variable definition cleanup is NOT done here. // After apply_patch_plan returns, the caller must call SsaFunction::rebuild_ssa() // to reconstruct proper SSA form with correct PHI nodes for the new CFG structure. @@ -994,6 +1212,87 @@ pub fn apply_patch_plan(ssa: &mut SsaFunction, plan: &PatchPlan) -> Reconstructi } } +/// Recovers the value a phi should take on an edge created by a redirect. +/// +/// Unflattening rewires `source -> dispatcher -> target` into `source -> target`. +/// The phi at `target` recorded its incoming value against the dispatcher, not +/// against `source`, so after rewiring it has no operand for its new predecessor. +/// The value is still recoverable: the dispatcher's own phi records what each +/// case block contributed, so following `target`'s dispatcher-side operand back +/// through the dispatcher's phi to its operand for `source` yields the value that +/// was live when `source` finished executing. +/// +/// Chains of dispatcher blocks are followed up to [`MAX_PHI_RESOLUTION_HOPS`] +/// times. Returns `None` if the chain cannot be resolved to a value that still +/// has a definition, in which case the caller leaves the operand absent rather +/// than inventing one — a wrong value here would silently corrupt the recovered +/// program, which is worse than a failed unflattening. +fn resolve_redirected_operand( + original_phis: &PhiSnapshot, + defined: &BTreeSet, + block: usize, + phi_result: SsaVarId, + new_pred: usize, + live_preds: &BTreeSet, +) -> Option { + // Operands of this phi that are no longer reachable: these are the edges the + // patch removed, and one of them is the dispatcher path from `new_pred`. + let operands = original_phis.get(&block)?.get(&phi_result)?; + + for &(old_pred, value) in operands { + if live_preds.contains(&old_pred) { + continue; + } + + // Walk back through the removed blocks' phis looking for what `new_pred` + // contributed. + let mut current_block = old_pred; + let mut current_value = value; + for _ in 0..MAX_PHI_RESOLUTION_HOPS { + let Some(block_phis) = original_phis.get(¤t_block) else { + break; + }; + let Some(chain_operands) = block_phis.get(¤t_value) else { + // `current_value` is not defined by a phi here, so it is a plain + // definition that was already live before the dispatcher — usable + // only if it survived the patch. + break; + }; + + if let Some(&(_, from_source)) = + chain_operands.iter().find(|(pred, _)| *pred == new_pred) + { + if defined.contains(&from_source) { + return Some(from_source); + } + current_value = from_source; + current_block = new_pred; + continue; + } + + // Not contributed directly by `new_pred`; step through the single + // unreachable predecessor if there is exactly one candidate. + let mut candidates = chain_operands + .iter() + .filter(|(pred, _)| !live_preds.contains(pred)); + let &(next_block, next_value) = candidates.next()?; + if candidates.next().is_some() { + // Ambiguous — several removed edges could supply this value, and + // guessing risks selecting one that does not dominate `new_pred`. + return None; + } + current_block = next_block; + current_value = next_value; + } + + if defined.contains(¤t_value) { + return Some(current_value); + } + } + + None +} + /// Filters out state-tainted instructions from a block. /// /// An instruction is filtered if: diff --git a/dotscope/src/deobfuscation/passes/unflattening/spill.rs b/dotscope/src/deobfuscation/passes/unflattening/spill.rs new file mode 100644 index 00000000..03d7e7ba --- /dev/null +++ b/dotscope/src/deobfuscation/passes/unflattening/spill.rs @@ -0,0 +1,220 @@ +//! Giving cross-block values a storage location before the CFG is rewired. +//! +//! # Why this exists +//! +//! Unflattening restructures control flow: case blocks are redirected past the +//! dispatcher, merge blocks are cloned, and dead dispatch machinery is removed. +//! The resulting CFG has edges that did not exist before, so the SSA form has to +//! be reconstructed afterwards by [`SsaFunction::rebuild_ssa`]. +//! +//! Reconstruction is a reaching-definition problem, and it can only be solved +//! for values that have an identity independent of the control flow — a storage +//! location. Arguments and locals have one: a slot index. Rebuild re-derives +//! their versions and phi placement from the definitions that reach each slot, +//! whatever shape the CFG has taken. +//! +//! Stack temporaries do not. A temporary produced in one block and consumed in +//! another exists only as an SSA name, and the fact that two names in different +//! blocks denote the same value is recorded *solely* in the phi node that merges +//! them. Rewiring the CFG invalidates exactly that record: a phi describing a +//! merge that no longer exists is discarded, and with it the only evidence of +//! what the value was. Rebuild is then asked to find a reaching definition for a +//! value it has no way to track, and the result is a use with no definition — +//! reported as an undefined-variable failure in whichever block the value was +//! needed. +//! +//! No amount of phi bookkeeping during patching fixes this, because the +//! information is missing before patching starts: for an edge the patch +//! *creates*, no phi ever recorded what flows along it. +//! +//! # What this does +//! +//! Every value that crosses a block boundary is promoted to a local slot before +//! the CFG is touched. This is the classical "spill temporaries before +//! restructuring" step. Afterwards every cross-block value has a storage +//! location, so rebuild can reconstruct versions and phis for all of them by the +//! ordinary reaching-definition algorithm, and the transformation is correct for +//! any CFG rewiring rather than only the shapes whose phis happen to survive. +//! +//! Only values that genuinely cross a boundary are promoted. Temporaries used +//! within the block that defines them keep their `Phi` origin, so the local +//! count grows by the number of values live across edges — for a flattened +//! method, the values carried through the dispatcher, which is precisely the set +//! that has to survive unflattening. + +use std::collections::{BTreeMap, BTreeSet}; + +use crate::analysis::{SsaFunction, SsaVarId, VariableOrigin}; + +/// Narrows a variable id to the union-find key space. +/// +/// Ids beyond `u32::MAX` cannot occur — the variable table is indexed by `usize` +/// but allocated densely from zero — so saturating is unreachable in practice. +fn var_key(var: SsaVarId) -> u32 { + u32::try_from(var.index()).unwrap_or(u32::MAX) +} + +/// Union-find over SSA variable ids, used to group names that a phi proves +/// denote the same value. +struct ValueClasses { + parent: Vec, +} + +impl ValueClasses { + fn new(capacity: usize) -> Self { + let parent = (0..u32::try_from(capacity).unwrap_or(u32::MAX)).collect(); + Self { parent } + } + + fn find(&mut self, var: u32) -> u32 { + let mut root = var; + while let Some(&parent) = self.parent.get(root as usize) { + if parent == root { + break; + } + root = parent; + } + // Path compression. + let mut current = var; + while let Some(&parent) = self.parent.get(current as usize) { + if parent == root { + break; + } + if let Some(slot) = self.parent.get_mut(current as usize) { + *slot = root; + } + current = parent; + } + root + } + + fn union(&mut self, a: u32, b: u32) { + let (ra, rb) = (self.find(a), self.find(b)); + if ra != rb { + if let Some(slot) = self.parent.get_mut(rb as usize) { + *slot = ra; + } + } + } +} + +/// Promotes every value that crosses a block boundary to a local slot. +/// +/// Must be called on the function that is about to be patched, before any +/// terminator is rewired — the phi graph it reads is the record of which names +/// denote the same value, and patching destroys it. +/// +/// Returns the number of new local slots allocated. +pub fn promote_cross_block_values(ssa: &mut SsaFunction) -> usize { + let capacity = ssa.var_id_capacity(); + if capacity == 0 { + return 0; + } + + // Where each variable is defined, and whether it is ever referenced from a + // block other than that one. + let mut def_block: BTreeMap = BTreeMap::new(); + for block in ssa.blocks() { + let id = block.id(); + for phi in block.phi_nodes() { + def_block.insert(phi.result(), id); + } + for instr in block.instructions() { + if let Some(dest) = instr.def() { + def_block.insert(dest, id); + } + } + } + + let mut crosses: BTreeSet = BTreeSet::new(); + let mut classes = ValueClasses::new(capacity); + + for block in ssa.blocks() { + let id = block.id(); + + for phi in block.phi_nodes() { + // A phi is the statement that its result and operands are the same + // value observed on different paths, so the whole set is one class + // and every member of it crosses an edge. + let result = phi.result(); + crosses.insert(result); + for operand in phi.operands() { + let value = operand.value(); + crosses.insert(value); + classes.union(var_key(result), var_key(value)); + } + } + + for instr in block.instructions() { + for used in instr.uses() { + if def_block.get(&used).is_some_and(|&def| def != id) { + crosses.insert(used); + } + } + } + } + + if crosses.is_empty() { + return 0; + } + + // Group the crossing values, and record which classes already have a + // storage location. A class containing an argument or a local is already + // reconstructible — rebuild treats that slot as canonical — so promoting it + // would only rename something that already works. + let mut members: BTreeMap> = BTreeMap::new(); + let mut anchored: BTreeSet = BTreeSet::new(); + for &var in &crosses { + let root = classes.find(var_key(var)); + members.entry(root).or_default().push(var); + if let Some(variable) = ssa.variable(var) { + if matches!( + variable.origin(), + VariableOrigin::Argument(_) | VariableOrigin::Local(_) + ) { + anchored.insert(root); + } + } + } + + let mut next_local = u16::try_from(ssa.num_locals()).unwrap_or(u16::MAX); + let mut promotions: Vec<(SsaVarId, u16)> = Vec::new(); + for (root, vars) in members { + if anchored.contains(&root) { + continue; + } + if next_local == u16::MAX { + // Local indices are u16 in CIL; a method needing more than that is + // beyond what we can represent, so leave the rest alone rather than + // aliasing two values onto one slot. + break; + } + let slot = next_local; + next_local = next_local.saturating_add(1); + for var in vars { + promotions.push((var, slot)); + } + } + + if promotions.is_empty() { + return 0; + } + + // Origin and rename group must move together. `rebuild_ssa` decides which + // names denote the same value from the rename group, while its + // argument/local representative map is keyed on the origin; if the two + // disagree the value is silently split and ends up with no reaching + // definition. The group for a local is `num_args + index`, as documented on + // `SsaFunction::rename_groups`. + let num_args = u32::try_from(ssa.num_args()).unwrap_or(0); + for (var, slot) in &promotions { + if let Some(variable) = ssa.variable_mut(*var) { + variable.set_origin(VariableOrigin::Local(*slot)); + } + ssa.set_rename_group(*var, num_args.saturating_add(u32::from(*slot))); + } + + let added = usize::from(next_local).saturating_sub(ssa.num_locals()); + ssa.set_num_locals(usize::from(next_local), ssa.original_num_locals()); + added +} diff --git a/dotscope/src/deobfuscation/passes/unflattening/tracer/context.rs b/dotscope/src/deobfuscation/passes/unflattening/tracer/context.rs index e1f7b8ce..1dad825b 100644 --- a/dotscope/src/deobfuscation/passes/unflattening/tracer/context.rs +++ b/dotscope/src/deobfuscation/passes/unflattening/tracer/context.rs @@ -12,14 +12,15 @@ //! semantic methods that encapsulate invariants (visit budgets, case loop //! detection thresholds, expression switch mode transitions). -use std::{collections::BTreeSet, mem}; +use std::mem; use analyssa::BitSet; +use rustc_hash::FxHashSet; use crate::{ analysis::{ - cff_taint_config, ConstValue, SsaEvaluator, SsaFunction, SsaOp, SsaVarId, SsaVariable, - TaintAnalysis, + cff_taint_config, ConstValue, EvaluatorMark, SsaEvaluator, SsaFunction, SsaOp, SsaVarId, + SsaVariable, TaintAnalysis, VariableOrigin, }, deobfuscation::passes::unflattening::{ tracer::{helpers, types::TracedDispatcher}, @@ -28,6 +29,75 @@ use crate::{ CilObject, }; +/// Multiplier applied to `max_block_visits` to derive the monotonic global +/// visit cap. +/// +/// Generous enough that methods which trace normally never reach it — only +/// pathological nesting, where the per-arm budget reset would otherwise let +/// tracing run unbounded, is cut off. +const GLOBAL_VISIT_BUDGET_FACTOR: usize = 4; + +/// Per-block properties that depend only on the shape of the SSA and on which +/// blocks are dispatchers — never on the values a particular path carries. +/// +/// The tracer asks these questions once per block *visit*, and a flattened +/// method is visited millions of times, so answering them by re-walking the +/// CFG turns constant-time predicates into a dominant cost. Every field here is +/// a pure function of `(ssa, dispatcher, other_dispatcher_blocks)`, so a single +/// computation is valid for the whole trace. +struct BlockFacts { + /// Whether the block is a direct target of the dispatcher switch. + /// + /// Replaces a linear scan of the switch's target list, which for a + /// flattened method has one entry per case. + dispatch_target: Vec, + + /// Whether the block is a dispatcher of a *different* CFF instance in the + /// same method. + other_dispatcher: Vec, + + /// Jump target of the block if it is a constant-producer, else `None`. + /// + /// See [`helpers::const_producer_target`], which this caches. + const_producer: Vec>, + + /// Memoized [`overflow_dispatch_site`](Self::overflow_dispatch_site) + /// answers: `None` until first queried. + /// + /// Computed lazily rather than eagerly because the predecessor walk is + /// only ever asked about blocks ending in an equality comparison, a small + /// minority of the CFG. + overflow_site: Vec>, +} + +impl BlockFacts { + fn new(ssa: &SsaFunction, dispatcher: Option<&TracedDispatcher>) -> Self { + let count = ssa.block_count(); + let mut dispatch_target = vec![false; count]; + if let Some(d) = dispatcher { + for &target in d.targets.iter().chain(std::iter::once(&d.default)) { + if let Some(slot) = dispatch_target.get_mut(target) { + *slot = true; + } + } + } + + let mut const_producer = vec![None; count]; + for block in ssa.blocks() { + if let Some(slot) = const_producer.get_mut(block.id()) { + *slot = helpers::const_producer_target(block); + } + } + + Self { + dispatch_target, + other_dispatcher: vec![false; count], + const_producer, + overflow_site: vec![None; count], + } + } +} + /// Context for tree-based tracing. /// /// Fields are private — engine and helpers interact through semantic methods @@ -41,7 +111,32 @@ pub struct TreeTraceContext<'a> { state_tainted: BitSet, next_node_id: usize, total_visits: usize, - visited_states: BTreeSet<(usize, i64)>, + /// Monotonic visit counter that is never reset, unlike `total_visits`. + /// + /// `total_visits` is deliberately reset when entering an expression-switch + /// false arm so each arm gets its own budget. On heavily nested methods that + /// reset fires often enough that the effective budget becomes unbounded — a + /// 348-block ConfuserEx method reached 3.3M block visits against a 50k + /// budget. This counter is the backstop: it bounds total tracing work per + /// context while leaving the per-arm budget semantics untouched. + global_visits: usize, + /// Hard cap on `global_visits`, derived from `max_block_visits`. + max_global_visits: usize, + /// Blocks already entered on the current path, keyed by execution state. + /// + /// Only ever grows while a path is followed — [`mark_visited`](Self::mark_visited) + /// is its sole writer — so a fork records the keys it adds and a restore + /// removes them again, the same trick the evaluator uses. Copying the set + /// per fork was the tracer's largest remaining cost once the evaluator + /// stopped being copied. + visited_states: FxHashSet<(usize, i64)>, + /// Keys added to [`visited_states`](Self::visited_states) since journaling + /// began, oldest first. + visited_journal: Vec<(usize, i64)>, + /// Whether [`visited_journal`](Self::visited_journal) is being recorded. + /// + /// Latched on by the first snapshot, mirroring the evaluator's own journal. + visit_journaling: bool, last_case_index: usize, visited_case_counts: Vec, /// Last state value dispatched to each case, parallel to `visited_case_counts`. @@ -55,6 +150,17 @@ pub struct TreeTraceContext<'a> { max_tree_depth: usize, other_dispatcher_blocks: Vec, no_fork: bool, + /// Structural per-block answers, shared by every visit. See [`BlockFacts`]. + facts: BlockFacts, + /// SSA variables grouped by the local slot they originate from, in variable + /// table order. + /// + /// The cross-scope bridge in the inner trace loop needs "the first variable + /// for this local that currently has a value". Scanning the whole variable + /// table for that runs inside the per-instruction loop, making the trace + /// quadratic in a method's variable count; this index makes it proportional + /// to the number of versions of the one local involved. + vars_by_local: Vec>, } impl<'a> TreeTraceContext<'a> { @@ -68,6 +174,19 @@ impl<'a> TreeTraceContext<'a> { config: &UnflattenConfig, assembly: Option<&'a CilObject>, ) -> Self { + let mut vars_by_local: Vec> = Vec::new(); + for var in ssa.variables() { + if let VariableOrigin::Local(idx) = var.origin() { + let idx = usize::from(idx); + if vars_by_local.len() <= idx { + vars_by_local.resize_with(idx.saturating_add(1), Vec::new); + } + if let Some(slot) = vars_by_local.get_mut(idx) { + slot.push(var.id()); + } + } + } + Self { ssa, evaluator: SsaEvaluator::new(ssa, config.pointer_size), @@ -76,7 +195,13 @@ impl<'a> TreeTraceContext<'a> { state_tainted: BitSet::new(ssa.var_id_capacity()), next_node_id: 0, total_visits: 0, - visited_states: BTreeSet::new(), + global_visits: 0, + max_global_visits: config + .max_block_visits + .saturating_mul(GLOBAL_VISIT_BUDGET_FACTOR), + visited_states: FxHashSet::default(), + visited_journal: Vec::new(), + visit_journaling: false, last_case_index: usize::MAX, visited_case_counts: Vec::new(), last_case_state: Vec::new(), @@ -84,6 +209,8 @@ impl<'a> TreeTraceContext<'a> { max_tree_depth: config.max_tree_depth, other_dispatcher_blocks: Vec::new(), no_fork: false, + facts: BlockFacts::new(ssa, None), + vars_by_local, } } @@ -178,6 +305,7 @@ impl<'a> TreeTraceContext<'a> { // (+1 for default, which uses targets.len() as its index). ctx.visited_case_counts = vec![0u8; dispatcher.targets.len().saturating_add(1)]; ctx.last_case_state = vec![None; dispatcher.targets.len().saturating_add(1)]; + ctx.facts = BlockFacts::new(ssa, Some(&dispatcher)); ctx.dispatcher = Some(dispatcher); ctx } @@ -223,9 +351,11 @@ impl<'a> TreeTraceContext<'a> { /// Returns true if the given block is a direct target of the dispatcher /// (case block or default). pub fn is_dispatch_target(&self, block: usize) -> bool { - self.dispatcher - .as_ref() - .is_some_and(|d| d.targets.contains(&block) || d.default == block) + self.facts + .dispatch_target + .get(block) + .copied() + .unwrap_or(false) } /// Returns the state variable (phi at the dispatcher), if detected. @@ -241,14 +371,65 @@ impl<'a> TreeTraceContext<'a> { /// Returns true if the given block is another CFF dispatcher in the same /// method (not the one we're tracing for). pub fn is_other_dispatcher(&self, block: usize) -> bool { - self.other_dispatcher_blocks.contains(&block) + self.facts + .other_dispatcher + .get(block) + .copied() + .unwrap_or(false) } /// Sets the blocks of other CFF dispatchers in this method. pub fn set_other_dispatcher_blocks(&mut self, blocks: Vec) { + for slot in &mut self.facts.other_dispatcher { + *slot = false; + } + for &block in &blocks { + if let Some(slot) = self.facts.other_dispatcher.get_mut(block) { + *slot = true; + } + } + // `is_overflow_dispatch_site` consults the other-dispatcher set, so any + // answer cached before this point was computed against a stale one. + for slot in &mut self.facts.overflow_site { + *slot = None; + } self.other_dispatcher_blocks = blocks; } + /// Returns the jump target of `block` if it is a constant-producer block. + /// + /// Cached form of [`helpers::const_producer_target`]. + pub fn const_producer_target(&self, block: usize) -> Option { + self.facts.const_producer.get(block).copied().flatten() + } + + /// Returns the cached answer for `block`, computing it with `compute` on + /// first use. + /// + /// See [`BlockFacts::overflow_site`] for why this one is lazy. + pub fn overflow_dispatch_site( + &mut self, + block: usize, + compute: impl FnOnce(&Self) -> bool, + ) -> bool { + if let Some(cached) = self.facts.overflow_site.get(block).copied().flatten() { + return cached; + } + let answer = compute(self); + if let Some(slot) = self.facts.overflow_site.get_mut(block) { + *slot = Some(answer); + } + answer + } + + /// Returns the SSA variables that originate from local slot `local_idx`, in + /// variable table order. + pub fn vars_for_local(&self, local_idx: u16) -> &[SsaVarId] { + self.vars_by_local + .get(usize::from(local_idx)) + .map_or(&[], Vec::as_slice) + } + /// Checks if a variable is state-tainted. pub fn is_tainted(&self, var: SsaVarId) -> bool { self.state_tainted.contains(var.index()) @@ -317,13 +498,17 @@ impl<'a> TreeTraceContext<'a> { /// Marks a block as visited in the current execution context. pub fn mark_visited(&mut self, block: usize) { - self.visited_states.insert((block, self.visit_state())); + let key = (block, self.visit_state()); + if self.visited_states.insert(key) && self.visit_journaling { + self.visited_journal.push(key); + } } /// Increments the visit counter and returns true if the budget is exceeded. pub fn check_visit_budget(&mut self) -> bool { self.total_visits = self.total_visits.saturating_add(1); - self.total_visits > self.max_block_visits + self.global_visits = self.global_visits.saturating_add(1); + self.total_visits > self.max_block_visits || self.global_visits > self.max_global_visits } /// Returns the maximum tree depth allowed. @@ -379,12 +564,15 @@ impl<'a> TreeTraceContext<'a> { self.no_fork } - /// Takes a full snapshot of the mutable context state that must be - /// preserved across branch/switch forks. - pub fn snapshot(&self) -> ContextSnapshot<'a> { + /// Takes a snapshot of the mutable context state that must be preserved + /// across branch/switch forks. + /// + /// The evaluator is marked rather than copied — see [`ContextSnapshot`]. + pub fn snapshot(&mut self) -> ContextSnapshot { + self.visit_journaling = true; ContextSnapshot { - evaluator: self.evaluator.clone(), - visited_states: self.visited_states.clone(), + evaluator: self.evaluator.checkpoint(), + visited_mark: self.visited_journal.len(), last_case_index: self.last_case_index, visited_case_counts: self.visited_case_counts.clone(), last_case_state: self.last_case_state.clone(), @@ -392,9 +580,14 @@ impl<'a> TreeTraceContext<'a> { } /// Restores all mutable context fields from a snapshot, consuming it. - pub fn restore(&mut self, snap: ContextSnapshot<'a>) { - self.evaluator = snap.evaluator; - self.visited_states = snap.visited_states; + pub fn restore(&mut self, snap: ContextSnapshot) { + self.evaluator.rollback(snap.evaluator); + while self.visited_journal.len() > snap.visited_mark { + let Some(key) = self.visited_journal.pop() else { + break; + }; + self.visited_states.remove(&key); + } self.last_case_index = snap.last_case_index; self.visited_case_counts = snap.visited_case_counts; self.last_case_state = snap.last_case_state; @@ -467,7 +660,11 @@ impl<'a> TreeTraceContext<'a> { state_tainted: self.state_tainted.clone(), next_node_id: node_id_offset, total_visits: 0, - visited_states: BTreeSet::new(), + global_visits: 0, + max_global_visits: self.max_global_visits, + visited_states: FxHashSet::default(), + visited_journal: Vec::new(), + visit_journaling: false, last_case_index: usize::MAX, visited_case_counts: vec![0u8; case_count_len], last_case_state: vec![None; case_count_len], @@ -475,6 +672,13 @@ impl<'a> TreeTraceContext<'a> { max_tree_depth: self.max_tree_depth, other_dispatcher_blocks: self.other_dispatcher_blocks.clone(), no_fork: false, + facts: BlockFacts { + dispatch_target: self.facts.dispatch_target.clone(), + other_dispatcher: self.facts.other_dispatcher.clone(), + const_producer: self.facts.const_producer.clone(), + overflow_site: self.facts.overflow_site.clone(), + }, + vars_by_local: self.vars_by_local.clone(), } } @@ -492,21 +696,33 @@ impl<'a> TreeTraceContext<'a> { /// Snapshot of `TreeTraceContext` mutable state saved at branch/switch fork /// points. Allows the iterative tracer to restore context before tracing /// each alternative arm. -pub struct ContextSnapshot<'a> { - evaluator: SsaEvaluator<'a>, - visited_states: BTreeSet<(usize, i64)>, +/// +/// The evaluator is held as a mark into its undo journal, not as a copy. A +/// flattened method forks millions of times and the evaluator's state grows +/// with everything the trace has learned, so copying it per fork costs more +/// than the tracing itself — measured at 99% of tracer time on a NetReactor +/// sample. Rolling back to a mark instead costs one entry per value the +/// abandoned arm actually changed. +/// +/// This makes fork discipline part of the contract: marks are released in +/// reverse order of creation. The work stack in +/// [`engine`](super::engine) guarantees it — a snapshot lives in a work item, +/// and every item pushed after it is popped before it is. +pub struct ContextSnapshot { + evaluator: EvaluatorMark, + visited_mark: usize, last_case_index: usize, visited_case_counts: Vec, last_case_state: Vec>, } -impl<'a> ContextSnapshot<'a> { +impl ContextSnapshot { /// Clones this snapshot (used when restoring the same snapshot for /// multiple switch case arms). pub fn clone_snapshot(&self) -> Self { Self { evaluator: self.evaluator.clone(), - visited_states: self.visited_states.clone(), + visited_mark: self.visited_mark, last_case_index: self.last_case_index, visited_case_counts: self.visited_case_counts.clone(), last_case_state: self.last_case_state.clone(), diff --git a/dotscope/src/deobfuscation/passes/unflattening/tracer/engine.rs b/dotscope/src/deobfuscation/passes/unflattening/tracer/engine.rs index 6cb47388..f75918e9 100644 --- a/dotscope/src/deobfuscation/passes/unflattening/tracer/engine.rs +++ b/dotscope/src/deobfuscation/passes/unflattening/tracer/engine.rs @@ -23,7 +23,7 @@ //! until hitting a terminator that requires forking or state transition. //! Returns either a completed node or a [`ForkRequest`]. -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeSet; use crate::{ analysis::{ @@ -32,8 +32,8 @@ use crate::{ }, deobfuscation::passes::unflattening::tracer::{ context::{ContextSnapshot, TreeTraceContext}, - helpers::{const_producer_target, detect_expression_switch, resolve_call_result}, - types::{InstructionWithValues, StopReason, TraceNode, TraceTerminator}, + helpers::{detect_expression_switch, resolve_call_result}, + types::{StopReason, TraceNode, TraceTerminator}, }, }; @@ -42,7 +42,7 @@ use crate::{ /// Each variant represents a pending operation that was deferred when the /// tracer encountered a branch or switch fork. Instead of recursing, the /// tracer pushes these frames and processes them one at a time. -enum WorkItem<'a> { +enum WorkItem { /// Start tracing a block. The resulting TraceNode becomes `current_result`. TraceBlock { block: usize, depth: usize }, @@ -63,7 +63,7 @@ enum WorkItem<'a> { condition: SsaVarId, false_target: usize, depth: usize, - snapshot: ContextSnapshot<'a>, + snapshot: ContextSnapshot, case_counts_snapshot: Option>, is_expr_switch: bool, }, @@ -86,7 +86,7 @@ enum WorkItem<'a> { targets: Vec, default_target: usize, depth: usize, - snapshot: ContextSnapshot<'a>, + snapshot: ContextSnapshot, completed_cases: Vec<(i64, Box)>, next_case_index: usize, }, @@ -111,7 +111,7 @@ pub fn trace_from_block( block_idx: usize, depth: usize, ) -> TraceNode { - let mut work_stack: Vec> = Vec::new(); + let mut work_stack: Vec = Vec::new(); let mut current_result: Option = None; work_stack.push(WorkItem::TraceBlock { @@ -338,11 +338,11 @@ pub fn trace_from_block( /// stop). When a user branch/switch fork is needed, pushes continuation frames /// onto the `work_stack` and returns the node-so-far as `current_result` so /// the outer `trace_from_block` loop can process the fork. -fn trace_from_block_linear<'a>( - ctx: &mut TreeTraceContext<'a>, +fn trace_from_block_linear( + ctx: &mut TreeTraceContext<'_>, block_idx: usize, depth: usize, - work_stack: &mut Vec>, + work_stack: &mut Vec, ) -> TraceNode { // State transition chain — same iterative mechanism as before. let mut transition_chain: Vec<(TraceNode, i64, usize)> = Vec::new(); @@ -469,13 +469,13 @@ fn trace_from_block_linear<'a>( /// Fork request returned by the inner tracer when it encounters a user /// branch or switch that needs to trace multiple arms. -enum ForkRequest<'a> { +enum ForkRequest { Branch { block_idx: usize, condition: SsaVarId, true_target: usize, false_target: usize, - snapshot: ContextSnapshot<'a>, + snapshot: ContextSnapshot, case_counts_snapshot: Option>, is_expr_switch: bool, }, @@ -484,7 +484,7 @@ enum ForkRequest<'a> { value: SsaVarId, targets: Vec, default_target: usize, - snapshot: ContextSnapshot<'a>, + snapshot: ContextSnapshot, is_foreign: bool, }, } @@ -499,11 +499,11 @@ enum ForkRequest<'a> { /// `(node, Some(ForkRequest))` for the caller to trace both arms /// - A **leaf** (return, throw, loop, stop) → returns `(node, None)` as a /// completed terminal node -fn trace_from_block_inner<'a>( - ctx: &mut TreeTraceContext<'a>, +fn trace_from_block_inner( + ctx: &mut TreeTraceContext<'_>, block_idx: usize, depth: usize, -) -> (TraceNode, Option>) { +) -> (TraceNode, Option) { let mut node = TraceNode::new(ctx.next_id(), block_idx); // Safety limits @@ -648,25 +648,25 @@ fn trace_from_block_inner<'a>( // Process instructions with cross-scope variable bridging for instr in block.instructions() { - let step = trace_instruction(ctx, instr, current_block); - node.add_instruction(step); + trace_instruction(ctx, instr); // Bridge unknown local-variable references from known definitions // of the same local index (cross-scope reaching definitions). if let SsaOp::Copy { dest, src } = instr.op() { if ctx.evaluator().get(*dest).is_none() { - if let Some(src_var) = ssa.variable(*src) { - if let VariableOrigin::Local(local_idx) = src_var.origin() { - for var in ssa.variables() { - if var.id() != *src - && matches!(var.origin(), VariableOrigin::Local(li) if li == local_idx) - { - if let Some(val) = ctx.evaluator().get(var.id()).cloned() { - ctx.evaluator_mut().set_symbolic_expr(*dest, val); - break; - } - } - } + if let Some(VariableOrigin::Local(local_idx)) = + ssa.variable(*src).map(|v| v.origin()) + { + // The context indexes variables by local slot, so this + // walks the versions of one local rather than the whole + // variable table — same order, same first match. + let bridged = ctx + .vars_for_local(local_idx) + .iter() + .filter(|&&var| var != *src) + .find_map(|&var| ctx.evaluator().get(var).cloned()); + if let Some(val) = bridged { + ctx.evaluator_mut().set_symbolic_expr(*dest, val); } } } @@ -781,7 +781,7 @@ fn bridge_phi_operands(ctx: &mut TreeTraceContext<'_>, block_idx: usize) { /// /// Tells the inner trace loop how to proceed after processing a block's /// terminator (jump, branch, switch, return, etc.). -enum TerminatorResult<'a> { +enum TerminatorResult { /// Follow an unconditional edge to the next block (jump, leave). /// The caller continues the linear block chain within the same node. Continue(usize), @@ -799,7 +799,7 @@ enum TerminatorResult<'a> { condition: SsaVarId, true_target: usize, false_target: usize, - snapshot: ContextSnapshot<'a>, + snapshot: ContextSnapshot, case_counts_snapshot: Option>, is_expr_switch: bool, }, @@ -809,19 +809,19 @@ enum TerminatorResult<'a> { value: SsaVarId, targets: Vec, default_target: usize, - snapshot: ContextSnapshot<'a>, + snapshot: ContextSnapshot, is_foreign: bool, }, } /// Handles a block terminator, potentially forking the trace. -fn handle_terminator<'a>( - ctx: &mut TreeTraceContext<'a>, +fn handle_terminator( + ctx: &mut TreeTraceContext<'_>, block: &SsaBlock, block_idx: usize, node: &mut TraceNode, depth: usize, -) -> TerminatorResult<'a> { +) -> TerminatorResult { let Some(terminator) = block.instructions().last() else { node.set_terminator(TraceTerminator::Stopped { reason: StopReason::UnknownControlFlow { block: block_idx }, @@ -937,13 +937,8 @@ fn handle_terminator<'a>( } else { // BranchCmp in no_fork mode has an additional check: allow forking // for conditional CFF state transitions even when no_fork is set. - let is_expr_switch = detect_expression_switch( - ctx.ssa(), - *true_target, - *false_target, - ctx.state_tainted(), - ) - .is_some(); + let is_expr_switch = + detect_expression_switch(ctx, *true_target, *false_target).is_some(); if ctx.no_fork() && !is_expr_switch { let is_cff_state_transition = @@ -988,17 +983,15 @@ fn handle_terminator<'a>( /// /// Detects expression switches, respects no_fork mode, creates snapshot /// and returns a ForkBranch result. -fn handle_user_branch_fork<'a>( - ctx: &mut TreeTraceContext<'a>, +fn handle_user_branch_fork( + ctx: &mut TreeTraceContext<'_>, block_idx: usize, true_target: usize, false_target: usize, condition: SsaVarId, _: usize, -) -> TerminatorResult<'a> { - let is_expr_switch = - detect_expression_switch(ctx.ssa(), true_target, false_target, ctx.state_tainted()) - .is_some(); +) -> TerminatorResult { + let is_expr_switch = detect_expression_switch(ctx, true_target, false_target).is_some(); // In no_fork mode, only fork for expression switches. if ctx.no_fork() && !is_expr_switch { @@ -1016,14 +1009,14 @@ fn handle_user_branch_fork<'a>( } /// Builds a ForkBranch result with the appropriate snapshot. -fn build_fork_branch<'a>( - ctx: &mut TreeTraceContext<'a>, +fn build_fork_branch( + ctx: &mut TreeTraceContext<'_>, block_idx: usize, true_target: usize, false_target: usize, condition: SsaVarId, is_expr_switch: bool, -) -> TerminatorResult<'a> { +) -> TerminatorResult { let snapshot = ctx.snapshot(); let case_counts_snapshot = if is_expr_switch { None @@ -1056,11 +1049,8 @@ fn is_conditional_state_transition( return false; }; - let true_merge = ctx.ssa().block(true_target).and_then(const_producer_target); - let false_merge = ctx - .ssa() - .block(false_target) - .and_then(const_producer_target); + let true_merge = ctx.const_producer_target(true_target); + let false_merge = ctx.const_producer_target(false_target); match (true_merge, false_merge) { (Some(tm), Some(fm)) if tm == fm => { @@ -1092,7 +1082,17 @@ fn is_conditional_state_transition( /// through other pure/overflow blocks), the current block is part of the /// chain and a tainted BranchCmp can safely be forked as a CFF continuation /// rather than preserved as user code. -fn is_overflow_dispatch_site(ctx: &TreeTraceContext<'_>, block_idx: usize) -> bool { +/// Memoizing wrapper — the answer depends only on the CFG and on which blocks +/// are dispatchers, both fixed for the lifetime of a trace, while the walk +/// itself allocates and visits up to eight levels of predecessors. On a +/// flattened method the same blocks are asked about millions of times. +fn is_overflow_dispatch_site(ctx: &mut TreeTraceContext<'_>, block_idx: usize) -> bool { + ctx.overflow_dispatch_site(block_idx, |ctx| { + compute_overflow_dispatch_site(ctx, block_idx) + }) +} + +fn compute_overflow_dispatch_site(ctx: &TreeTraceContext<'_>, block_idx: usize) -> bool { let Some(dispatcher) = ctx.dispatcher_block() else { return false; }; @@ -1205,15 +1205,15 @@ fn default_has_overflow_check(ctx: &TreeTraceContext<'_>, default: usize) -> boo } /// Handles a switch terminator (dispatcher or user switch). -fn handle_switch<'a>( - ctx: &mut TreeTraceContext<'a>, +fn handle_switch( + ctx: &mut TreeTraceContext<'_>, node: &mut TraceNode, block_idx: usize, value: &SsaVarId, targets: &[usize], default: &usize, _: usize, -) -> TerminatorResult<'a> { +) -> TerminatorResult { let is_dispatcher = ctx.is_dispatcher_block(block_idx); let is_argument = ctx @@ -1314,13 +1314,13 @@ fn handle_switch<'a>( } /// Handles a user switch by forking for all cases. -fn handle_user_switch<'a>( - ctx: &mut TreeTraceContext<'a>, +fn handle_user_switch( + ctx: &mut TreeTraceContext<'_>, block_idx: usize, value: &SsaVarId, targets: &[usize], default: &usize, -) -> TerminatorResult<'a> { +) -> TerminatorResult { // No-fork mode: follow the evaluated path or first target. if ctx.no_fork() { let target = ctx @@ -1345,24 +1345,15 @@ fn handle_user_switch<'a>( } } -/// Traces a single instruction, evaluating it and recording values. -fn trace_instruction( - ctx: &mut TreeTraceContext<'_>, - instr: &SsaInstruction, - block_idx: usize, -) -> InstructionWithValues { - // Capture input values BEFORE evaluation - let input_values: BTreeMap = instr - .uses() - .iter() - .filter_map(|&var| { - ctx.evaluator() - .get_concrete(var) - .and_then(ConstValue::as_i64) - .map(|v| (var, v)) - }) - .collect(); - +/// Evaluates a single instruction, updating the evaluator and taint state. +/// +/// The tracer used to record each instruction together with the concrete values +/// of its operands. Nothing consumed those values — the only reader of the log +/// needed the opcode, which is available from the SSA — while building it cost a +/// `BTreeMap` allocation and a full `SsaInstruction` clone per instruction per +/// visit. On a heavily nested method that is billions of allocations, so the log +/// is no longer produced. +fn trace_instruction(ctx: &mut TreeTraceContext<'_>, instr: &SsaInstruction) { if ctx.any_tainted(&instr.uses()) { if let Some(def) = instr.def() { ctx.taint(def); @@ -1396,17 +1387,4 @@ fn trace_instruction( } } } - - // Capture output value AFTER evaluation - let output_value = instr - .def() - .and_then(|d| ctx.evaluator().get_concrete(d)) - .and_then(ConstValue::as_i64); - - InstructionWithValues { - instruction: instr.clone(), - block_idx, - input_values, - output_value, - } } diff --git a/dotscope/src/deobfuscation/passes/unflattening/tracer/helpers.rs b/dotscope/src/deobfuscation/passes/unflattening/tracer/helpers.rs index 0167a693..2a494f7a 100644 --- a/dotscope/src/deobfuscation/passes/unflattening/tracer/helpers.rs +++ b/dotscope/src/deobfuscation/passes/unflattening/tracer/helpers.rs @@ -15,8 +15,7 @@ //! - **Statistics** ([`compute_tree_stats`]): Recursive tree traversal counting //! nodes, branches, transitions, exits, and max depth -use std::collections::BTreeSet; - +use analyssa::BitSet; use rayon::prelude::*; use crate::{ @@ -32,7 +31,6 @@ use crate::{ metadata::{token::Token, typesystem::PointerSize}, CilObject, }; -use analyssa::BitSet; /// Traces exception handler entry blocks that were not visited by the main trace. /// @@ -208,36 +206,36 @@ pub fn resolve_call_result( /// tracer forks O(2^N) at these branches. With detection, both forks share /// accumulated tracking state so the false branch stops at the convergence point. pub fn detect_expression_switch( - ssa: &SsaFunction, + ctx: &TreeTraceContext<'_>, true_target: usize, false_target: usize, - tainted: &BitSet, ) -> Option { - let (Some(tb), Some(fb)) = (ssa.block(true_target), ssa.block(false_target)) else { - return None; - }; - - let true_merge = const_producer_target(tb)?; - let false_merge = const_producer_target(fb)?; + // The structural half of the test — are both arms constant-producers that + // meet at the same block — is a property of the CFG, so it is answered from + // the context's precomputed table rather than re-walked. Only the taint + // check below depends on trace state. + let true_merge = ctx.const_producer_target(true_target)?; + let false_merge = ctx.const_producer_target(false_target)?; if true_merge != false_merge { return None; } - let merge = ssa.block(true_merge)?; - if merge.phi_nodes().is_empty() { + let merge = ctx.ssa().block(true_merge)?; + let phis = merge.phi_nodes(); + if phis.is_empty() { return None; } - let phi_results: BTreeSet = - merge.phi_nodes().iter().map(|phi| phi.result()).collect(); + let tainted = ctx.state_tainted(); + let is_phi_result = |var: &SsaVarId| phis.iter().any(|phi| phi.result() == *var); let feeds_tainted = merge.instructions().iter().any(|instr| match instr.op() { SsaOp::Xor { left, right, .. } | SsaOp::Add { left, right, .. } | SsaOp::Sub { left, right, .. } | SsaOp::Mul { left, right, .. } => { - let one_is_phi = phi_results.contains(left) || phi_results.contains(right); + let one_is_phi = is_phi_result(left) || is_phi_result(right); let one_is_tainted = tainted.contains(left.index()) || tainted.contains(right.index()); one_is_phi && one_is_tainted } diff --git a/dotscope/src/deobfuscation/passes/unflattening/tracer/mod.rs b/dotscope/src/deobfuscation/passes/unflattening/tracer/mod.rs index 2258b6cf..ea274e01 100644 --- a/dotscope/src/deobfuscation/passes/unflattening/tracer/mod.rs +++ b/dotscope/src/deobfuscation/passes/unflattening/tracer/mod.rs @@ -145,7 +145,6 @@ fn build_trace_tree(ctx: &mut TreeTraceContext<'_>) -> TraceTree { for ht in &tree.handler_traces { compute_tree_stats(&ht.root, &mut tree.stats, 0); } - tree } diff --git a/dotscope/src/deobfuscation/passes/unflattening/tracer/types.rs b/dotscope/src/deobfuscation/passes/unflattening/tracer/types.rs index 639b9f3b..f19ea8b6 100644 --- a/dotscope/src/deobfuscation/passes/unflattening/tracer/types.rs +++ b/dotscope/src/deobfuscation/passes/unflattening/tracer/types.rs @@ -10,11 +10,19 @@ //! terminated by one of the [`TraceTerminator`] variants (state transition, user //! branch, exit, loop, or stop). -use std::collections::BTreeMap; - use analyssa::BitSet; +use smallvec::{smallvec, SmallVec}; + +use crate::analysis::SsaVarId; -use crate::analysis::{SsaInstruction, SsaVarId}; +/// Blocks recorded for one trace segment. +/// +/// A segment nearly always covers a single block — a 1200-block NetReactor +/// method produced 4.8 million nodes averaging about one block each — so the +/// common case is stored inline. Heap-allocating a one-element vector per node +/// cost ~92 million allocations across that method's dispatchers, and the +/// allocator churn showed up as the tracer's single largest expense. +pub type VisitedBlocks = SmallVec<[usize; 1]>; /// Information about the dispatcher found during tracing. #[derive(Debug, Clone)] @@ -55,22 +63,6 @@ pub enum StopReason { InfiniteLoop { block: usize }, } -/// An instruction together with the concrete values it used at this trace step. -#[derive(Debug, Clone)] -pub struct InstructionWithValues { - /// The SSA instruction that was executed. - pub instruction: SsaInstruction, - - /// Block index where this instruction lives. - pub block_idx: usize, - - /// Concrete values of input variables at this point. - pub input_values: BTreeMap, - - /// Concrete value of output variable (if instruction defines one). - pub output_value: Option, -} - /// A trace of an exception handler entry block. /// /// When CFF exists inside exception handler blocks (catch/finally/filter), @@ -136,11 +128,8 @@ pub struct TraceNode { /// The block index where this segment starts. pub start_block: usize, - /// Linear sequence of instructions in this segment. - pub instructions: Vec, - /// Blocks visited in this segment (in order). - pub blocks_visited: Vec, + pub blocks_visited: VisitedBlocks, /// How this segment ends. pub terminator: TraceTerminator, @@ -248,19 +237,13 @@ impl TraceNode { Self { id, start_block, - instructions: Vec::new(), - blocks_visited: vec![start_block], + blocks_visited: smallvec![start_block], terminator: TraceTerminator::Stopped { reason: StopReason::UnknownControlFlow { block: start_block }, }, } } - /// Adds an instruction to this node. - pub fn add_instruction(&mut self, instr: InstructionWithValues) { - self.instructions.push(instr); - } - /// Records visiting a block. pub fn visit_block(&mut self, block: usize) { self.blocks_visited.push(block); diff --git a/dotscope/src/deobfuscation/renamer/mod.rs b/dotscope/src/deobfuscation/renamer/mod.rs index adc0fb6c..4996cc5a 100644 --- a/dotscope/src/deobfuscation/renamer/mod.rs +++ b/dotscope/src/deobfuscation/renamer/mod.rs @@ -26,10 +26,14 @@ mod prompt; mod providers; mod validate; -pub use config::SmartRenameConfig; - use std::collections::{HashMap, HashSet}; +pub use config::SmartRenameConfig; + +use self::{ + cascade::CascadeRenamer, + providers::{SimpleNameGenerator, SimpleProvider}, +}; use crate::{ cilassembly::CilAssembly, deobfuscation::utils::{is_obfuscated_name, is_special_name}, @@ -37,11 +41,6 @@ use crate::{ CilObject, Result, }; -use self::{ - cascade::CascadeRenamer, - providers::{SimpleNameGenerator, SimpleProvider}, -}; - /// A provider that generates names from context. /// /// Implementors must be `Send + Sync` for use from rayon parallel iterators. @@ -395,6 +394,8 @@ fn update_row_name_field( #[cfg(test)] mod tests { + // Only the `smart-rename`-off equivalence test uses this. + #[cfg(not(feature = "smart-rename"))] use std::collections::HashSet; #[cfg(feature = "smart-rename")] use std::path::PathBuf; @@ -692,6 +693,13 @@ mod tests { /// `renames_collect(_, None)` should produce identical results to /// `renames_collect(_, Some(&SmartRenameConfig::default()))` — both use SimpleProvider. + /// + /// Only with `smart-rename` off. With the feature on, `create_provider` + /// honours the `Some(..)` by building a `LocalProvider`, and + /// `SmartRenameConfig::default()` carries an empty `model_path` that + /// `LocalProvider::initialize` correctly rejects — so the two calls take + /// different providers by design and there is no equivalence to assert. + #[cfg(not(feature = "smart-rename"))] #[test] fn test_cascade_with_smart_config_none_matches_default() { let assembly_a = load_sample(RENAMER_SAMPLE); diff --git a/dotscope/src/deobfuscation/renamer/prompt.rs b/dotscope/src/deobfuscation/renamer/prompt.rs index 3c1833b5..c6553eca 100644 --- a/dotscope/src/deobfuscation/renamer/prompt.rs +++ b/dotscope/src/deobfuscation/renamer/prompt.rs @@ -479,7 +479,6 @@ fn truncate_phases(phases: &[PhaseInfo], max_phases: usize) -> Vec<&PhaseInfo> { #[cfg(test)] mod tests { use super::*; - use crate::deobfuscation::renamer::context::{ ApiCallInfo, IdentifierKind, ParamInfo, PhaseInfo, RenameContext, }; diff --git a/dotscope/src/deobfuscation/renamer/providers/local.rs b/dotscope/src/deobfuscation/renamer/providers/local.rs index c4c15dfe..dbe65118 100644 --- a/dotscope/src/deobfuscation/renamer/providers/local.rs +++ b/dotscope/src/deobfuscation/renamer/providers/local.rs @@ -321,14 +321,14 @@ impl RenameProvider for LocalProvider { #[cfg(test)] mod tests { + use std::path::PathBuf; + use crate::deobfuscation::renamer::{ context::{IdentifierKind, RenameContext}, providers::local::LocalProvider, RenameProvider, SmartRenameConfig, }; - use std::path::PathBuf; - #[test] fn test_local_provider_config() { let config = SmartRenameConfig { diff --git a/dotscope/src/deobfuscation/techniques/bitmono/hooks.rs b/dotscope/src/deobfuscation/techniques/bitmono/hooks.rs index cfd1f93d..d59a5899 100644 --- a/dotscope/src/deobfuscation/techniques/bitmono/hooks.rs +++ b/dotscope/src/deobfuscation/techniques/bitmono/hooks.rs @@ -911,8 +911,9 @@ fn is_redirect_stub_memberref( #[cfg(test)] mod tests { - use crate::test::helpers::load_sample; - use crate::{deobfuscation::techniques::Technique, metadata::token::Token}; + use crate::{ + deobfuscation::techniques::Technique, metadata::token::Token, test::helpers::load_sample, + }; #[test] fn test_hook_mapping_token_extraction() { diff --git a/dotscope/src/deobfuscation/techniques/bitmono/junk.rs b/dotscope/src/deobfuscation/techniques/bitmono/junk.rs index 4bc18171..f9a3bcbd 100644 --- a/dotscope/src/deobfuscation/techniques/bitmono/junk.rs +++ b/dotscope/src/deobfuscation/techniques/bitmono/junk.rs @@ -123,8 +123,10 @@ impl Technique for BitMonoJunk { #[cfg(test)] mod tests { - use crate::deobfuscation::techniques::{bitmono::BitMonoJunk, Technique}; - use crate::test::helpers::load_sample; + use crate::{ + deobfuscation::techniques::{bitmono::BitMonoJunk, Technique}, + test::helpers::load_sample, + }; #[test] fn test_detect_positive() { diff --git a/dotscope/src/deobfuscation/techniques/bitmono/nops.rs b/dotscope/src/deobfuscation/techniques/bitmono/nops.rs index 26c2fb35..05559fde 100644 --- a/dotscope/src/deobfuscation/techniques/bitmono/nops.rs +++ b/dotscope/src/deobfuscation/techniques/bitmono/nops.rs @@ -126,8 +126,10 @@ impl Technique for BitMonoNops { #[cfg(test)] mod tests { - use crate::deobfuscation::techniques::{bitmono::BitMonoNops, Technique}; - use crate::test::helpers::load_sample; + use crate::{ + deobfuscation::techniques::{bitmono::BitMonoNops, Technique}, + test::helpers::load_sample, + }; #[test] fn test_detect_positive() { diff --git a/dotscope/src/deobfuscation/techniques/bitmono/pe.rs b/dotscope/src/deobfuscation/techniques/bitmono/pe.rs index 3cd018b9..938bda90 100644 --- a/dotscope/src/deobfuscation/techniques/bitmono/pe.rs +++ b/dotscope/src/deobfuscation/techniques/bitmono/pe.rs @@ -115,8 +115,10 @@ impl Technique for BitMonoPeRepair { #[cfg(test)] mod tests { - use crate::deobfuscation::techniques::{bitmono::BitMonoPeRepair, Technique}; - use crate::test::helpers::load_sample; + use crate::{ + deobfuscation::techniques::{bitmono::BitMonoPeRepair, Technique}, + test::helpers::load_sample, + }; #[test] fn test_detect_positive() { diff --git a/dotscope/src/deobfuscation/techniques/bitmono/renamer.rs b/dotscope/src/deobfuscation/techniques/bitmono/renamer.rs index a6659c3c..8f1ad827 100644 --- a/dotscope/src/deobfuscation/techniques/bitmono/renamer.rs +++ b/dotscope/src/deobfuscation/techniques/bitmono/renamer.rs @@ -83,8 +83,10 @@ impl Technique for BitMonoRenamer { #[cfg(test)] mod tests { - use crate::deobfuscation::techniques::{bitmono::BitMonoRenamer, Technique}; - use crate::test::helpers::load_sample; + use crate::{ + deobfuscation::techniques::{bitmono::BitMonoRenamer, Technique}, + test::helpers::load_sample, + }; #[test] fn test_detect_positive() { diff --git a/dotscope/src/deobfuscation/techniques/confuserex/constants.rs b/dotscope/src/deobfuscation/techniques/confuserex/constants.rs index c150fcad..014e4ade 100644 --- a/dotscope/src/deobfuscation/techniques/confuserex/constants.rs +++ b/dotscope/src/deobfuscation/techniques/confuserex/constants.rs @@ -264,23 +264,40 @@ impl Technique for ConfuserExConstants { let sig = &method.signature; - // Check for string(int32) signature - let first_param_is_i4 = sig - .params - .first() - .is_some_and(|p| p.base == TypeSignature::I4); + // The single parameter is an index into the constant blob. Its + // declared width varies across ConfuserEx forks — stock 1.6.0 + // emits `int32`, others emit `uint32` — so accept any integral + // type rather than pinning one. The surrounding constraints + // (static, or non-ASCII owner, arity 1, string or `!!0` + // return) carry the selectivity. + let first_param_is_integral = sig.params.first().is_some_and(|p| { + matches!( + p.base, + TypeSignature::I1 + | TypeSignature::U1 + | TypeSignature::I2 + | TypeSignature::U2 + | TypeSignature::I4 + | TypeSignature::U4 + | TypeSignature::I8 + | TypeSignature::U8 + | TypeSignature::I + | TypeSignature::U + ) + }); + // Check for string() signature let is_string_decryptor = sig.param_count_generic == 0 && sig.return_type.base == TypeSignature::String && sig.params.len() == 1 - && first_param_is_i4; + && first_param_is_integral; - // Check for generic T(int32) signature (param_count_generic == 1, + // Check for generic T() signature (param_count_generic == 1, // return type is GenericParamMethod(0)) let is_generic_decryptor = sig.param_count_generic == 1 && matches!(sig.return_type.base, TypeSignature::GenericParamMethod(0)) && sig.params.len() == 1 - && first_param_is_i4; + && first_param_is_integral; if is_string_decryptor || is_generic_decryptor { decryptor_tokens.push(method.token); diff --git a/dotscope/src/deobfuscation/techniques/confuserex/statemachine.rs b/dotscope/src/deobfuscation/techniques/confuserex/statemachine.rs index 6435258c..d2a874c1 100644 --- a/dotscope/src/deobfuscation/techniques/confuserex/statemachine.rs +++ b/dotscope/src/deobfuscation/techniques/confuserex/statemachine.rs @@ -33,6 +33,7 @@ use std::collections::{hash_map::Entry, HashMap, HashSet, VecDeque}; +use analyssa::graph::NodeId; use dashmap::DashSet; use crate::{ @@ -52,7 +53,6 @@ use crate::{ prelude::FlowType, CilObject, }; -use analyssa::graph::NodeId; /// Information about a call site to a decryptor method. pub struct DetectedCallSite { diff --git a/dotscope/src/deobfuscation/techniques/generic/metadata.rs b/dotscope/src/deobfuscation/techniques/generic/metadata.rs index 9b6bf5f4..f9e93546 100644 --- a/dotscope/src/deobfuscation/techniques/generic/metadata.rs +++ b/dotscope/src/deobfuscation/techniques/generic/metadata.rs @@ -237,8 +237,7 @@ impl Technique for GenericMetadata { #[cfg(test)] mod tests { - use crate::deobfuscation::techniques::Technique; - use crate::test::helpers::load_sample; + use crate::{deobfuscation::techniques::Technique, test::helpers::load_sample}; /// Verify detection runs without error on a protected sample. /// diff --git a/dotscope/src/deobfuscation/techniques/jiejienet/strings.rs b/dotscope/src/deobfuscation/techniques/jiejienet/strings.rs index 56e174de..a9180342 100644 --- a/dotscope/src/deobfuscation/techniques/jiejienet/strings.rs +++ b/dotscope/src/deobfuscation/techniques/jiejienet/strings.rs @@ -384,7 +384,6 @@ mod tests { use std::sync::Arc; use super::*; - use crate::{ deobfuscation::techniques::Technique, emulation::{EmValue, EmulationOutcome, ProcessBuilder}, diff --git a/dotscope/src/deobfuscation/techniques/mod.rs b/dotscope/src/deobfuscation/techniques/mod.rs index 26c49764..eee81112 100644 --- a/dotscope/src/deobfuscation/techniques/mod.rs +++ b/dotscope/src/deobfuscation/techniques/mod.rs @@ -60,18 +60,17 @@ mod obfuscar; mod registry; mod result; +use std::sync::Arc; + pub use assembly::WorkingAssembly; +// Re-export findings types needed by infrastructure passes and the engine. +#[cfg(feature = "legacy-crypto")] +pub(crate) use bitmono::StringFindings as BitMonoStringFindings; pub use detection::{AttributionResult, Detection, Detections, Evidence}; pub use registry::{ObfuscatorMatcher, ObfuscatorSignature, TechniqueRegistry}; pub use result::TechniqueResult; pub(crate) use result::TechniqueResults; -// Re-export findings types needed by infrastructure passes and the engine. -#[cfg(feature = "legacy-crypto")] -pub(crate) use bitmono::StringFindings as BitMonoStringFindings; - -use std::sync::Arc; - use crate::{ analysis::CilTarget, cilassembly::CleanupRequest, diff --git a/dotscope/src/deobfuscation/techniques/netreactor/antitamp.rs b/dotscope/src/deobfuscation/techniques/netreactor/antitamp.rs index 98892c55..2e1dc4a6 100644 --- a/dotscope/src/deobfuscation/techniques/netreactor/antitamp.rs +++ b/dotscope/src/deobfuscation/techniques/netreactor/antitamp.rs @@ -281,10 +281,9 @@ fn collect_runtime_method_tokens(assembly: &CilObject, runtime_type: Option Option { diff --git a/dotscope/src/deobfuscation/techniques/netreactor/resources.rs b/dotscope/src/deobfuscation/techniques/netreactor/resources.rs index a4cd0753..4d3cb0f8 100644 --- a/dotscope/src/deobfuscation/techniques/netreactor/resources.rs +++ b/dotscope/src/deobfuscation/techniques/netreactor/resources.rs @@ -1129,14 +1129,13 @@ fn encrypted_resource_names(assembly: &CilObject, findings: &ResourceFindings) - #[cfg(test)] mod tests { - use super::*; - use std::{ env, path::{Path, PathBuf}, sync::Arc, }; + use super::*; use crate::{ emulation::{EmulationOutcome, ProcessBuilder, TracingConfig}, metadata::validation::ValidationConfig, diff --git a/dotscope/src/deobfuscation/utils.rs b/dotscope/src/deobfuscation/utils.rs index e076eed9..eddcf237 100644 --- a/dotscope/src/deobfuscation/utils.rs +++ b/dotscope/src/deobfuscation/utils.rs @@ -16,7 +16,7 @@ use std::collections::{HashMap, HashSet}; use crate::{ - analysis::{SsaFunction, SsaOp, SsaVarId}, + analysis::{SsaFunction, SsaInstruction, SsaOp, SsaVarId}, metadata::{ signatures::{parse_field_signature, TypeSignature}, streams::Strings, @@ -97,6 +97,114 @@ pub(crate) fn build_def_map(ssa: &SsaFunction) -> HashMap { defs } +/// Shrinks a candidate removal set until nothing it destroys is still read. +/// +/// Neutralizing an instruction (rewriting it to `Nop`) or dropping a phi +/// destroys the definition it produced. If any *surviving* instruction or phi +/// operand still reads that variable, the result is IR with a read of a +/// variable nothing defines — which the SSA verifier rejects outright, failing +/// the whole method rather than the one rewrite. Taint analysis does not +/// prevent this on its own: [`PhiTaintMode::NoPropagation`] deliberately makes +/// phis taint barriers, so a phi can merge a tainted definition into code that +/// is never itself marked tainted. +/// +/// The resolution is to keep the definition, not to widen the removal into +/// legitimate code: a candidate whose result still has a reader is dropped from +/// the set. Dropping it makes it a survivor in turn, so its own operands must +/// be kept too — hence the fixpoint. The sets only ever shrink, so this +/// terminates in at most one round per candidate. +/// +/// # Arguments +/// +/// * `ssa` - The SSA function the candidates index into. +/// * `instrs` - Candidate `(block, instr)` instruction locations. Shrunk in place. +/// * `phis` - Candidate `(block, phi)` phi locations. Shrunk in place. +/// +/// [`PhiTaintMode::NoPropagation`]: crate::analysis::PhiTaintMode::NoPropagation +pub(crate) fn retain_removable( + ssa: &SsaFunction, + instrs: &mut HashSet<(usize, usize)>, + phis: &mut HashSet<(usize, usize)>, +) { + loop { + // Every variable the current candidate set would destroy, mapped back to + // the candidate that has to be dropped if the variable turns out live. + let mut destroyed: HashMap = HashMap::new(); + for &(block_idx, instr_idx) in instrs.iter() { + let Some(op) = ssa + .block(block_idx) + .and_then(|b| b.instruction(instr_idx)) + .map(SsaInstruction::op) + else { + continue; + }; + for def in op.defs() { + destroyed.insert(def, Removal::Instruction(block_idx, instr_idx)); + } + } + for &(block_idx, phi_idx) in phis.iter() { + if let Some(phi) = ssa + .block(block_idx) + .and_then(|b| b.phi_nodes().get(phi_idx)) + { + destroyed.insert(phi.result(), Removal::Phi(block_idx, phi_idx)); + } + } + if destroyed.is_empty() { + return; + } + + // Anything not being removed is a survivor, and its reads keep the + // definitions behind them alive. + let mut rescued: HashSet = HashSet::new(); + for (block_idx, block) in ssa.blocks().iter().enumerate() { + for (phi_idx, phi) in block.phi_nodes().iter().enumerate() { + if phis.contains(&(block_idx, phi_idx)) { + continue; + } + for operand in phi.operands() { + if let Some(removal) = destroyed.get(&operand.value()) { + rescued.insert(*removal); + } + } + } + for (instr_idx, instr) in block.instructions().iter().enumerate() { + if instrs.contains(&(block_idx, instr_idx)) { + continue; + } + instr.op().for_each_use(|used| { + if let Some(removal) = destroyed.get(&used) { + rescued.insert(*removal); + } + }); + } + } + if rescued.is_empty() { + return; + } + + for removal in rescued { + match removal { + Removal::Instruction(block_idx, instr_idx) => { + instrs.remove(&(block_idx, instr_idx)); + } + Removal::Phi(block_idx, phi_idx) => { + phis.remove(&(block_idx, phi_idx)); + } + } + } + } +} + +/// A location in the candidate removal set handled by [`retain_removable`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum Removal { + /// Instruction at `(block, instr)`. + Instruction(usize, usize), + /// Phi node at `(block, phi)`. + Phi(usize, usize), +} + /// Checks if a name contains obfuscation indicators (zero-width chars, PUA, spaces). pub(crate) fn is_obfuscated_name(name: &str) -> bool { if name.is_empty() { @@ -720,7 +828,6 @@ pub(crate) fn is_typed_method_named( #[cfg(test)] mod tests { - use crate::test::helpers::load_sample; use crate::{ deobfuscation::utils::{ build_call_site_counts, is_method_named, is_obfuscated_name, is_special_name, @@ -730,6 +837,7 @@ mod tests { tables::{MemberRefRaw, MethodDefRaw, TableId, TypeDefRaw, TypeRefRaw}, token::Token, }, + test::helpers::load_sample, }; #[test] diff --git a/dotscope/src/deobfuscation/workqueue.rs b/dotscope/src/deobfuscation/workqueue.rs index fc15d0e3..751e1cf0 100644 --- a/dotscope/src/deobfuscation/workqueue.rs +++ b/dotscope/src/deobfuscation/workqueue.rs @@ -9,11 +9,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use dashmap::{DashMap, DashSet}; use log::warn; -use crate::{ - analysis::SsaFunction, - metadata::token::Token, - {Error, Result}, -}; +use crate::{analysis::SsaFunction, metadata::token::Token, Error, Result}; /// A work item representing a pending operation in the deobfuscation pipeline. #[derive(Debug)] diff --git a/dotscope/src/emulation/engine/callresolver.rs b/dotscope/src/emulation/engine/callresolver.rs index d29e7727..42998abb 100644 --- a/dotscope/src/emulation/engine/callresolver.rs +++ b/dotscope/src/emulation/engine/callresolver.rs @@ -1928,7 +1928,6 @@ fn resolve_native_delegate_return( #[cfg(test)] mod tests { use super::*; - use crate::emulation::{ engine::generics::GenericRegistry, process::EmulationConfig, runtime::RuntimeState, tracer::TraceWriter, diff --git a/dotscope/src/emulation/engine/context/code.rs b/dotscope/src/emulation/engine/context/code.rs new file mode 100644 index 00000000..74d853e9 --- /dev/null +++ b/dotscope/src/emulation/engine/context/code.rs @@ -0,0 +1,92 @@ +//! Cached, offset-indexed method bodies for the emulation hot path. +//! +//! The interpreter fetches one instruction per executed step, and branches +//! convert absolute RVAs back to method-relative offsets. Both operations used +//! to re-materialize the entire instruction list for the method and scan it +//! linearly, which made executing a method quadratic in its instruction count. +//! +//! [`MethodCode`] decodes a method body once and stores an offset -> index map +//! alongside it, so both lookups become O(1) and the instruction list is shared +//! by reference instead of cloned. + +use rustc_hash::FxHashMap; + +use crate::assembly::Instruction; + +/// A method's instruction list plus an index for O(1) offset lookup. +/// +/// Instances are cached per method token in +/// [`EmulationContext`](crate::emulation::engine::context::EmulationContext) and +/// shared behind an `Arc`. A method's instructions are immutable once decoded +/// (`Method::blocks` is a write-once `OnceLock`), so a cached entry stays valid +/// for the lifetime of the owning assembly. +pub struct MethodCode { + /// The method's instructions in address order. + instructions: Vec, + + /// RVA of the first instruction, used to convert RVAs to method offsets. + base_rva: u64, + + /// Maps a method-relative offset to its index in `instructions`. + /// + /// Populated only for offsets that correspond to a real instruction start, + /// so a lookup miss is a genuine invalid instruction pointer. + offset_to_index: FxHashMap, +} + +impl MethodCode { + /// Builds the offset index for a decoded instruction list. + /// + /// Returns `None` if the list is empty, since an empty body has no base RVA + /// and must not be cached (the decoder may populate blocks lazily). + #[must_use] + pub fn new(instructions: Vec) -> Option { + let base_rva = instructions.first()?.rva; + + let mut offset_to_index = FxHashMap::default(); + offset_to_index.reserve(instructions.len()); + for (index, instr) in instructions.iter().enumerate() { + // Offsets are method-relative and bounded by the IL body size, + // which the format caps well below u32::MAX. + let offset = instr.rva.saturating_sub(base_rva); + if let (Ok(offset), Ok(index)) = (u32::try_from(offset), u32::try_from(index)) { + offset_to_index.insert(offset, index); + } + } + + Some(MethodCode { + instructions, + base_rva, + offset_to_index, + }) + } + + /// Returns the RVA of the method's first instruction. + #[must_use] + pub fn base_rva(&self) -> u64 { + self.base_rva + } + + /// Returns the full instruction list. + #[must_use] + pub fn instructions(&self) -> &[Instruction] { + &self.instructions + } + + /// Returns the instruction starting at `offset`, or `None` if no + /// instruction begins there. + #[must_use] + pub fn instruction_at(&self, offset: u32) -> Option<&Instruction> { + let index = *self.offset_to_index.get(&offset)?; + self.instructions.get(index as usize) + } + + /// Converts an absolute RVA to a method-relative offset. + /// + /// Method-relative offsets are bounded by the IL body size (< `u32::MAX`). + #[must_use] + #[allow(clippy::cast_possible_truncation)] + pub fn rva_to_offset(&self, rva: u64) -> u32 { + rva.saturating_sub(self.base_rva) as u32 + } +} diff --git a/dotscope/src/emulation/engine/context/methods.rs b/dotscope/src/emulation/engine/context/methods.rs index ffb6da5c..d9b12f3c 100644 --- a/dotscope/src/emulation/engine/context/methods.rs +++ b/dotscope/src/emulation/engine/context/methods.rs @@ -8,7 +8,10 @@ use std::sync::Arc; use crate::{ assembly::Instruction, emulation::{ - engine::{context::EmulationContext, error::EmulationError}, + engine::{ + context::{EmulationContext, MethodCode}, + error::EmulationError, + }, exception::ExceptionClause, }, metadata::{method::Method, signatures::TypeSignature, token::Token, typesystem::CilFlavor}, @@ -44,6 +47,12 @@ impl EmulationContext { return Ok(synthetic.instructions.clone()); } + // Serve from the code cache when possible so repeated callers do not + // re-decode the whole body. + if let Some(code) = self.code_cache.get(&method_token) { + return Ok(code.instructions().to_vec()); + } + let method = self.get_method(method_token)?; // Get instructions from the method's blocks @@ -57,6 +66,52 @@ impl EmulationContext { Ok(instructions) } + /// Gets the offset-indexed, cached body for a method. + /// + /// The body is decoded once per method token and shared behind an `Arc`. + /// Both instruction fetch and RVA-to-offset conversion are O(1) against the + /// returned value, which is what the execution loop relies on. + /// + /// Synthetic method bodies are not cached — `ILGenerator` can mutate them + /// after registration — so they are rebuilt on each call. + /// + /// # Errors + /// + /// Returns an error if the method is not found or has no decodable body. + pub fn get_method_code(&self, method_token: Token) -> Result> { + if let Some(code) = self.code_cache.get(&method_token) { + return Ok(Arc::clone(code.value())); + } + + // Synthetic bodies are mutable at runtime; build fresh and do not cache. + if let Some(synthetic) = self.synthetic_methods.get(&method_token) { + return MethodCode::new(synthetic.instructions.clone()) + .map(Arc::new) + .ok_or_else(|| { + EmulationError::MissingMethodBody { + token: method_token, + } + .into() + }); + } + + let method = self.get_method(method_token)?; + let instructions: Vec = method.instructions().cloned().collect(); + + // An empty list means either a genuinely bodiless method or a body whose + // blocks have not been decoded yet. Neither is safe to cache. + let Some(code) = MethodCode::new(instructions) else { + return Err(EmulationError::MissingMethodBody { + token: method_token, + } + .into()); + }; + + let code = Arc::new(code); + self.code_cache.insert(method_token, Arc::clone(&code)); + Ok(code) + } + /// Gets the base RVA (address of the first instruction) of a method. /// /// This is used to convert between absolute RVAs and method-relative offsets. @@ -65,13 +120,7 @@ impl EmulationContext { /// /// Returns an error if the method is not found or has no instructions. pub fn get_method_base_rva(&self, method_token: Token) -> Result { - let instructions = self.get_instructions(method_token)?; - instructions.first().map(|instr| instr.rva).ok_or_else(|| { - EmulationError::MissingMethodBody { - token: method_token, - } - .into() - }) + Ok(self.get_method_code(method_token)?.base_rva()) } /// Converts an absolute RVA to a method-relative offset. @@ -83,11 +132,8 @@ impl EmulationContext { /// /// Returns an error if the method is not found. /// Method-relative offsets are bounded by IL method body size (< u32::MAX) - #[allow(clippy::cast_possible_truncation)] pub fn rva_to_method_offset(&self, method_token: Token, rva: u64) -> Result { - let base_rva = self.get_method_base_rva(method_token)?; - let offset = rva.saturating_sub(base_rva); - Ok(offset as u32) + Ok(self.get_method_code(method_token)?.rva_to_offset(rva)) } /// Gets an instruction at a specific method-relative offset. @@ -99,26 +145,10 @@ impl EmulationContext { /// /// Returns an error if the method is not found or no instruction exists at the offset. pub fn get_instruction_at(&self, method_token: Token, offset: u32) -> Result { - let instructions = self.get_instructions(method_token)?; - - // Get the base RVA to compute method-relative offsets from RVAs - // Branch targets are stored as absolute RVAs and converted to method offsets - // using rva_to_method_offset(target_rva - base_rva), so we need to match - // instructions by their RVA-based offset, not accumulated instruction sizes - let base_rva = instructions - .first() - .map(|instr| instr.rva) - .ok_or(EmulationError::InvalidInstructionPointer { offset })?; - - // Find instruction at the given method-relative offset using RVA - for instr in instructions { - let instr_offset = instr.rva.saturating_sub(base_rva); - if instr_offset == u64::from(offset) { - return Ok(instr); - } - } - - Err(EmulationError::InvalidInstructionPointer { offset }.into()) + self.get_method_code(method_token)? + .instruction_at(offset) + .cloned() + .ok_or_else(|| EmulationError::InvalidInstructionPointer { offset }.into()) } /// Gets an instruction by index within a method's instruction list. diff --git a/dotscope/src/emulation/engine/context/mod.rs b/dotscope/src/emulation/engine/context/mod.rs index 062ded12..27b795f2 100644 --- a/dotscope/src/emulation/engine/context/mod.rs +++ b/dotscope/src/emulation/engine/context/mod.rs @@ -3,6 +3,7 @@ //! The [`EmulationContext`] provides the interpreter with access to //! the loaded assembly's metadata, instructions, and strings. +mod code; mod generics; mod lookup; mod metadata; @@ -11,6 +12,7 @@ mod types; use std::sync::Arc; +pub use code::MethodCode; use dashmap::DashMap; use crate::{ @@ -49,6 +51,14 @@ pub struct EmulationContext { pub(crate) assembly: Arc, /// Synthetic method bodies created by DynamicMethod/ILGenerator. pub(crate) synthetic_methods: Arc>, + /// Offset-indexed method bodies, decoded once per method token. + /// + /// Shared so that contexts recreated for the same assembly (e.g. per-frame + /// context resolution in the execution loop) reuse the same decoded bodies. + /// Only non-synthetic, non-empty bodies are cached — synthetic bodies can be + /// mutated at runtime by `ILGenerator`, and an empty body may simply mean the + /// decoder has not populated blocks yet. + pub(crate) code_cache: Arc>>, } impl EmulationContext { @@ -61,6 +71,25 @@ impl EmulationContext { EmulationContext { assembly, synthetic_methods, + code_cache: Arc::new(DashMap::new()), + } + } + + /// Creates a new emulation context that shares an existing code cache. + /// + /// Used when a context must be rebuilt for the same assembly (for example + /// when resolving the context for a call frame in another loaded assembly) + /// so the decoded method bodies are not thrown away. + #[must_use] + pub fn with_code_cache( + assembly: Arc, + synthetic_methods: Arc>, + code_cache: Arc>>, + ) -> Self { + EmulationContext { + assembly, + synthetic_methods, + code_cache, } } diff --git a/dotscope/src/emulation/engine/context/types.rs b/dotscope/src/emulation/engine/context/types.rs index 1c277a2c..6344629d 100644 --- a/dotscope/src/emulation/engine/context/types.rs +++ b/dotscope/src/emulation/engine/context/types.rs @@ -15,6 +15,9 @@ use crate::{ Result, }; +/// Deepest inheritance chain the override lookup will follow. +const MAX_BASE_WALK_DEPTH: usize = 256; + impl EmulationContext { /// Converts a type token to a CilFlavor. /// @@ -273,7 +276,7 @@ impl EmulationContext { // Search for an override in the runtime type if let Some(override_token) = - Self::find_method_override(&runtime_type_info, method_name, &method) + Self::find_method_override(0, &runtime_type_info, method_name, &method) { return override_token; } @@ -288,10 +291,16 @@ impl EmulationContext { /// signature of the base method. This ensures proper override resolution /// and avoids matching methods that merely hide the base method. fn find_method_override( + depth: usize, type_info: &CilType, method_name: &str, base_method: &Method, ) -> Option { + // Tail-recurses down `base()`, which may be cyclic — `set_base` allows + // that by design so the circularity validator can report it. + if depth >= MAX_BASE_WALK_DEPTH { + return None; + } // First check the type itself for a matching override if let Some(method) = type_info .query_methods() @@ -305,7 +314,12 @@ impl EmulationContext { // Check base types (inheritance chain) if let Some(base) = type_info.base() { - return Self::find_method_override(&base, method_name, base_method); + return Self::find_method_override( + depth.saturating_add(1), + &base, + method_name, + base_method, + ); } None diff --git a/dotscope/src/emulation/engine/controller.rs b/dotscope/src/emulation/engine/controller.rs index 86750b92..c5f420f0 100644 --- a/dotscope/src/emulation/engine/controller.rs +++ b/dotscope/src/emulation/engine/controller.rs @@ -35,6 +35,7 @@ use std::sync::{ Arc, RwLock, }; +use dashmap::DashMap; use log::{debug, trace}; use crate::{ @@ -132,6 +133,12 @@ pub struct EmulationController { /// Monotonically increasing call ID for correlating call/return trace events. next_call_id: AtomicU64, + + /// Memoized per-assembly emulation contexts, keyed by loaded assembly index. + /// + /// Keeps each assembly's decoded-method cache alive across the execution + /// loop instead of rebuilding it on every instruction. + assembly_contexts: DashMap>, } impl EmulationController { @@ -160,6 +167,7 @@ impl EmulationController { cctor_tracker: CctorTracker::new(), generics, next_call_id: AtomicU64::new(1), + assembly_contexts: DashMap::new(), }) } @@ -304,7 +312,14 @@ impl EmulationController { /// Returns `None` if the assembly index doesn't exist in the `RuntimeState`. /// This is called per-iteration when executing a frame from a loaded assembly, /// but `EmulationContext::new` is trivial (wraps an `Arc`), so the cost is negligible. - fn loaded_assembly_context(&self, index: u8) -> Result> { + fn loaded_assembly_context(&self, index: u8) -> Result>> { + // The execution loop resolves the frame's context on every instruction. + // Rebuilding the context each time meant taking the runtime lock and + // discarding the assembly's decoded-method cache per step, so memoize it. + if let Some(cached) = self.assembly_contexts.get(&index) { + return Ok(Some(Arc::clone(cached.value()))); + } + let state = self .context .runtime @@ -312,12 +327,16 @@ impl EmulationController { .map_err(|_| EmulationError::LockPoisoned { description: "runtime state", })?; - Ok(state - .app_domain() - .get_parsed_assembly(index as usize) - .map(|asm| { - EmulationContext::new(Arc::clone(asm), Arc::clone(&self.context.synthetic_methods)) - })) + let Some(asm) = state.app_domain().get_parsed_assembly(index as usize) else { + return Ok(None); + }; + + let context = Arc::new(EmulationContext::new( + Arc::clone(asm), + Arc::clone(&self.context.synthetic_methods), + )); + self.assembly_contexts.insert(index, Arc::clone(&context)); + Ok(Some(context)) } /// Emulates a method with the given arguments. @@ -586,14 +605,18 @@ impl EmulationController { let loaded_context; let context = if let Some(idx) = asm_index { loaded_context = self.loaded_assembly_context(idx)?; - loaded_context.as_ref().unwrap_or(context) + loaded_context.as_deref().unwrap_or(context) } else { context }; - // Get the instruction at current offset - let instruction = - context.get_instruction_at(current_method, interpreter.ip().offset())?; + // Get the instruction at current offset. The decoded body is cached + // and shared, so this borrows the instruction rather than cloning it. + let method_code = context.get_method_code(current_method)?; + let offset = interpreter.ip().offset(); + let instruction = method_code + .instruction_at(offset) + .ok_or(EmulationError::InvalidInstructionPointer { offset })?; // Capture pre-execution instruction info for tracing let trace_info = if self.trace_instructions_enabled() { @@ -631,7 +654,7 @@ impl EmulationController { }; // Execute the instruction - let step_result = match interpreter.step(thread, &instruction) { + let step_result = match interpreter.step(thread, instruction) { Ok(result) => result, Err(err) => { match self.handle_step_error( @@ -2744,10 +2767,9 @@ impl Default for EmulationController { #[cfg(test)] mod tests { - use super::*; - use dashmap::DashMap; + use super::*; use crate::{ emulation::{engine::typeops, process::UnknownMethodBehavior}, test::emulation::create_test_thread, diff --git a/dotscope/src/emulation/engine/dispatch.rs b/dotscope/src/emulation/engine/dispatch.rs index 22973540..382a69fc 100644 --- a/dotscope/src/emulation/engine/dispatch.rs +++ b/dotscope/src/emulation/engine/dispatch.rs @@ -93,6 +93,15 @@ pub struct DispatchResolver { vtables: DashMap, } +/// Deepest inheritance chain the dispatch walkers will follow. +/// +/// These walk `base()` to the root and are tail-recursive, which Rust does not +/// guarantee to turn into a loop. `CilType::set_base` deliberately permits a +/// cyclic chain — a dedicated validator reports it as a finding rather than +/// refusing to represent it — so a corrupt assembly whose TypeDefs extend each +/// other reaches these with no terminating condition. +const MAX_BASE_WALK_DEPTH: usize = 256; + impl DispatchResolver { /// Creates a new empty resolver. #[must_use] @@ -185,7 +194,7 @@ impl DispatchResolver { return interface_method; }; - if let Some(found) = Self::find_interface_impl(&rt, interface_method, base_method) { + if let Some(found) = Self::find_interface_impl(0, &rt, interface_method, base_method) { return found; } @@ -206,10 +215,14 @@ impl DispatchResolver { /// (skipping methods that explicitly override a DIFFERENT interface method) /// 3. Walk base types — recurse on the base type fn find_interface_impl( + depth: usize, type_info: &CilType, interface_method: Token, base_method: &Method, ) -> Option { + if depth >= MAX_BASE_WALK_DEPTH { + return None; + } // Step 1: Explicit MethodImpl — check each method's overrides list for (_, method_ref) in type_info.methods.iter() { let Some(method) = method_ref.upgrade() else { @@ -260,7 +273,12 @@ impl DispatchResolver { // Step 3: Walk base types if let Some(base) = type_info.base() { - return Self::find_interface_impl(&base, interface_method, base_method); + return Self::find_interface_impl( + depth.saturating_add(1), + &base, + interface_method, + base_method, + ); } None @@ -277,7 +295,7 @@ impl DispatchResolver { return base_method.token; }; - if let Some(found) = Self::find_virtual_override(&rt, &base_method.name, base_method) { + if let Some(found) = Self::find_virtual_override(0, &rt, &base_method.name, base_method) { return found; } @@ -286,10 +304,14 @@ impl DispatchResolver { /// Finds a virtual method override in a type hierarchy. fn find_virtual_override( + depth: usize, type_info: &CilType, method_name: &str, base_method: &Method, ) -> Option { + if depth >= MAX_BASE_WALK_DEPTH { + return None; + } for (_, method_ref) in type_info.methods.iter() { let Some(method) = method_ref.upgrade() else { continue; @@ -304,7 +326,12 @@ impl DispatchResolver { // Walk base types if let Some(base) = type_info.base() { - return Self::find_virtual_override(&base, method_name, base_method); + return Self::find_virtual_override( + depth.saturating_add(1), + &base, + method_name, + base_method, + ); } None @@ -332,19 +359,22 @@ impl DispatchResolver { let mut slots = HashMap::new(); // Collect all virtual methods from the type hierarchy (most-derived first) - Self::collect_virtual_slots(&rt, &mut slots); + Self::collect_virtual_slots(0, &rt, &mut slots); // Collect interface implementations - Self::collect_interface_slots(&rt, &mut slots); + Self::collect_interface_slots(0, &rt, &mut slots); self.vtables.insert(runtime_type, VTable { slots }); } /// Collects virtual method slots from a type's inheritance chain. - fn collect_virtual_slots(type_info: &CilType, slots: &mut HashMap) { + fn collect_virtual_slots(depth: usize, type_info: &CilType, slots: &mut HashMap) { + if depth >= MAX_BASE_WALK_DEPTH { + return; + } // Walk base types first (root → derived) so derived overrides win if let Some(base) = type_info.base() { - Self::collect_virtual_slots(&base, slots); + Self::collect_virtual_slots(depth.saturating_add(1), &base, slots); } // Override slots for virtual methods on this type @@ -374,7 +404,14 @@ impl DispatchResolver { } /// Collects interface method implementations for a type. - fn collect_interface_slots(type_info: &CilType, slots: &mut HashMap) { + fn collect_interface_slots( + depth: usize, + type_info: &CilType, + slots: &mut HashMap, + ) { + if depth >= MAX_BASE_WALK_DEPTH { + return; + } // For each interface the type implements, resolve all methods for (_, iface_entry) in type_info.interfaces.iter() { let Some(iface_type) = iface_entry.interface.upgrade() else { @@ -393,7 +430,7 @@ impl DispatchResolver { // Try to find implementation if let Some(impl_token) = - Self::find_interface_impl(type_info, iface_method.token, &iface_method) + Self::find_interface_impl(0, type_info, iface_method.token, &iface_method) { slots.insert(iface_method.token, impl_token); } else if iface_method.has_body() && !iface_method.is_abstract() { @@ -405,7 +442,7 @@ impl DispatchResolver { // Recurse into base type for inherited interface implementations if let Some(base) = type_info.base() { - Self::collect_interface_slots(&base, slots); + Self::collect_interface_slots(depth.saturating_add(1), &base, slots); } } } diff --git a/dotscope/src/emulation/engine/exhandler.rs b/dotscope/src/emulation/engine/exhandler.rs index 3f944f4a..e440cd33 100644 --- a/dotscope/src/emulation/engine/exhandler.rs +++ b/dotscope/src/emulation/engine/exhandler.rs @@ -43,6 +43,8 @@ //! entered via reflection invoke, the exception is wrapped in a //! `TargetInvocationException` per real .NET behavior. +use log::{debug, trace}; + use crate::{ emulation::{ engine::{ @@ -59,7 +61,6 @@ use crate::{ metadata::token::Token, Result, }; -use log::{debug, trace}; /// Creates a synthetic CLR exception object from an [`EmulationError`]. /// diff --git a/dotscope/src/emulation/engine/interpreter/handlers.rs b/dotscope/src/emulation/engine/interpreter/handlers.rs index ae5f8291..0406deba 100644 --- a/dotscope/src/emulation/engine/interpreter/handlers.rs +++ b/dotscope/src/emulation/engine/interpreter/handlers.rs @@ -943,18 +943,11 @@ impl Interpreter { let index = thread.pop()?; let array_ref = thread.pop()?; - let idx_i64 = match index { - EmValue::I32(v) => i64::from(v), - EmValue::NativeInt(v) => v, - _ => { - return Err(EmulationError::TypeMismatch { - operation: "ldelema", - expected: "integer index", - found: index.cil_flavor().as_str(), - } - .into()) - } - }; + // Share the index extraction used by `ldelem`/`stelem` rather than + // matching a narrower set here. `native uint` is a legal array index and + // appears in ConfuserEx's constants `Initialize()`; accepting only + // `I32`/`NativeInt` rejected it and aborted the whole emulation. + let idx_i64 = Self::extract_array_index(thread, &index, "ldelema")?; let idx = usize::try_from(idx_i64).map_err(|_| { Error::from(EmulationError::ArrayIndexOutOfBounds { @@ -1372,6 +1365,39 @@ impl Interpreter { } } } + PointerTarget::ArrayElement { array, index } => { + // `ldelema` on a value-type array yields a pointer to the + // element itself, and `stfld` through it must update one + // field *within* that element. Storing through the pointer + // would replace the whole element with the field value, so + // read-modify-write the struct instead. LZMA's bit-decoder + // arrays in ConfuserEx's constants runtime take this path. + let element = thread.heap().get_array_element(*array, *index).ok(); + match element { + Some(vt @ EmValue::ValueType { .. }) => { + if let Some(updated) = + store_into_valuetype(thread, vt, field_token, value.clone()) + { + thread + .heap_mut() + .set_array_element(*array, *index, updated)?; + return Ok(StepResult::Continue); + } + thread.store_through_pointer(&ptr, value)?; + Ok(StepResult::Continue) + } + Some(EmValue::ObjectRef(href)) => { + // Reference-type element: the field belongs to the + // referenced object, not to the array slot. + thread.heap_mut().set_field(href, field_token, value)?; + Ok(StepResult::Continue) + } + _ => { + thread.store_through_pointer(&ptr, value)?; + Ok(StepResult::Continue) + } + } + } _ => { // For other pointer targets, store through the pointer thread.store_through_pointer(&ptr, value)?; @@ -1964,19 +1990,21 @@ impl Interpreter { let value = thread.pop()?; let addr = thread.pop()?; - // Verify this is a value type operation (stobj is for value types) + // `stobj` is not restricted to value types. ECMA-335 III.4.29 defines it + // over any `typeTok`, and states that when `typeTok` is a reference type + // the instruction is equivalent to `stind.ref`. Generic code relies on + // this: a method with a `T&` destination emits `stobj !!T`, which stores + // a reference whenever `T` is instantiated with a reference type — the + // shape ConfuserEx's `T Get(uint)` decryptor takes at `T = string`. + // + // `type_token` is not resolved here, so there is nothing meaningful to + // validate the value against; only `Void` is categorically unstorable. let value_type = value.cil_flavor(); - let is_value_type = value_type == CilFlavor::ValueType - || value_type == CilFlavor::I4 - || value_type == CilFlavor::I8 - || value_type == CilFlavor::R4 - || value_type == CilFlavor::R8 - || value_type == CilFlavor::I; - if !is_value_type { + if value_type == CilFlavor::Void { let _ = type_token; // Token available for future type resolution return Err(EmulationError::TypeMismatch { operation: "stobj", - expected: "value type", + expected: "storable value", found: value_type.as_str(), } .into()); diff --git a/dotscope/src/emulation/exception/unwinder.rs b/dotscope/src/emulation/exception/unwinder.rs index 862b7ebc..74ee855e 100644 --- a/dotscope/src/emulation/exception/unwinder.rs +++ b/dotscope/src/emulation/exception/unwinder.rs @@ -732,9 +732,8 @@ impl UnwindSequenceBuilder { #[cfg(test)] mod tests { - use crate::{emulation::HeapRef, metadata::typesystem::CilFlavor}; - use super::*; + use crate::{emulation::HeapRef, metadata::typesystem::CilFlavor}; fn create_test_exception() -> ExceptionInfo { ExceptionInfo::new( diff --git a/dotscope/src/emulation/mod.rs b/dotscope/src/emulation/mod.rs index 648d4f8f..7b5536d2 100644 --- a/dotscope/src/emulation/mod.rs +++ b/dotscope/src/emulation/mod.rs @@ -136,11 +136,31 @@ mod tracer; mod value; // Re-export primary types from value module -pub use value::{ - BinaryOp, CompareOp, ConversionType, EmValue, HeapRef, ManagedPointer, PointerTarget, - SymbolicValue, TaintSource, UnaryOp, +// Re-export primary types from capture module +pub use capture::{ + AssemblyLoadMethod, BufferSource, CaptureContext, CaptureSource, CapturedAssembly, + CapturedBuffer, CapturedMethodReturn, CapturedString, FileOpKind, FileOperation, + MemorySnapshot, NetworkOpKind, NetworkOperation, +}; +// Re-export primary types from engine module +pub use engine::{ + synthetic_exception, EmulationContext, EmulationController, EmulationError, EmulationOutcome, + InstructionPointer, Interpreter, LimitExceeded, StepResult, +}; +// Re-export primary types from exception module +pub use exception::{ + ExceptionClause, ExceptionHandler, ExceptionInfo, FrameSearchInfo, HandlerMatch, + HandlerSearchState, InstructionLocation, MethodHandlerResult, PendingFinally, StackUnwinder, + ThreadExceptionState, UnwindSequenceBuilder, UnwindStepResult, +}; +// Re-export primary types from fakeobjects module +pub use fakeobjects::{FakeObjects, SharedFakeObjects}; +// Re-export primary types from filesystem module +pub use filesystem::VirtualFs; +// Re-export primary types from loader module +pub use loader::{ + DataLoader, LoadedImage, LoadedSection, MappedRegionInfo, PeLoader, PeLoaderConfig, }; - // Re-export primary types from memory module pub use memory::{ AddressSpace, ArgumentStorage, DictionaryKey, EncodingType, EvaluationStack, HeapObject, @@ -148,40 +168,12 @@ pub use memory::{ StaticFieldStorage, ThreadId, TypeInitState, TypeWrapper, UnmanagedMemory, UnmanagedRef, PAGE_SIZE, }; - -// Re-export primary types from engine module -pub use engine::{ - synthetic_exception, EmulationContext, EmulationController, EmulationError, EmulationOutcome, - InstructionPointer, Interpreter, LimitExceeded, StepResult, -}; - // Re-export primary types from process module pub use process::{ CaptureConfig, EmulationConfig, EmulationLimits, EmulationProcess, EnvironmentConfig, LimitKind, MemoryConfig, ProcessBuilder, ProcessSummary, StackTraceEntry, StubConfig, TracingConfig, UnknownMethodBehavior, }; - -// Re-export tracing infrastructure -pub use tracer::{ - build_call_tree, CallTreeBuilder, CallTreeNode, ExceptionRecord, InstructionTraceLevel, - MethodPattern, TraceEvent, TraceFilter, TraceListener, TraceWriter, -}; - -// Re-export primary types from exception module -pub use exception::{ - ExceptionClause, ExceptionHandler, ExceptionInfo, FrameSearchInfo, HandlerMatch, - HandlerSearchState, InstructionLocation, MethodHandlerResult, PendingFinally, StackUnwinder, - ThreadExceptionState, UnwindSequenceBuilder, UnwindStepResult, -}; - -// Re-export primary types from thread module -pub use thread::{ - EmulationThread, EventState, MonitorState, MutexState, SchedulerOutcome, SemaphoreState, - SyncError, SyncState, ThreadCallFrame, ThreadContext, ThreadPriority, ThreadScheduler, - ThreadState, WaitReason, WakeCondition, -}; - // Re-export primary types from runtime module pub use runtime::{ AppDomainState, Hook, HookContext, HookManager, HookMatcher, HookOutcome, HookPriority, @@ -189,21 +181,18 @@ pub use runtime::{ PostHookResult, PreHookFn, PreHookResult, RuntimeMatcher, RuntimeState, RuntimeStateBuilder, SignatureMatcher, }; - -// Re-export primary types from capture module -pub use capture::{ - AssemblyLoadMethod, BufferSource, CaptureContext, CaptureSource, CapturedAssembly, - CapturedBuffer, CapturedMethodReturn, CapturedString, FileOpKind, FileOperation, - MemorySnapshot, NetworkOpKind, NetworkOperation, +// Re-export primary types from thread module +pub use thread::{ + EmulationThread, EventState, MonitorState, MutexState, SchedulerOutcome, SemaphoreState, + SyncError, SyncState, ThreadCallFrame, ThreadContext, ThreadPriority, ThreadScheduler, + ThreadState, WaitReason, WakeCondition, }; - -// Re-export primary types from loader module -pub use loader::{ - DataLoader, LoadedImage, LoadedSection, MappedRegionInfo, PeLoader, PeLoaderConfig, +// Re-export tracing infrastructure +pub use tracer::{ + build_call_tree, CallTreeBuilder, CallTreeNode, ExceptionRecord, InstructionTraceLevel, + MethodPattern, TraceEvent, TraceFilter, TraceListener, TraceWriter, +}; +pub use value::{ + BinaryOp, CompareOp, ConversionType, EmValue, HeapRef, ManagedPointer, PointerTarget, + SymbolicValue, TaintSource, UnaryOp, }; - -// Re-export primary types from filesystem module -pub use filesystem::VirtualFs; - -// Re-export primary types from fakeobjects module -pub use fakeobjects::{FakeObjects, SharedFakeObjects}; diff --git a/dotscope/src/emulation/process/builder.rs b/dotscope/src/emulation/process/builder.rs index 7eaeada2..81996503 100644 --- a/dotscope/src/emulation/process/builder.rs +++ b/dotscope/src/emulation/process/builder.rs @@ -75,12 +75,10 @@ //! .build()?; //! ``` -use std::path::Path; -use std::sync::Arc; - -use log::debug; +use std::{path::Path, sync::Arc}; use cowfile::CowFile; +use log::debug; use crate::{ emulation::{ diff --git a/dotscope/src/emulation/runtime/bcl/io/binaryreader.rs b/dotscope/src/emulation/runtime/bcl/io/binaryreader.rs index dfef5a06..289ae49d 100644 --- a/dotscope/src/emulation/runtime/bcl/io/binaryreader.rs +++ b/dotscope/src/emulation/runtime/bcl/io/binaryreader.rs @@ -1118,14 +1118,13 @@ mod tests { }, hook::{HookContext, HookManager, PreHookResult}, }, + thread::EmulationThread, EmValue, HeapRef, }, metadata::{token::Token, typesystem::PointerSize}, test::emulation::create_test_thread, }; - use crate::emulation::thread::EmulationThread; - /// Helper to create a BinaryReader backed by a stream with the given data. fn create_binary_reader(thread: &mut EmulationThread, data: Vec) -> HeapRef { // Create the stream diff --git a/dotscope/src/emulation/runtime/bcl/io/filestream.rs b/dotscope/src/emulation/runtime/bcl/io/filestream.rs index dbc78cde..9b14f936 100644 --- a/dotscope/src/emulation/runtime/bcl/io/filestream.rs +++ b/dotscope/src/emulation/runtime/bcl/io/filestream.rs @@ -1067,6 +1067,7 @@ fn streamreader_noop_pre(_ctx: &HookContext<'_>, _thread: &mut EmulationThread) mod tests { use std::sync::{Arc, RwLock}; + use super::*; use crate::{ emulation::{ filesystem::VirtualFs, @@ -1081,8 +1082,6 @@ mod tests { test::emulation::create_test_thread, }; - use super::*; - /// Creates a test thread with a virtual filesystem containing "test.exe". fn create_thread_with_vfs() -> EmulationThread { let mut vfs = VirtualFs::new(); diff --git a/dotscope/src/emulation/runtime/bcl/reflection/members.rs b/dotscope/src/emulation/runtime/bcl/reflection/members.rs index 695d15f7..cf128854 100644 --- a/dotscope/src/emulation/runtime/bcl/reflection/members.rs +++ b/dotscope/src/emulation/runtime/bcl/reflection/members.rs @@ -838,6 +838,7 @@ fn extract_member_custom_attrs( #[cfg(test)] mod tests { + use super::field_get_value_pre; use crate::{ emulation::{ runtime::hook::{HookContext, PreHookResult}, @@ -847,8 +848,6 @@ mod tests { test::emulation::create_test_thread, }; - use super::field_get_value_pre; - #[test] fn test_field_get_value_hook() { let ctx = HookContext::new( diff --git a/dotscope/src/emulation/runtime/bcl/reflection/methods.rs b/dotscope/src/emulation/runtime/bcl/reflection/methods.rs index 82063cbf..4b161490 100644 --- a/dotscope/src/emulation/runtime/bcl/reflection/methods.rs +++ b/dotscope/src/emulation/runtime/bcl/reflection/methods.rs @@ -1327,6 +1327,7 @@ fn il_generator_noop_pre(_ctx: &HookContext<'_>, _thread: &mut EmulationThread) #[cfg(test)] mod tests { + use super::method_invoke_pre; use crate::{ emulation::{ runtime::hook::{HookContext, PreHookResult}, @@ -1336,8 +1337,6 @@ mod tests { test::emulation::create_test_thread, }; - use super::method_invoke_pre; - #[test] fn test_method_invoke_hook() { let ctx = HookContext::new( diff --git a/dotscope/src/emulation/runtime/bcl/reflection/types.rs b/dotscope/src/emulation/runtime/bcl/reflection/types.rs index e21016be..f4d026cf 100644 --- a/dotscope/src/emulation/runtime/bcl/reflection/types.rs +++ b/dotscope/src/emulation/runtime/bcl/reflection/types.rs @@ -2658,6 +2658,7 @@ fn assembly_name_get_public_key_token_pre( #[cfg(test)] mod tests { + use super::{type_get_module_pre, type_get_type_from_handle_pre}; use crate::{ emulation::{ runtime::hook::{HookContext, PreHookResult}, @@ -2667,8 +2668,6 @@ mod tests { test::emulation::create_test_thread, }; - use super::{type_get_module_pre, type_get_type_from_handle_pre}; - #[test] fn test_get_module_hook() { let ctx = HookContext::new( diff --git a/dotscope/src/emulation/runtime/bcl/statics.rs b/dotscope/src/emulation/runtime/bcl/statics.rs index bf71a1dd..86f483a9 100644 --- a/dotscope/src/emulation/runtime/bcl/statics.rs +++ b/dotscope/src/emulation/runtime/bcl/statics.rs @@ -157,9 +157,8 @@ fn opcode_value_from_field_name(field_name: &str) -> Option { #[cfg(test)] mod tests { - use crate::emulation::memory::ManagedHeap; - use super::*; + use crate::emulation::memory::ManagedHeap; fn heap() -> ManagedHeap { ManagedHeap::new(1024 * 1024) diff --git a/dotscope/src/emulation/runtime/bcl/system/environment.rs b/dotscope/src/emulation/runtime/bcl/system/environment.rs index d2a6b177..17a8b8f9 100644 --- a/dotscope/src/emulation/runtime/bcl/system/environment.rs +++ b/dotscope/src/emulation/runtime/bcl/system/environment.rs @@ -400,14 +400,13 @@ fn get_os_version_pre(_ctx: &HookContext<'_>, thread: &mut EmulationThread) -> P #[cfg(test)] mod tests { + use super::*; use crate::{ emulation::runtime::hook::{HookContext, HookManager, PreHookResult}, metadata::{token::Token, typesystem::PointerSize}, test::emulation::create_test_thread, }; - use super::*; - #[test] fn test_register_hooks() { let manager = HookManager::new(); diff --git a/dotscope/src/emulation/runtime/bcl/system/spans.rs b/dotscope/src/emulation/runtime/bcl/system/spans.rs index 340db006..b8a603b8 100644 --- a/dotscope/src/emulation/runtime/bcl/system/spans.rs +++ b/dotscope/src/emulation/runtime/bcl/system/spans.rs @@ -432,6 +432,7 @@ fn memory_get_length_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> #[cfg(test)] mod tests { + use super::*; use crate::{ emulation::{runtime::hook::HookManager, EmValue}, metadata::{ @@ -441,8 +442,6 @@ mod tests { test::emulation::create_test_thread, }; - use super::*; - fn ctx<'a>( type_name: &'a str, method: &'a str, diff --git a/dotscope/src/emulation/runtime/hook/mod.rs b/dotscope/src/emulation/runtime/hook/mod.rs index 8fd53238..a0a62630 100644 --- a/dotscope/src/emulation/runtime/hook/mod.rs +++ b/dotscope/src/emulation/runtime/hook/mod.rs @@ -135,6 +135,7 @@ mod matcher; mod types; pub use core::Hook; + pub use manager::HookManager; pub use matcher::{ HookMatcher, InternalMethodMatcher, NameMatcher, NativeMethodMatcher, RuntimeMatcher, diff --git a/dotscope/src/emulation/runtime/native.rs b/dotscope/src/emulation/runtime/native.rs index 156dc7b7..b67d0ba7 100644 --- a/dotscope/src/emulation/runtime/native.rs +++ b/dotscope/src/emulation/runtime/native.rs @@ -688,6 +688,7 @@ fn register_get_current_thread(manager: &HookManager) -> Result<()> { #[cfg(test)] mod tests { + use super::{register, NativeFunctionRegistry}; use crate::{ emulation::{ runtime::{HookContext, HookManager, HookOutcome}, @@ -698,8 +699,6 @@ mod tests { test::emulation::create_test_thread, }; - use super::{register, NativeFunctionRegistry}; - fn create_native_context<'a>(dll: &'a str, function: &'a str) -> HookContext<'a> { HookContext::native(Token::new(0x06000001), dll, function, PointerSize::Bit64) } diff --git a/dotscope/src/emulation/runtime/state.rs b/dotscope/src/emulation/runtime/state.rs index 4c091492..b6436956 100644 --- a/dotscope/src/emulation/runtime/state.rs +++ b/dotscope/src/emulation/runtime/state.rs @@ -76,6 +76,8 @@ use std::sync::Arc; +pub use native::NativeFunctionRegistry; + use crate::{ emulation::{ process::{EmulationConfig, UnknownMethodBehavior}, @@ -85,8 +87,6 @@ use crate::{ Result, }; -pub use native::NativeFunctionRegistry; - /// Central runtime state for .NET emulation. /// /// `RuntimeState` is the main entry point for runtime services during emulation. diff --git a/dotscope/src/file/mod.rs b/dotscope/src/file/mod.rs index 5ac476a3..5564ebc4 100644 --- a/dotscope/src/file/mod.rs +++ b/dotscope/src/file/mod.rs @@ -125,6 +125,8 @@ pub mod repair; use std::path::Path; use cowfile::CowFile; +use goblin::pe::PE; +use pe::{DataDirectory, DataDirectoryType, Pe}; use crate::{ file::{ @@ -135,8 +137,6 @@ use crate::{ Error::{self, Goblin, LayoutFailed, Other}, ParseFailure, ParseStage, Result, }; -use goblin::pe::PE; -use pe::{DataDirectory, DataDirectoryType, Pe}; /// Represents a loaded PE file. /// diff --git a/dotscope/src/file/pe.rs b/dotscope/src/file/pe.rs index 2ee870ba..46d35823 100644 --- a/dotscope/src/file/pe.rs +++ b/dotscope/src/file/pe.rs @@ -52,9 +52,7 @@ fn pe_invalid(field: &'static str, reason: String) -> Error { } .into() } -use std::collections::HashMap; -use std::fmt; -use std::io::Write; +use std::{collections::HashMap, fmt, io::Write}; metadata_flags! { /// PE machine type identifier. @@ -2050,11 +2048,25 @@ pub fn relocate_resource_section(data: &mut [u8], old_rva: u32, new_rva: u32) -> })?; // Process the root directory at offset 0 - relocate_resource_directory(data, 0, delta) + relocate_resource_directory(data, 0, delta, 0) } +/// Deepest resource-directory nesting the relocator will follow. +const MAX_RESOURCE_DEPTH: usize = 32; + /// Recursively processes a resource directory and its entries, adjusting RVAs as needed. -fn relocate_resource_directory(data: &mut [u8], offset: usize, delta: i64) -> Result<()> { +fn relocate_resource_directory( + data: &mut [u8], + offset: usize, + delta: i64, + depth: usize, +) -> Result<()> { + // Nothing checks that a subdirectory entry points *forward*, so a `.rsrc` + // entry naming its own directory recursed forever. Resource trees are three + // levels by convention (type/name/language). + if depth >= MAX_RESOURCE_DEPTH { + return Ok(()); + } // Read the directory header let dir = ImageResourceDirectory::read_from(data, offset)?; let res_invalid = |field: &'static str, reason: String| ParseFailure::InvalidField { @@ -2078,7 +2090,12 @@ fn relocate_resource_directory(data: &mut [u8], offset: usize, delta: i64) -> Re if entry.is_directory() { // Entry points to another directory - recurse - relocate_resource_directory(data, entry.target_offset(), delta)?; + relocate_resource_directory( + data, + entry.target_offset(), + delta, + depth.saturating_add(1), + )?; } else { // Entry points to a ResourceDataEntry - adjust the RVA in-place. // The RVA is the first 4 bytes of the ResourceDataEntry structure. diff --git a/dotscope/src/lib.rs b/dotscope/src/lib.rs index a765d5c3..af25ef9d 100644 --- a/dotscope/src/lib.rs +++ b/dotscope/src/lib.rs @@ -503,6 +503,10 @@ pub mod analysis; /// - [`crate::compiler::DeadCodeEliminationPass`] - Removes unreachable blocks /// - [`crate::compiler::DeadMethodEliminationPass`] - Identifies methods with no callers /// +/// Memory: +/// - [`crate::compiler::MemoryOptimizationPass`] - Forwards stores to loads, drops +/// redundant loads, removes dead stores, each gated on an alias proof +/// /// Other passes: /// - [`crate::compiler::OpaquePredicatePass`] - Removes always-true/false conditions /// - [`crate::deobfuscation::DecryptionPass`] - Decrypts values via emulation @@ -894,6 +898,40 @@ pub mod project; /// ``` pub type Result = std::result::Result; +/// Mutable assembly for editing and modification operations. +/// +/// `CilAssembly` provides a mutable layer on top of [`CilAssemblyView`] that enables +/// editing of .NET assembly metadata while tracking changes efficiently. It uses a +/// copy-on-write strategy to minimize memory usage and provides high-level APIs +/// for adding, modifying, and deleting metadata elements. +/// +/// # Key Features +/// +/// - **Change Tracking**: Efficiently tracks modifications without duplicating unchanged data +/// - **High-level APIs**: Builder patterns for creating types, methods, fields, etc. +/// - **Binary Generation**: Write modified assemblies back to disk +/// - **Validation**: Optional validation of metadata consistency +/// +/// # Usage Examples +/// +/// ```rust,no_run +/// use dotscope::{CilAssemblyView, CilAssembly}; +/// +/// // Load and convert to mutable assembly +/// let view = CilAssemblyView::from_path(std::path::Path::new("assembly.dll"))?; +/// let mut assembly = view.to_owned(); +/// +/// // Add a new string to the heap +/// let string_index = assembly.string_add("Hello, World!")?; +/// +/// // Write changes back to file +/// assembly.to_file("modified_assembly.dll")?; +/// # Ok::<(), dotscope::Error>(()) +/// ``` +pub use cilassembly::{ + ChangeRefKind, ChangeRefRc, CilAssembly, CleanupRequest, LastWriteWinsResolver, + MethodBodyBuilder, MethodBuilder, +}; /// `dotscope` Error type. /// /// The main error type for all operations in this crate. Provides detailed error information @@ -912,7 +950,6 @@ pub type Result = std::result::Result; /// } /// ``` pub use error::{Error, HeapKind, MethodLookupError, ParseFailure, ParseStage, StreamKind}; - /// Raw assembly view for editing and modification operations. /// /// `CilAssemblyView` provides direct access to .NET assembly metadata structures @@ -961,43 +998,31 @@ pub use error::{Error, HeapKind, MethodLookupError, ParseFailure, ParseStage, St /// # Ok::<(), dotscope::Error>(()) /// ``` pub use metadata::cilassemblyview::CilAssemblyView; +mod cilassembly; -/// Mutable assembly for editing and modification operations. -/// -/// `CilAssembly` provides a mutable layer on top of [`CilAssemblyView`] that enables -/// editing of .NET assembly metadata while tracking changes efficiently. It uses a -/// copy-on-write strategy to minimize memory usage and provides high-level APIs -/// for adding, modifying, and deleting metadata elements. -/// -/// # Key Features +/// Provides access to low-level file and memory parsing utilities. /// -/// - **Change Tracking**: Efficiently tracks modifications without duplicating unchanged data -/// - **High-level APIs**: Builder patterns for creating types, methods, fields, etc. -/// - **Binary Generation**: Write modified assemblies back to disk -/// - **Validation**: Optional validation of metadata consistency +/// The [`crate::Parser`] type is used for decoding CIL bytecode and metadata streams. /// /// # Usage Examples /// /// ```rust,no_run -/// use dotscope::{CilAssemblyView, CilAssembly}; -/// -/// // Load and convert to mutable assembly -/// let view = CilAssemblyView::from_path(std::path::Path::new("assembly.dll"))?; -/// let mut assembly = view.to_owned(); -/// -/// // Add a new string to the heap -/// let string_index = assembly.string_add("Hello, World!")?; -/// -/// // Write changes back to file -/// assembly.to_file("modified_assembly.dll")?; +/// use dotscope::{Parser, assembly::decode_instruction}; +/// let code = [0x2A]; // ret +/// let mut parser = Parser::new(&code); +/// let instr = decode_instruction(&mut parser, 0x1000)?; +/// assert_eq!(instr.mnemonic, "ret"); /// # Ok::<(), dotscope::Error>(()) /// ``` -pub use cilassembly::{ - ChangeRefKind, ChangeRefRc, CilAssembly, CleanupRequest, LastWriteWinsResolver, - MethodBodyBuilder, MethodBuilder, +pub use file::{ + parser::Parser, + pe::{ + CoffHeader, DataDirectories, DataDirectory, DataDirectoryType, DosHeader, + Export as PeExport, Import as PeImport, Machine, OptionalHeader, Pe, PeCharacteristics, + SectionTable, StandardFields, Subsystem, WindowsFields, + }, + File, }; -mod cilassembly; - /// Main entry point for working with .NET assemblies. /// /// See [`crate::metadata::cilobject::CilObject`] for high-level analysis and metadata access. @@ -1011,26 +1036,6 @@ mod cilassembly; /// # Ok::<(), dotscope::Error>(()) /// ``` pub use metadata::cilobject::CilObject; - -/// Configuration for metadata validation during assembly loading. -/// -/// Controls which validation checks are performed when loading .NET assemblies. -/// Different presets are available for various use cases. -/// -/// # Usage Examples -/// -/// ```rust,no_run -/// use dotscope::{CilObject, ValidationConfig}; -/// -/// // Use minimal validation for best performance -/// let assembly = CilObject::from_path_with_validation( -/// std::path::Path::new("tests/samples/WindowsBase.dll"), -/// ValidationConfig::minimal() -/// )?; -/// # Ok::<(), dotscope::Error>(()) -/// ``` -pub use metadata::validation::{ValidationConfig, ValidationEngine}; - /// Composable query builders for filtering types and methods. /// /// `TypeQuery` and `MethodQuery` provide a fluent API for searching and filtering @@ -1051,7 +1056,6 @@ pub use metadata::validation::{ValidationConfig, ValidationEngine}; /// # Ok::<(), dotscope::Error>(()) /// ``` pub use metadata::query::{MethodQuery, TypeQuery}; - /// Metadata streams and heaps for direct access to ECMA-335 data structures. /// /// These types provide low-level access to the metadata structures: @@ -1088,27 +1092,21 @@ pub use metadata::streams::{ Blob, BlobIterator, Guid, GuidIterator, StreamHeader, Strings, StringsIterator, TablesHeader, UserStrings, UserStringsIterator, }; - -/// Provides access to low-level file and memory parsing utilities. +/// Configuration for metadata validation during assembly loading. /// -/// The [`crate::Parser`] type is used for decoding CIL bytecode and metadata streams. +/// Controls which validation checks are performed when loading .NET assemblies. +/// Different presets are available for various use cases. /// /// # Usage Examples /// /// ```rust,no_run -/// use dotscope::{Parser, assembly::decode_instruction}; -/// let code = [0x2A]; // ret -/// let mut parser = Parser::new(&code); -/// let instr = decode_instruction(&mut parser, 0x1000)?; -/// assert_eq!(instr.mnemonic, "ret"); +/// use dotscope::{CilObject, ValidationConfig}; +/// +/// // Use minimal validation for best performance +/// let assembly = CilObject::from_path_with_validation( +/// std::path::Path::new("tests/samples/WindowsBase.dll"), +/// ValidationConfig::minimal() +/// )?; /// # Ok::<(), dotscope::Error>(()) /// ``` -pub use file::{ - parser::Parser, - pe::{ - CoffHeader, DataDirectories, DataDirectory, DataDirectoryType, DosHeader, - Export as PeExport, Import as PeImport, Machine, OptionalHeader, Pe, PeCharacteristics, - SectionTable, StandardFields, Subsystem, WindowsFields, - }, - File, -}; +pub use metadata::validation::{ValidationConfig, ValidationEngine}; diff --git a/dotscope/src/metadata/cilassemblyview.rs b/dotscope/src/metadata/cilassemblyview.rs index e41da312..1e7ab370 100644 --- a/dotscope/src/metadata/cilassemblyview.rs +++ b/dotscope/src/metadata/cilassemblyview.rs @@ -107,9 +107,10 @@ //! - [`crate::metadata::cor20header`] - Provides CLR header information //! - File I/O abstraction for memory-mapped or in-memory access +use std::{path::Path, sync::Arc}; + use log::{debug, warn}; use ouroboros::self_referencing; -use std::{path::Path, sync::Arc}; use crate::{ cilassembly::CilAssembly, @@ -1274,9 +1275,10 @@ impl CilAssemblyView { #[cfg(test)] mod tests { + use std::{fs, path::PathBuf}; + use super::*; use crate::test::factories::metadata::cilassemblyview::verify_assembly_view_complete; - use std::{fs, path::PathBuf}; #[test] fn from_file() { diff --git a/dotscope/src/metadata/cor20header.rs b/dotscope/src/metadata/cor20header.rs index 82471ada..9171c7ab 100644 --- a/dotscope/src/metadata/cor20header.rs +++ b/dotscope/src/metadata/cor20header.rs @@ -85,9 +85,9 @@ //! # Reference //! - [ECMA-335 II.24](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) +use std::{fmt, io::Write}; + use crate::{file::parser::Parser, Result}; -use std::fmt; -use std::io::Write; metadata_flags! { /// COR20 runtime flags controlling .NET assembly behavior. diff --git a/dotscope/src/metadata/customattributes/encoder.rs b/dotscope/src/metadata/customattributes/encoder.rs index 57a4f136..ddc07822 100644 --- a/dotscope/src/metadata/customattributes/encoder.rs +++ b/dotscope/src/metadata/customattributes/encoder.rs @@ -502,8 +502,10 @@ fn write_string(buffer: &mut Vec, value: &str) -> Result<()> { #[cfg(test)] mod tests { use super::*; - use crate::metadata::customattributes::{CustomAttributeNamedArgument, CustomAttributeValue}; - use crate::metadata::typesystem::CilTypeReference; + use crate::metadata::{ + customattributes::{CustomAttributeNamedArgument, CustomAttributeValue}, + typesystem::CilTypeReference, + }; #[test] fn test_encode_simple_custom_attribute() { diff --git a/dotscope/src/metadata/customattributes/mod.rs b/dotscope/src/metadata/customattributes/mod.rs index 26338427..98848687 100644 --- a/dotscope/src/metadata/customattributes/mod.rs +++ b/dotscope/src/metadata/customattributes/mod.rs @@ -125,15 +125,19 @@ pub use types::*; #[cfg(test)] mod tests { - use crate::metadata::customattributes::{ - encode_custom_attribute_value, parse_custom_attribute_data, - parse_custom_attribute_data_with_registry, CustomAttributeArgument, - CustomAttributeNamedArgument, CustomAttributeValue, - }; - use crate::metadata::typesystem::{CilFlavor, CilTypeReference}; - use crate::test::factories::metadata::customattributes::{ - create_constructor_with_params_and_registry, create_empty_method, - create_method_with_params, get_test_type_registry, + use crate::{ + metadata::{ + customattributes::{ + encode_custom_attribute_value, parse_custom_attribute_data, + parse_custom_attribute_data_with_registry, CustomAttributeArgument, + CustomAttributeNamedArgument, CustomAttributeValue, + }, + typesystem::{CilFlavor, CilTypeReference}, + }, + test::factories::metadata::customattributes::{ + create_constructor_with_params_and_registry, create_empty_method, + create_method_with_params, get_test_type_registry, + }, }; /// Helper macro to assert that two `CustomAttributeArgument` values match and are equal. diff --git a/dotscope/src/metadata/customattributes/parser.rs b/dotscope/src/metadata/customattributes/parser.rs index 729dfd38..76b720ad 100644 --- a/dotscope/src/metadata/customattributes/parser.rs +++ b/dotscope/src/metadata/customattributes/parser.rs @@ -116,6 +116,8 @@ //! - **Memory Safety**: Comprehensive bounds checking and nesting depth limiting //! - **Error Handling**: Detailed error messages for debugging malformed data +use std::sync::Arc; + use crate::{ file::parser::Parser, metadata::{ @@ -131,7 +133,6 @@ use crate::{ Error::DepthLimitExceeded, Result, }; -use std::sync::Arc; /// Maximum nesting depth for custom attribute parsing. /// @@ -143,6 +144,12 @@ use std::sync::Arc; /// while still protecting against malformed or malicious metadata. const MAX_NESTING_DEPTH: usize = 1000; +/// Maximum **native** recursion depth for nested attribute arguments. +/// +/// Separate from [`MAX_NESTING_DEPTH`], which bounds a heap work stack where +/// 1000 entries are cheap. This bounds real stack frames, where they are not. +const MAX_ARGUMENT_RECURSION: usize = 64; + /// Maximum number of named arguments in a custom attribute. /// /// Custom attributes can have named properties/fields, but excessive counts indicate @@ -391,6 +398,13 @@ pub struct CustomAttributeParser<'a> { parser: Parser<'a>, /// Optional TypeRegistry for cross-assembly type resolution type_registry: Option>, + /// Current native-recursion depth, for stack-overflow prevention. + /// + /// `parse_fixed_argument` recurses into itself for array elements. The + /// `MAX_NESTING_DEPTH` defined in this file bounds a *heap* work stack + /// elsewhere and was never consulted here, so the recursion was unbounded + /// over an attacker-supplied attribute blob. + depth: usize, } impl<'a> CustomAttributeParser<'a> { @@ -412,6 +426,7 @@ impl<'a> CustomAttributeParser<'a> { Self { parser: Parser::new(data), type_registry: None, + depth: 0, } } @@ -440,6 +455,7 @@ impl<'a> CustomAttributeParser<'a> { Self { parser: Parser::new(data), type_registry: Some(type_registry), + depth: 0, } } @@ -677,6 +693,21 @@ impl<'a> CustomAttributeParser<'a> { fn parse_fixed_argument( &mut self, cil_type: &CilTypeRef, + ) -> Result> { + self.depth = self.depth.saturating_add(1); + if self.depth >= MAX_ARGUMENT_RECURSION { + self.depth = self.depth.saturating_sub(1); + return Err(DepthLimitExceeded(MAX_ARGUMENT_RECURSION)); + } + let result = self.parse_fixed_argument_inner(cil_type); + self.depth = self.depth.saturating_sub(1); + result + } + + /// Inner implementation of [`parse_fixed_argument`]; depth handled by caller. + fn parse_fixed_argument_inner( + &mut self, + cil_type: &CilTypeRef, ) -> Result> { let Some(type_ref) = cil_type.upgrade() else { return Err(malformed_error!("Type reference has been dropped")); @@ -1387,17 +1418,6 @@ impl<'a> CustomAttributeParser<'a> { #[cfg(test)] mod tests { - use super::*; - use crate::metadata::{ - identity::AssemblyIdentity, - tables::{Param, ParamAttributes}, - token::Token, - typesystem::{CilFlavor, CilPrimitiveKind, CilTypeRef, TypeBuilder, TypeRegistry}, - }; - use crate::test::factories::metadata::customattributes::{ - create_constructor_with_params, create_constructor_with_params_and_registry, - create_empty_constructor, get_test_type_registry, - }; use std::{ collections::HashMap, sync::{ @@ -1406,6 +1426,20 @@ mod tests { }, }; + use super::*; + use crate::{ + metadata::{ + identity::AssemblyIdentity, + tables::{Param, ParamAttributes}, + token::Token, + typesystem::{CilFlavor, CilPrimitiveKind, CilTypeRef, TypeBuilder, TypeRegistry}, + }, + test::factories::metadata::customattributes::{ + create_constructor_with_params, create_constructor_with_params_and_registry, + create_empty_constructor, get_test_type_registry, + }, + }; + #[test] fn test_parse_empty_blob_with_method() { let method = create_empty_constructor(); diff --git a/dotscope/src/metadata/dependencies/graph.rs b/dotscope/src/metadata/dependencies/graph.rs index 2e5518b8..490823f8 100644 --- a/dotscope/src/metadata/dependencies/graph.rs +++ b/dotscope/src/metadata/dependencies/graph.rs @@ -9,9 +9,8 @@ use std::sync::{ Arc, RwLock, }; -use dashmap::DashMap; - use analyssa::graph::{algorithms, DirectedGraph, IndexedGraph, NodeId}; +use dashmap::DashMap; use crate::{ metadata::{dependencies::AssemblyDependency, identity::AssemblyIdentity}, @@ -242,22 +241,23 @@ impl AssemblyDependencyGraph { /// Detect circular dependencies in the assembly graph. /// - /// Uses a depth-first search algorithm to detect cycles in the dependency - /// graph. Returns the first cycle found, or None if the graph is acyclic. - /// Results are cached to improve performance on repeated calls. + /// Returns the members of one cyclic strongly connected component, or + /// `None` if the graph is acyclic. Results are cached to improve + /// performance on repeated calls. /// /// # Returns - /// * `Ok(Some(cycle))` - Circular dependency found, returns the cycle path + /// * `Ok(Some(cycle))` - Circular dependency found, returns the assemblies + /// participating in it /// * `Ok(None)` - No circular dependencies detected /// * `Err(_)` - Error occurred during cycle detection /// /// # Algorithm - /// Uses a modified DFS with three-color marking: - /// - **White**: Unvisited nodes - /// - **Gray**: Currently being processed (in recursion stack) - /// - **Black**: Completely processed - /// - /// A back edge from gray to gray indicates a cycle. + /// One Tarjan pass over the dependency graph, reporting the + /// lowest-numbered component that is cyclic. The result is a **set of + /// participants, not a closed walk**: each assembly appears once, so a + /// self-dependency reports a single entry rather than the same assembly + /// twice. Reporting by component (rather than the first back edge DFS + /// happens to find) keeps the answer deterministic across runs. /// /// # Examples /// @@ -751,10 +751,9 @@ impl Default for AssemblyDependencyGraph { #[cfg(test)] mod tests { - use super::*; - use std::{collections::HashMap, thread}; + use super::*; use crate::{ metadata::dependencies::DependencyType, test::helpers::dependencies::{create_test_dependency, create_test_identity}, @@ -883,9 +882,10 @@ mod tests { assert!(cycles.is_some()); let cycle = cycles.unwrap(); - assert_eq!(cycle.len(), 2); // A -> A + // The reported cycle is the participating component, not a closed walk, + // so a self-dependency names "A" once rather than twice. + assert_eq!(cycle.len(), 1); assert_eq!(cycle[0].name, "A"); - assert_eq!(cycle[1].name, "A"); } #[test] diff --git a/dotscope/src/metadata/diagnostics.rs b/dotscope/src/metadata/diagnostics.rs index ad78c195..dd40db9d 100644 --- a/dotscope/src/metadata/diagnostics.rs +++ b/dotscope/src/metadata/diagnostics.rs @@ -492,9 +492,9 @@ impl fmt::Display for Diagnostics { #[cfg(test)] mod tests { + use std::{sync::Arc, thread}; + use super::*; - use std::sync::Arc; - use std::thread; #[test] fn test_diagnostic_creation() { diff --git a/dotscope/src/metadata/exports/builder.rs b/dotscope/src/metadata/exports/builder.rs index 0819a815..c56b572c 100644 --- a/dotscope/src/metadata/exports/builder.rs +++ b/dotscope/src/metadata/exports/builder.rs @@ -361,9 +361,10 @@ impl Default for NativeExportsBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::cilassembly::CilAssembly; - use std::path::PathBuf; #[test] fn test_native_exports_builder_basic() { diff --git a/dotscope/src/metadata/exports/container.rs b/dotscope/src/metadata/exports/container.rs index c51bc44f..3c7e4bd5 100644 --- a/dotscope/src/metadata/exports/container.rs +++ b/dotscope/src/metadata/exports/container.rs @@ -43,9 +43,10 @@ //! let functions = container.get_all_exported_functions(); //! ``` -use dashmap::{mapref::entry::Entry, DashMap}; use std::sync::atomic::{AtomicBool, Ordering}; +use dashmap::{mapref::entry::Entry, DashMap}; + use crate::{ metadata::{ exports::{native::NativeExports, Exports as CilExports}, diff --git a/dotscope/src/metadata/imports/builder.rs b/dotscope/src/metadata/imports/builder.rs index f90a01d7..77f72340 100644 --- a/dotscope/src/metadata/imports/builder.rs +++ b/dotscope/src/metadata/imports/builder.rs @@ -297,9 +297,10 @@ impl Default for NativeImportsBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::cilassembly::CilAssembly; - use std::path::PathBuf; #[test] fn test_native_imports_builder_basic() { diff --git a/dotscope/src/metadata/imports/cil.rs b/dotscope/src/metadata/imports/cil.rs index cfe45ce0..61b6e7b2 100644 --- a/dotscope/src/metadata/imports/cil.rs +++ b/dotscope/src/metadata/imports/cil.rs @@ -125,9 +125,10 @@ //! - [`dashmap::DashMap`] for high-performance index lookups //! - Reference counting enables safe sharing across threads without contention +use std::sync::Arc; + use crossbeam_skiplist::SkipMap; use dashmap::DashMap; -use std::sync::Arc; use crate::{ metadata::{ @@ -1381,12 +1382,11 @@ pub trait ImportContainer { #[cfg(test)] mod tests { + use super::*; use crate::test::{ create_assembly_ref, create_cil_type, create_file, create_method, create_module_ref, }; - use super::*; - #[test] fn test_add_method_import() { let imports = Imports::new(); diff --git a/dotscope/src/metadata/imports/container.rs b/dotscope/src/metadata/imports/container.rs index aae8a08e..f3f6fde5 100644 --- a/dotscope/src/metadata/imports/container.rs +++ b/dotscope/src/metadata/imports/container.rs @@ -43,12 +43,13 @@ //! let dependencies = container.get_all_dll_dependencies(); //! ``` -use dashmap::{mapref::entry::Entry, DashMap}; use std::{ collections::HashSet, sync::atomic::{AtomicBool, Ordering}, }; +use dashmap::{mapref::entry::Entry, DashMap}; + use crate::{ metadata::{ imports::{ diff --git a/dotscope/src/metadata/loader/mod.rs b/dotscope/src/metadata/loader/mod.rs index 53360b62..ac1c618c 100644 --- a/dotscope/src/metadata/loader/mod.rs +++ b/dotscope/src/metadata/loader/mod.rs @@ -141,10 +141,12 @@ static LOADERS: [&'static dyn MetadataLoader; 54] = [ &inheritance::InheritanceResolver, ]; -use crate::{metadata::tables::TableId, project::ProjectContext, Error, Result}; +use std::{sync::LazyLock, time::Instant}; + use log::debug; use rayon::prelude::*; -use std::{sync::LazyLock, time::Instant}; + +use crate::{metadata::tables::TableId, project::ProjectContext, Error, Result}; /// Static cache of pre-computed execution levels for parallel loader execution. /// diff --git a/dotscope/src/metadata/method/body.rs b/dotscope/src/metadata/method/body.rs index 9fd4b0a6..bfce74f3 100644 --- a/dotscope/src/metadata/method/body.rs +++ b/dotscope/src/metadata/method/body.rs @@ -850,9 +850,8 @@ impl MethodBody { #[cfg(test)] mod tests { - use crate::metadata::method::ExceptionHandlerFlags; - use super::*; + use crate::metadata::method::ExceptionHandlerFlags; #[test] fn tiny() { diff --git a/dotscope/src/metadata/method/mod.rs b/dotscope/src/metadata/method/mod.rs index af747300..742e887f 100644 --- a/dotscope/src/metadata/method/mod.rs +++ b/dotscope/src/metadata/method/mod.rs @@ -99,10 +99,10 @@ mod exceptions; mod iter; mod types; -use crossbeam_skiplist::SkipMap; use std::sync::{atomic::AtomicU32, Arc, OnceLock, Weak}; pub use body::*; +use crossbeam_skiplist::SkipMap; pub use encode::encode_method_body_header; pub use exceptions::*; pub use iter::InstructionIterator; @@ -1962,7 +1962,6 @@ impl Method { #[cfg(test)] mod tests { use super::*; - use crate::{ assembly::{ BasicBlock, FlowType, Instruction, InstructionCategory, Operand, StackBehavior, diff --git a/dotscope/src/metadata/resources/encoder.rs b/dotscope/src/metadata/resources/encoder.rs index 3abc2962..354a9f7a 100644 --- a/dotscope/src/metadata/resources/encoder.rs +++ b/dotscope/src/metadata/resources/encoder.rs @@ -99,12 +99,13 @@ //! - [.NET Binary Format Data Structure](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/) //! - Microsoft .NET Framework Resource Management Documentation +use std::collections::BTreeMap; + use crate::{ metadata::resources::{ResourceType, RESOURCE_MAGIC}, utils::{compressed_uint_size, to_u32, write_7bit_encoded_int, write_compressed_uint}, Error, Result, }; -use std::collections::BTreeMap; /// Computes the hash value for a resource name using the official .NET hash function. /// diff --git a/dotscope/src/metadata/resources/mod.rs b/dotscope/src/metadata/resources/mod.rs index 46c363d2..15b95f53 100644 --- a/dotscope/src/metadata/resources/mod.rs +++ b/dotscope/src/metadata/resources/mod.rs @@ -161,13 +161,13 @@ mod encoder; mod parser; mod types; +use std::{collections::BTreeMap, sync::Arc}; + +use dashmap::DashMap; pub use encoder::*; pub use parser::{parse_dotnet_resource, parse_dotnet_resource_ref, Resource}; pub use types::*; -use dashmap::DashMap; -use std::{collections::BTreeMap, sync::Arc}; - use crate::{file::File, metadata::tables::ManifestResourceRc}; /// Container for all resources in an assembly with thread-safe access and efficient lookup. @@ -635,9 +635,8 @@ impl<'a> IntoIterator for &'a Resources { #[cfg(test)] mod tests { - use crate::metadata::resources::parser::{parse_dotnet_resource, parse_dotnet_resource_ref}; - use super::*; + use crate::metadata::resources::parser::{parse_dotnet_resource, parse_dotnet_resource_ref}; /// Helper trait to abstract over owned (ResourceType) and borrowed (ResourceTypeRef) variants. /// This allows writing generic test code that works with both. diff --git a/dotscope/src/metadata/root.rs b/dotscope/src/metadata/root.rs index b2f99e8a..9568371f 100644 --- a/dotscope/src/metadata/root.rs +++ b/dotscope/src/metadata/root.rs @@ -38,12 +38,13 @@ //! //! - [ECMA-335 II.24.2.1: Metadata root](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) +use std::io::Write; + use crate::{ metadata::streams::StreamHeader, utils::{read_le, read_le_at}, Error, ParseFailure, ParseStage, Result, }; -use std::io::Write; /// The MAGIC value indicating the CIL header ("BSJB" in ASCII). pub const CIL_HEADER_MAGIC: u32 = 0x424A_5342; diff --git a/dotscope/src/metadata/security/encoder.rs b/dotscope/src/metadata/security/encoder.rs index 2b86fbc6..450d82a5 100644 --- a/dotscope/src/metadata/security/encoder.rs +++ b/dotscope/src/metadata/security/encoder.rs @@ -123,6 +123,8 @@ //! - [ECMA-335 6th Edition, Partition II, Section 23.1.4 - Security Permission Sets](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) //! - Microsoft .NET Framework Security Documentation (archived) +use std::{collections::HashMap, io::Write}; + use crate::{ metadata::security::{ ArgumentType, ArgumentValue, NamedArgument, Permission, PermissionSetFormat, @@ -130,7 +132,6 @@ use crate::{ utils::{to_u32, write_compressed_int, write_compressed_uint}, Result, }; -use std::{collections::HashMap, io::Write}; /// Encodes a permission set to binary format. /// diff --git a/dotscope/src/metadata/security/namedargument.rs b/dotscope/src/metadata/security/namedargument.rs index fafdf0e3..2880e876 100644 --- a/dotscope/src/metadata/security/namedargument.rs +++ b/dotscope/src/metadata/security/namedargument.rs @@ -149,9 +149,10 @@ //! //! [`NamedArgument`] instances are immutable after creation and safe to share across threads. -use crate::metadata::security::{ArgumentType, ArgumentValue}; use std::fmt; +use crate::metadata::security::{ArgumentType, ArgumentValue}; + /// Represents a named argument (property or field) in a .NET security permission. /// /// Named arguments configure specific aspects of a permission, such as which files can be accessed diff --git a/dotscope/src/metadata/security/permission.rs b/dotscope/src/metadata/security/permission.rs index 85108091..dfc55355 100644 --- a/dotscope/src/metadata/security/permission.rs +++ b/dotscope/src/metadata/security/permission.rs @@ -224,10 +224,11 @@ //! //! [`Permission`] instances are immutable after creation and safe to share across threads. +use std::fmt; + use crate::metadata::security::{ security_classes, ArgumentValue, NamedArgument, SecurityPermissionFlags, }; -use std::fmt; /// Represents a .NET security permission within a permission set. /// diff --git a/dotscope/src/metadata/security/permissionset.rs b/dotscope/src/metadata/security/permissionset.rs index b2014239..ab3f930f 100644 --- a/dotscope/src/metadata/security/permissionset.rs +++ b/dotscope/src/metadata/security/permissionset.rs @@ -270,6 +270,13 @@ //! including various binary format variations and assembly name mappings that changed //! over time (e.g., mscorlib vs System.Private.CoreLib). +use std::fmt; + +use quick_xml::{ + events::{attributes::Attributes, Event}, + Reader, +}; + use crate::{ file::parser::Parser, metadata::security::{ @@ -279,11 +286,6 @@ use crate::{ utils::EnumUtils, ParseFailure, ParseStage, Result, }; -use quick_xml::{ - events::{attributes::Attributes, Event}, - Reader, -}; -use std::fmt; /// Maximum number of permissions in a permission set. /// @@ -419,6 +421,9 @@ pub struct PermissionSet { data: Vec, } +/// Deepest `TAGGED_OBJECT` nesting a permission-set blob may declare. +const MAX_ARGUMENT_NESTING: usize = 64; + impl PermissionSet { /// Creates a new `PermissionSet` from binary data. /// @@ -596,16 +601,21 @@ impl PermissionSet { String::new() }; - let (arg_type, value) = - Self::parse_argument_value(&mut parser, prop_type, &class_name, &prop_name) - .map_err(|e| { - malformed_error!( + let (arg_type, value) = Self::parse_argument_value( + &mut parser, + prop_type, + 0, + &class_name, + &prop_name, + ) + .map_err(|e| { + malformed_error!( "Permission '{}', property '{}': failed to parse argument value: {}", class_name, prop_name, e ) - })?; + })?; named_arguments.push(NamedArgument { name: prop_name, @@ -1029,9 +1039,16 @@ impl PermissionSet { fn parse_argument_value( parser: &mut Parser, arg_type: u8, + depth: usize, permission_class: &str, property_name: &str, ) -> Result<(ArgumentType, ArgumentValue)> { + // `TAGGED_OBJECT` (0x51) nests another value inside itself, so one + // attacker byte bought one stack frame. Permission-set blobs come from + // the analyzed assembly's metadata, so this is untrusted input. + if depth >= MAX_ARGUMENT_NESTING { + return Err(crate::Error::DepthLimitExceeded(MAX_ARGUMENT_NESTING)); + } match arg_type { // ELEMENT_TYPE_BOOLEAN (0x02) 0x02 => { @@ -1111,6 +1128,7 @@ impl PermissionSet { let (_inner_arg_type, inner_value) = Self::parse_argument_value( parser, inner_type, + depth.saturating_add(1), permission_class, property_name, )?; diff --git a/dotscope/src/metadata/signatures/parser.rs b/dotscope/src/metadata/signatures/parser.rs index 648abd0f..7349d916 100644 --- a/dotscope/src/metadata/signatures/parser.rs +++ b/dotscope/src/metadata/signatures/parser.rs @@ -191,6 +191,21 @@ pub mod CALLING_CONVENTION { /// with deep generic hierarchies while still preventing resource exhaustion. const MAX_NESTING_DEPTH: usize = 1000; +/// Maximum depth of **native** (call-stack) recursion in this parser. +/// +/// Deliberately far tighter than [`MAX_NESTING_DEPTH`], because the two bound +/// different resources. That one limits a heap work stack, where 1000 entries +/// cost a few hundred KB and are harmless. This one limits real stack frames: +/// `parse_type_simple` recurses natively, and so does the +/// `parse_type` → `parse_method_signature` → `parse_param` → `parse_type` +/// cycle. At debug frame sizes 1000 of those exhaust the stack outright, so +/// reusing the heap number here left the guard nominally present and +/// practically useless. +/// +/// Real signatures do not approach this: pointer/generic nesting in shipped +/// assemblies is single digits. +const MAX_NATIVE_RECURSION_DEPTH: usize = 64; + /// Maximum number of array dimensions allowed in a signature. /// /// .NET supports multi-dimensional arrays, but reasonable limits prevent memory exhaustion @@ -347,6 +362,19 @@ const MAX_LOCAL_VARIABLES: u32 = 65536; pub struct SignatureParser<'a> { /// Binary data parser for reading signature bytes parser: Parser<'a>, + /// Current recursion depth, for stack-overflow prevention. + /// + /// Lives on the parser rather than as a per-call argument so the bound + /// survives the wrappers. [`parse_type`] is iterative and bounds itself with + /// a heap work stack, but it allocates a *fresh* one per invocation — so a + /// signature that re-enters it through `FNPTR` (`parse_type` → + /// `parse_method_signature` → `parse_param` → `parse_type`) re-armed the cap + /// on every hop and was effectively unbounded. `parse_type_simple`, the + /// lookahead helper, recursed natively with no bound at all. + /// + /// Both are attacker-reachable: signature blobs come straight from the + /// metadata heap of the .NET file being analyzed. + depth: usize, } impl<'a> SignatureParser<'a> { @@ -382,6 +410,7 @@ impl<'a> SignatureParser<'a> { pub fn new(data: &'a [u8]) -> Self { SignatureParser { parser: Parser::new(data), + depth: 0, } } @@ -432,6 +461,19 @@ impl<'a> SignatureParser<'a> { /// using an iterative stack-based approach. Custom modifiers are parsed inline /// and associated with the appropriate type elements. fn parse_type(&mut self) -> Result { + // Accounted on the parser, not just the local work stack: this function + // re-enters itself through `FNPTR` (→ `parse_method_signature` → + // `parse_param` → here), and each entry allocated a fresh work stack, so + // the local bound below re-armed on every hop and never fired. The + // running depth makes the cap survive the round trip. + self.enter()?; + let result = self.parse_type_inner(); + self.exit(); + result + } + + /// Inner implementation of [`parse_type`]; depth is handled by the caller. + fn parse_type_inner(&mut self) -> Result { /// Work items for the iterative parsing stack enum WorkItem { /// Parse a type signature and push result @@ -462,8 +504,14 @@ impl<'a> SignatureParser<'a> { work_stack.push(WorkItem::ParseType); while let Some(work) = work_stack.pop() { - // Check nesting depth limit - if work_stack.len().saturating_add(result_stack.len()) > MAX_NESTING_DEPTH { + // Nesting limit, counting both this invocation's pending work and + // the depth already accumulated by any enclosing invocations. + if self + .depth + .saturating_add(work_stack.len()) + .saturating_add(result_stack.len()) + > MAX_NESTING_DEPTH + { return Err(DepthLimitExceeded(MAX_NESTING_DEPTH)); } @@ -745,9 +793,40 @@ impl<'a> SignatureParser<'a> { .ok_or_else(|| malformed_error!("internal: result stack empty after validation")) } + /// Enters one level of native recursion, refusing to go past + /// [`MAX_NATIVE_RECURSION_DEPTH`]. + /// + /// Paired with [`exit`](Self::exit) — every early return between the two + /// would leak a level, so callers use it around a single recursive call. + fn enter(&mut self) -> Result<()> { + self.depth = self.depth.saturating_add(1); + if self.depth >= MAX_NATIVE_RECURSION_DEPTH { + self.depth = self.depth.saturating_sub(1); + return Err(DepthLimitExceeded(MAX_NATIVE_RECURSION_DEPTH)); + } + Ok(()) + } + + /// Leaves one level of nesting. + fn exit(&mut self) { + self.depth = self.depth.saturating_sub(1); + } + /// Helper method to parse a type signature without building the result (for lookahead). /// This is used to skip over types when we need to read metadata that comes after them. + /// + /// Depth-bounded: one input byte is one stack frame here, so a run of `PTR` + /// bytes in a field signature is a direct stack-exhaustion vector. fn parse_type_simple(&mut self) -> Result<()> { + self.enter()?; + let result = self.parse_type_simple_inner(); + self.exit(); + result + } + + /// Inner implementation of [`parse_type_simple`]; depth is handled by the + /// caller, matching the `MarshallingParser` idiom in this crate. + fn parse_type_simple_inner(&mut self) -> Result<()> { let current_byte = self.parser.read_le::()?; match current_byte { ELEMENT_TYPE::VOID @@ -2384,4 +2463,52 @@ mod tests { let mut parser = SignatureParser::new(&[0x07, 0x08]); // Should be 0x06 for FIELD assert!(parser.parse_field_signature().is_err()); } + + /// A long run of `PTR` bytes in a field signature is refused, not recursed. + /// + /// `parse_type_simple` is the lookahead helper; it recursed natively with no + /// bound, so one attacker byte bought one stack frame. Measured before the + /// fix: a 50 KB blob aborted the process on an 8 MiB stack, ~5 KB on 2 MiB. + /// Signature blobs come straight out of the analyzed file's metadata heap, + /// so this is reachable by every .NET sample. + #[test] + fn a_deep_pointer_run_is_refused_rather_than_overflowing() { + // FIELD, ARRAY, then a long run of PTR, terminated by I4. + let mut blob = vec![0x06, 0x14]; + blob.extend(std::iter::repeat_n(0x0F, 8192)); + blob.push(0x08); + + let mut parser = SignatureParser::new(&blob); + // Reaching a verdict at all is the assertion — this used to abort the + // process rather than return. + assert!( + parser.parse_field_signature().is_err(), + "a signature nested past the cap must be refused" + ); + } + + /// The nesting cap survives re-entry through `FNPTR`. + /// + /// `parse_type` bounds itself with a heap work stack, but allocated a fresh + /// one per invocation — so `parse_type` → `parse_method_signature` → + /// `parse_param` → `parse_type` re-armed the cap on every hop. Before the + /// fix, 1000 nested FNPTRs parsed successfully and the cap never fired. + #[test] + fn the_nesting_cap_survives_fnptr_reentry() { + // FIELD, then N × (FNPTR, DEFAULT calling convention, 0 params), + // bottoming out in I4. + let mut blob = vec![0x06]; + for _ in 0..2000 { + blob.push(0x1B); // FNPTR + blob.push(0x00); // DEFAULT + blob.push(0x00); // param count 0 + } + blob.push(0x08); // I4 return + + let mut parser = SignatureParser::new(&blob); + assert!( + parser.parse_field_signature().is_err(), + "nesting through FNPTR must count toward the same cap" + ); + } } diff --git a/dotscope/src/metadata/streams/streamheader.rs b/dotscope/src/metadata/streams/streamheader.rs index bc791ad7..e682fb1a 100644 --- a/dotscope/src/metadata/streams/streamheader.rs +++ b/dotscope/src/metadata/streams/streamheader.rs @@ -159,9 +159,10 @@ //! - **ECMA-335 II.24.2.2**: Stream header format and directory structure //! - **ECMA-335 II.24.2**: Complete metadata stream architecture overview -use crate::{utils::read_le_at, ParseFailure, ParseStage, Result}; use std::io::Write; +use crate::{utils::read_le_at, ParseFailure, ParseStage, Result}; + /// ECMA-335 compliant stream header providing metadata stream location and identification. /// /// A stream header describes a single metadata stream within a .NET assembly's metadata directory. diff --git a/dotscope/src/metadata/streams/tablesheader.rs b/dotscope/src/metadata/streams/tablesheader.rs index 114df7b8..69467098 100644 --- a/dotscope/src/metadata/streams/tablesheader.rs +++ b/dotscope/src/metadata/streams/tablesheader.rs @@ -294,6 +294,7 @@ //! - **ECMA-335 II.25**: File format and metadata integration within PE files use std::{io::Write, sync::Arc}; + use strum::IntoEnumIterator; use crate::{ diff --git a/dotscope/src/metadata/streams/userstrings.rs b/dotscope/src/metadata/streams/userstrings.rs index a03acb15..30ac050c 100644 --- a/dotscope/src/metadata/streams/userstrings.rs +++ b/dotscope/src/metadata/streams/userstrings.rs @@ -40,13 +40,13 @@ //! # Reference //! - [ECMA-335 II.24.2.4](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) +use widestring::U16Str; + use crate::{ utils::{read_compressed_int, read_compressed_int_at}, Error, HeapKind, ParseFailure, Result, }; -use widestring::U16Str; - /// The `UserStrings` object provides helper methods to access the data within the '#US' heap. /// /// This heap contains all user-defined string literals from the source code, stored in UTF-16 encoding diff --git a/dotscope/src/metadata/tables/assembly/builder.rs b/dotscope/src/metadata/tables/assembly/builder.rs index 907003ac..fa34971f 100644 --- a/dotscope/src/metadata/tables/assembly/builder.rs +++ b/dotscope/src/metadata/tables/assembly/builder.rs @@ -239,12 +239,13 @@ impl Default for AssemblyBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{ChangeRefKind, CilAssembly}, metadata::cilassemblyview::CilAssemblyView, }; - use std::path::PathBuf; #[test] fn test_assembly_builder_basic() { diff --git a/dotscope/src/metadata/tables/assembly/mod.rs b/dotscope/src/metadata/tables/assembly/mod.rs index bf72a746..5b68f6ca 100644 --- a/dotscope/src/metadata/tables/assembly/mod.rs +++ b/dotscope/src/metadata/tables/assembly/mod.rs @@ -23,9 +23,9 @@ //! //! # Reference //! - [ECMA-335 II.22.2](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - Assembly table specification +use std::{fmt, sync::Arc}; + use crossbeam_skiplist::SkipMap; -use std::fmt; -use std::sync::Arc; use crate::metadata::token::Token; diff --git a/dotscope/src/metadata/tables/assemblyos/mod.rs b/dotscope/src/metadata/tables/assemblyos/mod.rs index 390c0b06..11f79d90 100644 --- a/dotscope/src/metadata/tables/assemblyos/mod.rs +++ b/dotscope/src/metadata/tables/assemblyos/mod.rs @@ -27,9 +27,10 @@ //! # Reference //! - [ECMA-335 II.22.3](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - `AssemblyOS` table specification -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + use crate::metadata::token::Token; mod builder; diff --git a/dotscope/src/metadata/tables/assemblyos/writer.rs b/dotscope/src/metadata/tables/assemblyos/writer.rs index 6304f069..76cf7768 100644 --- a/dotscope/src/metadata/tables/assemblyos/writer.rs +++ b/dotscope/src/metadata/tables/assemblyos/writer.rs @@ -76,9 +76,9 @@ impl RowWritable for AssemblyOsRaw { #[cfg(test)] mod tests { use super::*; - use crate::{ - metadata::tables::types::{RowReadable, TableInfo, TableRow}, - metadata::token::Token, + use crate::metadata::{ + tables::types::{RowReadable, TableInfo, TableRow}, + token::Token, }; #[test] diff --git a/dotscope/src/metadata/tables/assemblyprocessor/mod.rs b/dotscope/src/metadata/tables/assemblyprocessor/mod.rs index 540ee084..565b8fe6 100644 --- a/dotscope/src/metadata/tables/assemblyprocessor/mod.rs +++ b/dotscope/src/metadata/tables/assemblyprocessor/mod.rs @@ -47,9 +47,10 @@ //! # References //! //! - [ECMA-335 II.22.4](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - `AssemblyProcessor` table specification -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + use crate::metadata::token::Token; mod builder; diff --git a/dotscope/src/metadata/tables/assemblyprocessor/reader.rs b/dotscope/src/metadata/tables/assemblyprocessor/reader.rs index 0dd82aae..0caa314e 100644 --- a/dotscope/src/metadata/tables/assemblyprocessor/reader.rs +++ b/dotscope/src/metadata/tables/assemblyprocessor/reader.rs @@ -78,9 +78,8 @@ impl RowReadable for AssemblyProcessorRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; - use super::*; + use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; #[test] fn crafted_short() { diff --git a/dotscope/src/metadata/tables/assemblyprocessor/writer.rs b/dotscope/src/metadata/tables/assemblyprocessor/writer.rs index 756394e0..ee3f694c 100644 --- a/dotscope/src/metadata/tables/assemblyprocessor/writer.rs +++ b/dotscope/src/metadata/tables/assemblyprocessor/writer.rs @@ -70,9 +70,9 @@ impl RowWritable for AssemblyProcessorRaw { #[cfg(test)] mod tests { use super::*; - use crate::{ - metadata::tables::types::{RowReadable, TableInfo, TableRow}, - metadata::token::Token, + use crate::metadata::{ + tables::types::{RowReadable, TableInfo, TableRow}, + token::Token, }; #[test] diff --git a/dotscope/src/metadata/tables/assemblyref/assemblyrefhash.rs b/dotscope/src/metadata/tables/assemblyref/assemblyrefhash.rs index 3c696136..087c5ec8 100644 --- a/dotscope/src/metadata/tables/assemblyref/assemblyrefhash.rs +++ b/dotscope/src/metadata/tables/assemblyref/assemblyrefhash.rs @@ -61,13 +61,14 @@ //! - [RFC 1321](https://tools.ietf.org/html/rfc1321) - MD5 Message-Digest Algorithm (deprecated) //! - [RFC 3174](https://tools.ietf.org/html/rfc3174) - SHA-1 Hash Function (deprecated) +use std::fmt::Write; + #[cfg(feature = "legacy-crypto")] use crate::utils::{compute_md5, compute_sha1}; use crate::{ utils::{compute_sha256, compute_sha384, compute_sha512}, Result, }; -use std::fmt::Write; /// Convert bytes to lowercase hexadecimal string representation /// diff --git a/dotscope/src/metadata/tables/assemblyref/mod.rs b/dotscope/src/metadata/tables/assemblyref/mod.rs index 456c34b1..b2c9ac6a 100644 --- a/dotscope/src/metadata/tables/assemblyref/mod.rs +++ b/dotscope/src/metadata/tables/assemblyref/mod.rs @@ -48,9 +48,10 @@ //! # References //! //! - [ECMA-335 II.22.5](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - `AssemblyRef` table specification -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + use crate::metadata::{ imports::{ImportContainer, ImportRc, Imports}, token::Token, diff --git a/dotscope/src/metadata/tables/assemblyrefos/mod.rs b/dotscope/src/metadata/tables/assemblyrefos/mod.rs index 52352411..441d3e37 100644 --- a/dotscope/src/metadata/tables/assemblyrefos/mod.rs +++ b/dotscope/src/metadata/tables/assemblyrefos/mod.rs @@ -46,9 +46,10 @@ //! //! - [ECMA-335 II.22.7](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - `AssemblyRefOS` table specification -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + use crate::metadata::token::Token; mod builder; diff --git a/dotscope/src/metadata/tables/assemblyrefos/writer.rs b/dotscope/src/metadata/tables/assemblyrefos/writer.rs index f0d910d4..899c0d11 100644 --- a/dotscope/src/metadata/tables/assemblyrefos/writer.rs +++ b/dotscope/src/metadata/tables/assemblyrefos/writer.rs @@ -87,9 +87,9 @@ impl RowWritable for AssemblyRefOsRaw { #[cfg(test)] mod tests { use super::*; - use crate::{ - metadata::tables::types::{RowReadable, TableInfo, TableRow}, - metadata::token::Token, + use crate::metadata::{ + tables::types::{RowReadable, TableInfo, TableRow}, + token::Token, }; #[test] diff --git a/dotscope/src/metadata/tables/assemblyrefprocessor/mod.rs b/dotscope/src/metadata/tables/assemblyrefprocessor/mod.rs index f5567c4b..67c59f09 100644 --- a/dotscope/src/metadata/tables/assemblyrefprocessor/mod.rs +++ b/dotscope/src/metadata/tables/assemblyrefprocessor/mod.rs @@ -44,9 +44,10 @@ //! # References //! //! - [ECMA-335 II.22.8](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - `AssemblyRefProcessor` table specification -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + use crate::metadata::token::Token; mod builder; diff --git a/dotscope/src/metadata/tables/assemblyrefprocessor/writer.rs b/dotscope/src/metadata/tables/assemblyrefprocessor/writer.rs index d3ea8e34..252a000d 100644 --- a/dotscope/src/metadata/tables/assemblyrefprocessor/writer.rs +++ b/dotscope/src/metadata/tables/assemblyrefprocessor/writer.rs @@ -81,10 +81,12 @@ impl RowWritable for AssemblyRefProcessorRaw { #[cfg(test)] mod tests { use super::*; - use crate::{ - metadata::tables::types::{RowReadable, TableInfo}, - metadata::tables::TableRow, - metadata::token::Token, + use crate::metadata::{ + tables::{ + types::{RowReadable, TableInfo}, + TableRow, + }, + token::Token, }; #[test] diff --git a/dotscope/src/metadata/tables/classlayout/builder.rs b/dotscope/src/metadata/tables/classlayout/builder.rs index d2f137dc..bfb0ed2f 100644 --- a/dotscope/src/metadata/tables/classlayout/builder.rs +++ b/dotscope/src/metadata/tables/classlayout/builder.rs @@ -314,12 +314,13 @@ impl ClassLayoutBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{ChangeRefKind, CilAssembly}, metadata::cilassemblyview::CilAssemblyView, }; - use std::path::PathBuf; #[test] fn test_class_layout_builder_basic() { diff --git a/dotscope/src/metadata/tables/classlayout/mod.rs b/dotscope/src/metadata/tables/classlayout/mod.rs index d3a5f94a..74aee281 100644 --- a/dotscope/src/metadata/tables/classlayout/mod.rs +++ b/dotscope/src/metadata/tables/classlayout/mod.rs @@ -55,9 +55,10 @@ //! //! - [ECMA-335 II.22.8](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - `ClassLayout` table specification -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + use crate::metadata::token::Token; mod builder; diff --git a/dotscope/src/metadata/tables/classlayout/reader.rs b/dotscope/src/metadata/tables/classlayout/reader.rs index 0c40e439..e7f4f493 100644 --- a/dotscope/src/metadata/tables/classlayout/reader.rs +++ b/dotscope/src/metadata/tables/classlayout/reader.rs @@ -32,9 +32,8 @@ impl RowReadable for ClassLayoutRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; - use super::*; + use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; #[test] fn crafted_short() { diff --git a/dotscope/src/metadata/tables/classlayout/writer.rs b/dotscope/src/metadata/tables/classlayout/writer.rs index c22a9e6d..080747af 100644 --- a/dotscope/src/metadata/tables/classlayout/writer.rs +++ b/dotscope/src/metadata/tables/classlayout/writer.rs @@ -69,11 +69,13 @@ impl RowWritable for ClassLayoutRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{ - classlayout::ClassLayoutRaw, - types::{RowReadable, RowWritable, TableId, TableInfo, TableRow}, + use crate::metadata::{ + tables::{ + classlayout::ClassLayoutRaw, + types::{RowReadable, RowWritable, TableId, TableInfo, TableRow}, + }, + token::Token, }; - use crate::metadata::token::Token; #[test] fn test_classlayout_row_size() { diff --git a/dotscope/src/metadata/tables/constant/builder.rs b/dotscope/src/metadata/tables/constant/builder.rs index ef1ff41f..e62b0188 100644 --- a/dotscope/src/metadata/tables/constant/builder.rs +++ b/dotscope/src/metadata/tables/constant/builder.rs @@ -403,12 +403,13 @@ impl ConstantBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{ChangeRefKind, CilAssembly}, metadata::cilassemblyview::CilAssemblyView, }; - use std::path::PathBuf; #[test] fn test_constant_builder_basic_integer() { diff --git a/dotscope/src/metadata/tables/constant/mod.rs b/dotscope/src/metadata/tables/constant/mod.rs index e952ce5e..5a087803 100644 --- a/dotscope/src/metadata/tables/constant/mod.rs +++ b/dotscope/src/metadata/tables/constant/mod.rs @@ -50,9 +50,10 @@ //! //! - [ECMA-335 II.22.9](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - Constant table specification -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + use crate::metadata::token::Token; mod builder; diff --git a/dotscope/src/metadata/tables/constant/reader.rs b/dotscope/src/metadata/tables/constant/reader.rs index 7511c883..18bb4cd7 100644 --- a/dotscope/src/metadata/tables/constant/reader.rs +++ b/dotscope/src/metadata/tables/constant/reader.rs @@ -31,9 +31,8 @@ impl RowReadable for ConstantRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; - use super::*; + use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; #[test] fn crafted_short() { diff --git a/dotscope/src/metadata/tables/constant/writer.rs b/dotscope/src/metadata/tables/constant/writer.rs index 96ca8a7d..0616513c 100644 --- a/dotscope/src/metadata/tables/constant/writer.rs +++ b/dotscope/src/metadata/tables/constant/writer.rs @@ -84,13 +84,15 @@ impl RowWritable for ConstantRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{ - constant::ConstantRaw, - types::{ - CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + use crate::metadata::{ + tables::{ + constant::ConstantRaw, + types::{ + CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + }, }, + token::Token, }; - use crate::metadata::token::Token; #[test] fn test_constant_row_size() { diff --git a/dotscope/src/metadata/tables/customattribute/builder.rs b/dotscope/src/metadata/tables/customattribute/builder.rs index 7a66d9e7..60117189 100644 --- a/dotscope/src/metadata/tables/customattribute/builder.rs +++ b/dotscope/src/metadata/tables/customattribute/builder.rs @@ -261,12 +261,13 @@ impl CustomAttributeBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{ChangeRefKind, CilAssembly}, metadata::cilassemblyview::CilAssemblyView, }; - use std::path::PathBuf; #[test] fn test_custom_attribute_builder_basic() { diff --git a/dotscope/src/metadata/tables/customattribute/mod.rs b/dotscope/src/metadata/tables/customattribute/mod.rs index a5fbc70e..ffe3b30f 100644 --- a/dotscope/src/metadata/tables/customattribute/mod.rs +++ b/dotscope/src/metadata/tables/customattribute/mod.rs @@ -69,9 +69,10 @@ //! //! - [ECMA-335 II.22.10](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - `CustomAttribute` table specification //! - [ECMA-335 II.23.3](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - Custom attribute encoding -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + use crate::metadata::token::Token; mod builder; diff --git a/dotscope/src/metadata/tables/customattribute/writer.rs b/dotscope/src/metadata/tables/customattribute/writer.rs index 41867969..e4b1af58 100644 --- a/dotscope/src/metadata/tables/customattribute/writer.rs +++ b/dotscope/src/metadata/tables/customattribute/writer.rs @@ -86,13 +86,15 @@ impl RowWritable for CustomAttributeRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{ - customattribute::CustomAttributeRaw, - types::{ - CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + use crate::metadata::{ + tables::{ + customattribute::CustomAttributeRaw, + types::{ + CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + }, }, + token::Token, }; - use crate::metadata::token::Token; #[test] fn test_customattribute_row_size() { diff --git a/dotscope/src/metadata/tables/customdebuginformation/mod.rs b/dotscope/src/metadata/tables/customdebuginformation/mod.rs index 9e1324a3..7b982331 100644 --- a/dotscope/src/metadata/tables/customdebuginformation/mod.rs +++ b/dotscope/src/metadata/tables/customdebuginformation/mod.rs @@ -94,14 +94,15 @@ mod raw; mod reader; mod writer; +use std::sync::Arc; + pub use builder::*; +use crossbeam_skiplist::SkipMap; pub(crate) use loader::*; pub use owned::*; pub use raw::*; use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; -use std::sync::Arc; /// Thread-safe map that holds the mapping of [`crate::metadata::token::Token`] to parsed [`crate::metadata::tables::customdebuginformation::CustomDebugInformation`] instances /// diff --git a/dotscope/src/metadata/tables/customdebuginformation/owned.rs b/dotscope/src/metadata/tables/customdebuginformation/owned.rs index fe75cf37..cf9acc5a 100644 --- a/dotscope/src/metadata/tables/customdebuginformation/owned.rs +++ b/dotscope/src/metadata/tables/customdebuginformation/owned.rs @@ -15,10 +15,11 @@ //! All types in this module are [`Send`] and [`Clone`], enabling safe sharing //! across threads and efficient copying when needed. +use uguid::Guid; + use crate::metadata::{ customdebuginformation::CustomDebugInfo, token::Token, typesystem::CilTypeReference, }; -use uguid::Guid; /// Owned representation of a `CustomDebugInformation` table entry /// diff --git a/dotscope/src/metadata/tables/customdebuginformation/raw.rs b/dotscope/src/metadata/tables/customdebuginformation/raw.rs index 061b5b0e..b88a06a9 100644 --- a/dotscope/src/metadata/tables/customdebuginformation/raw.rs +++ b/dotscope/src/metadata/tables/customdebuginformation/raw.rs @@ -15,6 +15,8 @@ //! All types in this module are [`Send`] and [`Clone`], enabling safe sharing //! across threads and efficient copying when needed. +use std::sync::Arc; + use crate::{ metadata::{ customdebuginformation::{parse_custom_debug_blob, CustomDebugKind}, @@ -28,7 +30,6 @@ use crate::{ }, Result, }; -use std::sync::Arc; /// Raw binary representation of a `CustomDebugInformation` table entry /// diff --git a/dotscope/src/metadata/tables/customdebuginformation/writer.rs b/dotscope/src/metadata/tables/customdebuginformation/writer.rs index 9da4c8dc..e98b91be 100644 --- a/dotscope/src/metadata/tables/customdebuginformation/writer.rs +++ b/dotscope/src/metadata/tables/customdebuginformation/writer.rs @@ -99,9 +99,12 @@ impl RowWritable for CustomDebugInformationRaw { #[cfg(test)] mod tests { use super::*; - use crate::{ - metadata::tables::types::{CodedIndex, CodedIndexType, RowReadable, TableInfo, TableRow}, - metadata::{tables::TableId, token::Token}, + use crate::metadata::{ + tables::{ + types::{CodedIndex, CodedIndexType, RowReadable, TableInfo, TableRow}, + TableId, + }, + token::Token, }; #[test] diff --git a/dotscope/src/metadata/tables/declsecurity/builder.rs b/dotscope/src/metadata/tables/declsecurity/builder.rs index 6cc32988..8504e25a 100644 --- a/dotscope/src/metadata/tables/declsecurity/builder.rs +++ b/dotscope/src/metadata/tables/declsecurity/builder.rs @@ -345,12 +345,13 @@ impl DeclSecurityBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{ChangeRefKind, CilAssembly}, metadata::{cilassemblyview::CilAssemblyView, security::SecurityAction}, }; - use std::path::PathBuf; #[test] fn test_decl_security_builder_basic() { diff --git a/dotscope/src/metadata/tables/declsecurity/mod.rs b/dotscope/src/metadata/tables/declsecurity/mod.rs index b9345436..ee4c5c83 100644 --- a/dotscope/src/metadata/tables/declsecurity/mod.rs +++ b/dotscope/src/metadata/tables/declsecurity/mod.rs @@ -87,10 +87,12 @@ //! - [ECMA-335 II.22.11](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - `DeclSecurity` table specification //! - [ECMA-335 II.23.1.16](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - `SecurityAction` enumeration -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/declsecurity/reader.rs b/dotscope/src/metadata/tables/declsecurity/reader.rs index c7369823..e6d33634 100644 --- a/dotscope/src/metadata/tables/declsecurity/reader.rs +++ b/dotscope/src/metadata/tables/declsecurity/reader.rs @@ -47,9 +47,8 @@ impl RowReadable for DeclSecurityRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; - use super::*; + use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; #[test] fn crafted_short() { diff --git a/dotscope/src/metadata/tables/declsecurity/writer.rs b/dotscope/src/metadata/tables/declsecurity/writer.rs index a11e602d..22bc7fb1 100644 --- a/dotscope/src/metadata/tables/declsecurity/writer.rs +++ b/dotscope/src/metadata/tables/declsecurity/writer.rs @@ -88,13 +88,15 @@ impl RowWritable for DeclSecurityRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{ - declsecurity::DeclSecurityRaw, - types::{ - CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + use crate::metadata::{ + tables::{ + declsecurity::DeclSecurityRaw, + types::{ + CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + }, }, + token::Token, }; - use crate::metadata::token::Token; #[test] fn test_declsecurity_row_size() { diff --git a/dotscope/src/metadata/tables/document/mod.rs b/dotscope/src/metadata/tables/document/mod.rs index 081dedfb..32c61beb 100644 --- a/dotscope/src/metadata/tables/document/mod.rs +++ b/dotscope/src/metadata/tables/document/mod.rs @@ -78,9 +78,10 @@ //! //! - [Portable PDB Format - Document Table](https://github.com/dotnet/core/blob/main/Documentation/diagnostics/portable_pdb.md#document-table-0x30) -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + use crate::metadata::token::Token; mod builder; diff --git a/dotscope/src/metadata/tables/document/writer.rs b/dotscope/src/metadata/tables/document/writer.rs index 15848781..326914eb 100644 --- a/dotscope/src/metadata/tables/document/writer.rs +++ b/dotscope/src/metadata/tables/document/writer.rs @@ -79,9 +79,9 @@ impl RowWritable for DocumentRaw { #[cfg(test)] mod tests { use super::*; - use crate::{ - metadata::tables::types::{RowReadable, TableInfo, TableRow}, - metadata::token::Token, + use crate::metadata::{ + tables::types::{RowReadable, TableInfo, TableRow}, + token::Token, }; #[test] diff --git a/dotscope/src/metadata/tables/enclog/mod.rs b/dotscope/src/metadata/tables/enclog/mod.rs index 3198520e..4b9854e6 100644 --- a/dotscope/src/metadata/tables/enclog/mod.rs +++ b/dotscope/src/metadata/tables/enclog/mod.rs @@ -68,9 +68,10 @@ //! //! - [ECMA-335 II.22.12](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - EncLog table specification -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + use crate::metadata::token::Token; mod builder; diff --git a/dotscope/src/metadata/tables/enclog/writer.rs b/dotscope/src/metadata/tables/enclog/writer.rs index 4034655a..1933b971 100644 --- a/dotscope/src/metadata/tables/enclog/writer.rs +++ b/dotscope/src/metadata/tables/enclog/writer.rs @@ -84,9 +84,9 @@ impl RowWritable for EncLogRaw { #[cfg(test)] mod tests { use super::*; - use crate::{ - metadata::tables::types::{RowReadable, TableInfo, TableRow}, - metadata::token::Token, + use crate::metadata::{ + tables::types::{RowReadable, TableInfo, TableRow}, + token::Token, }; #[test] diff --git a/dotscope/src/metadata/tables/encmap/mod.rs b/dotscope/src/metadata/tables/encmap/mod.rs index 53807a13..ccc7f4da 100644 --- a/dotscope/src/metadata/tables/encmap/mod.rs +++ b/dotscope/src/metadata/tables/encmap/mod.rs @@ -70,9 +70,10 @@ //! //! - [ECMA-335 II.22.13](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - EncMap table specification -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + use crate::metadata::token::Token; mod builder; diff --git a/dotscope/src/metadata/tables/encmap/writer.rs b/dotscope/src/metadata/tables/encmap/writer.rs index 96e29bea..fa1228b4 100644 --- a/dotscope/src/metadata/tables/encmap/writer.rs +++ b/dotscope/src/metadata/tables/encmap/writer.rs @@ -78,9 +78,9 @@ impl RowWritable for EncMapRaw { #[cfg(test)] mod tests { use super::*; - use crate::{ - metadata::tables::types::{RowReadable, TableInfo, TableRow}, - metadata::token::Token, + use crate::metadata::{ + tables::types::{RowReadable, TableInfo, TableRow}, + token::Token, }; #[test] diff --git a/dotscope/src/metadata/tables/event/builder.rs b/dotscope/src/metadata/tables/event/builder.rs index 364c84f5..20377326 100644 --- a/dotscope/src/metadata/tables/event/builder.rs +++ b/dotscope/src/metadata/tables/event/builder.rs @@ -222,12 +222,13 @@ impl EventBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{ChangeRefKind, CilAssembly}, metadata::{cilassemblyview::CilAssemblyView, tables::EventAttributes}, }; - use std::path::PathBuf; #[test] fn test_event_builder_basic() { diff --git a/dotscope/src/metadata/tables/event/mod.rs b/dotscope/src/metadata/tables/event/mod.rs index bb2ad4be..d5e7b812 100644 --- a/dotscope/src/metadata/tables/event/mod.rs +++ b/dotscope/src/metadata/tables/event/mod.rs @@ -76,10 +76,12 @@ //! //! - [ECMA-335 II.22.13](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - Event table specification -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/event/reader.rs b/dotscope/src/metadata/tables/event/reader.rs index c693208b..c40ca346 100644 --- a/dotscope/src/metadata/tables/event/reader.rs +++ b/dotscope/src/metadata/tables/event/reader.rs @@ -32,9 +32,8 @@ impl RowReadable for EventRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; - use super::*; + use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; #[test] fn crafted_short() { diff --git a/dotscope/src/metadata/tables/event/writer.rs b/dotscope/src/metadata/tables/event/writer.rs index e4a4559c..2c2a1ec6 100644 --- a/dotscope/src/metadata/tables/event/writer.rs +++ b/dotscope/src/metadata/tables/event/writer.rs @@ -85,15 +85,16 @@ impl RowWritable for EventRaw { #[cfg(test)] mod tests { + use std::sync::Arc; + use super::*; - use crate::{ - metadata::tables::{ + use crate::metadata::{ + tables::{ types::{RowReadable, TableInfo, TableRow}, CodedIndex, TableId, }, - metadata::token::Token, + token::Token, }; - use std::sync::Arc; #[test] fn test_row_size() { diff --git a/dotscope/src/metadata/tables/eventmap/mod.rs b/dotscope/src/metadata/tables/eventmap/mod.rs index 5643f75c..252c80b1 100644 --- a/dotscope/src/metadata/tables/eventmap/mod.rs +++ b/dotscope/src/metadata/tables/eventmap/mod.rs @@ -76,10 +76,12 @@ //! //! - [ECMA-335 II.22.12](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - EventMap table specification -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/eventmap/reader.rs b/dotscope/src/metadata/tables/eventmap/reader.rs index c8553dfc..8e6d2528 100644 --- a/dotscope/src/metadata/tables/eventmap/reader.rs +++ b/dotscope/src/metadata/tables/eventmap/reader.rs @@ -53,9 +53,8 @@ impl RowReadable for EventMapRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; - use super::*; + use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; #[test] fn crafted_short() { diff --git a/dotscope/src/metadata/tables/eventmap/writer.rs b/dotscope/src/metadata/tables/eventmap/writer.rs index eb4a2622..0f349b97 100644 --- a/dotscope/src/metadata/tables/eventmap/writer.rs +++ b/dotscope/src/metadata/tables/eventmap/writer.rs @@ -69,11 +69,13 @@ impl RowWritable for EventMapRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{ - eventmap::EventMapRaw, - types::{RowReadable, RowWritable, TableId, TableInfo, TableRow}, + use crate::metadata::{ + tables::{ + eventmap::EventMapRaw, + types::{RowReadable, RowWritable, TableId, TableInfo, TableRow}, + }, + token::Token, }; - use crate::metadata::token::Token; #[test] fn test_eventmap_row_size() { diff --git a/dotscope/src/metadata/tables/eventptr/mod.rs b/dotscope/src/metadata/tables/eventptr/mod.rs index 22af57de..4a415b81 100644 --- a/dotscope/src/metadata/tables/eventptr/mod.rs +++ b/dotscope/src/metadata/tables/eventptr/mod.rs @@ -38,10 +38,12 @@ //! # Reference //! - [ECMA-335 II.22.14](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - `EventPtr` table specification -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/eventptr/writer.rs b/dotscope/src/metadata/tables/eventptr/writer.rs index 20d8ce33..162b5aef 100644 --- a/dotscope/src/metadata/tables/eventptr/writer.rs +++ b/dotscope/src/metadata/tables/eventptr/writer.rs @@ -70,9 +70,9 @@ impl RowWritable for EventPtrRaw { #[cfg(test)] mod tests { use super::*; - use crate::{ - metadata::tables::types::{RowReadable, TableInfo, TableRow}, - metadata::token::Token, + use crate::metadata::{ + tables::types::{RowReadable, TableInfo, TableRow}, + token::Token, }; #[test] diff --git a/dotscope/src/metadata/tables/exportedtype/mod.rs b/dotscope/src/metadata/tables/exportedtype/mod.rs index 817803d3..9b3e9ed4 100644 --- a/dotscope/src/metadata/tables/exportedtype/mod.rs +++ b/dotscope/src/metadata/tables/exportedtype/mod.rs @@ -46,10 +46,12 @@ //! # Reference //! - [ECMA-335 II.22.14](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - `ExportedType` table specification -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/exportedtype/writer.rs b/dotscope/src/metadata/tables/exportedtype/writer.rs index 7b3d5df2..6557f701 100644 --- a/dotscope/src/metadata/tables/exportedtype/writer.rs +++ b/dotscope/src/metadata/tables/exportedtype/writer.rs @@ -109,8 +109,10 @@ impl RowWritable for ExportedTypeRaw { mod tests { use super::*; use crate::metadata::{ - tables::types::{RowReadable, TableId, TableInfo, TableRow}, - tables::CodedIndex, + tables::{ + types::{RowReadable, TableId, TableInfo, TableRow}, + CodedIndex, + }, token::Token, }; diff --git a/dotscope/src/metadata/tables/field/builder.rs b/dotscope/src/metadata/tables/field/builder.rs index 3243fa98..4893d65f 100644 --- a/dotscope/src/metadata/tables/field/builder.rs +++ b/dotscope/src/metadata/tables/field/builder.rs @@ -189,12 +189,13 @@ impl FieldBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{ChangeRefKind, CilAssembly}, metadata::cilassemblyview::CilAssemblyView, }; - use std::path::PathBuf; #[test] fn test_field_builder_basic() { diff --git a/dotscope/src/metadata/tables/field/mod.rs b/dotscope/src/metadata/tables/field/mod.rs index 652745d8..d2e93de0 100644 --- a/dotscope/src/metadata/tables/field/mod.rs +++ b/dotscope/src/metadata/tables/field/mod.rs @@ -48,10 +48,12 @@ //! - [ECMA-335 II.22.15](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - `Field` table specification //! - [ECMA-335 II.23.1.5](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - `FieldAttributes` specification -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/field/writer.rs b/dotscope/src/metadata/tables/field/writer.rs index 6f61d9a5..cf16a408 100644 --- a/dotscope/src/metadata/tables/field/writer.rs +++ b/dotscope/src/metadata/tables/field/writer.rs @@ -78,12 +78,13 @@ impl RowWritable for FieldRaw { #[cfg(test)] mod tests { + use std::sync::Arc; + use super::*; - use crate::{ - metadata::tables::types::{RowReadable, TableInfo, TableRow}, - metadata::token::Token, + use crate::metadata::{ + tables::types::{RowReadable, TableInfo, TableRow}, + token::Token, }; - use std::sync::Arc; #[test] fn test_row_size() { diff --git a/dotscope/src/metadata/tables/fieldlayout/builder.rs b/dotscope/src/metadata/tables/fieldlayout/builder.rs index 5c554734..efeb553e 100644 --- a/dotscope/src/metadata/tables/fieldlayout/builder.rs +++ b/dotscope/src/metadata/tables/fieldlayout/builder.rs @@ -267,12 +267,13 @@ impl FieldLayoutBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{ChangeRefKind, CilAssembly}, metadata::cilassemblyview::CilAssemblyView, }; - use std::path::PathBuf; #[test] fn test_field_layout_builder_basic() { diff --git a/dotscope/src/metadata/tables/fieldlayout/mod.rs b/dotscope/src/metadata/tables/fieldlayout/mod.rs index 61e69dc0..0b6f8607 100644 --- a/dotscope/src/metadata/tables/fieldlayout/mod.rs +++ b/dotscope/src/metadata/tables/fieldlayout/mod.rs @@ -32,10 +32,12 @@ //! # ECMA-335 Reference //! See ECMA-335, Partition II, §22.16 for the complete `FieldLayout` table specification. -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/fieldlayout/reader.rs b/dotscope/src/metadata/tables/fieldlayout/reader.rs index d4bcf657..8ed11272 100644 --- a/dotscope/src/metadata/tables/fieldlayout/reader.rs +++ b/dotscope/src/metadata/tables/fieldlayout/reader.rs @@ -45,9 +45,8 @@ impl RowReadable for FieldLayoutRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; - use super::*; + use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; #[test] fn crafted_short() { diff --git a/dotscope/src/metadata/tables/fieldlayout/writer.rs b/dotscope/src/metadata/tables/fieldlayout/writer.rs index 4f027980..eb736055 100644 --- a/dotscope/src/metadata/tables/fieldlayout/writer.rs +++ b/dotscope/src/metadata/tables/fieldlayout/writer.rs @@ -65,11 +65,13 @@ impl RowWritable for FieldLayoutRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{ - fieldlayout::FieldLayoutRaw, - types::{RowReadable, RowWritable, TableId, TableInfo, TableRow}, + use crate::metadata::{ + tables::{ + fieldlayout::FieldLayoutRaw, + types::{RowReadable, RowWritable, TableId, TableInfo, TableRow}, + }, + token::Token, }; - use crate::metadata::token::Token; #[test] fn test_fieldlayout_row_size() { diff --git a/dotscope/src/metadata/tables/fieldmarshal/builder.rs b/dotscope/src/metadata/tables/fieldmarshal/builder.rs index bf506692..d677c59f 100644 --- a/dotscope/src/metadata/tables/fieldmarshal/builder.rs +++ b/dotscope/src/metadata/tables/fieldmarshal/builder.rs @@ -597,12 +597,13 @@ impl FieldMarshalBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{ChangeRefKind, CilAssembly}, metadata::cilassemblyview::CilAssemblyView, }; - use std::path::PathBuf; #[test] fn test_field_marshal_builder_basic() { diff --git a/dotscope/src/metadata/tables/fieldmarshal/mod.rs b/dotscope/src/metadata/tables/fieldmarshal/mod.rs index 51094b5a..73206783 100644 --- a/dotscope/src/metadata/tables/fieldmarshal/mod.rs +++ b/dotscope/src/metadata/tables/fieldmarshal/mod.rs @@ -45,10 +45,12 @@ //! # ECMA-335 Reference //! See ECMA-335, Partition II, §22.17 for the complete `FieldMarshal` table specification. -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/fieldmarshal/reader.rs b/dotscope/src/metadata/tables/fieldmarshal/reader.rs index 5e880294..d027b99d 100644 --- a/dotscope/src/metadata/tables/fieldmarshal/reader.rs +++ b/dotscope/src/metadata/tables/fieldmarshal/reader.rs @@ -44,9 +44,8 @@ impl RowReadable for FieldMarshalRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; - use super::*; + use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; #[test] fn crafted_short() { diff --git a/dotscope/src/metadata/tables/fieldmarshal/writer.rs b/dotscope/src/metadata/tables/fieldmarshal/writer.rs index c467016a..ff50153f 100644 --- a/dotscope/src/metadata/tables/fieldmarshal/writer.rs +++ b/dotscope/src/metadata/tables/fieldmarshal/writer.rs @@ -74,13 +74,15 @@ impl RowWritable for FieldMarshalRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{ - fieldmarshal::FieldMarshalRaw, - types::{ - CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + use crate::metadata::{ + tables::{ + fieldmarshal::FieldMarshalRaw, + types::{ + CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + }, }, + token::Token, }; - use crate::metadata::token::Token; #[test] fn test_fieldmarshal_row_size() { diff --git a/dotscope/src/metadata/tables/fieldptr/mod.rs b/dotscope/src/metadata/tables/fieldptr/mod.rs index 4f3d9d9f..cdceff80 100644 --- a/dotscope/src/metadata/tables/fieldptr/mod.rs +++ b/dotscope/src/metadata/tables/fieldptr/mod.rs @@ -42,10 +42,12 @@ //! # ECMA-335 Reference //! See ECMA-335, Partition II, §22.18 for the complete `FieldPtr` table specification. -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/fieldrva/mod.rs b/dotscope/src/metadata/tables/fieldrva/mod.rs index ce0a47c9..91dc1e57 100644 --- a/dotscope/src/metadata/tables/fieldrva/mod.rs +++ b/dotscope/src/metadata/tables/fieldrva/mod.rs @@ -49,10 +49,12 @@ //! # ECMA-335 Reference //! See ECMA-335, Partition II, §22.19 for the complete `FieldRva` table specification. -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/fieldrva/writer.rs b/dotscope/src/metadata/tables/fieldrva/writer.rs index baa6da42..e22c248a 100644 --- a/dotscope/src/metadata/tables/fieldrva/writer.rs +++ b/dotscope/src/metadata/tables/fieldrva/writer.rs @@ -65,11 +65,13 @@ impl RowWritable for FieldRvaRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{ - fieldrva::FieldRvaRaw, - types::{RowReadable, RowWritable, TableId, TableInfo, TableRow}, + use crate::metadata::{ + tables::{ + fieldrva::FieldRvaRaw, + types::{RowReadable, RowWritable, TableId, TableInfo, TableRow}, + }, + token::Token, }; - use crate::metadata::token::Token; #[test] fn test_fieldrva_row_size() { diff --git a/dotscope/src/metadata/tables/file/mod.rs b/dotscope/src/metadata/tables/file/mod.rs index 1ea19943..0e45991f 100644 --- a/dotscope/src/metadata/tables/file/mod.rs +++ b/dotscope/src/metadata/tables/file/mod.rs @@ -62,12 +62,14 @@ //! # ECMA-335 Reference //! See ECMA-335, Partition II, §22.19 for the complete File table specification. +use std::sync::Arc; + +use crossbeam_skiplist::SkipMap; + use crate::metadata::{ imports::{ImportContainer, ImportRc, Imports}, token::Token, }; -use crossbeam_skiplist::SkipMap; -use std::sync::Arc; mod builder; mod loader; diff --git a/dotscope/src/metadata/tables/file/writer.rs b/dotscope/src/metadata/tables/file/writer.rs index 1fc85ead..07d933ac 100644 --- a/dotscope/src/metadata/tables/file/writer.rs +++ b/dotscope/src/metadata/tables/file/writer.rs @@ -77,11 +77,13 @@ impl RowWritable for FileRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{ - file::FileRaw, - types::{RowReadable, RowWritable, TableInfo, TableRow}, + use crate::metadata::{ + tables::{ + file::FileRaw, + types::{RowReadable, RowWritable, TableInfo, TableRow}, + }, + token::Token, }; - use crate::metadata::token::Token; #[test] fn test_file_row_size() { diff --git a/dotscope/src/metadata/tables/genericparam/builder.rs b/dotscope/src/metadata/tables/genericparam/builder.rs index 5ce4c01d..68bcb984 100644 --- a/dotscope/src/metadata/tables/genericparam/builder.rs +++ b/dotscope/src/metadata/tables/genericparam/builder.rs @@ -307,12 +307,13 @@ impl GenericParamBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{ChangeRefKind, CilAssembly}, metadata::cilassemblyview::CilAssemblyView, }; - use std::path::PathBuf; #[test] fn test_generic_param_builder_basic() { diff --git a/dotscope/src/metadata/tables/genericparam/mod.rs b/dotscope/src/metadata/tables/genericparam/mod.rs index 96dd4517..96a19fe3 100644 --- a/dotscope/src/metadata/tables/genericparam/mod.rs +++ b/dotscope/src/metadata/tables/genericparam/mod.rs @@ -64,10 +64,12 @@ //! # ECMA-335 Reference //! See ECMA-335, Partition II, §22.20 for the complete `GenericParam` table specification. -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/genericparam/writer.rs b/dotscope/src/metadata/tables/genericparam/writer.rs index cab681b3..4ceb4d7d 100644 --- a/dotscope/src/metadata/tables/genericparam/writer.rs +++ b/dotscope/src/metadata/tables/genericparam/writer.rs @@ -104,13 +104,15 @@ impl RowWritable for GenericParamRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{ - genericparam::GenericParamRaw, - types::{ - CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + use crate::metadata::{ + tables::{ + genericparam::GenericParamRaw, + types::{ + CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + }, }, + token::Token, }; - use crate::metadata::token::Token; #[test] fn test_genericparam_row_size() { diff --git a/dotscope/src/metadata/tables/genericparamconstraint/builder.rs b/dotscope/src/metadata/tables/genericparamconstraint/builder.rs index 233592f5..4f8e04dc 100644 --- a/dotscope/src/metadata/tables/genericparamconstraint/builder.rs +++ b/dotscope/src/metadata/tables/genericparamconstraint/builder.rs @@ -248,12 +248,13 @@ impl GenericParamConstraintBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{ChangeRefKind, CilAssembly}, metadata::cilassemblyview::CilAssemblyView, }; - use std::path::PathBuf; #[test] fn test_generic_param_constraint_builder_basic() { diff --git a/dotscope/src/metadata/tables/genericparamconstraint/mod.rs b/dotscope/src/metadata/tables/genericparamconstraint/mod.rs index e4dffc28..b458efba 100644 --- a/dotscope/src/metadata/tables/genericparamconstraint/mod.rs +++ b/dotscope/src/metadata/tables/genericparamconstraint/mod.rs @@ -64,9 +64,10 @@ //! # ECMA-335 Reference //! See ECMA-335, Partition II, §22.21 for the complete `GenericParamConstraint` table specification. -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + use crate::metadata::token::Token; mod builder; diff --git a/dotscope/src/metadata/tables/genericparamconstraint/writer.rs b/dotscope/src/metadata/tables/genericparamconstraint/writer.rs index fa100fd5..5725a00a 100644 --- a/dotscope/src/metadata/tables/genericparamconstraint/writer.rs +++ b/dotscope/src/metadata/tables/genericparamconstraint/writer.rs @@ -87,13 +87,15 @@ impl RowWritable for GenericParamConstraintRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{ - genericparamconstraint::GenericParamConstraintRaw, - types::{ - CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + use crate::metadata::{ + tables::{ + genericparamconstraint::GenericParamConstraintRaw, + types::{ + CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + }, }, + token::Token, }; - use crate::metadata::token::Token; #[test] fn test_genericparamconstraint_row_size() { diff --git a/dotscope/src/metadata/tables/implmap/mod.rs b/dotscope/src/metadata/tables/implmap/mod.rs index fa0e14c2..d0f3885e 100644 --- a/dotscope/src/metadata/tables/implmap/mod.rs +++ b/dotscope/src/metadata/tables/implmap/mod.rs @@ -49,9 +49,10 @@ //! [`SUPPORTS_LAST_ERROR`]: PInvokeAttributes::SUPPORTS_LAST_ERROR //! [`BEST_FIT_ENABLED`]: PInvokeAttributes::BEST_FIT_ENABLED //! [`THROW_ON_UNMAPPABLE_ENABLED`]: PInvokeAttributes::THROW_ON_UNMAPPABLE_ENABLED -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + use crate::metadata::token::Token; mod builder; diff --git a/dotscope/src/metadata/tables/implmap/reader.rs b/dotscope/src/metadata/tables/implmap/reader.rs index cec637a6..0a091459 100644 --- a/dotscope/src/metadata/tables/implmap/reader.rs +++ b/dotscope/src/metadata/tables/implmap/reader.rs @@ -47,9 +47,8 @@ impl RowReadable for ImplMapRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; - use super::*; + use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; #[test] fn crafted_short() { diff --git a/dotscope/src/metadata/tables/implmap/writer.rs b/dotscope/src/metadata/tables/implmap/writer.rs index dbeb94a4..72b1eee5 100644 --- a/dotscope/src/metadata/tables/implmap/writer.rs +++ b/dotscope/src/metadata/tables/implmap/writer.rs @@ -94,13 +94,15 @@ impl RowWritable for ImplMapRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{ - implmap::ImplMapRaw, - types::{ - CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + use crate::metadata::{ + tables::{ + implmap::ImplMapRaw, + types::{ + CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + }, }, + token::Token, }; - use crate::metadata::token::Token; #[test] fn test_implmap_row_size() { diff --git a/dotscope/src/metadata/tables/importscope/mod.rs b/dotscope/src/metadata/tables/importscope/mod.rs index ac52db13..0d1f5d5c 100644 --- a/dotscope/src/metadata/tables/importscope/mod.rs +++ b/dotscope/src/metadata/tables/importscope/mod.rs @@ -93,14 +93,15 @@ mod raw; mod reader; mod writer; +use std::sync::Arc; + pub use builder::*; +use crossbeam_skiplist::SkipMap; pub(crate) use loader::*; pub use owned::*; pub use raw::*; use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; -use std::sync::Arc; /// A map that holds the mapping of [`crate::metadata::token::Token`] to parsed [`ImportScope`] /// diff --git a/dotscope/src/metadata/tables/importscope/owned.rs b/dotscope/src/metadata/tables/importscope/owned.rs index cfd0cca9..33846669 100644 --- a/dotscope/src/metadata/tables/importscope/owned.rs +++ b/dotscope/src/metadata/tables/importscope/owned.rs @@ -5,7 +5,7 @@ //! All heap indices have been resolved to their actual values and //! the imports blob has been parsed into structured declarations. -use crate::{metadata::importscope::ImportsInfo, metadata::token::Token}; +use crate::metadata::{importscope::ImportsInfo, token::Token}; /// Owned representation of an `ImportScope` table entry /// diff --git a/dotscope/src/metadata/tables/importscope/raw.rs b/dotscope/src/metadata/tables/importscope/raw.rs index 34c9fb18..0e1dbd7a 100644 --- a/dotscope/src/metadata/tables/importscope/raw.rs +++ b/dotscope/src/metadata/tables/importscope/raw.rs @@ -5,6 +5,8 @@ //! the metadata tables stream. This is the low-level representation used during //! the initial parsing phase, containing unresolved heap indices. +use std::sync::Arc; + use crate::{ metadata::{ importscope::{parse_imports_blob, ImportsInfo}, @@ -14,7 +16,6 @@ use crate::{ }, Result, }; -use std::sync::Arc; /// Raw binary representation of an `ImportScope` table entry /// diff --git a/dotscope/src/metadata/tables/importscope/writer.rs b/dotscope/src/metadata/tables/importscope/writer.rs index 9ce733f3..0bc4dfd6 100644 --- a/dotscope/src/metadata/tables/importscope/writer.rs +++ b/dotscope/src/metadata/tables/importscope/writer.rs @@ -82,9 +82,9 @@ impl RowWritable for ImportScopeRaw { #[cfg(test)] mod tests { use super::*; - use crate::{ - metadata::tables::types::{RowReadable, TableInfo, TableRow}, - metadata::token::Token, + use crate::metadata::{ + tables::types::{RowReadable, TableInfo, TableRow}, + token::Token, }; #[test] diff --git a/dotscope/src/metadata/tables/interfaceimpl/mod.rs b/dotscope/src/metadata/tables/interfaceimpl/mod.rs index c0eb8e9e..0f922466 100644 --- a/dotscope/src/metadata/tables/interfaceimpl/mod.rs +++ b/dotscope/src/metadata/tables/interfaceimpl/mod.rs @@ -42,9 +42,10 @@ //! - ECMA-335, Partition II, §22.23: `InterfaceImpl` table specification //! - ECMA-335, Partition II, §23.2.14: `TypeDefOrRef` coded index encoding //! - ECMA-335, Partition I, §8.9.11: Interface type contracts and inheritance -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + use crate::metadata::token::Token; mod builder; diff --git a/dotscope/src/metadata/tables/interfaceimpl/reader.rs b/dotscope/src/metadata/tables/interfaceimpl/reader.rs index 1a30f69d..66cc21d5 100644 --- a/dotscope/src/metadata/tables/interfaceimpl/reader.rs +++ b/dotscope/src/metadata/tables/interfaceimpl/reader.rs @@ -42,9 +42,8 @@ impl RowReadable for InterfaceImplRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{MetadataTable, TableInfo}; - use super::*; + use crate::metadata::tables::{MetadataTable, TableInfo}; #[test] fn crafted_short() { diff --git a/dotscope/src/metadata/tables/interfaceimpl/writer.rs b/dotscope/src/metadata/tables/interfaceimpl/writer.rs index 53b468d2..f6883299 100644 --- a/dotscope/src/metadata/tables/interfaceimpl/writer.rs +++ b/dotscope/src/metadata/tables/interfaceimpl/writer.rs @@ -79,15 +79,16 @@ impl RowWritable for InterfaceImplRaw { #[cfg(test)] mod tests { + use std::sync::Arc; + use super::*; - use crate::{ - metadata::tables::{ + use crate::metadata::{ + tables::{ types::{RowReadable, TableInfo, TableRow}, CodedIndex, TableId, }, - metadata::token::Token, + token::Token, }; - use std::sync::Arc; #[test] fn test_row_size() { diff --git a/dotscope/src/metadata/tables/localconstant/mod.rs b/dotscope/src/metadata/tables/localconstant/mod.rs index 3c2efefb..f5aca970 100644 --- a/dotscope/src/metadata/tables/localconstant/mod.rs +++ b/dotscope/src/metadata/tables/localconstant/mod.rs @@ -44,10 +44,12 @@ //! # Reference //! - [Portable PDB Format - LocalConstant Table](https://github.com/dotnet/core/blob/main/Documentation/diagnostics/portable_pdb.md#localconstant-table-0x34) -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/localconstant/raw.rs b/dotscope/src/metadata/tables/localconstant/raw.rs index a0f0f4d2..5740c7a8 100644 --- a/dotscope/src/metadata/tables/localconstant/raw.rs +++ b/dotscope/src/metadata/tables/localconstant/raw.rs @@ -5,6 +5,8 @@ //! the metadata tables stream. This is the low-level representation used during //! the initial parsing phase, containing unresolved heap indices. +use std::sync::Arc; + use crate::{ metadata::{ signatures::{parse_field_signature, SignatureField, TypeSignature}, @@ -14,7 +16,6 @@ use crate::{ }, Result, }; -use std::sync::Arc; /// Raw binary representation of a `LocalConstant` table entry /// diff --git a/dotscope/src/metadata/tables/localconstant/writer.rs b/dotscope/src/metadata/tables/localconstant/writer.rs index 3fe1baab..f54ca1ff 100644 --- a/dotscope/src/metadata/tables/localconstant/writer.rs +++ b/dotscope/src/metadata/tables/localconstant/writer.rs @@ -74,9 +74,9 @@ impl RowWritable for LocalConstantRaw { #[cfg(test)] mod tests { use super::*; - use crate::{ - metadata::tables::types::{RowReadable, TableInfo, TableRow}, - metadata::token::Token, + use crate::metadata::{ + tables::types::{RowReadable, TableInfo, TableRow}, + token::Token, }; #[test] diff --git a/dotscope/src/metadata/tables/localscope/mod.rs b/dotscope/src/metadata/tables/localscope/mod.rs index ff9bcf02..1686a2c1 100644 --- a/dotscope/src/metadata/tables/localscope/mod.rs +++ b/dotscope/src/metadata/tables/localscope/mod.rs @@ -72,10 +72,12 @@ //! //! - [Portable PDB Format - LocalScope Table](https://github.com/dotnet/core/blob/main/Documentation/diagnostics/portable_pdb.md#localscope-table-0x32) -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::{Arc, Weak}; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/localscope/raw.rs b/dotscope/src/metadata/tables/localscope/raw.rs index 57f7b130..7bc9eec9 100644 --- a/dotscope/src/metadata/tables/localscope/raw.rs +++ b/dotscope/src/metadata/tables/localscope/raw.rs @@ -5,6 +5,8 @@ //! the metadata tables stream. This is the low-level representation used during //! the initial parsing phase, containing unresolved table indices. +use std::sync::Arc; + use crate::{ metadata::{ method::MethodMap, @@ -16,7 +18,6 @@ use crate::{ }, Result, }; -use std::sync::Arc; /// Raw binary representation of a `LocalScope` table entry /// diff --git a/dotscope/src/metadata/tables/localscope/writer.rs b/dotscope/src/metadata/tables/localscope/writer.rs index 61ab8efa..8b600df1 100644 --- a/dotscope/src/metadata/tables/localscope/writer.rs +++ b/dotscope/src/metadata/tables/localscope/writer.rs @@ -113,9 +113,9 @@ impl RowWritable for LocalScopeRaw { #[cfg(test)] mod tests { use super::*; - use crate::{ - metadata::tables::types::{RowReadable, TableInfo, TableRow}, - metadata::token::Token, + use crate::metadata::{ + tables::types::{RowReadable, TableInfo, TableRow}, + token::Token, }; #[test] diff --git a/dotscope/src/metadata/tables/localvariable/mod.rs b/dotscope/src/metadata/tables/localvariable/mod.rs index 30a44e54..77b182bf 100644 --- a/dotscope/src/metadata/tables/localvariable/mod.rs +++ b/dotscope/src/metadata/tables/localvariable/mod.rs @@ -46,10 +46,12 @@ //! # Reference //! - [Portable PDB Format - LocalVariable Table](https://github.com/dotnet/core/blob/main/Documentation/diagnostics/portable_pdb.md#localvariable-table-0x33) -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/localvariable/raw.rs b/dotscope/src/metadata/tables/localvariable/raw.rs index 1d8f7d4b..81612ea0 100644 --- a/dotscope/src/metadata/tables/localvariable/raw.rs +++ b/dotscope/src/metadata/tables/localvariable/raw.rs @@ -5,6 +5,8 @@ //! the metadata tables stream. This is the low-level representation used during //! the initial parsing phase, containing unresolved heap indices. +use std::sync::Arc; + use crate::{ metadata::{ streams::Strings, @@ -13,7 +15,6 @@ use crate::{ }, Result, }; -use std::sync::Arc; /// Raw binary representation of a `LocalVariable` table entry /// diff --git a/dotscope/src/metadata/tables/localvariable/writer.rs b/dotscope/src/metadata/tables/localvariable/writer.rs index e36be976..12741aa2 100644 --- a/dotscope/src/metadata/tables/localvariable/writer.rs +++ b/dotscope/src/metadata/tables/localvariable/writer.rs @@ -80,9 +80,9 @@ impl RowWritable for LocalVariableRaw { #[cfg(test)] mod tests { use super::*; - use crate::{ - metadata::tables::types::{RowReadable, TableInfo, TableRow}, - metadata::token::Token, + use crate::metadata::{ + tables::types::{RowReadable, TableInfo, TableRow}, + token::Token, }; #[test] diff --git a/dotscope/src/metadata/tables/manifestresource/mod.rs b/dotscope/src/metadata/tables/manifestresource/mod.rs index 6e903e97..164b0e61 100644 --- a/dotscope/src/metadata/tables/manifestresource/mod.rs +++ b/dotscope/src/metadata/tables/manifestresource/mod.rs @@ -37,9 +37,10 @@ //! - ECMA-335, Partition II, §22.24: `ManifestResource` table specification //! - ECMA-335, Partition II, §23.2.7: Implementation coded index encoding //! - ECMA-335, Partition II, §6.2.2: Resources and resource management -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + use crate::metadata::token::Token; mod builder; diff --git a/dotscope/src/metadata/tables/manifestresource/reader.rs b/dotscope/src/metadata/tables/manifestresource/reader.rs index efb3dd79..31d2c919 100644 --- a/dotscope/src/metadata/tables/manifestresource/reader.rs +++ b/dotscope/src/metadata/tables/manifestresource/reader.rs @@ -29,9 +29,8 @@ impl RowReadable for ManifestResourceRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; - use super::*; + use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; #[test] fn crafted_short() { diff --git a/dotscope/src/metadata/tables/manifestresource/writer.rs b/dotscope/src/metadata/tables/manifestresource/writer.rs index 528c5369..0a8861a8 100644 --- a/dotscope/src/metadata/tables/manifestresource/writer.rs +++ b/dotscope/src/metadata/tables/manifestresource/writer.rs @@ -93,13 +93,15 @@ impl RowWritable for ManifestResourceRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{ - manifestresource::ManifestResourceRaw, - types::{ - CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + use crate::metadata::{ + tables::{ + manifestresource::ManifestResourceRaw, + types::{ + CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + }, }, + token::Token, }; - use crate::metadata::token::Token; #[test] fn test_manifestresource_row_size() { diff --git a/dotscope/src/metadata/tables/memberref/builder.rs b/dotscope/src/metadata/tables/memberref/builder.rs index 8e751127..0cf27471 100644 --- a/dotscope/src/metadata/tables/memberref/builder.rs +++ b/dotscope/src/metadata/tables/memberref/builder.rs @@ -249,9 +249,10 @@ impl MemberRefBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{cilassembly::CilAssembly, metadata::cilassemblyview::CilAssemblyView}; - use std::path::PathBuf; #[test] fn test_memberref_builder_basic() { diff --git a/dotscope/src/metadata/tables/memberref/mod.rs b/dotscope/src/metadata/tables/memberref/mod.rs index a2c8bd11..608d81c7 100644 --- a/dotscope/src/metadata/tables/memberref/mod.rs +++ b/dotscope/src/metadata/tables/memberref/mod.rs @@ -46,9 +46,10 @@ //! - ECMA-335, Partition II, §22.25: `MemberRef` table specification //! - ECMA-335, Partition II, §23.2.6: `MemberRefParent` coded index encoding //! - ECMA-335, Partition II, §23.2: Method and field signature specifications -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + use crate::metadata::{ signatures::{SignatureField, SignatureMethod}, token::Token, diff --git a/dotscope/src/metadata/tables/memberref/reader.rs b/dotscope/src/metadata/tables/memberref/reader.rs index 5c5202a9..6ff6350e 100644 --- a/dotscope/src/metadata/tables/memberref/reader.rs +++ b/dotscope/src/metadata/tables/memberref/reader.rs @@ -26,9 +26,8 @@ impl RowReadable for MemberRefRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; - use super::*; + use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; #[test] fn crafted_short() { diff --git a/dotscope/src/metadata/tables/memberref/writer.rs b/dotscope/src/metadata/tables/memberref/writer.rs index 8212317e..55e3c7d4 100644 --- a/dotscope/src/metadata/tables/memberref/writer.rs +++ b/dotscope/src/metadata/tables/memberref/writer.rs @@ -80,13 +80,15 @@ impl RowWritable for MemberRefRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{ - memberref::MemberRefRaw, - types::{ - CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + use crate::metadata::{ + tables::{ + memberref::MemberRefRaw, + types::{ + CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + }, }, + token::Token, }; - use crate::metadata::token::Token; #[test] fn test_memberref_row_size() { diff --git a/dotscope/src/metadata/tables/methoddebuginformation/mod.rs b/dotscope/src/metadata/tables/methoddebuginformation/mod.rs index 534abe25..de3e52bb 100644 --- a/dotscope/src/metadata/tables/methoddebuginformation/mod.rs +++ b/dotscope/src/metadata/tables/methoddebuginformation/mod.rs @@ -67,10 +67,12 @@ //! //! - [Portable PDB Format - MethodDebugInformation Table](https://github.com/dotnet/core/blob/main/Documentation/diagnostics/portable_pdb.md#methoddebuginformation-table-0x31) -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/methoddebuginformation/raw.rs b/dotscope/src/metadata/tables/methoddebuginformation/raw.rs index a60760c4..c8244714 100644 --- a/dotscope/src/metadata/tables/methoddebuginformation/raw.rs +++ b/dotscope/src/metadata/tables/methoddebuginformation/raw.rs @@ -60,6 +60,8 @@ //! - [`crate::metadata::streams::Blob`] - Blob heap for sequence points data resolution //! - [`crate::metadata::method`] - Method definition association and debugging +use std::sync::Arc; + use crate::{ metadata::{ sequencepoints::parse_sequence_points, @@ -71,7 +73,6 @@ use crate::{ }, Result, }; -use std::sync::Arc; /// Raw binary representation of a `MethodDebugInformation` table entry. /// diff --git a/dotscope/src/metadata/tables/methoddebuginformation/writer.rs b/dotscope/src/metadata/tables/methoddebuginformation/writer.rs index a9723fe1..6191afe0 100644 --- a/dotscope/src/metadata/tables/methoddebuginformation/writer.rs +++ b/dotscope/src/metadata/tables/methoddebuginformation/writer.rs @@ -82,9 +82,9 @@ impl RowWritable for MethodDebugInformationRaw { #[cfg(test)] mod tests { use super::*; - use crate::{ - metadata::tables::types::{RowReadable, TableInfo, TableRow}, - metadata::token::Token, + use crate::metadata::{ + tables::types::{RowReadable, TableInfo, TableRow}, + token::Token, }; #[test] diff --git a/dotscope/src/metadata/tables/methoddef/builder.rs b/dotscope/src/metadata/tables/methoddef/builder.rs index 8e13119c..a8417ea0 100644 --- a/dotscope/src/metadata/tables/methoddef/builder.rs +++ b/dotscope/src/metadata/tables/methoddef/builder.rs @@ -301,12 +301,13 @@ impl Default for MethodDefBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{ChangeRefKind, CilAssembly}, metadata::method::{MethodAccessFlags, MethodImplCodeType, MethodModifiers}, }; - use std::path::PathBuf; #[test] fn test_method_builder_basic() { diff --git a/dotscope/src/metadata/tables/methoddef/reader.rs b/dotscope/src/metadata/tables/methoddef/reader.rs index 1d50a68b..dc194def 100644 --- a/dotscope/src/metadata/tables/methoddef/reader.rs +++ b/dotscope/src/metadata/tables/methoddef/reader.rs @@ -29,9 +29,8 @@ impl RowReadable for MethodDefRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{MetadataTable, TableInfo}; - use super::*; + use crate::metadata::tables::{MetadataTable, TableInfo}; #[test] fn crafted_short() { diff --git a/dotscope/src/metadata/tables/methoddef/writer.rs b/dotscope/src/metadata/tables/methoddef/writer.rs index 10a33766..f230bb6a 100644 --- a/dotscope/src/metadata/tables/methoddef/writer.rs +++ b/dotscope/src/metadata/tables/methoddef/writer.rs @@ -103,15 +103,16 @@ impl RowWritable for MethodDefRaw { #[cfg(test)] mod tests { + use std::sync::Arc; + use super::*; - use crate::{ - metadata::tables::{ + use crate::metadata::{ + tables::{ types::{RowReadable, TableInfo, TableRow}, TableId, }, - metadata::token::Token, + token::Token, }; - use std::sync::Arc; #[test] fn test_row_size() { diff --git a/dotscope/src/metadata/tables/methodimpl/builder.rs b/dotscope/src/metadata/tables/methodimpl/builder.rs index b79ab37f..e38a6678 100644 --- a/dotscope/src/metadata/tables/methodimpl/builder.rs +++ b/dotscope/src/metadata/tables/methodimpl/builder.rs @@ -452,12 +452,13 @@ impl MethodImplBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{ChangeRefKind, CilAssembly}, metadata::cilassemblyview::CilAssemblyView, }; - use std::path::PathBuf; #[test] fn test_methodimpl_builder_creation() { diff --git a/dotscope/src/metadata/tables/methodimpl/mod.rs b/dotscope/src/metadata/tables/methodimpl/mod.rs index a9c9eb55..5f30ea33 100644 --- a/dotscope/src/metadata/tables/methodimpl/mod.rs +++ b/dotscope/src/metadata/tables/methodimpl/mod.rs @@ -165,10 +165,12 @@ //! //! [`SkipMap`]: crossbeam_skiplist::SkipMap //! [`Arc`]: std::sync::Arc -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/methodimpl/writer.rs b/dotscope/src/metadata/tables/methodimpl/writer.rs index faabd71a..8915b7bc 100644 --- a/dotscope/src/metadata/tables/methodimpl/writer.rs +++ b/dotscope/src/metadata/tables/methodimpl/writer.rs @@ -88,13 +88,15 @@ impl RowWritable for MethodImplRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{ - methodimpl::MethodImplRaw, - types::{ - CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + use crate::metadata::{ + tables::{ + methodimpl::MethodImplRaw, + types::{ + CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + }, }, + token::Token, }; - use crate::metadata::token::Token; #[test] fn test_methodimpl_row_size() { diff --git a/dotscope/src/metadata/tables/methodptr/mod.rs b/dotscope/src/metadata/tables/methodptr/mod.rs index aa23fff3..4c2f3f06 100644 --- a/dotscope/src/metadata/tables/methodptr/mod.rs +++ b/dotscope/src/metadata/tables/methodptr/mod.rs @@ -52,10 +52,12 @@ //! //! [`SkipMap`]: crossbeam_skiplist::SkipMap //! [`Arc`]: std::sync::Arc -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/methodsemantics/builder.rs b/dotscope/src/metadata/tables/methodsemantics/builder.rs index dbd013d4..db4d973d 100644 --- a/dotscope/src/metadata/tables/methodsemantics/builder.rs +++ b/dotscope/src/metadata/tables/methodsemantics/builder.rs @@ -362,12 +362,13 @@ impl Default for MethodSemanticsBuilder { #[cfg(test)] mod tests { + use std::{env, path::PathBuf}; + use super::*; use crate::{ cilassembly::{ChangeRefKind, CilAssembly}, metadata::{cilassemblyview::CilAssemblyView, tables::MethodSemanticsAttributes}, }; - use std::{env, path::PathBuf}; #[test] fn test_methodsemantics_builder_creation() { diff --git a/dotscope/src/metadata/tables/methodsemantics/mod.rs b/dotscope/src/metadata/tables/methodsemantics/mod.rs index 828a3fef..2e2716b9 100644 --- a/dotscope/src/metadata/tables/methodsemantics/mod.rs +++ b/dotscope/src/metadata/tables/methodsemantics/mod.rs @@ -50,10 +50,12 @@ //! //! For detailed specifications, see [ECMA-335 6th Edition](https://www.ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf). -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/methodsemantics/writer.rs b/dotscope/src/metadata/tables/methodsemantics/writer.rs index b5307106..8c2975ab 100644 --- a/dotscope/src/metadata/tables/methodsemantics/writer.rs +++ b/dotscope/src/metadata/tables/methodsemantics/writer.rs @@ -88,13 +88,15 @@ impl RowWritable for MethodSemanticsRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{ - methodsemantics::MethodSemanticsRaw, - types::{ - CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + use crate::metadata::{ + tables::{ + methodsemantics::MethodSemanticsRaw, + types::{ + CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + }, }, + token::Token, }; - use crate::metadata::token::Token; #[test] fn test_methodsemantics_row_size() { diff --git a/dotscope/src/metadata/tables/methodspec/builder.rs b/dotscope/src/metadata/tables/methodspec/builder.rs index 93bf0119..af2a8e3e 100644 --- a/dotscope/src/metadata/tables/methodspec/builder.rs +++ b/dotscope/src/metadata/tables/methodspec/builder.rs @@ -353,12 +353,13 @@ impl MethodSpecBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{ChangeRefKind, CilAssembly}, metadata::cilassemblyview::CilAssemblyView, }; - use std::path::PathBuf; #[test] fn test_method_spec_builder_basic() { diff --git a/dotscope/src/metadata/tables/methodspec/mod.rs b/dotscope/src/metadata/tables/methodspec/mod.rs index c84eda49..6d348bc7 100644 --- a/dotscope/src/metadata/tables/methodspec/mod.rs +++ b/dotscope/src/metadata/tables/methodspec/mod.rs @@ -56,10 +56,12 @@ //! //! For detailed specifications, see [ECMA-335 6th Edition](https://www.ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf). -use crate::metadata::{token::Token, typesystem::CilTypeReference}; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::{token::Token, typesystem::CilTypeReference}; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/methodspec/writer.rs b/dotscope/src/metadata/tables/methodspec/writer.rs index b1cb79e5..1d5c3ee8 100644 --- a/dotscope/src/metadata/tables/methodspec/writer.rs +++ b/dotscope/src/metadata/tables/methodspec/writer.rs @@ -81,13 +81,15 @@ impl RowWritable for MethodSpecRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{ - methodspec::MethodSpecRaw, - types::{ - CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + use crate::metadata::{ + tables::{ + methodspec::MethodSpecRaw, + types::{ + CodedIndex, CodedIndexType, RowReadable, RowWritable, TableId, TableInfo, TableRow, + }, }, + token::Token, }; - use crate::metadata::token::Token; #[test] fn test_methodspec_row_size() { diff --git a/dotscope/src/metadata/tables/module/builder.rs b/dotscope/src/metadata/tables/module/builder.rs index f8b41a0f..1ab17c51 100644 --- a/dotscope/src/metadata/tables/module/builder.rs +++ b/dotscope/src/metadata/tables/module/builder.rs @@ -362,8 +362,10 @@ impl ModuleBuilder { fn generate_random_guid() -> [u8; 16] { // For now, generate a simple deterministic GUID based on timestamp and counter // In production, this should use a proper GUID generation library - use std::sync::atomic::{AtomicU64, Ordering}; - use std::time::{SystemTime, UNIX_EPOCH}; + use std::{ + sync::atomic::{AtomicU64, Ordering}, + time::{SystemTime, UNIX_EPOCH}, + }; static COUNTER: AtomicU64 = AtomicU64::new(1); diff --git a/dotscope/src/metadata/tables/module/mod.rs b/dotscope/src/metadata/tables/module/mod.rs index 7aa5229d..3fd98f73 100644 --- a/dotscope/src/metadata/tables/module/mod.rs +++ b/dotscope/src/metadata/tables/module/mod.rs @@ -56,10 +56,12 @@ //! //! For detailed specifications, see [ECMA-335 6th Edition](https://www.ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf). -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/module/reader.rs b/dotscope/src/metadata/tables/module/reader.rs index 0fe53b53..805054bc 100644 --- a/dotscope/src/metadata/tables/module/reader.rs +++ b/dotscope/src/metadata/tables/module/reader.rs @@ -105,9 +105,8 @@ impl RowReadable for ModuleRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; - use super::*; + use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; #[test] fn crafted_short() { diff --git a/dotscope/src/metadata/tables/module/writer.rs b/dotscope/src/metadata/tables/module/writer.rs index f89cf182..85049921 100644 --- a/dotscope/src/metadata/tables/module/writer.rs +++ b/dotscope/src/metadata/tables/module/writer.rs @@ -100,12 +100,13 @@ impl RowWritable for ModuleRaw { #[cfg(test)] mod tests { + use std::sync::Arc; + use super::*; use crate::metadata::{ tables::types::{RowReadable, TableInfo, TableRow}, token::Token, }; - use std::sync::Arc; #[test] fn test_round_trip_serialization_small_heaps() { diff --git a/dotscope/src/metadata/tables/moduleref/mod.rs b/dotscope/src/metadata/tables/moduleref/mod.rs index 22fd14a4..0d213f41 100644 --- a/dotscope/src/metadata/tables/moduleref/mod.rs +++ b/dotscope/src/metadata/tables/moduleref/mod.rs @@ -51,12 +51,14 @@ //! - **§II.24.2.1** - String heap references //! //! For detailed specifications, see [ECMA-335 6th Edition](https://www.ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf). +use std::sync::Arc; + +use crossbeam_skiplist::SkipMap; + use crate::metadata::{ imports::{ImportContainer, ImportRc, Imports}, token::Token, }; -use crossbeam_skiplist::SkipMap; -use std::sync::Arc; mod builder; mod loader; diff --git a/dotscope/src/metadata/tables/nestedclass/mod.rs b/dotscope/src/metadata/tables/nestedclass/mod.rs index bf76e9b8..fa02a896 100644 --- a/dotscope/src/metadata/tables/nestedclass/mod.rs +++ b/dotscope/src/metadata/tables/nestedclass/mod.rs @@ -53,10 +53,12 @@ //! - **§II.24.2.6** - `TypeDefOrRef` coded index format //! //! For detailed specifications, see [ECMA-335 6th Edition](https://www.ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf). -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/nestedclass/writer.rs b/dotscope/src/metadata/tables/nestedclass/writer.rs index 0678e76c..f8465e48 100644 --- a/dotscope/src/metadata/tables/nestedclass/writer.rs +++ b/dotscope/src/metadata/tables/nestedclass/writer.rs @@ -74,11 +74,13 @@ impl RowWritable for NestedClassRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{ - nestedclass::NestedClassRaw, - types::{RowReadable, RowWritable, TableId, TableInfo, TableRow}, + use crate::metadata::{ + tables::{ + nestedclass::NestedClassRaw, + types::{RowReadable, RowWritable, TableId, TableInfo, TableRow}, + }, + token::Token, }; - use crate::metadata::token::Token; #[test] fn test_nestedclass_row_size() { diff --git a/dotscope/src/metadata/tables/param/builder.rs b/dotscope/src/metadata/tables/param/builder.rs index 3d329263..3d3067e6 100644 --- a/dotscope/src/metadata/tables/param/builder.rs +++ b/dotscope/src/metadata/tables/param/builder.rs @@ -194,12 +194,13 @@ impl ParamBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{ChangeRefKind, CilAssembly}, metadata::{cilassemblyview::CilAssemblyView, tables::ParamAttributes}, }; - use std::path::PathBuf; #[test] fn test_param_builder_basic() { diff --git a/dotscope/src/metadata/tables/param/mod.rs b/dotscope/src/metadata/tables/param/mod.rs index f6c5bb5f..ee9dd207 100644 --- a/dotscope/src/metadata/tables/param/mod.rs +++ b/dotscope/src/metadata/tables/param/mod.rs @@ -60,10 +60,12 @@ //! - **§II.24.2.1** - String heap references //! //! For detailed specifications, see [ECMA-335 6th Edition](https://www.ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf). -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/param/reader.rs b/dotscope/src/metadata/tables/param/reader.rs index a2e93374..8fb072e2 100644 --- a/dotscope/src/metadata/tables/param/reader.rs +++ b/dotscope/src/metadata/tables/param/reader.rs @@ -108,9 +108,8 @@ impl RowReadable for ParamRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; - use super::*; + use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; #[test] fn crafted_short() { diff --git a/dotscope/src/metadata/tables/param/writer.rs b/dotscope/src/metadata/tables/param/writer.rs index eb5aa4b0..a295178f 100644 --- a/dotscope/src/metadata/tables/param/writer.rs +++ b/dotscope/src/metadata/tables/param/writer.rs @@ -82,12 +82,13 @@ impl RowWritable for ParamRaw { #[cfg(test)] mod tests { + use std::sync::Arc; + use super::*; - use crate::{ - metadata::tables::types::{RowReadable, TableInfo, TableRow}, - metadata::token::Token, + use crate::metadata::{ + tables::types::{RowReadable, TableInfo, TableRow}, + token::Token, }; - use std::sync::Arc; #[test] fn test_row_size() { diff --git a/dotscope/src/metadata/tables/paramptr/builder.rs b/dotscope/src/metadata/tables/paramptr/builder.rs index 72857d77..c72b17bc 100644 --- a/dotscope/src/metadata/tables/paramptr/builder.rs +++ b/dotscope/src/metadata/tables/paramptr/builder.rs @@ -182,9 +182,10 @@ impl Default for ParamPtrBuilder { #[cfg(test)] mod tests { + use std::sync::Arc; + use super::*; use crate::test::factories::table::assemblyref::get_test_assembly; - use std::sync::Arc; #[test] fn test_paramptr_builder_new() { diff --git a/dotscope/src/metadata/tables/paramptr/mod.rs b/dotscope/src/metadata/tables/paramptr/mod.rs index 6ebbc1a5..b9aa8fd9 100644 --- a/dotscope/src/metadata/tables/paramptr/mod.rs +++ b/dotscope/src/metadata/tables/paramptr/mod.rs @@ -34,10 +34,12 @@ //! - ECMA-335, Partition II, §22.26 - `ParamPtr` table specification //! - [`crate::metadata::tables::Param`] - Target parameter table entries //! - [`crate::metadata::loader`] - Metadata loading and resolution system -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/property/builder.rs b/dotscope/src/metadata/tables/property/builder.rs index a78c9862..de50b838 100644 --- a/dotscope/src/metadata/tables/property/builder.rs +++ b/dotscope/src/metadata/tables/property/builder.rs @@ -217,6 +217,8 @@ impl PropertyBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{ChangeRefKind, CilAssembly}, @@ -225,7 +227,6 @@ mod tests { tables::{PropertyAttributes, TableId}, }, }; - use std::path::PathBuf; #[test] fn test_property_builder_basic() { diff --git a/dotscope/src/metadata/tables/property/mod.rs b/dotscope/src/metadata/tables/property/mod.rs index e69ac2d1..a0b4fbcb 100644 --- a/dotscope/src/metadata/tables/property/mod.rs +++ b/dotscope/src/metadata/tables/property/mod.rs @@ -42,10 +42,12 @@ //! - [`crate::metadata::tables::PropertyMap`] - Property to type mapping //! - [`crate::metadata::tables::MethodSemantics`] - Property method associations //! - [`crate::metadata::signatures`] - Property signature parsing -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/property/writer.rs b/dotscope/src/metadata/tables/property/writer.rs index 395bce75..0f146a11 100644 --- a/dotscope/src/metadata/tables/property/writer.rs +++ b/dotscope/src/metadata/tables/property/writer.rs @@ -76,12 +76,13 @@ impl RowWritable for PropertyRaw { #[cfg(test)] mod tests { + use std::sync::Arc; + use super::*; - use crate::{ - metadata::tables::types::{RowReadable, TableInfo, TableRow}, - metadata::token::Token, + use crate::metadata::{ + tables::types::{RowReadable, TableInfo, TableRow}, + token::Token, }; - use std::sync::Arc; #[test] fn test_row_size() { diff --git a/dotscope/src/metadata/tables/propertymap/builder.rs b/dotscope/src/metadata/tables/propertymap/builder.rs index 552b10c2..f79cae06 100644 --- a/dotscope/src/metadata/tables/propertymap/builder.rs +++ b/dotscope/src/metadata/tables/propertymap/builder.rs @@ -286,9 +286,10 @@ impl PropertyMapBuilder { #[cfg(test)] mod tests { + use std::sync::Arc; + use super::*; use crate::test::factories::table::assemblyref::get_test_assembly; - use std::sync::Arc; #[test] fn test_property_map_builder_basic() -> Result<()> { diff --git a/dotscope/src/metadata/tables/propertymap/mod.rs b/dotscope/src/metadata/tables/propertymap/mod.rs index 21646af1..8d24b2a7 100644 --- a/dotscope/src/metadata/tables/propertymap/mod.rs +++ b/dotscope/src/metadata/tables/propertymap/mod.rs @@ -42,10 +42,12 @@ //! - [`crate::metadata::tables::Property`] - Property definitions //! - [`crate::metadata::tables::TypeDefRaw`] - Type definitions //! - [`crate::metadata::typesystem`] - Type system integration -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/propertymap/reader.rs b/dotscope/src/metadata/tables/propertymap/reader.rs index e4aa06e9..c739f25b 100644 --- a/dotscope/src/metadata/tables/propertymap/reader.rs +++ b/dotscope/src/metadata/tables/propertymap/reader.rs @@ -103,9 +103,8 @@ impl RowReadable for PropertyMapRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; - use super::*; + use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; #[test] fn crafted_short() { diff --git a/dotscope/src/metadata/tables/propertymap/writer.rs b/dotscope/src/metadata/tables/propertymap/writer.rs index dc0ffa9b..4002cdda 100644 --- a/dotscope/src/metadata/tables/propertymap/writer.rs +++ b/dotscope/src/metadata/tables/propertymap/writer.rs @@ -70,11 +70,13 @@ impl RowWritable for PropertyMapRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{ - propertymap::PropertyMapRaw, - types::{RowReadable, RowWritable, TableId, TableInfo, TableRow}, + use crate::metadata::{ + tables::{ + propertymap::PropertyMapRaw, + types::{RowReadable, RowWritable, TableId, TableInfo, TableRow}, + }, + token::Token, }; - use crate::metadata::token::Token; #[test] fn test_propertymap_row_size() { diff --git a/dotscope/src/metadata/tables/propertyptr/builder.rs b/dotscope/src/metadata/tables/propertyptr/builder.rs index 93d74ead..65f59531 100644 --- a/dotscope/src/metadata/tables/propertyptr/builder.rs +++ b/dotscope/src/metadata/tables/propertyptr/builder.rs @@ -185,9 +185,10 @@ impl Default for PropertyPtrBuilder { #[cfg(test)] mod tests { + use std::sync::Arc; + use super::*; use crate::test::factories::table::assemblyref::get_test_assembly; - use std::sync::Arc; #[test] fn test_propertyptr_builder_new() { diff --git a/dotscope/src/metadata/tables/propertyptr/mod.rs b/dotscope/src/metadata/tables/propertyptr/mod.rs index 066f1240..053c05d5 100644 --- a/dotscope/src/metadata/tables/propertyptr/mod.rs +++ b/dotscope/src/metadata/tables/propertyptr/mod.rs @@ -49,10 +49,12 @@ //! - [`crate::metadata::tables::Property`] - Target property table //! - [`crate::metadata::streams`] - Metadata stream formats and compression -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/propertyptr/writer.rs b/dotscope/src/metadata/tables/propertyptr/writer.rs index d554bfed..25433e43 100644 --- a/dotscope/src/metadata/tables/propertyptr/writer.rs +++ b/dotscope/src/metadata/tables/propertyptr/writer.rs @@ -75,9 +75,9 @@ impl RowWritable for PropertyPtrRaw { #[cfg(test)] mod tests { use super::*; - use crate::{ - metadata::tables::types::{RowReadable, TableInfo, TableRow}, - metadata::token::Token, + use crate::metadata::{ + tables::types::{RowReadable, TableInfo, TableRow}, + token::Token, }; #[test] diff --git a/dotscope/src/metadata/tables/standalonesig/builder.rs b/dotscope/src/metadata/tables/standalonesig/builder.rs index 872a1b27..122b8cf8 100644 --- a/dotscope/src/metadata/tables/standalonesig/builder.rs +++ b/dotscope/src/metadata/tables/standalonesig/builder.rs @@ -232,9 +232,10 @@ impl StandAloneSigBuilder { #[cfg(test)] mod tests { + use std::sync::Arc; + use super::*; use crate::test::factories::table::assemblyref::get_test_assembly; - use std::sync::Arc; #[test] fn test_standalonesig_builder_basic() -> Result<()> { diff --git a/dotscope/src/metadata/tables/standalonesig/mod.rs b/dotscope/src/metadata/tables/standalonesig/mod.rs index b23b515d..724665e5 100644 --- a/dotscope/src/metadata/tables/standalonesig/mod.rs +++ b/dotscope/src/metadata/tables/standalonesig/mod.rs @@ -51,10 +51,12 @@ //! - [`crate::metadata::signatures`] - Signature parsing and types //! - [`crate::metadata::streams::Blob`] - Blob stream access for signature data -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/standalonesig/raw.rs b/dotscope/src/metadata/tables/standalonesig/raw.rs index 9c64fba8..66850e64 100644 --- a/dotscope/src/metadata/tables/standalonesig/raw.rs +++ b/dotscope/src/metadata/tables/standalonesig/raw.rs @@ -13,8 +13,7 @@ use crate::{ CALLING_CONVENTION, SIGNATURE_HEADER, }, streams::Blob, - tables::{StandAloneSig, StandAloneSigRc, StandAloneSignature}, - tables::{TableInfoRef, TableRow}, + tables::{StandAloneSig, StandAloneSigRc, StandAloneSignature, TableInfoRef, TableRow}, token::Token, }, Result, diff --git a/dotscope/src/metadata/tables/standalonesig/reader.rs b/dotscope/src/metadata/tables/standalonesig/reader.rs index 1e2b7dbd..8ff997b1 100644 --- a/dotscope/src/metadata/tables/standalonesig/reader.rs +++ b/dotscope/src/metadata/tables/standalonesig/reader.rs @@ -92,9 +92,8 @@ impl RowReadable for StandAloneSigRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{MetadataTable, TableInfo}; - use super::*; + use crate::metadata::tables::{MetadataTable, TableInfo}; #[test] fn crafted_short() { diff --git a/dotscope/src/metadata/tables/standalonesig/writer.rs b/dotscope/src/metadata/tables/standalonesig/writer.rs index f1b37f35..5abd16ed 100644 --- a/dotscope/src/metadata/tables/standalonesig/writer.rs +++ b/dotscope/src/metadata/tables/standalonesig/writer.rs @@ -62,11 +62,13 @@ impl RowWritable for StandAloneSigRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{ - standalonesig::StandAloneSigRaw, - types::{RowReadable, RowWritable, TableInfo, TableRow}, + use crate::metadata::{ + tables::{ + standalonesig::StandAloneSigRaw, + types::{RowReadable, RowWritable, TableInfo, TableRow}, + }, + token::Token, }; - use crate::metadata::token::Token; #[test] fn test_standalonesig_row_size() { diff --git a/dotscope/src/metadata/tables/statemachinemethod/builder.rs b/dotscope/src/metadata/tables/statemachinemethod/builder.rs index d024d4a9..4a6e6b2e 100644 --- a/dotscope/src/metadata/tables/statemachinemethod/builder.rs +++ b/dotscope/src/metadata/tables/statemachinemethod/builder.rs @@ -217,9 +217,10 @@ impl Default for StateMachineMethodBuilder { #[cfg(test)] mod tests { + use std::sync::Arc; + use super::*; use crate::test::factories::table::assemblyref::get_test_assembly; - use std::sync::Arc; #[test] fn test_statemachinemethod_builder_new() { diff --git a/dotscope/src/metadata/tables/statemachinemethod/mod.rs b/dotscope/src/metadata/tables/statemachinemethod/mod.rs index 6ab2fe06..cbb7fe43 100644 --- a/dotscope/src/metadata/tables/statemachinemethod/mod.rs +++ b/dotscope/src/metadata/tables/statemachinemethod/mod.rs @@ -51,14 +51,15 @@ mod raw; mod reader; mod writer; +use std::sync::Arc; + pub use builder::*; +use crossbeam_skiplist::SkipMap; pub(crate) use loader::*; pub use owned::*; pub use raw::*; use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; -use std::sync::Arc; /// A map that holds the mapping of [`crate::metadata::token::Token`] to parsed [`StateMachineMethod`] /// diff --git a/dotscope/src/metadata/tables/statemachinemethod/raw.rs b/dotscope/src/metadata/tables/statemachinemethod/raw.rs index 9f4d8c26..a32ca307 100644 --- a/dotscope/src/metadata/tables/statemachinemethod/raw.rs +++ b/dotscope/src/metadata/tables/statemachinemethod/raw.rs @@ -5,6 +5,8 @@ //! the metadata tables stream. This is the low-level representation used during //! the initial parsing phase, containing unresolved method indices. +use std::sync::Arc; + use crate::{ metadata::{ method::MethodMap, @@ -14,7 +16,6 @@ use crate::{ Error::TypeNotFound, Result, }; -use std::sync::Arc; /// Raw binary representation of a `StateMachineMethod` table entry /// diff --git a/dotscope/src/metadata/tables/statemachinemethod/writer.rs b/dotscope/src/metadata/tables/statemachinemethod/writer.rs index 264d88d6..ed853231 100644 --- a/dotscope/src/metadata/tables/statemachinemethod/writer.rs +++ b/dotscope/src/metadata/tables/statemachinemethod/writer.rs @@ -92,9 +92,9 @@ impl RowWritable for StateMachineMethodRaw { #[cfg(test)] mod tests { use super::*; - use crate::{ - metadata::tables::types::{RowReadable, TableInfo, TableRow}, - metadata::token::Token, + use crate::metadata::{ + tables::types::{RowReadable, TableInfo, TableRow}, + token::Token, }; #[test] diff --git a/dotscope/src/metadata/tables/typedef/builder.rs b/dotscope/src/metadata/tables/typedef/builder.rs index 9fb71ba0..c9734ff7 100644 --- a/dotscope/src/metadata/tables/typedef/builder.rs +++ b/dotscope/src/metadata/tables/typedef/builder.rs @@ -273,6 +273,8 @@ impl TypeDefBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{ChangeRefKind, CilAssembly}, @@ -281,7 +283,6 @@ mod tests { tables::{TableId, TypeAttributes}, }, }; - use std::path::PathBuf; #[test] fn test_typedef_builder_basic() { diff --git a/dotscope/src/metadata/tables/typedef/reader.rs b/dotscope/src/metadata/tables/typedef/reader.rs index b1848cc6..0c338090 100644 --- a/dotscope/src/metadata/tables/typedef/reader.rs +++ b/dotscope/src/metadata/tables/typedef/reader.rs @@ -97,9 +97,8 @@ impl RowReadable for TypeDefRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; - use super::*; + use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; #[test] fn crafted_short() { diff --git a/dotscope/src/metadata/tables/typedef/writer.rs b/dotscope/src/metadata/tables/typedef/writer.rs index 89465f86..a6435cd3 100644 --- a/dotscope/src/metadata/tables/typedef/writer.rs +++ b/dotscope/src/metadata/tables/typedef/writer.rs @@ -109,15 +109,16 @@ impl RowWritable for TypeDefRaw { #[cfg(test)] mod tests { + use std::sync::Arc; + use super::*; - use crate::{ - metadata::tables::{ + use crate::metadata::{ + tables::{ types::{RowReadable, TableInfo, TableRow}, CodedIndex, CodedIndexType, }, - metadata::token::Token, + token::Token, }; - use std::sync::Arc; #[test] fn test_row_size() { diff --git a/dotscope/src/metadata/tables/typeref/builder.rs b/dotscope/src/metadata/tables/typeref/builder.rs index ce5d3f16..0ae63456 100644 --- a/dotscope/src/metadata/tables/typeref/builder.rs +++ b/dotscope/src/metadata/tables/typeref/builder.rs @@ -167,13 +167,14 @@ impl TypeRefBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{ChangeRefKind, CilAssembly}, metadata::cilassemblyview::CilAssemblyView, prelude::CodedIndexType, }; - use std::path::PathBuf; #[test] fn test_typeref_builder_basic() { diff --git a/dotscope/src/metadata/tables/typeref/reader.rs b/dotscope/src/metadata/tables/typeref/reader.rs index d5708eda..11d77b31 100644 --- a/dotscope/src/metadata/tables/typeref/reader.rs +++ b/dotscope/src/metadata/tables/typeref/reader.rs @@ -90,9 +90,8 @@ impl RowReadable for TypeRefRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; - use super::*; + use crate::metadata::tables::{MetadataTable, TableId, TableInfo}; #[test] fn crafted_short() { diff --git a/dotscope/src/metadata/tables/typeref/writer.rs b/dotscope/src/metadata/tables/typeref/writer.rs index eb7940f5..0f90db9a 100644 --- a/dotscope/src/metadata/tables/typeref/writer.rs +++ b/dotscope/src/metadata/tables/typeref/writer.rs @@ -85,15 +85,16 @@ impl RowWritable for TypeRefRaw { #[cfg(test)] mod tests { + use std::sync::Arc; + use super::*; - use crate::{ - metadata::tables::{ + use crate::metadata::{ + tables::{ types::{RowReadable, TableInfo, TableRow}, CodedIndex, TableId, }, - metadata::token::Token, + token::Token, }; - use std::sync::Arc; #[test] fn test_row_size() { diff --git a/dotscope/src/metadata/tables/types/common/info.rs b/dotscope/src/metadata/tables/types/common/info.rs index 2222bb3d..6d3852a6 100644 --- a/dotscope/src/metadata/tables/types/common/info.rs +++ b/dotscope/src/metadata/tables/types/common/info.rs @@ -25,6 +25,7 @@ //! * [ECMA-335 Partition II, Section 24.2.6](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - Coded Indices use std::sync::Arc; + use strum::{EnumCount, IntoEnumIterator}; use crate::{ diff --git a/dotscope/src/metadata/tables/types/read/iter.rs b/dotscope/src/metadata/tables/types/read/iter.rs index acbf745d..6abd3894 100644 --- a/dotscope/src/metadata/tables/types/read/iter.rs +++ b/dotscope/src/metadata/tables/types/read/iter.rs @@ -32,9 +32,10 @@ //! - [`crate::metadata::tables::types::read::traits`] - Core parsing traits //! - [`crate::metadata::tables::types::read::access`] - Low-level access utilities -use rayon::iter::{plumbing, IndexedParallelIterator, ParallelIterator}; use std::sync::{Arc, Mutex}; +use rayon::iter::{plumbing, IndexedParallelIterator, ParallelIterator}; + use crate::{ metadata::tables::{MetadataTable, RowReadable}, Error, Result, diff --git a/dotscope/src/metadata/tables/types/read/table.rs b/dotscope/src/metadata/tables/types/read/table.rs index eaa0920d..7b4d146c 100644 --- a/dotscope/src/metadata/tables/types/read/table.rs +++ b/dotscope/src/metadata/tables/types/read/table.rs @@ -31,11 +31,12 @@ //! - [`crate::metadata::tables::types::read::access`] - Low-level access utilities //! - [`crate::metadata::tables::types::read::traits`] - Core trait definitions +use std::{marker::PhantomData, sync::Arc}; + use crate::{ metadata::tables::{RowReadable, TableInfoRef, TableIterator, TableParIterator}, Result, }; -use std::{marker::PhantomData, sync::Arc}; /// Generic container for metadata table data with typed row access. /// diff --git a/dotscope/src/metadata/tables/typespec/builder.rs b/dotscope/src/metadata/tables/typespec/builder.rs index 67cac551..3e3b619a 100644 --- a/dotscope/src/metadata/tables/typespec/builder.rs +++ b/dotscope/src/metadata/tables/typespec/builder.rs @@ -558,6 +558,8 @@ impl TypeSpecBuilder { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::{ChangeRefKind, CilAssembly}, @@ -566,7 +568,6 @@ mod tests { signatures::{SignatureMethod, SignatureParameter}, }, }; - use std::path::PathBuf; #[test] fn test_typespec_builder_creation() { diff --git a/dotscope/src/metadata/tables/typespec/mod.rs b/dotscope/src/metadata/tables/typespec/mod.rs index 345d1b67..bd7049a7 100644 --- a/dotscope/src/metadata/tables/typespec/mod.rs +++ b/dotscope/src/metadata/tables/typespec/mod.rs @@ -74,10 +74,12 @@ //! //! * [ECMA-335 Partition II, Section 22.39](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - `TypeSpec` Table //! * [ECMA-335 Partition II, Section 23.2.14](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - `TypeSpec` Signatures -use crate::metadata::token::Token; -use crossbeam_skiplist::SkipMap; use std::sync::Arc; +use crossbeam_skiplist::SkipMap; + +use crate::metadata::token::Token; + mod builder; mod loader; mod owned; diff --git a/dotscope/src/metadata/tables/typespec/writer.rs b/dotscope/src/metadata/tables/typespec/writer.rs index 155f2a6c..8f276e06 100644 --- a/dotscope/src/metadata/tables/typespec/writer.rs +++ b/dotscope/src/metadata/tables/typespec/writer.rs @@ -63,11 +63,13 @@ impl RowWritable for TypeSpecRaw { mod tests { use std::sync::Arc; - use crate::metadata::tables::{ - types::{RowReadable, RowWritable, TableInfo, TableRow}, - typespec::TypeSpecRaw, + use crate::metadata::{ + tables::{ + types::{RowReadable, RowWritable, TableInfo, TableRow}, + typespec::TypeSpecRaw, + }, + token::Token, }; - use crate::metadata::token::Token; #[test] fn test_typespec_row_size() { diff --git a/dotscope/src/metadata/token.rs b/dotscope/src/metadata/token.rs index d62f3bfc..9e1c6f3f 100644 --- a/dotscope/src/metadata/token.rs +++ b/dotscope/src/metadata/token.rs @@ -391,9 +391,10 @@ impl Hash for Token { #[cfg(test)] mod tests { - use super::*; use std::collections::HashMap; + use super::*; + #[test] fn test_token_new() { let token = Token::new(0x06000001); diff --git a/dotscope/src/metadata/typesystem/encoder.rs b/dotscope/src/metadata/typesystem/encoder.rs index 86867627..39a7984e 100644 --- a/dotscope/src/metadata/typesystem/encoder.rs +++ b/dotscope/src/metadata/typesystem/encoder.rs @@ -322,7 +322,11 @@ impl TypeSignatureEncoder { // Function pointer TypeSignature::FnPtr(method_sig) => { buffer.push(0x1B); // ELEMENT_TYPE_FNPTR - Self::encode_method_signature(method_sig.as_ref(), buffer)?; + Self::encode_method_signature( + method_sig.as_ref(), + buffer, + depth.saturating_add(1), + )?; } // Custom modifiers @@ -385,7 +389,14 @@ impl TypeSignatureEncoder { /// # Returns /// /// Success or error result from encoding. - fn encode_method_signature(method_sig: &SignatureMethod, buffer: &mut Vec) -> Result<()> { + fn encode_method_signature( + method_sig: &SignatureMethod, + buffer: &mut Vec, + depth: usize, + ) -> Result<()> { + if depth >= MAX_RECURSION_DEPTH { + return Err(crate::Error::RecursionLimit(MAX_RECURSION_DEPTH)); + } let mut calling_conv = 0u8; if method_sig.has_this { calling_conv |= 0x20; @@ -423,10 +434,17 @@ impl TypeSignatureEncoder { })?, buffer, ); - Self::encode_type_signature(&method_sig.return_type.base, buffer)?; + // Carries the depth rather than calling the public depth-0 wrapper: + // re-entering at zero re-armed the cap on every FNPTR hop, so a + // signature nesting through method signatures was unbounded. + Self::encode_type_signature_internal( + &method_sig.return_type.base, + buffer, + depth.saturating_add(1), + )?; for param in &method_sig.params { - Self::encode_type_signature(¶m.base, buffer)?; + Self::encode_type_signature_internal(¶m.base, buffer, depth.saturating_add(1))?; } Ok(()) @@ -508,8 +526,10 @@ impl TypeSignatureEncoder { #[cfg(test)] mod tests { use super::*; - use crate::metadata::signatures::{SignatureArray, SignaturePointer, SignatureSzArray}; - use crate::metadata::typesystem::ArrayDimensions; + use crate::metadata::{ + signatures::{SignatureArray, SignaturePointer, SignatureSzArray}, + typesystem::ArrayDimensions, + }; #[test] fn test_encode_primitive_types() { diff --git a/dotscope/src/metadata/typesystem/hash.rs b/dotscope/src/metadata/typesystem/hash.rs index 38763ac3..1c3df6bb 100644 --- a/dotscope/src/metadata/typesystem/hash.rs +++ b/dotscope/src/metadata/typesystem/hash.rs @@ -27,11 +27,12 @@ //! .finalize(); //! ``` +use std::hash::{DefaultHasher, Hash, Hasher}; + use crate::metadata::{ token::Token, typesystem::{CilFlavor, TypeSource}, }; -use std::hash::{DefaultHasher, Hash, Hasher}; /// High-quality hash builder for type signatures using FNV-1a inspired mixing /// diff --git a/dotscope/src/metadata/typesystem/primitives.rs b/dotscope/src/metadata/typesystem/primitives.rs index e7bbdd07..8b55de7a 100644 --- a/dotscope/src/metadata/typesystem/primitives.rs +++ b/dotscope/src/metadata/typesystem/primitives.rs @@ -1589,9 +1589,10 @@ impl TryFrom for CilPrimitive { #[cfg(test)] mod tests { + use std::convert::TryFrom; + use super::*; use crate::ParseFailure; - use std::convert::TryFrom; #[test] fn test_primitive_creation() { diff --git a/dotscope/src/metadata/validation/context.rs b/dotscope/src/metadata/validation/context.rs index 89ba884c..fadb9742 100644 --- a/dotscope/src/metadata/validation/context.rs +++ b/dotscope/src/metadata/validation/context.rs @@ -55,6 +55,7 @@ use std::sync::{Arc, OnceLock}; +use rayon::ThreadPool; use rustc_hash::FxHashMap; use crate::{ @@ -68,7 +69,6 @@ use crate::{ validation::{config::ValidationConfig, scanner::ReferenceScanner}, }, }; -use rayon::ThreadPool; /// Validation stage indicator for context discrimination. /// @@ -788,11 +788,12 @@ impl ValidationContext for OwnedValidationContext<'_> { /// Factory functions for creating validation contexts. pub mod factory { + use rayon::ThreadPool; + use super::{ AssemblyChanges, CilAssemblyView, CilObject, OwnedValidationContext, RawValidationContext, ReferenceScanner, ValidationConfig, }; - use rayon::ThreadPool; /// Creates a raw validation context for loading validation. pub fn raw_loading_context<'a>( @@ -828,10 +829,12 @@ pub mod factory { #[cfg(test)] mod tests { + use std::path::PathBuf; + + use rayon::ThreadPoolBuilder; + use super::*; use crate::metadata::validation::config::ValidationConfig; - use rayon::ThreadPoolBuilder; - use std::path::PathBuf; #[test] fn test_raw_loading_context() { diff --git a/dotscope/src/metadata/validation/engine.rs b/dotscope/src/metadata/validation/engine.rs index 02157541..e1132dd9 100644 --- a/dotscope/src/metadata/validation/engine.rs +++ b/dotscope/src/metadata/validation/engine.rs @@ -49,7 +49,10 @@ //! - [`crate::metadata::validation::context`] - Validation context abstractions //! - [`crate::metadata::validation::config`] - Configuration for validation behavior +use std::{sync::OnceLock, time::Instant}; + use log::debug; +use rayon::{prelude::*, ThreadPool, ThreadPoolBuilder}; use crate::{ cilassembly::AssemblyChanges, @@ -77,8 +80,6 @@ use crate::{ }, Error, Result, }; -use rayon::{prelude::*, ThreadPool, ThreadPoolBuilder}; -use std::{sync::OnceLock, time::Instant}; /// Static registry of raw validators. /// @@ -587,6 +588,8 @@ pub mod factory { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::{ cilassembly::AssemblyChanges, @@ -598,7 +601,6 @@ mod tests { }, }, }; - use std::path::PathBuf; // Test validator for validation struct TestRawValidator { diff --git a/dotscope/src/metadata/validation/result.rs b/dotscope/src/metadata/validation/result.rs index 46330967..4b32a3d4 100644 --- a/dotscope/src/metadata/validation/result.rs +++ b/dotscope/src/metadata/validation/result.rs @@ -57,11 +57,12 @@ //! - [`crate::metadata::validation::traits`] - Validators return [`crate::Result`] converted to outcomes //! - [`crate::Error`] - Error types used in failed validation outcomes +use std::{fmt, sync::Arc, time::Duration}; + use crate::{ metadata::diagnostics::{DiagnosticCategory, Diagnostics}, Error, Result, }; -use std::{fmt, sync::Arc, time::Duration}; /// Represents the outcome of a validation operation. /// @@ -729,9 +730,10 @@ impl fmt::Display for TwoStageValidationResult { #[cfg(test)] mod tests { + use std::time::Duration; + use super::*; use crate::Error; - use std::time::Duration; #[test] fn test_validation_result_success() { diff --git a/dotscope/src/metadata/validation/scanner.rs b/dotscope/src/metadata/validation/scanner.rs index a9b27312..1871c269 100644 --- a/dotscope/src/metadata/validation/scanner.rs +++ b/dotscope/src/metadata/validation/scanner.rs @@ -55,6 +55,8 @@ //! - [`crate::metadata::validation::engine`] - Creates scanner for validation runs //! - [`crate::metadata::validation::traits`] - Validators use scanner for reference validation +use rustc_hash::{FxHashMap, FxHashSet}; + use crate::{ dispatch_table_type, metadata::{ @@ -69,7 +71,6 @@ use crate::{ }, Blob, Error, Guid, HeapKind, ParseFailure, Result, Strings, UserStrings, }; -use rustc_hash::{FxHashMap, FxHashSet}; /// Reference scanner for metadata validation. /// @@ -902,9 +903,10 @@ impl std::fmt::Display for ScannerStatistics { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::metadata::cilassemblyview::CilAssemblyView; - use std::path::PathBuf; #[test] fn test_reference_scanner_creation() { diff --git a/dotscope/src/metadata/validation/shared/references.rs b/dotscope/src/metadata/validation/shared/references.rs index 000d0816..4c4e6e5a 100644 --- a/dotscope/src/metadata/validation/shared/references.rs +++ b/dotscope/src/metadata/validation/shared/references.rs @@ -63,14 +63,15 @@ //! - Owned validators - Used by owned validators for cross-reference validation //! - [`crate::metadata::token`] - Validates token-based references +use std::collections::HashSet; + +use rustc_hash::{FxHashMap, FxHashSet}; use strum::IntoEnumIterator; use crate::{ metadata::{tables::TableId, token::Token, validation::scanner::ReferenceScanner}, Error, Result, }; -use rustc_hash::{FxHashMap, FxHashSet}; -use std::collections::HashSet; /// Shared reference validation utilities. /// @@ -591,9 +592,10 @@ impl std::fmt::Display for ReferenceStatistics { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::metadata::cilassemblyview::CilAssemblyView; - use std::path::PathBuf; #[test] fn test_reference_validator_creation() { diff --git a/dotscope/src/metadata/validation/shared/schema.rs b/dotscope/src/metadata/validation/shared/schema.rs index 8665adb9..07116793 100644 --- a/dotscope/src/metadata/validation/shared/schema.rs +++ b/dotscope/src/metadata/validation/shared/schema.rs @@ -506,9 +506,10 @@ impl std::fmt::Display for SchemaValidationStatistics { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::metadata::cilassemblyview::CilAssemblyView; - use std::path::PathBuf; #[test] fn test_schema_validator_creation() { diff --git a/dotscope/src/metadata/validation/shared/tokens.rs b/dotscope/src/metadata/validation/shared/tokens.rs index 4a5a6bb5..8c13bf26 100644 --- a/dotscope/src/metadata/validation/shared/tokens.rs +++ b/dotscope/src/metadata/validation/shared/tokens.rs @@ -527,9 +527,10 @@ impl TokenValidationResult { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; use crate::metadata::cilassemblyview::CilAssemblyView; - use std::path::PathBuf; #[test] fn test_token_validator_creation() { diff --git a/dotscope/src/metadata/validation/traits.rs b/dotscope/src/metadata/validation/traits.rs index 693bd143..11956588 100644 --- a/dotscope/src/metadata/validation/traits.rs +++ b/dotscope/src/metadata/validation/traits.rs @@ -365,13 +365,15 @@ macro_rules! owned_validators { #[cfg(test)] mod tests { + use std::path::PathBuf; + + use rayon::ThreadPoolBuilder; + use super::*; use crate::metadata::{ cilassemblyview::CilAssemblyView, validation::{config::ValidationConfig, context::factory, scanner::ReferenceScanner}, }; - use rayon::ThreadPoolBuilder; - use std::path::PathBuf; struct TestRawValidator { name: &'static str, diff --git a/dotscope/src/metadata/validation/validators/owned/constraints/types.rs b/dotscope/src/metadata/validation/validators/owned/constraints/types.rs index e210c3da..40021dd5 100644 --- a/dotscope/src/metadata/validation/validators/owned/constraints/types.rs +++ b/dotscope/src/metadata/validation/validators/owned/constraints/types.rs @@ -72,6 +72,8 @@ //! - [ECMA-335 I.8.9.1](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - Generic type instantiation //! - [ECMA-335 II.22.29](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - TypeSpec constraints +use std::collections::HashSet; + use crate::{ metadata::{ tables::GenericParamAttributes, @@ -83,7 +85,6 @@ use crate::{ }, Error, Result, }; -use std::collections::HashSet; /// Foundation validator for generic type constraints, inheritance compatibility, and interface implementation requirements. /// diff --git a/dotscope/src/metadata/validation/validators/owned/metadata/signature.rs b/dotscope/src/metadata/validation/validators/owned/metadata/signature.rs index 94a46e44..ee843535 100644 --- a/dotscope/src/metadata/validation/validators/owned/metadata/signature.rs +++ b/dotscope/src/metadata/validation/validators/owned/metadata/signature.rs @@ -71,9 +71,10 @@ //! - [ECMA-335 I.8.6](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - Assignment compatibility //! - [ECMA-335 II.10.1](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - Method overriding and signatures -use rayon::prelude::*; use std::collections::HashMap; +use rayon::prelude::*; + use crate::{ metadata::validation::{ context::{OwnedValidationContext, ValidationContext}, diff --git a/dotscope/src/metadata/validation/validators/owned/system/assembly.rs b/dotscope/src/metadata/validation/validators/owned/system/assembly.rs index 494c2e4e..1b20e274 100644 --- a/dotscope/src/metadata/validation/validators/owned/system/assembly.rs +++ b/dotscope/src/metadata/validation/validators/owned/system/assembly.rs @@ -76,6 +76,8 @@ //! - [ECMA-335 II.22.14](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - File table //! - [ECMA-335 I.6.3](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - Application domains and assemblies +use std::sync::Arc; + use crate::{ metadata::{ identity::Identity, @@ -87,7 +89,6 @@ use crate::{ }, Error, Result, }; -use std::sync::Arc; /// Foundation validator for assembly-level metadata, references, and integrity constraints. /// diff --git a/dotscope/src/metadata/validation/validators/owned/system/security.rs b/dotscope/src/metadata/validation/validators/owned/system/security.rs index 3af85f85..a32aadd6 100644 --- a/dotscope/src/metadata/validation/validators/owned/system/security.rs +++ b/dotscope/src/metadata/validation/validators/owned/system/security.rs @@ -74,6 +74,8 @@ //! - [ECMA-335 IV.7](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - Security attributes //! - [.NET Framework Security Model](https://docs.microsoft.com/en-us/dotnet/framework/security/) - Security model compliance +use rustc_hash::FxHashSet; + use crate::{ metadata::{ customattributes::{CustomAttributeArgument, CustomAttributeValue}, @@ -85,7 +87,6 @@ use crate::{ }, Error, Result, }; -use rustc_hash::FxHashSet; /// Foundation validator for security constraints, permissions, and security attributes. /// diff --git a/dotscope/src/metadata/validation/validators/owned/types/circularity.rs b/dotscope/src/metadata/validation/validators/owned/types/circularity.rs index e561d0b4..987a662d 100644 --- a/dotscope/src/metadata/validation/validators/owned/types/circularity.rs +++ b/dotscope/src/metadata/validation/validators/owned/types/circularity.rs @@ -77,6 +77,8 @@ use std::sync::Arc; +use rustc_hash::{FxHashMap, FxHashSet}; + use crate::{ metadata::{ token::Token, @@ -88,7 +90,6 @@ use crate::{ }, Error, Result, }; -use rustc_hash::{FxHashMap, FxHashSet}; /// Foundation validator for circular dependencies in type systems, methods, and references. /// diff --git a/dotscope/src/metadata/validation/validators/owned/types/definition.rs b/dotscope/src/metadata/validation/validators/owned/types/definition.rs index 1de5125f..07a9845f 100644 --- a/dotscope/src/metadata/validation/validators/owned/types/definition.rs +++ b/dotscope/src/metadata/validation/validators/owned/types/definition.rs @@ -74,6 +74,8 @@ //! - [ECMA-335 II.22.37](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - TypeDef table //! - [ECMA-335 I.8.9](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - Type definitions +use rayon::prelude::*; + use crate::{ metadata::{ tables::TypeAttributes, @@ -85,7 +87,6 @@ use crate::{ }, Error, Result, }; -use rayon::prelude::*; /// Foundation validator for basic type definition structure, attributes, and metadata consistency. /// diff --git a/dotscope/src/metadata/validation/validators/owned/types/dependency.rs b/dotscope/src/metadata/validation/validators/owned/types/dependency.rs index 3d7bb5d2..cee366b1 100644 --- a/dotscope/src/metadata/validation/validators/owned/types/dependency.rs +++ b/dotscope/src/metadata/validation/validators/owned/types/dependency.rs @@ -72,6 +72,11 @@ //! - [ECMA-335 II.22.35](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - TypeDef table dependencies //! - [ECMA-335 II.6.3](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - Accessing data and calling methods +use std::sync::Arc; + +use rayon::prelude::*; +use rustc_hash::{FxHashMap, FxHashSet}; + use crate::{ metadata::validation::{ context::{OwnedValidationContext, ValidationContext}, @@ -80,9 +85,6 @@ use crate::{ prelude::CilTypeRc, Error, Result, }; -use rayon::prelude::*; -use rustc_hash::{FxHashMap, FxHashSet}; -use std::sync::Arc; /// Foundation validator for dependency relationships between types, assemblies, and metadata elements. /// diff --git a/dotscope/src/metadata/validation/validators/owned/types/inheritance.rs b/dotscope/src/metadata/validation/validators/owned/types/inheritance.rs index ca2004c6..5dafa075 100644 --- a/dotscope/src/metadata/validation/validators/owned/types/inheritance.rs +++ b/dotscope/src/metadata/validation/validators/owned/types/inheritance.rs @@ -82,6 +82,11 @@ //! - [ECMA-335 II.12.2](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - Inheritance and overriding //! - [ECMA-335 II.22.37](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - TypeDef inheritance +use std::{collections::HashSet, mem, sync::Arc}; + +use rayon::prelude::*; +use rustc_hash::FxHashSet; + use crate::{ metadata::{ method::{Method, MethodAccessFlags, MethodModifiers}, @@ -94,9 +99,6 @@ use crate::{ }, Error, Result, }; -use rayon::prelude::*; -use rustc_hash::FxHashSet; -use std::{collections::HashSet, mem, sync::Arc}; /// Foundation validator for inheritance hierarchies, circular dependencies, interface implementation, and method inheritance. /// diff --git a/dotscope/src/metadata/validation/validators/owned/types/ownership.rs b/dotscope/src/metadata/validation/validators/owned/types/ownership.rs index 0d337933..889e7a50 100644 --- a/dotscope/src/metadata/validation/validators/owned/types/ownership.rs +++ b/dotscope/src/metadata/validation/validators/owned/types/ownership.rs @@ -67,9 +67,10 @@ //! - [ECMA-335 II.10.7](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - Nested types //! - [ECMA-335 I.6.2](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - Assemblies and application domains -use rayon::prelude::*; use std::collections::HashSet; +use rayon::prelude::*; + use crate::{ metadata::validation::{ context::{OwnedValidationContext, ValidationContext}, diff --git a/dotscope/src/metadata/validation/validators/raw/constraints/layout.rs b/dotscope/src/metadata/validation/validators/raw/constraints/layout.rs index 0e061ddb..98ab6fab 100644 --- a/dotscope/src/metadata/validation/validators/raw/constraints/layout.rs +++ b/dotscope/src/metadata/validation/validators/raw/constraints/layout.rs @@ -71,6 +71,8 @@ //! - [ECMA-335 II.22.8](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - ClassLayout table //! - [ECMA-335 II.22.16](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - FieldLayout table +use rustc_hash::FxHashMap; + use crate::{ metadata::{ cilassemblyview::CilAssemblyView, @@ -83,7 +85,6 @@ use crate::{ }, Result, }; -use rustc_hash::FxHashMap; /// Foundation validator for field and class layout constraint integrity and consistency. /// diff --git a/dotscope/src/metadata/validation/validators/raw/modification/integrity.rs b/dotscope/src/metadata/validation/validators/raw/modification/integrity.rs index 580ca752..9a91ba71 100644 --- a/dotscope/src/metadata/validation/validators/raw/modification/integrity.rs +++ b/dotscope/src/metadata/validation/validators/raw/modification/integrity.rs @@ -73,6 +73,10 @@ //! - [ECMA-335 II.22](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - Metadata table specifications //! - [ECMA-335 II.24](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - Metadata physical layout +use std::collections::HashMap; + +use rustc_hash::{FxHashMap, FxHashSet}; + use crate::{ cilassembly::{Operation, TableModifications}, metadata::{ @@ -84,8 +88,6 @@ use crate::{ }, Error, Result, }; -use rustc_hash::{FxHashMap, FxHashSet}; -use std::collections::HashMap; /// Foundation validator for post-change assembly integrity and consistency validation. /// @@ -680,6 +682,10 @@ impl Default for RawChangeIntegrityValidator { #[cfg(test)] mod tests { + use std::collections::{HashMap, HashSet}; + + use rayon::ThreadPoolBuilder; + use super::*; use crate::{ cilassembly::{AssemblyChanges, Operation, TableModifications, TableOperation}, @@ -700,8 +706,6 @@ mod tests { }, Error, }; - use rayon::ThreadPoolBuilder; - use std::collections::{HashMap, HashSet}; /// Direct corruption testing for RawChangeIntegrityValidator bypassing file I/O. /// diff --git a/dotscope/src/metadata/validation/validators/raw/modification/operation.rs b/dotscope/src/metadata/validation/validators/raw/modification/operation.rs index f376ae81..21eaf0fc 100644 --- a/dotscope/src/metadata/validation/validators/raw/modification/operation.rs +++ b/dotscope/src/metadata/validation/validators/raw/modification/operation.rs @@ -74,6 +74,10 @@ //! - [ECMA-335 II.22](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - Metadata table specifications //! - [ECMA-335 II.24](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - Metadata physical layout +use std::collections::HashMap; + +use rustc_hash::{FxHashMap, FxHashSet}; + use crate::{ cilassembly::{Operation, TableModifications}, metadata::{ @@ -85,8 +89,6 @@ use crate::{ }, Result, }; -use rustc_hash::{FxHashMap, FxHashSet}; -use std::collections::HashMap; /// Foundation validator for assembly modification operation integrity and consistency. /// @@ -605,6 +607,8 @@ impl Default for RawOperationValidator { #[cfg(test)] mod tests { + use rayon::ThreadPoolBuilder; + use super::*; use crate::{ cilassembly::AssemblyChanges, @@ -619,7 +623,6 @@ mod tests { }, Error, }; - use rayon::ThreadPoolBuilder; #[test] fn test_raw_operation_validator() -> Result<()> { diff --git a/dotscope/src/metadata/validation/validators/raw/structure/table.rs b/dotscope/src/metadata/validation/validators/raw/structure/table.rs index 0c3cef41..a81e0a26 100644 --- a/dotscope/src/metadata/validation/validators/raw/structure/table.rs +++ b/dotscope/src/metadata/validation/validators/raw/structure/table.rs @@ -72,6 +72,8 @@ //! - [ECMA-335 II.22](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - Metadata tables specification //! - [ECMA-335 II.24.2](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - Metadata layout requirements +use strum::IntoEnumIterator; + use crate::{ dispatch_table_type, metadata::{ @@ -85,7 +87,6 @@ use crate::{ }, Result, }; -use strum::IntoEnumIterator; /// Foundation validator for metadata table structure and integrity. /// diff --git a/dotscope/src/metadata/validation/validators/raw/structure/token.rs b/dotscope/src/metadata/validation/validators/raw/structure/token.rs index bf2f7b60..7da3a98d 100644 --- a/dotscope/src/metadata/validation/validators/raw/structure/token.rs +++ b/dotscope/src/metadata/validation/validators/raw/structure/token.rs @@ -71,6 +71,8 @@ //! - [ECMA-335 II.22](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - Metadata tables specification //! - [ECMA-335 II.24.2.6](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - Coded index encoding +use strum::IntoEnumIterator; + use crate::{ dispatch_table_type, metadata::{ @@ -88,7 +90,6 @@ use crate::{ }, Error, Result, }; -use strum::IntoEnumIterator; /// Foundation validator for token format, RID bounds, and coded index validation. /// diff --git a/dotscope/src/prelude.rs b/dotscope/src/prelude.rs index 3437ccd4..566928d0 100644 --- a/dotscope/src/prelude.rs +++ b/dotscope/src/prelude.rs @@ -937,8 +937,8 @@ pub use crate::compiler::{ AlgebraicSimplificationPass, BlockMergingPass, ConstantPropagationPass, ControlFlowSimplificationPass, CopyPropagationPass, DeadCodeEliminationPass, DeadMethodEliminationPass, GlobalValueNumberingPass, InliningPass, JumpThreadingPass, LicmPass, - LoopCanonicalizationPass, OpaquePredicatePass, ReassociationPass, StrengthReductionPass, - ValueRangePropagationPass, + LoopCanonicalizationPass, MemoryOptimizationPass, OpaquePredicatePass, ReassociationPass, + StrengthReductionPass, ValueRangePropagationPass, }; /// Deobfuscation-specific SSA passes. diff --git a/dotscope/src/project/context.rs b/dotscope/src/project/context.rs index b06824fc..9853285f 100644 --- a/dotscope/src/project/context.rs +++ b/dotscope/src/project/context.rs @@ -3,13 +3,15 @@ //! This module provides the `ProjectContext` which manages barrier synchronization //! during parallel assembly loading to handle circular dependencies safely. +use std::sync::Arc; + +use boxcar::Vec as BoxcarVec; + use crate::{ metadata::{identity::AssemblyIdentity, typesystem::TypeRegistry}, utils::FailFastBarrier, Error, Result, }; -use boxcar::Vec as BoxcarVec; -use std::sync::Arc; /// Coordination context for multi-assembly parallel loading with barrier synchronization. /// diff --git a/dotscope/src/project/loader.rs b/dotscope/src/project/loader.rs index 2db87be4..07f76498 100644 --- a/dotscope/src/project/loader.rs +++ b/dotscope/src/project/loader.rs @@ -4,6 +4,11 @@ //! with automatic dependency resolution, graceful fallback to single-assembly mode, and //! progressive dependency addition. +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; + use log::{debug, info, warn}; use crate::{ @@ -15,10 +20,6 @@ use crate::{ project::{context::ProjectContext, ProjectResult}, Error, Result, }; -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; /// Builder for creating and loading CilProject instances with flexible dependency management. /// diff --git a/dotscope/src/project/mod.rs b/dotscope/src/project/mod.rs index 0956c645..65d76895 100644 --- a/dotscope/src/project/mod.rs +++ b/dotscope/src/project/mod.rs @@ -89,6 +89,13 @@ pub mod context; mod loader; mod result; +use std::sync::{Arc, OnceLock}; + +pub(crate) use context::ProjectContext; +use dashmap::DashMap; +pub use loader::ProjectLoader; +pub use result::{ProjectResult, VersionMismatch}; + use crate::{ metadata::{ cilobject::CilObject, identity::AssemblyIdentity, tables::TableId, token::Token, @@ -96,12 +103,6 @@ use crate::{ }, Error, Result, }; -use dashmap::DashMap; -use std::sync::{Arc, OnceLock}; - -pub(crate) use context::ProjectContext; -pub use loader::ProjectLoader; -pub use result::{ProjectResult, VersionMismatch}; /// Multi-assembly project container with dependency management and cross-assembly resolution. /// @@ -545,9 +546,8 @@ impl std::fmt::Debug for CilProject { #[cfg(test)] #[cfg_attr(feature = "skip-expensive-tests", allow(unused_imports))] mod tests { - use crate::test::{verify_crafted_2, verify_windowsbasedll}; - use super::*; + use crate::test::{verify_crafted_2, verify_windowsbasedll}; #[test] fn test_cilproject_creation() { diff --git a/dotscope/src/project/result.rs b/dotscope/src/project/result.rs index c6621223..907c26ab 100644 --- a/dotscope/src/project/result.rs +++ b/dotscope/src/project/result.rs @@ -3,15 +3,16 @@ //! This module provides result types for project loading operations, tracking //! successfully loaded assemblies, failures, and missing dependencies. -use crate::{ - metadata::{cilassemblyview::CilAssemblyView, identity::AssemblyIdentity}, - project::CilProject, -}; use std::{ collections::{HashMap, HashSet, VecDeque}, path::PathBuf, }; +use crate::{ + metadata::{cilassemblyview::CilAssemblyView, identity::AssemblyIdentity}, + project::CilProject, +}; + /// A version mismatch between a required and actual assembly version. /// /// This occurs when an assembly is found but its version doesn't satisfy the diff --git a/dotscope/src/test/analysis/runner.rs b/dotscope/src/test/analysis/runner.rs index bcd2bb4c..d33f451e 100644 --- a/dotscope/src/test/analysis/runner.rs +++ b/dotscope/src/test/analysis/runner.rs @@ -3,8 +3,7 @@ //! This module provides the main test runner that compiles C# source, //! loads the resulting assembly, and verifies analysis results. -use std::path::PathBuf; -use std::sync::Arc; +use std::{path::PathBuf, sync::Arc}; use crate::{ analysis::CallGraph, diff --git a/dotscope/src/test/factories/general/file.rs b/dotscope/src/test/factories/general/file.rs index b1ba9fa7..ee02b432 100644 --- a/dotscope/src/test/factories/general/file.rs +++ b/dotscope/src/test/factories/general/file.rs @@ -3,9 +3,10 @@ //! Contains helper methods migrated from file source files //! for creating and verifying test data related to file operations. -use crate::{file::File, DataDirectoryType}; use goblin::pe::header::DOS_MAGIC; +use crate::{file::File, DataDirectoryType}; + /// Verifies the correctness of a loaded [`crate::file::File`] instance. /// /// This function checks various properties of the loaded PE file, including headers, diff --git a/dotscope/src/test/factories/metadata/customattributes.rs b/dotscope/src/test/factories/metadata/customattributes.rs index 890493e8..a81ade47 100644 --- a/dotscope/src/test/factories/metadata/customattributes.rs +++ b/dotscope/src/test/factories/metadata/customattributes.rs @@ -3,6 +3,8 @@ //! Contains helper methods migrated from custom attributes source files //! for creating test data related to custom attribute parsing and encoding. +use std::sync::Arc; + use crate::{ metadata::{ identity::AssemblyIdentity, @@ -11,7 +13,6 @@ use crate::{ }, test::MethodBuilder, }; -use std::sync::Arc; /// Helper to create a method with empty parameters for parsing tests /// diff --git a/dotscope/src/test/factories/table/assemblyref.rs b/dotscope/src/test/factories/table/assemblyref.rs index cf63ae7c..598e9bbb 100644 --- a/dotscope/src/test/factories/table/assemblyref.rs +++ b/dotscope/src/test/factories/table/assemblyref.rs @@ -3,9 +3,10 @@ //! Contains helper methods migrated from AssemblyRef table source files //! for creating test data related to assembly reference operations. -use crate::{cilassembly::CilAssembly, metadata::cilassemblyview::CilAssemblyView, Result}; use std::path::PathBuf; +use crate::{cilassembly::CilAssembly, metadata::cilassemblyview::CilAssemblyView, Result}; + /// Helper function to get a test assembly for AssemblyRef operations /// /// Originally from: `src/metadata/tables/assemblyref/builder.rs` diff --git a/dotscope/src/test/factories/table/constant.rs b/dotscope/src/test/factories/table/constant.rs index 8e8c6f24..782308e5 100644 --- a/dotscope/src/test/factories/table/constant.rs +++ b/dotscope/src/test/factories/table/constant.rs @@ -3,6 +3,8 @@ //! Contains helper methods migrated from Constant table source files //! for creating test data related to constant operations and field/property/parameter creation. +use std::sync::Arc; + use crate::{ metadata::{ signatures::TypeSignature, @@ -10,7 +12,6 @@ use crate::{ }, test::builders::{FieldBuilder, ParamBuilder, PropertyBuilder}, }; -use std::sync::Arc; /// Helper function to create a simple i4 field /// diff --git a/dotscope/src/test/mod.rs b/dotscope/src/test/mod.rs index e6e73ceb..1f4dae13 100644 --- a/dotscope/src/test/mod.rs +++ b/dotscope/src/test/mod.rs @@ -95,8 +95,8 @@ pub use crafted2::*; pub use helpers::*; pub use validator::*; pub use windowsbase::*; -//pub use scenarios::*; +//pub use scenarios::*; use crate::{ analysis::{SsaType, TypeProvider}, metadata::{signatures::SignatureLocalVariable, token::Token}, diff --git a/dotscope/src/test/mono/capabilities.rs b/dotscope/src/test/mono/capabilities.rs index 0bab4c68..f1ea590e 100644 --- a/dotscope/src/test/mono/capabilities.rs +++ b/dotscope/src/test/mono/capabilities.rs @@ -11,8 +11,10 @@ //! issues where architecture selection was static and didn't account for runtime //! limitations (e.g., .NET 8 SDK on 64-bit Windows cannot run x86 assemblies). -use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; +use std::{ + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; /// Available C# compiler types #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/dotscope/src/test/mono/compilation.rs b/dotscope/src/test/mono/compilation.rs index c91a5707..e97e21ab 100644 --- a/dotscope/src/test/mono/compilation.rs +++ b/dotscope/src/test/mono/compilation.rs @@ -3,12 +3,15 @@ //! This module handles compilation of C# source code to .NET assemblies using //! the detected compiler from TestCapabilities. +use std::{ + path::{Path, PathBuf}, + process::Command, +}; + use crate::{ prelude::*, test::mono::capabilities::{Architecture, Compiler, TestCapabilities}, }; -use std::path::{Path, PathBuf}; -use std::process::Command; /// Result of a compilation operation #[derive(Debug, Clone)] @@ -427,9 +430,10 @@ public class TestClass #[cfg(test)] mod tests { - use super::*; use tempfile::TempDir; + use super::*; + #[test] fn test_compilation_result() { let success = CompilationResult::success(PathBuf::from("/test/path.exe"), Compiler::Csc); diff --git a/dotscope/src/test/mono/disassembly.rs b/dotscope/src/test/mono/disassembly.rs index a30ce50d..1de46d74 100644 --- a/dotscope/src/test/mono/disassembly.rs +++ b/dotscope/src/test/mono/disassembly.rs @@ -3,12 +3,12 @@ //! This module handles disassembly of .NET assemblies using the detected //! disassembler from TestCapabilities. +use std::{path::Path, process::Command}; + use crate::{ prelude::*, test::mono::capabilities::{Disassembler, TestCapabilities}, }; -use std::path::Path; -use std::process::Command; /// Result of a disassembly operation #[derive(Debug, Clone)] @@ -275,9 +275,10 @@ pub fn verify( #[cfg(test)] mod tests { + use tempfile::TempDir; + use super::*; use crate::test::mono::compilation::{compile, templates}; - use tempfile::TempDir; #[test] fn test_disassembly_result() { diff --git a/dotscope/src/test/mono/execution.rs b/dotscope/src/test/mono/execution.rs index dce1da9a..288e5bb9 100644 --- a/dotscope/src/test/mono/execution.rs +++ b/dotscope/src/test/mono/execution.rs @@ -3,13 +3,16 @@ //! This module handles execution of .NET assemblies using the detected runtime //! from TestCapabilities. +use std::{ + path::Path, + process::{Command, Output}, + time::Duration, +}; + use crate::{ prelude::*, test::mono::capabilities::{Runtime, TestCapabilities}, }; -use std::path::Path; -use std::process::{Command, Output}; -use std::time::Duration; /// Result of executing an assembly #[derive(Debug, Clone)] @@ -305,9 +308,10 @@ impl ChildExt for std::process::Child { #[cfg(test)] mod tests { + use tempfile::TempDir; + use super::*; use crate::test::mono::compilation::{compile, templates}; - use tempfile::TempDir; #[test] fn test_execution_result() { diff --git a/dotscope/src/test/mono/mod.rs b/dotscope/src/test/mono/mod.rs index 744f0f54..875b0e5e 100644 --- a/dotscope/src/test/mono/mod.rs +++ b/dotscope/src/test/mono/mod.rs @@ -95,12 +95,13 @@ pub mod reflection; pub mod runner; // Re-export main types +use std::path::Path; + pub use capabilities::{Architecture, TestCapabilities}; pub use reflection::MethodTest; pub use runner::TestRunner; use crate::prelude::*; -use std::path::Path; /// Result of a complete test run for one architecture #[derive(Debug)] diff --git a/dotscope/src/test/mono/reflection.rs b/dotscope/src/test/mono/reflection.rs index 6bd9d5f8..da894420 100644 --- a/dotscope/src/test/mono/reflection.rs +++ b/dotscope/src/test/mono/reflection.rs @@ -4,6 +4,8 @@ //! to invoke methods in dotscope-modified assemblies. This validates that methods //! added or modified by dotscope are correctly callable at runtime. +use std::path::Path; + use crate::{ prelude::*, test::mono::{ @@ -12,7 +14,6 @@ use crate::{ execution::{execute, ExecutionResult}, }, }; -use std::path::Path; /// Result of a reflection test #[derive(Debug)] @@ -365,9 +366,10 @@ class Program #[cfg(test)] mod tests { + use tempfile::TempDir; + use super::*; use crate::test::mono::compilation::templates; - use tempfile::TempDir; #[test] fn test_method_test_builder() { diff --git a/dotscope/src/test/mono/runner.rs b/dotscope/src/test/mono/runner.rs index e5be9924..0c34a68e 100644 --- a/dotscope/src/test/mono/runner.rs +++ b/dotscope/src/test/mono/runner.rs @@ -3,12 +3,14 @@ //! This module provides the test runner infrastructure that coordinates compilation, //! execution, and verification of .NET assemblies across supported architectures. +use std::path::{Path, PathBuf}; + +use tempfile::TempDir; + use crate::{ prelude::*, test::mono::capabilities::{Architecture, TestCapabilities}, }; -use std::path::{Path, PathBuf}; -use tempfile::TempDir; /// Test runner that manages the test environment and coordinates test execution pub struct TestRunner { diff --git a/dotscope/src/test/validator.rs b/dotscope/src/test/validator.rs index f3d64574..7f1de214 100644 --- a/dotscope/src/test/validator.rs +++ b/dotscope/src/test/validator.rs @@ -17,6 +17,11 @@ //! - Centralized cleanup and error handling //! - Clear separation of concerns +use std::path::{Path, PathBuf}; + +use rayon::ThreadPoolBuilder; +use tempfile::NamedTempFile; + use crate::{ metadata::{ cilassemblyview::CilAssemblyView, @@ -26,9 +31,6 @@ use crate::{ }, Error, Result, }; -use rayon::ThreadPoolBuilder; -use std::path::{Path, PathBuf}; -use tempfile::NamedTempFile; /// Test assembly specification for validator testing. /// diff --git a/dotscope/src/utils/crypto.rs b/dotscope/src/utils/crypto.rs index 07b4641d..b9b417ce 100644 --- a/dotscope/src/utils/crypto.rs +++ b/dotscope/src/utils/crypto.rs @@ -26,9 +26,13 @@ use std::iter::repeat_n; use aes::Aes128; -use cbc::cipher::block_padding::{NoPadding, Pkcs7}; -use cbc::cipher::{BlockModeDecrypt, BlockModeEncrypt, KeyInit, KeyIvInit}; -use cbc::{Decryptor, Encryptor}; +use cbc::{ + cipher::{ + block_padding::{NoPadding, Pkcs7}, + BlockModeDecrypt, BlockModeEncrypt, KeyInit, KeyIvInit, + }, + Decryptor, Encryptor, +}; #[cfg(feature = "legacy-crypto")] use des::{Des, TdesEde3}; use ecb::{Decryptor as EcbDecryptor, Encryptor as EcbEncryptor}; @@ -1071,9 +1075,10 @@ mod tests { #[cfg(test)] #[cfg(feature = "legacy-crypto")] mod legacy_tests { - use crate::utils::crypto::{derive_pbkdf1_key, derive_pbkdf2_key}; use sha1::Digest; + use crate::utils::crypto::{derive_pbkdf1_key, derive_pbkdf2_key}; + #[test] fn test_pbkdf2_sha1_basic() { let password = b"password"; diff --git a/dotscope/src/utils/decompress.rs b/dotscope/src/utils/decompress.rs index 5c066131..a967c44c 100644 --- a/dotscope/src/utils/decompress.rs +++ b/dotscope/src/utils/decompress.rs @@ -6,10 +6,17 @@ //! //! # ConfuserEx LZMA Format //! -//! ConfuserEx uses a custom LZMA format: -//! - 5 bytes: LZMA decoder properties -//! - 4 bytes: Uncompressed size (little-endian i32) -//! - Rest: Compressed data stream +//! Two header layouts appear in the wild, sharing the same 5 property bytes and +//! differing only in the width of the uncompressed-size field that follows: +//! +//! - 5 bytes properties + 8 bytes size (little-endian u64) — the standard +//! `.lzma` (alone) header, written by builds that drive the LZMA SDK's stream +//! API directly +//! - 5 bytes properties + 4 bytes size (little-endian u32) — stock ConfuserEx's +//! `Lzma.Decompress` +//! +//! Nothing in the assembly records which is in use, so both are attempted and +//! each is held to the length its own header declares. //! //! # Deflate Format //! @@ -48,30 +55,33 @@ impl std::fmt::Display for DecompressError { impl std::error::Error for DecompressError {} -/// Checks if the given data appears to be ConfuserEx LZMA format. +/// Upper bound on a declared decompressed size, in bytes. /// -/// ConfuserEx LZMA format: -/// - 5 bytes: LZMA properties (first byte typically 0x5D for default settings) -/// - 4 bytes: Uncompressed size (little-endian i32, may be negative for unknown) -/// - Rest: Compressed LZMA stream +/// This is a guard against corrupt or hostile length fields driving a large +/// allocation, **not** a property of the format — LZMA itself allows any +/// `u64`. The bound only has to sit far above anything a real assembly +/// produces: a constants blob holds an assembly's literals, which run to +/// kilobytes for typical programs and single-digit megabytes for pathological +/// ones, so 512 MB leaves several orders of magnitude of headroom while still +/// rejecting a field that is plainly nonsense. +const MAX_DECOMPRESSED_SIZE: u64 = 512 * 1024 * 1024; + +/// Header layouts used by ConfuserEx and its forks. /// -/// # Arguments +/// Both start with the same 5 property bytes and differ only in the width of +/// the uncompressed-size field that follows. +const LZMA_HEADER_LAYOUTS: [usize; 2] = [ + 13, // 5 props + 8-byte size — the standard `.lzma` (alone) header + 9, // 5 props + 4-byte size — stock ConfuserEx's `Lzma.Decompress` +]; + +/// Validates the 5 LZMA property bytes shared by every supported layout. /// -/// * `data` - The potentially compressed data. -/// -/// # Returns -/// -/// `true` if the data appears to be ConfuserEx LZMA format. -#[must_use] -pub fn is_confuserex_lzma(data: &[u8]) -> bool { - if data.len() < 13 { - // Need at least 9 bytes header + some compressed data - return false; - } - - // LZMA properties byte: encodes lc, lp, pb parameters - // Valid range: 0-224 (9 * 5 * 5 - 1) - // ConfuserEx typically uses default settings: lc=3, lp=0, pb=2 -> 0x5D +/// Checks the properties byte encodes a legal `lc`/`lp`/`pb` triple and that +/// the dictionary size is one a real encoder would choose. +fn valid_lzma_props(data: &[u8]) -> bool { + // Properties byte encodes (pb * 5 + lp) * 9 + lc; the largest legal value + // is 9 * 5 * 5 - 1 = 224. let Some(&props_byte) = data.first() else { return false; }; @@ -79,46 +89,72 @@ pub fn is_confuserex_lzma(data: &[u8]) -> bool { return false; } - // For better heuristics, check if props byte matches common LZMA settings - // 0x5D is the most common (lc=3, lp=0, pb=2) - // But we allow any valid props for flexibility - - // Check dictionary size (bytes 1-4 of LZMA properties) - // ConfuserEx typically uses 1MB dictionary (0x00100000) + // Dictionary size: the SDK's presets span 64 KB to 64 MB depending on the + // fork's compression level, so accept a generous band around that. let Some(dict_bytes) = data.get(1..5).and_then(|s| <[u8; 4]>::try_from(s).ok()) else { return false; }; let dict_size = u32::from_le_bytes(dict_bytes); + (1024u32..=1024u32.saturating_mul(1024).saturating_mul(1024)).contains(&dict_size) +} - // Dictionary size must be reasonable: 1KB to 16MB - // Too small or too large suggests this isn't LZMA - if !(1024u32..=16u32.saturating_mul(1024).saturating_mul(1024)).contains(&dict_size) { - return false; - } - - // Bytes 5-8: Uncompressed size (little-endian i32) - // For ConfuserEx, this is typically a small positive number (the decrypted constants) - let Some(size_bytes) = data.get(5..9).and_then(|s| <[u8; 4]>::try_from(s).ok()) else { - return false; +/// Reads the declared uncompressed size for a given header length. +/// +/// Returns `None` when the buffer is too short, or the size is zero or beyond +/// [`MAX_DECOMPRESSED_SIZE`]. An all-ones field means "unknown" and maps to +/// `Some(None)` — plausible, but with no length to verify against. +fn declared_size(data: &[u8], header_len: usize) -> Option> { + let size = match header_len { + 13 => u64::from_le_bytes(data.get(5..13)?.try_into().ok()?), + _ => u64::from(u32::from_le_bytes(data.get(5..9)?.try_into().ok()?)), }; - let uncompressed_size = i32::from_le_bytes(size_bytes); + // `-1` in either width signals an unknown length + if size == u64::MAX || size == u64::from(u32::MAX) { + return Some(None); + } + if size == 0 || size > MAX_DECOMPRESSED_SIZE { + return None; + } + Some(Some(size)) +} - // Uncompressed size should be positive and reasonable (< 10MB) - // Negative or very large sizes indicate this isn't LZMA data - if uncompressed_size <= 0 || uncompressed_size > 10i32.saturating_mul(1024).saturating_mul(1024) - { +/// Checks if the given data appears to be ConfuserEx LZMA format. +/// +/// Two header layouts are recognised, both sharing the same 5 property bytes +/// and differing in the width of the uncompressed-size field: +/// +/// - 5 bytes properties + 8 bytes size — the standard `.lzma` (alone) header, +/// used by forks that call the LZMA SDK's stream API directly. +/// - 5 bytes properties + 4 bytes size — stock ConfuserEx's `Lzma.Decompress`. +/// +/// # Arguments +/// +/// * `data` - The potentially compressed data. +/// +/// # Returns +/// +/// `true` if the data appears to be ConfuserEx LZMA format. +#[must_use] +pub fn is_confuserex_lzma(data: &[u8]) -> bool { + if data.len() < 13 { + // Need at least the largest header plus some compressed data return false; } - // Additional check: compressed data should be smaller than uncompressed - // (otherwise why compress it?) - let compressed_data_len = data.len().saturating_sub(9); // minus header - if compressed_data_len > uncompressed_size.cast_unsigned() as usize { - // Compressed larger than uncompressed - suspicious + if !valid_lzma_props(data) { return false; } - true + // Accept if either header layout yields a plausible declared size. + // + // Deliberately *not* checked: that the compressed payload is smaller than + // the decompressed output. LZMA expands small high-entropy inputs, and the + // constants blob is XOR-encrypted before compression — so a 44-byte blob + // routinely compresses to more than 44 bytes. Requiring shrinkage rejected + // exactly the assemblies this hook exists to handle. + LZMA_HEADER_LAYOUTS + .iter() + .any(|&len| declared_size(data, len).is_some()) } /// Decompresses ConfuserEx LZMA data. @@ -133,47 +169,67 @@ pub fn is_confuserex_lzma(data: &[u8]) -> bool { /// /// # Format /// +/// Two layouts are attempted, sharing the same 5 property bytes and differing +/// in the width of the uncompressed-size field: +/// /// ```text /// [0..5] : LZMA properties (5 bytes) -/// [5..9] : Uncompressed size (4 bytes, little-endian i32) +/// [5..13] : Uncompressed size (8 bytes, little-endian u64) -- standard `.lzma` +/// [13..] : LZMA compressed stream +/// +/// [0..5] : LZMA properties (5 bytes) +/// [5..9] : Uncompressed size (4 bytes, little-endian u32) -- stock ConfuserEx /// [9..] : LZMA compressed stream /// ``` +/// +/// The layout is not recorded anywhere in the assembly, so both are tried and +/// the first that decodes to the length its own header declares wins. Guessing +/// wrong shifts the payload by four bytes and corrupts the range coder, so a +/// successful decode is itself the discriminator. pub fn decompress_confuserex_lzma(data: &[u8]) -> DecompressResult> { if data.len() < 9 { return Err(DecompressError::BufferTooSmall); } + if !valid_lzma_props(data) { + return Err(DecompressError::InvalidLzmaHeader); + } - // Parse header let props = data.get(0..5).ok_or(DecompressError::BufferTooSmall)?; - let size_bytes = data - .get(5..9) - .and_then(|s| <[u8; 4]>::try_from(s).ok()) - .ok_or(DecompressError::BufferTooSmall)?; - let uncompressed_size = i32::from_le_bytes(size_bytes); - let compressed = data.get(9..).ok_or(DecompressError::BufferTooSmall)?; - - // Build LZMA stream header for lzma-rs - // lzma-rs expects: 5 bytes props + 8 bytes uncompressed size (little-endian u64) + compressed data - let mut lzma_stream = Vec::with_capacity(compressed.len().saturating_add(13)); - lzma_stream.extend_from_slice(props); - - // Convert i32 to u64 for lzma-rs format - let size_u64 = if uncompressed_size < 0 { - u64::MAX // Unknown size - } else { - u64::from(uncompressed_size.cast_unsigned()) - }; - lzma_stream.extend_from_slice(&size_u64.to_le_bytes()); - lzma_stream.extend_from_slice(compressed); - - // Decompress using lzma-rs - let mut cursor = Cursor::new(&lzma_stream); - let mut decompressed = Vec::new(); + let mut last_err: Option = None; + + for &header_len in &LZMA_HEADER_LAYOUTS { + let Some(size) = declared_size(data, header_len) else { + continue; + }; + let Some(compressed) = data.get(header_len..) else { + continue; + }; + if compressed.is_empty() { + continue; + } - lzma_rs::lzma_decompress(&mut cursor, &mut decompressed) - .map_err(|e| DecompressError::LzmaError(e.to_string()))?; + // lzma-rs expects the alone format: 5 props + 8-byte size + payload. + let mut lzma_stream = Vec::with_capacity(compressed.len().saturating_add(13)); + lzma_stream.extend_from_slice(props); + lzma_stream.extend_from_slice(&size.unwrap_or(u64::MAX).to_le_bytes()); + lzma_stream.extend_from_slice(compressed); + + let mut cursor = Cursor::new(&lzma_stream); + let mut decompressed = Vec::new(); + match lzma_rs::lzma_decompress(&mut cursor, &mut decompressed) { + Ok(()) => { + // A wrong layout can still decode into garbage of the wrong + // length; hold the result to its declared size when known. + if size.is_none_or(|expected| decompressed.len() as u64 == expected) { + return Ok(decompressed); + } + last_err = Some(DecompressError::InvalidLzmaHeader); + } + Err(e) => last_err = Some(DecompressError::LzmaError(e.to_string())), + } + } - Ok(decompressed) + Err(last_err.unwrap_or(DecompressError::InvalidLzmaHeader)) } /// Decompresses Deflate data using flate2. @@ -220,7 +276,10 @@ pub fn decompress_gzip(data: &[u8]) -> DecompressResult> { mod tests { use std::io::Write; - use flate2::{write::DeflateEncoder, write::GzEncoder, Compression}; + use flate2::{ + write::{DeflateEncoder, GzEncoder}, + Compression, + }; use super::*; @@ -255,6 +314,71 @@ mod tests { assert!(!is_confuserex_lzma(&too_small)); } + /// A real ConfuserEx constants blob, taken from an assembly produced by a + /// fork that writes the standard 13-byte `.lzma` header (5 property bytes + /// followed by an 8-byte size) rather than stock ConfuserEx's 9-byte one. + /// + /// 51 bytes of payload decode to 44 bytes of output — the compressed form + /// is *larger* than the decompressed form, because the constants blob is + /// XOR-encrypted before compression and LZMA cannot shrink high-entropy + /// input that small. + const FORK_BLOB: [u8; 64] = [ + 0x5D, 0x00, 0x00, 0x80, 0x00, // props: lc=3 lp=0 pb=2, 8 MB dictionary + 0x2C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // size: 44, as u64 + 0x00, 0x04, 0x00, 0x35, 0x03, 0xA1, 0xBC, 0x67, 0x7D, 0x8E, 0xD0, 0x35, 0x60, 0x52, 0x59, + 0x6E, 0x4A, 0xF6, 0x76, 0x12, 0xF7, 0xD1, 0x80, 0xD2, 0xA5, 0xEA, 0x78, 0xC3, 0x73, 0x0E, + 0x4B, 0x7C, 0xD8, 0x8E, 0xF4, 0xE6, 0x1C, 0x93, 0x81, 0x81, 0x68, 0xCC, 0xEC, 0x3A, 0x04, + 0x8E, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + #[test] + fn test_confuserex_lzma_standard_13_byte_header() { + assert!(is_confuserex_lzma(&FORK_BLOB)); + + let out = decompress_confuserex_lzma(&FORK_BLOB).unwrap(); + assert_eq!(out.len(), 44, "must honour the declared size"); + // Length-prefixed UTF-8 constants, as ConfuserEx stores them. + assert!(out.windows(8).any(|w| w == b"Result: ")); + assert!(out.windows(27).any(|w| w == b"Hello From ConfuserEx test.")); + } + + #[test] + fn test_confuserex_lzma_accepts_payload_larger_than_output() { + // Guards a heuristic that used to reject any blob whose payload was + // bigger than its declared output, on the reasoning that compression + // shrinks data. It does not, for small high-entropy inputs — and that + // rejected precisely the assemblies this path exists to decompress. + assert!(FORK_BLOB.len() - 13 > 44); + assert!(is_confuserex_lzma(&FORK_BLOB)); + } + + #[test] + fn test_confuserex_lzma_stock_9_byte_header() { + // Stock ConfuserEx writes the size as 4 bytes, putting the payload at + // offset 9. Rebuild that layout from an alone-format stream. + let original = b"ConfuserEx constants blob, repeated repeated repeated repeated."; + let mut alone = Vec::new(); + lzma_rs::lzma_compress(&mut Cursor::new(&original[..]), &mut alone).unwrap(); + + let mut stock = Vec::with_capacity(alone.len()); + stock.extend_from_slice(&alone[0..5]); + stock.extend_from_slice(&(original.len() as u32).to_le_bytes()); + stock.extend_from_slice(&alone[13..]); + + assert!(is_confuserex_lzma(&stock)); + assert_eq!(decompress_confuserex_lzma(&stock).unwrap(), original); + } + + #[test] + fn test_confuserex_lzma_rejects_non_lzma() { + // Plausible length, but the dictionary size is nonsense. + let junk = [ + 0x5D, 0x11, 0x22, 0x33, 0x44, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + assert!(!is_confuserex_lzma(&junk)); + assert!(decompress_confuserex_lzma(&junk).is_err()); + } + #[test] fn test_decompress_deflate() { let original = b"Hello, World! This is a test of deflate compression."; diff --git a/dotscope/src/utils/enums.rs b/dotscope/src/utils/enums.rs index 0ccf86b3..982895f5 100644 --- a/dotscope/src/utils/enums.rs +++ b/dotscope/src/utils/enums.rs @@ -4,6 +4,8 @@ //! and parsing enum values from binary data. It's used by custom attributes, security //! permissions, and other metadata parsers that need to handle enum values. +use std::sync::Arc; + use crate::{ file::parser::Parser, metadata::{ @@ -12,7 +14,6 @@ use crate::{ }, ParseFailure, ParseStage, Result, }; -use std::sync::Arc; /// Utilities for working with .NET enum types pub struct EnumUtils; diff --git a/dotscope/src/utils/mod.rs b/dotscope/src/utils/mod.rs index 6e34ea76..802ed31f 100644 --- a/dotscope/src/utils/mod.rs +++ b/dotscope/src/utils/mod.rs @@ -60,6 +60,23 @@ pub use alignment::align_to; #[cfg(feature = "emulation")] pub use base64::{base64_decode, base64_encode}; pub use compression::compressed_uint_size; +#[cfg(all(feature = "deobfuscation", feature = "legacy-crypto"))] +pub(crate) use crypto::derive_key_iv; +#[cfg(all(feature = "legacy-crypto", feature = "emulation"))] +pub(crate) use crypto::derive_pbkdf1_key; +#[cfg(feature = "deobfuscation")] +pub(crate) use crypto::CryptoParameters; +#[cfg(feature = "emulation")] +pub(crate) use crypto::{apply_crypto_transform, derive_pbkdf2_key, verify_rsa_pkcs1v15}; +#[cfg(feature = "emulation")] +pub(crate) use crypto::{compute_hmac_sha256, compute_hmac_sha512}; +#[cfg(feature = "legacy-crypto")] +pub(crate) use crypto::{compute_md5, compute_sha1}; +pub(crate) use crypto::{compute_sha256, compute_sha384, compute_sha512}; +#[allow(unused_imports)] +pub(crate) use decompress::{ + decompress_confuserex_lzma, decompress_deflate, decompress_gzip, is_confuserex_lzma, +}; pub use dot::escape_dot; pub use enums::EnumUtils; pub use hash::{hash_blob, hash_guid, hash_string}; @@ -80,22 +97,3 @@ pub use math::to_i32_saturating; pub use math::to_u32; pub use synchronization::FailFastBarrier; pub use visitedmap::VisitedMap; - -#[cfg(all(feature = "deobfuscation", feature = "legacy-crypto"))] -pub(crate) use crypto::derive_key_iv; -#[cfg(all(feature = "legacy-crypto", feature = "emulation"))] -pub(crate) use crypto::derive_pbkdf1_key; -#[cfg(feature = "deobfuscation")] -pub(crate) use crypto::CryptoParameters; -#[cfg(feature = "emulation")] -pub(crate) use crypto::{apply_crypto_transform, derive_pbkdf2_key, verify_rsa_pkcs1v15}; -#[cfg(feature = "emulation")] -pub(crate) use crypto::{compute_hmac_sha256, compute_hmac_sha512}; -#[cfg(feature = "legacy-crypto")] -pub(crate) use crypto::{compute_md5, compute_sha1}; -pub(crate) use crypto::{compute_sha256, compute_sha384, compute_sha512}; - -#[allow(unused_imports)] -pub(crate) use decompress::{ - decompress_confuserex_lzma, decompress_deflate, decompress_gzip, is_confuserex_lzma, -}; diff --git a/dotscope/src/utils/synchronization.rs b/dotscope/src/utils/synchronization.rs index 843ebc2c..5cec1410 100644 --- a/dotscope/src/utils/synchronization.rs +++ b/dotscope/src/utils/synchronization.rs @@ -284,10 +284,9 @@ impl FailFastBarrier { #[cfg(test)] mod tests { + use std::{sync::Arc, thread, time::Duration}; + use super::*; - use std::sync::Arc; - use std::thread; - use std::time::Duration; #[test] fn test_normal_barrier_operation() { diff --git a/dotscope/tests/builders.rs b/dotscope/tests/builders.rs index b37d6979..cc722f13 100644 --- a/dotscope/tests/builders.rs +++ b/dotscope/tests/builders.rs @@ -3,9 +3,10 @@ //! This module tests realistic scenarios where multiple builders are used together //! to create complete .NET types with properties, events, and methods. -use dotscope::{metadata::tables::TableId, prelude::*, ChangeRefKind, Result}; use std::path::PathBuf; +use dotscope::{metadata::tables::TableId, prelude::*, ChangeRefKind, Result}; + fn get_test_assembly() -> Result { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/samples/WindowsBase.dll"); CilAssembly::from_path(&path) diff --git a/dotscope/tests/common/compatibility.rs b/dotscope/tests/common/compatibility.rs index 34d478dd..163527e5 100644 --- a/dotscope/tests/common/compatibility.rs +++ b/dotscope/tests/common/compatibility.rs @@ -15,14 +15,15 @@ #![allow(dead_code)] -use dotscope::{project::ProjectLoader, CilAssemblyView}; -use rayon::prelude::*; use std::{ collections::HashMap, fs, path::{Path, PathBuf}, }; +use dotscope::{project::ProjectLoader, CilAssemblyView}; +use rayon::prelude::*; + /// Result of loading an assembly with a specific loader. #[derive(Debug, Clone)] pub struct LoadResult { diff --git a/dotscope/tests/common/framework.rs b/dotscope/tests/common/framework.rs index ca719588..02b15670 100644 --- a/dotscope/tests/common/framework.rs +++ b/dotscope/tests/common/framework.rs @@ -16,16 +16,17 @@ use std::{fmt::Debug, sync::Arc}; -use super::verification::{ - assert_deobfuscation_diagnostics, assert_structural_match, verify_semantic_preservation, - AssemblyStats, SemanticVerificationResult, StructuralConfig, VerificationLevel, -}; use dotscope::{ deobfuscation::{DeobfuscationEngine, DeobfuscationResult, EngineConfig}, metadata::validation::ValidationConfig, CilObject, }; +use super::verification::{ + assert_deobfuscation_diagnostics, assert_structural_match, verify_semantic_preservation, + AssemblyStats, SemanticVerificationResult, StructuralConfig, VerificationLevel, +}; + /// The 17 standard methods used for semantic preservation checks across all /// obfuscator test suites. Each obfuscator's original.exe contains these /// methods and every deobfuscated output is compared against them. diff --git a/dotscope/tests/modify_add.rs b/dotscope/tests/modify_add.rs index 0dbdf60a..a3c4e0fc 100644 --- a/dotscope/tests/modify_add.rs +++ b/dotscope/tests/modify_add.rs @@ -12,8 +12,7 @@ //! These tests verify the complete end-to-end functionality of writing //! modified assemblies to disk and ensuring they can be loaded back correctly. -use dotscope::prelude::*; -use dotscope::ChangeRefKind; +use dotscope::{prelude::*, ChangeRefKind}; #[test] fn extend_crafted_2() -> Result<()> { diff --git a/dotscope/tests/modify_impexp.rs b/dotscope/tests/modify_impexp.rs index a80b5474..02421f50 100644 --- a/dotscope/tests/modify_impexp.rs +++ b/dotscope/tests/modify_impexp.rs @@ -13,8 +13,7 @@ //! native PE imports and exports to assemblies, writing them to disk, //! and ensuring they can be loaded back correctly with the modifications intact. -use dotscope::prelude::*; -use dotscope::DataDirectoryType; +use dotscope::{prelude::*, DataDirectoryType}; #[test] fn test_native_imports_with_minimal_changes() -> Result<()> { diff --git a/dotscope/tests/modify_roundtrips_crafted2.rs b/dotscope/tests/modify_roundtrips_crafted2.rs index b8c2d5a8..ee3b5b3c 100644 --- a/dotscope/tests/modify_roundtrips_crafted2.rs +++ b/dotscope/tests/modify_roundtrips_crafted2.rs @@ -16,9 +16,10 @@ //! All tests use only the public API exported in the prelude to ensure they represent //! actual user usage patterns. -use dotscope::prelude::*; use std::path::Path; +use dotscope::prelude::*; + const TEST_ASSEMBLY_PATH: &str = "tests/samples/mono_4.8/mscorlib.dll"; /// Helper function to create a test assembly for integration testing diff --git a/dotscope/tests/modify_roundtrips_method.rs b/dotscope/tests/modify_roundtrips_method.rs index e375d05c..59c5b265 100644 --- a/dotscope/tests/modify_roundtrips_method.rs +++ b/dotscope/tests/modify_roundtrips_method.rs @@ -15,6 +15,8 @@ //! 3. Both original and injected methods can be disassembled //! 4. Method bodies are preserved correctly +use std::path::Path; + use dotscope::{ metadata::{ signatures::{encode_method_signature, SignatureMethod, SignatureParameter, TypeSignature}, @@ -24,7 +26,6 @@ use dotscope::{ prelude::*, ChangeRefKind, ChangeRefRc, }; -use std::path::Path; const TEST_ASSEMBLY_PATH: &str = "tests/samples/mono_4.8/mscorlib.dll"; diff --git a/dotscope/tests/modify_roundtrips_wbdll.rs b/dotscope/tests/modify_roundtrips_wbdll.rs index 85d764f5..bda206e0 100644 --- a/dotscope/tests/modify_roundtrips_wbdll.rs +++ b/dotscope/tests/modify_roundtrips_wbdll.rs @@ -16,9 +16,10 @@ //! 4. Loading the written bytes again //! 5. Verifying changes are correctly persisted -use dotscope::prelude::*; use std::path::PathBuf; +use dotscope::prelude::*; + const TEST_ASSEMBLY_PATH: &str = "tests/samples/WindowsBase.dll"; /// Helper function to get test assembly path diff --git a/dotscope/tests/netreactor.rs b/dotscope/tests/netreactor.rs index 55c0dbe3..e830c15d 100644 --- a/dotscope/tests/netreactor.rs +++ b/dotscope/tests/netreactor.rs @@ -459,9 +459,26 @@ fn test_all_netreactor_samples() { continue; } + // Over-cleanup guard. Virtualized samples are not devirtualized by any + // technique, so the VM interpreter and its handler types remain live + // code — the stubs left in the virtualized methods still call into + // them. Cleanup reachability analysis must follow candidate-to-candidate + // call edges transitively to see that; a single-step version reads the + // handler cluster as isolated infrastructure and strips the assembly + // down to its application methods (854 -> 45 on reactor_virtualization). + if result.success && result.sample.expected_protections.has_virtualization { + assert!( + result.methods_after * 2 > result.methods_before, + "{}: cleanup removed {} of {} methods — the VM runtime is still \ + referenced by the virtualized method stubs and must survive", + result.sample.filename, + result.methods_before.saturating_sub(result.methods_after), + result.methods_before + ); + } + // Once detection is implemented, add assertions here: // - Verify correct technique detection per sample - // - Verify deobfuscation success // - Verify semantic preservation } } diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..8a6f6311 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,13 @@ +# Enforces the import convention mechanically rather than by review: +# +# 1. std / core / alloc +# 2. external crates +# 3. crate:: / self:: / super:: +# +# separated by blank lines, with one grouped `use` per root. +# +# Both options are nightly-only, so formatting runs as `cargo +nightly fmt`. +# This affects formatting only — the crate still builds on the stable toolchain +# named in `rust-version`. +group_imports = "StdExternalCrate" +imports_granularity = "Crate"