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.
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.
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.
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.
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).
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 rulesrust-python-boundary-audit— where the Python↔Rust line should sitconstraint-export-no-pre-pin— pre-pinning is dangerous; Rust solver eval fallback is the clean fixcharacterization-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.
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.
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.
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).
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).
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.
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.
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.)
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').
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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 viamgr.fork_state_solver(state_id)-> RustSolverContextmgr.fork_pending_solver()-> fullRustSolverContextwith 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:
RustStateProxywraps 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.
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-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.
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.
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.
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.
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).
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.
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.
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.
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).
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.
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-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-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.
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.
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.