angr-memories

rust

28 remembered, 38 forgotten.

rust-ast-identity-rust-side-only forgotten

Rust-symex AST identity round-trip is Rust-side only: set_state_addr_to_ast/get_state_addr_to_ast (rust_state_export.py:1058) pins ASTs in the per-state RustSimState addr_to_ast map, and Rust holds its own strong PyObject ref until the state drops (test_addr_to_ast_round_trip asserts 'recovered_ast is ast'). There is NO Python-side identity map: the former SymbolicIdentityTracker + _ast_handle_cache were write-only (only reader _lookup_handle had no prod callers) and leaked strong refs unbounded — deleted in angr-iu40 (commit fd3cc6325). _register_handle now only forwards to set_state_addr_to_ast when addr+state_id given; calls without them are no-ops. Don't reintroduce a Python identity cache for export.

forgotten 2026-08-05T04:34:40Z — Fully superseded by a near-verbatim module docstring in angr/exploration/rust_identity.py (same claim: SymbolicIdentityTracker write-only, no prod reader, removed in angr-iu40) — the invariant now lives as a code comment exactly where criterion (f) says it belongs, so the memory is redundant bd-prime weight.

rust-bogus-found-state-triage remembered

Debugging a bogus 'found' state in the Rust engine: check get_state_bbl_history_tail(sid, N) FIRST. For angr-je2xt the bogus found state's history started mid-path (0x401265) instead of at main, which instantly ruled out the entire resume/fork/constraint-sync surface the bug report pointed at and localized it to a bad SEED state. Constraint counts (state_constraint_count) tell you constraints are missing; the bbl history tells you WHERE the state came from.

rust-bv-constructor-surface forgotten

RustBV constructor surface (post-angr-pkxv, 2026-05-31): canonical concrete ctor is RustBV::concrete(value: u128, width: u32) in native/angr/src/symbolic/value.rs. The dead from_u64/from_u32 delegates were deleted — they had zero callers in either Rust or Python. Don't reintroduce u64/u32 named ctors or From<(u64,Width)> / Width newtype impls without a real caller to validate the shape; otherwise it's just dead code. If a single u64/u32 caller appears, inlining 'RustBV::concrete(v as u128, w)' at the callsite is fine for one or two sites.

forgotten 2026-06-04T21:30:03.539094+00:00 — Refactor receipt for angr-pkxv (deleted dead from_u64/from_u32 delegates); rule against re-introducing dead ctors is obvious code-review hygiene, not a durable design rule.

rust-bv-handle-single-accessor forgotten

RustBVHandle (native/angr/src/symbolic/handle.rs) accessors are defined ONCE in the #[pymethods] block with #[getter] for properties (id, width, is_concrete) and a regular method for concrete(). Plain impl block only has new_symbolic/new_concrete/set_concrete. Do NOT re-add Rust-side duplicates like width()/is_concrete() in a separate impl block — pyo3 lets Rust callers invoke pymethods directly. Also do NOT re-add Python aliases length/symbolic; canonical names are width/is_concrete.

forgotten 2026-06-04T21:30:03.882957+00:00 — Code-shape note about not duplicating pymethods accessors; the source file is the answer and pyo3 conventions cover this generally.

rust-engine-2026-07-roadmap forgotten

2026-07-03 roadmap after angr-nkoct: corpus perf is DONE (angr-9w6ad: engine not the bottleneck; losses = bimodal-Z3 floor, deferred) and corpus-wide bounce elimination is low-ROI (bounce-reduction-low-roi-corpus). Next frontier = (1) the BOUNCE-SERVICE WALL -- what capped steady parallel mode at workers=1 parity and dominates real-binary init storms -- and (2) SCHEDULING, a green field (entire scheduler is one use_lifo bool; unused reconvergence collision signal in helpers.rs::record_reconvergence_sample). Filed: EPIC angr-a32jl (SelectionPolicy seam at pop_active/route_successor + policy lab: random-path, coverage-guided new-block-first, loop-head round-robin fairness, CFG-distance-snapshot directed as undefer vehicle for 11djq.15); angr-1ilq.8/.9 (measure then fix num_find=1 speculative waste via find-aware dispatch + prompt cancellation -- unblocks ship gate 1ilq.5); EPIC angr-gorvf (zero-Python moonshot: gil_work_time_ns==0 post-setup on >=8/32 benches; children: .1 bounce-service attribution measure-first, .2 lazy SimState views contingent on reconstruction>=30%, .3 ADDS_EXITS+a8epx gate flip, .4 milestone; angr-z087y libVEX FFI re-scoped as staged core). WHAT NOT TO DO: no corpus wall-time chasing; no pure-greedy directed search; no merging before a32jl.3 Stage-1 collision data; no native CFG construction (metadata snapshot instead); nothing defaults on without bench-gate + A/B; no Veritesting/csaw_wyvern ports/full posix. Execution order: gorvf.1 + 1ilq.8 (measurements) -> a32jl.1 (seam) -> z087y Stage 1 (long pole) -> 1ilq.9 -> a32jl.2/.3 -> gorvf.3 -> gorvf.2 (if gated in) -> a32jl.4 -> gorvf.4.

forgotten 2026-07-03T23:13:37.053048+00:00 — status-shape

rust-engine-32bit-entry-state-gap forgotten

Rust engine + 32-bit x86 + entry_state path: known semantic divergence from Python (iter 277, csci4968_crackme0x00a). When Python emits SYMBOL_FILL_UNCONSTRAINED_MEMORY warnings during __libc_csu_init (and similar init code) it silently fills with symbolic and continues. Rust engine may terminate exploration silently (0 found states, 0.10s) under the same conditions. Symptom = bench wrapper appears to 'work' but returns IndexError on sm.found[0]. Workaround: use blank_state at the checking function (like flareon2015_2 does) instead of full entry_state for 32-bit x86 crackmes. Related: existing rust_ok=False entries asisctffinals2015_license, whitehat_crypto400 hit 'list index error' which is likely the same root cause family (entry_state semantic gap surfacing as IndexError on empty sm.found).

forgotten 2026-08-05T04:34:40Z — Stale: verified against tests/benchmarks/run_single.py — asisctffinals2015_license is now rust_ok=True (fixed via bounded-symbolic-file export), and whitehat_crypto400's own note says 'Catalog previously claimed list index error; the guard supersedes that path entirely now.' csci4968_crackme0x00a no longer appears in the bench catalog. The described divergence/workaround no longer matches current behavior.

rust-engine-claude-md-pointer remembered

Pointer, not a snapshot

For Rust engine architecture, test counts, benchmark status, build/bootstrap recipe, arch support matrix, SimOption coverage, state.inspect limitation, key-file map, and Z3 profiling counters — read CLAUDE.md in the repo root and docs/advanced-topics/rust_engine.rst. They are the authoritative live documents.

Earlier versions of this memory contained a frozen architecture summary that rotted (claimed "14/14 tests passing" while the tree had ~389). On branch rust-symex the live picture moves fast enough that snapshots in memory are a liability, not an asset.

Where each topic lives

  • Build/bootstrap, Makefile shortcuts, log levels, profiling, autonomous loop — CLAUDE.md (repo root)
  • Test count: grep -rc 'def test_' tests/engines/rust/
  • Per-bench speedup table: CLAUDE.md "Current Status" section
  • Live per-bench timings: tests/benchmarks/baseline_timings.json (authoritative; the table in CLAUDE.md can lag)
  • Architecture support matrix, SimOption coverage matrix, state.inspect limitation, Z3 solver profiling, exploration-technique compatibility — docs/advanced-topics/rust_engine.rst
  • VEX op extension contributor guide — docs/extending-angr/rust_vex_ops.rst
  • SimProcedure extension contributor guide — docs/extending-angr/simprocedures.rst

(Older versions of this memory pointed at docs/RUST_SIMOPTION_COVERAGE.md and docs/RUST_STATE_INSPECT.md. Those files were folded into rust_engine.rst and no longer exist as standalones.)

What stays in memory (siblings)

  • core-goal-design-philosophy — design rationale (stable, slow-changing)
  • rust-proxy-architecture-decision — RustStateProxy decision (architectural)
  • benchmark-bimodal-variance-rules — bimodal-Z3 variance handling rules
  • rust-python-boundary-audit — where the Python↔Rust line should sit
  • constraint-export-no-pre-pin — pre-pinning is dangerous; Rust solver eval fallback is the clean fix
  • characterization-vs-fix-pattern — separate characterization tasks from regression bisects

How to apply

When asked about the Rust engine's architecture or current capabilities, read CLAUDE.md and rust_engine.rst first. Use the sibling memories above only for context that is genuinely stable (design rationale, lessons-learned) — not for counts, speedups, or test results, which belong in the live docs.

rust-engine-deprecated-decorator forgotten

Rust-engine @_deprecated decorator pattern (angr-1rx1): angr/exploration/_deprecation.py defines _deprecated(version='X.Y', removed_in='X.Z', replacement=None) for marking public Rust-engine names as deprecated during step 1 of the API stability deprecation cycle. Underscore prefix = private (do not import downstream). Emits DeprecationWarning once per decorated callable per process via a _warned set keyed on func object — keeps test logs and long-running sessions legible. stacklevel=2 so warning points at caller. functools.wraps preserves name/doc so the public-API snapshot test (angr-tww5) still sees the method on its class. Test file tests/engines/test_rust_deprecation.py has a _reset_warned_set fixture so per-test isolation works. Policy doc section 'Applying the @_deprecated decorator (Python side)' is the user-facing docs under :ref:rust-engine-api-stability. Decorator lives in tree even when no current public name is being deprecated — covers the first cycle that needs it.

