rust
28 remembered, 41 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-agents-md-pointer
remembered
Pointer, not a snapshot
Do not keep architecture summaries, counts, speedups or test results in memory; they rot. Read the live documents: repo-root AGENTS.md (workflow, hygiene rules, the static-gates table, and the "Where context lives" table that maps every topic to its doc and its bd entry-point key), docs/advanced-topics/rust_engine.rst (using the engine: SimOption matrix, inspect dispatch, techniques, errors, API stability), rust_engine_decisions.rst (rejected alternatives), rust_build.rst (build, tests, bench gates, profiling, logging). Live numbers: tests/benchmarks/baseline_timings.json and grep -rc 'def test_' tests/engines/rust/.
What stays in memory
Only context that is genuinely stable: design rationale (core-goal-design-philosophy, rust-proxy-architecture-decision), lessons learned (constraint-export-no-pre-pin, characterization-vs-fix-pattern, rust-python-boundary-audit), and the sync checklists docs cannot enforce (benchmark-bimodal-variance-rules).
rust-engine-claude-md-pointer
forgotten
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-naming-convention
forgotten
Sibling test-module naming convention: the recipe in rust-mod-tests-sibling-extraction originally emitted 'mod tests;' as the identifier, which left a directory-wide inconsistency. Canonical form is now '#[cfg(test)]\n#[path = "_tests.rs"]\nmod _tests;' — identifier matches the sibling file name, and the #[path] attribute is kept even though it is redundant (all 22 files in native/angr/src/syscalls/ do this). Rationale: grepping for the owning module name finds the parent file. angr-9ke6b.159 (commit 6f725cb68) fixed the last 7 stragglers in syscalls/ (exit, munmap, concurrency, identity, sigaction, memory_extras, signals). Nothing references these mods by path, so the rename is safe and test-only; no pip rebuild needed.
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-module-root-split-recipe
remembered
Recipe for splitting a large Rust module ROOT (mod.rs) into sibling files, from angr-9ke6b.91 (interpreter/mod.rs into execution_stats.rs / execution_error.rs / block_result.rs): (1) slice the spans out, (2) give each new file its own use header — the parent's imports do NOT follow, (3) re-export from mod.rs (pub(crate) use execution_error::{...}) so every crate::interpreter::X path is unchanged; a plain private use child::{PrivateEnum} in the root is enough for descendant modules referring to it as super::PrivateEnum, so an item that was file-private can stay pub(super) rather than being widened to pub(crate). SIBLING TEST FILES (the <name>_tests.rs convention used across native/angr/src): to move an inline #[cfg(test)] mod tests { ... } block out of <name>.rs, write <name>_tests.rs starting with use super::*; and replace the whole block in the parent with #[cfg(test)]\n#[path = "<name>_tests.rs"]\nmod tests;, then cargo fmt + cargo test --lib ::tests + clippy --all-targets. A test-only sibling extraction renames no public symbol, so it needs no citation sweep. GOTCHA that clippy --all-targets catches but plain cargo check does not: the sibling #[path] mod tests files use use super::* and silently depend on imports in the ROOT's use block. Removing a now-unused root import (here crate::callbacks::RunErrorKind, dropped because the only user moved to execution_error.rs) breaks execution_tests.rs — add the explicit import to the test file. Unlike sibling test extraction, a root split DOES need a citation sweep: bd memories and in-source/docs comments cite the old interpreter/mod.rs path for the moved symbols even though the Rust import paths are unchanged.
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
Problem: rust_manager.py created full Python SimState objects for every callback and maintained a dozen 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 — get_state_register / get_state_memory (manager_methods_export.rs), state_satisfiable, per-state eval via fork_state_solver(state_id) -> RustSolverContext, fork_pending_solver(), and handle-based symbolic math (no claripy round-trips). Most consumers (ExplorationTechniques, predicates, found-state users) only need to read registers, memory, and solve constraints.
How to apply:
RustStateProxywraps a(rust_mgr, state_id)pair — O(1) creation, no sync- Sub-proxies:
RustSolverProxy,RustRegisterProxy,RustMemoryProxy,RustHistoryProxy posix.dumps(1)(stdout): Rust-side per-state buffer;posix.dumps(0)(stdin): evaluate stdin BVS variables under found-state constraints- Full SimState still needed for SimProcedure execution; proxy writes are write-through (docs/advanced-topics/rust_engine.rst "RustStateProxy write-through contract"; design rationale in rust_proxy_writes_design.rst)
fork_state_solver and export_state_constraints are #[pymethods] in exploration/manager_methods_constraints.rs (bodies in state_api.rs); anchor on the symbol names, not lines.
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
forgotten
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/export.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/helpers.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.