Oxidizer: Rust pseudocode generation by ltfish · Pull Request #6283 · angr/angr

@ltfish self-assigned this

Mar 26, 2026

@bluesadi

@bluesadi

@bluesadi

@bluesadi

@bluesadi

@bluesadi

@bluesadi

@bluesadi

@bluesadi

@bluesadi

* Refine how CleanupCodeRemover removes function calls to be compatible with ReturnDuplicator
* Fix CFGTransformationMixin.remove_block/replace_jump_target
* Fix how PatternMatchSimplifier selects arms
* Add ErrorPropagationSimplifier

@bluesadi

@bluesadi

@bluesadi

@bluesadi

@ltfish @bluesadi

@ltfish @bluesadi

@ltfish @bluesadi

@mahaloz @bluesadi

@pre-commit-ci @bluesadi

@bluesadi

@pre-commit-ci @bluesadi

@bluesadi

@bluesadi

@bluesadi

…t and Assignment(dst, Call)

@bluesadi

@bluesadi

@bluesadi

@pre-commit-ci @bluesadi

@bluesadi

@pre-commit-ci

…ypeConstant

The assertion added in 8f2e9f7 wrongly narrowed typevar to
(TypeVariable, DerivedTypeVariable), but the elif branch above explicitly
allows TypeConstant (e.g., Pointer64(Int8)) to flow through. This caused
~120 decompiler tests to fail with AssertionError in simple_solver.py:451.

Remove the erroneous assert and broaden DerivedTypeVariable.type_var's
type annotation to include TypeConstant, which reflects the actual
runtime contract.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tests/rust/test_rustc_version_identification.py requires Rust FLIRT
signatures to detect rustc versions via angr.rust.utils.rust_sigs. The
flirt_signatures package is not on PyPI, so add it as a git source and
include it in the extras dependency group (installed by default via
`uv sync`), mirroring how pysoot is handled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the caller passes default decompiler options (e.g., from
angr-management), options_by_class["region_simplifier"] contains
simplify_ifelse (registered as a default-True region_simplifier option in
decompilation_options.py). Previously it was also passed explicitly via
simplify_ifelse=self._flavor != "rust", causing
"TypeError: got multiple values for keyword argument 'simplify_ifelse'"
every time angr-management decompiled a function.

Pop simplify_ifelse from the unpacked options dict so the explicit kwarg
wins unconditionally, preserving the intent of forcing if-else
simplification off for the Rust flavor regardless of user options.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Function.is_prototype_guessed is a read-only @Property derived from
self._prototype_source. Assigning to it raises AttributeError at runtime
(no setter) and also trips pyright. Three Rust-path call sites hit this:

  - rust/optimization_passes/rust_calling_convention.py: use
    PrototypeSource.CCA_DECOMPILER (decompiler-level CCA result).
  - rust/optimization_passes/function_prototype_inference.py: same.
  - rust/analyses/type_db_loader.py: use PrototypeSource.SIGNATURES
    (prototypes loaded from a curated type database).

Also fix a related TypedDict collision in
analyses/decompiler/clinic.py where `is_prototype_guessed=False` was
passed explicitly to ailment.Expr.Call alongside `**last_stmt.tags`,
whose TagDict already declares is_prototype_guessed. Merge the override
into a local dict before unpacking so pyright is happy and the intent
is preserved.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- clinic.py: remove duplicate `Typehoon` import (F811); the package-level
  re-export in angr.analyses.typehoon already provides it.
- engine_ail.py: collapse nested `if` into a single conjunction (SIM102).
- rust/typehoon/translator.py: merge two `startswith` calls into tuple
  form for RustEnum name detection (PIE810 x2).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
RustcVersionIdentification relies on angr.rust.utils.rust_sigs to find
the Rust FLIRT signature directory. That helper looks at the
angr-management submodule path (angrmanagement/resources/flirt_signatures),
but only consults `sys.modules["angrmanagement"]` — it does not import
angrmanagement itself. test_rustc_version_identification.py only imported
angr, so the check always failed and the analysis returned
rustc_version=None, causing every SUBFAILED assertion.

Import angrmanagement at the top of the test module so the signature
directory is discovered. angr-management is installed in CI via
install.sh, so the import succeeds there.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Top-level `import angrmanagement` broke collection on the smoketest jobs
(Windows/macOS), which don't install angr-management. Switch to
pytest.importorskip so environments without angrmanagement skip the
module at collection time, while the main CI test jobs (where
angr-management is installed) still import it and let
RustcVersionIdentification find the bundled FLIRT signatures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@bluesadi

…for Rust flavor

Commit ed10e60 ("Fix some bugs") changed the Python default of
PhoenixStructurer.__init__'s use_multistmtexprs from MAX_ONE_CALL to
NEVER. This was inconsistent with the option registered in
decompilation_options.py (which keeps MAX_ONE_CALL) and silently
affected every caller that did not explicitly pass use_multistmtexprs
through `decompiler_options`, because Decompiler.options_to_params only
threads user-provided options and falls back to Python parameter
defaults for the rest.

The observable regression: tests that assert `count("goto") == 0` or
expect `return` statements to appear (e.g. test_decompiling_tr_build_spec_list,
test_eliminating_stack_canary_reused_stack_chk_fail_call) failed
because phoenix could no longer eliminate certain gotos by folding
assignments into multi-statement expressions.

Revert the Python default so the C path is unchanged, and force
use_multistmtexprs=NEVER at the Decompiler level when the flavor is
"rust", mirroring how simplify_ifelse is handled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…_build_spec_list

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…binary

Commit 7fa5227 added tracking of writes to the overflow return register
(e.g., rdx on x64) so that 128-bit Rust return values could be detected,
and unconditionally added max(overflow_retval_sizes) onto every per-block
retval_size. Commit 3ffb99e correctly gated the 9..16 return-size type
mapping in CallingConventionAnalysis._guess_retval_type behind
is_rust_binary, but the fact collector's overflow accumulation was not
gated the same way.

On non-Rust binaries the overflow register is typically a scratch
register, so any incidental write to it inflates retval_size past 8.
_guess_retval_type then finds no matching size bucket (because the
9..16 range is gated off) and returns SimTypeBottom(label="void"),
making _adjust_prototype mark the whole function as returning void.

Concretely, test_decompiling_msvcrt_IsExceptionObjectToBeDestroyed's
vcruntime_test.exe walks a linked list with rdx, which bumped its
retval_size from 4 to 12 and wiped `return 1;` / `return 0;` from the
output.

Only set overflow_retreg_offset for Rust binaries so the scratch-register
case no longer pollutes retval_sizes on C binaries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Commit ed2a601 ("Support Rust v0 demangling") replaced the old
split-and-rejoin-dropping-the-last-node logic with a direct
`rust_demangler.demangle(self.name)` call, which preserves the trailing
17-character `h<16 hex>` disambiguation hash (e.g.,
"std::rt::lang_start::h9b2e0b6aeda0bae0"). The C code generator then
emits function names with these hashes in calls, breaking tests that
expect the clean Rust name (e.g., test_function_pointer_identification
asserts `"std::rt::lang_start(rust_hello_world::main"` in the output).

Drop the trailing hash component when it matches Rust's format
("h" + 16 lowercase hex chars), restoring the pre-PR behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@pre-commit-ci