forgotten 2026-06-04T21:30:04.241112+00:00 — Implementation receipt for angr-1rx1 decorator pattern; code in angr/exploration/_deprecation.py + test fixture is the answer. Policy doc covers user-facing side.

rust-engine-docs-canonical-rst forgotten

User-facing Rust engine documentation lives in docs/advanced-topics/rust_engine.rst (Sphinx). As of commit 027faaad7 it covers Usage, Architecture, Architecture support matrix, Z3 solver API, Z3 solver profiling counters, SimOption coverage matrix (full per-option breakdown), state.inspect limitation, and known slower benchmarks. CLAUDE.md should point at this file rather than duplicate prose — when adding a new user-facing fact, put it in rust_engine.rst and cite the file path in CLAUDE.md only if Claude itself needs it during operator workflows.

forgotten 2026-06-04T21:30:04.593876+00:00 — Duplicates CLAUDE.md's 'User-facing documentation' section verbatim — pointer-only memory whose content is already in CLAUDE.md final section.

rust-engine-inheritance-contract forgotten

use_rust_engine inheritance contract (angr-kc77, 2026-06-03): the kwarg on proj.factory.simulation_manager(state, use_rust_engine=True) is PER-CALL ONLY. (1) No project-level toggle exists — there is no proj.use_rust_engine or factory attribute. Attaching a RustExplorationManager does NOT bias subsequent factory.simulation_manager() calls. (2) factory.successors(state, ...) ALWAYS dispatches to factory.default_engine.process(...) which is wired to UberEngine / UberEnginePcode at factory construction and is not Rust-aware. (3) Every analysis under angr/analyses/ that builds an internal SimulationManager goes through factory.simulation_manager(...) WITHOUT threading the kwarg, so internal analyses always get Python — a deliberate safety default. (4) The only opt-in for an analysis to run under Rust is an in-tree port (per-analysis verdict in docs/advanced-topics/rust_engine.rst) or an out-of-tree monkey-patch like tests/benchmarks/run_single.py / tests/engines/test_callable_rust.py. (5) Contract is pinned by tests/engines/test_factory_rust_inheritance.py (7 tests). Doc label: rust-engine-inheritance-contract.

forgotten 2026-06-04T16:18:48.727024+00:00 — use_rust_engine inheritance contract documented in docs/advanced-topics/rust_engine.rst (per-analysis verdict) and pinned by tests/engines/test_factory_rust_inheritance.py

rust-engine-lift-failure-silent-deadend forgotten

RustExplorationManager.run() does NOT surface typed RustExecutionError on lift failures end-to-end; instead the state silently deadends. Root cause: rust_manager.py::_cb_lift_block catches (SimEngineError, ClaripyError, PyVEXError) and returns '{}' (empty IRSB). For exceptions NOT in that triad, the PyErr bubbles up as CbExecutionError::LiftError -> RunResult::Error -> stepping.rs:346 deadend heuristic: 'if message.contains("No bytes in memory") || message.contains("lift") || addr == 0 { Deadended }'. The 'lift' substring catches lift-callback-failure messages too. To test typed-error contract for VEX lift validation, use execute_irsb_for_test (engine.rs:90) directly — that path maps CbExecutionError::InvalidIR -> RustExecError::MalformedIRSB { addr, reason } -> RustMalformedIRSBError. Cheapest InvalidIR trigger: Ist_LLSC with result=99 + tyenv only declaring t0 (hits statements.rs:708).

forgotten 2026-07-04T00:34:18.021392+00:00 — Describes the old stepping.rs message.contains heuristic which no longer exists (now structural run_error_kind); unique test-trigger detail preserved in canonical. (merged into rust-runresult-error-typed-routing)

rust-engine-simplification-audit-clean forgotten

Rust engine simplification audit (native/angr/src/, 2026-06-22) came back CLEAN: cargo check --release reports ZERO dead-code warnings; the only #[allow(dead_code)] (vex/ccall/arm32.rs arm_cond table) is justified (documentation-only encoding variants); no abandoned fast-paths (baseline_counters.json shows live counters on hot paths); unused-&self clippy hits are deliberate API-parity signatures, not dead code. context.rs was excluded (active refactor epic a2br.2). Don't re-run a Rust-side dead-code hunt without new churn. Simplification opportunities live ONLY in the Python bridge -> epic angr- (Rust-symex bridge simplification): inline callback timing wrappers, extract RustSolverProxyBase (~70% dup, relates to angr-fjhk9), audit proxy-gate fallback paths. 4 hunter findings were CUT in verification (install-proxy methods not identical; LazyStateProxy genuinely lazy; protocol stubs + cache-split intentional).

forgotten 2026-07-04T00:34:18.410923+00:00 — Point-in-time audit receipt (2026-06-22) with 'don't re-run without new churn'; heavy churn since; references blank epic. Iteration-receipt scrap.

rust-engine-version-attr forgotten

Rust engine version metadata pattern (angr-zcjx): native/angr/src/engine.rs::vex_engine adds m.add('version', env!('CARGO_PKG_VERSION'))? — sourced from native/angr/Cargo.toml at compile time. Python re-exports as angr.exploration.rust_engine_version (imported as 'from angr.rustylib.vex_engine import version as rust_engine_version'). Intentionally NOT in all (standard version convention — explicit access only, excluded from star-import). Test contract in tests/engines/test_rust_public_api.py::TestRustEngineVersion has 3 checks: (1) non-empty string, (2) value matches between angr.exploration and angr.rustylib.vex_engine, (3) absent from all. Bump version in Cargo.toml only — Python picks it up on next Rust rebuild. Documented under 'Reading the engine version' subsection of rust_engine.rst api-stability section.

forgotten 2026-06-04T21:30:04.947095+00:00 — Implementation receipt for angr-zcjx __version__ pattern; bump-version recipe is one line and the test contract documents itself.

rust-error-variant-production-coverage forgotten

RustExecError variants Z3, Oom, UnsupportedSyscall (errors.rs:78-93) have NO production trigger paths today — only the inject_test_error_kind() helper (engine.rs:285-313) constructs them. The PyO3 classes are registered for forward-compatibility. Real production raise sites are: MalformedIRSB (from CbExecutionError::InvalidIR at engine.rs:33), UnsupportedVexOp (from OpError::UnsupportedNeon/VectorOp/VexOp at engine.rs:54-68), and Other (catch-all for Memory/TypeMismatch/UnknownTemp/Callback/LiftError/Unsupported/NeedPythonFallback variants). The exhaustive (no _-wildcard) match in cb_execution_error_to_typed prevents new errors silently demoting to Other. Documented in rust_engine.rst 'User-facing error taxonomy' section landed via angr-5ek9 commit ab011dd64.

forgotten 2026-06-04T16:21:57.073556+00:00 — closed-only AND status-shape: iteration receipt for closed bead

rust-errored-stash-unreachable-from-python remembered

The high-level RustExplorationManager.errored stash CANNOT be populated by Python-driven execution. Empirically: unmapped data loads are swallowed to a zero buffer (state dropped, all stashes 0); unmapped/bad block fetches -> deadended; UD2/div-by-zero stay active; hlt/int3 -> deadended; STRICT_PAGE_ACCESS execution RAISES SimSegfaultException out of run() rather than stashing. The lazy page-fetch callback (_cb_fetch_page in rust_manager.py) hardcodes perms=7 (RWX), so ENABLE_NX/set_enforce_nx never fires for lazily-loaded binaries (fauxware included). The errored stash is only populated by (1) Rust-side Panic-strategy IR errors (Op/TypeMismatch/InvalidIR via interpreter FallbackStrategy) or (2) the NX check on a Rust-side non-X mapped page, reachable today only via the BARE _RustExplorationManager + state.map_memory(addr,len,perms<X) (see test_manager_core.py::test_strict_page_access_blocks_nx_block_fetch -> errored==1). Bare path does not expose the high-level RustErrorRecord-wrapping errored property. See angr-9drh4.

rust-fallback-strategy-semantics forgotten

Rust interpreter FallbackStrategy semantics (pub enum FallbackStrategy in native/angr/src/interpreter/mod.rs): PythonCallback variants surface as RunResult::NeedPythonVEX and the failing block re-runs through Python's HeavyVEXMixin -> HeavyResilienceMixin, so any SimOption that gates resilience IN PYTHON is automatically honored under Rust for the unsupported-feature path. Panic variants surface as RunResult::Error and move the state to the errored stash without re-entering Python. The CbExecutionError variants are: Unsupported -> PythonCallback (honored); Memory/Op/InvalidIR/TypeMismatch/UnknownTemp/Callback/Lift -> Panic (NOT honored). This is why BYPASS_UNSUPPORTED_* work but BYPASS_ERRORED_* don't — Python's check_unsupported* fires after the fallback, but Python's check_errored* never runs because Rust took the Panic path before any Python evaluation got the chance to raise SimError. (Symbol-anchored per refactor-memory-sweep-rule; the original raw range interpreter/mod.rs:262-323 drifted to ~308-340 by iter52.)

