Compare commits

...

122 commits

Author SHA1 Message Date
dependabot[bot]
a8a5cabdfa
rust: bump pyo3 from 0.29.0 to 0.29.2 (#6800)
Some checks failed
CI / Rust Check (push) Failing after 2s
CI / rust_test (ubuntu-latest) (push) Failing after 2s
Test with coverage / Test-6 (push) Has been skipped
Test with coverage / Test-7 (push) Has been skipped
Test with coverage / Build (push) Failing after 2s
Test with coverage / Test (push) Has been skipped
Test with coverage / Test-1 (push) Has been skipped
Test with coverage / Test-2 (push) Has been skipped
Test with coverage / Test-3 (push) Has been skipped
Test with coverage / Test-4 (push) Has been skipped
Test with coverage / Test-5 (push) Has been skipped
Test with coverage / Test-8 (push) Has been skipped
Test with coverage / Test-9 (push) Has been skipped
Test with coverage / Report (push) Has been skipped
Test with coverage / Test Rust packages (push) Failing after 2s
CI / ci (push) Has been cancelled
CI / Test installation (macos-26, py3.12) (push) Has been cancelled
CI / Test installation (ubuntu-24.04, py3.14) (push) Has been cancelled
CI / Test installation (windows-2025, py3.12) (push) Has been cancelled
CI / rust_test (macos-latest) (push) Has been cancelled
CI / rust_test (windows-latest) (push) Has been cancelled
Bumps [pyo3](https://github.com/pyo3/pyo3) from 0.29.0 to 0.29.2.
- [Release notes](https://github.com/pyo3/pyo3/releases)
- [Changelog](https://github.com/PyO3/pyo3/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pyo3/pyo3/compare/v0.29.0...v0.29.2)

---
updated-dependencies:
- dependency-name: pyo3
  dependency-version: 0.29.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 06:22:01 -07:00
dependabot[bot]
2165b8e2a5
ci: bump Swatinem/rust-cache from 2.9.1 to 2.9.2 (#6801)
Bumps [Swatinem/rust-cache](https://github.com/swatinem/rust-cache) from 2.9.1 to 2.9.2.
- [Release notes](https://github.com/swatinem/rust-cache/releases)
- [Changelog](https://github.com/Swatinem/rust-cache/blob/master/CHANGELOG.md)
- [Commits](c19371144d...6323deb102)

---
updated-dependencies:
- dependency-name: Swatinem/rust-cache
  dependency-version: 2.9.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 05:56:49 -07:00
dependabot[bot]
09e59e4204
ci: bump taiki-e/install-action from 2.85.5 to 2.85.10 (#6799)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.85.5 to 2.85.10.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](6a1bd70eaa...6c6fd71fe4)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.85.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 05:56:43 -07:00
Yan Shoshitaishvili
55530509ce
SimLinux: Stop pre-growing the stack past address zero. (#6806)
SimLinux.state_blank pre-grows the stack by a fixed 0x20 pages without
checking that 0x20 pages exist beneath the stack pointer. When they do
not, the allocation loop wraps past address 0 and hands out the remainder
at the top of the address space.

On x86-64 that is silent: blank_state(stack_end=0x10000) maps sixteen
stack pages from 0x0 up and sixteen more from 0xfffffffffffff000 down.
Where the wrap reaches a page the same call already handed out, the state
fails with SimSegfaultException("stack collided with heap") instead, and
where it reaches the loaded image it replaces it with blank pages.

Skip the pre-grow when that much space does not exist. Clamping it to the
space that does exist is not an option: the pre-allocated pages are not
backed by the loader, so a stack that reaches down to an image beneath it
hides that image. The pages that are skipped are still faulted in on
demand.

Also reject an allocation that does not fit beneath the top of the stack
in allocate_stack_pages() itself, so a caller that asks for one gets an
error rather than pages at the top of the address space.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 05:55:58 -07:00
Fish
503b1be066
SLiveness: Drop the redundant successors/predecessors caches. (#6843) 2026-08-14 03:13:58 -04:00
Yan Shoshitaishvili
2c95eb92e6
CFG: Warn when the regions to analyze cover no bytes. (#6825) 2026-08-13 17:26:22 -05:00
Fish
b80f4cf2af
Outliner: Rebuild phi statements; update output phis; return only region-defined vvars. (#6831)
* Rebuild phi statements instead of mutating them in place

* Update the phis of every dispatcher target

* Return only the variables the outlined region defines
2026-08-13 13:51:21 -07:00
Fish
41a7db2f17
SLiveness: propagate to predecessors instead of re-walking the graph (#6830) 2026-08-13 12:38:23 -07:00
Yan Shoshitaishvili
2bfaa7e8f7
CFGModel: Stop aborting when tidying a data reference at an unmapped address (#6811)
Fix #6770.
2026-08-11 13:27:06 -07:00
Fish
0c293dc0de
Phoenix: Fix incorrect virtualization of orphaned edges. (#6790) 2026-08-09 02:00:48 -07:00
Fish
ede5faf162
Decompiler: Add edits layer; add more tools to MCP. (#6789) 2026-08-09 01:48:07 -07:00
Fish
bdc1321845
RustCodeGen: Add more handlers. (#6788) 2026-08-08 09:28:22 -07:00
Fish
666fadabc5
Dephication: Keep the remapped assignment destination with source is not a vvar. (#6787) 2026-08-07 23:50:03 -07:00
Fish
fdd66945ea
CFGTransformationMixin: Rebuild conditional jumps when replacing branch targets. (#6786) 2026-08-07 23:46:34 -07:00
Fish
b0e3541dfd
SwitchClusterSimplifier: Do not merge different default nodes. (#6782) 2026-08-07 19:13:10 -07:00
Fish
780a80de79
Phoenix: Correct Loop.continue_addr. (#6785) 2026-08-07 18:41:56 -07:00
Fish
c751678d55
Decompiler: Handle compare-and-swap statements that CASIntrinsics leaves behind. (#6783) 2026-08-07 18:13:32 -07:00
Fish
08c7e48d5b
CFGFast: Mark bad blocks found by complete scanning as nodecode. (#6781) 2026-08-07 14:16:21 -07:00
Fish
c844a13eac
Clinic: Skip unnecessary simplification runs and recomputations. (#6780)
* Clinic: Skip the second pre-SSA-level1 _simplify_function when it cannot help.

* Clinic: Merge the two identical post-SSA-level1 _simplify_function calls.

* AILSimplifier, SLiveness: Skip recomputation that cannot change anything.
2026-08-07 12:06:49 -07:00
Fish
795eadfe84
Decompiler: Deterministically pick variables from sets. (#6778)
* Decompiler: Deterministically pick variables from sets.

* Add missing file.
2026-08-06 20:12:43 -05:00
Fish
0f4be85db4
Make CallSiteMaker a normal class instead of an Analysis. (#6776) 2026-08-06 08:44:29 -04:00
Fish
04b40a1eec
InlinedStringTransformationSimplifier: Pre-filter loops before symbolic execution. (#6775) 2026-08-05 20:00:09 -07:00
Fish
4018e26076
ExpressionNarrower: Compare rebuilt operands by value instead of identity. (#6774) 2026-08-05 22:34:33 -04:00
Fish
e76c4118a7
AILSimplifier: Only store replaced blocks into self.blocks. (#6773)
_rebuild_func_graph marks all blocks in self.blocks dirty, so adding unchanged blocks to self.blocks led to redundant
simplification runs after.
2026-08-05 22:34:13 -04:00
Fish
7d3c7c82ce
AILSimplifier: De-quadratify _unify_local_variables. (#6772) 2026-08-05 22:33:55 -04:00
Fish
b948687876
AIL: Fix identity comparisons against re-read statement attributes. (#6771) 2026-08-05 19:42:30 -05:00
Md7
1c99579f69
Fix #6744: avoid empty Or expression in memory.find (#6750) 2026-08-05 04:01:10 -07:00
angr-bot
f62f45363c Update version to 9.3.3.dev0 [ci skip] 2026-08-05 09:02:54 +00:00
Fish
3333f39fff
CFGFast: Make the smart scan nodecode ratio O(log n) (#6767) 2026-08-05 01:42:45 -07:00
Fish
7c2e3501e6
SimStruct: Remove _arch_memo to fix the cache leak. (#6757)
* SimStruct: Remove _arch_memo to fix the cache leak.

* Fix the comment

* Better typing.

* Fix RustSimTypes.

* More fixes.

* Fix caching for anonymous structs.

* SimUnion: Cache alignment.

* Convert returnty to a arch-ed returnty.

* RustSimType: Do not shadow SimType.with_arch.

* Rust: Bind an arch to types before they reach the type solver.

* RustSimType: Make it a subclass of SimType.
2026-08-05 01:12:55 -07:00
Yan Shoshitaishvili
f160178837
reaching_definitions: reject mismatched conversion widths (#6749)
* reaching_definitions: reject mismatched conversion widths

* Tests: satisfy RDA lint and type checks
2026-08-05 00:26:01 -07:00
Fish
71bf42c9b8
Decompiler: Test common C conditions. (#6762)
* Decompiler: Test common C conditions.

* Improve c-style null compatison implementation

* Make order-sensitive

---------

Co-authored-by: Kevin Phoenix <kevin@kphoenix.us>
2026-08-04 18:27:09 -07:00
Kevin Phoenix
fd235fcb90
icicle: Disable inspect and actions during memory sync-back (#6761) 2026-08-04 10:51:07 -07:00
Kevin Phoenix
f4b23d4444
UltraPage: Write to concrete_data as a single block instead of loop (#6760) 2026-08-04 10:11:26 -07:00
Kevin Phoenix
4a9c1454dc
icicle: Remove double-underscore methods (#6759) 2026-08-04 09:58:57 -07:00
Kevin Phoenix
233724f38e
icicle: Avoid repeatedly calling memory.permissions (#6758) 2026-08-04 09:54:45 -07:00
Fish
551df09fc5
Decompiler: Fix duplicate struct defs for pre-defined structs. (#6756) 2026-08-03 22:12:47 -07:00
Kevin Phoenix
f71b07cebf
CodeGen: Skip parenthesis on binops when representing as an unop (#6755) 2026-08-03 21:11:36 -07:00
pre-commit-ci[bot]
f9c789ff6a
[pre-commit.ci] pre-commit autoupdate (#6754)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.16.0 → v0.16.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.16.0...v0.16.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-08-03 11:15:42 -07:00
dependabot[bot]
15757fb2da
ci: bump taiki-e/install-action from 2.85.2 to 2.85.5 (#6753)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.85.2 to 2.85.5.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](41049aa566...6a1bd70eaa)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.85.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-03 09:37:54 -07:00
Yibo Liu
0f8e082f9a
Clinic: recover variable-length arrays (VLAs) (#6634) 2026-08-01 13:41:12 -07:00
Kevin Phoenix
49434bc3fc
Render truncations to non-C widths as masks instead of casts (#6741) 2026-07-31 17:05:47 -07:00
Kevin Phoenix
2f891d1d69
Update capstone to 5.0.9 (#6740) 2026-07-31 13:47:32 -07:00
Quintin Kong
d46e56f891
Fix signed division and remainder in the pcode engine (#6739)
* Fix signed division and remainder in the pcode engine

OpBehaviorIntSdiv and OpBehaviorIntSrem used Claripy's `/` and `%`, which are
unsigned bit-vector operations. For negative operands they therefore produced
the same results as the unsigned INT_DIV and INT_REM behaviors.

INT_SDIV now uses claripy.SDiv (truncation toward zero). INT_SREM is defined as
in1 - SDiv(in1, in2) * in2, giving a remainder with the dividend's sign, which
matches the p-code semantics documented in the class comments.

For 64-bit -5 and 2, INT_SDIV now yields -2 (0xfffffffffffffffe) and INT_SREM
yields -1 (0xffffffffffffffff) instead of large unsigned values.

The arithmetic behavior test table enables both INT_SDIV and INT_SREM with the
matching signed reference expressions, and a new concrete test checks mixed-sign
combinations (-5/2, 5/-2, -5/-2, ...) that an unsigned implementation cannot
satisfy.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Use claripy.SMod for INT_SREM

Per review, INT_SREM uses claripy.SMod directly instead of the equivalent
in1 - claripy.SDiv(in1, in2) * in2. Verified identical to a truncated-toward-zero
reference over 100k random 64-bit pairs, including the INT_MIN / -1 corner.

* Address pcode signed arithmetic review comments

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-31 12:23:05 -07:00
Fish
61bac8ffd0
SimConstantVariable: Fix overflows and value out of range errors. (#6738)
* SimConstantVariable: Fix overflows and value out of range errors.

* Fix usages of SimConstantVariable.

* Mask the value.

* Fix negative values.
2026-07-29 13:58:20 -07:00
pre-commit-ci[bot]
d38cc5a019
[pre-commit.ci] pre-commit autoupdate (#6721)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.15.22 → v0.16.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.22...v0.16.0)

* Apply fixes

* Add values()

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Kevin Phoenix <kevin@kphoenix.us>
2026-07-29 13:46:11 -07:00
angr-bot
a4b05a0ce9 Update version to 9.3.2.dev0 [ci skip] 2026-07-29 09:44:55 +00:00
Fish
7cedfbfa8b
Dephication: Correctly consider phi congruence classes. (#6735) 2026-07-29 01:53:02 -07:00
Fish
b9358da5da
AIL: Fix __eq__. (#6728)
* AIL: Merge likes/matches into a single mode-parameterized walk

* AIL: Make __eq__ idx-aware at every node, not just the root

* AIL: Stop hashing fields that equality does not compare

* AIL: Regression-test the hash/eq contract

* AIL: Compare bits in StringLiteral and Struct

* AIL: Replace the CMP_* constants with a CmpMode enum

* AIL: cargo fmt

* AIL: Compare and hash rounding_mode on Convert and BinaryOp

* Update comments.
2026-07-29 01:22:10 -07:00
Ati Priya
6933b065f0
Decompiler: rewrite CondBE and CondNB ccalls on amd64 (#6645)
* Decompiler: rewrite CondBE and CondNB ccalls on amd64

* Decompiler: fix inverted CondZ/CondNZ over G_CC_OP_COPY on amd64

* tests: add binary-driven regressions for CondBE/CondNB ccall recovery
2026-07-29 01:11:09 -07:00
Quintin Kong
508ac3a44c
aarch64: fix adcs/sbcs carry flag (select on cc_dep3, not cc_dep2) (#6702)
arm64g_calculate_flag_c selected the ADC*/SBC* carry-in with `cc_dep2 != 0`
(the second operand). The arm64 flag thunk layout puts the old carry in
cc_dep3 (angr's own comment: "DEP3 = oldC (in LSB)", matching VEX's
guest_arm64_helpers.c). So the C flag after adcs/sbcs was computed from an
operand value instead of the incoming carry.

Random operands usually mask it (cc_dep2 != 0 nearly always holds); the
equal-operand case exposes it, e.g. `sbcs x,y,y` must give C = oldC but
returned a value keyed on y. flag_n/z/v and the arm32 port (which correctly
uses cc_dep3) were unaffected.
2026-07-29 00:57:43 -07:00
Fish
6b2637c446
Dephication: Consider the vvar used in block-end jumps during intersection. (#6733)
* Dephication: Consider the vvar used in block-end jumps during intersection.

This is a subtlety in the Sreedhar et. al. paper.

* Fix test cases.
2026-07-29 00:42:21 -07:00
Fish
a7ae033c69
EagerEval: Fix broken expression type comparison. (#6734)
This is a bug introduced by the Rusty AIL migration.
2026-07-29 00:05:07 -07:00
Fish
659f3d7f5d
CCodeGen: Fix the display of negative offsets. (#6730)
* CCodeGen: Fix the display of negative offsets.

* Fix a test case.
2026-07-28 22:29:12 -07:00
Audrey Dutcher
4462c849b9
RegionIdentifier: typecheck post-regionoverlay (#6664) 2026-07-28 19:52:00 -07:00
Fish
22613f4a0a
CompleteCallingConventions: fail loudly when all workers die. (#6718) 2026-07-27 18:20:59 -07:00
Fish
2fffb71f86
state_plugins: Map the heap region lazily and grow it on demand. (#6715) 2026-07-27 18:20:07 -07:00
Fish
55f059982b
UltraPage: Make the symbolic map an actual bitmap. (#6714) 2026-07-27 17:35:52 -07:00
Fish
1a5eedf622
Decompiler: Speed up AIL block simplifications. (#6712)
- Make SPropagator, SRDA, and BlockSimplifier normal classes instead of Analysis classes.

- Share peephole optimizer instances across BlockSimplifiers.

- BlockSimplifier: Skip unnecessary peephole passes; avoid block-level comparisons for fixpoint determination.

- Add a runtime-only peephole_optimized flag to AIL statements so we skip running peephole optimizations on already optimized statements.
2026-07-27 16:34:10 -07:00
Fish
2080c15a26
RemoveRedundantBitmasks: Fix an in-place expression update. (#6722) 2026-07-27 15:53:53 -07:00
dependabot[bot]
3e2e4b90fc
ci: bump taiki-e/install-action from 2.84.0 to 2.85.2 (#6719)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.84.0 to 2.85.2.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](a6b2e2dcd8...41049aa566)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.85.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 13:13:52 -07:00
dependabot[bot]
59400706f8
ci: bump astral-sh/setup-uv from 8.3.2 to 9.0.0 (#6720)
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 8.3.2 to 9.0.0.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](11f9893b08...c771a70e62)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 9.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 13:13:35 -07:00
Fish
bc72b9e1a6
MCP: Protect stdio from forked workers. (#6717)
Some checks failed
CI / Rust Check (push) Failing after 6s
CI / rust_test (ubuntu-latest) (push) Failing after 9s
Test with coverage / Build (push) Failing after 8s
Test with coverage / Test (push) Has been skipped
Test with coverage / Test-1 (push) Has been skipped
Test with coverage / Test-2 (push) Has been skipped
Test with coverage / Test-3 (push) Has been skipped
Test with coverage / Test-4 (push) Has been skipped
Test with coverage / Test-5 (push) Has been skipped
Test with coverage / Test-6 (push) Has been skipped
Test with coverage / Test-7 (push) Has been skipped
Test with coverage / Test-8 (push) Has been skipped
Test with coverage / Test-9 (push) Has been skipped
Test with coverage / Test Rust packages (push) Failing after 6s
Test with coverage / Report (push) Has been skipped
CI / ci (push) Has been cancelled
CI / Test installation (macos-26, py3.12) (push) Has been cancelled
CI / Test installation (ubuntu-24.04, py3.14) (push) Has been cancelled
CI / Test installation (windows-2025, py3.12) (push) Has been cancelled
CI / rust_test (macos-latest) (push) Has been cancelled
CI / rust_test (windows-latest) (push) Has been cancelled
2026-07-27 03:01:56 -07:00
Fish
651d9cccc2
Tests: Speed up decompiler-related test cases (strike 1). (#6713) 2026-07-27 01:56:24 -07:00
Fish
9c1fb2367e
StructuringOptimizationPass: Cache structurability across passes. (#6711) 2026-07-27 00:03:32 -07:00
Quintin Kong
e4ff240001
x86/amd64: fix UMUL CF/OF flags (widen before multiply) (#6703)
Fixes #6067

pc_actions_UMUL computed the product at operand width, so the extracted
"high half" was always zero and CF/OF (OF = CF) were constantly 0:

    lo = (cc_dep1 * cc_dep2)[nbits-1:0]   # truncated to nbits
    hi = (lo >> nbits)[nbits-1:0]         # lo is nbits wide -> always 0

The sibling pc_actions_SMUL is correct because it widens first
(sign_extend). Mirror it with zero_extend: multiply the operands widened to
2*nbits and take the high half. For CC_OP_MUL{B,W,L,Q}, CF = OF =
(high half != 0) per the Intel SDM. This is why `imul` reported CF/OF
correctly while `mul` did not (issue #6067: `mul %ebx` left CF clear).
2026-07-26 22:32:25 -07:00
Fish
6d5860d0ba
COWDict: Faster chain walks. (#6710) 2026-07-26 22:20:20 -07:00
Fish
3b41a92e29
SimpleSolver: Hash memoization. (#6709) 2026-07-26 22:10:26 -07:00
Fish
83f364dc01
Memoize C++ prototype parsing. (#6708) 2026-07-26 21:33:20 -07:00
Fish
787c2c7d8e
SimpleSolver: Memoize least common ancestors on type lattices. (#6707) 2026-07-26 17:17:09 -07:00
Fish
b37cca0101
VRA: Register the Reference stack variable against its atom. (#6705) 2026-07-26 07:58:06 -07:00
Fish
fc7ff8e62c
CFGFast: Linear scan heuristics for monotonic byte ramps and floats. (#6701) 2026-07-26 00:35:43 -07:00
Yan Shoshitaishvili
83d0061e12
Calling conventions: ignore stack canary comparisons as returns (#6699)
* Calling conventions: ignore stack canary comparisons as returns
2026-07-24 16:34:21 -07:00
Yan Shoshitaishvili
f74d1c5c1e
Typehoon: index subtype constraint components (#6696)
* Typehoon: index subtype constraint components

* Refactor the code to eliminate weird terminology.

* Fix test cases.

---------

Co-authored-by: Fish <fishw@asu.edu>
2026-07-24 16:26:47 -07:00
Max Ambaum
be9c801b16
Add __sprintf_chk as a simprocedure (#6698)
* Add __sprintf_chk as a simprocedure

http://refspecs.linux-foundation.org/LSB_4.0.0/LSB-Core-generic/LSB-Core-generic/libc---sprintf-chk-1.html
2026-07-24 14:42:53 -07:00
Fish
3efd1ec6db
SimLibrary/SimSyscallLibrary: Treat None prototypes as absent. (#6673) 2026-07-24 11:29:58 -07:00
Ati Priya
db21fb0fee
Rewrite the amd64 CondO/CondNO ccall (#6693)
* Rewrite the amd64 CondO/CondNO ccall family

amd64g_calculate_condition with cond CondO/CondNO had no rewrite arm at
all, so every jo/jno/seto/cmovno site leaked into the decompilation as an
uncompilable _ccall(0|1, cc_op, ...).

Add arms for the cc_op families that define OF:

  LOGIC{B,W,L,Q}  and/or/xor always clear OF -> constant 0 / 1
  ADD{B,W,L,Q}    -> __OFADD__(dep_1, dep_2)
  SUB{B,W,L,Q}    -> __OFSUB__(dep_1, dep_2)
  UMUL{B,W,L,Q}   -> __OFUMUL__(dep_1, dep_2)
  SMUL{B,W,L,Q}   -> __OFSMUL__(dep_1, dep_2)
  INC{B,W,L,Q}    result == signed minimum
  DEC{B,W,L,Q}    result == signed maximum
  COPY            test the stored OF bit

The overflow helpers follow the existing __CFADD__ arm: a named usercall
whose operands carry the operation width. CondNO reuses the same helper
and compares it against zero.

Unsigned multiply overflow is defined as "the high half of the full
2N-bit product is nonzero", i.e. the product does not fit in N unsigned
bits. Note this is NOT the threshold used by the x86 rewriter, which
compares the product against 1 << (N - 1) -- that is the signed
threshold, half the correct unsigned one, and it reports overflow for
every product in [2^(N-1), 2^N - 1] even though those fit. At 8 bits it
misclassifies 820 of 65536 operand pairs, all false positives.

pc_actions_UMUL in the VEX ccall helpers is itself wrong here: it
multiplies two N-bit values without widening, so its `>> nbits` is
always zero and its CF/OF do not agree with the hardware. The rewrite
arm follows the hardware and pc_actions_SMUL's (correct) structure
instead; fixing the helper is left alone.

Every arm was checked exhaustively at 8 bits against
pc_calculate_condition, and the ADD/SUB/UMUL/SMUL arms additionally
against real setcc results.

* Drop the synthetic CondO fixture test

The real gzip and file fixtures already cover the CondO arms; a purpose
built binary added nothing that the unit tests do not already check.

* Cover the CondNO overflow path with a real binary

tar's argp helper guards a multiply with 'mul %rbp; jno', exercising the
CondNO side of UMULQ that gzip and file do not reach. Other cc_op
families still leak a ccall in that function, so only the OF conditions
are asserted.

* Cover the CondO overflow arms with three more real binaries

coreutils cat, grep and zlib's minigzip each carry the xalloc /
__builtin_mul_overflow idiom, between them exercising CondO against
ADDQ, SMULQ and UMULQ across three separate projects. Verified symbols
and addresses are cited on each test.

* Correct the tar overflow test to CondO

The jno there is canonicalized into CondO with an inverted branch, so the
ccall reaching the rewriter carries cond 0, not cond 1. The negation seen
in the output is the structurer's, not the condition's.
2026-07-24 11:28:39 -07:00
Fish
91cc026062
AILVexLifter: Fix libVEX overread by padding in convert_from_lift. (#6686) 2026-07-24 01:43:20 -07:00
Yan Shoshitaishvili
90062a9914
Support native AIL Abs expressions in light engines (#6683)
* Fix AIL Abs unary operation dispatch

* Fix Abs regression test lint
2026-07-24 00:43:03 -07:00
Yan Shoshitaishvili
fe434a049b
Decompiler: reject float constants in string simplifiers (#6682)
* Decompiler: reject float constants in string simplifiers

* Tests: allow private simplifier coverage
2026-07-24 00:28:36 -07:00
Yan Shoshitaishvili
208ec719a6
Decompiler: disambiguate Extract condition placeholders (#6677) 2026-07-23 22:33:21 -07:00
Yan Shoshitaishvili
8b98c5ad6c
Keep eager evaluation integer-only (#6681) 2026-07-23 22:30:15 -07:00
Yan Shoshitaishvili
2dd6cb393b
Typehoon: preserve standard SimTypeNum types (#6678) 2026-07-23 21:15:18 -07:00
Yan Shoshitaishvili
cf54c35b9b
AIL: handle HAddV operations (#6680)
* AIL: handle HAddV operations

* Fix HAddV CI diagnostics
2026-07-23 21:10:10 -07:00
Vedant Soni
1808e7fadd
Migrate rust demangling to pydemumble (#6663)
* use pydemumble to demangle rust symbols

* update tests

* remove rust_demangle and unpin pydemumble from pyproject

* pin pydemumble to latest version
2026-07-23 17:32:00 -07:00
Michał Kowalczyk
c13863214c
docs: Fix dangling links (#6533) 2026-07-23 17:31:25 -07:00
Fish
75500dd271
VRA: Copy over integer signedness to type constraints during Convert. (#6672)
* VRA: Copy over integer signedness to type constraints during Convert.

* Adjust a test case.
2026-07-23 15:57:26 -07:00
Fish
85d77f0301
RemoveRedundantShifts: Retain sign extensions. (#6671)
`(x << N) >> N` was rewritten into a Convert-of-Convert pair whose outer Convert
zero-extended for BOTH logical (Shr) and arithmetic (Sar) right shifts. For Sar
this is unsound: the idiom sign-extends the low (M-N) bits, but the zero-extending
Convert rendered as a bitmask, so e.g. `(int)(x << 20) >> 20` decompiled to
`x & 0xfff` (and the 64-bit twin to `x & 0xffffffffff`), which drops the
sign bit.
2026-07-23 14:43:45 -07:00
Fish
0849ddb03c
CFGFast: Tolerate leading null bytes during string scanning. (#6670)
* CFGFast: Tolerate a single leading null byte when scanning for strings.

* CFGFast: Scan for mixed pointers in high-based images during complete scanning.

* Tests: Add a regression test for data detection in a PE32 with data tables in .text.
2026-07-23 14:43:32 -07:00
Fish
8dc3772762
tests: CFG resolves delay-load import calls to named imports. (#6666) 2026-07-23 14:16:54 -07:00
Fish
8608d976e3
Decompiler: Fix re-rendering of decompilation caches reloaded from angrDb. (#6669)
* Decompiler: Fix re-rendering of decompilation caches reloaded from angrdb.

Re-rendering a deserialized codegen (as angr-management does on display/edit)
dropped variable declarations and string constants and rendered slightly different
C, because several pieces of state were not restored:

- VariableManagerInternal never serialized variable_to_types / variables_with_
  manual_types, so get_variable_type() returned None and all locals rendered as int.
- parse_codegen did not attach the project, and left display options that serialize
  as None (e.g. max_str_len) unset. Attach project and initialize display options
  from the codegen constructor defaults.
- CConstant string references lost MemoryData.content (not serialized); re-read it
  from the loader at parse time so strings render as strings, not raw addresses.
- regenerate_text() now refreshes CFunction.unified_local_vars from the (restored or
  updated) variable manager so declarations reflect current types.
- CBinaryOp._cstyle_null_cmp is rebuilt from the codegen flag in set_codegen,
  restoring !x vs x == 0.
- Compound-assignment folding (x += 1) compared unified variables by identity; use
  == so it works across deserialized variables that are equal but not the same
  object.

Adds an end-to-end test (1after909::doit) asserting a reloaded cache re-renders
byte-identically.

* Lint code.
2026-07-22 05:01:09 -07:00
Fish
2eadec31c6
Decompiler: Tolerate peephole optimizations not importable at parse time. (#6668)
* Decompiler: Tolerate peephole optimizations not importable at parse time.

* Lint code.
2026-07-22 03:56:06 -07:00
Fish
2f8266bdda
CFGFast: Constant-fold AMD64 PE IAT calls. (#6667)
* CFGFast: constant-fold AMD64 PE IAT jumps/calls, bypassing the resolver

* CFGFast: fold delay-load IAT calls too, matching MemoryLoadResolver

* tests: update lwip xrefs for corrected RO-region ordering
2026-07-22 03:55:07 -07:00
Fish
02c374b5b4
DecompilationCache: Serialization support. (#6624)
Also,

- Refactored variable_kb into kb.dec_variables.

- Spill decompilation cache into RuntimeDb.

- Save decompilation cache into angrDb. Decompilation results can be preserved across runs.

- No longer check in _pb2.py files; they are generated during build.
2026-07-22 03:03:40 -07:00
Ati Priya
e8858b82cd
typehoon: type pointer-to-array locals as pointers (#6620)
* typehoon: type function-scope pointer-to-array locals as element pointers (T*) so they render as pointers, not arrays

c_repr drops the "*" for pointer-to-array, so such locals were declared
as arrays and assignments to them were invalid C. Flatten T (*)[N] to
T * for function-scope variables; globals and plain arrays unchanged.

* tests: accept element-pointer rendering in reverting-switch-lowering test

Locals typed as element pointers render "ptr = p + 1;" instead of
"ptr = &p[1];"; both are equivalent. Accept either form.
2026-07-22 02:28:25 -07:00
Yan Shoshitaishvili
bcf5902d07
Keep entry jumps inside sized function symbols (#6651)
* Keep entry jumps within sized function symbols

* Narrow CFG regression symbol type

* Use committed binary for CFG entry-jump regression
2026-07-22 01:52:32 -07:00
Yan Shoshitaishvili
21cea3815f
Refine C++ symbol prototypes with machine ABI facts (#6652)
* Refine C++ symbol prototypes with machine ABI facts

* Preserve explicit C++ calling conventions

* Use prebuilt C++ calling-convention fixture
2026-07-22 01:50:12 -07:00
Fish
1102d5fc47
RustSimTypeInt: Include size in equality and hashing; fix copy() dropping size (#6659)
Fixes #6625.
2026-07-22 01:48:02 -07:00
volodya
95fa7ea003
utils/graph: answer subgraph_between_nodes reachability in one pass (#6662)
subgraph_between_nodes() copied the whole graph and then ran a fresh
networkx.has_path() search for every (candidate successor, frontier node)
pair, so its worst case was O(candidate_edges * frontier_nodes * (V + E)).
Negative queries are the expensive ones: a successor that cannot reach any
frontier node forces a full traversal of everything reachable from it, once
per frontier node.

RegionIdentifier._find_initial_loop_nodes() calls this for every loop it
recovers, so a loop head whose successors lead into a large region that only
returns to the head (which the function's own "remove all incoming edges of
the source" step makes unable to reach any latch) makes loop recovery
quadratic. On a 1.1k-node AArch64 CFG built to have that shape, decompilation
spends 40.5s of 52.5s inside 608,847 has_path() calls.

Replace the repeated searches with a single reverse multi-source BFS from the
frontier that stops at the source; membership in the resulting set answers
every reachability question the forward walk asks. Stopping the reverse walk
at the source is exactly equivalent to deleting all incoming edges of the
source, so the graph copy is no longer needed either. Also peel dead leaves
with a degree worklist instead of rescanning all nodes after each removal.

Results are unchanged, including node/edge insertion order and edge
attributes: 6,000 randomized fixed-seed comparisons (3,000 graphs x both
include_frontier modes) against the previous implementation are identical, as
is the decompiler output on the CFG above (52.5s -> 12.5s end to end, 0
has_path() calls).

One behaviour change is deliberate: frontier is now turned into a set before
the "source not in graph or any(node not in graph for node in frontier)"
check rather than after it. In the old order an iterator argument was consumed
by that check, so the subsequent set(frontier) was empty and the function
silently sliced with no frontier at all. Every in-tree caller passes a list or
a set, so no in-tree behaviour changes.
2026-07-22 01:41:36 -07:00
Ati Priya
26736ec096
decompiler: ExpressionNarrower over-narrows vvars used as Insert bases (#6649)
EffectiveSizeExtractor skipped the base operand of an Insert, so a use as
an Insert base contributed no width requirement in AILSimplifier's
narrowing pass. A register vvar whose remaining uses were narrow (e.g. an
ah-style byte Extract) was then narrowed below the Insert base width and
zero-extended back at the use site, destroying every preserved byte of the
base and misplacing the extracted byte:

    v1 = a0->field_10;                        // v1 narrowed to char
    a0->field_10 = _INSERT(v1, 1, v1 & 239);  // upper 3 bytes zeroed,
                                              // wrong byte masked

for what is really a full-width read-modify-write (field_10 &= 0xffffefff).

Walk the Insert base in EffectiveSizeExtractor so it is recorded as a
full-width use: every byte outside the inserted range is preserved into
the result, so the base can never be narrowed below its own width.
2026-07-20 23:33:10 -07:00
Kevin Phoenix
0c87229837
Pass new ret_expr during construction rather than mutating (#6642)
* Pass new ret_expr during construction rather than mutating

* More improvements.

---------

Co-authored-by: Fish <fishw@asu.edu>
2026-07-20 23:22:14 -07:00
Ati Priya
8ad0dc91aa
Decompiler: Recover CondNL (jge) over SUB and LOGIC in the amd64 ccall rewriter (#6657) 2026-07-20 23:19:14 -07:00
Vedant Soni
a3c8d835c7
RustStructuredCodeGenerator : align __init__ with CStructuredCodeGenerator.__init__ (#6650)
* Align RustStructuredCodeGenerator.__init__ with CStructuredCodeGenerator

* type fix
2026-07-20 23:17:44 -07:00
Yan Shoshitaishvili
0fcf7c050b
Copy stateful address concretization strategies on state fork (#6653)
* Copy stateful address concretization strategies

* Fix address strategy test typing
2026-07-20 23:17:11 -07:00
Yan Shoshitaishvili
dd1cefa642
Handle case-insensitive P-code memory space names in AIL conversion (#6654)
* Handle uppercase P-code memory spaces

* Fix P-code regression test typing
2026-07-20 23:15:12 -07:00
Kevin Phoenix
d099a933bf
Remove ConcreteBackerMixin (#6643) 2026-07-20 11:00:28 -07:00
pre-commit-ci[bot]
2c6c986f66
[pre-commit.ci] pre-commit autoupdate (#6644)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.15.21 → v0.15.22](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.21...v0.15.22)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-20 10:49:02 -07:00
dependabot[bot]
7d74ec8924
ci: bump actions/setup-python from 6.3.0 to 7.0.0 (#6637)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.3.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](ece7cb06ca...5fda3b95a4)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 10:08:44 -07:00
dependabot[bot]
41baf181f3
rust: bump regex from 1.12.2 to 1.13.1 (#6640)
Bumps [regex](https://github.com/rust-lang/regex) from 1.12.2 to 1.13.1.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.12.2...1.13.1)

---
updated-dependencies:
- dependency-name: regex
  dependency-version: 1.13.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 10:08:14 -07:00
dependabot[bot]
ada9fcd585
rust: bump serde from 1.0.228 to 1.0.229 (#6639)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.228 to 1.0.229.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.228...v1.0.229)

---
updated-dependencies:
- dependency-name: serde
  dependency-version: 1.0.229
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 10:08:07 -07:00
dependabot[bot]
635ddcd134
ci: bump taiki-e/install-action from 2.83.2 to 2.84.0 (#6636)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.83.2 to 2.84.0.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](43aecc8d72...a6b2e2dcd8)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 10:07:54 -07:00
dependabot[bot]
e9d795193a
ci: bump actions/checkout from 7.0.0 to 7.0.1 (#6638)
Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](9c091bb21b...3d3c42e5aa)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 10:07:39 -07:00
Kevin Phoenix
d145bd41fd
Remove global condition from SimState (#6641) 2026-07-20 10:06:54 -07:00
Ati Priya
3984a816b6
Fix bswap32 intrinsic name: __buildin_ -> __builtin_ (#6635)
gcc has no __buildin_bswap32, so the emitted call never resolved.
2026-07-20 02:12:23 -07:00
Kevin Phoenix
1944e72e90
Remove widen() from state api (#6632) 2026-07-17 13:23:53 -07:00
Kevin Phoenix
bd58a30e6e
Update installation CI run (#6631) 2026-07-17 11:50:28 -07:00
Kevin Phoenix
a6f88cd76f
Move STRONGREF_STATE logic entirely within history plugin (#6629) 2026-07-17 11:15:50 -07:00
Kevin Phoenix
c213b234b3
Pin a recent pydantic-ai (#6630) 2026-07-17 11:09:09 -07:00
Kevin Phoenix
352d642363
identifier: Deduplicate 22 copies of rand_str into shared helpers (#6628) 2026-07-17 10:41:08 -07:00
Vedant Soni
07115bb2ad
RustStructuredCodeGenerator: Make indent_delta configurable (#6626)
* add indent_size kwarg to rustcodegen

* make indent_delta variable
2026-07-17 02:06:11 -07:00
Fish
d9071a160e
Decompiler: Fix "unsupported instructions" when rbp is used as GPR. (#6627)
* VariableRecoveryFast: Do not seed bp with a stack address when bp is a GPR.

* RegisterSaveAreaSimplifierAdvanced: Fix extern check and handle shrink-wrapped spills.
2026-07-16 17:25:45 -07:00
angr-bot
f70bd5f0e1 Update version to 9.3.1.dev0 [ci skip] 2026-07-15 10:17:19 +00:00
414 changed files with 20331 additions and 4398 deletions

View file

@ -16,20 +16,26 @@ jobs:
ci:
uses: angr/ci-settings/.github/workflows/angr-ci.yml@master
smoketest:
name: Test installation
installation:
name: Test installation (${{ matrix.environment.os }}, py${{ matrix.environment.python-version }})
strategy:
matrix:
os: [windows-2022, macos-15-intel]
environment:
- os: windows-2025
python-version: 3.12
- os: macos-26
python-version: 3.12
- os: ubuntu-24.04
python-version: 3.14
fail-fast: false
runs-on: ${{ matrix.os }}
runs-on: ${{ matrix.environment.os }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
- uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1
if: startsWith(runner.os, 'windows')
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v6
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v6
- name: Sync dependencies
run: uv sync -p 3.12
run: uv sync -p ${{ matrix.environment.python-version }}
- name: Collect tests
run: uv run pytest --collect-only tests
@ -37,7 +43,7 @@ jobs:
name: Rust Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
- uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
with:
components: clippy, rustfmt
@ -50,9 +56,9 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
- uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v5
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v5
with:
python-version: "3.12"
- run: cargo test --release

View file

@ -23,9 +23,9 @@ jobs:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v6
- name: Restore test durations cache
uses: actions/cache/restore@v6
with:
@ -61,7 +61,7 @@ jobs:
tar -xpf $PWD/env.tzst -C /
rm env.tzst
- name: Download test binaries
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
with:
repository: angr/binaries
path: binaries
@ -96,13 +96,13 @@ jobs:
name: Test Rust packages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
- uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2
- uses: taiki-e/install-action@6c6fd71fe4fb72c3697d269963d0e15df8adedad # v2
with:
tool: cargo-llvm-cov
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v5
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v5
with:
python-version: "3.12"
- name: Run tests

View file

@ -28,15 +28,15 @@ jobs:
runner_id: [1, 2, 3, 4, 5]
fail-fast: false
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
with:
path: angr
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
with:
repository: angr/binaries
path: binaries
- uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v6
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v6
- name: Setup Java for pysoot
uses: actions/setup-java@v5
with:
@ -54,14 +54,14 @@ jobs:
runner_id: [1, 2, 3]
fail-fast: false
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
with:
path: angr
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
with:
repository: angr/binaries
path: binaries
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v6
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v6
- name: Sync dependencies
run: uv --directory angr sync -p 3.12
- name: Run pytest

3
.gitignore vendored
View file

@ -38,4 +38,5 @@ target/
.DS_Store
*_angr_rtdb
*_angr_rtdb_*
.claude/
.claude
angr/protos/*_pb2.py

View file

@ -62,7 +62,7 @@ repos:
args: [--py310-plus]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.21
rev: v0.16.1
hooks:
- id: ruff-check
args: [--fix, --exit-non-zero-on-fix]

57
Cargo.lock generated
View file

@ -1257,9 +1257,9 @@ dependencies = [
[[package]]
name = "pyo3"
version = "0.29.0"
version = "0.29.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c"
checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b"
dependencies = [
"inventory",
"libc",
@ -1274,18 +1274,18 @@ dependencies = [
[[package]]
name = "pyo3-build-config"
version = "0.29.0"
version = "0.29.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078"
checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9"
dependencies = [
"target-lexicon",
]
[[package]]
name = "pyo3-ffi"
version = "0.29.0"
version = "0.29.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b"
checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327"
dependencies = [
"libc",
"pyo3-build-config",
@ -1293,9 +1293,9 @@ dependencies = [
[[package]]
name = "pyo3-macros"
version = "0.29.0"
version = "0.29.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771"
checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
@ -1305,9 +1305,9 @@ dependencies = [
[[package]]
name = "pyo3-macros-backend"
version = "0.29.0"
version = "0.29.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362"
checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952"
dependencies = [
"heck",
"proc-macro2",
@ -1367,9 +1367,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.12.2"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
dependencies = [
"aho-corasick",
"memchr",
@ -1379,9 +1379,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.13"
version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
dependencies = [
"aho-corasick",
"memchr",
@ -1390,9 +1390,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
version = "0.8.8"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "region"
@ -1497,9 +1497,9 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
[[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",
@ -1529,22 +1529,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.114",
"syn 3.0.2",
]
[[package]]
@ -1669,6 +1669,17 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "target-lexicon"
version = "0.13.5"

View file

@ -3,3 +3,5 @@ include README.md
include Cargo.toml
include Cargo.lock
graft native
recursive-include angr/protos *.proto
recursive-exclude angr/protos *_pb2.py

View file

@ -43,11 +43,13 @@ import angr
project = angr.Project("angr-doc/examples/defcamp_r100/r100", auto_load_libs=False)
@project.hook(0x400844)
def print_flag(state):
print("FLAG SHOULD BE:", state.posix.dumps(0))
project.terminate_execution()
project.execute()
```

View file

@ -1,7 +1,7 @@
# pylint: disable=wrong-import-position
from __future__ import annotations
__version__ = "9.3.0.dev0"
__version__ = "9.3.3.dev0"
if bytes is str:
raise Exception("""

View file

@ -2,6 +2,8 @@ from __future__ import annotations
import logging
from angr.rustylib.ailment import ExpressionKind
from . import expression, statement
from .block import Block
from .block_walker import AILBlockRewriter, AILBlockViewer, AILBlockWalker
@ -69,6 +71,7 @@ __all__ = [
"Const",
"Expr",
"Expression",
"ExpressionKind",
"IRSBConverter",
"Manager",
"NoOp",

View file

@ -935,11 +935,12 @@ class AILBlockRewriter(AILBlockWalker[Expression, Statement, Block]):
return stmt
def _handle_Return(self, stmt_idx: int, stmt: Return, block: Block | None) -> Statement:
if stmt.ret_exprs:
ret_exprs_in = stmt.ret_exprs
if ret_exprs_in:
new_ret_exprs = [
self._handle_expr(idx, expr, stmt_idx, stmt, block) for idx, expr in enumerate(stmt.ret_exprs)
self._handle_expr(idx, expr, stmt_idx, stmt, block) for idx, expr in enumerate(ret_exprs_in)
]
changed = any(old is not new for new, old in zip(new_ret_exprs, stmt.ret_exprs))
changed = any(old != new for new, old in zip(new_ret_exprs, ret_exprs_in))
if changed:
return Return(stmt.idx, new_ret_exprs, **stmt.tags)
@ -1114,13 +1115,12 @@ class AILBlockRewriter(AILBlockWalker[Expression, Statement, Block]):
super()._handle_Phi(expr_idx, expr, stmt_idx, stmt, block)
return expr
changed = False
src_and_vvars_in = expr.src_and_vvars
src_and_vvars = [
(src, self._handle_expr(idx, vvar, stmt_idx, stmt, block) if vvar is not None else None)
for idx, (src, vvar) in enumerate(expr.src_and_vvars)
for idx, (src, vvar) in enumerate(src_and_vvars_in)
]
changed = any(new is not old for (_, new), (_, old) in zip(src_and_vvars, expr.src_and_vvars))
changed = any(new != old for (_, new), (_, old) in zip(src_and_vvars, src_and_vvars_in))
if changed:
assert all(vvar is None or isinstance(vvar, VirtualVariable) for _, vvar in src_and_vvars)
@ -1137,7 +1137,7 @@ class AILBlockRewriter(AILBlockWalker[Expression, Statement, Block]):
) -> Expression:
operands_in = expr.operands
new_operands = [self._handle_expr(0, operand, stmt_idx, stmt, block) for operand in operands_in]
changed = any(new is not old for new, old in zip(new_operands, operands_in))
changed = any(new != old for new, old in zip(new_operands, operands_in))
new_guard = None
guard_in = expr.guard

View file

@ -329,7 +329,7 @@ class PCodeIRSBConverter(Converter):
return Convert(self._manager.next_atom(), t.bits, size, False, t, ins_addr=self._manager.ins_addr)
return Tmp(self._manager.next_atom(), offset, size)
if space_name in ["ram", "mem"]:
if space_name.lower() in ["ram", "mem"]:
assert not is_write
addr = Const(self._manager.next_atom(), varnode.offset, self._manager.arch.bits)
# Note: Load takes bytes, not bits, for size
@ -359,7 +359,7 @@ class PCodeIRSBConverter(Converter):
return Assignment(
self._statement_idx, self._convert_varnode(varnode, True), value, ins_addr=self._manager.ins_addr
)
if space_name in ["ram", "mem"]:
if space_name.lower() in ["ram", "mem"]:
addr = Const(self._manager.next_atom(), varnode.offset, self._manager.arch.bits)
return Store(
self._statement_idx,

View file

@ -1,7 +1,5 @@
from __future__ import annotations
import struct
import archinfo
from angr import ailment
@ -11,11 +9,6 @@ try:
except ImportError:
from typing import Never as Bits
try:
import _md5 as md5lib # type: ignore # stdlib C module without stubs
except ImportError:
import hashlib as md5lib
type GetBitsTypeParams = "ailment.expression.Expression"
@ -27,62 +20,6 @@ def get_bits(expr: GetBitsTypeParams) -> int:
raise TypeError(type(expr))
md5_unpacker = struct.Struct("4I")
def stable_hash(t: tuple) -> int:
cnt = _dump_tuple(t)
hd = md5lib.md5(cnt).digest()
return md5_unpacker.unpack(hd)[0] # 32 bits
def _dump_tuple(t: tuple) -> bytes:
cnt = b""
for item in t:
if item is not None:
type_ = type(item)
if type_ in _DUMP_BY_TYPE:
cnt += _DUMP_BY_TYPE[type_](item)
else:
# for TaggedObjects, hash(item) is stable
# other types of items may show up, such as pyvex.expr.CCall and Dirty. they will be removed some day.
cnt += struct.pack("<Q", hash(item) & 0xFFFF_FFFF_FFFF_FFFF)
cnt += b"\xf0"
return cnt
def _dump_str(t: str) -> bytes:
return t.encode("utf-8")
def _dump_int(t: int) -> bytes:
prefix = b"" if t >= 0 else b"-"
t = abs(t)
if t <= 0xFFFF:
return prefix + struct.pack("<H", t)
if t <= 0xFFFF_FFFF:
return prefix + struct.pack("<I", t)
if t <= 0xFFFF_FFFF_FFFF_FFFF:
return prefix + struct.pack("<Q", t)
cnt = b""
while t > 0:
cnt += _dump_int(t & 0xFFFF_FFFF_FFFF_FFFF)
t >>= 64
return prefix + cnt
def _dump_type(t: type) -> bytes:
return t.__name__.encode("ascii")
_DUMP_BY_TYPE = {
tuple: _dump_tuple,
str: _dump_str,
int: _dump_int,
type: _dump_type,
}
def is_none_or_likeable(arg1, arg2, is_list=False):
"""
Returns whether two things are both None or can like each other

View file

@ -37,8 +37,8 @@ from .proximity_graph import ProximityGraphAnalysis
from .reaching_definitions import ReachingDefinitionsAnalysis
from .reassembler import Reassembler
from .s_liveness import SLivenessAnalysis
from .s_propagator import SPropagatorAnalysis
from .s_reaching_definitions import SReachingDefinitionsAnalysis
from .s_propagator import SPropagator, SPropagatorAnalysis
from .s_reaching_definitions import SReachingDefinitions, SReachingDefinitionsAnalysis
from .smc import SelfModifyingCodeAnalysis
from .soot_class_hierarchy import SootClassHierarchy
from .stack_pointer_tracker import StackPointerTracker
@ -97,7 +97,9 @@ __all__ = (
"ReachingDefinitionsAnalysis",
"Reassembler",
"SLivenessAnalysis",
"SPropagator",
"SPropagatorAnalysis",
"SReachingDefinitions",
"SReachingDefinitionsAnalysis",
"SelfModifyingCodeAnalysis",
"SootClassHierarchy",

View file

@ -323,9 +323,7 @@ class BackwardSlice(Analysis):
if simrun not in cfg:
l.error("SimRun instance %s is not in the CFG.", simrun)
stack = []
for simrun in simruns:
stack.append(simrun)
stack = simruns.copy()
self.runs_in_slice = networkx.DiGraph()
self.cfg_nodes_in_slice = networkx.DiGraph()

View file

@ -108,12 +108,9 @@ class BinaryOptimizer(Analysis):
BLOCKS_THRESHOLD = 500 # do not optimize a function if it has more than this number of blocks
def __init__(self, cfg, techniques):
def __init__(self, cfg, techniques: set[str]):
self.cfg = cfg
if techniques is None:
raise Exception("At least one optimization technique must be specified.")
supported_techniques = {
"constant_propagation",
"redundant_stack_variable_removal",
@ -122,7 +119,7 @@ class BinaryOptimizer(Analysis):
}
if techniques - supported_techniques:
raise Exception("At least one optimization technique specified is not supported.")
raise ValueError("At least one optimization technique specified is not supported.")
self._techniques = techniques.copy()

View file

@ -747,8 +747,7 @@ class FunctionDiff:
ordered_succ.append(x)
# add the rest (sorting might be better than no order)
for s in sorted(succ - set(ordered_succ), key=lambda x: x.addr):
ordered_succ.append(s)
ordered_succ.extend(sorted(succ - set(ordered_succ), key=lambda x: x.addr))
return ordered_succ
except (SimMemoryError, SimEngineError):
return sorted(succ, key=lambda x: x.addr)

View file

@ -56,9 +56,7 @@ class BoyScout(Analysis):
l.debug("%s %s hits %d times", arch.name, arch.memory_endness, votes[(arch.name, arch.memory_endness)])
arch_name, endianness, hits = sorted(
[(k[0], k[1], v) for k, v in votes.items()], key=lambda x: x[2], reverse=True
)[0]
arch_name, endianness, hits = max([(k[0], k[1], v) for k, v in votes.items()], key=lambda x: x[2])
if hits < self.cookiesize * 2:
# this cannot possibly be code

View file

@ -45,6 +45,7 @@ from angr.sim_type import (
SimTypeInt128,
SimTypeLongLong,
SimTypePointer,
SimTypeReg,
SimTypeShort,
parse_cpp_file,
)
@ -183,13 +184,24 @@ class CallingConventionAnalysis(Analysis):
assert self._function is not None
cpp_symbol_result: tuple[SimCC, SimTypeCppFunction, str | None] | None = None
demangled_name = self._function.demangled_name
if demangled_name != self._function.name:
r_demangled = self._analyze_demangled_name(demangled_name)
if r_demangled is not None:
self.cc, self.prototype, self.prototype_libname = r_demangled
self.proto_from_symbol = True
return
# Itanium names usually omit the return type, and a qualified name does
# not distinguish a namespace/static function from a non-static member.
# parse_cpp_file() consequently carries a possible-this placeholder.
# Do not let that incomplete declaration bypass callee/callsite analysis;
# refine it with machine facts below. Declarations that encode an explicit
# calling convention (such as Microsoft C++ symbols) remain authoritative.
demangled_cc, demangled_proto, demangled_libname = r_demangled
if isinstance(demangled_proto, SimTypeCppFunction) and demangled_proto.convention is None:
cpp_symbol_result = demangled_cc, demangled_proto, demangled_libname
else:
self.cc, self.prototype, self.prototype_libname = r_demangled
self.proto_from_symbol = True
return
if self._function.is_simprocedure:
hooker = self.project.hooked_by(self._function.addr)
@ -282,6 +294,9 @@ class CallingConventionAnalysis(Analysis):
r = self._analyze_function()
if r is None:
l.warning("Cannot determine calling convention for %r.", self._function)
if cpp_symbol_result is not None:
self.cc, self.prototype, self.prototype_libname = cpp_symbol_result
self.proto_from_symbol = True
else:
# adjust prototype if needed
cc, prototype = r
@ -296,9 +311,48 @@ class CallingConventionAnalysis(Analysis):
else None
)
if cpp_symbol_result is not None and prototype is not None:
prototype = self._refine_cpp_symbol_prototype(prototype, cpp_symbol_result[1])
self.cc = cc
self.prototype = prototype
@staticmethod
def _refine_cpp_symbol_prototype(
machine_proto: SimTypeFunction, symbol_proto: SimTypeCppFunction
) -> SimTypeFunction:
"""Merge encoded C++ types only where machine ABI arity disambiguates them.
The parser's first pointer is a *possible* ``this``. If machine facts recover
one fewer arguments, the qualified name was a namespace/static function and the
placeholder is removed. If arity agrees it is retained. Any other disagreement
keeps the machine-derived arguments. A non-Bottom encoded template return may
refine the return type; ordinary Itanium names keep the machine-derived return.
"""
machine_args = tuple(machine_proto.args or ())
symbol_args = tuple(symbol_proto.args or ())
if len(symbol_args) == len(machine_args):
selected = symbol_args
elif len(symbol_args) == len(machine_args) + 1 and symbol_args and isinstance(symbol_args[0], SimTypePointer):
selected = symbol_args[1:]
else:
selected = machine_args
# Opaque C++ classes cannot be laid out by a calling convention. Preserve the
# machine-derived slot for those arguments; only scalar/reference or pointer
# types from the linkage name are safe refinements.
args = tuple(
sym if isinstance(sym, (SimTypeReg, SimTypePointer)) else machine
for machine, sym in zip(machine_args, selected)
)
symbol_ret = symbol_proto.returnty
ret = (
symbol_ret
if symbol_ret is not None
and not isinstance(symbol_ret, SimTypeBottom)
and isinstance(symbol_ret, (SimTypeReg, SimTypePointer))
else machine_proto.returnty
)
return SimTypeFunction(args, ret, variadic=machine_proto.variadic)
def _analyze_callsite_only(self):
assert self.caller_func_addr is not None
assert self.callsite_block_addr is not None

View file

@ -2,6 +2,7 @@
from __future__ import annotations
from collections import defaultdict
from collections.abc import Container, Iterator
from typing import TYPE_CHECKING
import pyvex
@ -467,6 +468,141 @@ class FactCollector(Analysis):
state.register_written(offset, self.project.arch.registers[reg_name][1])
state.simple_regs[offset] = None
@staticmethod
def _resolve_vex_tmp(
expr: pyvex.IRExpr.IRExpr,
tmp_definitions: dict[int, pyvex.IRExpr.IRExpr],
seen_tmps: frozenset[int] = frozenset(),
) -> pyvex.IRExpr.IRExpr:
while isinstance(expr, pyvex.IRExpr.RdTmp) and expr.tmp not in seen_tmps:
definition = tmp_definitions.get(expr.tmp)
if definition is None:
break
seen_tmps |= {expr.tmp}
expr = definition
return expr
@classmethod
def _walk_vex_expr(
cls,
expr: pyvex.IRExpr.IRExpr,
tmp_definitions: dict[int, pyvex.IRExpr.IRExpr],
seen_tmps: frozenset[int] = frozenset(),
) -> Iterator[pyvex.IRExpr.IRExpr]:
if isinstance(expr, pyvex.IRExpr.RdTmp):
if expr.tmp in seen_tmps:
return
definition = tmp_definitions.get(expr.tmp)
if definition is not None:
yield from cls._walk_vex_expr(definition, tmp_definitions, seen_tmps | {expr.tmp})
return
yield expr
for child in expr.child_expressions:
yield from cls._walk_vex_expr(child, tmp_definitions, seen_tmps)
def _stack_canary_tls_location(self) -> tuple[int, int] | None:
if self.project.arch.name == "AMD64":
reg_name, offset = "fs", 0x28
elif self.project.arch.name == "X86":
reg_name, offset = "gs", 0x14
else:
return None
return self.project.arch.registers[reg_name][0], offset
@classmethod
def _is_tls_canary_load(
cls,
expr: pyvex.IRExpr.IRExpr,
tmp_definitions: dict[int, pyvex.IRExpr.IRExpr],
tls_reg_offset: int,
canary_offset: int,
) -> bool:
expr = cls._resolve_vex_tmp(expr, tmp_definitions)
if not isinstance(expr, pyvex.IRExpr.Load):
return False
addr_nodes = tuple(cls._walk_vex_expr(expr.addr, tmp_definitions))
return any(isinstance(node, pyvex.IRExpr.Get) and node.offset == tls_reg_offset for node in addr_nodes) and any(
isinstance(node, pyvex.IRExpr.Const) and node.con.value == canary_offset for node in addr_nodes
)
@classmethod
def _is_stack_load(
cls,
expr: pyvex.IRExpr.IRExpr,
tmp_definitions: dict[int, pyvex.IRExpr.IRExpr],
stack_reg_offsets: Container[int | None],
) -> bool:
expr = cls._resolve_vex_tmp(expr, tmp_definitions)
if not isinstance(expr, pyvex.IRExpr.Load):
return False
return any(
isinstance(node, pyvex.IRExpr.Get) and node.offset in stack_reg_offsets
for node in cls._walk_vex_expr(expr.addr, tmp_definitions)
)
def _has_terminal_call_successor(self, node: BlockNode) -> bool:
func_graph = self.function.transition_graph
for _, succ, data in func_graph.out_edges(node, data=True):
if data.get("type") != "transition" or data.get("outside", False) or not isinstance(succ, BlockNode):
continue
succ_block = self.project.factory.block(succ.addr, size=succ.size)
if succ_block.vex.jumpkind != "Ijk_Call":
continue
if not any(
edge_data.get("type") == "fake_return" for _, _, edge_data in func_graph.out_edges(succ, data=True)
):
return True
return False
def _is_stack_canary_retval_write(
self,
node: BlockNode,
block: Block,
expr: pyvex.IRExpr.IRExpr,
tmp_definitions: dict[int, pyvex.IRExpr.IRExpr],
) -> bool:
tls_location = self._stack_canary_tls_location()
if tls_location is None:
return False
expr = self._resolve_vex_tmp(expr, tmp_definitions)
if not isinstance(expr, pyvex.IRExpr.Binop) or expr.op not in {
"Iop_Sub32",
"Iop_Sub64",
"Iop_Xor32",
"Iop_Xor64",
}:
return False
tls_reg_offset, canary_offset = tls_location
stack_reg_offsets = {self.project.arch.sp_offset, self.project.arch.bp_offset}
op0, op1 = expr.args
if not (
(
self._is_tls_canary_load(op0, tmp_definitions, tls_reg_offset, canary_offset)
and self._is_stack_load(op1, tmp_definitions, stack_reg_offsets)
)
or (
self._is_tls_canary_load(op1, tmp_definitions, tls_reg_offset, canary_offset)
and self._is_stack_load(op0, tmp_definitions, stack_reg_offsets)
)
):
return False
if not self._has_terminal_call_successor(node):
return False
for stmt in block.vex.statements:
if not isinstance(stmt, pyvex.IRStmt.Exit):
continue
guard_nodes = tuple(self._walk_vex_expr(stmt.guard, tmp_definitions))
if any(
self._is_tls_canary_load(node, tmp_definitions, tls_reg_offset, canary_offset) for node in guard_nodes
) and any(self._is_stack_load(node, tmp_definitions, stack_reg_offsets) for node in guard_nodes):
return True
return False
def _analyze_endpoints_for_retval_size(self, end_states):
"""
Analyze all endpoints to determine the return value size.
@ -591,6 +727,7 @@ class FactCollector(Analysis):
# to account for the common case where the shorter register (e.g., al) is extended to the full register
# (e.g., rax) before returning.
block_retval_size = None
stack_canary_barrier = False
for stmt in reversed(block.vex.statements):
if isinstance(stmt, pyvex.IRStmt.Put):
assert block.vex.tyenv is not None
@ -610,6 +747,11 @@ class FactCollector(Analysis):
size = 4
if stmt.offset == retreg_offset:
if isinstance(node, BlockNode) and self._is_stack_canary_retval_write(
node, block, stmt.data, tmp_definitions
):
stack_canary_barrier = True
break
block_retval_size = max(size, 1)
if stmt.offset == overflow_retreg_offset:
overflow_retval_sizes.append(max(size, 1))
@ -617,6 +759,8 @@ class FactCollector(Analysis):
if block_retval_size is not None:
retval_sizes.append(block_retval_size)
continue
if stack_canary_barrier:
continue
for pred, _, data in func_graph.in_edges(node, data=True):
edge_type = data.get("type")

View file

@ -255,6 +255,7 @@ class CFGBase(Analysis):
'"auto_load_libs" disabled, or specify "regions" to limit the scope of CFG recovery.'
)
regions_derived_from_objects = regions is None
if regions is None:
regions = self._exec_mem_regions
if not self._skip_unmapped_addrs and not regions:
@ -284,6 +285,14 @@ class CFGBase(Analysis):
for start, end in self._regions.items():
l.debug("... %#x - %#x", start, end)
if regions_derived_from_objects and not self._regions_size:
l.warning(
"CFG recovery has nothing to scan: the regions to analyze cover 0 bytes. If %s does contain code, "
'pass the address ranges that hold it in "regions", or set "force_segment" to derive regions from '
"segments instead of sections.",
self._binary,
)
def __contains__(self, cfg_node):
return cfg_node in self.graph
@ -325,6 +334,15 @@ class CFGBase(Analysis):
"""
return self.kb.functions
@property
def regions(self) -> list[tuple[int, int]]:
"""
The memory regions that this analysis covers. An empty list means it had nothing to scan.
:return: A sorted list of (start address, end address) tuples.
"""
return list(self._regions.items())
#
# Methods
#
@ -891,6 +909,9 @@ class CFGBase(Analysis):
if not memory_regions and not has_executable:
memory_regions = [(start, start + len(backer)) for start, backer in self.project.loader.memory.backers()]
# A section or segment that maps no bytes, such as the empty .text of a data-only relocatable, is not a region.
memory_regions = [(start, end) for start, end in memory_regions if end > start]
return sorted(memory_regions, key=lambda x: x[0])
def _addr_in_exec_memory_regions(self, addr):

View file

@ -1139,7 +1139,7 @@ class CFGEmulated(ForwardAnalysis, CFGBase): # pylint: disable=abstract-method
self._update_function_callsites(funcaddrs_do_not_return)
# Create all pending edges
for _, edges in self._pending_edges.items():
for edges in self._pending_edges.values():
for src_node, dst_node, data in edges:
self._graph_add_edge(src_node, dst_node, **data)

View file

@ -48,6 +48,7 @@ from angr.knowledge_plugins.cfg.spilling_cfg import block_key_to_addr, block_key
from angr.knowledge_plugins.xrefs import XRef, XRefType
from angr.misc.ux import once
from angr.rustylib import SegmentList
from angr.simos import SimWindows
from angr.utils.constants import DEFAULT_STATEMENT
from angr.utils.funcid import (
is_function_likely_security_init_cookie,
@ -869,6 +870,10 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
# mapping to all known thunks
self._known_thunks = {}
# when True, jump/call targets loaded from registered read-only regions (e.g. PE IAT slots) are
# constant-folded at lift time and consumed in _create_jobs without invoking indirect jump resolvers
self._fold_ro_const_loads = False
self._initial_state = None
self._next_addr: int | None = None
@ -1233,6 +1238,86 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
return repeating_length
return 0
def _scan_for_fp_constants(self, start_addr: int, threshold: int = 4) -> int:
"""
Scan from a given address for a run of plausible floating-point constants.
A double-precision value qualifies when its biased exponent falls within a band covering magnitudes
between 2 ** -64 and 2 ** 64, which is where constants in compiler- and libm-generated tables (polynomial
coefficients, logarithm and trigonometry tables, etc.) almost always live. Code bytes rarely produce
multiple consecutive qualifying values.
Single-precision values are detected as well, but with a tighter magnitude band (2 ** -32 to 2 ** 32) and
twice the run-length requirement: an 8-bit exponent in a 4-byte value is a much weaker signal than an
11-bit exponent in an 8-byte value, and anything looser starts matching real code.
:param start_addr: The address to start scanning from.
:param threshold: The minimum number of consecutive qualifying double-precision values.
:return: The total size in bytes of the qualifying values, or 0 if not enough values are found.
"""
for size, exp_shift, exp_mask, exp_lo, exp_hi, min_count in (
(8, 52, 0x7FF, 959, 1087, threshold), # doubles: 1023 +/- 64
(4, 23, 0xFF, 95, 159, threshold * 2), # floats: 127 +/- 32
):
addr = start_addr
fp_count = 0
first_val = None
has_multiple_values = False
uniform_mul = ((1 << (size * 8)) - 1) // 0xFF
while self._inside_regions(addr):
val = self._fast_memory_load_pointer(addr, size=size)
if val is None:
break
if val == (val & 0xFF) * uniform_mul:
# all bytes are identical: this is filler (e.g., 0xCC padding or "????", whose bit patterns
# carry in-band exponents), not a constant
break
exponent = (val >> exp_shift) & exp_mask
if not exp_lo <= exponent <= exp_hi:
break
if first_val is None:
first_val = val
elif val != first_val:
has_multiple_values = True
fp_count += 1
addr += size
# a run of one repeated value carries no table evidence
if fp_count >= min_count and has_multiple_values:
return fp_count * size
return 0
def _scan_for_monotonic_byte_ramp(self, start_addr: int, threshold: int = 16) -> int:
"""
Scan from a given address for a run of monotonically increasing bytes, where each byte equals the previous
byte plus one, modulo 256. Character case-conversion and translation tables are laid out this way.
:param start_addr: The address to start scanning from.
:param threshold: The minimum run length.
:return: The length of the run, or 0 if the run is shorter than threshold.
"""
addr = start_addr
last_byte = None
ramp_length = 0
while self._inside_regions(addr):
val = self._load_a_byte_as_int(addr)
if val is None:
break
if last_byte is not None and val != (last_byte + 1) & 0xFF:
break
last_byte = val
ramp_length += 1
addr += 1
if ramp_length >= threshold:
return ramp_length
return 0
def _scan_for_consecutive_pointers(self, start_addr: int, threshold: int = 2) -> int:
"""
Scan from a given address and determine if there are at least `threshold` of pointers.
@ -1343,11 +1428,19 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
)
start_addr += pointer_length
elif start_addr <= 0x100000:
# for high addresses, all pointers have been found in _scan_for_consecutive_pointers() because we
# set threshold there to 1
threshold = 4
pointer_count = self._scan_for_mixed_pointers(start_addr, threshold=threshold, window=6)
else:
if start_addr <= 0x100000:
# for low addresses, in-object values are common false positives, so
# _scan_for_consecutive_pointers() ran with a high threshold and may have missed
# non-consecutive pointers; require a high pointer density here
threshold, window = 4, 6
else:
# for high addresses, all consecutive pointers have been found in
# _scan_for_consecutive_pointers() because we set threshold there to 1. what remains are
# interleaved tables (e.g., alternating value-pointer pairs), which have at most window // 2
# pointers; use a wider window with the same evidence requirement
threshold, window = 4, 8
pointer_count = self._scan_for_mixed_pointers(start_addr, threshold=threshold, window=window)
pointer_length = pointer_count * self.project.arch.bytes
if pointer_length:
@ -1359,24 +1452,54 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
start_addr += pointer_length
if not matched_something:
# find strings
# find floating-point constant tables; this must run before the string and repeating-zero scans
# because the low mantissa bytes of table entries are frequently zero or incidentally printable,
# which would misphase the table. since scanning misclassified code often dumps us in the middle
# of a table entry, probe the next 4- and 8-byte boundaries as well.
fp_addr_4 = start_addr + (-start_addr % 4)
fp_addr_8 = start_addr + (-start_addr % 8)
for fp_addr in (fp_addr_4,) if fp_addr_4 == fp_addr_8 else (fp_addr_4, fp_addr_8):
fp_length = self._scan_for_fp_constants(fp_addr)
if fp_length:
matched_something = True
if fp_addr > start_addr:
self._seg_list.occupy(start_addr, fp_addr - start_addr, "alignment")
self.model.memory_data[start_addr] = MemoryData(
start_addr, fp_addr - start_addr, MemoryDataSort.Alignment
)
self._seg_list.occupy(fp_addr, fp_length, "fp")
self.model.memory_data[fp_addr] = MemoryData(fp_addr, fp_length, MemoryDataSort.FloatingPoint)
start_addr = fp_addr + fp_length
break
if not matched_something:
# find strings; tolerate a single leading null byte, which is usually the leftover of a multi-null
# string separator (string scans only consume one null terminator of the preceding string, and a
# single remaining null byte is not caught by the repeating-zero scan below)
leading_nulls = 1 if self._load_a_byte_as_int(start_addr) == 0 else 0
str_addr = start_addr + leading_nulls
is_widestring = False
string_length = self._scan_for_printable_strings(start_addr)
string_length = self._scan_for_printable_strings(str_addr)
if string_length == 0:
is_widestring = True
string_length = self._scan_for_printable_widestrings(start_addr)
string_length = self._scan_for_printable_widestrings(str_addr)
if string_length:
matched_something = True
self._seg_list.occupy(start_addr, string_length, "string")
if leading_nulls:
self._seg_list.occupy(start_addr, leading_nulls, "alignment")
self.model.memory_data[start_addr] = MemoryData(
start_addr, leading_nulls, MemoryDataSort.Alignment
)
self._seg_list.occupy(str_addr, string_length, "string")
md = MemoryData(
start_addr,
str_addr,
string_length,
MemoryDataSort.String if not is_widestring else MemoryDataSort.UnicodeString,
)
md.fill_content(self.project.loader)
self.model.memory_data[start_addr] = md
start_addr += string_length
self.model.memory_data[str_addr] = md
start_addr = str_addr + string_length
if not matched_something and self.project.arch.name in {"X86", "AMD64"}:
cc_length = self._scan_for_repeating_bytes(start_addr, 0xCC, threshold=1)
@ -1416,6 +1539,14 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
)
start_addr += repeating_byte_length
# a long run of monotonically increasing bytes is a character or translation table, not code
ramp_length = self._scan_for_monotonic_byte_ramp(start_addr, threshold=16)
if ramp_length:
matched_something = True
self._seg_list.occupy(start_addr, ramp_length, "nodecode")
self.model.memory_data[start_addr] = MemoryData(start_addr, ramp_length, MemoryDataSort.Unknown)
start_addr += ramp_length
if not matched_something:
# umm now it's probably code
break
@ -1461,28 +1592,7 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
def _nodecode_bytes_ratio(self, cutoff_addr: int, window_size: int) -> float:
if cutoff_addr - 1 < 0:
return 0.0
idx = self._seg_list.search(cutoff_addr - 1)
if idx is None or idx >= len(self._seg_list):
return 0.0
segment = self._seg_list[idx]
if segment.sort != "nodecode":
return 0.0
total_bytes = 0
nodecode_bytes = 0
while idx >= 0:
segment = self._seg_list[idx]
if segment.sort == "nodecode":
nodecode_bytes += segment.size
total_bytes += segment.size
if total_bytes >= window_size:
break
idx -= 1
if total_bytes < window_size:
return 0.0
return nodecode_bytes / total_bytes
return self._seg_list.sort_ratio_backwards(cutoff_addr - 1, window_size, "nodecode")
def _next_code_addr_smart(self) -> int | None:
# in the smart scanning mode, if there are more than N consecutive no-decode cases, we skip an entire window of
@ -3133,6 +3243,34 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
return entries
def _resolve_const_folded_next(self, irsb: pyvex.IRSB | None, jumpkind: str) -> int | None:
"""
Check whether the jump/call target of this block was constant-folded from a registered read-only region
at lift time (recorded in IRSB.const_vals), e.g. an import call through the IAT (or delay-load IAT) in a PE
binary. This reproduces the decision of the timeless load-based resolvers (AMD64PeIatResolver and
MemoryLoadResolver) without re-lifting the block or dispatching the resolvers: the loaded pointer is the
same value they would read, and it is accepted when it is a valid jump target (executable or hooked), which
is exactly MemoryLoadResolver's ``_is_target_valid`` criterion (a superset of AMD64PeIatResolver's hooked
check). Everything else falls back to the regular indirect jump resolution logic.
:param irsb: The (possibly statement-less) IRSB of the block.
:param jumpkind: The jumpkind of the default exit.
:return: The resolved target, or None if unavailable.
"""
if not self._fold_ro_const_loads or irsb is None:
return None
if jumpkind not in ("Ijk_Call", "Ijk_Boring"):
return None
if not irsb.const_vals or not isinstance(irsb.next, pyvex.IRExpr.RdTmp):
return None
next_tmp = irsb.next.tmp
for cv in irsb.const_vals:
if cv.tmp == next_tmp:
if self._addr_in_exec_memory_regions(cv.value) or self.project.is_hooked(cv.value):
return cv.value
return None
return None
def _create_jobs(
self,
target: Any,
@ -3218,14 +3356,24 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
jumpkind in ("Ijk_Boring", "Ijk_Call", "Ijk_InvalICache") or jumpkind.startswith("Ijk_Sys")
):
# This is an indirect jump. Try to resolve it.
# FIXME: in some cases, a statementless irsb will be missing its instr addresses
# and this next part will fail. Use the real IRSB instead
irsb = self._lift(cfg_node.addr, size=cfg_node.size).vex
assert irsb is not None
cfg_node.instruction_addrs = InsAddrList.from_addr_list(irsb.instruction_addresses)
resolved, resolved_targets, ij = self._indirect_jump_encountered(
addr, cfg_node, irsb, current_function_addr, stmt_idx
)
# fast path: the target may have been constant-folded from a read-only region at lift time
# (e.g. an AMD64 PE IAT slot); consuming it here avoids re-lifting the block and running the
# indirect jump resolvers
folded_target = self._resolve_const_folded_next(irsb, jumpkind)
if folded_target is not None:
# the statement-less irsb already carries the instruction addresses that the resolver path
# would recompute from a re-lift
cfg_node.instruction_addrs = InsAddrList.from_addr_list(irsb.instruction_addresses)
resolved, resolved_targets, ij = True, {folded_target}, None
else:
# FIXME: in some cases, a statementless irsb will be missing its instr addresses
# and this next part will fail. Use the real IRSB instead
irsb = self._lift(cfg_node.addr, size=cfg_node.size).vex
assert irsb is not None
cfg_node.instruction_addrs = InsAddrList.from_addr_list(irsb.instruction_addresses)
resolved, resolved_targets, ij = self._indirect_jump_encountered(
addr, cfg_node, irsb, current_function_addr, stmt_idx
)
if resolved:
for resolved_target in resolved_targets:
if jumpkind == "Ijk_Call":
@ -3596,7 +3744,10 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
target_func_addr = node.function_address
# case 2: if the source instruction is the first instruction of the current function, has only one branch
# to the target address, and is a jump (Ijk_Boring, not a call), then the target address is likely the
# start of another function
# start of another function. A compiler may also begin a function with an
# unconditional jump to an internal loop guard (loop rotation). When the loader
# supplies a non-empty function symbol, its extent is stronger evidence than this
# tail-jump heuristic: keep a target inside that extent in the current function.
if (
target_func_addr is None
and len(src_node.instruction_addrs) == 1
@ -3605,7 +3756,17 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
and all_successors is not None
and len(all_successors) == 1
):
target_func_addr = target_addr
current_symbol = self.project.loader.find_symbol(current_function_addr)
current_symbol_size = getattr(current_symbol, "size", 0) or 0
target_is_inside_current_symbol = (
current_symbol is not None
and current_symbol.is_function
and current_symbol.rebased_addr == current_function_addr
and current_symbol_size > 0
and current_function_addr <= target_addr < current_function_addr + current_symbol_size
)
if not target_is_inside_current_symbol:
target_func_addr = target_addr
# last resort: the block probably belongs to the current function
if target_func_addr is None:
target_func_addr = current_function_addr
@ -5193,6 +5354,30 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
self._ro_region_cdata_cache.append(content_buf)
pyvex.pvc.register_readonly_region(section.vaddr, section.memsize, content_buf)
elif self.project.arch.name in {"AMD64", "X86"} and isinstance(self.project.simos, SimWindows):
# register sections that hold jump/call targets so that rip-relative import calls and jumps
# (call/jmp qword ptr [rip+disp]) can be constant-folded at lift time and resolved without a re-lift
# and resolver dispatch:
# - non-writable sections (e.g. .rdata, the bound IAT), and
# - the delay-load import table (.didat): although writable, its slots are static during CFG recovery
# and point to the delay-load thunks, which MemoryLoadResolver already resolves by reading them.
# The folded value is only accepted when it is a valid jump target, so registering these regions cannot
# introduce edges the timeless load resolvers would not also produce.
self._ro_region_cdata_cache = []
for section in self.project.loader.main_object.sections:
register = (section.is_readable and not section.is_writable and section.memsize >= 8) or (
section.name == ".didat" and section.is_readable and section.memsize >= 8
)
if register:
try:
content = self.project.loader.memory.load(section.vaddr, section.memsize)
except KeyError:
continue
content_buf = pyvex.ffi.from_buffer(content)
self._ro_region_cdata_cache.append(content_buf)
pyvex.pvc.register_readonly_region(section.vaddr, section.memsize, content_buf)
self._fold_ro_const_loads = bool(self._ro_region_cdata_cache)
def _lifter_deregister_readonly_regions(self):
pyvex.pvc.deregister_all_readonly_regions()
self._ro_region_cdata_cache = None
@ -5456,6 +5641,7 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
collect_data_refs=True,
strict_block_end=True,
load_from_ro_regions=True,
const_prop=self._fold_ro_const_loads,
initial_regs=initial_regs,
)
irsb = lifted_block.vex_nostmt # may raise SimTranslationError
@ -5500,6 +5686,7 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
collect_data_refs=True,
strict_block_end=True,
load_from_ro_regions=True,
const_prop=self._fold_ro_const_loads,
initial_regs=initial_regs,
)
irsb = lifted_block.vex_nostmt
@ -5678,6 +5865,16 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
)
irsb_string = irsb_string[: irsb.size]
if (
cfg_job.job_type == CFGJobType.COMPLETE_SCANNING
and addr == current_function_addr
and irsb.jumpkind == "Ijk_NoDecode"
):
# linear sweep decided that this block is undecodable. because drop_bad_functions() will remove this
# function anyway, we bail out early and mark the whole block as nodecode for performance.
self._seg_list.occupy(real_addr, max(irsb.size, 1), "nodecode")
return None, None, None, None
# Occupy the block in segment list
if irsb is not None and irsb.size > 0:
self._seg_list.occupy(real_addr, irsb.size, "code")

View file

@ -341,7 +341,6 @@ class CFGFastSoot(CFGFast):
addr = cfg_node.addr
stmts_count = cfg_node.size
else:
addr = addr
stmts_count = size
if addr is None:

View file

@ -25,7 +25,7 @@ class AMD64PeIatResolver(IndirectJumpResolver):
if jumpkind not in {"Ijk_Call", "Ijk_Boring"}:
return False
insns = self.project.factory.block(addr).capstone.insns
insns = self.project.factory.block(addr, size=block.size).capstone.insns
if not insns:
return False
if not insns[-1].insn.operands:
@ -36,7 +36,7 @@ class AMD64PeIatResolver(IndirectJumpResolver):
return bool(opnd.type == X86_OP_MEM and opnd.mem.disp and opnd.mem.base == X86_REG_RIP and opnd.mem.index == 0)
def resolve(self, cfg, addr, func_addr, block, jumpkind, func_graph_complete: bool = True, **kwargs): # pylint:disable=unused-argument
call_insn = self.project.factory.block(addr).capstone.insns[-1].insn
call_insn = self.project.factory.block(addr, size=block.size).capstone.insns[-1].insn
addr = (call_insn.disp + call_insn.address + call_insn.size) & 0xFFFF_FFFF_FFFF_FFFF
target = cfg._fast_memory_load_pointer(addr)
if target is None:

View file

@ -14,10 +14,6 @@ from __future__ import annotations
import logging
import struct
from typing import TYPE_CHECKING
if TYPE_CHECKING:
pass
log = logging.getLogger(__name__)

View file

@ -17,6 +17,7 @@ from angr.analyses.analysis import Analysis, register_analysis
from angr.analyses.calling_convention import CallingConventionAnalysis
from angr.analyses.cfg import CFGFast
from angr.analyses.variable_recovery import VariableRecoveryFast
from angr.errors import AngrRuntimeError
from angr.knowledge_plugins.cfg import CFGModel
from angr.knowledge_plugins.functions.function import PrototypeSource
from angr.simos import SimWindows
@ -34,6 +35,10 @@ _l = logging.getLogger(name=__name__)
_mp_context = mp_context()
# How long (in seconds) the result collector keeps polling the results queue after it observes that every worker
# process has exited.
DEAD_WORKER_GRACE_PERIOD = 5.0
class CallingConventionAnalysisMode(Enum):
"""
@ -295,6 +300,9 @@ class CompleteCallingConventionsAnalysis(Analysis):
self._update_progress(0)
idx = 0
assert self._results_lock is not None
# the timestamp when we first noticed that all workers had exited.
# None while at least one worker is still alive.
all_workers_dead_since: float | None = None
while idx < total_funcs:
try:
with self._results_lock:
@ -302,9 +310,26 @@ class CompleteCallingConventionsAnalysis(Analysis):
True, timeout=0.01
)
except queue.Empty:
# No result is available right now.
# If all workers have exited then no result will ever become available, and looping here would
# hang forever (angr #6529). Detect that and fail loudly.
if any(proc.is_alive() for proc in procs):
all_workers_dead_since = None
elif all_workers_dead_since is None:
all_workers_dead_since = time.time()
elif time.time() - all_workers_dead_since >= DEAD_WORKER_GRACE_PERIOD:
exitcodes = ", ".join(f"{proc.name}: {proc.exitcode}" for proc in procs)
raise AngrRuntimeError(
f"All {len(procs)} CompleteCallingConventions worker processes exited before the "
f"analysis finished; only {idx} of {total_funcs} functions were analyzed. Worker exit "
f"codes: {exitcodes}."
) from None
time.sleep(0.1)
continue
# we made progress, so any previously observed all-dead state is no longer interesting
all_workers_dead_since = None
func = self.kb.functions.get_by_addr(func_addr)
if cc is not None or proto is not None:
func.calling_convention = cc
@ -376,8 +401,13 @@ class CompleteCallingConventionsAnalysis(Analysis):
except Exception: # pylint:disable=broad-except
_l.error("Worker %d: Exception occurred during _analyze_core().", worker_id, exc_info=True)
cc, proto, proto_libname, proto_source, varman = None, None, None, None, None
with self._results_lock:
self._results.put((func_addr, cc, proto, proto_libname, proto_source, varman))
try:
with self._results_lock:
self._results.put((func_addr, cc, proto, proto_libname, proto_source, varman))
except Exception: # pylint:disable=broad-except
_l.error(
"Worker %d: Failed to report the result for function %#x.", worker_id, func_addr, exc_info=True
)
def _analyze_core(
self, func_addr: int

View file

@ -756,7 +756,7 @@ class DDG(Analysis):
matched = True
except (SimUnsatError, SimSolverModeError, ZeroDivisionError):
# ignore
matched = matched
pass
if not matched:
break
@ -1017,7 +1017,7 @@ class DDG(Analysis):
self._stmt_graph_annotate_edges(self._register_edges[reg_offset], subtype="mem_addr")
reg_variable = SimRegisterVariable(reg_offset, self._get_register_size(reg_offset))
prev_defs = self._def_lookup(reg_variable)
for loc, _ in prev_defs.items():
for loc in prev_defs:
v = ProgramVariable(reg_variable, loc, arch=self.project.arch)
self._data_graph_add_edge(v, prog_var, type="mem_addr")
@ -1039,7 +1039,7 @@ class DDG(Analysis):
self._stmt_graph_annotate_edges(self._register_edges[reg_offset], subtype="mem_data")
reg_variable = SimRegisterVariable(reg_offset, self._get_register_size(reg_offset))
prev_defs = self._def_lookup(reg_variable)
for loc, _ in prev_defs.items():
for loc in prev_defs:
v = ProgramVariable(reg_variable, loc, arch=self.project.arch)
self._data_graph_add_edge(v, prog_var, type="mem_data")
@ -1156,9 +1156,11 @@ class DDG(Analysis):
if not action.reg_deps and not action.tmp_deps:
# moving a constant into the register
# try to parse out the constant from statement
const_variable = SimConstantVariable(size=1)
if statement is not None and isinstance(statement.data, pyvex.IRExpr.Const):
const_variable = SimConstantVariable(value=statement.data.con.value, size=statement.data.con.size)
else:
# use a default value of 0 if we cannot find the constant
const_variable = SimConstantVariable(1, value=0)
const_pv = ProgramVariable(const_variable, location, arch=self.project.arch)
self._data_graph_add_edge(const_pv, pv)
@ -1229,7 +1231,7 @@ class DDG(Analysis):
if not action.tmp_deps and not self._variables_per_statement and not ast:
# read in a constant
# try to parse out the constant from statement
const_variable = SimConstantVariable(size=1)
const_variable = SimConstantVariable(size=1, value=0) # default value if we can't find the constant
if statement is not None:
if isinstance(statement, pyvex.IRStmt.Dirty):
l.warning("Dirty statements are not supported in DDG for now.")
@ -1497,7 +1499,7 @@ class DDG(Analysis):
# Group all dependencies first
block_addr_to_func = {}
for _, func in self.kb.functions.items():
for func in self.kb.functions.values():
for block in func.blocks:
block_addr_to_func[block.addr] = func

View file

@ -4,7 +4,7 @@ from __future__ import annotations
import logging
import os
from collections import defaultdict
from collections.abc import Container, Iterable
from collections.abc import Iterable
from enum import Enum
from typing import TYPE_CHECKING, Any
@ -41,8 +41,8 @@ from angr.ailment.statement import (
WeakAssignment,
)
from angr.analyses.analysis import AnalysesHub, Analysis
from angr.analyses.s_propagator import SPropagatorAnalysis
from angr.analyses.s_reaching_definitions import SRDAModel, SReachingDefinitionsAnalysis
from angr.analyses.s_propagator import SPropagator
from angr.analyses.s_reaching_definitions import SRDAModel, SReachingDefinitions
from angr.code_location import AILCodeLocation
from angr.errors import AngrRuntimeError
from angr.knowledge_plugins.functions.function import Function
@ -80,6 +80,58 @@ _l = logging.getLogger(__name__)
_VERIFY_INCREMENTAL_RD = os.environ.get("VERIFY_INCREMENTAL_RD", "").lower() not in {"", "0", "no", "false"}
def _strongly_connected_components(succs: dict[int, set[int]]):
"""
Iterative Tarjan SCC over a plain adjacency map. Yields sets of node IDs, like
networkx.strongly_connected_components().
"""
index_of: dict[int, int] = {}
lowlink: dict[int, int] = {}
on_stack: set[int] = set()
stack: list[int] = []
counter = 0
for root, root_succs in succs.items():
if root in index_of:
continue
work = [(root, iter(root_succs))]
index_of[root] = lowlink[root] = counter
counter += 1
stack.append(root)
on_stack.add(root)
while work:
node, it = work[-1]
advanced = False
for succ in it:
if succ not in index_of:
index_of[succ] = lowlink[succ] = counter
counter += 1
stack.append(succ)
on_stack.add(succ)
work.append((succ, iter(succs[succ])))
advanced = True
break
if succ in on_stack and index_of[succ] < lowlink[node]:
lowlink[node] = index_of[succ]
if advanced:
continue
work.pop()
if work:
parent = work[-1][0]
lowlink[parent] = min(lowlink[parent], lowlink[node])
if lowlink[node] == index_of[node]:
scc = set()
while True:
member = stack.pop()
on_stack.discard(member)
scc.add(member)
if member == node:
break
yield scc
class HasVVarNotification(Exception):
"""
Notifies the existence of a VirtualVariable.
@ -189,7 +241,7 @@ class AILSimplifier(Analysis):
self.func = func
self.func_graph = func_graph
self._reaching_definitions: SRDAModel | None = None
self._propagator: SPropagatorAnalysis | None = None
self._propagator: SPropagator | None = None
self._remove_dead_memdefs = remove_dead_memdefs
self._stackarg_offset_manager = stackarg_offset_manager
@ -208,6 +260,11 @@ class AILSimplifier(Analysis):
self._arg_vvars = arg_vvars
self._avoid_vvar_ids = avoid_vvar_ids if avoid_vvar_ids is not None else set()
self._propagator_dead_vvar_ids: set[int] = set()
# per-block cache of dirty/ccall-defined vvar IDs, keyed by block key, validated by block identity
self._dirty_vvar_scan_cache: dict[tuple[int, int | None], tuple[Block, set[int]]] = {}
# only set to True when any simplification pass has modified the graph or updated any blocks.
# skips _remove_dead_assignments if this flag is False.
self._should_eliminate_dead_assignments: bool = True
self._calls_to_remove: set[AILCodeLocation] = set()
self._assignments_to_remove: set[AILCodeLocation] = set()
@ -335,33 +392,32 @@ class AILSimplifier(Analysis):
AILGraphWalker(self.func_graph, _handler, replace_nodes=True).walk()
self.blocks = {}
self._should_eliminate_dead_assignments = True
def _compute_reaching_definitions(self) -> SRDAModel:
# Computing reaching definitions or return the cached one
if self._reaching_definitions is not None:
return self._reaching_definitions
func_args = {vvar for vvar, _ in self._arg_vvars.values()} if self._arg_vvars else set()
rd = (
self.project.analyses[SReachingDefinitionsAnalysis]
.prep()(
subject=self.func,
func_graph=self.func_graph,
func_args=func_args,
use_callee_saved_regs_at_return=self._use_callee_saved_regs_at_return,
# track_tmps=True,
)
.model
)
rd = SReachingDefinitions(
self.project,
subject=self.func,
func_graph=self.func_graph,
func_args=func_args,
use_callee_saved_regs_at_return=self._use_callee_saved_regs_at_return,
# track_tmps=True,
).model
self._reaching_definitions = rd
return rd
@timethis
def _compute_propagation(self) -> SPropagatorAnalysis:
def _compute_propagation(self) -> SPropagator:
# Propagate expressions or return the existing result
if self._propagator is not None:
return self._propagator
func_args = {vvar for vvar, _ in self._arg_vvars.values()} if self._arg_vvars else set()
prop = self.project.analyses[SPropagatorAnalysis].prep(fail_fast=self._fail_fast)(
prop = SPropagator(
self.project,
subject=self.func,
func_graph=self.func_graph,
func_args=func_args,
@ -374,6 +430,7 @@ class AILSimplifier(Analysis):
)
self._propagator = prop
self._propagator_dead_vvar_ids = prop.dead_vvar_ids
self._should_eliminate_dead_assignments = True
return prop
@timethis
@ -527,8 +584,8 @@ class AILSimplifier(Analysis):
vvar, simvar = self._arg_vvars[func_arg_idx]
if vvar.varid == new_vvar.varid:
simvar_new = simvar.copy()
simvar_new._hash = None
simvar_new.size = new_vvar.size
simvar_new.clear_hash()
self._arg_vvars[func_arg_idx] = new_vvar, simvar_new
return narrowed
@ -933,11 +990,12 @@ class AILSimplifier(Analysis):
}
reps = filtered_reps
r, new_block = BlockSimplifier._replace_and_build(
r, new_block = BlockSimplifier.replace_and_build(
block, reps, self._ail_manager, gp=self._gp, replace_loads=replace_loads
)
replaced |= r
self.blocks[block] = new_block
if r:
self.blocks[block] = new_block
if replaced:
# blocks have been rebuilt - expression propagation results are no longer reliable
@ -1099,6 +1157,9 @@ class AILSimplifier(Analysis):
# out-of-date
updated_locs: set[AILCodeLocation] = set()
# built on-demand
stack_defs_by_offset: dict[int, list[Definition[atoms.VirtualVariable, AILCodeLocation]]] | None = None
for _, atom in sorted_loc_and_atoms:
eqs = equivalences[atom]
filtered_eqs: list[tuple[Equivalence, VirtualVariable, bool]] = []
@ -1169,14 +1230,9 @@ class AILSimplifier(Analysis):
rd = self._compute_reaching_definitions()
the_def = None
if to_replace_is_def:
# find defs
defs: Container[Definition[atoms.VirtualVariable, AILCodeLocation]] = []
for def_ in rd.all_definitions:
if def_.atom.varid == to_replace.varid:
defs.append(def_)
if len(defs) != 1:
the_def = rd.get_definition_by_varid(to_replace.varid)
if the_def is None:
continue
the_def = defs[0]
else:
# find uses
defs = rd.get_uses_by_location(eq.codeloc)
@ -1202,38 +1258,42 @@ class AILSimplifier(Analysis):
# (a) the on-stack or in-register copy of it has never been modified in this function
# (b) the function argument register has never been updated.
# TODO: we may loosen requirement (b) once we have real register versioning in AIL.
defs = [def_ for def_ in rd.all_definitions if def_.codeloc == eq.codeloc]
defs = rd.get_defs_by_location(eq.codeloc)
all_uses_with_def = None
replace_with = None
remove_initial_assignment = None
def_eq_rel = DefEqRelation.DEF_IS_FUNCARG
if defs and len(defs) == 1:
arg_copy_def = defs[0]
if len(defs) == 1:
arg_copy_def = next(iter(defs))
if (isinstance(arg_copy_def.atom, atoms.VirtualVariable) and arg_copy_def.atom.was_stack) or (
isinstance(arg_copy_def.atom, atoms.VirtualVariable) and arg_copy_def.atom.was_reg
):
# found the copied definition (either a stack variable or a register variable)
# Make sure there is no other write to this stack location if the copy is a stack variable
if (
isinstance(arg_copy_def.atom, atoms.VirtualVariable)
and arg_copy_def.atom.was_stack
and any(
(def_ != arg_copy_def and def_.atom.stack_offset == arg_copy_def.atom.stack_offset)
for def_ in rd.all_definitions
if isinstance(def_.atom, atoms.VirtualVariable) and def_.atom.was_stack
)
):
continue
if isinstance(arg_copy_def.atom, atoms.VirtualVariable) and arg_copy_def.atom.was_stack:
if stack_defs_by_offset is None:
stack_defs_by_offset = defaultdict(list)
for def_ in rd.all_definitions:
if def_.atom.was_stack:
stack_defs_by_offset[def_.atom.stack_offset].append(def_)
if any(
def_ != arg_copy_def
for def_ in stack_defs_by_offset.get(arg_copy_def.atom.stack_offset, ())
):
continue
# Make sure the register is never updated across this function
if any(
(def_ != the_def and def_.atom == the_def.atom)
for def_ in rd.all_definitions
if isinstance(def_.atom, atoms.VirtualVariable)
and def_.atom.was_reg
and rd.get_vvar_uses(def_.atom)
# Make sure the register is never updated across this function. Only the definition of
# the_def's own vvar id can have an equal atom (atom equality is (varid, size)), and SSA gives
# that id exactly one definition, so this is a single lookup rather than a full scan.
other_def = rd.get_definition_by_varid(the_def.atom.varid)
if (
other_def is not None
and other_def != the_def
and other_def.atom == the_def.atom
and other_def.atom.was_reg
and rd.get_vvar_uses(other_def.atom)
):
continue
@ -1269,13 +1329,7 @@ class AILSimplifier(Analysis):
def_eq_rel = DefEqRelation.DEF_EQ_SAME_BLOCK
else:
# the definition is in the predecessor block of the eq
eq_block = next(
iter(
bb
for bb in self.func_graph
if bb.addr == eq.codeloc.block_addr and bb.idx == eq.codeloc.block_idx
)
)
eq_block = addr_and_idx_to_block[(eq.codeloc.block_addr, eq.codeloc.block_idx)]
eq_block_preds = set(self.func_graph.predecessors(eq_block))
if not any(
pred.addr == the_def.codeloc.block_addr and pred.idx == the_def.codeloc.block_idx
@ -1738,13 +1792,6 @@ class AILSimplifier(Analysis):
if isinstance(eq.atom0, VirtualVariable):
src = used_expr
dst: Expression = call.copy()
if isinstance(dst, SideEffectStatement):
dst_bits = dst.ret_expr.bits if dst.ret_expr is not None else dst.bits
# extract the Call expression from the SideEffectStatement
dst = dst.expr
dst.bits = dst_bits
if src.bits != dst.bits and not eq.is_weakassignment:
dst = Convert(
self._ail_manager.next_atom(),
@ -1849,6 +1896,14 @@ class AILSimplifier(Analysis):
@timethis
def _iteratively_remove_dead_assignments(self) -> bool:
if (
not self._should_eliminate_dead_assignments
and not self.blocks
and not self._calls_to_remove
and not self._assignments_to_remove
):
# nothing that _remove_dead_assignments() reads has changed since it last reported nothing to remove
return False
anything_removed = False
while True:
r, changed_block_keys = self._remove_dead_assignments()
@ -1869,6 +1924,8 @@ class AILSimplifier(Analysis):
# propagation results are no longer reliable after removing statements
self._propagator = None
self._should_eliminate_dead_assignments = False
# NoOp placeholders are left in the graph and the reaching-definitions cache is kept valid: subsequent
# simplification steps reuse it instead of rebuilding from scratch. The placeholders are compacted away once,
# at the end of _simplify().
@ -1891,16 +1948,13 @@ class AILSimplifier(Analysis):
# rebuild on the current (NoOp-containing) graph.
assert self._reaching_definitions is not None
func_args = {vvar for vvar, _ in self._arg_vvars.values()} if self._arg_vvars else set()
reference = (
self.project.analyses[SReachingDefinitionsAnalysis]
.prep()(
subject=self.func,
func_graph=self.func_graph,
func_args=func_args,
use_callee_saved_regs_at_return=self._use_callee_saved_regs_at_return,
)
.model
)
reference = SReachingDefinitions(
self.project,
subject=self.func,
func_graph=self.func_graph,
func_args=func_args,
use_callee_saved_regs_at_return=self._use_callee_saved_regs_at_return,
).model
if self._reaching_definitions.canonical_form() != reference.canonical_form():
raise AssertionError("Incremental SRDA update diverged from a full rebuild")
@ -1938,7 +1992,7 @@ class AILSimplifier(Analysis):
if uses is None:
vvar = rd.varid_to_vvar[vvar_id]
def_codeloc = rd.all_vvar_definitions[vvar_id]
def_codeloc = codeloc
if def_codeloc.is_extern:
def_stmt = None
else:
@ -2052,12 +2106,22 @@ class AILSimplifier(Analysis):
# this statement declares more than one variable. we should handle it surgically
# case 1: stmt.ret_expr and stmt.fp_ret_expr are both set, but one of them is not used
if isinstance(stmt.ret_expr, VirtualVariable) and stmt.ret_expr.varid in dead_vvar_ids:
stmt = stmt.copy()
stmt.ret_expr = None
stmt = SideEffectStatement(
self._ail_manager.next_atom(),
stmt.expr,
ret_expr=None,
fp_ret_expr=stmt.fp_ret_expr,
**stmt.tags,
)
simplified = True
if isinstance(stmt.fp_ret_expr, VirtualVariable) and stmt.fp_ret_expr.varid in dead_vvar_ids:
stmt = stmt.copy()
stmt.fp_ret_expr = None
stmt = SideEffectStatement(
self._ail_manager.next_atom(),
stmt.expr,
ret_expr=stmt.ret_expr,
fp_ret_expr=None,
**stmt.tags,
)
simplified = True
if idx in stmts_to_remove and idx not in stmts_to_keep and not isinstance(stmt, DirtyStatement):
@ -2118,9 +2182,9 @@ class AILSimplifier(Analysis):
isinstance(stmt.ret_expr, VirtualVariable) and stmt.ret_expr.was_combo_reg
):
# both the return expr and the fp_ret_expr are not used
stmt = stmt.copy()
stmt.ret_expr = None
stmt.fp_ret_expr = None
stmt = SideEffectStatement(
self._ail_manager.next_atom(), stmt.expr, ret_expr=None, fp_ret_expr=None, **stmt.tags
)
simplified = True
else:
# Should not happen!
@ -2170,21 +2234,32 @@ class AILSimplifier(Analysis):
def _find_cyclic_dependent_phis_and_dirty_vvars(self, rd: SRDAModel, dead_vvar_ids: set[int]) -> set[int]:
blocks_dict: dict[tuple[int, int | None], Block] = {(bb.addr, bb.idx): bb for bb in self.func_graph}
# find dirty vvars and vexccall vvars
dirty_vvar_ids = set()
# cache dirty or ccall vvar IDs per block to avoid re-scanning
# TODO: Move this cache to ailment.Block once per-block defs/uses cache lands on master.
cache = self._dirty_vvar_scan_cache
for bb in self.func_graph:
for stmt in bb.statements:
# reg/tmp = ccall(...)
# we see tmps when it's used in a cycle;
# see binary ddc2b4cbf6ac841524375cdf82b93b9948f8ea09bbf6e8bf3410e6bc410a9d95 function 0x18001722c
# block 0x18001724c
if (
isinstance(stmt, Assignment)
and isinstance(stmt.dst, VirtualVariable)
and (stmt.dst.was_reg or stmt.dst.was_tmp)
and isinstance(stmt.src, (DirtyExpression, VEXCCallExpression))
):
dirty_vvar_ids.add(stmt.dst.varid)
key = bb.addr, bb.idx
entry = cache.get(key)
if entry is not None and entry[0] is bb:
block_dirty_ids = entry[1]
else:
block_dirty_ids = set()
for stmt in bb.statements:
# reg/tmp = ccall(...)
# we see tmps when it's used in a cycle;
# see binary ddc2b4cbf6ac841524375cdf82b93b9948f8ea09bbf6e8bf3410e6bc410a9d95 function 0x18001722c
# block 0x18001724c
if (
isinstance(stmt, Assignment)
and isinstance(stmt.dst, VirtualVariable)
and (stmt.dst.was_reg or stmt.dst.was_tmp)
and isinstance(stmt.src, (DirtyExpression, VEXCCallExpression))
):
block_dirty_ids.add(stmt.dst.varid)
cache[key] = bb, block_dirty_ids
if block_dirty_ids:
dirty_vvar_ids |= block_dirty_ids
phi_and_dirty_vvar_ids = (rd.phi_vvar_ids | dirty_vvar_ids).difference(dead_vvar_ids)
@ -2202,18 +2277,25 @@ class AILSimplifier(Analysis):
vvar_used_by[used_by_varid].add(var_id) # probably unnecessary
vvar_used_by[var_id] |= self._get_vvar_used_by(var_id, rd, blocks_dict).difference(dead_vvar_ids)
g = networkx.DiGraph()
# build a plain adjacency map instead of a throwaway networkx DiGraph for better performance. the performance
# improvement is observable on notepad.exe:NPInit
# TODO: Investigate if switching to rustworkx eliminates the need for this optimization.
dummy_vvar_id = -1
succs_map: dict[int, set[int]] = {}
for var_id, used_by_initial in vvar_used_by.items():
for u in used_by_initial:
if u is None:
# we can't have None in networkx.DiGraph
g.add_edge(var_id, dummy_vvar_id)
else:
g.add_edge(var_id, u)
if not used_by_initial:
continue
targets = {dummy_vvar_id if u is None else u for u in used_by_initial}
if var_id in succs_map:
succs_map[var_id] |= targets
else:
succs_map[var_id] = set(targets)
for target in targets:
if target not in succs_map:
succs_map[target] = set()
cyclic_dependent_phi_varids = set()
for scc in networkx.strongly_connected_components(g):
for scc in _strongly_connected_components(succs_map):
if len(scc) == 1:
continue
@ -2224,11 +2306,9 @@ class AILSimplifier(Analysis):
if varid in vvar_used_by and None in vvar_used_by[varid]:
bail = True
break
if bail is False:
succs = list(g.successors(varid))
if any(succ_varid not in scc for succ_varid in succs):
bail = True
break
if any(succ_varid not in scc for succ_varid in succs_map[varid]):
bail = True
break
if bail:
continue
@ -2258,7 +2338,14 @@ class AILSimplifier(Analysis):
def _handle_VEXCCallExpression(
expr_idx: int, expr: VEXCCallExpression, stmt_idx: int, stmt: Statement | None, block: Block | None
) -> Expression:
r_expr = AILBlockRewriter._handle_VEXCCallExpression(walker, expr_idx, expr, stmt_idx, stmt, block)
r_expr = AILBlockRewriter._handle_VEXCCallExpression( # pylint:disable=protected-access
walker,
expr_idx,
expr,
stmt_idx,
stmt,
block,
)
rewriter = rewriter_cls(r_expr, self.project, self._ail_manager, rename_ccalls=self._should_rename_ccalls)
if rewriter.result is not None:
_any_update.v = True
@ -2304,7 +2391,7 @@ class AILSimplifier(Analysis):
rewriter = rewriter_cls(stmt, self.project.arch, self._ail_manager)
if rewriter.result is not None:
_any_update.v = True
if walker._update_block and block is not None:
if walker._update_block and block is not None: # pylint:disable=protected-access
block.statements[stmt_idx] = rewriter.result # type: ignore
assert isinstance(rewriter.result, Statement)
return rewriter.result
@ -2313,7 +2400,14 @@ class AILSimplifier(Analysis):
def _handle_DirtyExpression(
expr_idx: int, expr: DirtyExpression, stmt_idx: int, stmt: Statement | None, block: Block | None
):
r_expr = AILBlockRewriter._handle_DirtyExpression(walker, expr_idx, expr, stmt_idx, stmt, block)
r_expr = AILBlockRewriter._handle_DirtyExpression( # pylint:disable=protected-access
walker,
expr_idx,
expr,
stmt_idx,
stmt,
block,
)
assert isinstance(r_expr, DirtyExpression)
rewriter = rewriter_cls(r_expr, self.project.arch, self._ail_manager)
if rewriter.result is not None:

View file

@ -8,9 +8,8 @@ from typing import TYPE_CHECKING
from angr.ailment.expression import Call, Const, Convert, Expression, Load, Register, Tmp, VirtualVariable
from angr.ailment.manager import Manager
from angr.ailment.statement import Assignment, Jump, SideEffectStatement, Statement, Store
from angr.analyses.analysis import Analysis, register_analysis
from angr.analyses.s_propagator import SPropagatorAnalysis
from angr.analyses.s_reaching_definitions import SRDAModel, SReachingDefinitionsAnalysis
from angr.analyses.s_propagator import SPropagator
from angr.analyses.s_reaching_definitions import SRDAModel, SReachingDefinitions
from angr.code_location import AILCodeLocation
from angr.knowledge_plugins.key_definitions import atoms
from angr.utils.ssa import has_reference_to_vvar
@ -34,6 +33,7 @@ from .utils import (
if TYPE_CHECKING:
from angr.ailment.block import Block
from angr.project import Project
_l = logging.getLogger(name=__name__)
@ -42,35 +42,118 @@ _l = logging.getLogger(name=__name__)
_HAS_CALL_EXPR_WALKER = HasCallExprWalker()
class BlockSimplifier(Analysis):
class PeepholeOptimizationBundle:
"""
PeepholeOptimizationBundle describes a set of initialized peephole optimizer instances and the dispatch structures
derived from them. This bundle of peephole optimizations is reusable across `BlockSimplifier` invocations (so we
avoid rebuilding the same optimizer instances).
"""
__slots__ = (
"_params",
"expr_opts",
"expr_walker",
"multistmt_opts",
"stmt_opts",
"stmt_opts_by_kind",
)
def __init__(
self,
project,
kb,
ail_manager: Manager,
func_addr: int | None = None,
preserve_vvar_ids: set[int] | None = None,
type_hints: list[tuple[atoms.VirtualVariable | atoms.MemoryLocation, str]] | None = None,
peephole_optimizations: Iterable[
type[PeepholeOptimizationStmtBase | PeepholeOptimizationExprBase | PeepholeOptimizationMultiStmtBase]
]
| None = None,
):
if peephole_optimizations is None:
expr_classes: Iterable = EXPR_OPTS
stmt_classes: Iterable = STMT_OPTS
multistmt_classes: Iterable = MULTI_STMT_OPTS
else:
peephole_optimizations = tuple(peephole_optimizations)
expr_classes = [cls for cls in peephole_optimizations if issubclass(cls, PeepholeOptimizationExprBase)]
stmt_classes = [cls for cls in peephole_optimizations if issubclass(cls, PeepholeOptimizationStmtBase)]
multistmt_classes = [
cls for cls in peephole_optimizations if issubclass(cls, PeepholeOptimizationMultiStmtBase)
]
args = (project, kb, ail_manager, func_addr, preserve_vvar_ids, type_hints)
self.expr_opts = [cls(*args) for cls in expr_classes]
self.stmt_opts = [cls(*args) for cls in stmt_classes]
self.multistmt_opts = [cls(*args) for cls in multistmt_classes]
self.stmt_opts_by_kind = build_stmt_opts_by_kind(self.stmt_opts)
self.expr_walker = _PeepholeExprsWalker(expr_opts=self.expr_opts)
self._params = (project, ail_manager, func_addr, preserve_vvar_ids, type_hints, peephole_optimizations)
def matches(
self,
project,
ail_manager: Manager,
func_addr: int | None,
preserve_vvar_ids: set[int] | None,
type_hints: list | None,
peephole_optimizations,
) -> bool:
p_project, p_manager, p_func_addr, p_preserve, p_hints, p_opts = self._params
return (
p_project is project
and p_manager is ail_manager
and p_func_addr == func_addr
and p_preserve is preserve_vvar_ids
and p_hints is type_hints
and (
p_opts is peephole_optimizations
or (
p_opts is not None
and peephole_optimizations is not None
and p_opts == tuple(peephole_optimizations)
)
)
)
class BlockSimplifier:
"""
Simplify an AIL block.
Deliberately not an :class:`Analysis`: it is instantiated once per block, hundreds of times per decompilation,
so it skips the analysis-factory ceremony. Instantiate it directly with the project as the first argument;
exceptions always propagate.
"""
def __init__(
self,
project: Project,
block: Block | None,
ail_manager: Manager,
func_addr: int | None = None,
stack_pointer_tracker=None,
peephole_optimizations: None
| (
Iterable[
type[PeepholeOptimizationStmtBase]
| type[PeepholeOptimizationExprBase]
| type[PeepholeOptimizationMultiStmtBase]
]
) = None,
peephole_optimizations: Iterable[
type[PeepholeOptimizationStmtBase | PeepholeOptimizationExprBase | PeepholeOptimizationMultiStmtBase]
]
| None = None,
preserve_vvar_ids: set[int] | None = None,
type_hints: list[tuple[atoms.VirtualVariable | atoms.MemoryLocation, str]] | None = None,
cached_reaching_definitions=None,
cached_propagator=None,
peephole_bundle: PeepholeOptimizationBundle | None = None,
):
"""
:param block: The AIL block to simplify. Setting it to None to skip calling self._analyze(), which is useful
in test cases.
:param peephole_bundle: A pre-built PeepholeOptimizationBundle to reuse. Its construction parameters must
match this BlockSimplifier's; callers that simplify many blocks should build one bundle and
pass it to every BlockSimplifier they create.
"""
self.project = project
self.kb = project.kb
self.block = block
self.func_addr = func_addr
@ -79,42 +162,25 @@ class BlockSimplifier(Analysis):
self._type_hints = type_hints
self._ail_manager = ail_manager
if peephole_optimizations is None:
self._expr_peephole_opts = [
cls(self.project, self.kb, ail_manager, self.func_addr, self._preserve_vvar_ids, self._type_hints)
for cls in EXPR_OPTS
]
self._stmt_peephole_opts = [
cls(self.project, self.kb, ail_manager, self.func_addr, self._preserve_vvar_ids, self._type_hints)
for cls in STMT_OPTS
]
self._multistmt_peephole_opts = [
cls(self.project, self.kb, ail_manager, self.func_addr, self._preserve_vvar_ids, self._type_hints)
for cls in MULTI_STMT_OPTS
]
self._stmt_peephole_opts_by_kind = build_stmt_opts_by_kind(self._stmt_peephole_opts)
else:
self._expr_peephole_opts = [
cls(self.project, self.kb, ail_manager, self.func_addr, self._preserve_vvar_ids, self._type_hints)
for cls in peephole_optimizations
if issubclass(cls, PeepholeOptimizationExprBase)
]
self._stmt_peephole_opts = [
cls(self.project, self.kb, ail_manager, self.func_addr, self._preserve_vvar_ids, self._type_hints)
for cls in peephole_optimizations
if issubclass(cls, PeepholeOptimizationStmtBase)
]
self._multistmt_peephole_opts = [
cls(self.project, self.kb, ail_manager, self.func_addr, self._preserve_vvar_ids, self._type_hints)
for cls in peephole_optimizations
if issubclass(cls, PeepholeOptimizationMultiStmtBase)
]
self._stmt_peephole_opts_by_kind = build_stmt_opts_by_kind(self._stmt_peephole_opts)
if peephole_bundle is None:
peephole_bundle = PeepholeOptimizationBundle(
self.project,
self.kb,
ail_manager,
func_addr=self.func_addr,
preserve_vvar_ids=self._preserve_vvar_ids,
type_hints=self._type_hints,
peephole_optimizations=peephole_optimizations,
)
self._expr_peephole_opts = peephole_bundle.expr_opts
self._stmt_peephole_opts = peephole_bundle.stmt_opts
self._multistmt_peephole_opts = peephole_bundle.multistmt_opts
self._stmt_peephole_opts_by_kind = peephole_bundle.stmt_opts_by_kind
self.result_block = None
# cached peephole expression walker
self._expr_peephole_walker = _PeepholeExprsWalker(expr_opts=self._expr_peephole_opts)
self._expr_peephole_walker = peephole_bundle.expr_walker
# cached Propagator and ReachingDefinitions results. Clear them if the block is updated
self._propagator = cached_propagator
@ -125,25 +191,33 @@ class BlockSimplifier(Analysis):
def _analyze(self):
block = self.block
assert block is not None
ctr = 0
max_ctr = 30
new_block = self._eliminate_self_assignments(block)
new_block, changed = self._eliminate_self_assignments(block)
# True once dead-assignment elimination is known to have nothing to do on the block the loop below starts
# from -- either because it just ran over it without a change, or because its gate is off for that block.
dead_assignments_clean = True
if self._count_nonconstant_statements(new_block) >= 2 and self._has_propagatable_assignments(new_block):
new_block = self._eliminate_dead_assignments(new_block)
# Structural ``likes`` (idx-agnostic) instead of ``!=`` which always trips on fresh ``manager.next_atom()``
# ids even when nothing changed structurally.
# TODO: Keep track of changes and skip .likes(); .likes() is expensive.
if not new_block.likes(block):
new_block, dead_changed = self._eliminate_dead_assignments(new_block)
changed |= dead_changed
dead_assignments_clean = not dead_changed
if changed:
self._clear_cache()
block = new_block
while True:
ctr += 1
new_block = self._simplify_block_once(block)
# TODO: Keep track of changes and skip .likes(); .likes() is expensive.
if new_block.likes(block):
# the entry peephole pass is only useful on the first iteration: every later iteration receives the
# output of the previous iteration's exit peephole pass, so running peephole again on entry is redundant.
new_block, changed = self._simplify_block_once(
block, entry_peephole=ctr == 1, dead_assignments_clean=dead_assignments_clean
)
if not changed:
break
assert new_block is not None
self._clear_cache()
block = new_block
if ctr >= max_ctr:
@ -156,9 +230,10 @@ class BlockSimplifier(Analysis):
self.result_block = block
def _compute_propagation(self, block) -> SPropagatorAnalysis:
def _compute_propagation(self, block) -> SPropagator:
if self._propagator is None:
self._propagator = self.project.analyses[SPropagatorAnalysis].prep(fail_fast=self._fail_fast)(
self._propagator = SPropagator(
self.project,
subject=block,
func_addr=self.func_addr,
stack_pointer_tracker=self._stack_pointer_tracker,
@ -168,15 +243,12 @@ class BlockSimplifier(Analysis):
def _compute_reaching_definitions(self, block) -> SRDAModel:
if self._reaching_definitions is None:
self._reaching_definitions = (
self.project.analyses[SReachingDefinitionsAnalysis]
.prep(fail_fast=self._fail_fast)(
subject=block,
track_tmps=True,
func_addr=self.func_addr,
)
.model
)
self._reaching_definitions = SReachingDefinitions(
self.project,
subject=block,
track_tmps=True,
func_addr=self.func_addr,
).model
return self._reaching_definitions
def _clear_cache(self):
@ -191,35 +263,56 @@ class BlockSimplifier(Analysis):
def _count_nonconstant_statements(block) -> int:
return sum(1 for stmt in block.statements if not (isinstance(stmt, Jump) and isinstance(stmt.target, Const)))
def _simplify_block_once(self, block):
block = self._peephole_optimize(block)
def _simplify_block_once(
self, block, entry_peephole: bool = True, dead_assignments_clean: bool = False
) -> tuple[Block, bool]:
"""
Run one round of simplification. Returns the new block and if any step reported a change.
:param dead_assignments_clean: True if dead-assignment elimination is known to have nothing to do on
``block`` as passed in. Only meaningful together with ``entry_peephole``.
"""
changed = False
# True once we know ``block`` is untouched and already at the fixpoint of every pass that has run over it:
# re-running those passes on it cannot report a change.
clean = False
if entry_peephole:
block, peephole_changed, exprs_updated = self._peephole_optimize(block)
changed |= peephole_changed
clean = dead_assignments_clean and not peephole_changed and not exprs_updated
nonconstant_stmts = self._count_nonconstant_statements(block)
has_propagatable_assignments = self._has_propagatable_assignments(block)
# propagator
# only call propagation if something is potentially propagatable
if nonconstant_stmts >= 2 and has_propagatable_assignments:
propagator = self._compute_propagation(block)
new_block = block
if propagator.model is not None:
replacements = propagator.model.replacements
if replacements:
_, new_block = self._replace_and_build(
replaced, new_block = self.replace_and_build(
block, replacements, self._ail_manager, replace_registers=True
)
new_block = self._eliminate_self_assignments(new_block)
changed |= replaced
new_block, self_assign_changed = self._eliminate_self_assignments(new_block)
changed |= self_assign_changed
self._clear_cache()
else:
# Skipped calling Propagator
new_block = block
if nonconstant_stmts >= 2 and has_propagatable_assignments:
new_block = self._eliminate_dead_assignments(new_block)
if clean and new_block is block:
return block, False
return self._peephole_optimize(new_block)
if nonconstant_stmts >= 2 and has_propagatable_assignments:
new_block, dead_changed = self._eliminate_dead_assignments(new_block)
changed |= dead_changed
new_block, peephole_changed, _ = self._peephole_optimize(new_block)
return new_block, changed | peephole_changed
@staticmethod
def _replace_and_build(
def replace_and_build(
block: Block,
replacements: Mapping[AILCodeLocation, Mapping[Expression, Expression]],
ail_manager: Manager,
@ -309,7 +402,7 @@ class BlockSimplifier(Analysis):
return True, new_block
@staticmethod
def _eliminate_self_assignments(block):
def _eliminate_self_assignments(block) -> tuple[Block, bool]:
new_statements = []
for stmt in block.statements:
@ -329,9 +422,12 @@ class BlockSimplifier(Analysis):
continue
new_statements.append(stmt)
return block.copy(statements=new_statements)
if len(new_statements) == len(block.statements):
# nothing was eliminated; keep the original block
return block, False
return block.copy(statements=new_statements), True
def _eliminate_dead_assignments(self, block):
def _eliminate_dead_assignments(self, block) -> tuple[Block, bool]:
def _statement_has_calls(stmt: Statement) -> bool:
"""
Check if a statement has any Call expressions.
@ -354,7 +450,7 @@ class BlockSimplifier(Analysis):
new_statements = []
if not block.statements:
return block
return block, False
rd = self._compute_reaching_definitions(block)
block_loc = (block.addr, block.idx)
@ -382,6 +478,7 @@ class BlockSimplifier(Analysis):
used_tmps.add(tmp.tmp_idx)
# Remove dead assignments
changed = False
for idx, stmt in enumerate(block.statements):
if isinstance(stmt, Assignment):
# tmps can't execute new code
@ -390,39 +487,50 @@ class BlockSimplifier(Analysis):
# does .src involve any Call expressions? if so, we cannot remove it
if not _expression_has_calls(stmt.src):
changed = True
continue
if isinstance(stmt.dst, Tmp) and isinstance(stmt.src, Call):
# eliminate the assignment and replace it with the call
stmt = SideEffectStatement(self._ail_manager.next_atom(), stmt.src, **stmt.tags)
changed = True
if isinstance(stmt, Assignment) and stmt.src == stmt.dst:
changed = True
continue
new_statements.append(stmt)
return block.copy(statements=new_statements)
if not changed:
# nothing was eliminated; keep the original block
return block, False
return block.copy(statements=new_statements), True
#
# Peephole optimization
#
def _peephole_optimize(self, block):
# expressions are updated in place
peephole_optimize_exprs(block, self._expr_peephole_opts, walker=self._expr_peephole_walker)
def _peephole_optimize(self, block) -> tuple[Block, bool, bool]:
"""
Run all three peephole optimization levels on the block.
:return: (block, changed, exprs_updated), where ``changed`` is True if any optimization applied and
``exprs_updated`` is True if the expression walker rewrote any expression.
"""
exprs_updated = peephole_optimize_exprs(block, self._expr_peephole_opts, walker=self._expr_peephole_walker)
# run statement-level optimizations
statements, stmts_updated = peephole_optimize_stmts(
block, self._stmt_peephole_opts, stmt_opts_by_kind=self._stmt_peephole_opts_by_kind
block,
self._stmt_peephole_opts,
stmt_opts_by_kind=self._stmt_peephole_opts_by_kind,
fixpoint_exprs=self._expr_peephole_walker.fixpoint_stmts,
)
new_block = block.copy(statements=statements) if stmts_updated else block
statements, multi_stmts_updated = peephole_optimize_multistmts(new_block, self._multistmt_peephole_opts)
if not multi_stmts_updated:
return new_block
return new_block.copy(statements=statements)
register_analysis(BlockSimplifier, "AILBlockSimplifier")
if multi_stmts_updated:
new_block = new_block.copy(statements=statements)
return new_block, stmts_updated or multi_stmts_updated, exprs_updated

View file

@ -8,7 +8,6 @@ import archinfo
from angr.ailment import Const, Expr, Stmt
from angr.ailment.manager import Manager
from angr.analyses.analysis import Analysis, register_analysis
from angr.analyses.s_reaching_definitions import SRDAView
from angr.calling_conventions import (
SimCC,
@ -38,19 +37,28 @@ if TYPE_CHECKING:
from angr.analyses.s_reaching_definitions import SRDAModel
from angr.knowledge_plugins.functions import Function
from angr.knowledge_plugins.key_definitions.definition import Definition
from angr.project import Project
l = logging.getLogger(name=__name__)
class CallSiteMaker(Analysis):
class CallSiteMaker:
"""
Add calling convention, declaration, and args to a call site.
"""
def __init__(
self, block, *, ail_manager: Manager, reaching_definitions: SRDAModel | None = None, stack_pointer_tracker=None
self,
project: Project,
block,
*,
ail_manager: Manager,
reaching_definitions: SRDAModel | None = None,
stack_pointer_tracker=None,
):
self.project = project
self.kb = project.kb
self.block = block
self._reaching_definitions = reaching_definitions
@ -652,6 +660,3 @@ class CallSiteMaker(Analysis):
def _atom_idx(self) -> int:
return self._ail_manager.next_atom()
register_analysis(CallSiteMaker, "AILCallSiteMaker")

View file

@ -245,7 +245,8 @@ class AMD64CCallRewriter(CCallRewriterBase):
self.ail_manager.next_atom(), "And", [dep_1, flag], False, **ccall.tags
)
zero = Expr.Const(self.ail_manager.next_atom(), 0, dep_1.bits)
expr_op = "CmpEQ" if cond_v == AMD64_CondTypes["CondZ"] else "CmpNE"
# dep_1 holds the old flags: ZF is *set* iff the masked bit is non-zero
expr_op = "CmpNE" if cond_v == AMD64_CondTypes["CondZ"] else "CmpEQ"
r = Expr.BinaryOp(ccall.idx, expr_op, (masked_dep, zero), False, **ccall.tags)
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
@ -319,6 +320,55 @@ class AMD64CCallRewriter(CCallRewriterBase):
r = Expr.BinaryOp(ccall.idx, "CmpLT", (dep_1, zero), True, **ccall.tags)
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
elif cond_v == AMD64_CondTypes["CondNL"]:
if op_v in {
AMD64_OpTypes["G_CC_OP_SUBB"],
AMD64_OpTypes["G_CC_OP_SUBW"],
AMD64_OpTypes["G_CC_OP_SUBL"],
AMD64_OpTypes["G_CC_OP_SUBQ"],
}:
# CondNL (jge) is SF == OF, i.e. dep_1 >=s dep_2
dep_1 = self._fix_size(
dep_1,
op_v,
AMD64_OpTypes["G_CC_OP_SUBB"],
AMD64_OpTypes["G_CC_OP_SUBW"],
AMD64_OpTypes["G_CC_OP_SUBL"],
ccall.tags,
)
dep_2 = self._fix_size(
dep_2,
op_v,
AMD64_OpTypes["G_CC_OP_SUBB"],
AMD64_OpTypes["G_CC_OP_SUBW"],
AMD64_OpTypes["G_CC_OP_SUBL"],
ccall.tags,
)
r = Expr.BinaryOp(ccall.idx, "CmpGE", (dep_1, dep_2), True, **ccall.tags)
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
if op_v in {
AMD64_OpTypes["G_CC_OP_LOGICB"],
AMD64_OpTypes["G_CC_OP_LOGICW"],
AMD64_OpTypes["G_CC_OP_LOGICL"],
AMD64_OpTypes["G_CC_OP_LOGICQ"],
}:
# and/or/xor clear OF, so CondNL = SF == 0, i.e. the result dep_1 >=s 0
dep_1 = self._fix_size(
dep_1,
op_v,
AMD64_OpTypes["G_CC_OP_LOGICB"],
AMD64_OpTypes["G_CC_OP_LOGICW"],
AMD64_OpTypes["G_CC_OP_LOGICL"],
ccall.tags,
)
zero = Expr.Const(self.ail_manager.next_atom(), 0, dep_1.bits)
r = Expr.BinaryOp(ccall.idx, "CmpGE", (dep_1, zero), True, **ccall.tags)
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
elif cond_v == AMD64_CondTypes["CondNBE"]:
if op_v in {
AMD64_OpTypes["G_CC_OP_SUBB"],
@ -347,8 +397,10 @@ class AMD64CCallRewriter(CCallRewriterBase):
r = Expr.BinaryOp(ccall.idx, "CmpGT", (dep_1, dep_2), False, **ccall.tags)
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
elif cond_v == AMD64_CondTypes["CondB"]:
if op_v in {
elif cond_v in {AMD64_CondTypes["CondB"], AMD64_CondTypes["CondBE"]}:
# CondB tests CF; CondBE tests CF | ZF
is_be = cond_v == AMD64_CondTypes["CondBE"]
if not is_be and op_v in {
AMD64_OpTypes["G_CC_OP_ADDB"],
AMD64_OpTypes["G_CC_OP_ADDW"],
AMD64_OpTypes["G_CC_OP_ADDL"],
@ -390,7 +442,8 @@ class AMD64CCallRewriter(CCallRewriterBase):
AMD64_OpTypes["G_CC_OP_SUBL"],
AMD64_OpTypes["G_CC_OP_SUBQ"],
}:
# dep_1 <u dep_2
# CF is dep_1 <u dep_2 and ZF is dep_1 == dep_2, so
# dep_1 <u dep_2 for CondB, dep_1 <=u dep_2 for CondBE
dep_1 = self._fix_size(
dep_1,
@ -411,12 +464,110 @@ class AMD64CCallRewriter(CCallRewriterBase):
r = Expr.BinaryOp(
ccall.idx,
"CmpLT",
"CmpLE" if is_be else "CmpLT",
(dep_1, dep_2),
False,
**ccall.tags,
)
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
if op_v in {
AMD64_OpTypes["G_CC_OP_LOGICB"],
AMD64_OpTypes["G_CC_OP_LOGICW"],
AMD64_OpTypes["G_CC_OP_LOGICL"],
AMD64_OpTypes["G_CC_OP_LOGICQ"],
}:
# and/or/xor always clear CF, so CondB is never true and CondBE degenerates to ZF
if not is_be:
return Expr.Const(self.ail_manager.next_atom(), 0, ccall.bits, **ccall.tags)
dep_1 = self._fix_size(
dep_1,
op_v,
AMD64_OpTypes["G_CC_OP_LOGICB"],
AMD64_OpTypes["G_CC_OP_LOGICW"],
AMD64_OpTypes["G_CC_OP_LOGICL"],
ccall.tags,
)
zero = Expr.Const(self.ail_manager.next_atom(), 0, dep_1.bits)
r = Expr.BinaryOp(ccall.idx, "CmpEQ", (dep_1, zero), False, **ccall.tags)
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
elif cond_v == AMD64_CondTypes["CondNB"]:
if op_v in {
AMD64_OpTypes["G_CC_OP_ADDB"],
AMD64_OpTypes["G_CC_OP_ADDW"],
AMD64_OpTypes["G_CC_OP_ADDL"],
AMD64_OpTypes["G_CC_OP_ADDQ"],
}:
# CondNB is !CF, i.e. the negation of the __CFADD__ carry test that CondB
# emits. An inline (a + b) >=u a comparison would be wrong here: C integer
# promotion keeps sub-int additions from wrapping, making it a tautology at
# 8/16-bit widths.
dep_1 = self._fix_size(
dep_1,
op_v,
AMD64_OpTypes["G_CC_OP_ADDB"],
AMD64_OpTypes["G_CC_OP_ADDW"],
AMD64_OpTypes["G_CC_OP_ADDL"],
ccall.tags,
)
dep_2 = self._fix_size(
dep_2,
op_v,
AMD64_OpTypes["G_CC_OP_ADDB"],
AMD64_OpTypes["G_CC_OP_ADDW"],
AMD64_OpTypes["G_CC_OP_ADDL"],
ccall.tags,
)
cfadd_call = Expr.Call(
self.ail_manager.next_atom(),
"__CFADD__",
args=[dep_1, dep_2],
bits=ccall.bits,
**ccall.tags,
)
variable_map_of(self.ail_manager).set_calling_convention(
cfadd_call, SimCCUsercall(self.project.arch, [], None)
)
zero = Expr.Const(self.ail_manager.next_atom(), 0, cfadd_call.bits)
r = Expr.BinaryOp(ccall.idx, "CmpEQ", (cfadd_call, zero), False, **ccall.tags)
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
if op_v in {
AMD64_OpTypes["G_CC_OP_SUBB"],
AMD64_OpTypes["G_CC_OP_SUBW"],
AMD64_OpTypes["G_CC_OP_SUBL"],
AMD64_OpTypes["G_CC_OP_SUBQ"],
}:
# dep_1 >=u dep_2
dep_1 = self._fix_size(
dep_1,
op_v,
AMD64_OpTypes["G_CC_OP_SUBB"],
AMD64_OpTypes["G_CC_OP_SUBW"],
AMD64_OpTypes["G_CC_OP_SUBL"],
ccall.tags,
)
dep_2 = self._fix_size(
dep_2,
op_v,
AMD64_OpTypes["G_CC_OP_SUBB"],
AMD64_OpTypes["G_CC_OP_SUBW"],
AMD64_OpTypes["G_CC_OP_SUBL"],
ccall.tags,
)
r = Expr.BinaryOp(ccall.idx, "CmpGE", (dep_1, dep_2), False, **ccall.tags)
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
if op_v in {
AMD64_OpTypes["G_CC_OP_LOGICB"],
AMD64_OpTypes["G_CC_OP_LOGICW"],
AMD64_OpTypes["G_CC_OP_LOGICL"],
AMD64_OpTypes["G_CC_OP_LOGICQ"],
}:
# and/or/xor always clear CF, so CondNB is always true
return Expr.Const(self.ail_manager.next_atom(), 1, ccall.bits, **ccall.tags)
elif (
cond_v == AMD64_CondTypes["CondS"]
and op_v
@ -496,6 +647,184 @@ class AMD64CCallRewriter(CCallRewriterBase):
)
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
elif cond_v in {AMD64_CondTypes["CondO"], AMD64_CondTypes["CondNO"]}:
# overflow flag (jo / jno)
is_no = cond_v == AMD64_CondTypes["CondNO"]
if op_v in {
AMD64_OpTypes["G_CC_OP_LOGICB"],
AMD64_OpTypes["G_CC_OP_LOGICW"],
AMD64_OpTypes["G_CC_OP_LOGICL"],
AMD64_OpTypes["G_CC_OP_LOGICQ"],
}:
# and/or/xor always clear OF: CondO -> 0, CondNO -> 1
return Expr.Const(self.ail_manager.next_atom(), 1 if is_no else 0, ccall.bits, **ccall.tags)
if op_v in {
AMD64_OpTypes["G_CC_OP_ADDB"],
AMD64_OpTypes["G_CC_OP_ADDW"],
AMD64_OpTypes["G_CC_OP_ADDL"],
AMD64_OpTypes["G_CC_OP_ADDQ"],
}:
# signed overflow of dep_1 + dep_2
dep_1 = self._fix_size(
dep_1,
op_v,
AMD64_OpTypes["G_CC_OP_ADDB"],
AMD64_OpTypes["G_CC_OP_ADDW"],
AMD64_OpTypes["G_CC_OP_ADDL"],
ccall.tags,
)
dep_2 = self._fix_size(
dep_2,
op_v,
AMD64_OpTypes["G_CC_OP_ADDB"],
AMD64_OpTypes["G_CC_OP_ADDW"],
AMD64_OpTypes["G_CC_OP_ADDL"],
ccall.tags,
)
return self._overflow_helper(ccall, "__OFADD__", dep_1, dep_2, is_no)
if op_v in {
AMD64_OpTypes["G_CC_OP_SUBB"],
AMD64_OpTypes["G_CC_OP_SUBW"],
AMD64_OpTypes["G_CC_OP_SUBL"],
AMD64_OpTypes["G_CC_OP_SUBQ"],
}:
# signed overflow of dep_1 - dep_2
dep_1 = self._fix_size(
dep_1,
op_v,
AMD64_OpTypes["G_CC_OP_SUBB"],
AMD64_OpTypes["G_CC_OP_SUBW"],
AMD64_OpTypes["G_CC_OP_SUBL"],
ccall.tags,
)
dep_2 = self._fix_size(
dep_2,
op_v,
AMD64_OpTypes["G_CC_OP_SUBB"],
AMD64_OpTypes["G_CC_OP_SUBW"],
AMD64_OpTypes["G_CC_OP_SUBL"],
ccall.tags,
)
return self._overflow_helper(ccall, "__OFSUB__", dep_1, dep_2, is_no)
if op_v in {
AMD64_OpTypes["G_CC_OP_UMULB"],
AMD64_OpTypes["G_CC_OP_UMULW"],
AMD64_OpTypes["G_CC_OP_UMULL"],
AMD64_OpTypes["G_CC_OP_UMULQ"],
}:
# unsigned multiply overflow: high half of the full product is nonzero
dep_1 = self._fix_size(
dep_1,
op_v,
AMD64_OpTypes["G_CC_OP_UMULB"],
AMD64_OpTypes["G_CC_OP_UMULW"],
AMD64_OpTypes["G_CC_OP_UMULL"],
ccall.tags,
)
dep_2 = self._fix_size(
dep_2,
op_v,
AMD64_OpTypes["G_CC_OP_UMULB"],
AMD64_OpTypes["G_CC_OP_UMULW"],
AMD64_OpTypes["G_CC_OP_UMULL"],
ccall.tags,
)
return self._overflow_helper(ccall, "__OFUMUL__", dep_1, dep_2, is_no)
if op_v in {
AMD64_OpTypes["G_CC_OP_SMULB"],
AMD64_OpTypes["G_CC_OP_SMULW"],
AMD64_OpTypes["G_CC_OP_SMULL"],
AMD64_OpTypes["G_CC_OP_SMULQ"],
}:
# signed multiply overflow
dep_1 = self._fix_size(
dep_1,
op_v,
AMD64_OpTypes["G_CC_OP_SMULB"],
AMD64_OpTypes["G_CC_OP_SMULW"],
AMD64_OpTypes["G_CC_OP_SMULL"],
ccall.tags,
)
dep_2 = self._fix_size(
dep_2,
op_v,
AMD64_OpTypes["G_CC_OP_SMULB"],
AMD64_OpTypes["G_CC_OP_SMULW"],
AMD64_OpTypes["G_CC_OP_SMULL"],
ccall.tags,
)
return self._overflow_helper(ccall, "__OFSMUL__", dep_1, dep_2, is_no)
if op_v in {
AMD64_OpTypes["G_CC_OP_INCB"],
AMD64_OpTypes["G_CC_OP_INCW"],
AMD64_OpTypes["G_CC_OP_INCL"],
AMD64_OpTypes["G_CC_OP_INCQ"],
}:
# inc overflows only when the result is the signed minimum
nbits = self._op_nbits(
op_v,
AMD64_OpTypes["G_CC_OP_INCB"],
AMD64_OpTypes["G_CC_OP_INCW"],
AMD64_OpTypes["G_CC_OP_INCL"],
)
dep_1 = self._fix_size(
dep_1,
op_v,
AMD64_OpTypes["G_CC_OP_INCB"],
AMD64_OpTypes["G_CC_OP_INCW"],
AMD64_OpTypes["G_CC_OP_INCL"],
ccall.tags,
)
signmin = Expr.Const(self.ail_manager.next_atom(), 1 << (nbits - 1), dep_1.bits)
expr_op = "CmpNE" if is_no else "CmpEQ"
r = Expr.BinaryOp(ccall.idx, expr_op, (dep_1, signmin), False, **ccall.tags)
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
if op_v in {
AMD64_OpTypes["G_CC_OP_DECB"],
AMD64_OpTypes["G_CC_OP_DECW"],
AMD64_OpTypes["G_CC_OP_DECL"],
AMD64_OpTypes["G_CC_OP_DECQ"],
}:
# dec overflows only when the result is the signed maximum
nbits = self._op_nbits(
op_v,
AMD64_OpTypes["G_CC_OP_DECB"],
AMD64_OpTypes["G_CC_OP_DECW"],
AMD64_OpTypes["G_CC_OP_DECL"],
)
dep_1 = self._fix_size(
dep_1,
op_v,
AMD64_OpTypes["G_CC_OP_DECB"],
AMD64_OpTypes["G_CC_OP_DECW"],
AMD64_OpTypes["G_CC_OP_DECL"],
ccall.tags,
)
signmax = Expr.Const(self.ail_manager.next_atom(), (1 << (nbits - 1)) - 1, dep_1.bits)
expr_op = "CmpNE" if is_no else "CmpEQ"
r = Expr.BinaryOp(ccall.idx, expr_op, (dep_1, signmax), False, **ccall.tags)
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
if op_v == AMD64_OpTypes["G_CC_OP_COPY"]:
# dep_1 holds the packed flags; test the stored OF bit
bitmask = AMD64_CondBitMasks["G_CC_MASK_O"]
assert isinstance(bitmask, int)
flag = Expr.Const(self.ail_manager.next_atom(), bitmask, dep_1.bits)
masked_dep = Expr.BinaryOp(
self.ail_manager.next_atom(), "And", [dep_1, flag], False, **ccall.tags
)
zero = Expr.Const(self.ail_manager.next_atom(), 0, dep_1.bits)
expr_op = "CmpEQ" if is_no else "CmpNE"
r = Expr.BinaryOp(ccall.idx, expr_op, (masked_dep, zero), False, **ccall.tags)
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
elif ccall.callee == "amd64g_calculate_rflags_c":
# calculate the carry flag
op = ccall.operands[0]
@ -611,6 +940,33 @@ class AMD64CCallRewriter(CCallRewriterBase):
return None
@staticmethod
def _op_nbits(op_v: int, type_8bit, type_16bit, type_32bit) -> int:
if op_v == type_8bit:
return 8
if op_v == type_16bit:
return 16
if op_v == type_32bit:
return 32
return 64
def _overflow_helper(self, ccall, name: str, dep_1, dep_2, is_no: bool):
# Emit a named overflow-helper call (mirrors the __CFADD__ arm). The helper
# returns a 0/1 flag; for the negated condition (CondNO) compare it to 0.
call = Expr.Call(
ccall.idx,
name,
args=[dep_1, dep_2],
bits=ccall.bits,
**ccall.tags,
)
variable_map_of(self.ail_manager).set_calling_convention(call, SimCCUsercall(self.project.arch, [], None))
if not is_no:
return call
zero = Expr.Const(self.ail_manager.next_atom(), 0, ccall.bits)
r = Expr.BinaryOp(self.ail_manager.next_atom(), "CmpEQ", (call, zero), False, **ccall.tags)
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
def _fix_size(self, expr, op_v: int, type_8bit, type_16bit, type_32bit, tags):
if op_v == type_8bit:
bits = 8

View file

@ -2,7 +2,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
import angr.ailment as ailment
from angr import ailment
if TYPE_CHECKING:
from angr.ailment.manager import Manager

File diff suppressed because it is too large Load diff

View file

@ -995,7 +995,11 @@ class ConditionProcessor:
if not isinstance(condition.offset, ailment.expression.Const)
else condition.offset.value
)
var = claripy.BVS(f"ailexpr_Extract({offset_expr}, {hash(var_)})", condition.bits, explicit_name=True)
var = claripy.BVS(
f"ailexpr_Extract({condition.bits}, {condition.endness}, {offset_expr}, {hash(var_)})",
condition.bits,
explicit_name=True,
)
self._condition_mapping[var.args[0]] = condition
return var
if isinstance(condition, ailment.expression.Insert):

View file

@ -2,7 +2,7 @@ from __future__ import annotations
from collections import defaultdict
import angr.ailment as ailment
from angr import ailment
from angr.analyses.decompiler.sequence_walker import SequenceWalker
from angr.analyses.decompiler.structurer_nodes import LoopNode

View file

@ -1,26 +1,174 @@
from __future__ import annotations
import json
import time
from typing import TYPE_CHECKING, Any
from angr.protos import decompilation_cache_pb2
from angr.serializable import Serializable
from angr.utils.ail_serialization import (
pack_arg_vvars,
pack_ite_exprs,
pack_static_buffers,
pack_static_vvars,
parse_arg_vvars,
parse_ite_exprs,
parse_static_buffers,
parse_static_vvars,
)
from .clinic import Clinic
if TYPE_CHECKING:
from angr import ailment
from angr.analyses.decompiler.optimization_passes.expr_op_swapper import OpDescriptor
from angr.analyses.typehoon.typevars import TypeConstraint, TypeVariable
from angr.knowledge_plugins.cfg import CFGModel
from .notes import DecompilationNote
from .structured_codegen import BaseStructuredCodeGenerator
from .variable_map import VariableMap
class DecompilationCache:
# ---------------------------------------------------------------------------------------------------------------------
# Serialization helpers.
#
# Conventions:
# - Heavy sub-objects (``clinic``, ``codegen``) are embedded as already-serialized bytes (each manages its own format).
# - AIL-typed top-level slots (``arg_vvars``, ``ite_exprs``) use the typed messages from ``ail_types.proto``.
# - ``cfg`` is intentionally not serialized — it comes from the parent Project. Decompilation variables live on
# kb.dec_variables.
# - The 4 typehoon-typed slots are skipped entirely (typehoon is out of scope for now).
# ---------------------------------------------------------------------------------------------------------------------
def _simvar_to_bytes(v) -> bytes:
return type(v).__name__.encode("ascii") + b"\0" + v.serialize()
def _simvar_from_bytes(b: bytes):
import angr.sim_variable as sv_mod # pylint:disable=import-outside-toplevel
sep = b.index(b"\0")
return getattr(sv_mod, b[:sep].decode("ascii")).parse(b[sep + 1 :])
def _serialize_binop_operators(binop_operators, out_msg, set_flag=None) -> None:
if binop_operators is None:
return
if set_flag is not None:
setattr(set_flag[0], set_flag[1], True)
for op_desc, value in binop_operators.items():
entry = out_msg.add()
entry.key_json = op_desc.to_json()
entry.value = value
def _parse_binop_operators(entries):
from angr.analyses.decompiler.optimization_passes.expr_op_swapper import ( # pylint:disable=import-outside-toplevel
OpDescriptor,
)
return {OpDescriptor.from_json(e.key_json): e.value for e in entries}
def _serialize_parameters(params: dict, out_msg) -> None:
"""Translate the 15-key parameters dict into a DecompilationParameters cmessage."""
from angr.analyses.decompiler.optimization_pass_registry import ( # pylint:disable=import-outside-toplevel
pass_to_name,
)
if params.get("flavor") is not None:
out_msg.flavor = params["flavor"]
if "sp_tracker_track_memory" in params:
out_msg.sp_tracker_track_memory = bool(params["sp_tracker_track_memory"])
# Collection-typed parameters are never None (the Decompiler normalizes them to empty collections), so each is
# written directly; an empty collection is left unset and parses back to empty.
out_msg.vars_must_struct.extend(sorted(params.get("vars_must_struct") or ()))
out_msg.desired_variables.extend(sorted(params.get("desired_variables") or ()))
out_msg.inline_functions.extend(sorted(params.get("inline_functions") or ()))
for option, value in params.get("options") or ():
entry = out_msg.options.add()
entry.param = option.param
try:
entry.value_json = json.dumps(value)
except (TypeError, ValueError):
entry.value_json = json.dumps(None)
for cls in params.get("optimization_passes") or ():
out_msg.optimization_passes.append(pass_to_name(cls))
# peephole_optimizations is the one None-able collection: None means "use the default peephole set"
peepholes = params.get("peephole_optimizations")
out_msg.peephole_optimizations_use_default = peepholes is None
if peepholes is not None:
for cls in peepholes:
out_msg.peephole_optimizations.append(pass_to_name(cls))
for k, v in (params.get("expr_comments") or {}).items():
out_msg.expr_comments[k] = v
for k, v in (params.get("stmt_comments") or {}).items():
out_msg.stmt_comments[k] = v
_serialize_binop_operators(params.get("binop_operators") or {}, out_msg.binop_operators)
if params.get("ite_exprs"):
out_msg.ite_exprs.CopyFrom(pack_ite_exprs(params["ite_exprs"]))
if params.get("static_vvars"):
out_msg.static_vvars.CopyFrom(pack_static_vvars(params["static_vvars"]))
if params.get("static_buffers"):
out_msg.static_buffers.CopyFrom(pack_static_buffers(params["static_buffers"]))
out_msg.save_unoptimized_graph = bool(params.get("save_unoptimized_graph"))
def _parse_parameters(msg) -> dict:
"""Always populate every one of the 15 keys in the returned dict; scalar fields that were not set come back as
None and collection fields come back empty, except peephole_optimizations where None means "use the default
peephole set". This matches the decompiler's normalized _cache_parameters, which _can_use_decompilation_cache
compares key by key against the deserialized cache."""
from angr.analyses.decompiler.decompilation_options import PARAM_TO_OPTION # pylint:disable=import-outside-toplevel
from angr.analyses.decompiler.optimization_pass_registry import ( # pylint:disable=import-outside-toplevel
name_to_pass,
)
# Collection-typed values come back as empty collections (never None) so they match the Decompiler's normalized
# _cache_parameters during cache-validity comparison.
return {
"flavor": msg.flavor if msg.HasField("flavor") else None,
"sp_tracker_track_memory": msg.sp_tracker_track_memory if msg.HasField("sp_tracker_track_memory") else None,
"vars_must_struct": set(msg.vars_must_struct),
"desired_variables": frozenset(msg.desired_variables),
"inline_functions": frozenset(msg.inline_functions),
"options": {
(PARAM_TO_OPTION[e.param], json.loads(e.value_json) if e.value_json else None)
for e in msg.options
if e.param in PARAM_TO_OPTION
},
# unresolvable pass names (defining module not imported) drop out; the resulting shorter list will not match
# the live decompiler's parameters, so the cache falls through to a fresh decompilation
"optimization_passes": [cls for n in msg.optimization_passes if (cls := name_to_pass(n)) is not None],
"peephole_optimizations": (
None
if msg.peephole_optimizations_use_default
else [cls for n in msg.peephole_optimizations if (cls := name_to_pass(n)) is not None]
),
"expr_comments": dict(msg.expr_comments),
"stmt_comments": dict(msg.stmt_comments),
"binop_operators": _parse_binop_operators(msg.binop_operators),
"ite_exprs": parse_ite_exprs(msg.ite_exprs) if msg.HasField("ite_exprs") else set(),
"static_vvars": parse_static_vvars(msg.static_vvars) if msg.HasField("static_vvars") else {},
"static_buffers": parse_static_buffers(msg.static_buffers) if msg.HasField("static_buffers") else {},
"save_unoptimized_graph": msg.save_unoptimized_graph,
}
class DecompilationCache(Serializable):
"""
Caches key data structures that can be used later for refining decompilation results, such as retyping variables.
"""
# ``cfg`` is a decompile-time input used only for cache-validity checks. It is not serialized; after
# deserialization it is None until the caller re-attaches it.
__slots__ = (
"addr",
"arg_vvars",
"binop_operators",
"cfg",
"clinic",
"codegen",
"errors",
@ -32,32 +180,139 @@ class DecompilationCache:
"parameters",
"stack_offset_typevars",
"stackvar_max_sizes",
"timestamp",
"type_constraints",
"var_to_typevar",
"variable_map",
"version",
)
def __init__(self, addr):
import angr # pylint:disable=import-outside-toplevel,cyclic-import
self.parameters: dict[str, Any] = {}
# angr version and creation time of this decompilation
self.version: str = angr.__version__
self.timestamp: int = int(time.time())
self.addr = addr
self.type_constraints: dict[TypeVariable, set[TypeConstraint]] | None = None
self.arg_vvars: dict | None = None
self.cfg: CFGModel | None = None
# Collection-typed fields default to empty containers rather than None, so serialization never has to
# distinguish None from empty.
self.type_constraints: dict[TypeVariable, set[TypeConstraint]] = {}
self.arg_vvars: dict = {}
self.func_typevar: TypeVariable | None = None
self.var_to_typevar: dict | None = None
self.stackvar_max_sizes: dict | None = None
self.stack_offset_typevars: dict | None = None
self.var_to_typevar: dict = {}
self.stackvar_max_sizes: dict = {}
self.stack_offset_typevars: dict = {}
self.codegen: BaseStructuredCodeGenerator | None = None
self.clinic: Clinic | None = None
self.variable_map: VariableMap | None = None
self.ite_exprs: set[tuple[int, Any]] | None = None
self.binop_operators: dict[OpDescriptor, str] | None = None
self.ite_exprs: set[tuple[int, ailment.Expression]] = set()
self.binop_operators: dict[OpDescriptor, str] = {}
self.errors: list[str] = []
self.function_summary: str | None = None
self.notes: dict[str, str] = {}
self.notes: dict[str, DecompilationNote] = {}
self.max_tv_id: int = 0
@property
def local_types(self):
if self.clinic is None or self.clinic.variable_kb is None:
if self.clinic is None or self.clinic.kb is None or self.addr not in self.clinic.kb.dec_variables:
return None
return self.clinic.variable_kb.variables[self.addr].types
return self.clinic.kb.dec_variables[self.addr].types
# -----------------------------------------------------------------------------------------------------------------
# Protobuf serialization. Heavy sub-objects (clinic, codegen) are embedded as already-serialized bytes; AIL-typed
# top-level fields (arg_vvars, ite_exprs) use the typed messages from ail_types.proto. The four typehoon-typed
# slots and the ``cfg`` input are not serialized and come back as None.
# -----------------------------------------------------------------------------------------------------------------
@classmethod
def _get_cmsg(cls):
return decompilation_cache_pb2.DecompilationCache() # pylint:disable=no-member
def serialize_to_cmessage(self):
msg = decompilation_cache_pb2.DecompilationCache(addr=self.addr) # pylint:disable=no-member
if self.clinic is not None:
msg.clinic = self.clinic.serialize()
if self.codegen is not None:
msg.codegen = self.codegen.serialize()
msg.errors.extend(self.errors)
if self.function_summary is not None:
msg.function_summary = self.function_summary
# Collection fields are never None; an empty collection is simply left unset and parses back to empty.
if self.arg_vvars:
msg.arg_vvars.CopyFrom(pack_arg_vvars(self.arg_vvars))
if self.ite_exprs:
msg.ite_exprs.CopyFrom(pack_ite_exprs(self.ite_exprs))
_serialize_binop_operators(self.binop_operators, msg.binop_operators)
for simvar, size in self.stackvar_max_sizes.items():
entry = msg.stackvar_max_sizes.add()
entry.simvar = _simvar_to_bytes(simvar)
entry.max_size = size
msg.version = self.version
msg.timestamp = self.timestamp
# An unset parameters message means "no recorded parameters"; cache-validity checks treat such a cache as
# always usable (matching runs with use_cache=False).
if self.parameters:
_serialize_parameters(self.parameters, msg.parameters)
for k, note in self.notes.items():
msg.notes_json[k] = note.to_json()
return msg
@classmethod
def parse_from_cmessage(
cls,
cmsg,
*,
project=None,
kb=None,
function=None,
cfg=None,
**_,
):
"""Parse a DecompilationCache from a cmessage. Runtime back-references (project, kb, function, cfg) are
passed through to the embedded Clinic / codegen parsers so the parsed cache is functional for cache-hit
validity checks. Decompilation variables live on kb.dec_variables."""
from .notes import DecompilationNote # pylint:disable=import-outside-toplevel
from .structured_codegen.c import CStructuredCodeGenerator # pylint:disable=import-outside-toplevel
cache = cls(cmsg.addr)
# cfg is not serialized; reattach from kwargs so cache-validity checks still work.
cache.cfg = cfg
if cmsg.HasField("clinic"):
cache.clinic = Clinic.parse(cmsg.clinic, project=project, kb=kb, function=function, cfg=cfg)
if cmsg.HasField("codegen"):
cache.codegen = CStructuredCodeGenerator.parse(cmsg.codegen, project=project, kb=kb, func=function)
cache.errors = list(cmsg.errors)
if cmsg.HasField("function_summary"):
cache.function_summary = cmsg.function_summary
# Collection fields default to empty (set in __init__); only assign when the message carries content.
if cmsg.HasField("arg_vvars"):
cache.arg_vvars = parse_arg_vvars(cmsg.arg_vvars)
if cmsg.HasField("ite_exprs"):
cache.ite_exprs = parse_ite_exprs(cmsg.ite_exprs)
cache.binop_operators = _parse_binop_operators(cmsg.binop_operators)
cache.stackvar_max_sizes = {_simvar_from_bytes(e.simvar): e.max_size for e in cmsg.stackvar_max_sizes}
# legacy blobs carry the proto3 defaults ""/0, meaning "unknown"; do not re-stamp them with current values
cache.version = cmsg.version
cache.timestamp = cmsg.timestamp
if cache.codegen is not None:
# mirror the stamps onto the codegen (a fresh decompile does the same in Decompiler._decompile)
cache.codegen.version = cache.version
cache.codegen.timestamp = cache.timestamp
if cmsg.HasField("parameters"):
cache.parameters = _parse_parameters(cmsg.parameters)
cache.notes = {k: DecompilationNote.from_json(v) for k, v in cmsg.notes_json.items()}
return cache

View file

@ -45,6 +45,10 @@ class DecompilationOption[T]:
O = DecompilationOption
# Serialization contract for display options (cls="codegen"): to survive Codegen serialization, an option's param
# must have a matching optional scalar proto field, named identically, in the trailing display-option block of the
# Codegen message (protos/codegen.proto). Options without such a field are dropped on round-trip.
options = [
O(
"Aggressively remove dead memdefs",

View file

@ -16,11 +16,9 @@ from angr.analyses.s_propagator import sprop_cache_scope
from angr.analyses.typehoon.typehoon import Typehoon
from angr.analyses.typehoon.typevars import TypeVariableManager
from angr.errors import AngrAIError
from angr.knowledge_base import KnowledgeBase
from angr.knowledge_plugins.functions.function import Function
from angr.rust.optimization_passes import get_rust_optimization_passes
from angr.rust.typehoon.typehoon import RustTypehoon
from angr.sim_type import parse_type
from angr.sim_variable import SimMemoryVariable, SimRegisterVariable, SimStackVariable
from angr.utils import timethis
@ -29,11 +27,21 @@ from .clinic import ClinicStage
from .condition_processor import ConditionProcessor
from .decompilation_cache import DecompilationCache
from .decompilation_options import PARAM_TO_OPTION, DecompilationOption
from .edits import (
DecompilationEditError,
list_variable_names,
reflow_types,
rename_function,
rename_variable,
resolve_variable,
set_variable_type,
)
from .notes import DecompilationNote
from .optimization_passes.optimization_pass import OptimizationPassStage
from .presets import DECOMPILATION_PRESETS, DecompilationPreset
from .region_identifier import RegionIdentifier
from .sequence_walker import SequenceWalker
from .structured_codegen import DummyStructuredCodeGenerator
from .structured_codegen.c import CStructuredCodeGenerator
from .structured_codegen.rust import RustStructuredCodeGenerator
from .structurer_nodes import SequenceNode
@ -62,6 +70,15 @@ class Decompiler(Analysis):
Run this on a Function object for which a normalized CFG has been constructed.
The fully processed output can be found in result.codegen.text
AIL graphs exposed on the result (both on a fresh run and on a cache hit, including caches reloaded from
angrdb or the runtime-db spill):
- ``ail_graph`` (= ``clinic.cc_graph``): the simplified graph before region identification.
- ``clinic.graph``: the final graph after region identification and region simplification.
- ``unoptimized_ail_graph`` (= ``clinic.unoptimized_graph``): a snapshot before the first structure-altering
optimization pass; use it for an exact instruction-to-AIL mapping. Only built when
``save_unoptimized_graph=True`` is passed; otherwise this attribute is None on both fresh runs and cache hits.
"""
def __init__(
@ -72,7 +89,6 @@ class Decompiler(Analysis):
preset: str | DecompilationPreset | None = None,
optimization_passes=None,
sp_tracker_track_memory=True,
variable_kb=None,
peephole_optimizations: _PEEPHOLE_OPTIMIZATIONS_TYPE = None,
vars_must_struct: set[str] | None = None,
flavor="pseudocode",
@ -81,7 +97,7 @@ class Decompiler(Analysis):
ite_exprs=None,
binop_operators=None,
decompile=True,
regen_clinic=True,
regen_clinic=False,
inline_functions=None,
desired_variables=None,
update_memory_data: bool = True,
@ -98,6 +114,7 @@ class Decompiler(Analysis):
static_vvars: dict | None = None,
static_buffers: dict | None = None,
codegen_cls=CStructuredCodeGenerator,
save_unoptimized_graph: bool = False,
):
if not isinstance(func, Function):
func = self.kb.functions[func]
@ -138,7 +155,6 @@ class Decompiler(Analysis):
self._sp_tracker_track_memory = sp_tracker_track_memory
self._peephole_optimizations = peephole_optimizations
self._vars_must_struct = vars_must_struct
self._variable_kb = variable_kb
self._expr_comments = expr_comments
self._stmt_comments = stmt_comments
self._ite_exprs = ite_exprs
@ -151,24 +167,29 @@ class Decompiler(Analysis):
self._desired_variables = frozenset(desired_variables) if desired_variables else set()
self._static_vvars = static_vvars if static_vvars is not None else {}
self._static_buffers = static_buffers if static_buffers is not None else {}
self._save_unoptimized_graph = save_unoptimized_graph
# ``cfg`` is not in this dict: it is an input, not part of the decompilation result. Its identity is
# checked separately in :meth:`_can_use_decompilation_cache`.
# Collection-typed values are normalized to empty collections (never None) so the serialized cache does not
# need to distinguish None from empty. The exception is peephole_optimizations, where None means "use the
# default peephole set" and is distinct from an explicitly empty list.
self._cache_parameters = (
{
"cfg": self._cfg,
"variable_kb": self._variable_kb,
"options": {(o, v) for o, v in self._options if o.category != "Display" and v != o.default_value},
"optimization_passes": self._optimization_passes,
"sp_tracker_track_memory": self._sp_tracker_track_memory,
"peephole_optimizations": self._peephole_optimizations,
"vars_must_struct": self._vars_must_struct,
"vars_must_struct": self._vars_must_struct or set(),
"flavor": self._flavor,
"expr_comments": self._expr_comments,
"stmt_comments": self._stmt_comments,
"ite_exprs": self._ite_exprs,
"binop_operators": self._binop_operators,
"expr_comments": self._expr_comments or {},
"stmt_comments": self._stmt_comments or {},
"ite_exprs": self._ite_exprs or set(),
"binop_operators": self._binop_operators or {},
"inline_functions": self._inline_functions,
"desired_variables": self._desired_variables,
"static_vvars": self._static_vvars,
"static_buffers": self._static_buffers,
"save_unoptimized_graph": self._save_unoptimized_graph,
}
if use_cache
else None
@ -238,9 +259,14 @@ class Decompiler(Analysis):
def _can_use_decompilation_cache(self, cache: DecompilationCache) -> bool:
if self._cache_parameters is None or cache.parameters is None:
return False
# deserialized caches come back with cfg unset until the caller re-attaches it; unset is not a mismatch
if cache.cfg is not None and cache.cfg is not self._cfg:
return False
a, b = self._cache_parameters, cache.parameters
id_checks = {"cfg", "variable_kb"}
return all(a[k] is b[k] if k in id_checks else a[k] == b[k] for k in self._cache_parameters)
if not b:
# AngrDB-loaded caches carry no recorded parameters; there is nothing to validate against
return True
return all(k in b and a[k] == b[k] for k in a)
@staticmethod
def _parse_options(options: list[tuple[DecompilationOption | str, Any]]) -> list[tuple[DecompilationOption, Any]]:
@ -260,6 +286,30 @@ class Decompiler(Analysis):
with sprop_cache_scope(self._sprop_walker_cache):
self._decompile()
def _reuse_cached_decompilation(self, cache, clinic, codegen) -> None:
"""Full-reuse fast path: expose the cached clinic and codegen as this run's results without re-running the
pipeline. A live codegen's text is re-rendered to pick up in-place display edits; a freshly-deserialized
codegen (``_handlers is None``) keeps its stored text. The codegen inherits the cache's version and
timestamp."""
codegen.version = cache.version
codegen.timestamp = cache.timestamp
if codegen._handlers is not None:
codegen.regenerate_text()
self.cache = cache
self.clinic = clinic
self.codegen = codegen
self.seq_node = None
self.ail_graph = clinic.cc_graph
self.unoptimized_ail_graph = clinic.unoptimized_graph
self._variable_map = clinic.variable_map
self.vvar_id_start = clinic.vvar_id_start
self._copied_var_ids = clinic.copied_var_ids
if self.update_cache:
self.kb.decompilations[(self.func.addr, self._flavor)] = cache
self._finish_progress()
@timethis
def _decompile(self):
if self.func.is_simprocedure:
@ -284,10 +334,28 @@ class Decompiler(Analysis):
else:
old_codegen = None
old_clinic = None
ite_exprs = self._ite_exprs
binop_operators = self._binop_operators
# normalize to empty collections so the cache never stores None (passes treat None and empty the same)
ite_exprs = self._ite_exprs or set()
binop_operators = self._binop_operators or {}
l.debug("Decompilation cache miss")
# Full-reuse fast path: with use_cache and without regen_clinic (the default), a valid cache short-circuits
# the entire pipeline and hands back the cached clinic and codegen. Requires an AST-carrying codegen (not
# DummyStructuredCodeGenerator) and this function's variables in kb.dec_variables; anything else falls
# through to a fresh decompilation.
if (
self.use_cache
and not self._regen_clinic
and cache is not None
and old_clinic is not None
and old_codegen is not None
and not isinstance(old_codegen, DummyStructuredCodeGenerator)
and self.func.addr in self.kb.dec_variables
and self.func.prototype is not None
):
self._reuse_cached_decompilation(cache, old_clinic, old_codegen)
return
self.options_by_class = defaultdict(list)
if self._options:
@ -298,15 +366,7 @@ class Decompiler(Analysis):
self._set_global_variables()
self._update_progress(5.0, text="Converting to AIL")
variable_kb = self._variable_kb
# fall back to old codegen
if variable_kb is None and old_codegen is not None and isinstance(old_codegen, CStructuredCodeGenerator):
variable_kb = old_codegen._variable_kb
if variable_kb is None:
reset_variable_names = True
else:
reset_variable_names = self.func.addr not in variable_kb.variables.function_managers
reset_variable_names = self.func.addr not in self.kb.dec_variables.function_managers
# determine a few arguments according to the structuring algorithm
fold_callexprs_into_conditions = False
@ -327,6 +387,7 @@ class Decompiler(Analysis):
fold_callexprs_into_conditions = True
cache = DecompilationCache(self.func.addr)
cache.cfg = self._cfg
if self._cache_parameters is not None:
cache.parameters = self._cache_parameters
cache.ite_exprs = ite_exprs
@ -341,12 +402,17 @@ class Decompiler(Analysis):
def progress_callback(p, **kwargs):
return self._update_progress(p * (70 - 5) / 100.0 + 5, **kwargs)
if self._regen_clinic or old_clinic is None or self.func.prototype is None:
# a deserialized clinic whose function has no dec_variables cannot drive codegen; re-run Clinic instead
if (
self._regen_clinic
or old_clinic is None
or self.func.prototype is None
or self.func.addr not in self.kb.dec_variables
):
clinic = self.project.analyses.Clinic(
self.func,
kb=self.kb,
fail_fast=self._fail_fast,
variable_kb=variable_kb,
reset_variable_names=reset_variable_names,
optimization_passes=self._optimization_passes,
sp_tracker_track_memory=self._sp_tracker_track_memory,
@ -371,22 +437,28 @@ class Decompiler(Analysis):
notes=self.notes,
static_vvars=self._static_vvars,
static_buffers=self._static_buffers,
save_unoptimized_graph=self._save_unoptimized_graph,
flavor=self._flavor,
variable_map=variable_map,
**self.options_to_params(self.options_by_class["clinic"]),
)
else:
clinic = old_clinic
# the deserialized clinic may carry peephole-optimization names that were unresolvable at parse time
# (their defining module was not imported then); retry resolving before its passes run again
clinic.resolve_peephole_optimizations()
# reuse the old, unaltered graph
clinic.graph = clinic.cc_graph
clinic.cc_graph = clinic.copy_graph()
# the SRDA model is tied to the previous run's graph; drop it so the simplification passes below
# regenerate it fresh for the reused graph
clinic.reaching_definitions = None
self.clinic = clinic
self.cache = cache
# Make the VariableMap available on the cache regardless of whether Clinic re-linked variables (a partial
# Clinic run, or the reuse-cached-Clinic path, may not repopulate cache.variable_map during linking).
cache.variable_map = clinic.variable_map
self._variable_kb = clinic.variable_kb
self._variable_map = clinic.variable_map
self._update_progress(70.0, text="Identifying regions")
self.vvar_id_start = clinic.vvar_id_start
@ -396,11 +468,13 @@ class Decompiler(Analysis):
# the function is empty
return
# expose a copy of the graph before any optimizations that may change the graph occur;
# use this graph if you need a reference of exact mapping of instructions to AIL statements
self.unoptimized_ail_graph = (
clinic.unoptimized_graph if clinic.unoptimized_graph is not None else clinic.copy_graph()
)
# expose a copy of the graph before any optimizations that may change the graph occur; use this graph if you
# need an exact instruction-to-AIL mapping. Only built when save_unoptimized_graph is set. clinic captured
# the snapshot iff a structure-altering pass ran; if none did, the current graph is itself unoptimized.
if self._save_unoptimized_graph:
self.unoptimized_ail_graph = (
clinic.unoptimized_graph if clinic.unoptimized_graph is not None else clinic.copy_graph()
)
cond_proc = ConditionProcessor(self.project.arch, clinic._ail_manager)
clinic.graph = self._run_graph_simplification_passes(
@ -458,8 +532,8 @@ class Decompiler(Analysis):
# simplify it
# Get variable manager for loop counter naming in RegionSimplifier
variable_manager = None
if clinic.variable_kb is not None and self.func.addr in clinic.variable_kb.variables:
variable_manager = clinic.variable_kb.variables[self.func.addr]
if self.func.addr in self.kb.dec_variables:
variable_manager = self.kb.dec_variables[self.func.addr]
region_simplifier_params = self.options_to_params(self.options_by_class["region_simplifier"])
# The Rust flavor forces if-else simplification off regardless of user options.
region_simplifier_params.pop("simplify_ifelse", None)
@ -482,7 +556,7 @@ class Decompiler(Analysis):
binop_operators=cache.binop_operators,
goto_manager=s.goto_manager,
graph=clinic.graph,
variable_kb=self._variable_kb,
kb=self.kb,
)
# rewrite the sequence node to remove phi expressions
@ -501,7 +575,6 @@ class Decompiler(Analysis):
ail_graph=clinic.graph,
flavor=self._flavor,
func_args=clinic.arg_list,
variable_kb=clinic.variable_kb,
variable_map=clinic.variable_map,
expr_comments=old_codegen.expr_comments if old_codegen is not None else None,
stmt_comments=old_codegen.stmt_comments if old_codegen is not None else None,
@ -517,6 +590,10 @@ class Decompiler(Analysis):
# save a copy of the AIL graph that is optimized but not modified by region identification
self.ail_graph = clinic.cc_graph
self.cache.codegen = codegen
if codegen is not None:
# copy the cache's version and timestamp onto the codegen
codegen.version = self.cache.version
codegen.timestamp = self.cache.timestamp
self.cache.clinic = self.clinic
# LLM refinement pass
@ -591,7 +668,7 @@ class Decompiler(Analysis):
blocks_by_addr=addr_to_blocks,
blocks_by_addr_and_idx=addr_and_idx_to_blocks,
graph=ail_graph,
variable_kb=self._variable_kb,
kb=self.kb,
reaching_definitions=reaching_definitions,
entry_node_addr=self.clinic.entry_node_addr,
scratch=self._optimization_scratch,
@ -656,7 +733,7 @@ class Decompiler(Analysis):
blocks_by_addr=addr_to_blocks,
blocks_by_addr_and_idx=addr_and_idx_to_blocks,
graph=ail_graph,
variable_kb=self._variable_kb,
kb=self.kb,
arg_vvars=arg_vvars,
region_identifier=ri,
reaching_definitions=reaching_definitions,
@ -747,13 +824,13 @@ class Decompiler(Analysis):
# nothing to reflow; but this should not happen
return None
var_kb = self._variable_kb if self._variable_kb is not None else KnowledgeBase(self.project)
var_kb = self.kb
if self.func.addr not in var_kb.variables:
if self.func.addr not in var_kb.dec_variables:
# for some reason variables for the current function don't really exist...
groundtruth = {}
else:
var_manager = var_kb.variables[self.func.addr]
var_manager = var_kb.dec_variables[self.func.addr]
# ground-truth types
groundtruth = {}
for variable in var_manager.variables_with_manual_types:
@ -816,7 +893,7 @@ class Decompiler(Analysis):
and isinstance(codegen, CStructuredCodeGenerator)
and codegen.cfunc is not None
):
var_manager = var_kb.variables[self.func.addr]
var_manager = var_kb.dec_variables[self.func.addr]
for i, arg in enumerate(codegen.cfunc.arg_list):
if i >= len(self.func.prototype.args):
break
@ -885,12 +962,10 @@ class Decompiler(Analysis):
:param ail_graph: The AIL graph to transform out of SSA form.
:return: The translated AIL graph.
"""
variable_kb = self._variable_kb
dephication = self.project.analyses.GraphDephication(
self.func,
ail_graph,
rewrite=True,
variable_kb=variable_kb,
variable_map=self._variable_map,
kb=self.kb,
fail_fast=self._fail_fast,
@ -898,12 +973,10 @@ class Decompiler(Analysis):
return dephication.output
def transform_seqnode_from_ssa(self, seq_node: SequenceNode) -> SequenceNode:
variable_kb = self._variable_kb
dephication = self.project.analyses.SeqNodeDephication(
self.func,
seq_node,
rewrite=True,
variable_kb=variable_kb,
variable_map=self._variable_map,
kb=self.kb,
fail_fast=self._fail_fast,
@ -963,29 +1036,10 @@ class Decompiler(Analysis):
if not code_text:
return False
# collect unified variables
varman = self._variable_kb.variables[self.func.addr]
unified_vars = varman.get_unified_variables(sort=None)
# also collect argument variables
arg_vars = []
if (
self.codegen
and isinstance(self.codegen, CStructuredCodeGenerator)
and self.codegen.cfunc
and self.codegen.cfunc.arg_list
):
for cvar in self.codegen.cfunc.arg_list:
v = cvar.unified_variable if cvar.unified_variable is not None else cvar.variable
if v not in unified_vars:
arg_vars.append(v)
all_vars = unified_vars + arg_vars
if not all_vars:
var_names = list_variable_names(self.codegen, self.kb, self.func.addr)
if not var_names:
return False
var_names = [v.name or str(v) for v in all_vars]
prompt = (
"You are a reverse engineering assistant. Given the following decompiled C code, suggest better, "
"more descriptive variable names. Only include variables that you want to rename. "
@ -1000,27 +1054,27 @@ class Decompiler(Analysis):
if not result:
return False
# build name-to-variable lookup
name_to_var = {}
for v in all_vars:
key = v.name or str(v)
name_to_var[key] = v
changed = False
for rename in result.renames:
old_name = rename.old_name
new_name = rename.new_name
if not new_name:
old_name, new_name = rename.old_name, rename.new_name
if not new_name or old_name == new_name:
continue
var = name_to_var.get(old_name)
if var is None:
try:
edit = rename_variable(
self.project,
self.func,
old_name,
new_name,
kb=self.kb,
flavor=self._flavor,
rerender=False,
)
except DecompilationEditError as ex:
l.debug("LLM rename %s -> %s rejected: %s", old_name, new_name, ex)
continue
if old_name == new_name:
continue
var.name = new_name
var.renamed = True
changed = True
l.info("LLM renamed variable %s -> %s", old_name, new_name)
if edit.changed:
changed = True
l.info("LLM renamed variable %s -> %s", old_name, new_name)
return changed
@ -1064,13 +1118,15 @@ class Decompiler(Analysis):
if not new_name or new_name == current_name:
return False
l.info("LLM renamed function %s -> %s", current_name, new_name)
self.func.name = new_name
self.func.is_default_name = False
if self.codegen and isinstance(self.codegen, CStructuredCodeGenerator) and self.codegen.cfunc:
self.codegen.cfunc.name = new_name
try:
edit = rename_function(self.project, self.func, new_name, kb=self.kb, flavor=self._flavor, rerender=False)
except DecompilationEditError as ex:
l.debug("LLM function rename %s -> %s rejected: %s", current_name, new_name, ex)
return False
return True
if edit.changed:
l.info("LLM renamed function %s -> %s", current_name, new_name)
return edit.changed
def llm_suggest_variable_types(
self, llm_client=None, code_text: str | None = None, raise_exc: bool = False
@ -1093,19 +1149,20 @@ class Decompiler(Analysis):
if not code_text:
return False
varman = self._variable_kb.variables[self.func.addr]
unified_vars = varman.get_unified_variables(sort=None)
varman = self.kb.dec_variables[self.func.addr]
if not unified_vars:
return False
# build current type info
var_type_info = {}
for v in unified_vars:
name = v.name or str(v)
current_type = varman.get_variable_type(v)
for name in list_variable_names(self.codegen, self.kb, self.func.addr):
try:
rv = resolve_variable(self.kb, self.func.addr, name, codegen=self.codegen, flavor=self._flavor)
except DecompilationEditError:
continue
current_type = varman.get_variable_type(rv.variable)
var_type_info[name] = str(current_type) if current_type else "unknown"
if not var_type_info:
return False
prompt = (
"You are a reverse engineering assistant. Given the following decompiled C code and the current "
"variable types, suggest better C types for the variables. Only include variables whose types "
@ -1120,33 +1177,33 @@ class Decompiler(Analysis):
if not result:
return False
# build name-to-variable lookup
name_to_var = {}
for v in unified_vars:
key = v.name or str(v)
name_to_var[key] = v
changed = False
for type_change in result.type_changes:
var_name = type_change.variable_name
type_str = type_change.new_type
var_name, type_str = type_change.variable_name, type_change.new_type
if not type_str:
continue
var = name_to_var.get(var_name)
if var is None:
continue
try:
new_type = parse_type(type_str, arch=self.project.arch)
except Exception: # pylint:disable=broad-exception-caught
l.debug("LLM suggested unparseable type '%s' for %s", type_str, var_name)
# reflow once at the end rather than per variable: Typehoon is expensive
edit = set_variable_type(
self.project,
self.func,
var_name,
type_str,
kb=self.kb,
flavor=self._flavor,
reflow=False,
)
except DecompilationEditError as ex:
l.debug("LLM retype of %s to '%s' rejected: %s", var_name, type_str, ex)
continue
if edit.changed:
changed = True
l.info("LLM changed type of %s to %s", var_name, type_str)
varman.set_variable_type(var, new_type, mark_manual=True, all_unified=True)
changed = True
l.info("LLM changed type of %s to %s", var_name, type_str)
if changed and self.codegen:
self.codegen.reload_variable_types()
if changed:
new_codegen = reflow_types(self.project, self.func, kb=self.kb, flavor=self._flavor, rerender=False)
if new_codegen is not None:
self.codegen = new_codegen
return changed

View file

@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Any
from angr.analyses.analysis import Analysis
if TYPE_CHECKING:
from angr import KnowledgeBase
from angr.analyses.decompiler.variable_map import VariableMap
l = logging.getLogger(name=__name__)
@ -24,7 +23,6 @@ class DephicationBase(Analysis):
func,
vvar_to_vvar_mapping: dict[int, int] | None = None,
rewrite: bool = False,
variable_kb: KnowledgeBase | None = None,
variable_map: VariableMap | None = None,
):
if isinstance(func, str):
@ -32,7 +30,6 @@ class DephicationBase(Analysis):
else:
self._function = func
self.variable_kb = variable_kb
self.vvar_to_vvar_mapping = vvar_to_vvar_mapping if vvar_to_vvar_mapping is not None else None
self.variable_map = variable_map
self.rewrite = rewrite

View file

@ -15,7 +15,6 @@ from .dephication_base import DephicationBase
from .graph_rewriting import GraphRewritingAnalysis
if TYPE_CHECKING:
from angr import KnowledgeBase
from angr.analyses.decompiler.variable_map import VariableMap
@ -34,7 +33,6 @@ class GraphDephication(DephicationBase): # pylint:disable=abstract-method
ail_graph,
vvar_to_vvar_mapping: dict[int, int] | None = None,
rewrite: bool = False,
variable_kb: KnowledgeBase | None = None,
variable_map: VariableMap | None = None,
):
"""
@ -48,7 +46,6 @@ class GraphDephication(DephicationBase): # pylint:disable=abstract-method
func,
vvar_to_vvar_mapping=vvar_to_vvar_mapping,
rewrite=rewrite,
variable_kb=variable_kb,
variable_map=variable_map,
)
@ -76,7 +73,7 @@ class GraphDephication(DephicationBase): # pylint:disable=abstract-method
self._function,
self._graph,
self.vvar_to_vvar_mapping,
variable_kb=self.variable_kb,
kb=self.kb,
variable_map=self.variable_map,
)
return rewriter.out_graph

View file

@ -26,13 +26,13 @@ class GraphRewritingAnalysis(ForwardAnalysis[None, NodeType, object, object, obj
func,
ail_graph,
vvar_to_vvar: dict[int, int],
variable_kb=None,
kb=None,
variable_map=None,
):
self.project = project
self._function = func
self._graph_visitor = FunctionGraphVisitor(self._function, ail_graph)
self.variable_kb = variable_kb
self._dvars_kb = kb
self.variable_map = variable_map
ForwardAnalysis.__init__(
@ -44,7 +44,7 @@ class GraphRewritingAnalysis(ForwardAnalysis[None, NodeType, object, object, obj
self.project,
self._vvar_to_vvar,
func_addr=self._function.addr,
variable_kb=self.variable_kb,
kb=self._dvars_kb,
variable_map=self.variable_map,
)

View file

@ -9,7 +9,7 @@ from angr.ailment.block import Block
from angr.ailment.expression import Phi, VirtualVariable
from angr.ailment.statement import Assignment, ConditionalJump, Jump, Label
from angr.analyses.analysis import Analysis, register_analysis
from angr.analyses.s_reaching_definitions import SRDAModel
from angr.analyses.s_reaching_definitions import SRDAModel, SReachingDefinitions
from angr.knowledge_plugins.functions import Function
from angr.utils.ssa import is_phi_assignment
@ -60,9 +60,7 @@ class GraphDephicationVVarMapping(Analysis): # pylint:disable=abstract-method
self.vvar_to_vvar_mapping = None
self.copied_vvar_ids: set[int] = set()
self._rd: SRDAModel = self.project.analyses.SReachingDefinitions(
subject=self._function, func_graph=self._graph
).model
self._rd: SRDAModel = SReachingDefinitions(self.project, subject=self._function, func_graph=self._graph).model
self._blocks: dict[tuple[int, int | None], Block] = {(block.addr, block.idx): block for block in self._graph}
self._analyze()
@ -74,14 +72,6 @@ class GraphDephicationVVarMapping(Analysis): # pylint:disable=abstract-method
# collect phi assignments
phi_to_srcvarid = self._collect_phi_assignments()
# initialize phi_congruence_class
phi_congruence_class: dict[int, set[int]] = {}
for phi_varid in phi_to_srcvarid:
phi_congruence_class[phi_varid] = {phi_varid}
for src_and_varids in phi_to_srcvarid.values():
for _, varid in src_and_varids:
phi_congruence_class[varid] = {varid}
# compute liveness
liveness = self.project.analyses.SLiveness(
self._function, func_graph=self._graph, entry=self._entry, arg_vvars=self._arg_vvars
@ -91,13 +81,73 @@ class GraphDephicationVVarMapping(Analysis): # pylint:disable=abstract-method
live_outs = liveness.model.live_outs
interference = liveness.interference_graph()
unresolved_neighbor_map = defaultdict(set)
# A phi congruence class is the transitive closure over phi statements. This means two vvars that never appear
# together in a single phi statement can still land in the same phi congruence class through a chain of phi
# statements.
#
# The following code maintains phi congruence classes in a union-find structure.
# Each congruence class records its members and the set of vvars interfering with any of its members.
parent: dict[int, int] = {}
members: dict[int, set[int]] = {}
interferes_with: dict[int, set[int]] = {}
# check for interferences
candidate_vvar_set_to_phiid: defaultdict[frozenset[int], set[int]] = defaultdict(set)
for phi_id, src_and_varids in phi_to_srcvarid.items():
def _make(v: int) -> None:
if v not in parent:
parent[v] = v
members[v] = {v}
nbrs = set(interference[v]) if interference.has_node(v) else set()
nbrs.discard(v) # a self-loop carries no information for coalescing
interferes_with[v] = nbrs
def _find(v: int) -> int:
_make(v)
root = v
while parent[root] != root:
root = parent[root]
while parent[v] != root:
parent[v], v = root, parent[v]
return root
def _classes_interfere(v0: int, v1: int) -> bool:
r0, r1 = _find(v0), _find(v1)
if r0 == r1:
return False
if len(members[r0]) > len(members[r1]):
r0, r1 = r1, r0
return not interferes_with[r1].isdisjoint(members[r0])
def _union(v0: int, v1: int) -> None:
r0, r1 = _find(v0), _find(v1)
if r0 == r1:
return
if len(members[r0]) < len(members[r1]):
r0, r1 = r1, r0
parent[r1] = r0
members[r0] |= members[r1]
interferes_with[r0] |= interferes_with[r1]
del members[r1]
del interferes_with[r1]
def _note_interference(v0: int, v1: int) -> None:
if v0 == v1:
return
interference.add_edge(v0, v1)
interferes_with[_find(v0)].add(v1)
interferes_with[_find(v1)].add(v0)
def _loc_key(loc: tuple[int, int | None]) -> tuple[int, int]:
return loc[0], -1 if loc[1] is None else loc[1]
# process phi statements in a deterministic order
phi_ids = sorted(
phi_to_srcvarid,
key=lambda vid: (_loc_key(self._vvar_defloc[vid][0]), self._vvar_defloc[vid][1], vid),
)
for phi_id in phi_ids:
src_and_varids = sorted(phi_to_srcvarid[phi_id], key=lambda t: (_loc_key(t[0]), t[1]))
candidate_vvar_set: set[int] = set()
src_and_varids = list(src_and_varids)
unresolved_neighbor_map: defaultdict[int, set[int]] = defaultdict(set)
for i in range(-1, len(src_and_varids)):
for j in range(i + 1, len(src_and_varids)):
@ -111,9 +161,18 @@ class GraphDephicationVVarMapping(Analysis): # pylint:disable=abstract-method
if var1 == var2:
continue
if interference.has_edge(var1, var2):
intersection_1 = phi_congruence_class[var1].intersection(live_outs[src1])
intersection_2 = phi_congruence_class[var2].intersection(live_outs[src2])
if _classes_interfere(var1, var2):
# the intersection considers both liveouts and the vvars used in the last statement of the
# block if it is a jump or a conditional jump because we cannot insert a vvar copy statement
# after the jump. this is a special case that is not covered in Sreedhar et al.'s paper. It is
# documented in "Revisiting Out-of-SSA Translation for Correctness, Code Quality, and
# Efficiency" (Section II.A) by Boissinot et. al.
intersection_1 = members[_find(var1)].intersection(
live_outs[src2] | liveness.model.block_end_vvars.get(src2, set())
)
intersection_2 = members[_find(var2)].intersection(
live_outs[src1] | liveness.model.block_end_vvars.get(src1, set())
)
if intersection_1 and not intersection_2:
# case 1
candidate_vvar_set.add(var1)
@ -131,7 +190,7 @@ class GraphDephicationVVarMapping(Analysis): # pylint:disable=abstract-method
# process unresolved_neighbor_map in a decreasing order of the number of neighbors
while unresolved_neighbor_map:
varid, neighbors = max(unresolved_neighbor_map.items(), key=lambda x: len(x[1]))
varid, neighbors = max(unresolved_neighbor_map.items(), key=lambda x: (len(x[1]), x[0]))
del unresolved_neighbor_map[varid]
candidate_vvar_set.add(varid)
@ -142,19 +201,16 @@ class GraphDephicationVVarMapping(Analysis): # pylint:disable=abstract-method
if not unresolved_neighbor_map[neighbor]:
del unresolved_neighbor_map[neighbor]
if candidate_vvar_set:
candidate_vvar_set_to_phiid[frozenset(candidate_vvar_set)].add(phi_id)
for vvar_set, phi_ids in candidate_vvar_set_to_phiid.items():
# insert copies of variables as needed
for varid in vvar_set:
insertion_type, new_vvar_ids = self._insert_vvar_copy(varid, phi_ids)
for varid in sorted(candidate_vvar_set):
insertion_type, new_vvar_ids = self._insert_vvar_copy(varid, {phi_id})
if insertion_type == 0:
for src, old_vvar_id, new_vvar_id in new_vvar_ids:
for src, old_vvar_id, new_vvar_id in sorted(
new_vvar_ids, key=lambda t: (_loc_key(t[0]), t[1], t[2])
):
self.copied_vvar_ids.add(new_vvar_id)
phi_congruence_class[new_vvar_id] = {new_vvar_id}
_make(new_vvar_id)
live_outs[src].add(new_vvar_id)
src_block = self._blocks[(src[0], src[1])]
@ -167,47 +223,44 @@ class GraphDephicationVVarMapping(Analysis): # pylint:disable=abstract-method
# update interference graph
for vvar_id in live_outs[src]:
interference.add_edge(new_vvar_id, vvar_id)
_note_interference(new_vvar_id, vvar_id)
else: # insertion_type == 1, i.e. the set has only one element
for phi_block_loc, old_phi_varid, new_phi_varid in new_vvar_ids:
else: # insertion_type == 1, i.e. the copy replaces the phi destination
for phi_block_loc, old_phi_varid, new_phi_varid in sorted(
new_vvar_ids, key=lambda t: (_loc_key(t[0]), t[1], t[2])
):
self.copied_vvar_ids.add(new_phi_varid)
phi_congruence_class[new_phi_varid] = {new_phi_varid}
_make(new_phi_varid)
live_ins[phi_block_loc].discard(old_phi_varid)
live_ins[phi_block_loc].add(new_phi_varid)
# update interference graph
for vvar_id in live_ins[phi_block_loc]:
interference.add_edge(new_phi_varid, vvar_id)
_note_interference(new_phi_varid, vvar_id)
# update phi_congruence_class
for phi_id in phi_to_srcvarid:
# merge the congruence classes of the phi destination and its (possibly rewritten) sources
(phidef_block_addr, phidef_block_idx), phidef_stmt_idx = self._vvar_defloc[phi_id]
phi_block = self._blocks[(phidef_block_addr, phidef_block_idx)]
# phi_stmt is the newly created phi statement with variables replaced
phi_stmt = phi_block.statements[phidef_stmt_idx]
phi_src_vvar_ids = {src_vvar.varid for _, src_vvar in phi_stmt.src.src_and_vvars if src_vvar is not None}
new_class = phi_congruence_class[phi_stmt.dst.varid]
for src_vvar_id in phi_src_vvar_ids:
new_class |= phi_congruence_class[src_vvar_id]
phi_congruence_class[src_vvar_id] = new_class
for _, src_vvar in phi_stmt.src.src_and_vvars:
if src_vvar is not None:
_union(phi_stmt.dst.varid, src_vvar.varid)
# append statements that were recorded for prepending
for block, stmts in self._stmts_to_prepend.items():
for stmt in stmts:
self._prepend_stmt(block, stmt)
# remove congruence classes with only one element
for phi_varid in list(phi_congruence_class):
if len(phi_congruence_class[phi_varid]) == 1:
del phi_congruence_class[phi_varid]
mapping: dict[int, int] = {}
for phi_varid, congruence_class in phi_congruence_class.items():
for varid in congruence_class:
mapping[varid] = phi_varid
for class_members in members.values():
if len(class_members) <= 1:
# congruence classes with only one element require no remapping
continue
rep = min(class_members)
for varid in class_members:
mapping[varid] = rep
return mapping
@ -237,7 +290,9 @@ class GraphDephicationVVarMapping(Analysis): # pylint:disable=abstract-method
if src not in stmt_appended_locs:
# we have not yet appended a statement to this block
the_block = self._blocks[src]
ins_addr = the_block.addr + the_block.original_size - 1
ins_addr = (
the_block.addr + (the_block.original_size if the_block.original_size is not None else 1) - 1
)
new_category = phi_stmt.dst.category
new_oident = phi_stmt.dst.oident
new_vvar = VirtualVariable(

View file

@ -56,7 +56,7 @@ class SimEngineDephiRewriting(SimEngineNostmtAIL[None, Expression | None, Statem
project,
vvar_to_vvar: dict[int, int],
func_addr: int | None = None,
variable_kb: KnowledgeBase | None = None,
kb: KnowledgeBase | None = None,
variable_map: VariableMap | None = None,
):
super().__init__(project)
@ -64,7 +64,7 @@ class SimEngineDephiRewriting(SimEngineNostmtAIL[None, Expression | None, Statem
self.vvar_to_vvar = vvar_to_vvar
self.out_block = None
self.func_addr = func_addr
self.variable_kb = variable_kb
self._dvars_kb = kb
self.variable_map = variable_map
self._stmt_handlers["IncompleteSwitchCaseHeadStatement"] = self._handle_stmt_IncompleteSwitchCaseHeadStatement
@ -121,13 +121,13 @@ class SimEngineDephiRewriting(SimEngineNostmtAIL[None, Expression | None, Statem
return ()
if (
self.func_addr is not None
and self.variable_kb is not None
and self.func_addr in self.variable_kb.variables
and self._dvars_kb is not None
and self.func_addr in self._dvars_kb.dec_variables
and self.variable_map is not None
):
dst_var = self.variable_map.variable(dst)
src_var = self.variable_map.variable(src)
var_manager = self.variable_kb.variables[self.func_addr]
var_manager = self._dvars_kb.dec_variables[self.func_addr]
if (
dst_var is not None
and src_var is not None
@ -136,6 +136,7 @@ class SimEngineDephiRewriting(SimEngineNostmtAIL[None, Expression | None, Statem
# skip it
return ()
if new_dst is not None or new_src is not None:
return Assignment(stmt.idx, dst, src, **stmt.tags)
return None
@ -236,8 +237,9 @@ class SimEngineDephiRewriting(SimEngineNostmtAIL[None, Expression | None, Statem
return None
def _handle_stmt_DirtyStatement(self, stmt: DirtyStatement) -> DirtyStatement | None:
dirty = self._expr(stmt.dirty)
if dirty is None or dirty is stmt.dirty:
dirty_in = stmt.dirty
dirty = self._expr(dirty_in)
if dirty is None or dirty == dirty_in:
return None
assert isinstance(dirty, DirtyExpression)
return DirtyStatement(stmt.idx, dirty, **stmt.tags)
@ -394,19 +396,24 @@ class SimEngineDephiRewriting(SimEngineNostmtAIL[None, Expression | None, Statem
return None
def _handle_expr_Extract(self, expr):
base = self._expr(expr.base) or expr.base
offset = self._expr(expr.offset) or expr.offset
base_in = expr.base
offset_in = expr.offset
base = self._expr(base_in) or base_in
offset = self._expr(offset_in) or offset_in
if base is not expr.base or offset is not expr.offset:
if base != base_in or offset != offset_in:
return Extract(expr.idx, expr.bits, base, offset, expr.endness, **expr.tags)
return None
def _handle_expr_Insert(self, expr):
base = self._expr(expr.base) or expr.base
offset = self._expr(expr.offset) or expr.offset
value = self._expr(expr.value) or expr.value
base_in = expr.base
offset_in = expr.offset
value_in = expr.value
base = self._expr(base_in) or base_in
offset = self._expr(offset_in) or offset_in
value = self._expr(value_in) or value_in
if base is not expr.base or offset is not expr.offset or value is not expr.value:
if base != base_in or offset != offset_in or value != value_in:
return Insert(expr.idx, base, offset, value, expr.endness, **expr.tags)
return None
@ -549,6 +556,7 @@ class SimEngineDephiRewriting(SimEngineNostmtAIL[None, Expression | None, Statem
_handle_binop_CmpLTV = _unreachable
_handle_binop_MinV = _unreachable
_handle_binop_MaxV = _unreachable
_handle_binop_HAddV = _unreachable
_handle_binop_QAddV = _unreachable
_handle_binop_QSubV = _unreachable
_handle_binop_QNarrowBinV = _unreachable

View file

@ -63,8 +63,8 @@ class SeqNodeRewriter(SequenceWalker):
seq_node: SequenceNode,
vvar_to_vvar: dict[int, int],
project: angr.Project,
variable_kb: KnowledgeBase | None = None,
func_addr: int | None = None,
kb: KnowledgeBase | None = None,
variable_map: VariableMap | None = None,
):
super().__init__(
@ -81,7 +81,7 @@ class SeqNodeRewriter(SequenceWalker):
self.vvar_to_vvar = vvar_to_vvar
self.variable_map = variable_map
self.engine = SimEngineDephiRewriting(
project, self.vvar_to_vvar, func_addr=func_addr, variable_kb=variable_kb, variable_map=self.variable_map
project, self.vvar_to_vvar, func_addr=func_addr, kb=kb, variable_map=self.variable_map
)
self.output = self.walk(seq_node)
@ -134,14 +134,12 @@ class SeqNodeDephication(DephicationBase):
seq_node,
vvar_to_vvar_mapping: dict[int, int] | None = None,
rewrite: bool = False,
variable_kb: KnowledgeBase | None = None,
variable_map: VariableMap | None = None,
):
super().__init__(
func,
vvar_to_vvar_mapping=vvar_to_vvar_mapping,
rewrite=rewrite,
variable_kb=variable_kb,
variable_map=variable_map,
)
@ -160,7 +158,7 @@ class SeqNodeDephication(DephicationBase):
self.vvar_to_vvar_mapping,
self.project,
func_addr=self._function.addr,
variable_kb=self.variable_kb,
kb=self.kb,
variable_map=self.variable_map,
)
return rewriter.output

View file

@ -0,0 +1,89 @@
"""
Knowledge-base edits for decompilation output: renaming, retyping, and commenting.
This layer is deliberately free of any UI or transport dependency so both the headless MCP server
and angr-management drive the same code. It is *not* thread-safe: locking belongs to whoever owns
the knowledge base's lifetime.
"""
from __future__ import annotations
from .cache import (
DEFAULT_FLAVOR,
get_cache,
invalidate,
require_cache,
restore_user_edits,
snapshot_user_edits,
)
from .errors import (
AmbiguousFunctionError,
DecompilationEditError,
FunctionNotFoundError,
InvalidNameError,
NameCollisionError,
NotDecompiledError,
TypeParseError,
UnsupportedEditError,
VariableNotFoundError,
)
from .hooks import EditHooks, NullEditHooks
from .ops import (
global_variable_at,
reflow_types,
rename_function,
rename_global,
rename_variable,
set_comment,
set_function_prototype,
set_global_type,
set_variable_type,
)
from .resolve import (
ResolvedVariable,
concrete_variables,
list_variable_names,
parse_address,
resolve_function,
resolve_variable,
validate_name,
)
from .results import EditResult, Refresh
__all__ = [
"DEFAULT_FLAVOR",
"AmbiguousFunctionError",
"DecompilationEditError",
"EditHooks",
"EditResult",
"FunctionNotFoundError",
"InvalidNameError",
"NameCollisionError",
"NotDecompiledError",
"NullEditHooks",
"Refresh",
"ResolvedVariable",
"TypeParseError",
"UnsupportedEditError",
"VariableNotFoundError",
"concrete_variables",
"get_cache",
"global_variable_at",
"invalidate",
"list_variable_names",
"parse_address",
"reflow_types",
"rename_function",
"rename_global",
"rename_variable",
"require_cache",
"resolve_function",
"resolve_variable",
"restore_user_edits",
"set_comment",
"set_function_prototype",
"set_global_type",
"set_variable_type",
"snapshot_user_edits",
"validate_name",
]

View file

@ -0,0 +1,126 @@
"""
Decompilation-cache access for the edit layer.
Both ``kb.decompilations`` and ``kb.dec_variables`` spill to LMDB under memory pressure and hand
back a freshly deserialized -- that is, *different* -- Python object on reload. Never hold a
``DecompilationCache`` or a ``VariableManagerInternal`` across anything that can decompile; always
re-fetch by key at the point of mutation.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from .errors import NotDecompiledError
if TYPE_CHECKING:
from collections.abc import Iterable
from angr.analyses.decompiler.decompilation_cache import DecompilationCache
from angr.knowledge_base import KnowledgeBase
from angr.sim_type import SimType
DEFAULT_FLAVOR = "pseudocode"
def get_cache(kb: KnowledgeBase, func_addr: int, flavor: str = DEFAULT_FLAVOR) -> DecompilationCache | None:
"""Return the cached decompilation for a function, or None if there is none."""
return kb.decompilations.get((func_addr, flavor), None)
def require_cache(kb: KnowledgeBase, func_addr: int, flavor: str = DEFAULT_FLAVOR) -> DecompilationCache:
"""Return the cached decompilation, raising NotDecompiledError if it is missing or empty."""
cache = get_cache(kb, func_addr, flavor)
if cache is None or cache.codegen is None:
raise NotDecompiledError(
f"Function {func_addr:#x} has not been decompiled yet (flavor {flavor!r}). Decompile it first."
)
return cache
def invalidate(
kb: KnowledgeBase,
func_addr: int,
*,
flavors: Iterable[str] | None = None,
drop_variables: bool = False,
) -> None:
"""
Drop cached decompilations for a function.
:param flavors: Flavors to drop; None drops every flavor currently cached. A prototype
change must drop all of them, or a non-pseudocode flavor silently
retains the old signature.
:param drop_variables: Also drop ``kb.dec_variables[func_addr]``. Required for new argument
names to take effect, but it discards every rename and manual type for
the function -- see :func:`snapshot_user_edits`.
"""
if flavors is None:
flavors = list(kb.decompilations.available_flavors(func_addr))
for flavor in flavors:
kb.decompilations.discard((func_addr, flavor))
if drop_variables and kb.dec_variables.has_function_manager(func_addr):
del kb.dec_variables[func_addr]
def snapshot_user_edits(kb: KnowledgeBase, func_addr: int) -> dict[str, tuple[str | None, SimType | None]]:
"""
Capture user renames and manual types for a function, keyed by ``SimVariable.ident``.
Used to survive the ``dec_variables`` drop that a prototype change requires.
"""
if not kb.dec_variables.has_function_manager(func_addr):
return {}
varman = kb.dec_variables[func_addr]
snapshot: dict[str, tuple[str | None, SimType | None]] = {}
for var in varman.get_unified_variables(sort=None):
if not var.ident:
continue
name = var.name if var.renamed else None
ty = varman.get_variable_type(var) if var in varman.variables_with_manual_types else None
if name is not None or ty is not None:
snapshot[var.ident] = (name, ty)
return snapshot
def restore_user_edits(
kb: KnowledgeBase, func_addr: int, snapshot: dict[str, tuple[str | None, SimType | None]]
) -> tuple[int, list[str]]:
"""
Re-apply a :func:`snapshot_user_edits` result after re-decompilation.
Best-effort: idents can change across a re-decompile, so the unmatched ones are returned rather
than silently dropped.
"""
if not snapshot or not kb.dec_variables.has_function_manager(func_addr):
return 0, sorted(snapshot)
varman = kb.dec_variables[func_addr]
by_ident = {var.ident: var for var in varman.get_unified_variables(sort=None) if var.ident}
restored = 0
missing: list[str] = []
for ident, (name, ty) in snapshot.items():
var = by_ident.get(ident)
if var is None:
missing.append(ident)
continue
if name is not None:
var.name = name
var.renamed = True
var.clear_hash()
if ty is not None:
varman.set_variable_type(var, ty, all_unified=True, mark_manual=True)
restored += 1
return restored, sorted(missing)
def function_summary(kb: KnowledgeBase, func_addr: int) -> dict[str, Any]:
"""Small helper for edit results: what is currently cached for a function."""
return {
"flavors": sorted(kb.decompilations.available_flavors(func_addr)),
"has_variables": kb.dec_variables.has_function_manager(func_addr),
}

View file

@ -0,0 +1,56 @@
from __future__ import annotations
from angr.errors import AngrError
class DecompilationEditError(AngrError):
"""Base class for every failure raised by the decompilation edit layer."""
class FunctionNotFoundError(DecompilationEditError):
"""No function matches the given address or name."""
class AmbiguousFunctionError(DecompilationEditError):
"""More than one function matches the given name. Renames make this reachable."""
def __init__(self, message: str, addresses: list[int] | None = None):
super().__init__(message)
self.addresses: list[int] = addresses if addresses is not None else []
class VariableNotFoundError(DecompilationEditError):
"""
No variable in the decompilation matches the given display name.
``candidates`` carries the names that *are* available, so a caller that failed can correct
itself without a second round trip.
"""
def __init__(self, message: str, candidates: list[str] | None = None):
super().__init__(message)
self.candidates: list[str] = candidates if candidates is not None else []
class NotDecompiledError(DecompilationEditError):
"""The function has no cached decompilation, so there is nothing to edit."""
class InvalidNameError(DecompilationEditError):
"""The requested name is not a usable identifier."""
class NameCollisionError(DecompilationEditError):
"""The requested name is already bound to a different function, variable, or label."""
def __init__(self, message: str, existing: int | str | None = None):
super().__init__(message)
self.existing = existing
class TypeParseError(DecompilationEditError):
"""A C type declaration or function signature could not be parsed."""
class UnsupportedEditError(DecompilationEditError):
"""The requested edit is not supported for this kind of target."""

View file

@ -0,0 +1,93 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Protocol, runtime_checkable
if TYPE_CHECKING:
from angr.knowledge_plugins.functions import Function
from angr.sim_type import SimType, SimTypeFunction
from angr.sim_variable import SimVariable
@runtime_checkable
class EditHooks(Protocol):
"""
Notifications fired immediately *before* each mutation, while the old value is still readable
from the knowledge base.
The method set mirrors angr-management's plugin hooks one-for-one so its adapter is a pure
forwarder. Firing before the mutation matches what the GUI's own edit dialogs do, and is what
lets a handler snapshot pre-edit state.
"""
def before_function_renamed(self, func: Function, old_name: str, new_name: str) -> None: ...
def before_stack_var_renamed(self, func: Function, offset: int, old_name: str, new_name: str) -> None: ...
def before_func_arg_renamed(self, func: Function, arg_index: int, old_name: str, new_name: str) -> None: ...
def before_global_var_renamed(self, addr: int, old_name: str, new_name: str) -> None: ...
def before_stack_var_retyped(
self, func: Function, offset: int, old_type: SimType | None, new_type: SimType
) -> None: ...
def before_func_arg_retyped(
self, func: Function, arg_index: int, old_type: SimType | None, new_type: SimType
) -> None: ...
def before_global_var_retyped(self, addr: int, old_type: SimType | None, new_type: SimType) -> None: ...
def before_other_var_retyped(self, var: SimVariable, old_type: SimType | None, new_type: SimType) -> None: ...
def before_function_retyped(
self, func: Function, old_proto: SimTypeFunction | None, new_proto: SimTypeFunction
) -> None: ...
def before_comment_changed(self, addr: int, old: str, new: str, created: bool, decomp: bool) -> None: ...
class NullEditHooks:
"""A concrete no-op implementation. Subclass it so an adapter only overrides what it needs."""
def before_function_renamed(self, func: Function, old_name: str, new_name: str) -> None:
pass
def before_stack_var_renamed(self, func: Function, offset: int, old_name: str, new_name: str) -> None:
pass
def before_func_arg_renamed(self, func: Function, arg_index: int, old_name: str, new_name: str) -> None:
pass
def before_global_var_renamed(self, addr: int, old_name: str, new_name: str) -> None:
pass
def before_stack_var_retyped(
self, func: Function, offset: int, old_type: SimType | None, new_type: SimType
) -> None:
pass
def before_func_arg_retyped(
self, func: Function, arg_index: int, old_type: SimType | None, new_type: SimType
) -> None:
pass
def before_global_var_retyped(self, addr: int, old_type: SimType | None, new_type: SimType) -> None:
pass
def before_other_var_retyped(self, var: SimVariable, old_type: SimType | None, new_type: SimType) -> None:
pass
def before_function_retyped(
self, func: Function, old_proto: SimTypeFunction | None, new_proto: SimTypeFunction
) -> None:
pass
def before_comment_changed(self, addr: int, old: str, new: str, created: bool, decomp: bool) -> None:
pass
NULL_HOOKS = NullEditHooks()
def coerce_hooks(hooks: EditHooks | None) -> EditHooks:
return NULL_HOOKS if hooks is None else hooks

View file

@ -0,0 +1,638 @@
"""
The mutating operations of the edit layer.
Every operation re-fetches the decompilation cache by key at the point of mutation rather than
holding a reference: see the note in :mod:`.cache`.
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from angr.knowledge_plugins.functions.function import PrototypeSource
from angr.sim_type import parse_signature, parse_type
from angr.sim_variable import SimMemoryVariable, SimStackVariable
from .cache import DEFAULT_FLAVOR, get_cache, invalidate, require_cache, restore_user_edits, snapshot_user_edits
from .errors import NameCollisionError, TypeParseError, UnsupportedEditError
from .hooks import coerce_hooks
from .resolve import concrete_variables, list_variable_names, resolve_variable, validate_name
from .results import EditResult, Refresh
if TYPE_CHECKING:
from angr.knowledge_base import KnowledgeBase
from angr.knowledge_plugins.functions import Function
from angr.project import Project
from angr.sim_type import SimType, SimTypeFunction
from .hooks import EditHooks
l = logging.getLogger(name=__name__)
def _require_free_function_name(kb: KnowledgeBase, name: str, own_addr: int) -> None:
for other in kb.functions.get_by_name(name):
if other is not None and other.addr != own_addr:
raise NameCollisionError(
f"A function named {name!r} already exists at {other.addr:#x}.", existing=other.addr
)
existing = kb.labels.lookup(name, None)
if existing is not None and existing != own_addr:
raise NameCollisionError(f"The label {name!r} is already bound to {existing:#x}.", existing=existing)
def _set_arg_name(codegen, arg_index: int, new_name: str) -> None:
"""
Rewrite an argument's name in the rendered signature.
``cfunc.functy`` *is* ``func.prototype``, so this single write also updates the stored
prototype -- do not write both.
"""
cfunc = getattr(codegen, "cfunc", None)
if cfunc is None or cfunc.functy is None or not cfunc.functy.arg_names:
return
arg_names = list(cfunc.functy.arg_names)
if 0 <= arg_index < len(arg_names):
arg_names[arg_index] = new_name
cfunc.functy.arg_names = tuple(arg_names)
def rename_function(
project: Project,
func: Function,
new_name: str,
*,
kb: KnowledgeBase | None = None,
hooks: EditHooks | None = None,
flavor: str = DEFAULT_FLAVOR,
allow_overwrite: bool = True,
strict_names: bool = True,
rerender: bool = True,
) -> EditResult:
"""
Rename a function.
Nothing is invalidated: other functions' cached ASTs reference the same Function object, so only
their rendered text goes stale. That is reported through ``Refresh.text_stale_all`` for the
caller to act on lazily.
"""
kb = project.kb if kb is None else kb
hooks = coerce_hooks(hooks)
validate_name(new_name, strict=strict_names)
old_name = func.name
if old_name == new_name:
return EditResult(changed=False, kind="function_name", func_addr=func.addr, old=old_name, new=new_name)
if not allow_overwrite:
_require_free_function_name(kb, new_name, func.addr)
hooks.before_function_renamed(func, old_name, new_name)
kb.functions.get_by_addr(func.addr).name = new_name
# the name setter does not clear this, and leaving it set makes the default-naming machinery
# treat the function as still auto-named
func.is_default_name = False
cache = get_cache(kb, func.addr, flavor)
if cache is not None and cache.codegen is not None and getattr(cache.codegen, "cfunc", None) is not None:
cache.codegen.cfunc.name = new_name
cache.codegen.cfunc.demangled_name = new_name
if rerender:
cache.codegen.regenerate_text()
return EditResult(
changed=True,
kind="function_name",
func_addr=func.addr,
old=old_name,
new=new_name,
refresh=Refresh(
text_stale=frozenset({func.addr}),
text_stale_all=True,
function_list_dirty=True,
disassembly_dirty=True,
),
)
def rename_variable(
project: Project,
func: Function,
variable_name: str,
new_name: str,
*,
kb: KnowledgeBase | None = None,
hooks: EditHooks | None = None,
flavor: str = DEFAULT_FLAVOR,
allow_overwrite: bool = True,
strict_names: bool = True,
rerender: bool = True,
codegen=None,
) -> EditResult:
"""
Rename a local, an argument, or a global as it appears in a function's decompilation.
Sets ``renamed`` on the variable, without which a later re-decompilation overwrites the name.
"""
kb = project.kb if kb is None else kb
hooks = coerce_hooks(hooks)
validate_name(new_name, strict=strict_names)
cache = require_cache(kb, func.addr, flavor)
if codegen is None:
codegen = cache.codegen
rv = resolve_variable(kb, func.addr, variable_name, codegen=codegen, flavor=flavor)
old_name = rv.name
if old_name == new_name:
return EditResult(
changed=False,
kind="variable_name",
func_addr=func.addr,
old=old_name,
new=new_name,
detail=rv.detail(),
)
if not allow_overwrite and new_name in list_variable_names(codegen, kb, func.addr):
raise NameCollisionError(f"A variable named {new_name!r} already exists in {func.name}.", existing=new_name)
# dispatch order matches the GUI's: a stack-backed argument fires the stack hook, not the
# argument hook
if rv.kind == "global":
addr = rv.global_addr
if addr is not None and addr in kb.functions:
raise UnsupportedEditError(
f"{variable_name!r} is the entry point of a function at {addr:#x}; "
"rename it with rename_function instead."
)
hooks.before_global_var_renamed(addr, old_name, new_name)
target = rv.variable
elif rv.stack_offset is not None:
hooks.before_stack_var_renamed(func, rv.stack_offset, old_name, new_name)
target = rv.unified
elif rv.kind == "argument":
hooks.before_func_arg_renamed(func, rv.arg_index, old_name, new_name)
target = rv.unified
else:
target = rv.unified
target.name = new_name
target.renamed = True
target.clear_hash()
if rv.kind == "global" and rv.global_addr is not None:
kb.labels[rv.global_addr] = new_name
if rv.kind == "argument" and rv.arg_index is not None:
_set_arg_name(codegen, rv.arg_index, new_name)
if rerender:
codegen.regenerate_text()
return EditResult(
changed=True,
kind="variable_name",
func_addr=func.addr,
old=old_name,
new=new_name,
refresh=Refresh(
text_stale=frozenset({func.addr}),
disassembly_dirty=rv.kind == "global",
),
detail=rv.detail(),
)
def _parse_type(c_type: str | SimType, arch) -> SimType:
if not isinstance(c_type, str):
return c_type.with_arch(arch)
try:
return parse_type(c_type).with_arch(arch)
except Exception as ex: # pylint:disable=broad-exception-caught
raise TypeParseError(f"Could not parse C type {c_type!r}: {ex}") from ex
def _parse_prototype(prototype: str | SimTypeFunction, arch) -> SimTypeFunction:
if not isinstance(prototype, str):
return prototype.with_arch(arch)
try:
return parse_signature(prototype).with_arch(arch)
except Exception as ex: # pylint:disable=broad-exception-caught
raise TypeParseError(f"Could not parse C prototype {prototype!r}: {ex}") from ex
def reflow_types(
project: Project,
func: Function,
*,
kb: KnowledgeBase | None = None,
flavor: str = DEFAULT_FLAVOR,
rerender: bool = True,
):
"""
Re-run type inference over the cached constraints and refresh the rendered code.
Separate from :func:`set_variable_type` so a batch can retype many variables and reflow once;
per-variable reflow would re-run Typehoon N times.
This is not a re-decompilation: the AST is untouched, so a retype that should change how an
access renders (a struct field, an array index) needs a full re-decompilation instead.
"""
kb = project.kb if kb is None else kb
dec = project.analyses.Decompiler(func, decompile=False, use_cache=True, flavor=flavor)
cache = require_cache(kb, func.addr, flavor)
new_codegen = dec.reflow_variable_types(cache)
if new_codegen is None:
return None
cache.codegen = new_codegen
if rerender:
# reflow_variable_types ends at reload_variable_types(), which refreshes CVariable types but
# does not re-render; without this the text stays stale
new_codegen.regenerate_text()
return new_codegen
def _set_argument_type(
project: Project,
func: Function,
rv,
new_type: SimType,
*,
hooks: EditHooks,
) -> EditResult:
"""Retyping an argument means rewriting the prototype; the variable's own type is not enough."""
proto = func.prototype
if proto is None or rv.arg_index is None or rv.arg_index >= len(proto.args):
raise UnsupportedEditError(
f"Cannot retype argument {rv.name!r}: {func.name} has no prototype covering argument "
f"index {rv.arg_index}. Set the whole prototype instead."
)
old_type = proto.args[rv.arg_index]
hooks.before_func_arg_retyped(func, rv.arg_index, old_type, new_type)
new_proto = proto.copy()
args = list(new_proto.args)
args[rv.arg_index] = new_type
new_proto.args = tuple(args)
func.prototype = new_proto.with_arch(project.arch)
func.prototype_source = PrototypeSource.USER
func.ran_cca = True
return EditResult(
changed=True,
kind="variable_type",
func_addr=func.addr,
old=str(old_type),
new=str(new_type),
refresh=Refresh(redecompile=frozenset({func.addr})),
detail=rv.detail(),
)
def set_variable_type(
project: Project,
func: Function,
variable_name: str,
c_type: str | SimType,
*,
kb: KnowledgeBase | None = None,
hooks: EditHooks | None = None,
flavor: str = DEFAULT_FLAVOR,
reflow: bool = True,
rerender: bool = True,
allow_prototype_change: bool = True,
codegen=None,
) -> EditResult:
"""
Change the type of a local, an argument, or a global.
Arguments are retyped by rewriting the function prototype, which requires a re-decompilation --
the returned Refresh says so. Pass ``allow_prototype_change=False`` to refuse instead.
"""
kb = project.kb if kb is None else kb
hooks = coerce_hooks(hooks)
new_type = _parse_type(c_type, project.arch)
cache = require_cache(kb, func.addr, flavor)
if codegen is None:
codegen = cache.codegen
rv = resolve_variable(kb, func.addr, variable_name, codegen=codegen, flavor=flavor)
if rv.kind == "argument":
if not allow_prototype_change:
raise UnsupportedEditError(
f"{variable_name!r} is an argument of {func.name}; change it by setting the whole prototype."
)
return _set_argument_type(project, func, rv, new_type, hooks=hooks)
if rv.kind == "global":
varman = kb.dec_variables["global"]
old_type = varman.get_variable_type(rv.variable)
hooks.before_global_var_retyped(rv.global_addr, old_type, new_type)
varman.set_variable_type(rv.variable, new_type, all_unified=False, mark_manual=True)
else:
varman = kb.dec_variables[func.addr]
old_type = varman.get_variable_type(rv.variable)
if rv.stack_offset is not None:
hooks.before_stack_var_retyped(func, rv.stack_offset, old_type, new_type)
else:
hooks.before_other_var_retyped(rv.variable, old_type, new_type)
# mark_manual is what makes the type survive re-inference: reflow reads
# variables_with_manual_types as its ground truth
for var in concrete_variables(varman, rv):
varman.set_variable_type(var, new_type, all_unified=True, mark_manual=True)
if reflow:
reflow_types(project, func, kb=kb, flavor=flavor, rerender=rerender)
return EditResult(
changed=True,
kind="variable_type",
func_addr=func.addr,
old=None if old_type is None else str(old_type),
new=str(new_type),
refresh=Refresh(text_stale=frozenset({func.addr})),
detail=rv.detail(),
)
def set_function_prototype(
project: Project,
func: Function,
prototype: str | SimTypeFunction,
*,
kb: KnowledgeBase | None = None,
hooks: EditHooks | None = None,
flavor: str = DEFAULT_FLAVOR,
invalidate_cache: bool = True,
preserve_user_edits: bool = True,
redecompile: bool = False,
) -> EditResult:
"""
Set a function's prototype.
The function name inside the signature is ignored; use :func:`rename_function` to rename.
Dropping kb.dec_variables is what makes new argument names take effect, but it also discards
every rename and manual type for the function. Those are snapshotted and, when this function
re-decompiles, restored. Otherwise the snapshot is returned in ``detail["user_edits"]`` so a
caller that decompiles asynchronously can restore it once its own job finishes.
"""
kb = project.kb if kb is None else kb
hooks = coerce_hooks(hooks)
new_proto = _parse_prototype(prototype, project.arch)
old_proto = func.prototype
hooks.before_function_retyped(func, old_proto, new_proto)
snapshot = snapshot_user_edits(kb, func.addr) if preserve_user_edits else {}
func.prototype = new_proto
func.prototype_source = PrototypeSource.USER
# keep CompleteCallingConventions from overwriting a user-supplied prototype
func.ran_cca = True
if invalidate_cache:
invalidate(kb, func.addr, flavors=None, drop_variables=True)
detail: dict = {"user_edits": snapshot}
code = None
if redecompile:
dec = project.analyses.Decompiler(func, flavor=flavor)
if snapshot:
restored, missing = restore_user_edits(kb, func.addr, snapshot)
detail.update({"restored_user_edits": restored, "unrestored_user_edits": missing})
detail["user_edits"] = {}
if restored and dec.codegen is not None:
dec.codegen.regenerate_text()
code = dec.codegen.text if dec.codegen is not None else None
detail["code"] = code
return EditResult(
changed=True,
kind="prototype",
func_addr=func.addr,
old=None if old_proto is None else str(old_proto),
new=str(new_proto),
refresh=Refresh(redecompile=frozenset({func.addr}), function_list_dirty=True),
detail=detail,
)
_ORPHAN_MARKER = "// Orphaned comments"
def _snap_comment_addr(codegen, addr: int) -> int:
"""
Snap to the nearest address the codegen tracks, at or below ``addr``.
stmt_comments is keyed by ins_addr, so an address the codegen never emits cannot match. This
only maps an arbitrary in-function address onto a tracked one; whether the result actually
renders inline additionally depends on it being the *last* tracked address on its line, which
is not derivable from the position maps -- map_addr_to_pos records each address's first
position, not its last. :func:`_rendered_inline` reports the real outcome afterwards.
"""
insmap = getattr(codegen, "map_addr_to_pos", None)
if insmap is None:
return addr
rendered = [ins_addr for ins_addr, _ in insmap.items()]
if not rendered or addr in set(rendered):
return addr
below = [a for a in rendered if a < addr]
return max(below) if below else addr
def _rendered_inline(codegen, comment: str) -> bool:
"""Whether a comment rendered next to a statement rather than in the orphaned block."""
text = getattr(codegen, "text", None) or ""
marker = text.find(_ORPHAN_MARKER)
return marker == -1 or comment not in text[marker:]
def set_comment(
project: Project,
addr: int,
comment: str | None,
*,
kb: KnowledgeBase | None = None,
hooks: EditHooks | None = None,
flavor: str = DEFAULT_FLAVOR,
mirror_to_pseudocode: bool = True,
snap: bool = True,
rerender: bool = True,
) -> EditResult:
"""
Set the comment at an address, or clear it with an empty string or None.
The comment goes into kb.comments, which the disassembly renders and which the decompiler reads
for the function header. Per-statement pseudocode comments live in codegen.stmt_comments
instead, so both are written -- except at the function entry, where kb.comments is already what
the header renders and mirroring would show the comment twice.
"""
kb = project.kb if kb is None else kb
hooks = coerce_hooks(hooks)
text = comment or ""
old = kb.comments.get(addr, "")
existed = addr in kb.comments
hooks.before_comment_changed(addr, old, text, not existed, False)
if text:
kb.comments[addr] = text
elif existed:
del kb.comments[addr]
in_pseudocode = False
inline: bool | None = None
snapped_from = None
func = kb.functions.floor_func(addr) if mirror_to_pseudocode else None
if func is not None and not (func.addr <= addr < func.addr + max(func.size, 1)):
func = None
if func is not None:
cache = get_cache(kb, func.addr, flavor)
codegen = cache.codegen if cache is not None else None
if codegen is not None:
if addr == func.addr:
# rendered as the header comment straight from kb.comments; mirroring it into
# stmt_comments would show it twice
in_pseudocode = bool(text)
if rerender:
codegen.regenerate_text()
else:
target = _snap_comment_addr(codegen, addr) if snap else addr
if target != addr:
snapped_from = addr
cdict = codegen.stmt_comments
prev = cdict.get(target, "")
hooks.before_comment_changed(target, prev, text, target not in cdict, True)
if text:
cdict[target] = text
in_pseudocode = True
elif target in cdict:
del cdict[target]
if rerender:
codegen.regenerate_text()
if in_pseudocode:
inline = _rendered_inline(codegen, text)
return EditResult(
changed=old != text,
kind="comment",
func_addr=None if func is None else func.addr,
old=old,
new=text,
refresh=Refresh(
text_stale=frozenset() if func is None else frozenset({func.addr}),
disassembly_dirty=True,
),
detail={
"address": hex(addr),
"shown_in_pseudocode": in_pseudocode,
# None when not re-rendered here, so the caller cannot yet know
"rendered_inline": inline,
"snapped_from": None if snapped_from is None else hex(snapped_from),
},
)
def global_variable_at(kb: KnowledgeBase, addr: int):
"""The global SimVariable recorded at an address, if variable recovery produced one."""
varman = kb.dec_variables["global"]
for var in varman.get_variables(sort=None):
if isinstance(var, SimMemoryVariable) and not isinstance(var, SimStackVariable) and var.addr == addr:
return var
return None
def _reject_function_entry(kb: KnowledgeBase, addr: int, what: str) -> None:
"""kb.labels[addr] = name renames the function when addr is a function entry."""
if addr in kb.functions:
raise UnsupportedEditError(
f"{addr:#x} is the entry point of a function; {what} it with the function-level operation instead."
)
def rename_global(
project: Project,
addr: int,
new_name: str,
*,
kb: KnowledgeBase | None = None,
hooks: EditHooks | None = None,
allow_overwrite: bool = True,
strict_names: bool = True,
) -> EditResult:
"""Rename a global by address, without needing a function whose decompilation shows it."""
kb = project.kb if kb is None else kb
hooks = coerce_hooks(hooks)
validate_name(new_name, strict=strict_names)
_reject_function_entry(kb, addr, "rename")
var = global_variable_at(kb, addr)
old_name = var.name if var is not None and var.name else kb.labels.get(addr, "")
if old_name == new_name:
return EditResult(changed=False, kind="global_name", old=old_name, new=new_name)
if not allow_overwrite:
existing = kb.labels.lookup(new_name, None)
if existing is not None and existing != addr:
raise NameCollisionError(f"The label {new_name!r} is already bound to {existing:#x}.", existing=existing)
hooks.before_global_var_renamed(addr, old_name, new_name)
kb.labels[addr] = new_name
if var is not None:
var.name = new_name
var.renamed = True
var.clear_hash()
return EditResult(
changed=True,
kind="global_name",
old=old_name,
new=new_name,
refresh=Refresh(text_stale_all=True, disassembly_dirty=True),
detail={"global_address": hex(addr)},
)
def set_global_type(
project: Project,
addr: int,
c_type: str | SimType,
*,
kb: KnowledgeBase | None = None,
hooks: EditHooks | None = None,
) -> EditResult:
"""Set the type of a global by address."""
kb = project.kb if kb is None else kb
hooks = coerce_hooks(hooks)
new_type = _parse_type(c_type, project.arch)
var = global_variable_at(kb, addr)
if var is None:
raise UnsupportedEditError(
f"No global variable is recorded at {addr:#x}. Decompile a function that references it first."
)
varman = kb.dec_variables["global"]
old_type = varman.get_variable_type(var)
hooks.before_global_var_retyped(addr, old_type, new_type)
varman.set_variable_type(var, new_type, all_unified=False, mark_manual=True)
return EditResult(
changed=True,
kind="global_type",
old=None if old_type is None else str(old_type),
new=str(new_type),
refresh=Refresh(text_stale_all=True),
detail={"global_address": hex(addr)},
)

View file

@ -0,0 +1,342 @@
"""
Resolution helpers shared by every edit operation: address-or-name to Function, and pseudocode
display name to the underlying SimVariable.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from typing import TYPE_CHECKING, Literal
from angr.sim_variable import SimMemoryVariable, SimStackVariable
from .cache import DEFAULT_FLAVOR, require_cache
from .errors import (
AmbiguousFunctionError,
FunctionNotFoundError,
InvalidNameError,
VariableNotFoundError,
)
if TYPE_CHECKING:
from angr.analyses.decompiler.structured_codegen.c import CVariable
from angr.knowledge_base import KnowledgeBase
from angr.knowledge_plugins.functions import Function
from angr.sim_variable import SimVariable
_C_IDENTIFIER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
VariableKind = Literal["argument", "local", "global"]
def parse_address(value: str | int) -> int:
"""Parse an address given as an int or a string. Accepts 0x-prefixed hex and decimal."""
if isinstance(value, int):
return value
try:
return int(value, 0)
except (TypeError, ValueError) as ex:
raise InvalidNameError(f'Invalid address {value!r}. Pass a hex string such as "0x401000".') from ex
def validate_name(name: str, *, strict: bool = True) -> None:
"""
Reject names that cannot be used as identifiers.
``strict`` requires a C identifier. Without it only whitespace-free names are required, which
still admits things like ``int;`` that render as uncompilable C -- so strict is the default.
"""
if not name:
raise InvalidNameError("Name must not be empty.")
if strict:
if _C_IDENTIFIER_RE.match(name) is None:
raise InvalidNameError(
f"Invalid name {name!r}: must be a C identifier (letters, digits and underscores, "
"not starting with a digit)."
)
elif re.search(r"\s", name) is not None:
raise InvalidNameError(f"Invalid name {name!r}: names must not contain whitespace.")
def resolve_function(
kb: KnowledgeBase,
*,
address: str | int | None = None,
name: str | None = None,
containing: bool = True,
) -> Function:
"""
Find a function by address or by name.
:param containing: If True, an address inside a function resolves to that function rather
than requiring the exact entry address.
"""
if address is None and name is None:
raise FunctionNotFoundError("Specify either an address or a name.")
if address is not None:
addr = parse_address(address)
func = kb.functions.get(addr)
if func is None and containing:
func = kb.functions.floor_func(addr)
if func is not None and not (func.addr <= addr < func.addr + max(func.size, 1)):
func = None
if func is None:
raise FunctionNotFoundError(f"No function found at address {addr:#x}.")
return func
matches = [f for f in kb.functions.get_by_name(name) if f is not None]
if not matches:
raise FunctionNotFoundError(f"No function named {name!r}.")
if len(matches) > 1:
addrs = sorted(f.addr for f in matches)
raise AmbiguousFunctionError(
f"{len(matches)} functions are named {name!r}: {', '.join(hex(a) for a in addrs)}. "
"Specify an address instead.",
addresses=addrs,
)
return matches[0]
@dataclass(frozen=True)
class ResolvedVariable:
"""
A pseudocode display name resolved to the objects an edit needs.
``variable`` is the concrete/SSA variable that ``set_variable_type`` expects; ``unified`` is the
unified variable that a rename mutates (None for globals, which have no unified form).
"""
kind: VariableKind
variable: SimVariable
unified: SimVariable | None
cvar: CVariable | None = None
arg_index: int | None = None
stack_offset: int | None = None
global_addr: int | None = None
ambiguous: bool = False
@property
def name(self) -> str | None:
target = self.variable if self.unified is None else self.unified
return target.name
@property
def storage(self) -> str:
if self.stack_offset is not None:
return "stack"
if isinstance(self.variable, SimMemoryVariable) and not isinstance(self.variable, SimStackVariable):
return "memory"
return "register"
def detail(self) -> dict:
return {
"storage": self.storage,
"is_argument": self.kind == "argument",
"stack_offset": self.stack_offset,
"arg_index": self.arg_index,
"global_address": None if self.global_addr is None else hex(self.global_addr),
}
def _stack_offset(var: SimVariable) -> int | None:
return var.offset if isinstance(var, SimStackVariable) else None
def _iter_candidates(kb: KnowledgeBase, func_addr: int, codegen) -> list[ResolvedVariable]:
"""
Collect every addressable variable, in precedence order: arguments, then locals, then globals.
Locals come from the codegen (which carries the concrete variable) unioned with the variable
manager's unified variables, so unified variables not currently referenced in the body are still
addressable. Globals come from cexterns first, then any non-stack memory variable in use, then a
walk of the position map for inline references that cexterns filters out.
"""
out: list[ResolvedVariable] = []
seen: set[int] = set()
cfunc = getattr(codegen, "cfunc", None)
if cfunc is not None and cfunc.arg_list:
for idx, cvar in enumerate(cfunc.arg_list):
var = getattr(cvar, "variable", None)
if var is None:
continue
unified = getattr(cvar, "unified_variable", None) or var
out.append(
ResolvedVariable(
kind="argument",
variable=var,
unified=unified,
cvar=cvar,
arg_index=idx,
stack_offset=_stack_offset(var),
)
)
seen.add(id(unified))
if cfunc is not None and cfunc.variables_in_use:
for var, cvar in cfunc.variables_in_use.items():
unified = getattr(cvar, "unified_variable", None)
if unified is None or id(unified) in seen:
continue
out.append(
ResolvedVariable(
kind="local",
variable=var,
unified=unified,
cvar=cvar,
stack_offset=_stack_offset(var),
)
)
seen.add(id(unified))
if kb.dec_variables.has_function_manager(func_addr):
varman = kb.dec_variables[func_addr]
concrete_by_unified: dict[int, SimVariable] = {}
for var in varman.get_variables(sort=None):
unified = varman.unified_variable(var)
if unified is not None:
concrete_by_unified.setdefault(id(unified), var)
for unified in varman.get_unified_variables(sort=None):
if id(unified) in seen:
continue
# prefer a concrete SSA variable: set_variable_type(all_unified=True) keys off the
# SSA-to-unified map, so passing the unified variable would not propagate
concrete = concrete_by_unified.get(id(unified), unified)
out.append(
ResolvedVariable(
kind="local",
variable=concrete,
unified=unified,
stack_offset=_stack_offset(concrete),
)
)
seen.add(id(unified))
for cvar in _iter_global_cvars(codegen, cfunc):
var = cvar.variable
if var is None or id(var) in seen:
continue
out.append(
ResolvedVariable(
kind="global",
variable=var,
unified=None,
cvar=cvar,
global_addr=getattr(var, "addr", None),
)
)
seen.add(id(var))
return out
def concrete_variables(varman, resolved: ResolvedVariable) -> list[SimVariable]:
"""
Every SSA variable sharing the resolved variable's unified form.
Retyping applies to all of them. Computed here rather than relying on
``set_variable_type(all_unified=True)``, which silently does nothing when the variable it is
handed is not a key in the SSA-to-unified map.
"""
if resolved.unified is None:
return [resolved.variable]
out = [var for var in varman.get_variables(sort=None) if varman.unified_variable(var) is resolved.unified]
if not any(var is resolved.variable for var in out):
out.append(resolved.variable)
return out
def _iter_global_cvars(codegen, cfunc):
from angr.analyses.decompiler.structured_codegen.c import CVariable # pylint:disable=import-outside-toplevel
if codegen.cexterns:
yield from codegen.cexterns
if cfunc is not None and cfunc.variables_in_use:
for var, cvar in cfunc.variables_in_use.items():
if (
getattr(cvar, "unified_variable", None) is None
and isinstance(var, SimMemoryVariable)
and not isinstance(var, SimStackVariable)
):
yield cvar
# inline references (strings, function pointers) that cexterns filters out
if codegen.map_pos_to_node is not None:
for item in codegen.map_pos_to_node.values():
obj = getattr(item, "obj", None)
if isinstance(obj, CVariable) and obj.unified_variable is None and obj.variable is not None:
yield obj
def list_variable_names(codegen, kb: KnowledgeBase | None = None, func_addr: int | None = None) -> list[str]:
"""Every display name addressable in this decompilation, for error messages."""
if kb is None or func_addr is None:
cfunc = getattr(codegen, "cfunc", None)
names = set()
if cfunc is not None:
for cvar in cfunc.arg_list or []:
if cvar.name:
names.add(cvar.name)
for cvar in (cfunc.variables_in_use or {}).values():
if cvar.name:
names.add(cvar.name)
return sorted(names)
return sorted({rv.name for rv in _iter_candidates(kb, func_addr, codegen) if rv.name})
def resolve_variable(
kb: KnowledgeBase,
func_addr: int,
display_name: str,
*,
codegen=None,
flavor: str = DEFAULT_FLAVOR,
) -> ResolvedVariable:
"""
Resolve a name as it appears in the pseudocode to the underlying variable.
Precedence is argument > local > global. A name matching more than one variable within the same
bucket resolves deterministically and sets ``ambiguous``, rather than failing -- a batch edit
should be able to report the collision and continue.
"""
if codegen is None:
codegen = require_cache(kb, func_addr, flavor).codegen
candidates = _iter_candidates(kb, func_addr, codegen)
matches = [rv for rv in candidates if rv.name == display_name]
if not matches:
raise VariableNotFoundError(
f"No variable named {display_name!r} in the decompilation of function {func_addr:#x}.",
candidates=sorted({rv.name for rv in candidates if rv.name}),
)
if len(matches) == 1:
return matches[0]
order = {"argument": 0, "local": 1, "global": 2}
matches.sort(
key=lambda rv: (
order[rv.kind],
rv.arg_index if rv.arg_index is not None else 0,
rv.variable.ident or "",
rv.global_addr if rv.global_addr is not None else 0,
)
)
best = matches[0]
return ResolvedVariable(
kind=best.kind,
variable=best.variable,
unified=best.unified,
cvar=best.cvar,
arg_index=best.arg_index,
stack_offset=best.stack_offset,
global_addr=best.global_addr,
ambiguous=True,
)

View file

@ -0,0 +1,46 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
@dataclass(frozen=True)
class Refresh:
"""
What a caller has to redo after an edit.
Lets a UI pick between re-rendering text, rebuilding the codegen AST, and a full
re-decompilation instead of guessing. ``text_stale_all`` means every cached decompilation's
rendered text is stale (a function rename changes call sites everywhere), but the caller should
re-render lazily: eagerly re-rendering the whole cache would thrash its LRU.
"""
text_stale: frozenset[int] = frozenset()
text_stale_all: bool = False
reanalyze: frozenset[int] = frozenset()
redecompile: frozenset[int] = frozenset()
function_list_dirty: bool = False
disassembly_dirty: bool = False
def merge(self, other: Refresh) -> Refresh:
return Refresh(
text_stale=self.text_stale | other.text_stale,
text_stale_all=self.text_stale_all or other.text_stale_all,
reanalyze=self.reanalyze | other.reanalyze,
redecompile=self.redecompile | other.redecompile,
function_list_dirty=self.function_list_dirty or other.function_list_dirty,
disassembly_dirty=self.disassembly_dirty or other.disassembly_dirty,
)
@dataclass
class EditResult:
"""The outcome of a single edit. ``changed`` is False when the edit was a no-op."""
changed: bool
kind: str
func_addr: int | None = None
old: Any = None
new: Any = None
refresh: Refresh = field(default_factory=Refresh)
detail: dict[str, Any] = field(default_factory=dict)

View file

@ -122,7 +122,9 @@ class EffectiveSizeExtractor(AILBlockWalker[None, None, None]):
super()._handle_expr(expr_idx, expr, stmt_idx, stmt, block)
def _handle_Insert(self, expr_idx: int, expr, stmt_idx: int, stmt: Statement | None, block: Block | None):
# self._handle_expr(0, expr.base, stmt_idx, stmt, block)
# the base of an Insert is consumed at full width: every byte outside the inserted range is preserved
# into the result, so narrowing the base (and zero-extending it back) would destroy those bytes
self._handle_expr(0, expr.base, stmt_idx, stmt, block)
if isinstance(expr.base, VirtualVariable):
self.vvars_used_as_insert_base.add(expr.base.varid)
self._handle_expr(1, expr.offset, stmt_idx, stmt, block)
@ -270,11 +272,14 @@ class ExpressionNarrower(AILBlockRewriter):
return super().walk(block)
def _handle_Assignment(self, stmt_idx: int, stmt: Assignment, block: Block | None) -> Assignment:
if isinstance(stmt.src, Phi):
src_in = stmt.src
dst_in = stmt.dst
if isinstance(src_in, Phi):
changed = False
src_and_vvars = []
for src, vvar in stmt.src.src_and_vvars:
for src, vvar in src_in.src_and_vvars:
if vvar is None:
src_and_vvars.append((src, None))
continue
@ -296,39 +301,39 @@ class ExpressionNarrower(AILBlockRewriter):
src_and_vvars.append((src, new_var))
new_src = Phi(stmt.src.idx, stmt.src.bits, src_and_vvars, **stmt.src.tags)
new_src = Phi(src_in.idx, src_in.bits, src_and_vvars, **src_in.tags)
else:
new_src = self._handle_expr(1, stmt.src, stmt_idx, stmt, block)
changed = new_src is not stmt.src
new_src = self._handle_expr(1, src_in, stmt_idx, stmt, block)
changed = new_src != src_in
if isinstance(stmt.dst, VirtualVariable) and stmt.dst.varid in self.new_vvar_sizes:
if isinstance(dst_in, VirtualVariable) and dst_in.varid in self.new_vvar_sizes:
changed = True
new_dst = VirtualVariable(
stmt.dst.idx,
stmt.dst.varid,
self.new_vvar_sizes[stmt.dst.varid] * self.project.arch.byte_width,
category=stmt.dst.category,
oident=stmt.dst.oident,
**stmt.dst.tags,
dst_in.idx,
dst_in.varid,
self.new_vvar_sizes[dst_in.varid] * self.project.arch.byte_width,
category=dst_in.category,
oident=dst_in.oident,
**dst_in.tags,
)
self.replacement_core_vvars[new_dst.varid].append(new_dst)
if isinstance(new_src, Phi):
new_src.bits = self.new_vvar_sizes[stmt.dst.varid] * self.project.arch.byte_width
new_src.bits = self.new_vvar_sizes[dst_in.varid] * self.project.arch.byte_width
else:
new_src = Convert(
self.manager.next_atom(),
stmt.src.bits,
self.new_vvar_sizes[stmt.dst.varid] * self.project.arch.byte_width,
src_in.bits,
self.new_vvar_sizes[dst_in.varid] * self.project.arch.byte_width,
False,
new_src,
**new_src.tags,
)
else:
new_dst = self._handle_expr(0, stmt.dst, stmt_idx, stmt, block)
changed |= new_dst is not stmt.dst
new_dst = self._handle_expr(0, dst_in, stmt_idx, stmt, block)
changed |= new_dst != dst_in
if changed:
self.narrowed_any = True

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import networkx
import angr.ailment as ailment
from angr import ailment
from angr.ailment.block import Block
from .utils import find_block_by_addr

View file

@ -1,7 +1,7 @@
# pylint:disable=unused-argument
from __future__ import annotations
import angr.ailment as ailment
from angr import ailment
from .sequence_walker import SequenceWalker

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from collections import defaultdict
import angr.ailment as ailment
from angr import ailment
from .sequence_walker import SequenceWalker

View file

@ -2,7 +2,12 @@ from __future__ import annotations
from .decompilation_note import DecompilationNote, DecompilationNoteLevel
# importing the module registers the subclass for DecompilationNote.from_json dispatch
from .deobfuscated_strings import DeobfuscatedString, DeobfuscatedStringsNote
__all__ = (
"DecompilationNote",
"DecompilationNoteLevel",
"DeobfuscatedString",
"DeobfuscatedStringsNote",
)

View file

@ -1,8 +1,12 @@
from __future__ import annotations
import json
import logging
from enum import Enum
from typing import Any
l = logging.getLogger(name=__name__)
class DecompilationNoteLevel(Enum):
"""
@ -28,6 +32,8 @@ class DecompilationNote:
DecompilationNoteLevel.INFO, DecompilationNoteLevel.WARNING, and DecompilationNoteLevel.CRITICAL.
"""
_subclasses: dict[str, type[DecompilationNote]] = {}
__slots__ = (
"content",
"key",
@ -41,8 +47,51 @@ class DecompilationNote:
self.content = content
self.level = level
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
DecompilationNote._subclasses[cls.__name__] = cls
def __repr__(self):
return f"<DecompilationNote: {self.name}>"
def __str__(self):
return f"{self.name}: {self.content}"
#
# JSON serialization
#
def to_jsonable(self) -> dict[str, Any]:
try:
content = json.loads(json.dumps(self.content))
except (TypeError, ValueError):
l.warning("Failed to serialize content of decompilation note %s to JSON", self.key)
content = None
return {
"class": type(self).__name__,
"key": self.key,
"name": self.name,
"content": content,
"level": int(self.level.value),
}
def to_json(self) -> str:
return json.dumps(self.to_jsonable())
@classmethod
def from_jsonable(cls, d: dict[str, Any]) -> DecompilationNote:
klass = cls._subclasses.get(d.get("class", ""), DecompilationNote)
return klass._from_jsonable_impl(d) # pylint:disable=protected-access
@classmethod
def _from_jsonable_impl(cls, d: dict[str, Any]) -> DecompilationNote:
return cls(
key=d["key"],
name=d["name"],
content=d.get("content"),
level=DecompilationNoteLevel(d.get("level", DecompilationNoteLevel.INFO.value)),
)
@classmethod
def from_json(cls, s: str) -> DecompilationNote:
return cls.from_jsonable(json.loads(s))

View file

@ -1,5 +1,8 @@
from __future__ import annotations
import base64
from typing import Any
from .decompilation_note import DecompilationNote
@ -54,3 +57,26 @@ class DeobfuscatedStringsNote(DecompilationNote):
lines.append(f" Type {deobf_str.type} @ {deobf_str.ref_addr:#x}: {deobf_str.value!r}")
return "\n".join(lines)
#
# JSON serialization
#
def to_jsonable(self) -> dict[str, Any]:
d = super().to_jsonable()
d["strings"] = [
{
"ref_addr": s.ref_addr,
"type": s.type,
"value": base64.b64encode(s.value).decode("ascii"),
}
for _, s in sorted(self.strings.items())
]
return d
@classmethod
def _from_jsonable_impl(cls, d: dict[str, Any]) -> DeobfuscatedStringsNote:
note = cls(key=d["key"], name=d["name"])
for entry in d.get("strings", []):
note.add_string(entry["type"], base64.b64decode(entry["value"]), ref_addr=entry["ref_addr"])
return note

View file

@ -0,0 +1,30 @@
"""
Stable name resolution for decompiler optimization passes and peephole optimizations.
"""
from __future__ import annotations
def _known_passes() -> dict[str, type]:
# Recomputed on each call to pick up classes added via ``register_optimization_pass`` after this module is imported.
from .optimization_passes import ALL_OPTIMIZATION_PASSES # pylint:disable=import-outside-toplevel
from .peephole_optimizations import ALL_PEEPHOLE_OPTS # pylint:disable=import-outside-toplevel
return {cls.__qualname__: cls for cls in (*ALL_OPTIMIZATION_PASSES, *ALL_PEEPHOLE_OPTS)}
def pass_to_name(cls: type) -> str:
"""Return a stable string identifier (the class name) for an optimization pass or peephole class."""
return cls.__qualname__
def name_to_pass(name: str) -> type | None:
"""
Resolve a class name back to its registered pass class. Returns None for names that are not registered at the time
this method is called. Peephole passes might be defined by analyses or plugins that have not yet been imported.
"""
return _known_passes().get(name)
__all__ = ("name_to_pass", "pass_to_name")

View file

@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
from archinfo import Arch
import angr.analyses.decompiler as decompiler
from angr.analyses import decompiler
from .base_ptr_save_simplifier import BasePointerSaveSimplifier
from .call_stmt_rewriter import CallStatementRewriter

View file

@ -3,7 +3,7 @@ from __future__ import annotations
import logging
import angr.ailment as ailment
from angr import ailment
from angr.analyses.decompiler.stack_item import StackItem, StackItemType
from .optimization_pass import OptimizationPass, OptimizationPassStage

View file

@ -9,6 +9,7 @@ from angr.ailment import AILBlockRewriter, Block, Expression
from angr.ailment.expression import BinaryOp, Const, VirtualVariable
from angr.ailment.statement import Assignment, ConditionalJump, Statement
from angr.analyses.decompiler.utils import first_nonlabel_nonphi_statement
from angr.analyses.s_reaching_definitions import SReachingDefinitions
from angr.utils.graph import dominates
from angr.utils.timing import timethis
@ -140,7 +141,7 @@ class ConditionConstantPropagation(OptimizationPass):
entry_node_addr, entry_node_idx = self.entry_node_addr
entry_node = self._get_block(entry_node_addr, idx=entry_node_idx)
idoms = networkx.algorithms.immediate_dominators(self._graph, entry_node)
rda: SRDAModel = self.project.analyses.SReachingDefinitions(self._func, func_graph=self._graph).model
rda: SRDAModel = SReachingDefinitions(self.project, self._func, func_graph=self._graph).model
for src, cconds in cconds_by_src.items():
head_block = self._get_block(src[0], idx=src[1])

View file

@ -343,7 +343,7 @@ class AILMergeGraph:
self.original_ends = start_blocks
# moved here
for _, pair in merge_to_end_pair.items():
for pair in merge_to_end_pair.values():
for block in pair:
other_block = pair[0] if pair[1] is block else pair[1]
while True:
@ -390,7 +390,7 @@ class AILMergeGraph:
#
def _find_block_pair_in_originals(self, block: Block):
for _, originals in self.merge_blocks_to_originals.items():
for originals in self.merge_blocks_to_originals.values():
# need at least 2 for a pair
if len(originals) < 2:
continue
@ -454,7 +454,7 @@ class AILMergeGraph:
return None
def _find_split_block_by_original(self, block: Block) -> AILBlockSplit | None:
for _, split_blocks in self.original_split_blocks.items():
for split_blocks in self.original_split_blocks.values():
for split_block in split_blocks:
if split_block.original == block:
return split_block

View file

@ -7,7 +7,7 @@ from itertools import combinations
import networkx as nx
import angr.ailment as ailment
from angr import ailment
from angr.ailment.block import Block
from angr.ailment.expression import Const, Convert, Expression, Register, VirtualVariable
from angr.ailment.statement import Assignment, ConditionalJump, Jump, Label, Return
@ -337,7 +337,7 @@ class DuplicationReverter(StructuringOptimizationPass):
for node in graph.nodes:
nodes_by_addr[node.addr].append(node)
for _, nodes in nodes_by_addr.items():
for nodes in nodes_by_addr.values():
if len(nodes) == 1:
continue
@ -681,9 +681,9 @@ class DuplicationReverter(StructuringOptimizationPass):
# conditions yet. This means the graph is still missing the divergence of the two graphs.
try:
graph_lcs = longest_ail_graph_subseq(blocks, graph)
except SAILRSemanticError as e:
except SAILRSemanticError:
self.candidate_blacklist.add(tuple(blocks))
raise e
raise
ail_merge_graph = AILMergeGraph(original_graph=graph)
# some blocks in originals may update during this time (if-statements can change)

View file

@ -4,7 +4,7 @@ import logging
import networkx as nx
import angr.ailment as ailment
from angr import ailment
from angr.ailment import Const
from angr.ailment.block import Block
from angr.ailment.statement import ConditionalJump, Jump, Statement

View file

@ -3,7 +3,7 @@ from __future__ import annotations
import logging
import angr.ailment as ailment
from angr import ailment
from angr.engines.light import SimEngineLightAIL
_l = logging.getLogger(name=__name__)
@ -377,6 +377,7 @@ class SimplifierAILEngine(
return ailment.expression.UnaryOp(expr.idx, expr.op, operand, **expr.tags)
return expr
_handle_unop_Abs = _handle_unop_Default
_handle_unop_Not = _handle_unop_Default
_handle_unop_Neg = _handle_unop_Default
_handle_unop_BitwiseNeg = _handle_unop_Default
@ -509,6 +510,8 @@ class SimplifierAILEngine(
_handle_binop_MaxV = _handle_binop_Default
_handle_binop_HAddV = _handle_binop_Default
_handle_binop_QAddV = _handle_binop_Default
_handle_binop_QSubV = _handle_binop_Default

View file

@ -1,5 +1,6 @@
from __future__ import annotations
import json
import logging
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
@ -90,7 +91,7 @@ class ExpressionReplacer(AILBlockRewriter):
class OpDescriptor:
"""
Describes a specific operator.
Describes a specific operator. Serializes to JSON.
"""
def __init__(self, block_addr: int, stmt_idx: int, ins_addr: int, op: str):
@ -111,6 +112,29 @@ class OpDescriptor:
and self.op == other.op
)
#
# JSON serialization
#
def to_jsonable(self) -> dict[str, Any]:
return {
"block_addr": self.block_addr,
"stmt_idx": self.stmt_idx,
"ins_addr": self.ins_addr,
"op": self.op,
}
def to_json(self) -> str:
return json.dumps(self.to_jsonable())
@classmethod
def from_jsonable(cls, d: dict[str, Any]) -> OpDescriptor:
return cls(d["block_addr"], d["stmt_idx"], d["ins_addr"], d["op"])
@classmethod
def from_json(cls, s: str) -> OpDescriptor:
return cls.from_jsonable(json.loads(s))
class ExprOpSwapper(SequenceOptimizationPass):
"""

View file

@ -104,7 +104,7 @@ class InlinedMemsetSimplifier(OptimizationPass):
replaced_indices: set[int] = set()
replacements: dict[int, SideEffectStatement] = {}
for _kind, lst in info_by_kind.items():
for lst in info_by_kind.values():
if len(lst) <= 1:
continue
candidates = self._find_memset_candidates(lst)

View file

@ -96,9 +96,11 @@ class InlinedStrcpySimplifier(OptimizationPass):
elif (
isinstance(stmt.src, Insert)
and isinstance(stmt.src.base, (Const, VirtualVariable))
and (not isinstance(stmt.src.base, Const) or stmt.src.base.is_int)
and isinstance(stmt.src.value, Const)
and stmt.src.value.is_int
and isinstance(stmt.src.offset, Const)
and stmt.src.offset.is_int
):
inlined_strcpy_candidate = True
src = stmt.src.value
@ -253,7 +255,7 @@ class InlinedStrcpySimplifier(OptimizationPass):
delta = self._get_delta(addr_last, addr_curr)
if delta is not None and delta == len(s_last):
new_str = s_last + s_curr
elif isinstance(stmt, Store) and isinstance(stmt.data, Const):
elif isinstance(stmt, Store) and isinstance(stmt.data, Const) and stmt.data.is_int:
addr_curr = stmt.addr
delta = self._get_delta(addr_last, addr_curr)
if delta is not None and delta == len(s_last):
@ -322,7 +324,7 @@ class InlinedStrcpySimplifier(OptimizationPass):
and stmt.dst.was_stack
and isinstance(stmt.dst.stack_offset, int)
):
if isinstance(stmt.src, Const):
if isinstance(stmt.src, Const) and stmt.src.is_int:
r[stmt.dst.stack_offset] = idx, ail_const_to_be(stmt.src, self.project.arch.memory_endness)
if (
isinstance(stmt.src, Insert)
@ -334,17 +336,20 @@ class InlinedStrcpySimplifier(OptimizationPass):
and stmt.src.base.stack_offset == stmt.dst.stack_offset
)
)
and (not isinstance(stmt.src.base, Const) or stmt.src.base.is_int)
and isinstance(stmt.src.offset, Const)
and stmt.src.offset.is_int
and isinstance(stmt.src.value, Const)
and stmt.src.value.is_int
):
r[stmt.dst.stack_offset + stmt.src.offset.value] = (
r[stmt.dst.stack_offset + stmt.src.offset.value_int] = (
idx,
ail_const_to_be(stmt.src.value, self.project.arch.memory_endness),
)
else:
r[stmt.dst.stack_offset] = idx, None
elif isinstance(stmt, Store) and isinstance(stmt.addr, StackBaseOffset):
if isinstance(stmt.data, Const):
if isinstance(stmt.data, Const) and stmt.data.is_int:
r[stmt.addr.offset] = idx, ail_const_to_be(stmt.data, self.project.arch.memory_endness)
else:
r[stmt.addr.offset] = idx, None
@ -420,12 +425,12 @@ class InlinedStrcpySimplifier(OptimizationPass):
):
return StackBaseOffset(-1, addr.bits, 0), addr.operand.stack_offset
if isinstance(addr, BinaryOp):
if addr.op == "Add" and isinstance(addr.operands[1], Const):
if addr.op == "Add" and isinstance(addr.operands[1], Const) and addr.operands[1].is_int:
base_0, offset_0 = InlinedStrcpySimplifier._parse_addr(addr.operands[0])
return base_0, offset_0 + addr.operands[1].value
if addr.op == "Sub" and isinstance(addr.operands[1], Const):
return base_0, offset_0 + addr.operands[1].value_int
if addr.op == "Sub" and isinstance(addr.operands[1], Const) and addr.operands[1].is_int:
base_0, offset_0 = InlinedStrcpySimplifier._parse_addr(addr.operands[0])
return base_0, offset_0 - addr.operands[1].value
return base_0, offset_0 - addr.operands[1].value_int
return addr, 0
@staticmethod

View file

@ -9,6 +9,7 @@ import archinfo
import claripy
from archinfo import Endness
from angr.ailment import AILBlockViewer
from angr.ailment.expression import (
BinaryOp,
Const,
@ -21,7 +22,7 @@ from angr.ailment.expression import (
UnaryOp,
VirtualVariable,
)
from angr.ailment.statement import ConditionalJump, Jump, Store
from angr.ailment.statement import Assignment, ConditionalJump, Jump, Store
from angr.code_location import CodeLocation
from angr.engines.light import SimEngineNostmtAIL
from angr.errors import SimMemoryMissingError
@ -347,6 +348,9 @@ class InlinedStringTransformationAILEngine(
return ~v
return None
def _handle_unop_Abs(self, expr: UnaryOp):
self._expr(expr.operand)
def _handle_unop_Default(self, expr: UnaryOp):
return None
@ -502,6 +506,7 @@ class InlinedStringTransformationAILEngine(
_handle_binop_SubV = _handle_binop_Default
_handle_binop_MinV = _handle_binop_Default
_handle_binop_MaxV = _handle_binop_Default
_handle_binop_HAddV = _handle_binop_Default
_handle_binop_QAddV = _handle_binop_Default
_handle_binop_QSubV = _handle_binop_Default
_handle_binop_QNarrowBinV = _handle_binop_Default
@ -509,6 +514,71 @@ class InlinedStringTransformationAILEngine(
_handle_binop_Set = _handle_binop_Default
class _StackReadNotification(Exception):
"""Abort the walk on the first potential stack read."""
class _HasStackReadWalker(AILBlockViewer):
"""
Raises ``_StackReadNotification`` on the first expression that InlinedStringTransformationAILEngine could turn
into a "load" stack-access record: a Load, or a virtual variable that lives on the stack.
"""
def _handle_Load(self, expr_idx, expr, stmt_idx, stmt, block): # pylint:disable=unused-argument
raise _StackReadNotification
def _handle_VirtualVariable(self, expr_idx, expr, stmt_idx, stmt, block): # pylint:disable=unused-argument
if expr.was_stack:
raise _StackReadNotification
_HAS_STACK_READ_WALKER = _HasStackReadWalker()
def _addr_may_be_stack(addr: Expression) -> bool:
"""
Syntactic over-approximation of the addresses ``InlinedStringTransformationAILEngine._process_address`` can
resolve. Anything outside this shape never produces a stack-access record.
"""
if isinstance(addr, (Const, StackBaseOffset)):
return True
if isinstance(addr, UnaryOp) and addr.op == "Reference":
return True
return (
isinstance(addr, BinaryOp)
and addr.op in {"Add", "Sub"}
and isinstance(addr.operands[0], (StackBaseOffset, UnaryOp, Const))
)
def _reads_stack(expr: Expression) -> bool:
try:
_HAS_STACK_READ_WALKER.walk_expression(expr)
except _StackReadNotification:
return True
return False
def _may_transform_stack_bytes(block) -> bool:
"""
A descriptor is only ever built when some statement records a "load" and a "store" stack access at the *same*
code location, i.e. a store to the stack whose value is derived from a stack read. Checking that syntactically is
far cheaper than symbolically executing the loop, and no statement outside this shape can produce that pair.
"""
for stmt in block.statements:
if isinstance(stmt, Store):
if _addr_may_be_stack(stmt.addr) and _reads_stack(stmt.data):
return True
elif (
isinstance(stmt, Assignment)
and isinstance(stmt.dst, VirtualVariable)
and stmt.dst.was_stack
and _reads_stack(stmt.src)
):
return True
return False
class InlineStringTransformationDescriptor:
"""
Describes an instance of inline string transformation.
@ -630,6 +700,10 @@ class InlinedStringTransformationSimplifier(OptimizationPass):
for loop_node in self_loops:
pred = next(iter(nn for nn in self._graph.predecessors(loop_node) if nn is not loop_node))
succ = next(iter(nn for nn in self._graph.successors(loop_node) if nn is not loop_node))
if not _may_transform_stack_bytes(loop_node) and not _may_transform_stack_bytes(pred):
# no statement here can produce the load-then-store-at-the-same-code-location pair a descriptor
# needs; skip the (expensive) symbolic execution entirely
continue
engine = InlinedStringTransformationAILEngine(
self.project, {pred.addr: pred, loop_node.addr: loop_node}, pred.addr, succ.addr, 1024
)

View file

@ -176,8 +176,11 @@ class InlinedWcscpySimplifier(OptimizationPass):
if isinstance(stmt, SideEffectStatement) and self.is_inlined_wcsncpy(stmt):
assert stmt.expr.args is not None and len(stmt.expr.args) >= 3
base, off = self._parse_addr(stmt.expr.args[0])
store_size = stmt.expr.args[2].value * 2 if isinstance(stmt.expr.args[2], Const) else None
if off is not None and store_size is not None:
count = stmt.expr.args[2]
if not isinstance(count, Const) or not count.is_int:
return None
store_size = count.value_int * 2
if off is not None:
candidates.append((i, base, off, store_size, stmt))
elif isinstance(stmt, Store) and isinstance(stmt.data, Const):
base, off = self._parse_addr(stmt.addr)
@ -262,8 +265,10 @@ class InlinedWcscpySimplifier(OptimizationPass):
merged_stmt = merged[0]
new_base, new_off = self._parse_addr(merged_stmt.expr.args[0])
new_sz = (
merged_stmt.expr.args[2].value * 2
if len(merged_stmt.expr.args) >= 3 and isinstance(merged_stmt.expr.args[2], Const)
merged_stmt.expr.args[2].value_int * 2
if len(merged_stmt.expr.args) >= 3
and isinstance(merged_stmt.expr.args[2], Const)
and merged_stmt.expr.args[2].is_int
else sz0 + sz1
)
new_item = idx0, new_base, new_off, new_sz, merged_stmt
@ -442,6 +447,7 @@ class InlinedWcscpySimplifier(OptimizationPass):
and starting_stmt.addr.op == "Add"
and isinstance(starting_stmt.addr.operands[0], VirtualVariable)
and isinstance(starting_stmt.addr.operands[1], Const)
and starting_stmt.addr.operands[1].is_int
):
expected_store_varid = starting_stmt.addr.operands[0].varid
else:
@ -464,7 +470,9 @@ class InlinedWcscpySimplifier(OptimizationPass):
):
offset = stmt.dst.stack_offset
value = (
ail_const_to_be(stmt.src, self.project.arch.memory_endness) if isinstance(stmt.src, Const) else None
ail_const_to_be(stmt.src, self.project.arch.memory_endness)
if isinstance(stmt.src, Const) and stmt.src.is_int
else None
)
elif expected_type == "store" and isinstance(stmt, Store):
if isinstance(stmt.addr, VirtualVariable) and stmt.addr.varid == expected_store_varid:
@ -474,14 +482,15 @@ class InlinedWcscpySimplifier(OptimizationPass):
and stmt.addr.op == "Add"
and isinstance(stmt.addr.operands[0], VirtualVariable)
and isinstance(stmt.addr.operands[1], Const)
and stmt.addr.operands[1].is_int
and stmt.addr.operands[0].varid == expected_store_varid
):
offset = stmt.addr.operands[1].value
offset = stmt.addr.operands[1].value_int
else:
offset = None
value = (
ail_const_to_be(stmt.data, self.project.arch.memory_endness)
if isinstance(stmt.data, Const)
if isinstance(stmt.data, Const) and stmt.data.is_int
else None
)
else:
@ -508,16 +517,19 @@ class InlinedWcscpySimplifier(OptimizationPass):
def even_offsets_are_zero(lst):
if len(lst) >= 2 and lst[-1] == 0 and lst[-2] == 0:
lst = lst[:-2]
return all((ch == 0 if i % 2 == 0 else ch != 0) for i, ch in enumerate(lst))
return all(isinstance(ch, int) and (ch == 0 if i % 2 == 0 else ch != 0) for i, ch in enumerate(lst))
@staticmethod
def odd_offsets_are_zero(lst):
if len(lst) >= 2 and lst[-1] == 0 and lst[-2] == 0:
lst = lst[:-2]
return all((ch == 0 if i % 2 == 1 else ch != 0) for i, ch in enumerate(lst))
return all(isinstance(ch, int) and (ch == 0 if i % 2 == 1 else ch != 0) for i, ch in enumerate(lst))
@staticmethod
def is_integer_likely_a_wide_string(v, size, endness, min_length=4):
if not isinstance(v, int) or not isinstance(size, int):
return False, None
chars = []
if endness == Endness.LE:
while v != 0:
@ -579,12 +591,12 @@ class InlinedWcscpySimplifier(OptimizationPass):
):
return StackBaseOffset(-1, 64, 0), addr.operand.stack_offset
if isinstance(addr, BinaryOp):
if addr.op == "Add" and isinstance(addr.operands[1], Const) and isinstance(addr.operands[1].value, int):
if addr.op == "Add" and isinstance(addr.operands[1], Const) and addr.operands[1].is_int:
base_0, offset_0 = InlinedWcscpySimplifier._parse_addr(addr.operands[0])
return base_0, offset_0 + addr.operands[1].value
if addr.op == "Sub" and isinstance(addr.operands[1], Const) and isinstance(addr.operands[1].value, int):
return base_0, offset_0 + addr.operands[1].value_int
if addr.op == "Sub" and isinstance(addr.operands[1], Const) and addr.operands[1].is_int:
base_0, offset_0 = InlinedWcscpySimplifier._parse_addr(addr.operands[0])
return base_0, offset_0 - addr.operands[1].value
return base_0, offset_0 - addr.operands[1].value_int
return addr, 0
@staticmethod

View file

@ -230,8 +230,9 @@ class LoweredSwitchSimplifier(StructuringOptimizationPass):
graph_copy = networkx.DiGraph(self._graph)
self.out_graph = graph_copy
node_to_heads = defaultdict(set)
modified = False
for _, caselists in variablehash_to_cases.items():
for caselists in variablehash_to_cases.values():
for cases, redundant_nodes in caselists:
real_cases = [case for case in cases if case.value != "default"]
max_continuous_cases = self._count_max_continuous_cases(real_cases)
@ -346,6 +347,7 @@ class LoweredSwitchSimplifier(StructuringOptimizationPass):
new_head.statements[-1] = switch_stmt
# update the block
self._update_block(original_head, new_head)
modified = True
# sanity check that no switch head points to either itself
# or to any if-head that was merged into the new switch head; this
@ -412,6 +414,10 @@ class LoweredSwitchSimplifier(StructuringOptimizationPass):
else:
graph_copy.add_edge(node_copy, succ)
if not modified:
# the graph is not modified
self.out_graph = None
return False
return True
def _find_cascading_switch_variable_comparisons(self):

View file

@ -2,6 +2,7 @@
from __future__ import annotations
import logging
import weakref
from collections import namedtuple
from collections.abc import Generator
from enum import Enum
@ -13,6 +14,7 @@ import angr
from angr import ailment
from angr.ailment.manager import Manager
from angr.analyses.decompiler.ailgraph_walker import AILGraphWalker
from angr.analyses.decompiler.block_simplifier import BlockSimplifier, PeepholeOptimizationBundle
from angr.analyses.decompiler.condition_processor import ConditionProcessor
from angr.analyses.decompiler.counters import ControlFlowStructureCounter
from angr.analyses.decompiler.goto_manager import Goto, GotoManager
@ -20,6 +22,7 @@ from angr.analyses.decompiler.structuring import RecursiveStructurer, SAILRStruc
from angr.analyses.decompiler.utils import add_labels, is_empty_node, remove_edges_in_ailgraph
if TYPE_CHECKING:
from angr.analyses.decompiler.region_identifier import RegionIdentifier, RegionOverlay
from angr.analyses.decompiler.stack_item import StackItem
from angr.knowledge_plugins.functions import Function
from angr.project import Project
@ -125,6 +128,9 @@ class OptimizationPass(BaseOptimizationPass):
_graph: networkx.DiGraph
# self._scratch[_STRUCTURABILITY_CACHE_KEY] = weakref(graph), params for applying the structurability result
_STRUCTURABILITY_CACHE_KEY = "structurability_cache"
def __init__(
self,
func,
@ -133,7 +139,7 @@ class OptimizationPass(BaseOptimizationPass):
graph,
blocks_by_addr=None,
blocks_by_addr_and_idx=None,
variable_kb=None,
kb=None,
region_identifier=None,
reaching_definitions=None,
vvar_id_start: int = 0,
@ -155,7 +161,7 @@ class OptimizationPass(BaseOptimizationPass):
self._blocks_by_addr: dict[int, set[ailment.Block]] = blocks_by_addr or {}
self._blocks_by_addr_and_idx: dict[tuple[int, int | None], ailment.Block] = blocks_by_addr_and_idx or {}
self._graph = graph
self._variable_kb = variable_kb
self._dvars_kb = kb
self._ri = region_identifier
self._rd = reaching_definitions
self._scratch = scratch if scratch is not None else {}
@ -178,6 +184,24 @@ class OptimizationPass(BaseOptimizationPass):
self.out_graph: networkx.DiGraph | None = None
self.stack_items: dict[int, StackItem] = {}
def analyze(self):
super().analyze()
self._invalidate_structurability_cache_if_modified()
def _invalidate_structurability_cache_if_modified(self) -> None:
"""
Drop the shared structurability cache if this pass produced an output graph.
``out_graph`` is the single channel through which a pass reports a graph change, and it covers both flavors:
a freshly built graph object (which would miss the identity-keyed cache anyway) and an in-place mutation of
the graph the pass was handed (which would *wrongly hit* it). Invalidating on any non-None ``out_graph``
keeps the cache sound without having to fingerprint AIL contents -- note that ``Block.__hash__`` is memoized
by the Rust backend and does not track in-place statement edits, so content fingerprints based on it would be
unsound.
"""
if self.out_graph is not None:
self._scratch.pop(self._STRUCTURABILITY_CACHE_KEY, None)
@property
def blocks_by_addr(self) -> dict[int, set[ailment.Block]]:
return self._blocks_by_addr
@ -342,8 +366,9 @@ class OptimizationPass(BaseOptimizationPass):
ail_block,
cache=cache,
)
key = ail_block.addr, ail_block.idx
blocks_by_addr_and_idx[key] = simplified
if simplified is not None:
key = ail_block.addr, ail_block.idx
blocks_by_addr_and_idx[key] = simplified
# update blocks_map to allow node_addr to node lookup
def _replace_node_handler(node):
@ -356,6 +381,21 @@ class OptimizationPass(BaseOptimizationPass):
return ail_graph
def _get_peephole_bundle(self) -> PeepholeOptimizationBundle:
bundle: PeepholeOptimizationBundle | None = self._scratch.get("peephole_bundle")
if bundle is None or not bundle.matches(
self.project, self.manager, self._func.addr, None, None, self._peephole_optimizations
):
bundle = PeepholeOptimizationBundle(
self.project,
self.kb,
self.manager,
func_addr=self._func.addr,
peephole_optimizations=self._peephole_optimizations,
)
self._scratch["peephole_bundle"] = bundle
return bundle
def _simplify_block(self, ail_block, cache=None):
"""
Simplify a single AIL block.
@ -374,13 +414,14 @@ class OptimizationPass(BaseOptimizationPass):
cached_rd = cache_item.rd
cached_prop = cache_item.prop
simp = self.project.analyses.AILBlockSimplifier(
simp = BlockSimplifier(
self.project,
ail_block,
self.manager,
self._func.addr,
peephole_optimizations=self._peephole_optimizations,
cached_reaching_definitions=cached_rd,
cached_propagator=cached_prop,
peephole_bundle=self._get_peephole_bundle(),
)
# update the cache
if cache is not None:
@ -500,6 +541,12 @@ class StructuringOptimizationPass(OptimizationPass):
raise NotImplementedError
def analyze(self):
try:
self._analyze_and_verify()
finally:
self._invalidate_structurability_cache_if_modified()
def _analyze_and_verify(self):
"""
Wrapper for _analyze() that verifies the graph is structurable before and after the optimization.
"""
@ -600,18 +647,55 @@ class StructuringOptimizationPass(OptimizationPass):
if not had_any_changes:
self.out_graph = None
def _graph_is_structurable(self, graph, readd_labels=False, initial=False) -> bool:
def _graph_is_structurable(self, graph, readd_labels: bool = False, initial: bool = False) -> bool:
"""
Checks weather the input graph is structurable under the Phoenix schema-matching structuring algorithm.
As a side effect, this will also update the region identifier and goto manager of this optimization pass.
Consequently, a true return guarantees up-to-date goto information in the goto manager.
Consequently, a True return guarantees up-to-date goto information in the goto manager.
We cache the structurability probe result in self._scratch. An optimization pass invalidates the cached
structurability result if it updates the graph.
"""
# Only the probe of an unmodified input graph is cacheable
cacheable = initial and not readd_labels
if self._edges_to_remove:
# remove_edges_in_ailgraph() below mutates the graph in place, so anything cached for it describes the
# graph as it was before those edges were dropped.
self._scratch.pop(self._STRUCTURABILITY_CACHE_KEY, None)
cacheable = False
if cacheable:
# check cache
entry = self._scratch.get(self._STRUCTURABILITY_CACHE_KEY)
if entry is not None and entry[0]() is graph:
structurable, ri, goto_manager, region = entry[1]
self._apply_structurability_result(structurable, ri, goto_manager, region, initial=initial)
return structurable
# the old route
structurable, ri, goto_manager, region = self._compute_structurability(graph, readd_labels)
if cacheable:
# cache the result
self._scratch[self._STRUCTURABILITY_CACHE_KEY] = (
weakref.ref(graph),
(structurable, ri, goto_manager, region),
)
self._apply_structurability_result(structurable, ri, goto_manager, region, initial=initial)
return structurable
def _compute_structurability(
self, graph, readd_labels: bool
) -> tuple[bool, RegionIdentifier | None, GotoManager | None, RegionOverlay | None]:
"""
Run region identification, structuring, and region simplification on ``graph`` to determine whether it is
structurable or not.
"""
if readd_labels:
graph = add_labels(graph, self.manager)
remove_edges_in_ailgraph(graph, self._edges_to_remove)
self._ri = self.project.analyses[angr.analyses.decompiler.RegionIdentifier].prep(kb=self.kb)(
ri = self.project.analyses[angr.analyses.decompiler.RegionIdentifier].prep(kb=self.kb)(
self._func,
graph=graph,
ail_manager=self.manager,
@ -622,15 +706,15 @@ class StructuringOptimizationPass(OptimizationPass):
expose_loop_head_backedges=True,
entry_node_addr=self.entry_node_addr,
)
if self._ri is None:
return False
if ri is None:
return False, None, None, None
# we should try-catch structuring here because we can often pass completely invalid graphs
# that break the assumptions of the structuring algorithm
try:
rs = self.project.analyses[RecursiveStructurer].prep(kb=self.kb)(
self._ri.region,
cond_proc=self._ri.cond_proc,
ri.region,
cond_proc=ri.cond_proc,
ail_manager=self.manager,
func=self._func,
structurer_cls=SAILRStructurer,
@ -641,17 +725,29 @@ class StructuringOptimizationPass(OptimizationPass):
rs = None
if not rs or not rs.result or is_empty_node(rs.result) or rs.result_incomplete:
return False
return False, ri, None, None
rs = self.project.analyses.RegionSimplifier(
self._func, rs.result, self.manager, arg_vvars=self._arg_vvars, kb=self.kb
)
if not rs or rs.goto_manager is None or rs.result is None:
return False
return False, ri, None, None
self._analyze_simplified_region(rs.result, initial=initial)
self._goto_manager = rs.goto_manager
return True
return True, ri, rs.goto_manager, rs.result
def _apply_structurability_result(
self,
structurable: bool,
ri: RegionIdentifier | None,
goto_manager: GotoManager | None,
region: RegionOverlay | None,
initial: bool = False,
) -> None:
self._ri = ri
if structurable:
assert ri is not None and goto_manager is not None and region is not None
self._analyze_simplified_region(region, initial=initial)
self._goto_manager = goto_manager
# pylint:disable=no-self-use
def _analyze_simplified_region(self, region, initial=False):

View file

@ -1,10 +1,7 @@
from __future__ import annotations
from angr import ailment
from angr.analyses.decompiler.peephole_optimizations import (
EXPR_OPTS,
PeepholeOptimizationExprBase,
)
from angr.analyses.decompiler.block_simplifier import BlockSimplifier, PeepholeOptimizationBundle
from angr.analyses.decompiler.sequence_walker import SequenceWalker
from angr.analyses.decompiler.utils import (
peephole_optimize_expr,
@ -40,11 +37,15 @@ class PostStructuringPeepholeOptimizationPass(SequenceOptimizationPass):
def __init__(self, *args, peephole_optimizations=None, **kwargs):
super().__init__(*args, **kwargs)
self._peephole_optimizations = peephole_optimizations
self._expr_peephole_opts = [
cls(self.project, self.kb, ail_manager=self.manager, func_addr=self._func.addr)
for cls in (self._peephole_optimizations or EXPR_OPTS)
if issubclass(cls, PeepholeOptimizationExprBase)
]
# one bundle for all BlockSimplifier invocations of this pass
self._peephole_bundle = PeepholeOptimizationBundle(
self.project,
self.kb,
self.manager,
func_addr=self._func.addr,
peephole_optimizations=self._peephole_optimizations,
)
self._expr_peephole_opts = self._peephole_bundle.expr_opts
self.analyze()
def _check(self):
@ -65,12 +66,13 @@ class PostStructuringPeepholeOptimizationPass(SequenceOptimizationPass):
old_block, new_block = None, block
while old_block != new_block:
old_block = new_block
# Note: AILBlockSimplifier updates expressions in place
simp = self.project.analyses.AILBlockSimplifier(
# Note: BlockSimplifier updates expressions in place
simp = BlockSimplifier(
self.project,
new_block,
func_addr=self._func.addr,
peephole_optimizations=self._peephole_optimizations,
ail_manager=self.manager,
peephole_bundle=self._peephole_bundle,
)
assert simp.result_block is not None
new_block = simp.result_block

View file

@ -8,7 +8,7 @@ from itertools import chain
import archinfo
import angr.ailment as ailment
from angr import ailment
from angr.analyses.decompiler.stack_item import StackItem, StackItemType
from angr.calling_conventions import SimRegArg
from angr.code_location import CodeLocation

View file

@ -2,15 +2,20 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from angr.ailment.expression import VirtualVariable
from angr.ailment.statement import Assignment
from angr.analyses.decompiler.stack_item import StackItem, StackItemType
from angr.code_location import CodeLocation, ExternalCodeLocation
from angr.analyses.s_reaching_definitions import SReachingDefinitions
from angr.utils.ail import is_phi_assignment
from .optimization_pass import OptimizationPass, OptimizationPassStage
if TYPE_CHECKING:
from angr.analyses.s_reaching_definitions import SRDAModel
from angr.code_location import AILCodeLocation
_l = logging.getLogger(name=__name__)
@ -36,7 +41,8 @@ class RegisterSaveAreaSimplifierAdvanced(OptimizationPass):
self.analyze()
def _check(self):
self._srda = self.project.analyses.SReachingDefinitions(
self._srda = SReachingDefinitions(
self.project,
subject=self._func,
func_graph=self._graph,
func_args={vvar for vvar, _ in arg_vvars.values()} if (arg_vvars := self._arg_vvars) is not None else set(),
@ -60,17 +66,16 @@ class RegisterSaveAreaSimplifierAdvanced(OptimizationPass):
if cache is None:
return
info: list[tuple[int, CodeLocation, int, CodeLocation, int]] = cache["info"]
info: list[tuple[list[AILCodeLocation], int]] = cache["info"]
updated_blocks = {}
for _regvar, regvar_loc, _stackvar, stackvar_loc, _ in info:
# remove storing statements
old_block = self._get_block(regvar_loc.block_addr, idx=regvar_loc.block_idx)
assert regvar_loc.stmt_idx is not None
self._modify_statement(old_block, regvar_loc.stmt_idx, updated_blocks)
old_block = self._get_block(stackvar_loc.block_addr, idx=stackvar_loc.block_idx)
assert stackvar_loc.stmt_idx is not None
self._modify_statement(old_block, stackvar_loc.stmt_idx, updated_blocks)
for locs, _ in info:
# remove all statements involved in this save (the store plus its matching restore, or the store plus the
# dead phi statements it feeds)
for loc in locs:
old_block = self._get_block(loc.block_addr, idx=loc.block_idx)
assert old_block is not None and loc.stmt_idx is not None
self._modify_statement(old_block, loc.stmt_idx, updated_blocks)
for old_block, new_block in updated_blocks.items():
# remove all statements that are None
@ -80,20 +85,23 @@ class RegisterSaveAreaSimplifierAdvanced(OptimizationPass):
if updated_blocks:
# update stack_items
for _, _, _, _, stack_offset in info:
for _, stack_offset in info:
self.stack_items[stack_offset] = StackItem(
stack_offset, self.project.arch.bytes, "regs", StackItemType.SAVED_REGS
)
def _find_reg_store_and_restore_locations(self) -> list[tuple[int, CodeLocation, int, CodeLocation, int]]:
results = []
def _find_reg_store_and_restore_locations(self) -> list[tuple[list[AILCodeLocation], int]]:
results: list[tuple[list[AILCodeLocation], int]] = []
assert self._srda is not None
srda_model = self._srda.model
srda_model: SRDAModel = self._srda.model
# find all registers that are defined externally and used exactly once
saved_vvars: set[tuple[int, CodeLocation]] = set()
saved_vvars: set[tuple[int, AILCodeLocation]] = set()
for vvar_id, loc in srda_model.all_vvar_definitions.items():
if isinstance(loc, ExternalCodeLocation):
# SReachingDefinitions records externally-defined (function live-in) vvars via
# AILCodeLocation.make_extern(). These are AILCodeLocation instances (not ExternalCodeLocation), so the
# extern check must go through AILCodeLocation.is_extern.
if loc.is_extern:
uses = srda_model.all_vvar_uses.get(vvar_id, [])
if len(uses) == 1:
vvar, used_loc = next(iter(uses))
@ -105,8 +113,13 @@ class RegisterSaveAreaSimplifierAdvanced(OptimizationPass):
# for each candidate, we check to ensure:
# - it is stored onto the stack (into a stack virtual variable)
# - the stack virtual variable is only used once and restores the value to the same register
# - the restore location is in the dominance frontier of the store location
# - either
# (a) the stack virtual variable is used exactly once (ignoring phi uses) to restore the value to the same
# register, and the restore location is in the dominance frontier of the store location; or
# (b) the stack virtual variable has no non-phi uses and only feeds phi nodes whose results are dead. This
# happens with shrink-wrapped prologues (e.g. MSVC drivers) where the callee-saved register is
# conditionally spilled and the matching restore has already been removed as a dead assignment; what
# remains is a dead store whose stack vvar merges with an undefined value at a loop/branch join.
for vvar_id, used_loc in saved_vvars:
def_block = self._get_block(used_loc.block_addr, idx=used_loc.block_idx)
assert def_block is not None and used_loc.stmt_idx is not None
@ -122,40 +135,59 @@ class RegisterSaveAreaSimplifierAdvanced(OptimizationPass):
continue
stack_vvar = stmt.dst
all_stack_vvar_uses = srda_model.all_vvar_uses.get(stack_vvar.varid, [])
# eliminate the use location if it's a phi statement
# partition the uses into phi uses and non-phi uses
stack_vvar_uses = set()
phi_use_locs: list[AILCodeLocation] = []
for vvar_, loc_ in all_stack_vvar_uses:
use_block = self._get_block(loc_.block_addr, idx=loc_.block_idx)
if use_block is None or loc_.stmt_idx is None:
continue
use_stmt = use_block.statements[loc_.stmt_idx]
if is_phi_assignment(use_stmt):
phi_use_locs.append(loc_)
continue
stack_vvar_uses.add((vvar_, loc_))
if len(stack_vvar_uses) != 1:
continue
_, stack_vvar_use_loc = next(iter(stack_vvar_uses))
restore_block = self._get_block(stack_vvar_use_loc.block_addr, idx=stack_vvar_use_loc.block_idx)
assert restore_block is not None
restore_stmt = restore_block.statements[stack_vvar_use_loc.stmt_idx]
if not (
isinstance(restore_stmt, Assignment)
and isinstance(restore_stmt.src, VirtualVariable)
and restore_stmt.src.varid == stack_vvar.varid
and isinstance(restore_stmt.dst, VirtualVariable)
and restore_stmt.dst.was_reg
and restore_stmt.dst.reg_offset == stmt.src.reg_offset
):
continue
# this is the dumb version of the dominance frontier check
if self._within_dominance_frontier(def_block, restore_block, True, True):
results.append(
(stmt.src.varid, used_loc, stack_vvar.varid, stack_vvar_use_loc, stack_vvar.stack_offset)
)
if len(stack_vvar_uses) == 1:
# case (a): a genuine store/restore pair
_, stack_vvar_use_loc = next(iter(stack_vvar_uses))
restore_block = self._get_block(stack_vvar_use_loc.block_addr, idx=stack_vvar_use_loc.block_idx)
assert restore_block is not None
restore_stmt = restore_block.statements[stack_vvar_use_loc.stmt_idx]
if not (
isinstance(restore_stmt, Assignment)
and isinstance(restore_stmt.src, VirtualVariable)
and restore_stmt.src.varid == stack_vvar.varid
and isinstance(restore_stmt.dst, VirtualVariable)
and restore_stmt.dst.was_reg
and restore_stmt.dst.reg_offset == stmt.src.reg_offset
):
continue
# this is the dumb version of the dominance frontier check
if self._within_dominance_frontier(def_block, restore_block, True, True):
results.append(([used_loc, stack_vvar_use_loc], stack_vvar.stack_offset))
elif not stack_vvar_uses and phi_use_locs and self._phi_uses_are_dead(srda_model, phi_use_locs):
# case (b): a dead spill whose stack vvar only feeds dead phi nodes
results.append(([used_loc, *phi_use_locs], stack_vvar.stack_offset))
return results
def _phi_uses_are_dead(self, srda_model, phi_use_locs: list[AILCodeLocation]) -> bool:
"""Return True iff every phi statement at ``phi_use_locs`` defines a virtual variable that has no uses. Such a
phi is dead and can be removed together with the store that feeds it."""
for loc in phi_use_locs:
block = self._get_block(loc.block_addr, idx=loc.block_idx)
if block is None or loc.stmt_idx is None:
return False
phi_stmt = block.statements[loc.stmt_idx]
if not (isinstance(phi_stmt, Assignment) and isinstance(phi_stmt.dst, VirtualVariable)):
return False
if srda_model.all_vvar_uses.get(phi_stmt.dst.varid, []):
return False
return True
def _within_dominance_frontier(self, dom_node, node, use_preds: bool, use_succs: bool) -> bool:
if use_succs:
# scan forward

View file

@ -3,7 +3,7 @@ from __future__ import annotations
import logging
from typing import Any
import angr.ailment as ailment
from angr import ailment
from angr.calling_conventions import DEFAULT_CC, SimRegArg, default_cc
from .optimization_pass import OptimizationPass, OptimizationPassStage

View file

@ -5,7 +5,7 @@ from typing import Any
import networkx
import angr.ailment as ailment
from angr import ailment
from angr.ailment import AILBlockRewriter, Block
from angr.ailment.expression import Const, Phi, VirtualVariable
from angr.ailment.statement import Assignment, ConditionalJump, Jump, Label, Return, SideEffectStatement

View file

@ -4,7 +4,7 @@ from __future__ import annotations
import logging
from collections import defaultdict
import angr.ailment as ailment
from angr import ailment
from angr.analyses.decompiler.stack_item import StackItem, StackItemType
from angr.utils.bits import s2u
@ -78,9 +78,7 @@ class StackCanarySimplifier(OptimizationPass):
pred_addr_to_endpoint_addrs[pred.addr].add(node_addr)
found_endpoints = False
for pred_addr in pred_addr_to_endpoint_addrs:
endpoint_addrs = pred_addr_to_endpoint_addrs[pred_addr]
for endpoint_addrs in pred_addr_to_endpoint_addrs.values():
if len(endpoint_addrs) != 2:
# we expect there to be only two nodes: one for canary-check-success, and the other for
# canary-check-failure. if not, we check the next predecessor

View file

@ -3,7 +3,7 @@ from __future__ import annotations
import logging
from typing import Any
import angr.ailment as ailment
from angr import ailment
from .optimization_pass import OptimizationPass, OptimizationPassStage

View file

@ -16,9 +16,18 @@ if TYPE_CHECKING:
class PeepholeOptimizationStmtBase:
"""
The base class for all peephole optimizations that are applied on AIL statements.
``fixpoint_reached`` is an output parameter; it tells the caller whether this optimizer should run on this
statement again (e.g., when statements prior to the current statement are optimized and changed). The caller sets
``fixpoint_reached`` to True before every :meth:`optimize` call; the optimizer optionally sets it to False if it
wants to be invoked again on the same statement.
So, ``fixpoint_reached`` should be set to (or kept) True if (a) the optimizer has optimized the statement and does
not expect to optimize it ever again, or (b) the optimizer cannot ever optimize the statement.
"""
__slots__ = (
"fixpoint_reached",
"func_addr",
"kb",
"manager",
@ -31,6 +40,7 @@ class PeepholeOptimizationStmtBase:
func_addr: int | None
preserve_vvar_ids: set[int]
type_hints: list[tuple[atoms.VirtualVariable | atoms.MemoryLocation, str]]
fixpoint_reached: bool
NAME = "Peephole Optimization - Statement"
DESCRIPTION = "Peephole Optimization - Statement"
@ -51,6 +61,7 @@ class PeepholeOptimizationStmtBase:
self.func_addr = func_addr
self.preserve_vvar_ids = set() if preserve_vvar_ids is None else preserve_vvar_ids
self.type_hints = [] if type_hints is None else type_hints
self.fixpoint_reached = False
def optimize(self, stmt, stmt_idx: int | None = None, block=None, **kwargs):
raise NotImplementedError("_optimize() is not implemented.")
@ -59,9 +70,13 @@ class PeepholeOptimizationStmtBase:
class PeepholeOptimizationMultiStmtBase:
"""
The base class for all peephole optimizations that are applied on multiple AIL statements at once.
``fixpoint_reached`` exists for uniformity but is unused. Multi-statement optimizers always run regardless of
whether the statements have reached fixed points or not.
"""
__slots__ = (
"fixpoint_reached",
"func_addr",
"kb",
"manager",
@ -74,6 +89,7 @@ class PeepholeOptimizationMultiStmtBase:
func_addr: int | None
preserve_vvar_ids: set[int]
type_hints: list[tuple[atoms.VirtualVariable | atoms.MemoryLocation, str]]
fixpoint_reached: bool
NAME = "Peephole Optimization - Multi-statement"
DESCRIPTION = "Peephole Optimization - Multi-statement"
@ -94,6 +110,7 @@ class PeepholeOptimizationMultiStmtBase:
self.func_addr = func_addr
self.preserve_vvar_ids = set() if preserve_vvar_ids is None else preserve_vvar_ids
self.type_hints = [] if type_hints is None else type_hints
self.fixpoint_reached = False
def optimize(self, stmts: list[Statement], stmt_idx: int | None = None, block=None, **kwargs):
raise NotImplementedError("_optimize() is not implemented.")
@ -101,10 +118,12 @@ class PeepholeOptimizationMultiStmtBase:
class PeepholeOptimizationExprBase:
"""
The base class for all peephole optimizations that are applied on AIL expressions.
The base class for all peephole optimizations that are applied on AIL expressions. Please refer to
:class:`PeepholeOptimizationStmtBase` for the ``fixpoint_reached`` contract.
"""
__slots__ = (
"fixpoint_reached",
"func_addr",
"kb",
"manager",
@ -117,6 +136,7 @@ class PeepholeOptimizationExprBase:
func_addr: int | None
preserve_vvar_ids: set[int]
type_hints: list[tuple[atoms.VirtualVariable | atoms.MemoryLocation, str]]
fixpoint_reached: bool
NAME = "Peephole Optimization - Expression"
DESCRIPTION = "Peephole Optimization - Expression"
@ -137,6 +157,7 @@ class PeepholeOptimizationExprBase:
self.func_addr = func_addr
self.preserve_vvar_ids = set() if preserve_vvar_ids is None else preserve_vvar_ids
self.type_hints = [] if type_hints is None else type_hints
self.fixpoint_reached = False
def optimize(self, expr, *, stmt_idx: int | None = None, block=None, **kwargs) -> Expression | None:
raise NotImplementedError("_optimize() is not implemented.")

View file

@ -42,7 +42,7 @@ class SimplifyBitwiseInserts(PeepholeOptimizationExprBase):
# I don't know if 0 is right here
pb2l.append((0, potential_base.operand, other_base))
for (pb1o, pb1), (pb2o, pb2, pb2x) in itertools.product(pb1l, pb2l): # noqa: B007
for (pb1o, pb1), (pb2o, pb2, pb2x) in itertools.product(pb1l, pb2l):
if pb1o == pb2o and pb1.bits == pb2.bits and pb1.likes(pb2):
break
else:

View file

@ -100,7 +100,7 @@ class Bswap(PeepholeOptimizationExprBase):
(">>", 8, 0xFF00),
}:
core_expr = next(iter(cores))
return Call(expr.idx, "__buildin_bswap32", args=[core_expr], bits=expr.bits, **expr.tags)
return Call(expr.idx, "__builtin_bswap32", args=[core_expr], bits=expr.bits, **expr.tags)
return None

View file

@ -2,7 +2,7 @@
from __future__ import annotations
from angr.ailment import Const
from angr.ailment.expression import BinaryOp, Call, Expression, Load, Tmp
from angr.ailment.expression import ITE, BinaryOp, Call, Expression, Load, Tmp
from angr.ailment.statement import CAS, Assignment, ConditionalJump, Statement
from .base import PeepholeOptimizationMultiStmtBase
@ -27,6 +27,16 @@ _INTRINSICS_NAMES = {
}
def cas_intrinsic_name(mnemonic: str, os_name: str | None) -> str:
"""
Resolve the intrinsic name for a lock-prefixed instruction on the given OS, falling back to Linux naming.
"""
if mnemonic not in _INTRINSICS_NAMES:
return mnemonic
names = _INTRINSICS_NAMES[mnemonic]
return names[os_name] if os_name in names else names["Linux"]
class CASIntrinsics(PeepholeOptimizationMultiStmtBase):
"""
Rewrite lock-prefixed instructions (or rather, their VEX/AIL forms) into intrinsic calls.
@ -135,7 +145,9 @@ class CASIntrinsics(PeepholeOptimizationMultiStmtBase):
stmt = Assignment(cas_stmt.idx, assignment_dst, call_expr, **cas_stmt.tags) # type: ignore
return [stmt]
if next_stmt.tags["ins_addr"] <= cas_stmt.tags["ins_addr"]:
if next_stmt.tags["ins_addr"] <= cas_stmt.tags["ins_addr"] and not self._is_cas_writeback_ite(
cas_stmt, next_stmt
):
# avoid matching against statements prematurely
return None
@ -159,17 +171,31 @@ class CASIntrinsics(PeepholeOptimizationMultiStmtBase):
return None
@staticmethod
def _is_cas_writeback_ite(cas_stmt: CAS, stmt: Statement) -> bool:
"""
Detect the ITE assignment that models "cmpxchg writes the memory value back into the accumulator". It belongs
to the same instruction as the CAS, so the same-instruction guard above would otherwise keep this shape from
ever being rewritten. Case 1 cannot apply to it either, because that requires the next statement to be a
CasCmpNE conditional jump.
CAS(addr, expd_lo=X, data_lo=D, old_lo=OLD)
vvar = (X == OLD) ? X : OLD
"""
if cas_stmt.old_lo is None or not isinstance(stmt, Assignment) or not isinstance(stmt.src, ITE):
return False
ite = stmt.src
if not (isinstance(ite.cond, BinaryOp) and ite.cond.op == "CmpEQ"):
return False
expd, old = cas_stmt.expd_lo, cas_stmt.old_lo
cond_op0, cond_op1 = ite.cond.operands
return ((cond_op0.likes(expd) and cond_op1.likes(old)) or (cond_op0.likes(old) and cond_op1.likes(expd))) and (
(ite.iftrue.likes(expd) and ite.iffalse.likes(old)) or (ite.iftrue.likes(old) and ite.iffalse.likes(expd))
)
def _get_instrincs_name(self, mnemonic: str) -> str:
if mnemonic in _INTRINSICS_NAMES:
os = (
self.project.simos.name
if self.project is not None and self.project.simos is not None and self.project.simos.name is not None
else "Linux"
)
if os not in _INTRINSICS_NAMES[mnemonic]:
os = "Linux"
return _INTRINSICS_NAMES[mnemonic][os]
return mnemonic
os_name = self.project.simos.name if self.project is not None and self.project.simos is not None else None
return cas_intrinsic_name(mnemonic, os_name)
@staticmethod
def _resolve_tmp_expr(expr: Expression, block) -> Expression:

View file

@ -14,14 +14,15 @@ class CoalesceSameCascadingIfs(PeepholeOptimizationStmtBase):
def optimize(self, stmt: ConditionalJump, stmt_idx: int | None = None, block=None, **kwargs):
cond = stmt.condition
true_target_in = stmt.true_target
# if (cond) {ITE(cond, true_branch, false_branch)} else {} ==> if (cond) {true_branch} else {}
if isinstance(stmt.true_target, ITE) and cond == stmt.true_target.cond:
new_true_target = stmt.true_target.iftrue
if isinstance(true_target_in, ITE) and cond == true_target_in.cond:
new_true_target = true_target_in.iftrue
else:
new_true_target = stmt.true_target
new_true_target = true_target_in
if cond is not stmt.condition or new_true_target is not stmt.true_target:
if new_true_target != true_target_in:
# it's updated
return ConditionalJump(
stmt.idx, cond, new_true_target, stmt.false_target, false_target_idx=stmt.false_target_idx, **stmt.tags

View file

@ -106,6 +106,8 @@ class ConcatSimplifier(PeepholeOptimizationExprBase):
low,
**expr.tags,
)
# we may start matching once the block changes
self.fixpoint_reached = False
return None

View file

@ -2,6 +2,7 @@ from __future__ import annotations
from math import gcd
from angr.ailment import ExpressionKind
from angr.ailment.expression import BinaryOp, Const, Convert, StackBaseOffset, UnaryOp
from angr.utils.bits import sign_extend
@ -29,6 +30,10 @@ class EagerEvaluation(PeepholeOptimizationExprBase):
@staticmethod
def _optimize_binaryop(expr: BinaryOp):
# The identities below assume integer arithmetic and are not generally valid under IEEE 754.
if expr.floating_point:
return None
op0, op1 = expr.operands
if expr.op == "Add":
if (
@ -212,7 +217,7 @@ class EagerEvaluation(PeepholeOptimizationExprBase):
# constant multiplication
mask = (1 << expr.bits) - 1
return Const(expr.idx, (op0.value * op1.value) & mask, expr.bits, **expr.tags)
if {type(op0), type(op1)} == {BinaryOp, Const}:
if {op0.pykind, op1.pykind} == {ExpressionKind.BinaryOp, ExpressionKind.Const}:
op0, op1 = expr.operands
const_, x0 = (op0, op1) if isinstance(op0, Const) else (op1, op0)
if x0.op == "Mul" and (isinstance(x0.operands[0], Const) or isinstance(x0.operands[1], Const)):
@ -227,17 +232,20 @@ class EagerEvaluation(PeepholeOptimizationExprBase):
elif (
expr.op == "Div"
and isinstance(op1, Const)
and op1.is_int
and isinstance(op0, BinaryOp)
and op0.op == "Mul"
and not op0.floating_point
and isinstance(op0.operands[1], Const)
and op0.operands[1].is_int
):
expr0, const_0 = expr.operands
const_1 = expr0.operands[1]
if const_0.value != 0 and const_1.value != 0:
gcd_ = gcd(const_0.value, const_1.value)
if const_0.value_int != 0 and const_1.value_int != 0:
gcd_ = gcd(const_0.value_int, const_1.value_int)
if gcd_ != 1:
new_const_1 = Const(const_1.idx, const_1.value // gcd_, const_1.bits, **const_1.tags)
new_const_0 = Const(const_0.idx, const_0.value // gcd_, const_0.bits, **const_0.tags)
new_const_1 = Const(const_1.idx, const_1.value_int // gcd_, const_1.bits, **const_1.tags)
new_const_0 = Const(const_0.idx, const_0.value_int // gcd_, const_0.bits, **const_0.tags)
mul = BinaryOp(
expr0.idx,
"Mul",

View file

@ -18,7 +18,6 @@ class ExtendedByteAndMask(PeepholeOptimizationExprBase):
expr_classes = (BinaryOp,) # all expressions are allowed
def optimize(self, expr: BinaryOp, **kwargs):
#
if expr.op == "And" and isinstance(expr.operands[1], Const):
mask = expr.operands[1].value
to_bits = _MASK_TO_BITS.get(mask)

View file

@ -13,26 +13,25 @@ class RemoveEmptyIfBody(PeepholeOptimizationStmtBase):
stmt_classes = (ConditionalJump,)
def optimize(self, stmt: ConditionalJump, stmt_idx: int | None = None, block=None, **kwargs):
cond = stmt.condition
cond_in = stmt.condition
true_target_in = stmt.true_target
false_target_in = stmt.false_target
cond = cond_in
# if (!cond) {} else { ITE(cond, true_branch, false_branch } ==> if (cond) { ITE(...) } else {}
if isinstance(stmt.false_target, ITE) and isinstance(cond, UnaryOp) and cond.op == "Not":
new_true_target = stmt.false_target
if isinstance(false_target_in, ITE) and isinstance(cond, UnaryOp) and cond.op == "Not":
new_true_target = false_target_in
new_true_idx = stmt.false_target_idx
new_false_target = stmt.true_target
new_false_target = true_target_in
new_false_idx = stmt.true_target_idx
cond = cond.operand
else:
new_true_target = stmt.true_target
new_true_target = true_target_in
new_true_idx = stmt.true_target_idx
new_false_target = stmt.false_target
new_false_target = false_target_in
new_false_idx = stmt.false_target_idx
if (
cond is not stmt.condition
or new_true_target is not stmt.true_target
or new_false_target is not stmt.false_target
):
if cond != cond_in or new_true_target != true_target_in or new_false_target != false_target_in:
# it's updated
return ConditionalJump(
stmt.idx,

View file

@ -139,8 +139,17 @@ class RemoveRedundantBitmasks(PeepholeOptimizationExprBase):
new_op0 = op0.operands[0]
replaced, new_operand_expr = operand_expr.replace(op0, new_op0)
if replaced:
expr.operand = new_operand_expr
return expr
return Convert(
self.manager.next_atom(),
expr.from_bits,
expr.to_bits,
expr.is_signed,
new_operand_expr,
from_type=expr.from_type,
to_type=expr.to_type,
rounding_mode=expr.rounding_mode,
**expr.tags,
)
# Conv(64->32, (expr) - (expr) & 0xffffffff<64>)))
# => Conv(64->32, (expr - expr))
elif (

View file

@ -19,12 +19,19 @@ class RemoveRedundantShifts(PeepholeOptimizationExprBase):
def optimize(self, expr: BinaryOp, **kwargs):
# (expr << N) >> N ==> Convert((M-N)->M, Convert(M->(M-N), expr))
#
# For a *logical* right shift (Shr) the outer conversion zero-extends, which the C backend renders as a
# bitmask (e.g. `& 0xfff`) that is correct for any width. For an *arithmetic* right shift (Sar) the outer
# conversion must sign-extend the low (M-N) bits; that is only rendered faithfully by the C backend when
# (M-N) is a standard integer width (8/16/32/64). For non-standard widths we leave the Sar/Shl pair intact
# (which renders as a signed `(expr << N) >> N`) rather than emit a bogus zero-extend mask that silently
# drops the sign bit.
if expr.op in ("Shr", "Sar") and isinstance(expr.operands[1], Const):
expr_a = expr.operands[0]
n0 = expr.operands[1].value
if isinstance(expr_a, BinaryOp) and expr_a.op in {"Shl", "Mul"} and isinstance(expr_a.operands[1], Const):
n1 = get_expr_shift_left_amount(expr_a)
if n0 == n1:
if n0 == n1 and (expr.op == "Shr" or (expr_a.bits - n0) in (8, 16, 32, 64)):
inner_expr = expr_a.operands[0]
conv_inner_expr = Convert(
self.manager.next_atom(),
@ -38,7 +45,7 @@ class RemoveRedundantShifts(PeepholeOptimizationExprBase):
self.manager.next_atom(),
expr_a.bits - n0,
expr.bits,
False,
expr.op == "Sar", # sign-extend for arithmetic shift, zero-extend for logical shift
conv_inner_expr,
**expr.tags,
)

View file

@ -35,6 +35,9 @@ class RolRorRewriter(PeepholeOptimizationStmtBase):
op0, op1 = stmt.src.operands
if isinstance(op0, Tmp) and isinstance(op1, Tmp):
# matches against the two preceding statements, so it may start matching once the block changes
self.fixpoint_reached = False
if stmt_idx < 2:
return None
@ -70,6 +73,7 @@ class RolRorRewriter(PeepholeOptimizationStmtBase):
and shiftleft_amount + stmt2_op1.value == stmt.dst.bits
):
rol_amount = Const(self.manager.next_atom(), shiftleft_amount, 8, **stmt1_op1.tags)
self.fixpoint_reached = True
return Assignment(
stmt.idx,
stmt.dst,
@ -89,6 +93,7 @@ class RolRorRewriter(PeepholeOptimizationStmtBase):
and (shiftleft_amount := get_expr_shift_left_amount(stmt_2.src)) is not None
and stmt1_op1.value + shiftleft_amount == stmt.dst.bits
):
self.fixpoint_reached = True
return Assignment(
stmt.idx,
stmt.dst,

View file

@ -19,10 +19,13 @@ class SarToSignedDiv(PeepholeOptimizationExprBase):
if expr.op == "Sar" and isinstance(expr.operands[1], Const):
op0, const = expr.operands
if isinstance(op0, VirtualVariable) and op0.was_reg and stmt_idx is not None and block is not None:
# look back by one statement to find its definition
op0 = self.find_definition(op0, stmt_idx, block)
# TODO: Ensure the new op0 does not have any expressions that overlap with the old op0 (a register)
if isinstance(op0, VirtualVariable) and op0.was_reg:
# depends on the preceding statement, so it may start matching once the block changes
self.fixpoint_reached = False
if stmt_idx is not None and block is not None:
# look back by one statement to find its definition
op0 = self.find_definition(op0, stmt_idx, block)
# TODO: Ensure the new op0 does not have any expressions that overlap with the old op0 (a register)
const_value = const.value
conv = None
@ -75,6 +78,8 @@ class SarToSignedDiv(PeepholeOptimizationExprBase):
if conv is not None:
# wrap it up with a Convert again
r = Convert(conv.idx, conv.from_bits, conv.to_bits, conv.is_signed, r, **conv.tags)
# rewritten: the result no longer depends on the block context
self.fixpoint_reached = True
return r
return None

View file

@ -1,7 +1,7 @@
# pylint:disable=unused-argument
from __future__ import annotations
import angr.ailment as ailment
from angr import ailment
from .sequence_walker import SequenceWalker
from .structurer_nodes import SequenceNode

View file

@ -19,7 +19,7 @@ from angr.utils.doms import IncrementalDominators
from angr.utils.graph import GraphUtils, dfs_back_edges, dominates, subgraph_between_nodes
from .condition_processor import ConditionProcessor
from .region_overlay import OverlayManager, RegionOverlay
from .region_overlay import OverlayManager, RegionOverlay, Tx
from .structurer_nodes import ConditionNode, IncompleteSwitchCaseHeadStatement, MultiNode
from .utils import copy_graph, first_nonlabel_nonphi_statement, replace_last_statement
@ -29,7 +29,9 @@ l = logging.getLogger(name=__name__)
# an ever-incrementing counter
CONDITIONNODE_ADDR = count(0xFF000000)
type TNode = Block | RegionOverlay | MultiNode | ConditionNode
type TNode = Tx[Block | MultiNode | ConditionNode]
type TOverlay = RegionOverlay[Block | MultiNode | ConditionNode]
type TManager = OverlayManager[Block | MultiNode | ConditionNode]
type TGraph = "networkx.DiGraph[TNode]"
@ -78,8 +80,8 @@ class RegionIdentifier(Analysis):
# copy the graph so updates don't affect the original graph
graph = copy_graph(graph) # type: ignore
self.region: RegionOverlay | None = None
self.overlay_manager: OverlayManager | None = None
self.region: TOverlay | None = None
self.overlay_manager: TManager | None = None
self._start_node = None
self._loop_headers: list | None = None
self.regions_by_block_addrs = []
@ -89,7 +91,7 @@ class RegionIdentifier(Analysis):
self._expose_loop_head_backedges = expose_loop_head_backedges
# we keep a dictionary of node and their traversal order in a quasi-topological traversal and update this
# dictionary as we update the graph
self._node_order: dict[Any, tuple[int, int]] = {}
self._node_order: dict[TNode, tuple[int, int]] = {}
self._graph = self._analyze(graph)
@ -182,11 +184,11 @@ class RegionIdentifier(Analysis):
"""
assert self.region is not None
work_list: list[RegionOverlay] = [self.region]
work_list: list[TOverlay] = [self.region]
block_only_regions = []
seen_regions = set()
while work_list:
children_regions: list[RegionOverlay] = []
children_regions: list[TOverlay] = []
for region in work_list:
children_blocks = []
for node in region.members:
@ -198,7 +200,7 @@ class RegionIdentifier(Analysis):
if node not in seen_regions:
children_regions.append(node)
children_blocks.append(
(node.head.addr, node.head.idx if hasattr(node.head, "idx") else None)
(node.head.addr, node.head.idx if hasattr(node.head, "idx") else None) # type: ignore
)
seen_regions.add(node)
else:
@ -459,11 +461,11 @@ class RegionIdentifier(Analysis):
return refined_loop_nodes, refined_exit_nodes
def _make_regions(self, graph: TGraph) -> RegionOverlay:
def _make_regions(self, graph: TGraph) -> TOverlay:
assert self.overlay_manager is not None
root = self.overlay_manager.root
structured_loop_headers = set()
new_regions: list[RegionOverlay] = []
new_regions: list[TOverlay] = []
# FIXME: _get_start_node() will fail if the graph is just a loop
@ -518,6 +520,7 @@ class RegionIdentifier(Analysis):
# No more loops left. Structure acyclic regions.
while new_regions:
region = new_regions.pop(0)
assert region.head is not None
head = region.head
# collapse a working copy of the region body during acyclic region identification; for the root region,
# the phase-1 working graph already matches its member-level view
@ -618,7 +621,7 @@ class RegionIdentifier(Analysis):
return region
def _refine_loop_successors_to_guarded_successors(self, region: RegionOverlay, graph: TGraph):
def _refine_loop_successors_to_guarded_successors(self, region: TOverlay, graph: TGraph):
"""
If there are multiple successors of a loop, convert them into guarded successors. Eventually there should be
only one loop successor. This is used in the DREAM structuring algorithm.
@ -747,7 +750,7 @@ class RegionIdentifier(Analysis):
self,
head: TNode,
graph: TGraph,
parent_region: RegionOverlay,
parent_region: TOverlay,
failed_region_attempts: set[tuple[TNode, TNode]],
cyclic: bool,
):
@ -873,7 +876,7 @@ class RegionIdentifier(Analysis):
return region_created
@staticmethod
def _update_graph(graph: TGraph, new_region: RegionOverlay, replaced_nodes: set[TNode]) -> None:
def _update_graph(graph: TGraph, new_region: TOverlay, replaced_nodes: set[TNode]) -> None:
region_in_edges = RegionIdentifier._region_in_edges(graph, new_region, data=True)
region_out_edges = RegionIdentifier._region_out_edges(graph, new_region, data=True)
for node in replaced_nodes:
@ -954,7 +957,7 @@ class RegionIdentifier(Analysis):
@staticmethod
def _abstract_acyclic_region(
graph: TGraph,
region: RegionOverlay,
region: TOverlay,
frontier: set[TNode],
node_order: dict[TNode, tuple[int, int]],
dummy_endnode: TNode | None = None,
@ -993,7 +996,7 @@ class RegionIdentifier(Analysis):
normal_exit_node: TNode | None,
abnormal_exit_nodes: set[TNode],
node_order: dict[TNode, tuple[int, int]],
) -> RegionOverlay:
) -> TOverlay:
loop_nodes = set(loop_nodes)
region = self._parent_overlay_of(head).create_subregion(head, loop_nodes, cyclic=True)
@ -1033,7 +1036,7 @@ class RegionIdentifier(Analysis):
return region
def _parent_overlay_of(self, node: TNode) -> RegionOverlay:
def _parent_overlay_of(self, node: TNode) -> TOverlay:
"""Find the overlay that the given working-graph node is currently a direct member of."""
if isinstance(node, RegionOverlay):
assert node.parent is not None
@ -1046,19 +1049,19 @@ class RegionIdentifier(Analysis):
@overload
@staticmethod
def _region_in_edges(
graph: TGraph, region: RegionOverlay, data: Literal[True]
graph: TGraph, region: TOverlay, data: Literal[True]
) -> list[tuple[TNode, TNode, dict[str, Any]]]: ...
@overload
@staticmethod
def _region_in_edges(graph: TGraph, region: RegionOverlay, data: Literal[False]) -> list[tuple[TNode, TNode]]: ...
def _region_in_edges(graph: TGraph, region: TOverlay, data: Literal[False]) -> list[tuple[TNode, TNode]]: ...
@staticmethod
def _region_in_edges(graph, region, data=False):
return list(graph.in_edges(region.head, data=data))
@staticmethod
def _region_out_edges(graph, region: RegionOverlay, data=False):
def _region_out_edges(graph, region: TOverlay, data=False):
out_edges = []
for node in region.members:
out_ = graph.out_edges(node, data=data)
@ -1130,6 +1133,7 @@ class RegionIdentifier(Analysis):
def _ensure_jump_at_loop_exit_ends(self, node: TNode) -> None:
if isinstance(node, Block):
assert node.original_size is not None
if not node.statements:
node.statements.append(
Jump(

View file

@ -1,18 +1,30 @@
# pylint:disable=protected-access
# pylint:disable=protected-access,invalid-sequence-index,unsubscriptable-object
from __future__ import annotations
import logging
import os
from collections import defaultdict
from collections.abc import Callable, Iterable, Iterator, Mapping
from typing import Any
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
from typing import TYPE_CHECKING, Any, Protocol, cast
import networkx
l = logging.getLogger(name=__name__)
class OverlayManager:
class RegionBound(Protocol):
"""
The protocol indicating the minimum capabilities of graph nodes to be managed by an overlay region.
"""
@property
def addr(self) -> int: ...
type Tx[U: RegionBound] = "U | RegionOverlay[U]"
class OverlayManager[T: RegionBound]:
"""
OverlayManager owns the single shared control-flow graph that all RegionOverlay objects are views of, plus the
node-to-innermost-overlay ownership map.
@ -32,8 +44,8 @@ class OverlayManager:
"root",
)
def __init__(self, graph: networkx.DiGraph, expose_loop_head_backedges: bool = False):
self.graph = graph
def __init__(self, graph: networkx.DiGraph[T], expose_loop_head_backedges: bool = False):
self.graph = cast("networkx.DiGraph[Tx[T]]", graph)
self.expose_loop_head_backedges = expose_loop_head_backedges
self._version: int = 0
# per-node topology version: bumped when an edge incident to a node changes (in the shared graph or in the
@ -42,26 +54,27 @@ class OverlayManager:
# flip node ownership/representatives broadly (create_subregion/dissolve/finalize) and by rollback (whose
# inverse closures mutate the graph outside the touch-instrumented primitives); a change forces a full
# cache clear.
self._node_version: dict[Any, int] = {}
self._node_version: dict[Tx[T], int] = {}
self._adj_epoch: int = 0
self._undo_log: list[Callable[[], None]] | None = None
# NOTE: this is the only callsite w/ head=None. If we can add a head param to this constructor we can clean up
self.root = RegionOverlay(self, None, cyclic=False)
self.root._under = set(graph)
self.root._members = set(graph)
self._owner: dict[Any, RegionOverlay] = dict.fromkeys(graph, self.root)
self._owner: dict[Tx[T], RegionOverlay[T]] = dict.fromkeys(graph, self.root)
@property
def version(self) -> int:
return self._version
def owner_of(self, node) -> RegionOverlay | None:
def owner_of(self, node: Tx[T]) -> RegionOverlay[T] | None:
return self._owner.get(node)
def _bump(self) -> None:
self._version += 1
def _touch(self, node) -> None:
def _touch(self, node: Tx[T]) -> None:
"""Bump a node's topology version (its cached view-adjacency must be rebuilt). Also bump every enclosing
overlay: in an enclosing region's view the node is represented by the child overlay that contains it, so a
change to the node's edges changes that representative's adjacency too. Monotonic; not undone on rollback,
@ -188,7 +201,7 @@ class OverlayManager:
self._record(inverse)
class RegionOverlay:
class RegionOverlay[T: RegionBound]:
"""
A single-entry region marked over the shared graph held by an OverlayManager. The region tree is built out of
RegionOverlay objects (RegionIdentifier emits them) and they are the only region type the decompiler uses.
@ -231,8 +244,8 @@ class RegionOverlay:
def __init__(
self,
mgr: OverlayManager,
head,
mgr: OverlayManager[T],
head: Tx[T] | None,
cyclic: bool,
cyclic_ancestor: bool = False,
parent: RegionOverlay | None = None,
@ -243,16 +256,16 @@ class RegionOverlay:
self.cyclic_ancestor = cyclic_ancestor
self.parent = parent
self.children: list[RegionOverlay] = []
self._members: set = set()
self._under: set = set()
self._members: set[Tx[T]] = set()
self._under: set[T] = set()
# edges (pairs of shared-graph nodes) hidden from this overlay's views only
self._hidden: set[tuple[Any, Any]] = set()
self._hidden: set[tuple[Tx[T], Tx[T]]] = set()
# view-level edge pairs hidden from the with-successors view only
self._hidden_full: set[tuple[Any, Any]] = set()
self._hidden_full: set[tuple[Tx[T], Tx[T]]] = set()
# view-level edge pairs injected into the with-successors view only (successor absorption)
self._extra_full_edges: set[tuple[Any, Any]] = set()
self._extra_full_edges: set[tuple[Tx[T], Tx[T]]] = set()
# scratch edge marks (e.g., Phoenix's cyclic_refinement_outgoing), scoped to this overlay
self.edge_marks: defaultdict[str, set[tuple[Any, Any]]] = defaultdict(set)
self.edge_marks: defaultdict[str, set[tuple[Tx[T], Tx[T]]]] = defaultdict(set)
# the node this overlay was finalized into, if any
self.replacement = None
@ -267,6 +280,8 @@ class RegionOverlay:
@property
def addr(self):
if self.head is None:
raise TypeError("Region with no head has no address")
return self.head.addr
#
@ -274,11 +289,11 @@ class RegionOverlay:
#
@property
def manager(self) -> OverlayManager:
def manager(self) -> OverlayManager[T]:
return self._mgr
@property
def members(self) -> set:
def members(self) -> set[Tx[T]]:
return self._members
def ancestors(self) -> set[RegionOverlay]:
@ -290,15 +305,17 @@ class RegionOverlay:
node = node.parent
return result
def underlying_nodes(self) -> set:
def underlying_nodes(self) -> set[T]:
"""All shared-graph nodes inside this region (including nodes of nested regions)."""
return self._under
@staticmethod
def _underlying(x) -> set:
def _underlying(x) -> set[T]:
return x._under if isinstance(x, RegionOverlay) else {x}
def create_subregion(self, head, members: Iterable, cyclic: bool, cyclic_ancestor: bool = False) -> RegionOverlay:
def create_subregion(
self, head: Tx[T], members: Iterable, cyclic: bool, cyclic_ancestor: bool = False
) -> RegionOverlay[T]:
"""
Carve a new child overlay out of this overlay. ``members`` must be a subset of this overlay's members
(shared-graph nodes owned by this overlay and/or existing child overlays); ``head`` must be one of them.
@ -376,10 +393,10 @@ class RegionOverlay:
# Derived views
#
def _is_hidden(self, src, dst) -> bool:
def _is_hidden(self, src: Tx[T], dst: Tx[T]) -> bool:
return (src, dst) in self._hidden
def _hidden_context_head_under(self) -> frozenset | set:
def _hidden_context_head_under(self) -> set[Tx[T]]:
"""
Crossing edges that target the head of the region's processing context (the nearest cyclic ancestor, or
the root region) were invisible during region identification: in-edges of the head are stripped before
@ -397,7 +414,7 @@ class RegionOverlay:
return frozenset()
return self._underlying(anc.head)
def _crossing_out_edges(self) -> Iterator[tuple[Any, Any, dict]]:
def _crossing_out_edges(self) -> Iterator[tuple[Tx[T], Tx[T], dict[str, Any]]]:
"""All shared-graph edges leaving this region, except hidden ones."""
graph = self._mgr.graph
under = self._under
@ -413,7 +430,7 @@ class RegionOverlay:
def _in_loop(self) -> bool:
return self.cyclic or self.cyclic_ancestor
def successor_nodes(self) -> set:
def successor_nodes(self) -> set[Tx[T]]:
"""
The derived successor set of this region: representatives of all shared-graph nodes targeted by edges
leaving the region.
@ -429,14 +446,14 @@ class RegionOverlay:
self._cache_succs = (self._mgr.version, succs)
return succs
def _quotient_edges(self, with_successors: bool) -> Iterator[tuple[Any, Any, dict]]:
def _quotient_edges(self, with_successors: bool) -> Iterator[tuple[Tx[T], Tx[T], dict[str, Any]]]:
"""
Derive the edges of the region view (member -> member, and if requested member -> successor and
successor -> successor) from the shared graph.
"""
graph = self._mgr.graph
under = self._under
member_of: dict[Any, Any] = {}
member_of: dict[Tx[T], Tx[T]] = {}
for m in self._members:
for n in self._underlying(m):
member_of[n] = m
@ -483,7 +500,7 @@ class RegionOverlay:
def view_graph(
self, full: bool = False, include_marked: bool = False, blacklisted_edges: frozenset = frozenset()
) -> RegionOverlayGraph:
) -> RegionOverlayGraph[T]:
"""A zero-copy, networkx-compatible view of this region (see RegionOverlayGraph)."""
if blacklisted_edges:
return RegionOverlayGraph(
@ -501,7 +518,7 @@ class RegionOverlay:
# node so that RegionOverlayGraph can answer adjacency queries without materializing anything.
#
def _iter_view_out_edges(self, n, full: bool) -> Iterator[tuple[Any, dict]]:
def _iter_view_out_edges(self, n, full: bool) -> Iterator[tuple[Tx[T], dict[str, Any]]]:
"""Visible out-edges of view node ``n`` (a member, or a successor in the full view), deduplicated."""
graph = self._mgr.graph
under = self._under
@ -522,7 +539,7 @@ class RegionOverlay:
if rep_v not in seen:
seen.add(rep_v)
yield rep_v, data
elif full and v not in hidden_head:
elif hidden_head is not None and v not in hidden_head:
rep_v = self._representative_outside(v)
if rep_v is not None and rep_v not in seen:
seen.add(rep_v)
@ -560,7 +577,7 @@ class RegionOverlay:
seen.add(v)
yield v, {}
def _iter_view_in_edges(self, n, full: bool) -> Iterator[tuple[Any, dict]]:
def _iter_view_in_edges(self, n: Tx[T], full: bool) -> Iterator[tuple[Tx[T], dict[str, Any]]]:
"""Visible in-edges of view node ``n``, deduplicated. The transpose of _iter_view_out_edges."""
graph = self._mgr.graph
under = self._under
@ -621,21 +638,21 @@ class RegionOverlay:
seen.add(u)
yield u, {}
def view(self) -> RegionOverlayGraph:
def view(self) -> RegionOverlayGraph[T]:
"""The region graph (members only): a zero-copy networkx-compatible view; treat it as read-only."""
return self.view_graph(full=False)
def view_with_successors(self) -> RegionOverlayGraph:
def view_with_successors(self) -> RegionOverlayGraph[T]:
"""The region graph including successor nodes: a zero-copy view; treat it as read-only."""
return self.view_graph(full=True)
@property
def raw_graph(self) -> RegionOverlayGraph:
def raw_graph(self) -> RegionOverlayGraph[T]:
"""The member view including marked edges (the old graph with cyclic_refinement_outgoing attrs present)."""
return self.view_graph(full=False, include_marked=True)
@property
def raw_graph_with_successors(self) -> RegionOverlayGraph:
def raw_graph_with_successors(self) -> RegionOverlayGraph[T]:
"""The with-successors view including marked edges."""
return self.view_graph(full=True, include_marked=True)
@ -644,15 +661,15 @@ class RegionOverlay:
#
@property
def graph(self) -> networkx.DiGraph:
def graph(self) -> networkx.DiGraph[Tx[T]]:
return self.view()
@property
def graph_with_successors(self) -> networkx.DiGraph:
def graph_with_successors(self) -> networkx.DiGraph[Tx[T]]:
return self.view_with_successors()
@property
def successors(self) -> set:
def successors(self) -> set[Tx[T]]:
return self.successor_nodes()
@property
@ -710,7 +727,7 @@ class RegionOverlay:
self._on_node_added(node)
self._invalidate()
def remove_node(self, node, absorbed_into=None, absorb_out_edges: bool = True) -> None:
def remove_node(self, node: Tx[T], absorbed_into: Tx[T] | None = None, absorb_out_edges: bool = True) -> None:
"""
Remove a node. If ``node`` is a member (or a member overlay's node), it is removed from the shared graph
for real. If it is a successor of this region, the removal is interpreted as hiding all edges from this
@ -728,7 +745,9 @@ class RegionOverlay:
self.hide_edge_to_successor(node)
return
external_in_edges = []
rewire_out_edges = [] # (dst, data, hide): edges to rewire onto absorbed_into in the shared graph
rewire_out_edges: list[
tuple[Tx[T], dict[str, Any], bool]
] = [] # (dst, data, hide): edges to rewire onto absorbed_into in the shared graph
if absorbed_into is not None:
external_in_edges = [
(src, data)
@ -753,13 +772,14 @@ class RegionOverlay:
for src, data in external_in_edges:
self._mgr.graph_add_edge(src, absorbed_into, **data)
hidden_added = []
for dst, data, hide in rewire_out_edges:
self._mgr.graph_add_edge(absorbed_into, dst, **data)
if hide and (absorbed_into, dst) not in self._hidden:
self._hidden.add((absorbed_into, dst))
hidden_added.append((absorbed_into, dst))
if hidden_added:
self._mgr._record(lambda: self._hidden.difference_update(hidden_added))
if absorbed_into is not None:
for dst, data, hide in rewire_out_edges:
self._mgr.graph_add_edge(absorbed_into, dst, **data)
if hide and (absorbed_into, dst) not in self._hidden:
self._hidden.add((absorbed_into, dst))
hidden_added.append((absorbed_into, dst))
if hidden_added:
self._mgr._record(lambda: self._hidden.difference_update(hidden_added))
self._invalidate()
def hide_edge_to_successor(self, succ) -> None:
@ -782,7 +802,7 @@ class RegionOverlay:
self._mgr._touch(rep)
self._invalidate()
def underlying_edge_pairs(self, src, dst) -> list[tuple[Any, Any]]:
def underlying_edge_pairs(self, src: Tx[T], dst: Tx[T]) -> list[tuple[Tx[T], Tx[T]]]:
graph = self._mgr.graph
under_src = self._underlying(src)
under_dst = self._underlying(dst)
@ -795,7 +815,7 @@ class RegionOverlay:
pairs.append((u, v))
return pairs
def add_edge(self, src, dst, **data) -> None:
def add_edge(self, src: Tx[T], dst: Tx[T], **data) -> None:
"""
Add a real edge to the shared graph. Overlay endpoints are resolved to underlying nodes: the destination
resolves to its entry (head chain); overlay sources are not supported. Endpoints that are not in the
@ -805,6 +825,7 @@ class RegionOverlay:
dst_ = dst
while isinstance(dst_, RegionOverlay):
dst_ = dst_.head
assert dst_ is not None
if src not in self._mgr.graph:
self.add_node(src)
if dst_ not in self._mgr.graph:
@ -815,7 +836,7 @@ class RegionOverlay:
self._mgr._record(lambda: self._hidden.add((src, dst_)))
self._invalidate()
def detach_edge(self, src, dst) -> None:
def detach_edge(self, src: Tx[T], dst: Tx[T]) -> None:
"""
Remove an edge from the shared graph for real (e.g., when the edge has been virtualized into a goto).
Overlay endpoints remove all underlying edges between the two node sets.
@ -833,7 +854,7 @@ class RegionOverlay:
self._mgr._touch(dst)
self._invalidate()
def mark_edge(self, src, dst, **attrs) -> None:
def mark_edge(self, src: Tx[T], dst: Tx[T], **attrs) -> None:
"""
Mark a view-level edge (e.g. cyclic_refinement_outgoing) so RegionOverlayGraph hides it by default.
Marks live in overlay state, never reach the shared graph, and are remapped/cleared with the region.
@ -847,13 +868,13 @@ class RegionOverlay:
self._mgr._touch(dst)
self._invalidate()
def absorb_successor_into(self, succ, new_node) -> None:
def absorb_successor_into(self, succ: Tx[T], new_node: Tx[T]) -> None:
"""
Absorb a successor node into a structured member node in this region's with-successors view only (the
successor still belongs to an enclosing region): the successor's view out-edges are re-attached to the
member node as view-only extra edges, then the successor disappears from this region's views.
"""
added = []
added: list[tuple[Tx[T], Tx[T]]] = []
for dst, _ in self.view_with_successors().overlay._iter_view_out_edges(succ, full=True):
if dst is not new_node and (new_node, dst) not in self._extra_full_edges:
self._extra_full_edges.add((new_node, dst))
@ -865,18 +886,18 @@ class RegionOverlay:
self._mgr._touch(dst)
self.hide_edge_to_successor(succ)
def drop_edge_marks_from(self, node, key) -> None:
def drop_edge_marks_from(self, node: Tx[T], key: str) -> None:
"""Clear marks on all out-edges of a node (the new_node after a replace), undoably."""
removed = [(u, v) for (u, v) in self.edge_marks[key] if u is node]
if removed:
self.edge_marks[key].difference_update(removed)
self._mgr._record(lambda: self.edge_marks.update(removed))
self._mgr._record(lambda: self.edge_marks[key].update(removed))
self._mgr._touch(node)
for _, v in removed:
self._mgr._touch(v)
self._invalidate()
def remove_edge_with_successors_only(self, src, dst) -> None:
def remove_edge_with_successors_only(self, src: Tx[T], dst: Tx[T]) -> None:
"""
Hide an edge from the with-successors view only, leaving the member view and the shared graph alone (a
rare asymmetric bookkeeping pattern in Phoenix's switch-case structuring).
@ -888,11 +909,11 @@ class RegionOverlay:
self._mgr._touch(dst)
self._invalidate()
def hide_edge(self, src, dst) -> None:
def hide_edge(self, src: Tx[T], dst: Tx[T]) -> None:
"""
Remove an edge from this overlay's views only. Enclosing regions still see the underlying edge(s).
"""
added = []
added: list[tuple[Tx[T], Tx[T]]] = []
for u, v in self.underlying_edge_pairs(src, dst):
if (u, v) not in self._hidden:
self._hidden.add((u, v))
@ -903,7 +924,9 @@ class RegionOverlay:
self._mgr._touch(dst)
self._invalidate()
def replace_nodes(self, old_node_0, new_node, old_node_1=None, self_loop: bool = True) -> None:
def replace_nodes(
self, old_node_0: Tx[T], new_node: Tx[T], old_node_1: Tx[T] | None = None, self_loop: bool = True
) -> None:
"""
Replace one or two member nodes with a new node, preserving and rewiring all underlying edges (including
edges from/to nodes outside this region, which is how results become visible to enclosing regions).
@ -961,12 +984,12 @@ class RegionOverlay:
self._mgr._record(lambda: setattr(self, "head", old_head))
self._invalidate()
def _remap_bookkeeping(self, old_nodes: set, new_node) -> None:
def _remap_bookkeeping(self, old_nodes: set[Tx[T]], new_node: Tx[T]) -> None:
"""Remap hidden edges and edge marks that reference replaced nodes, here and in all enclosing overlays."""
anc: RegionOverlay | None = self
anc = self
while anc is not None:
for attr in ("_hidden", "_hidden_full", "_extra_full_edges"):
pairs: set[tuple[Any, Any]] = getattr(anc, attr)
pairs: set[tuple[Tx[T], Tx[T]]] = getattr(anc, attr)
stale = [(u, v) for u, v in pairs if u in old_nodes or v in old_nodes]
if stale:
remapped = [
@ -1002,7 +1025,7 @@ class RegionOverlay:
# Region lifecycle
#
def snapshot_successors(self) -> set:
def snapshot_successors(self) -> set[Tx[T]]:
"""
Capture this region's structural successors and how many member edges reach each, taken before the region
is structured. finalize() uses it to re-establish the region-to-successor edges that structuring removes
@ -1011,12 +1034,12 @@ class RegionOverlay:
return set(self.successor_nodes())
@staticmethod
def _resolve_entry(node):
def _resolve_entry(node) -> T | None:
while isinstance(node, RegionOverlay):
node = node.head
return node
def finalize(self, result_node=None, succ_snapshot=None):
def finalize(self, result_node: Tx[T] | None = None, succ_snapshot: set[Tx[T]] | None = None):
"""
Collapse this fully-structured region into its parent: the region must consist of a single member node
(the structuring result), which takes the region's place among the parent's members. Returns that node.
@ -1053,6 +1076,7 @@ class RegionOverlay:
s_entry = self._resolve_entry(s)
if (
s_entry is not result_node
and s_entry is not None
and s_entry is not parent_loop_head
and s_entry in graph
and not graph.has_edge(result_node, s_entry)
@ -1097,7 +1121,8 @@ class RegionOverlay:
self._invalidate()
return result_node
def collapse_to(self, result_node):
# NOTE: this function seems unused
def collapse_to(self, result_node: RegionOverlay[T]):
"""
Collapse this region into its parent by replacing all of its member nodes with a single external result
node (the structuring result). Used by structurers that compute their result without destructively
@ -1116,8 +1141,8 @@ class RegionOverlay:
underset = self._under
# capture crossing edges (one endpoint inside the region, the other outside) before removing the members
in_edges: list[tuple[Any, dict]] = []
out_edges: list[tuple[Any, dict]] = []
in_edges: list[tuple[Tx[T], dict[str, Any]]] = []
out_edges: list[tuple[Tx[T], dict[str, Any]]] = []
seen_in: set = set()
seen_out: set = set()
for u in under:
@ -1231,12 +1256,12 @@ class RegionOverlay:
_PARANOID_ADJ_CHECK = bool(os.environ.get("ANGR_PARANOID_ADJ"))
class _OverlayNodeAtlas(Mapping):
class _OverlayNodeAtlas[T: RegionBound](Mapping[Tx[T], dict[str, Any]]):
"""Lazy node mapping of a RegionOverlayGraph: the overlay's view nodes, attributes from the shared graph."""
__slots__ = ("_rog",)
def __init__(self, rog: RegionOverlayGraph):
def __init__(self, rog: RegionOverlayGraph[T]):
self._rog = rog
def __len__(self):
@ -1258,12 +1283,12 @@ class _OverlayNodeAtlas(Mapping):
return shared.nodes[n] if n in shared else {}
class _OverlayAdjInner(Mapping):
class _OverlayAdjInner[T: RegionBound](Mapping[Tx[T], dict[str, Any]]):
"""Adjacency of one view node: target -> edge data, derived on construction from the overlay."""
__slots__ = ("_d",)
def __init__(self, rog: RegionOverlayGraph, n, pred: bool):
def __init__(self, rog: RegionOverlayGraph[T], n, pred: bool):
overlay = rog.overlay
it = overlay._iter_view_in_edges(n, rog.full) if pred else overlay._iter_view_out_edges(n, rog.full)
if pred:
@ -1284,26 +1309,26 @@ class _OverlayAdjInner(Mapping):
return self._d[n]
class _OverlayAdjAtlas(Mapping):
class _OverlayAdjAtlas[T: RegionBound](Mapping[Tx[T], _OverlayAdjInner[T]]):
"""Outer adjacency mapping of a RegionOverlayGraph: view node -> _OverlayAdjInner."""
__slots__ = ("_cache", "_epoch", "_pred", "_rog", "_succ_cache", "_succ_version")
def __init__(self, rog: RegionOverlayGraph, pred: bool):
def __init__(self, rog: RegionOverlayGraph[T], pred: bool):
self._rog = rog
self._pred = pred
# Phoenix queries the same node's adjacency repeatedly (.successors/.predecessors/.in_degree/.out_degree/
# .has_edge all route through here); cache the derived _OverlayAdjInner per node, keyed by that node's
# topology version so an unrelated mutation does not evict it. _epoch tracks the manager's coarse epoch
# (bumped by lifecycle ops / rollback) and forces a full clear when it changes.
self._cache: dict[Any, tuple[int, _OverlayAdjInner]] = {}
self._cache: dict[Tx[T], tuple[int, _OverlayAdjInner]] = {}
self._epoch: int | None = None
# successor-node adjacency cache: a successor's view adjacency depends on the whole region's successor
# set and view state, not just on that node, so it cannot be keyed by the node's own version. Key the
# whole cache by the manager's global version instead (bumped by every mutation and view-state change,
# the same invariant _node_set relies on): reads between two mutations -- dominator fixpoints, SAILR's
# per-edge in_degree queries -- hit the cache, and any mutation drops it wholesale.
self._succ_cache: dict[Any, _OverlayAdjInner] = {}
self._succ_cache: dict[Tx[T], _OverlayAdjInner] = {}
self._succ_version: int | None = None
def __len__(self):
@ -1363,7 +1388,7 @@ class _OverlayAdjAtlas(Mapping):
return entry[1]
class RegionOverlayGraph[T](networkx.DiGraph):
class RegionOverlayGraph[T: RegionBound](networkx.DiGraph[Tx[T]] if TYPE_CHECKING else networkx.DiGraph):
"""
A read-only, networkx-compatible view of a RegionOverlay that stores no copy of the region's subgraph: all
queries traverse the original shared graph through the overlay's membership. Compatible with every networkx
@ -1378,10 +1403,10 @@ class RegionOverlayGraph[T](networkx.DiGraph):
def __init__(
self,
overlay: RegionOverlay,
overlay: RegionOverlay[T],
full: bool = False,
include_marked: bool = False,
blacklisted_edges: frozenset[tuple[Any, Any]] = frozenset(),
blacklisted_edges: frozenset[tuple[Tx[T], Tx[T]]] = frozenset(),
):
super().__init__()
self.overlay = overlay
@ -1399,7 +1424,7 @@ class RegionOverlayGraph[T](networkx.DiGraph):
# internals
#
def _node_set(self) -> frozenset:
def _node_set(self) -> frozenset[Tx[T]]:
version = self.overlay.manager.version
cached = self._ns_cache
if cached is not None and cached[0] == version:
@ -1414,14 +1439,14 @@ class RegionOverlayGraph[T](networkx.DiGraph):
self._ns_cache = (version, ns)
return ns
def _pair_visible(self, src, dst) -> bool:
def _pair_visible(self, src: Tx[T], dst: Tx[T]) -> bool:
if not self.include_marked and any((src, dst) in marks for marks in self.overlay.edge_marks.values()):
return False
if (src, dst) in self.blacklisted_edges:
return False
return not (self.full and (src, dst) in self.overlay._hidden_full)
def _variant(self, fullgraph, all_edges) -> RegionOverlayGraph:
def _variant(self, fullgraph: bool | None, all_edges: bool | None = None) -> RegionOverlayGraph[T]:
full = self.full if fullgraph is None else fullgraph
include_marked = self.include_marked if all_edges is None else all_edges
if full == self.full and include_marked == self.include_marked:
@ -1435,24 +1460,24 @@ class RegionOverlayGraph[T](networkx.DiGraph):
#
@property
def full_view(self) -> RegionOverlayGraph:
def full_view(self) -> RegionOverlayGraph[T]:
"""The with-successors sibling of this view (zero-copy)."""
return self._variant(True, None)
@property
def member_view(self) -> RegionOverlayGraph:
def member_view(self) -> RegionOverlayGraph[T]:
"""The members-only sibling of this view (zero-copy)."""
return self._variant(False, None)
def with_all_edges(self) -> RegionOverlayGraph:
def with_all_edges(self) -> RegionOverlayGraph[T]:
"""A sibling view that includes edges marked through RegionOverlay.mark_edge."""
return self._variant(None, True)
def filtered(self) -> RegionOverlayGraph:
def filtered(self) -> RegionOverlayGraph[T]:
"""A sibling view that hides edges marked through RegionOverlay.mark_edge (the default)."""
return self._variant(None, False)
def to_acyclic_by_order(self, node_order) -> RegionOverlayGraph:
def to_acyclic_by_order(self, node_order: Mapping[Tx[T], int]) -> RegionOverlayGraph[T]:
"""
An acyclic view of this graph, obtained by blacklisting back edges (edges whose source is ordered at or
after their destination in ``node_order``). Replaces utils.graph.to_acyclic_graph without a copy.
@ -1460,20 +1485,19 @@ class RegionOverlayGraph[T](networkx.DiGraph):
back_edges = [(u, v) for u, v in self.edges if node_order[u] >= node_order[v]]
return self.to_acyclic(back_edges)
def to_acyclic(self, blacklisted_edges) -> RegionOverlayGraph:
def to_acyclic(self, blacklisted_edges: Sequence[tuple[Tx[T], Tx[T]]]) -> RegionOverlayGraph[T]:
"""
A new view with the given (view-level) edges additionally blacklisted; used to traverse the region as an
acyclic graph without copying it.
"""
extra = frozenset((u, v) for u, v in blacklisted_edges)
return RegionOverlayGraph(
self.overlay,
full=self.full,
include_marked=self.include_marked,
blacklisted_edges=self.blacklisted_edges | extra,
blacklisted_edges=self.blacklisted_edges.union(blacklisted_edges),
)
def reverse_view(self) -> RegionOverlayGraph:
def reverse_view(self) -> RegionOverlayGraph[T]:
"""The reversed view of this graph (zero-copy)."""
g = RegionOverlayGraph(
self.overlay,
@ -1481,13 +1505,14 @@ class RegionOverlayGraph[T](networkx.DiGraph):
include_marked=self.include_marked,
blacklisted_edges=frozenset({(v, u) for u, v in self.blacklisted_edges}),
)
g._succ, g._pred = g._pred, g._succ # swap the adjacency mappings to reverse the graph
# swap the adjacency mappings to reverse the graph
g._succ, g._pred = g._pred, g._succ # type: ignore
# g._adj is synced with _succ
return g
def materialize(self) -> networkx.DiGraph:
def materialize(self) -> networkx.DiGraph[Tx[T]]:
"""An independent networkx.DiGraph copy of this view."""
g: networkx.DiGraph = networkx.DiGraph()
g: networkx.DiGraph[Tx[T]] = networkx.DiGraph()
g.add_nodes_from(self._node_set())
for u in self._node_set():
for v, data in self._adj[u].items():
@ -1499,24 +1524,24 @@ class RegionOverlayGraph[T](networkx.DiGraph):
# use .full_view / .member_view for those)
#
def edge_marked(self, u, v, mark_name: str | None = None) -> bool:
def edge_marked(self, u: Tx[T], v: Tx[T], mark_name: str | None = None) -> bool:
if mark_name is not None:
return (u, v) in self.overlay.edge_marks.get(mark_name, set())
return any((u, v) in marks for marks in self.overlay.edge_marks.values())
def successors(self, n, fullgraph: bool | None = None, all_edges: bool | None = None):
def successors(self, n: Tx[T], fullgraph: bool | None = None, all_edges: bool | None = None):
g = self._variant(fullgraph, all_edges)
if g is self:
return super().successors(n)
return g.successors(n)
def predecessors(self, n, fullgraph: bool | None = None, all_edges: bool | None = None):
def predecessors(self, n: Tx[T], fullgraph: bool | None = None, all_edges: bool | None = None):
g = self._variant(fullgraph, all_edges)
if g is self:
return super().predecessors(n)
return g.predecessors(n)
def has_edge(self, u, v, fullgraph: bool | None = None, all_edges: bool | None = None) -> bool:
def has_edge(self, u: Tx[T], v: Tx[T], fullgraph: bool | None = None, all_edges: bool | None = None) -> bool:
g = self._variant(fullgraph, all_edges)
if g is self:
return super().has_edge(u, v)
@ -1526,17 +1551,20 @@ class RegionOverlayGraph[T](networkx.DiGraph):
# overrides for inherited methods that would construct self.__class__() without arguments
#
def copy(self, as_view: bool = False) -> networkx.DiGraph:
# NOTE: this does not respect the intended semantics of as_view
def copy(self, as_view: bool = False) -> networkx.DiGraph[Tx[T]]:
return self.materialize()
def subgraph(self, nodes) -> networkx.DiGraph:
def subgraph(self, nodes) -> networkx.DiGraph[Tx[T]]:
# Build only the induced subgraph (cost O(induced subgraph)) instead of materialize()+restrict (cost O(whole
# region graph)): callers (e.g. quasi_topological_sort_nodes' SCC handling) want a small induced subgraph and
# then mutate it. Returns an independent, mutable networkx.DiGraph with exactly the nodes/edges/data (and the
# same node/edge iteration order) that self.materialize().subgraph(nodes) would have produced.
if not isinstance(nodes, Iterable):
raise TypeError("Please use the non-pathological versions of the NetworkX api")
node_set = self._node_set()
selset = {n for n in nodes if n in node_set}
g: networkx.DiGraph = networkx.DiGraph()
g: networkx.DiGraph[Tx[T]] = networkx.DiGraph()
g.add_nodes_from(n for n in node_set if n in selset)
for u in g:
for v, data in self._adj[u].items():
@ -1544,5 +1572,6 @@ class RegionOverlayGraph[T](networkx.DiGraph):
g.add_edge(u, v, **data)
return g
def to_directed(self, as_view: bool = False) -> networkx.DiGraph:
# NOTE: this does not respect the intended semantics of as_view
def to_directed(self, as_view: bool = False) -> networkx.DiGraph[Tx[T]]:
return self.materialize()

View file

@ -1,7 +1,7 @@
# pylint:disable=unused-argument,arguments-differ
from __future__ import annotations
import angr.ailment as ailment
from angr import ailment
from angr.analyses.decompiler.sequence_walker import SequenceWalker
from angr.analyses.decompiler.structurer_nodes import (
CascadingConditionNode,

View file

@ -174,7 +174,6 @@ class LoopNodeFinder(SequenceWalker):
def _handle_Loop(self, node: LoopNode, **kwargs):
super()._handle_Loop(node, **kwargs)
self.loop_nodes.append(node)
return None
class MultiStatementExpressionAssignmentFinder(AILBlockRewriter):
@ -387,8 +386,6 @@ class ExpressionCounter(SequenceWalker):
super()._handle_Loop(node, **kwargs)
self._outer_scope = outer_scope
return None
def _handle_SwitchCase(self, node: SwitchCaseNode, **kwargs):
self._collect_uses(node.switch_expr, ConditionLocation(node.addr))
return super()._handle_SwitchCase(node, **kwargs)
@ -602,11 +599,12 @@ class ExpressionReplacer(AILBlockRewriter):
else:
new_statements.append(stmt_)
new_expr = self._handle_expr(0, expr.expr, stmt_idx, stmt, block)
if new_expr is not None and new_expr is not expr.expr:
inner_in = expr.expr
new_expr = self._handle_expr(0, inner_in, stmt_idx, stmt, block)
if new_expr is not None and new_expr != inner_in:
changed = True
else:
new_expr = expr.expr
new_expr = inner_in
if changed:
if not new_statements:
@ -624,23 +622,26 @@ class ExpressionReplacer(AILBlockRewriter):
if is_phi_assignment(stmt):
return stmt
if isinstance(stmt.dst, VirtualVariable) and stmt.dst.varid in self._assignments:
dst_in = stmt.dst
src_in = stmt.src
if isinstance(dst_in, VirtualVariable) and dst_in.varid in self._assignments:
return stmt
changed = False
dst = self._handle_expr(0, stmt.dst, stmt_idx, stmt, block)
if dst is not stmt.dst and not isinstance(dst, (Call, ITE)):
dst = self._handle_expr(0, dst_in, stmt_idx, stmt, block)
if dst != dst_in and not isinstance(dst, (Call, ITE)):
changed = True
else:
dst = stmt.dst
dst = dst_in
assert isinstance(dst, Atom)
src = self._handle_expr(1, stmt.src, stmt_idx, stmt, block)
if src is not stmt.src:
src = self._handle_expr(1, src_in, stmt_idx, stmt, block)
if src != src_in:
changed = True
else:
src = stmt.src
src = src_in
if changed:
return Assignment(stmt.idx, dst, src, **stmt.tags)
@ -744,7 +745,6 @@ class ExpressionFolder(SequenceWalker):
node.condition = r
# again, do not replace into the loop body
return None
def _handle_SwitchCase(self, node: SwitchCaseNode, **kwargs):
replacer = ExpressionReplacer(self._assignments, self._uses, self._variable_map)

View file

@ -3,7 +3,7 @@ from __future__ import annotations
import logging
import angr.ailment as ailment
from angr import ailment
from angr.analyses.decompiler.goto_manager import Goto
from angr.analyses.decompiler.sequence_walker import SequenceWalker
from angr.analyses.decompiler.structurer_nodes import (

Some files were not shown because too many files have changed in this diff Show more