forgotten 2026-08-05T04:34:40Z — Stale on multiple axes: FallbackStrategy/CbExecutionError moved out of interpreter/mod.rs into interpreter/execution_error.rs (per that file's own doc comment: 'Split out of interpreter/mod.rs, angr-9ke6b.91'); current CbExecutionError variants are Memory/Op/InvalidIR/Unsupported/UnknownTemp/Callback/LiftError/NeedPythonFallback — there is no TypeMismatch variant as the memory claims, and NeedPythonFallback is missing entirely from the memory's account. Superseded in substance by rust-runresult-error-typed-routing, which is kept and is more accurate.

rust-fork-metadata-test-pattern remembered

De-vacuifying rust-symex fork/metadata tests: the Python-callable fork path is RustExplorationManager.fork_state_to_stash(parent_id, stash) (pymethod, exploration/mod.rs) -> _fork_state_to_stash -> RustSimState::fork. Use it to test fork-time clone isolation: child sees parent's planted addr_to_ast entry, but later set_state_addr_to_ast writes on each side stay isolated (independent backing maps). To find forked descendants after a real run, enumerate all stashes via mgr._rust_mgr.stash_counts() keys + get_state_ids(stash); every descendant carries a CLONE of the parent's metadata (same AST object identity). PC round-trip getter is get_state_pc(stash, index). Old vacuous tests compared unrelated managers or guarded all asserts behind 'if leftover:'.

rust-found-state-id-and-progress-signals remembered

When asserting against found/terminal-stash states in RustExplorationManager tests, the Python state proxies in mgr.found do NOT carry s.scratch._rust_state_id (that attr is only on driven active states). Resolve Rust-side ids via mgr._rust_mgr.get_state_ids('found') and pass them to export_state() — see test_call_stack_on_found_states for the canonical pattern. Also: on linear binaries (fauxware), a single active state steps forward without changing stash_counts (stays active=1), so 'run progressed' must be asserted via mgr.stats['steps']>0, NOT via stash-count deltas.

rust-helpers-rs-split-map remembered

exploration/helpers.rs was split by angr-9ke6b.76 (commit ae3fff5c6, 1753 -> 689 lines). Where former helpers.rs symbols now live:

  • native_technique.rs: apply_native_techniques + apply_merge_point, park_states_at_address, merge_waiters_by_callstack, move_state_by_id, drop_states_by_id, exceeds_loop_bound (co-located with the NativeTechnique enum they interpret).
  • constraint_sync.rs: sync_constraints_from_python, resolve_claripy_z3_backend, extract_z3_ptr_from_claripy.
  • fork_materialize.rs: add_fork_guard_constraint, reconstruct_deferred_fork_condition, build_unexplored_fork, PriorGuards, MaterializedForks, MaterializeForkCtx, materialize_deferred_forks.
  • native_proc_dispatch.rs: effective_pc, NativeProcDisposition, segfault_message, NativeProcCounters, FallbackBucket, dispatch_native_proc.
  • symbolic/bv_chunk.rs (angr-9ke6b.230, a layer DOWN not sideways): store_concrete_bytes_chunked, u128_to_le_bytes, load_concrete_bytes_chunked — moved so state/pymethods.rs can share them; re-exported from crate::symbolic. STILL in helpers.rs: fold_scheduler_dispatch_stats / record_reconvergence_sample / record_migration_sample, with_pending(_mut)/with_state(_mut)/find_state(_mut)/index_state/rebuild_state_index, push_to_active_or_drop/route_successor/push_found_capped/push_or_drop_terminal, compute_register_tuple_hash, apply_uniqueness_filter, extract_procedure_args/extract_syscall_args/get_return_addr, prepare_shared_callback_solver. Every new module carries the Python-boundary #![deny(clippy::unwrap_used, clippy::expect_used)] gate. helpers_tests.rs was split the same way into sibling *_tests.rs files. Callers import from the new modules directly -- there are deliberately NO re-exports through helpers::.

rust-history-granularity remembered

RustSimState.history records ONE entry per step_state_with_skip call (native/angr/src/exploration/stepping.rs:110: state.add_to_history(state.pc()) after set_pc(step.new_pc)). One 'step' can lift and execute many IRSB blocks via interpreter.run_until_event before terminating at a Hook/SimProcedure/Syscall/MaxBlocks event. Result: Rust's history.bbl_addrs (via export_state.get_history() / RustHistoryProxy.recent_bbl_addrs) shows the SimProcedure transition points, not per-IRSB-block addresses. Comparing it directly to Python's state.history.bbl_addrs is NOT meaningful — Python records every block. Verified 2026-05-17 on fauxware: Python=40 entries (mixed code + extern), Rust=9 entries (all extern: puts/read/open/exit). Conclusion: do NOT use bbl_addrs to confirm path divergence between engines; use detailed_history (per-IRSB-block with jumpkinds) or instrument run_loop directly.

rust-inspect-unsupported-event-set forgotten

The 5 inspect events the Rust engine refuses to register (raise via _format_unsupported_event_msg in rust_state_proxy.py) are exactly EventType minus _RUST_INSPECT_SUPPORTED_EVENTS: cfg_handle_job, vfg_handle_successor, vfg_widen_state, engine_process, memory_page_map. The other 21 (incl. symbolic_variable, vex_lift, constraints) are in the supported set — do not assume an event is unsupported just because it is unimplemented natively; some are Python-dispatched (dispatch_origin='python').

forgotten 2026-07-20T05:01:44.030617+00:00 — Duplicated by docs: rust_engine.rst lists exactly the 5 unsupported inspect events plus rationale and python-dispatch nuance (lines ~2559-2570, 2649)

rust-internal-error-convention forgotten

Internal Rust error convention (angr-0mqkc.10): PyResult ONLY at the PyO3 boundary (#[pymethods]/#[pyfunction]); internal fns return Result<T,DomainError> with subsystem-local thiserror enums (CbExecutionError, StepError/SubcallSetupError, MemoryError, OpError, ProcedureError, SyscallError, BridgeError, LiftError) composed via #[from]. NO Result<_,String>, NO anyhow (deliberately not a dep). Errors collapse to String/PyErr only at boundary (errored-stash record + From for PyErr). RustExecError is the TEST-ONLY user-facing taxonomy, not a general internal type. interpreter/ and exploration/ verified compliant. Authoritative copy: errors.rs module doc; mirror: rust_engine.rst 'Internal error convention' subsection.

forgotten 2026-07-20T05:01:44.536444+00:00 — Self-declared mirror: errors.rs module doc is authoritative and rust_engine.rst has the 'Internal error convention' subsection; memory adds nothing beyond them

rust-let-chain-clippy-autofix forgotten

Rust let-chain stabilization: as of Rust 2024 edition (stable in 1.85+), 'if let A = x && let B = y && cond { ... }' compiles natively. Clippy's collapsible_if autofix on 'if let Some(a) = x { if let Some(b) = y { body } }' rewrites to the let-chain form. The autofix LEAVES the inner block at its original indentation (over-indented by 4 spaces because the outer if is gone); rustfmt --edition 2024 cleans it up. Use rustfmt --config skip_children=true ON ONLY the clippy-changed files to avoid recursing into submodules and reformatting unrelated drift. The skip_children flag is rustfmt config-only (not a CLI option); pass via --config skip_children=true.

forgotten 2026-06-04T21:30:05.289604+00:00 — Generic Rust 2024 edition / clippy tooling tip with no angr-specific durable value; rustfmt/clippy docs cover this.

rust-log-warn-capture-pattern remembered

When testing Rust-emitted log::warn output from Python, use the low-level RustExplorationManager (from angr.rustylib.vex_engine import RustExplorationManager as _LL; _LL('amd64')) plus capfd, not the high-level Python wrapper. Reasons: (a) The high-level RustExplorationManager(proj, [state]) requires a real SimState and triggers many unrelated log lines during seeding (sync_registers, etc) that pollute capture. (b) capfd captures fd-level writes which is what eprintln! does — capsys captures only Python-level sys.stderr writes and misses Rust output. (c) Always set_rust_log_level('warn') first and drain capfd.readouterr() right before the assertion. Pattern used in TestStashNameValidation in tests/engines/rust/test_misc.py.

rust-macro-rules-child-module-textual-scoping forgotten

Rust macro_rules! macros declared in a parent mod.rs BEFORE 'mod child;' declarations are automatically visible inside the child module via legacy textual macro scoping — no #[macro_export], no 'pub(crate) use macroname;' re-export, no 'use super::macroname;' in the child is required. Verified 2026-05-31 in angr-61l0 (profile_start!/profile_add! in interpreter/mod.rs used in execution.rs/statements.rs/expressions.rs with zero imports). The compiler will flag any added 'pub(crate) use' OR 'use super::' as 'unused_imports' since the macros are already in scope. This is opposite to how function/type imports work — don't add them prophylactically for macros.

forgotten 2026-06-04T21:30:05.645625+00:00 — Generic Rust macro_rules scoping rule (legacy textual scoping); not angr-specific durable insight, available in Rust reference.

rust-manager-get-state-register-pitfall forgotten

RustExplorationManager Python wrapper attribute pitfall: mgr.get_state_register is NOT a method — mgr.__getattr__ resolves it as a stash proxy and returns [] (an empty list). Calling it raises TypeError: 'list' object is not callable. The correct Python-side calls are: mgr.eval_register(sid, name) (typed accessor, returns Optional[int]) OR mgr._rust_mgr.get_state_register(sid, name) (raw PyO3 method, what existing tests use). Same hazard likely affects other names that look like stash queries. Discovered while writing angr-95up.3 fileops error-return tests.

forgotten 2026-08-05T04:34:40Z — Stale: rust_manager.py's __getattr__ (line ~7191) no longer silently resolves unknown names to a stash proxy returning []; it now calls get_state_ids(name) and raises AttributeError directly on failure. The described symptom (TypeError: 'list' object is not callable) no longer reproduces.

rust-memory-sync-bug-2026-05-13 forgotten

Rust engine memory STORES at concrete non-stack addresses are NOT propagated back to Python state.memory.load. Reproducer: shellcode 'mov [0x4216c0], 0x12345678' run via RustExplorationManager.explore — mgr.found[0].memory.load(0x4216c0) returns the pre-init value, not the stored value. STACK-based writes (mov [esp-N], imm) DO propagate correctly. Affects sokohashv2 benchmark (angr-7vcx): hash writes to 0x4216C0+ get lost, test reads zeros, fails in 0.7s. Likely root cause area: state.rs export_full or angr/exploration/rust_state_export.py — concrete memory writes outside stack range aren't being included in the snapshot.

forgotten 2026-06-04T16:31:56.520529+00:00 — Bead/commit closure note

rust-mgr-stats-property-vs-method remembered

RustExplorationManager Python wrapper (angr/exploration/rust_manager.py) exposes 'stats' as a @property returning a dict — use 'mgr.stats' WITHOUT parens. But the raw Rust class angr.rustylib.vex_engine.RustExplorationManager exposes 'stats()' as a METHOD (call with parens). Mixing them up gives 'dict object is not callable'. The wrapper stats dict exposes 'steps' (cumulative across explore() calls, never reset — incremented at run_loop.rs self.steps+=1), NOT 'step_count'; 'step_count' only appears in the progress-callback dict (_check_limits in rust_manager.py). The raw-class stats dict includes 'drop_terminal_states' (round-trips with set_drop_terminal_states) and exposes 'find_addrs'/'avoid_addrs' as HashSet lengths (stats_api.rs), so they double as dedup assertions. Pitfall for de-vacuifying explore-resume tests: explore(find=0x4006ED, max_steps=5) on fauxware already FINDS the target (steps reaches 7, so max_steps is not a hard per-step cap) and drains active — a 'steps increase on 2nd explore' assertion fails (7>7). Assert found-retention instead: any(s.addr==0x4006ED for s in mgr.found).

rust-mgr-stats-steps-key forgotten

RustExplorationManager (Python wrapper) stats property exposes 'steps' (cumulative across explore() calls, never reset — incremented at run_loop.rs:699 self.steps+=1), NOT 'step_count'. 'step_count' only appears in the progress-callback dict (_check_limits in rust_manager.py). Native _RustExplorationManager.stats() exposes 'find_addrs'/'avoid_addrs' as HashSet lengths (stats_api.rs:26-27), so they double as dedup assertions. Pitfall for de-vacuifying explore-resume tests: explore(find=0x4006ED, max_steps=5) on fauxware already FINDS the target (steps reaches 7, so max_steps is not a hard per-step cap) and drains active — a 'steps increase on 2nd explore' assertion fails (7>7). Assert found-retention instead: any(s.addr==0x4006ED for s in mgr.found).

forgotten 2026-07-04T00:34:19.178819+00:00 — Same mgr.stats-access invariant as rust-mgr-stats-property-vs-method; unique 'steps' detail folded into canonical. (merged into rust-mgr-stats-property-vs-method)

rust-mod-tests-named-mods-extracted forgotten

Sibling-extraction campaign had a blind spot: the survey one-liner grepped for 'mod tests' and missed inline #[cfg(test)] modules with OTHER names. Iter 193 found+extracted the last 3: interpreter/mod.rs::smc_tests (78L), memory/page.rs::serde_tests (77L), exploration/stepping.rs::sizes (27L) -> *_tests.rs siblings (commit 42a7e60f3). To re-survey for stragglers, grep '#[cfg(test)]' then the NEXT line for 'mod ' (not just 'mod tests'). After this, no inline test module >20L remains. Recipe unchanged, see rust-mod-tests-sibling-extraction.

forgotten 2026-07-04T00:34:19.565459+00:00 — Iteration receipt for the test-split campaign; unique named-mod survey recipe folded into the canonical recipe memory. (merged into rust-mod-tests-sibling-extraction)

rust-mod-tests-sibling-extraction forgotten

Recipe for sibling-extracting a Rust #[cfg(test)] module: write sibling _tests.rs with 'use super::*;', replace the whole #[cfg(test)] mod tests block in the parent with '#[cfg(test)]\n#[path="_tests.rs"]\nmod tests;', then cargo fmt + cargo test --lib ::tests + clippy --all-targets. Because these mods are ungated/cfg(test)-only with no public-symbol rename, no pip rebuild and no citation sweep are needed. STATUS: campaign effectively exhausted below 20 lines. The >60-line tier was exhausted at iter 167 (67 splits; registry.rs last at 61 lines) — lower the survey threshold to >40 to find more. Survey blind spot: grepping 'mod tests' misses inline #[cfg(test)] modules with OTHER names; instead grep '#[cfg(test)]' then check the NEXT line for 'mod '. Iter 193 extracted the last 3 (interpreter/mod.rs::smc_tests, memory/page.rs::serde_tests, exploration/stepping.rs::sizes; commit 42a7e60f3) plus syscalls/exit.rs::exit_tests; after that no inline test module >20L remains.

forgotten 2026-08-05T04:34:40Z — Bead-closure scrap: the memory itself declares 'campaign effectively exhausted... no inline test module >20L remains' — a completed-refactor status snapshot rather than durable guidance, per criterion (c).

rust-mod-tests-tier-exhausted forgotten

rust-symex test-split fallback: as of iter 167 the >60-line 'mod tests' tier is EXHAUSTED (67 splits done, registry.rs was last at 61 lines). Re-survey returns zero files >60. To continue offline test-extraction, lower the threshold to >40 in the survey one-liner; otherwise the bd queue remains network/CI-blocked (epics kvn0/vx8p/ig3o/3gjm + iwmp). See rust-mod-tests-sibling-extraction for the recipe.

forgotten 2026-07-04T00:34:20.327815+00:00 — Status snapshot of the same campaign; threshold/exhaustion detail folded into the canonical recipe. (merged into rust-mod-tests-sibling-extraction)

rust-only-audit-2026-05-14 forgotten

Property-fuzzer audit of FAST_SUITE/MEDIUM_SUITE rust_only=True entries (angr-qz16, 2026-05-14, commit 507f3a449). PROMOTED to rust_only=False (10/10 trials pass exact normalized output match, mixed bfs/dfs): defcamp_r100 (bfs+dfs), ais3_crackme, google2016_unbreakable_0, strcpy_find, flareon2015_2, defcon2016quals_baby-re. KEPT rust_only=True (consistent exact-mismatch on every trial — these are legit Z3 model nondeterminism or rust empty-output cases): fauxware (Rust outputs empty, Python finds SOSNEAKY), unmapped_analysis, csgames2018, whitehatvn2015_re400, google2016_unbreakable_1 (also bimodal). MEDIUM non-bimodal: NONE promotable (all 4 candidates - flareon2015_5, ekopartyctf2016_rev250, csaw_wyvern, codegate_2017-angrybird - diverge exact-mismatch consistently). Fuzzer uses _normalize_output (suffix \xNN strip) so divergences here are pre-normalization mismatches in main content, not Z3 fill bytes.

forgotten 2026-06-04T16:31:56.875939+00:00 — Stale dated snapshot (≤May 2026)

rust-page-fetch-probe-technique remembered

Page-fetch probe technique (iter128): to find WHICH pages the Rust engine fetches from Python at steady state, temporarily wrap RustExplorationManager.cb_batch_fetch_pages with a collections.Counter keyed by (loader.find_object_containing(pa).binary_basename or 'NOOBJ', page_addr) plus an atexit dump to stderr, then run 'python tests/benchmarks/run_single.py --engine rust' (subprocess + RLIMIT_AS, memory-safe). On google2016_unbreakable_1 this showed ONE batch call of 7 pages, all NOOBJ = lazy stack region -- map_loader_pages already pre-populates every CLE-backed page at setup, so page fetches at run time are stack/heap misses, never loader misses. Read the ranking from gil_work_ns_callback counters (see zeropy-callback-site-lever-ranking), never from callback*_total_ns.

rust-proc-integration-test-rax-only remembered

TestSymbolicLibcProcedures (tests/engines/rust/test_procedures.py) integration tests assert ONLY on rax (the exported return value), not on guest-memory writes. Reading a native proc's destination buffer back via the post-run exported state's s.memory.load returns zeros even when the proc wrote correctly (stpncpy dst content read as \x00 while rax=dst+strlen was correct). The single-step export path does not reliably sync arbitrary written memory regions into the Python SimState for read-back, so memory-content assertions are flaky/wrong here — follow the existing strchr/memchr tests and assert on rax min/max only. Use this harness (_make_state hooks fauxware HOOK_ADDR 0x4008C0, BUF_ADDR 0x601100) to add dispatch/bridge coverage for newly-landed native procs.

rust-proxy-architecture-decision remembered

Decision: RustStateProxy over SimState sync (2026-03-29)

Problem: rust_manager.py (5,167 lines at the time) created full Python SimState objects for every callback and maintained 11 caches to sync state bidirectionally between Rust and Python. This was the source of most bugs (identity conflicts, constraint desync, empty stash exports, dual-solver problem).

Decision: Build a RustStateProxy that delegates to existing PyO3 bindings instead of creating/syncing full SimStates. Only create full SimState for SimProcedure execution (which genuinely needs angr plugins).

Why: Rust already exposes rich per-state access via PyO3:

  • mgr.get_state_register(state_id, name), mgr.get_state_memory(state_id, addr, size)
  • mgr.state_satisfiable(state_id), and per-state eval via mgr.fork_state_solver(state_id) -> RustSolverContext
  • mgr.fork_pending_solver() -> full RustSolverContext with Z3 access
  • Handle-based symbolic math (no claripy round-trips)

Most consumers (ExplorationTechniques, predicates, found-state users) only need to read registers, memory, and solve constraints — all available through PyO3.

How to apply:

  • RustStateProxy wraps a (rust_mgr, state_id) pair — O(1) creation, no sync
  • Sub-proxies: RustSolverProxy, RustRegisterProxy, RustMemoryProxy, RustHistoryProxy
  • For posix.dumps(1) (stdout): track in Rust-side per-state buffer
  • For posix.dumps(0) (stdin): evaluate stdin BVS variables under found-state constraints
  • Full SimState still needed for SimProcedure execution (angr plugins like posix, filesystem)
  • This eliminates: most of the 11 caches, bidirectional constraint sync, dual-solver bugs for non-SimProcedure paths

One Rust addition needed: fork_state_solver(state_id) — like fork_pending_solver() but for any state (now implemented; see fn fork_state_solver in exploration/state_api.rs + mod.rs). Pattern existed in fn export_state_constraints (was exploration.rs:2082 at the time; that file was split by the angr-zel8z god-module refactor, so it now lives in exploration/mod.rs — anchor on the symbol name, not the line).

rust-proxy-constraints-vs-exported-state remembered

Showcase vuln-finding demo (tests/benchmarks/show_vuln_finding.py, angr-4n26m.7): to display the PATH CONSTRAINTS that a Rust-engine explore() found to reach a sink, you MUST read them off the RustStateProxy at predicate-match time via proxy.solver.constraints (which calls RustExplorationManager.export_state_constraints(state_id) -> live Rust solver). The EXPORTED Python SimState in mgr.found has solver.constraints == [] (or just initial) by design — exported states intentionally do not pre-pin/carry Rust-side path constraints (see constraint-export-no-pre-pin); eval still returns the correct concrete value via the Rust solver fallback, which misleads you into thinking constraints are absent. A callable find= predicate on RustExplorationManager receives a RustStateProxy supporting .ip/.regs/.memory.load/.solver.eval/.solver.constraints, so the original strcpy_find/solve.py check() ports almost verbatim.

rust-proxy-copy-design remembered

RustStateProxy.copy() landed in angr-d1dr (2026-06-06, commit be05cf9f8) as a Rust-side CoW deep fork. Architecture: (1) Manager-side helper RustExplorationManager.fork_state_for_copy(source_id) -> new_id calls rust_mgr.fork_state_to_stash(source_id, '_copies') and mirrors Python-side metadata (options set, globals dict, stdout tracker entry) from source's current value (not lineage root) so caller-visible mutations carry over. (2) Proxy-side proxy.copy() invokes the manager helper, sets _owns_copy=True on the returned proxy, and wraps the new id in a fresh RustStateProxy. (3) Clones park in a dedicated _copies stash that step()/StashManager active iteration does not touch. Why _copies stash, not 'active': Veritesting/Spiller/ManualMergepoint call state.copy() to snapshot — they don't want the clone auto-stepping. GC (angr-yhe0, 2026-06-06, commit 4cd70b323): when the copy-proxy is GC'd, del calls python_mgr.drop_copy(state_id) which removes the state from _copies via Rust drop_state_from_stash and clears Python-side metadata. Eager cleanup also available by calling drop_copy(state_id) directly. See invariant-rust-proxy-copies-gc. Raises path: proxy constructed without python_mgr= (low-level unit tests against _RustExplorationManager directly) get NotImplementedError pointing at fork_state_to_stash as the workaround.

rust-proxy-find-perbyte-fallback remembered

RustMemoryProxy.find() (rust_state_proxy.py:~1440) does a single wide get_state_memory_ast(addr, haystack_size) to build the haystack. Rust state.memory_load returns Err on (1) any multi-page range, (2) any range mixing symbolic stores with lazy-region bytes, (3) cross-page boundaries. The Python proxy converts Err → None and previously used BVV(0, n*8) as a substitute haystack — silently wrong: strlen on a symbolic 5-byte buf with terminator at offset 5 would see all-zero, match offset 0, and return 0 instead of 5. Fix: per-byte get_state_memory_ast(addr+i, 1) loop with claripy.Concat(*reversed(byte_asts)) to restore LSB-first layout. The byte-by-byte path is slower but only fires when wide load fails. Pattern to keep in mind for other wide-load paths in the proxy (load(addr, large_size), etc.).

rust-proxy-symbolic-addr-witness-cache remembered

RustMemoryProxy symbolic-address STORE->LOAD witness consistency (angr-5rjbq, rust_state_proxy.py). The p1s02 symbolic-addr store fallback concretizes 'addr' via self._solver_ctx.eval(addr) and stores at that witness; a later load of the same symbolic addr independently concretized to a DIFFERENT witness -> Rust had no data there -> read back zeros (silent read-your-writes loss). FIX: _addr_witness_cache dict keyed by addr.hash() (NOT addr.cache_key — this claripy build's BV has no cache_key; use the .hash() method which is stable across structurally-identical ASTs). store() records witness; load() reuses it before asking the solver. Cache is snapshotted (dict copy) in copy() so forks inherit but don't share mutation. Only affects the gate-on p1s02 fallback path (unbounded/unconstrained symbolic addrs), a best-effort keep-alive path, so reusing the write witness is strictly safer than an independent read witness. 340 proxy/memory/callback tests pass. NOTE claripy AST structural key: .hash() method, not .cache_key attribute.

rust-proxy-writes-policy forgotten

Python writes to RustStateProxy must be implemented as write-through to Rust (the single source of truth). The existing 'Option A' deferral in docs/advanced-topics/rust_engine.rst:99-118 and the linked rust_proxy_writes_design.rst are SUPERSEDED — they framed the read-only proxy as the final design, but the policy is now: regs.=value and memory.store() should queue a mutation and apply it at the next safe boundary, mirroring the existing solver.add() write-through path in rust_callback_dispatch.py. Rust never reads back from a Python-side shadow store. How to apply: when proposing UX/proxy work, frame writes as 'implement write-through', not 'document the asymmetry'. Bench against baseline_timings.json with threshold 0.15 before merging.

forgotten 2026-06-04T20:54:23.914818+00:00 — status-shape

rust-python-boundary-audit remembered

Most Python bridge code MUST stay Python — don't try to move it to Rust.

Why: Rust can't construct Python objects (claripy.BVV), track Python object identity (id/hash), call Python plugins (SimProcedures, posix), or execute Python callables (ExplorationTechniques). The Python proxy wrappers exist because Rust returns raw values that need wrapping in claripy types.

How to apply:

  • Don't attempt to make proxy classes (#[pyclass]) in Rust — they need claripy.BVV construction
  • Don't move SymbolicIdentityTracker to Rust — it tracks Python id()/hash()
  • Don't move technique dispatch to Rust — techniques are Python callables
  • Instead focus on: (1) richer Rust export APIs that reduce Python reconstruction work, (2) reducing callback frequency via Rust-side caching, (3) exposing StateChanges tracking from Rust to eliminate Python-side diff computation
  • The irreducible Python core handles: SimProcedure callbacks, VEX lifting, constraint sync, plugin restoration, technique dispatch, callback setup, explore/step loops

rust-runresult-error-typed-routing remembered

RunResult::Error carries a typed RunErrorKind{Deadend,Fatal} (callbacks/events.rs) so exploration/stepping.rs routes structurally, NOT by message substrings. (This replaced an older stepping.rs heuristic that matched message.contains("No bytes in memory")/message.contains("lift"); that substring path no longer exists.) CbExecutionError::run_error_kind() (interpreter/mod.rs): LiftError->Deadend (unliftable block: the Python _cb_lift_block returns the literal '{}' empty-IRSB sentinel on SimEngineError/PyVEXError e.g. 'No bytes in memory'); all other Panic-strategy variants (Memory/Op/InvalidIR/UnknownTemp/Callback)->Fatal. stepping.rs RunResult::Error arm: Deadend->Deadended stash; Fatal+addr==0->Deadended (jump to 0x0 after clean exit); else Fatal->errored. KEY INVARIANT: get_or_lift_block (execution.rs) must check irsb_json.trim()=='{}' BEFORE deserialize and return LiftError for it; a real (non-sentinel) deserialize failure maps to InvalidIR so a genuinely malformed IRSB goes to errored, matching the InvalidIR sites in statements.rs. To exercise the typed-error contract directly (bypassing the deadend heuristic), call execute_irsb_for_test (engine.rs:89) which maps CbExecutionError::InvalidIR -> MalformedIRSB; cheapest InvalidIR trigger is Ist_LLSC with result=99 + tyenv only declaring t0 (statements.rs). Regression guards: TestCallableStepFunc::test_callable_with_rust_engine + test_strict_page_access_alone_does_not_block_nx_fetch.

rust-shared-lineage-solver-test-pattern remembered

De-vacuify pattern for the use_shared_lineage_solver kwarg tests (test_strategy_vex.py): the RustExplorationManager exposes NO per-state lineage getter (the a116s.12 audit's suggested mgr._get_rust_state(...).use_shared_lineage_solver() does not exist; the getter lives only on _RustSimState via PyO3 py_use_shared_lineage_solver, on the seed object that _add_rust_state builds locally and Rust forks internally — unreachable from a constructed manager). So you cannot directly observe the _add_rust_state gate (rust_manager.py, 'if self._use_shared_lineage_solver:'). Instead test BEHAVIORAL EQUIVALENCE: run an ON manager and an OFF manager on the same seed, explore(find=0x4006ED) on fauxware, and assert sorted(bytes(s.posix.dumps(0)) for s in mgr.found) byte-matches between runs (plus s.solver.satisfiable()). The SharedLineageSolver consumer code IS live now (snapshot_fork_ops.rs mints a fresh solver at fork time) so the old docstring claim 'flag is inert in this slice' is STALE. The on/off concretized-stdin match is deterministic across independent runs (Z3 model is stable for identical constraints) — not flaky in practice.

rust-silent-fallback-convention forgotten

Rust silent-fallback tagging convention (bd angr-qwyti.1): sites that discard an error/absent value and continue with a degraded result carry a '// SILENT(cat-a|b|c): ' comment above them (cat-a=expected control flow, cat-b=fallback with loss, cat-c=wrong-answer risk MUST also log::warn!). Audit via tools/audit_silent_fallback.py (fn scan/SHAPES): non-gating, detects 'return Ok(None)' + '.ok();' swallow only (unwrap_or/let _ = deliberately OUT of scope for FP reasons), checks a +-20/-12 line window for the tag, reports only sites new since tools/silent_fallback_baseline.txt. Baseline keys are (relpath,shape,stripped-code) so unrelated edits don't churn. Regenerate: --update-baseline. Ports the Python bare-except-cat-x-convention to native/. NOT wired as CI gate yet.

forgotten 2026-08-03T05:45:12Z — CLAUDE.md now has a full 'Silent-fallback tagging (Rust)' section covering the SILENT(cat-a|b|c) comment convention, tools/audit_silent_fallback.py, tools/silent_fallback_baseline.txt, and which shapes are/aren't auto-detected -- duplicating nearly all of this memory's content. Worse, the memory's own key claim 'NOT wired as CI gate yet' is now stale/wrong: CLAUDE.md states the script 'gates the currently-narrow return Ok(None) / .ok(); shapes in CI (rust_check)'. Superseded and partially incorrect; forget per rule (b)/(d).

rust-simproc-dispatch-name-helper forgotten

angr-gbk6 (2026-06-02): Rust SimProc dispatch now keys off display_name not class name. Helper _simproc_dispatch_name(proc) in angr/exploration/rust_callback_dispatch.py:18 used in three places: rust_manager.py:_register_simprocedures, rust_callback_dispatch.py:_find_simprocedure name-fallback loop, and the continuation-hook pre-registration site. SimLibrary instantiates ReturnUnconstrained(display_name='setenv') etc. for unimplemented libc symbols; without this fix the native registry (keyed on 'setenv') never matched. Backwards compatible: SimProcedure.init defaults display_name to type(self).name, so first-class procs (Strlen, Malloc, etc.) keep the same name. Tests: test_simproc_dispatch_name_prefers_display_name + test_register_simprocedures_uses_display_name_for_stubs in TestHooksAndProcedures (4099-...). Integration test uses monkeypatch with a wrapper that delegates via getattr — cannot directly monkeypatch _rust_mgr.register_simprocedures because PyO3 methods are read-only on the object.

forgotten 2026-06-04T21:30:05.991798+00:00 — Implementation receipt for angr-gbk6 _simproc_dispatch_name helper; code in rust_callback_dispatch.py:18 + tests are self-documenting.

rust-so-artifact-path-is-repo-root-target remembered

The release cargo artifact path in this repo is repo-root target/release/librustylib.so, NOT native/angr/target/release/ — the workspace uses a shared target dir. The rebuild-with-libvex-ffi-recipe memory and iter119/120 session notes say native/angr/target/; that is wrong (cp silently fails with 'cannot stat' and you then test a STALE .so while libvex_ffi_enabled() still returns True from the old build). Always verify the cp printed COPIED before running pytest.

rust-solver-authority forgotten

ARCHITECTURE: Rust solver is single source of truth. Python never holds authoritative constraints. When Python UNSAT, monkey-patch state.solver.eval to delegate to Rust solver.

forgotten 2026-06-04T21:30:06.342824+00:00 — Trivially short (one-sentence) and the principle is fully covered by rust-proxy-architecture-decision and rust-state-proxy-capabilities. Stale: 'Python never holds authoritative constraints' is the same point as write-through ownership.

rust-solver-primary-eval forgotten

eval_with_fallback now tries Rust solver FIRST, falls back to Python SolverComposite. This avoids 56s SolverComposite overhead for LAZY_SOLVES states. Also added byte-level decomposition for wide BVS (>64 bit): Extract each byte, eval individually, reassemble. Rust ctx.eval(full_160bit_BVS) returns None but per-byte works.

forgotten 2026-06-04T21:30:06.700579+00:00 — Implementation receipt for eval_with_fallback ordering + byte-level decomposition — code is the answer; superseded by constraint-export-no-pre-pin guidance on Rust-solver fallback.

rust-solver-proxy-base remembered

RustSolverProxyBase (angr/exploration/rust_state_proxy.py) is the shared solver-query core for RustSolverProxy (standalone state.solver read proxy) and RustSolverProxyPlugin (SimProc-callback state.solver). The ONLY thing subclasses differ on is fork strategy, expressed via two hooks: _query_ctx() returns the ctx to query (forking on first use), _forked_ctx() returns the already-forked ctx or None (for timeout setter propagation). Standalone forks lazily into self._solver_ctx via _ensure_solver; plugin caches in self._rust_ctx_cache (invalidated on add()). Base owns satisfiable/min/max/symbolic/constraints/timeout/_cast_result. eval/eval_upto/is_true/is_false/solution stay PER-SUBCLASS because plugin unwraps SimActionObject constraints + shortcuts concrete BoolV + munges eval's (expr,cast_to) signature; standalone does none of that — do NOT try to unify them. min_int/max_int are aliases assigned ONLY on the plugin (min_int=RustSolverProxyBase.min) so RustSolverProxy's public surface stays unchanged for test_rust_public_api inventory; adding them to the base would trip the public-surface-drift gate.

rust-solver-unsat-semantics forgotten

RustSolverContext UNSAT semantics (verified 2026-05-05): on contradictory constraints (x>100 AND x<50), satisfiable() returns False, and eval(x), min(x), max(x) all return None, eval_upto returns []. This is the correct behavior consistent with the satisfiable-wrong-answer invariant (False = definitive UNSAT). Tests in test_rust_exploration.py: test_contradictory_constraints_make_unsat (TestSolverOperations) and test_unsat_state_pruned_during_step (TestErrorRecovery).

forgotten 2026-06-05T17:23:05.265619+00:00 — Date-stamped 2026-05-05 verification of behavior covered by tests in test_rust_exploration.py (test_contradictory_constraints_make_unsat). The durable 'False = definitive UNSAT' rule lives in satisfiable-wrong-answer.

rust-stash-assignment-via-move-state remembered

Rust stash assignment (simgr.stashes[name]=[...]) is implementable purely in Python via existing FFI: state_stash(sid) gives current stash, move_state(sid,from,to) with from==to removes+re-appends (so iterating desired ids in order rebuilds a stash in exact order), drop_state_from_stash(sid,stash) frees omitted states. No new Rust FFI or SimState materialization needed. Implemented in _StashDict.setitem (rust_state_proxy.py). Validate proxies: isinstance RustStateProxy, _mgr is same manager, state_stash not None; else raise.

rust-state-depth-via-proxies remembered

RustExplorationManager state-depth measurement: full SimStates from mgr.active have empty history (exported state). To measure block-history depth, use mgr.active_proxies() / mgr.deadended_proxies() which return RustStateProxy wrappers; their .history.bbl_addrs reads the real per-state lineage tail via get_state_bbl_history_tail FFI (caps at 256 entries by default). Used 2026-06-06 in deep_loop_search/run_one.py to characterize BFS vs DFS depth reached.

rust-state-proxy-capabilities forgotten

RustStateProxy capabilities + known holes. Canonical reference for what the proxy at angr/exploration/rust_state_proxy.py supports today, what is intentionally read-only, and what routes to Python fallback.

Contract. Live mutable view (per angr-j28e, commit 6e4e517bc, 2026-06-02 — proxy is a mutable view over Rust state with immediate FFI write-through, no Python-side shadow store). Lazy materialization in stash properties. Lightweight bbl-tail FFI for RustHistoryProxy. Proxy-returning stash accessors.

Write-through model (supersedes diff-and-push). User directive 2026-06-02: Rust owns everything during exploration; Python's job is to update state IN Rust, not keep parallel copies. SimProcedures use a write-through model — when a Python SimProc writes to memory/regs/constraints, those writes land on the Rust side as they happen (via RustStateProxy plugins for SimMemory/SimRegisters/SimSolver/CallStack). A batched write-back cache within the proxy is allowed as a performance optimization, but the model is write-through, not diff-and-push. Once we drop out to Python for a SimProc we're already paying GIL+PyO3 cost, so the <0.2%-wall per-callback diff-push saving from prior Option C was the wrong metric. Reopen criteria from the prior verdict NO LONGER APPLY — the decision is architectural, not performance-driven.

Exposed plugins. state.options, state.globals, state.heap (with mmap_base, allocations, freed), state.callstack, state.inspect (raises NotImplementedError per docs/advanced-topics/rust_engine.rst). state.options/state.globals are Python-side per-state-id dicts on RustExplorationManager (_py_state_options, _py_state_globals). state.heap.mmap_base/allocations/freed read live from the Rust state via PyO3 accessors.

Write-through paths (angr-j28e). proxy.regs.<name> = value and proxy.memory.store(int_addr, data) are immediate write-through via set_state_register_symbolic_ast / set_state_memory_concrete / set_state_memory_ast; symbolic-address memory writes refuse loudly with proj.hook workaround pointer. proxy.solver.add(...) writes through to a forked Rust solver context. Symbolic-AST writes route through the shared cache so identity is preserved for subsequent solver.add(reg == K) constraints. RustRegisterProxy.setattr guards underscore-prefixed names with object.setattr so internal _mgr/_state_id/_cache/_arch don't recurse through the FFI write.

SimOption honoring. The Rust engine does NOT honor most SimOptions — only LAZY_SOLVES and STRICT_PAGE_ACCESS are mirrored onto the Rust state via _apply_state_metadata / set_enforce_permissions. Writes to proxy.options.add(X) for any other option are visible to Python callers but ignored by the Rust interpreter — intentional (matches no-mock semantics: live mutable storage), not a silent no-op. Full matrix in docs/advanced-topics/rust_engine.rst.

Known holes routing to Python fallback. state.libc not exposed; state.scratch not exposed (tracked by angr-jco0); symbolic-address memory store on proxy raises NotImplementedError pointing at proj.hook; RustStateProxy.addr returns 0 on PC lookup failure as fallback — WRONG-ANSWER RISK for find predicates that include addr 0. Logs at warn since commit 97e94a8bd. Same pattern in RustPosixProxy._eval_stdin (returns b'' on solver error, warn-level).

Affected paths to remove under write-through rule: rust_state_export.py:715-747 (memory writeback), 618-662 (callstack reconstruction), 812-936 (parallel claripy solver fallback), 2906-2948 (triple-pass constraint sync at init); rust_callback_dispatch.py:536-544, 1324-1381 (SimProc fork via Python instead of Rust). Tracking via boundary-rust-state-proxy-owns-memory-and-regs epic.

How to apply: sole reference for 'can the proxy do X?' decisions; update this memory (not new ones) when capabilities change.

forgotten 2026-08-05T04:34:40Z — Stale and now factually wrong on its two headline claims: docs/advanced-topics/rust_engine.rst now documents 'Full state.inspect dispatch... to all state.inspect events' (as of angr-4aach), contradicting the memory's claim that state.inspect 'raises NotImplementedError'; and the honored-SimOption list in the same doc (LAZY_SOLVES, ZERO_FILL_UNCONSTRAINED_MEMORY, APPROXIMATE_MEMORY_INDICES, SYMBOLIC_WRITE_ADDRESSES, STRICT_PAGE_ACCESS, ENABLE_NX, NO_IP_CONCRETIZATION, NO_SYMBOLIC_JUMP_RESOLUTION, NO_SYMBOLIC_SYSCALL_RESOLUTION, KEEP_IP_SYMBOLIC) is far larger than the memory's claimed LAZY_SOLVES/STRICT_PAGE_ACCESS-only list. This 'canonical reference' memory was not kept in sync and is now actively misleading; the live doc is the correct authoritative source per rust-engine-claude-md-pointer.

rust-state-proxy-write-through forgotten

RustStateProxy is now a live, mutable view (angr-j28e, 2026-06-02, commit 6e4e517bc). proxy.regs.=value and proxy.memory.store(int_addr, data) are immediate write-through to Rust via set_state_register_symbolic_ast / set_state_memory_concrete / set_state_memory_ast. There is no Python-side shadow store; Rust remains the single source of truth. Symbolic-address memory writes refuse with NotImplementedError pointing at proj.hook (lazy Multi-cell symbolic-address path needs solver coordination the proxy lacks). RustRegisterProxy.setattr guards underscore-prefixed names with object.setattr so internal _mgr/_state_id/_cache/_arch don't recurse through the FFI write. Symbolic AST writes round-trip through the shared cache so subsequent proxy.solver.add(reg == K) lands on the live Z3 symbol Rust is tracking (angr-4pm1 invariant carried forward to the write path). Doc: docs/advanced-topics/rust_engine.rst 'RustStateProxy write-through contract' section. The old read-only docs (and docs/advanced-topics/rust_proxy_writes_design.rst proposed-proxy section) are superseded; the diff-and-push path in that doc still governs the SimProcedure callback boundary.

forgotten 2026-06-04T21:58:51.710249+00:00 — Subset of rust-state-proxy-capabilities — both describe the live mutable view write-through contract. Capabilities memory already cites angr-j28e and the same set_state_* write-through APIs. (merged into rust-state-proxy-capabilities)

rust-strrchr-strset-pattern forgotten

Native strrchr (procedures/strchr.rs::NativeStrrchr) and strpbrk/strspn/strcspn (procedures/strset.rs) shipped in commit 5dd503476 (angr-f16h.5). Patterns:

  • strrchr reuses the strchr concrete-fast-path + symbolic-ITE-chain pattern, but the ITE chain is built FORWARD (not backward like strchr) so later matches override earlier ones. A 'seed' carries the last concrete match found pre-symbolic-byte; bytes past concrete null are dropped from the chain (already-stopped scan).
  • strpbrk/strspn/strcspn share a 256-bit byte-set lookup table built from the second arg via shared concrete-only scan_concrete_until_null. All three are concrete-only — symbolic args fall back to Python via SymbolicArgument.
  • 16 + 12 = 28 Rust unit tests plus 5 fauxware-stub integration tests in TestNativeExtendedStringProcedures. The integration test pattern matches TestNativeStringToNumericProcedures: hook stub at HOOK_ADDR=0x600e30 (fauxware .ctors), set rdi/rsi, single-step, check native_procedure_stats() call_counts and rax.
  • angr has NO Python SimProcedure for strrchr/strpbrk/strspn/strcspn — they're declared in glibc.json only. The native registry is the only handler.

forgotten 2026-06-04T21:30:07.402877+00:00 — Implementation receipt for angr-f16h.5 (commit 5dd503476) describing strrchr/strpbrk/strspn/strcspn shipping and their test layout. Code + tests are self-documenting; pattern matches general native-simproc-empirical-gate.

rust-symex-moonshots-portfolio forgotten

Rust-symex moonshots portfolio (peer-reviewed v2): 6 spike-gated moonshots + 2 shared enablers. Blueprint doc: /home/ubuntu/.claude/plans/we-have-started-thinking-smooth-valley.md. Tracked under epic angr-op0dn with 8 children — enablers E1 (successors()/step_state() proxy dispatch, angr-op0dn.1) + E2 (libVEX-FFI native lift wiring, removes 86% callbacks, angr-op0dn.2); lead spikes S1/M1 crack-the-Z3-wall (op0dn.3), S3/M2 deterministic symex (op0dn.4), S6/M3 directed-search LEAD (op0dn.5, blocked on angr-75mc real target), S-C1/M4 hybrid concolic icicle+fuzzer+symex — strongest add, 2/3 bricks built (op0dn.6), S7/M5 parallel find-all (op0dn.7, blocked on angr-75mc + E2), S10/M6 flip-the-default parity census (op0dn.8). Framing: engine is no longer the bottleneck anywhere — attack the Z3 check()/eval floor and how OFTEN it's called, not the interpreter. Every moonshot spike-gated: no impl commits before a GO. Critical path: E1+E2+M2 first -> M4 concolic & M3 directed -> M5 parallel -> M6 flip; M1 runs opportunistically (gates nothing). GO gates ranked on structural counts (z3_check_count/saved-check/callback), NOT wall-time. Corrected discipline: no CV<0.1 determinism gate (restart-heuristic floor is permanent); resume is search-continuity not byte-identity; checkpoint/resume + LibAFL fuzzer + icicle already ship in-tree (do not re-scope greenfield).

forgotten 2026-07-20T05:01:46.596254+00:00 — Roadmap-status cluster; bead routing/statuses superseded by the 2026-07-18 housekeeping; durable framing (Z3-floor focus, structural-count GO gates, in-tree bricks) folded into canonical (merged into roadmap-2026-07-18-housekeeping)

rust-technique-dispatch-step-hook-added forgotten

Rust manager (angr/exploration/rust_techniques.py) now dispatches setup/filter/STEP/complete hooks via HookSet LIFO composition (angr-rqvq, 2026-06-03). step() runs once per Rust batch through dispatch_step_with_hooks() — base impl on RustSimulationManagerProxy advances Rust by one batch via _step_callback wired by the manager. successors() and step_state() still raise NotImplementedError from the proxy (would require Python-side re-run incompatible with Rust engine). Native-tech names (DFS, BFS, Explorer, LengthLimiter, Timeout, CheckUniqueness) are SKIPPED in step dispatch to avoid double-stepping. use_technique() warns at registration when successors()/step_state() are overridden. Techniques newly-functional: MemoryWatcher, Spiller (broken at copy), Director (partial — stash-assignment lost), DrillerCore, ManualMergepoint (broken at copy), StubStasher, StochasticSearch (partial), Suggestions (heavy history access), Tracer (broken at writes). LoopSeer/LocalLoopSeer/Slicecutor/Bucketizer still no-op (successors-based). Supersedes rust-technique-dispatch-filter-complete-only.

forgotten 2026-06-04T16:31:57.211993+00:00 — Bead/commit closure note

rust-test-fork-mmap-pyinit remembered

Pre-existing test failure: native/angr/src/syscalls/mmap.rs::tests::fork_preserves_mmap_base panics with 'The Python interpreter is not initialized' under plain 'cargo test --release'. This is NOT a regression from any code change; it's a pre-existing issue because state.fork() touches PyO3 paths and the Rust unit-test runner doesn't initialize the Python interpreter. Confirmed by 'git stash && cargo test fork_preserves_mmap_base' on a clean tree at HEAD~1 (2026-06-01). Filing an issue is out of scope; just don't read it as a CI signal. Other 22 mmap tests pass.

rust-test-harness-no-shared-z3-context remembered

The bare RustSimState + _RustExplorationManager unit-test harness (tests/engines/rust/) does NOT wire up a shared claripy<->Rust z3 context. Consequences when writing tests: (1) constraints added via the raw-Z3-pointer fast path (_add_constraints_to_state / _add_constraints_to_pending, which do backends.z3.convert(item).as_ast().value then add_constraint_raw*) land in a z3 context DISCONNECTED from the manager's, so they never affect the state — borrow_pending_solver().satisfiable() and export_pending_constraints() do NOT reflect them (a concrete '1 != 1' reads SAT here). (2) Cross-context asserts of unshared pointers can SEGFAULT (observed with claripy FP asts fpGT/fpLT, which the bridge can't convert so they hit add_constraint_raw). set_register_symbolic(ptr) works for exploration because it only maps the pointer into the symbol table, not the solver. Implication: solver-level constraint-binding / satisfiability soundness of the raw-pointer fast paths CANNOT be verified from this harness; that needs the shared-context fixture scoped by angr-ph300.3.1. Verify such fixes by mirroring the production-tested twin (_add_constraints_to_state) instead.

rust-test-memory-store-needs-map remembered

In Rust unit tests, RustSimState::memory_store (state/memory.rs) does NOT auto-map: storing to an unmapped page returns MemoryError::Unmapped despite the doc-comment mentioning an 'auto-mapping store'. Call map_memory(addr,size,perm) / map_memory_data first. Pattern used in state_tests.rs export_full tests (angr-n0irt.17): map_memory then memory_store a RustBV::symbolic to populate page symbolic_offsets. build_populated_state() (state_tests.rs, cfg vex-engine-z3) is the canonical fully-populated fixture for export/snapshot tests.

rust-tests-conftest-shared remembered

tests/engines/conftest.py (added angr-7gdp) holds the shared Rust-engine test boilerplate: RUST_EXPLORATION_AVAILABLE guard, TEST_BINARIES_DIR/EXAMPLES_DIR path resolution, module-scoped fauxware_project fixture, and BufferedStringIO (re-exported from tests/benchmarks/test_utils). Pattern: conftest attempts the broadest import once; on ImportError every Rust symbol is set to None and the flag False, so the per-module skipif/pytestmark skips before any None is dereferenced. Test files do 'from tests.engines.conftest import ...' (absolute import works under pytest prepend importmode since tests/engines has init.py). KEY SUBTLETY: test_hooks_rust.py keeps its OWN function-scoped fauxware_project override because hooks mutate the project; the conftest fixture is module-scoped (read-only sharing) and must not be used where state is mutated.

rust-unit-test-free-fn-not-manager remembered

Testing exploration/ scratch-thread or channel helpers at the Rust unit level: drive the free function (e.g. spawn_shadow_probe_thread in shadow_probe.rs), NOT the RustExplorationManager method — the manager needs a live Python interpreter to construct, so its branches are only reachable from pytest. Free-fn tests cover the real risk (deserialize failure, channel-teardown break) with no PyO3 setup. shadow_probe_tests.rs is the pattern; it also needs an inner #![allow(clippy::unwrap_used, clippy::expect_used)] because shadow_probe.rs's file-level #![deny] extends into the #[path]-included tests submodule.

rust-veritesting-silently-unsupported forgotten

Rust manager silently no-ops simulation_manager(state, veritesting=True). The veritesting=True kwarg is accepted (passed via **kwargs to the patched_simulation_manager in run_single.py) but RustExplorationManager ignores it: no merging/static-analysis fast-path, explore() runs with the regular BFS/DFS strategy. Observed effect on cmu_binary_bomb's solve_flag_3 (2026-06-03): Python returns 8 unique solutions to multi-int phase, Rust returns empty list ([]). This isn't a runtime error — Rust just returns from explore() without finding the target via the merged paths Veritesting would have enumerated. If a real workload needs veritesting, the bench will under-report Rust's correctness work. Bench-side workaround: skip flag_3 (used in cmu_binary_bomb_partial wrapper). Engine-side: file a follow-up if veritesting becomes a real workload requirement.

forgotten 2026-06-04T20:54:24.254952+00:00 — status-shape

rust-write-through-supersedes-diff-push forgotten

WRITE-THROUGH MODEL SUPERSEDES proxy-writes-design-verdict (2026-06-02). User directive: Rust owns everything during exploration; Python's job is to update state IN Rust, not keep parallel copies. SimProcedures use a write-through model — when a Python SimProc writes to memory/regs/constraints, those writes land on the Rust side as they happen (via RustStateProxy plugins for SimMemory/SimRegisters/SimSolver/CallStack). A batched write-back cache within the proxy is allowed as a performance optimization, but the model is write-through, not diff-and-push. User rationale: once we drop out to Python for a SimProc we're already paying a large GIL+PyO3 cost, so the <0.2%-wall per-callback diff-push saving from Option C was the wrong metric. Reopen criteria from the prior verdict (callback density >10K/bench, angr-2k64 revival, residual mma/hackcon localized to diff-push) NO LONGER APPLY — the decision is now architectural, not performance-driven. Affected paths to remove under this rule: rust_state_export.py:715-747 (memory writeback), 618-662 (callstack reconstruction), 812-936 (parallel claripy solver fallback), 2906-2948 (triple-pass constraint sync at init); rust_callback_dispatch.py:536-544, 1324-1381 (SimProc fork via Python instead of Rust). Tracking work via boundary-rust-state-proxy-owns-memory-and-regs epic + 4 sub-beads.

forgotten 2026-06-04T21:58:52.065198+00:00 — Same write-through architectural directive as rust-state-proxy-write-through; folded into the canonical capabilities memory with the affected-paths list preserved. (merged into rust-state-proxy-capabilities)

rust-z3-sharing-doc-location forgotten

Rust↔Python shared Z3 context docs live at docs/advanced-topics/rust_z3_sharing.rst (angr-gcfa, commit f2b03df50). Covers: (a) build.rs runpath wiring + Z3_LIBRARY_PATH_OVERRIDE escape hatch; (b) SymContext::fork freezing local→shared Arc and lazy solver materialization (Mutex<Option>); (c) two distinct push/pop systems on a SymContext — transactional push_level vs scope_savepoint, dispatched on lineage None/Some; (d) bare_z3_push_depth as the fork-time SharedLineageSolver materialization gate; (e) get_solver_stats() counter cheat-sheet (z3_check_, z3_materialize_, z3_ast_build, z3_assume_, z3_branch_, z3_site_*). Cross-linked from rust_engine.rst Z3 solver API section. Sibling of rust_engine.rst, rust_lazy_memory_design.rst, rust_parallel_design.rst, rust_proxy_writes_design.rst, rust_bimodal_variance.rst in docs/advanced-topics/index.rst.