catch / 3
77 remembered, 165 forgotten in this chunk.
gate-cow-fork-scaling-variance
forgotten
The ralph clean/dirty gate (run_regression.py --rust-only --skip-bimodal --threshold 0.15 --retry-failures 2) can spuriously fail cow_fork_scaling with a consistent +19-22% regression across all 3 retries when the box is under load (e.g. the ralph systemd scope + Z3 memory pressure). Iter31 failed this way; at HEAD a fresh subprocess (run_single.py cow_fork_scaling --engine rust) measured 2.29-2.30s = baseline 2.28s, and re-running the exact gate command passed 20/20. cow_fork_scaling is a fork-scaling synthetic (run_regression.py SUITE @ ~line 191, prebuilt x86_64 in synthetic_examples/) and is timing-sensitive like the bimodal benches even though it is not in BIMODAL_BENCHMARKS. Before chasing a cow_fork_scaling gate regression: reproduce in a clean subprocess first; if it is at baseline there, the gate failure was load variance, not a real regression.
gate-fork-state-to-stash-test-pattern
forgotten
angr-t3mr (write-through boundary): adding a Rust-owned fork API exposed via PyO3 must NOT enable Python-side monkey-patching of the Rust pyclass method. PyO3 0.27 pyclass methods are read-only on the Python object — raises 'attribute is read-only'. Test dispatcher routing by patching the Python-side hook (mgr._add_rust_state for off-path; observe stash count delta for on-path). Gate plumbing matches the established 4-step pattern: kwarg with default None → env var fallback → bool attribute on manager → getattr(self, 'use*', False) in mixin.
gate-noise-floor-15pct
forgotten
The 15% gate threshold on tests/benchmarks/run_regression.py --skip-bimodal is tight for sub-second benches: a 0.79s baseline only buffers 0.118s before failing. Single-sample wall-clock variance routinely hits +5-15% in the full 15-bench sequential gate (much more than in isolation). When a 'gate red' commit looks unrelated to perf, check: (1) is the failing bench close to its baseline? (2) does its isolated 10-sample median still match baseline? If isolated median has drifted, refresh baseline rather than hunting for a code fault. See 868be2609 for one such refresh.
gate-on-fork-storm-root-cause
forgotten
The prefer_native_library_hooks gate-on fork storm on xmllint (angr-sgcye/angr-z21g0) is RESOLVED as of 2026-07-14, commit 71314609e. Root cause was apply_state_metadata dropping ZERO_FILL_UNCONSTRAINED* across the init cache — see invariant-apply-state-metadata-option-allowlist. Both original hypotheses (native string procs under-constraining; native malloc fill policy) were disproved. Post-fix, gate-on xmllint 220-step no-find: 1 active state / 359MB, matching Python (1 active / 272MB) and gate-off Rust.
gate-runner-smoke-bench-drift-gap
forgotten
gate-runner consistently hits 0.38-0.39s on sub-second synthetic smoke benches (mips32_le_branch, mips64_be_branch, android_arm_license_validation) when local 5-sample median is 0.33s — 15-20% env-drift gap. The smoke benches are rust_only with no python_time, so a baseline bump is pure noise-floor calibration, not perf concealment. Refresh tactic: new_baseline = gate_max / 1.15 + ~5% safety. See angr-zzfk (iter 78, 3 benches) and angr-5r0s (iter 79, mips32) for precedents.
getenv-heap-alloc
forgotten
Native getenv() heap-allocates a buffer for the value string via state.heap_alloc(len+1), writes value bytes + NUL, returns pointer. The heap region (0xC0000000+) must be mapped before getenv is called — this is handled by Python state sync which maps heap pages. If the heap isn't mapped, memory_store will fail silently (no page mapped). Environment data lives in HashMap<Vec, Vec> on RustSimState, cloned on all 5 fork methods and merge.
gffd-root-cause
forgotten
ROOT CAUSE of silent symbolic syscall dispatch (angr-gffd, 2026-05-17): CallbackInterpreter::get_syscall_num in native/angr/src/interpreter_cb/exits.rs read the syscall register then called .as_u64().unwrap_or(0). For a symbolic register .as_u64 returns None and unwrap_or pinned the dispatch number to 0. On amd64 syscall 0 is read (NativeReadSyscall), so any symbolic rax at a syscall instruction silently invoked read() with whatever happened to be in rdi/rsi/rdx — completely bypassing Python's _resolve_syscall enumeration and NO_SYMBOLIC_SYSCALL_RESOLUTION logic. Fix: thread Option through BlockResult::Syscall, RunResult::Syscall, CallbackReason::Syscall, and stepping.rs::RunResult::Syscall (the latter uses num.and_then(|n| self.native_syscalls.get(arch, n)) so symbolic syscalls skip native dispatch entirely and force the Python callback).
gfyl-python-time-backfilled-2026-05-09
forgotten
Backfilled python_time for all 20 missing entries in tests/benchmarks/baseline_timings.json (commit a571d4634, 2026-05-09). Notable speedups vs Python: csaw_wyvern 16.92x (Py 15.9s, Rust 0.94s), ekopartyctf2016_rev250 15.65x (Py 31.65s), flareon2015_5 10.27x (Py 57.16s), defcamp_r100__dfs 4.51x, ais3_crackme 2.95x. Known Rust-slower benches surfaced as SLA WARN (not FAIL): google2016_unbreakable_1 0.56x (bimodal — saw a one-off SLA FAIL at 0.48x in slow-mode), ekopartyctf2016_sokohashv2 0.58x, mma_howtouse 0.65x, hackcon2016_angry-reverser 0.88x, fauxware 0.99x. The new tests/benchmarks/backfill_python_time.py is reusable for future drift. To re-run: PYTHONPATH=/home/ubuntu/repos/angr python tests/benchmarks/backfill_python_time.py [--only NAME] [--force].
gil-strategy-decision
remembered
GIL-strategy decision for parallel exploration (angr-1ilq.7, 2026-06-29): chose Strategy A (lazy-GIL step), rejected Strategy B (allow_threads around Z3 solve only). Method: depth-guarded thread-local GIL-work timer (native/angr/src/gil_profile.rs: GilWorkGuard times only the outermost entry so nested/recursive touch points count once; RunLoopWallGuard brackets run-loop wall on Drop and gates timing on an active run loop so gil_work_ns<=run_wall_ns). Surfaced as gil_work_time_ns/run_wall_time_ns in stats(). Accumulators are process-monotonic (set_profiling does NOT reset) so multi-phase benches reporting via the last manager get the whole-bench fraction. Measured GIL_frac: codegate-angrybird 0.127 (stable gate target, A ceiling 1.77x at N=2), CADET_00001_partial 0.330 (1.50x), cmu_binary_bomb_partial 0.467 (1.36x, bridge-heavy). Solver_frac 0.00-0.32 everywhere << 0.67 needed -> B max <=1.19x, dead, as the ship-gate-retarget prior predicted. A clears 1.5x on the stable target but is marginal/failing on bridge-heavy benches; lifting is NOT the bottleneck (reduce claripy_to_rustbv/rustbv_to_claripy + memory-callback round-trips to lift the ceiling). Touch-points to gate under A = the guarded chokepoints (bridge entries, all PythonCallbacks call_/call_inspect_, state/fork.rs clone_py_metadata). Final go/no-go needs a real 2-worker calibration in 1ilq.3. Instrumentation commit 2fcadeaaa.
gmad2-dedup-no-false-positive
forgotten
angr-gmad2 verdict (2026-07-04): the source-side assume ptr-dedup does NOT drop an intended constraint on fauxware -- the kenpr caveat is resolved as a false alarm. (1) STRUCTURAL PROOF: dedup_set stale-ptr false positives are impossible because the only path that shrinks local.z3_assertions (transaction_rollback in symbolic/transaction_ops.rs) already does dedup_set.clear()+dedup_set_seeded=false (angr-sfp9). Every insert keeps dedup_set <=> z3_assertions in sync via contains_or_insert_ptr, so every dedup_set ptr is backed by a live Bool; Z3 hash-consing then guarantees a ptr HIT means a live structurally-equal AST exists. (2) EMPIRICAL: fauxware fires ZERO dedup HITs (z3_assume_dedup_hit=0 AND add_constraint_raw_dedup_hit=0; symbolic assumes happen deeper than step(n=10)). A new Debug-gated audit fn SymContext::debug_verify_dedup_backing (symbolic/constraint_ops.rs, called from check_z3_dedup_if_seeded when dup && log_enabled(Debug)) logs no UNBACKED warnings -- it is a permanent zero-cost sentinel: run RUST_LOG=rustylib::symbolic=debug to audit future dedup HITs for stale-ptr false positives. CONCLUSION: the kenpr constraint_count pin (snapshot-constraint-count-pin) STANDS; restore [4] is NOT more correct; the [3]->[4] inflation is a restore-replay COUNT artifact, not a dedup drop. No dedup_set invalidation-on-free needed. Commit 140490098.
god-module-split-free-fns
forgotten
God-module split of a FLAT-FILE module into sibling #[path] submodules that keep FREE FUNCTIONS + private helper structs together (nbim4.3, core_outcome.rs 1659->622+190+883). Keys: (1) A private fn moved to a child submodule is NOT visible to the parent (E0603) — mark the fns the PARENT still calls 'pub(super)' (core_outcome run_post_step_core dispatches to bounce/materialize_deferred_forks_core/handle_simprocedure_core/handle_syscall_core/handle_symbolic_jump_target_core). Fns called only among siblings stay private; externally-used ones stay pub(crate) (materialize_bounce_forks). (2) Private helper structs (ForkPayload/ForkSink/SimProcCall) with PRIVATE FIELDS that the moved handlers construct must STAY IN THE PARENT, not move to a sibling types file — private fields are visible to DESCENDANT modules (child handlers.rs) but NOT to siblings, so keeping them in the parent lets handlers.rs use 'use super::*' + field access with zero visibility edits. (3) Struct methods called from a SIBLING module need pub(crate) (CcSnapshot's 4 marshalling methods, called via ctx.cc from handlers.rs). (4) PATH FIX: 'super::helpers::X' in code moved one level deeper becomes 'super::super::helpers::X' (super=core_outcome now, need exploration). Complements god-module-split-state-rs / -impl-across-files / -test-glob-imports.
god-module-split-impl-across-files
forgotten
God-module split NEW gotcha (zel8z.2 callbacks.rs): (1) ONE inherent impl block CAN be split across multiple sibling files — moving half of 'impl PythonCallbacks { call_inspect_* }' to inspect.rs and half ('call_memory_/lift/hook' + private fn bv_to_bytes) to dispatch.rs each as its own 'impl PythonCallbacks { }' wrapper Just Works; submodules see private struct fields as descendants, and 'use super::' picks up the parent's private 'use' imports (RustBV/PyBytes/Arc) so no per-file import fixups were needed and clippy -D warnings stayed clean. (2) DETERMINISTIC-SLICER PITFALL: a test submodule decl with TWO stacked attributes ('#[cfg(test)]' then '#[path = ...]' on consecutive lines) is easy to mis-range — grabbing only the '#[path]'+'mod tests;' lines orphans '#[cfg(test)]' onto the preceding item -> 'expected item after attributes'. Likewise an item's CLOSING range that strays one struct too far (the LoopExecutionEvent impl end vs the doc comment of the item that followed it — at the time, the since-deleted CallbacksRef wrapper; angr-9ke6b.214 removed it as dead code) yields 'expected item after doc comment'. Always git-show the original and verify each slice boundary lands on a blank line between complete items before deleting the source. Complements god-module-split-test-glob-imports + -visibility-pitfall + -private-mod-deadcode.
god-module-split-private-mod-deadcode
forgotten
God-module split gotcha (NEW, zel8z.4 ccall.rs): moving a 'pub mod foo'/'pub const' block from a crate-reachable file into a NEW PRIVATE submodule (e.g. 'mod arm32;' holding 'pub mod arm_cond') demotes those pub items from crate-reachable to module-private. dead_code/unused_variables then FIRES on genuinely-unused members that were silently allowed before (complete constant tables: arm_cond ARM_COND_LE/LO/LS/LT/NE/PL/VC). Fix: add #[allow(dead_code)] to the table module (it documents the full 0-15 encoding), NOT delete the constants. Distinct from god-module-split-visibility-pitfall (that one is E0603/E0446 private-not-reachable ERRORS on items that ARE used); this is dead_code on items that are NOT used but were reachable-hence-allowed pre-split. See native/angr/src/vex/ccall/arm32.rs arm_cond.
god-module-split-private-struct
forgotten
god-module split E0446/private_interfaces: a private struct used only as a field-less helper bundle (e.g. interpreter/statements_cas.rs DcasState passed through cas_writeback/execute_cas_stmt) becomes 'more private than the item' once those fns move to a submodule with pub(super) visibility. Fix: widen the struct to pub(super) too. Complements god-module-split-visibility-pitfall (which covers fns + struct doc/derive).
god-module-split-state-rs
forgotten
God-module split (zel8z.1 state.rs 3692->2030 ln, the largest in the zel8z epic): the giant 'impl RustSimState' block (~1400 ln, ~130 methods) was chopped NON-CONTIGUOUSLY — only the fork/merge family (clone_py_metadata + fork/fork_true/fork_false/replace_solver/fork_from_snapshot/merge) extracted to state/fork.rs as a NEW 'impl RustSimState {}' wrapper; the remaining ~120 methods stayed in state/mod.rs. KEY DECISION: NEXT_STATE_ID static + private fn next_state_id() were KEPT in mod.rs (NOT moved to types.rs as the bead suggested) because they are pervasively called (new/from_vex_arch/fork/...) — a private free fn moved to a child submodule is NOT visible to the parent mod.rs (E0603), whereas keeping it in the parent lets child submodules reach it via descendant visibility (use super::). Value-type submodules (filesystem/inspection/types) own their already-pub fields so they are clean cut/paste; inspection.rs needed NO 'use super::' (self-contained serde+std) — clippy flags the unused glob (E0-unused-imports under -D warnings), so omit it when a value-type module references nothing from the parent. Deterministic /tmp/split_state.py slices 1-indexed ranges + wraps fork methods. Test mod path -> #[path="../state_tests.rs"]. Complements god-module-split-impl-across-files / -visibility-pitfall / -test-glob-imports.
god-module-split-test-glob-imports
forgotten
god-module dir-split pitfall (zel8z.5 claripy_bridge): a sibling #[path] test module using 'use super::*' silently inherits external TYPE names (e.g. RustBV, Arc, SymContext) from the PARENT module's private top-level 'use' declarations — glob-import of a child sees ancestor-private items. When the split moves those 'use' lines OUT of mod.rs into submodules (cache.rs/import.rs/export.rs), the test file loses them and fails E0433/E0425 'use of undeclared type'. Fix: add explicit 'use std::sync::Arc; use crate::symbolic::{RustBV,SymContext};' to the test file (claripy_bridge_tests.rs) — only the names it actually references, to avoid unused-import clippy warnings. Also: a leaf helper used by BOTH a kept-in-mod fn AND a moved submodule (extract_int_value, called from mod.rs and from import.rs::try_extract_bvv) is cleanest left in mod.rs as a private fn — children call it via 'use super::extract_int_value' and the test glob still resolves it. (The mod.rs-side caller at the time of the split, extract_concrete_value, was deleted as dead code in angr-9ke6b.214; try_extract_bvv is now the live consumer.) Complements god-module-split-visibility-pitfall + god-module-split-private-struct.
god-module-split-visibility-pitfall
remembered
God-module split checklist (epic zel8z + nbim4, splitting oversized flat Rust files into #[path] submodule directories). Recurring pitfalls, all encountered doing this repeatedly across the codebase:
VISIBILITY: (1) PRIVATE FN called by siblings (E0624 'method is private'): a private fn defined in a parent mod.rs's main impl block is callable from sibling submodules (they're descendants of the module where the type is defined). Once you MOVE that fn into a new submodule, a private fn there is NOT visible to siblings anymore. Fix: widen moved private methods that sibling modules call to pub(super) (mirrors the existing pub(super) fresh_unconstrained_read pattern). pub fn stays pub; only bare private fns need widening. Symmetric case for a PRIVATE FREE FUNCTION the PARENT still needs to call after it moved to a child (E0603): mark it pub(super) in the child (core_outcome.rs run_post_step_core dispatching to bounce/materialize_deferred_forks_core/etc.); fns called only among siblings stay private, externally-used ones pub(crate). PATH FIX when nesting one level deeper: 'super::helpers::X' becomes 'super::super::helpers::X'. (2) PRIVATE STRUCT used only as a field-less helper bundle (E0446 'private_interfaces', 'more private than the item'): once the fns using it move to a submodule with pub(super) visibility, the struct itself becomes more-private-than-the-item too. Fix: widen the struct to pub(super) as well (e.g. interpreter/statements_cas.rs DcasState passed through cas_writeback/execute_cas_stmt). (3) PRIVATE HELPER STRUCTS WITH PRIVATE FIELDS (ForkPayload/ForkSink/SimProcCall-style bundles) that moved handlers construct must STAY IN THE PARENT, not move to a sibling types file -- private fields are visible to DESCENDANT modules (child handlers.rs) but NOT to siblings, so keeping them in the parent lets handlers.rs use 'use super::' + field access with zero visibility edits. Struct methods called from a SIBLING module (not a descendant) still need pub(crate) (e.g. CcSnapshot's marshalling methods called via ctx.cc from handlers.rs). (4) NEXT_STATE_ID-style pervasively-called private free fn/static: keep it in the PARENT mod.rs rather than moving it to a value-type submodule as a refactor plan might suggest -- a private free fn moved to a child is not visible to the parent (E0603), whereas keeping it in the parent lets ALL child submodules reach it via descendant visibility ('use super::'). Value-type submodules that own already-pub fields (filesystem/inspection/types-style) are clean cut/paste; a module that references nothing from the parent needs NO 'use super::*' (clippy flags the unused glob under -D warnings). (5) DOC-COMMENTS/DERIVE ATTRS: line-range extraction must include each struct's leading doc-comment + #[derive(...)] attrs -- grabbing from 'pub struct' alone strips the derive (E0277, e.g. Clone not satisfied) and leaves an orphaned #[derive] that wrongly binds to the NEXT struct (E0119 conflicting impl). Re-export moved pub structs via 'pub use submod::Struct;' to preserve original crate-wide paths.
STRUCTURE: (6) ONE inherent impl block CAN be split across multiple sibling files (e.g. half of 'impl PythonCallbacks { call_inspect_* }' in inspect.rs, half in dispatch.rs, each its own 'impl PythonCallbacks {}' wrapper) -- submodules see private struct fields as descendants, and 'use super::' picks up the parent's private top-level 'use' imports, so no per-file import fixups are needed. The god-module split need NOT be contiguous either: e.g. state.rs's ~1400-line 'impl RustSimState' (~130 methods) was chopped non-contiguously -- only the fork/merge family extracted to state/fork.rs as a new impl block, the remaining ~120 methods stayed in mod.rs. (7) DEAD_CODE after privacy narrowing: moving a 'pub mod foo'/'pub const' block from a crate-reachable file into a NEW PRIVATE submodule demotes those pub items from crate-reachable to module-private, so dead_code/unused_variables FIRES on genuinely-unused members that were silently allowed before (e.g. complete constant tables like ARM condition codes). Fix: add #[allow(dead_code)] documenting the table is a complete reference, do NOT delete the constants -- distinct from the visibility pitfalls above (those are compile ERRORS on used items; this is a lint on unused-but-legitimate items). (8) TEST FILE GLOB-IMPORT BREAKAGE: a sibling #[path] test module using 'use super::' silently inherits external TYPE names (e.g. RustBV, Arc, SymContext) from the PARENT module's private top-level 'use' declarations. When the split moves those 'use' lines OUT of mod.rs into submodules, the test file loses them -> E0433/E0425 'use of undeclared type'. Fix: add explicit 'use' lines to the test file for only the names it actually references. Also: a leaf helper used by BOTH a kept-in-mod fn AND a moved submodule is cleanest left in mod.rs as a private fn -- children call it via 'use super::helper_name' and the test glob still resolves it. (9) DETERMINISTIC SLICING PITFALL: when mechanically slicing a file by line range (e.g. a /tmp/split_*.py script), a stacked-attribute item decl (e.g. '#[cfg(test)]' then '#[path=...]' on consecutive lines) is easy to mis-range -- grabbing only the later attribute line orphans the earlier one onto the preceding item ('expected item after attributes'). Likewise a closing range that strays one item too far yields 'expected item after doc comment'. Always git-show the original and verify each slice boundary lands on a blank line between complete items before deleting the source.
god-object-decomp-0mqkc5
forgotten
angr-0mqkc.5 god-object decomp COMPLETE for state/mod.rs. Increments: 1 constructors->construction.rs; 2 registers->registers.rs; 3 memory->memory.rs; 4 options->options.rs; 5 solver->solver.rs; 6 history/call-stack->history.rs; 7 hooks/metadata->hooks.rs; 8 process/OS-env->process.rs + inspection wrappers->inspection.rs (1807->1448); 9 (7ac9c0fe2) the whole #[pymethods] impl PyRustSimState block -> state/pymethods.rs (1448->697, UNDER the ~1200 acceptance target). state/mod.rs now holds only: module decls, CtypeLocPtrs/GetoptExternAddrs/NativeResumeFrame structs, the RustSimState struct def + core 'impl RustSimState' mechanics (state_id/pc/arch identity + apply_changes incremental-sync + track_history/max_history/concretizer setters), the PyRustSimState pyclass struct, and its inner()/inner_mut() plain impl. The pymethods move is the single-block MOVE pattern (B) from invariant-pyo3-single-pymethods-impl. exploration/mod.rs (2764) is structurally floored -- NOT peeled, the ~1200 target does not apply to it (see this key's prior guidance). Task closed: state half meets target, exploration half floored by design.
gorvf1-bounce-attribution
forgotten
gorvf.1 bounce-service cost attribution (measure-first, zero-Python epic). MEASURED 2026-07-04 via run_single.py --counters-json; the reconstruction-vs-exec split already existed (no new instrumentation needed — measure-first confirmed it): simproc path uses add_simprocedure_phase(state_create/execute/sync_back/state_copy) in rust_callback_dispatch.py::handle_simprocedure_callback; parallel-migration path uses migrate* phase timers (migrate_phase_timers.rs, env-gated ANGR_MIGRATE_PHASE_TIMERS=1).
KEY FINDING: "reconstruction" means TWO DIFFERENT THINGS on the two servicing paths, with OPPOSITE cost profiles: (1) SimProcedure bounce path (xmllint_getenv init storm, real-binary Python-simproc callbacks): reconstruction = _create_state_for_callback (state_create phase) = 48.6% of simproc service wall; execute=35.5%, sync_back=15.6%, state_copy=0.1% (sp_total=180ms, count=26). Also lift_block=28.2ms ~= 30% of the run-loop (gil) wall. (2) Parallel steady-migration bounce path (fork_solve_W6_S8, fork_solve_pbounce_W6_S8_M12_B2 under RUST_PARALLEL_WORKERS=2 RUST_PARALLEL_STEADY=1): reconstruction = migrate leaf_rebuild = 0.1% (W6_S8) / 0.7% (pbounce) of migrate_roundtrip. Here the cost is Z3 CONSTRAINT SERDE: serde 47.4%/37.4%, smtlib2 emit+parse 0%/30.1%. leaf_rebuild is NEGLIGIBLE. pbounce migrate_roundtrip=5.12s of 69.2s wall (~7.4%); W6_S8 0.15s of 23s (~0.6%). gil_frac ~0% both (pure-symbolic, no libc).
GO/NO-GO for lazy-SimState-views bead (angr-gorvf.2): GO, scoped to the SimProcedure bounce path only. state_create is 48.6% (>30% gate) of simproc service wall on the init-storm/real-binary path — lazy/delta SimState views directly attack that. Do NOT expect it to help the parallel-migration path: leaf_rebuild there is <1%; that path needs a constraint-transfer (SMT-LIB2/serde) optimization instead, not lazy SimState views.
libVEX-FFI (angr-z087y) win sizing: helps the INIT/COLD-block phase, ~0 in steady state. xmllint lift_block=28.2ms ~= 30% of the run-loop wall (135 blocks) -> native libVEX could reclaim most of the Python lift dispatch on init-heavy real binaries. Steady parallel benches: lift 22/118 blocks, <1% of wall -> libVEX-FFI gives ~nothing there. So z087y is an init-storm/cold-block optimization, not a steady-loop one.
gorvf19-register-acquire-once
forgotten
angr-gorvf.19 (commit a291a5a43 + 1074f883e): the SimProcedure-bounce register SERVICE cost was reducible — the gorvf.10 acquire-once lever generalizes from state.memory to the REGISTER file. Two symmetric wins on the callback register path, both from talking to the register UltraPage directly instead of the memory-mixin stack (state.registers is DefaultMemory+UltraPage, all GP regs in page 0, register_endness=Iend_LE): (1) WRITE side — export_callback_bundle's sc_bundle phase was 98% a loop of state.registers.store(offset,val,size) (17 AMD64 regs, ~37us each = full mixin traversal); _apply_bundle_registers (in rust_state_sync.py) acquires the reg page once (writing=True for COW) and blits concrete values into page.concrete_data + clears symbolic_bitmap over the range, matching UltraPage.store's concrete branch. sc_bundle 8.40->0.67ms (12.5x). (2) READ side — sync_back's _extract_register_changes called getattr(state.regs,name) for OLD and NEW state (~34 mixin loads building claripy ASTs) just to diff; _reg_concrete_from_page reads the concrete int straight off the page (read-only _get_page(pageno,False), no claripy, any() over symbolic_bitmap to detect symbolic) so concrete regs need zero getattr, falling back to getattr only for symbolic regs (the return reg rax). sync_back 8.60->2.44ms (3.5x). SUM 17.0->3.11ms=5.46x on defcon2016quals_baby-re. KEY: _acquire_ultrapage grew a memory= param to target state.registers. GENERALIZABLE: any per-crossing register store/diff that goes through the mixin stack is a candidate — same GENERALIZABLE RULE as memreplay-bottleneck-root-cause. Measure-first counters kept: sc_bundle_ffi/apply, sb_regdiff/memdiff/resume.
gorvf2-state-create-bottleneck
forgotten
gorvf.2 state_create sub-attribution (measure-first, xmllint_getenv 26 callbacks). The bead PREMISE was WRONG: it assumed state_create's cost is a full SimState copy ('reconstruction'); on the real-binary init-storm path sc_copy=0 (NO copy — find target is a plain addr so has_predicates=False, and the callback proxy gates default-off). Actual state_create=88.3ms breakdown: sc_meminstall=38.4ms/43.6% (_install_rust_memory_proxy, rust_state_sync.py), sc_memreplay=30.2ms/34.1% (_replay_rust_dirty_pages), sc_bundle=19.2ms/21.7% (export_callback_bundle FFI+reg apply), sc_copy=0, sc_sympage~0. So sc_memory (install+replay) = 77.7% is the real target, NOT the copy. KEY: both _install_rust_memory_proxy and _replay_rust_dirty_pages early-return when _is_rust_memory_proxy(state.memory) (angr-ryf6 clobber-guard) — i.e. the use_callback_memory_proxy gate (ANGR_RUST_USE_CALLBACK_MEMORY_PROXY=1) IS the lazy/delta-SimState view the bead asked for: it makes state.memory a live on-demand Rust proxy so both eager-writeback helpers become no-ops. MEASURED gate-on: state_create 88.3ms->30.1ms (2.9x, meets bead >=2x criterion), sc_meminstall 38.4->2.4ms, sc_memreplay 30.2->0.67ms, fidelity preserved (xmllint found=1 both, baby-re validated in ryf6). CAVEAT: negligible WALL impact on xmllint (3.47->3.56s) — state_create is only ~50% of simproc service wall and simproc service is a small fraction of init-dominated total wall; 58ms saved / 26 callbacks vs 3.5s wall. Instrumentation committed d9db0c565 (add_state_create_subphase, callback_simprocedure_sc*_ns in --counters-json).
gorvf31-xmllint-all-bucket-a
forgotten
gorvf.3.1 measured the xmllint_getenv 27 SimProc->Python fallbacks per-PC (tools/xmllint_fallback_classify.py): ALL 27 are Bucket A (is_in_binary-gate misroute) -- 8 distinct hook PCs (strcmp x10, pthread_once x4, pthread_mutex_lock x4, pthread_mutex_unlock x3, malloc x3, time/getenv/calloc x1) all resolve INSIDE libc.so.6's executable region, and a native proc exists for all 8, so the gate (native-dispatch-skipped-for-loaded-libs) routes every one to Python. ZERO are Bucket B (ADDS_EXITS-dependent): no hooked angr SimProc sets ADDS_EXITS (pthread_once is modelled as a leaf). This REVISES the ~14/27-A hypothesis in the bead -- it is 27/27 A. Consequence for gorvf.3: .4c (native dispatcher ADDS_EXITS/sub-call wiring) is UNFUNDED by the canonical libc bench (nothing to wire); all value lives in .4b (the gate flip), whose net win is already <0.1% per a8epx-gate-measured-defer (strcmp x10 dominant runs symbolic -> native bails symbolic anyway). Reproduce: ANGR_EXAMPLES_DIR=... python tools/xmllint_fallback_classify.py.
gotcha-endness-variants
forgotten
rustylib::vex::ir::Endness variants are 'Little' and 'Big' (NOT 'LE' / 'BE' as in some other VEX libraries). The enum is defined in native/angr/src/vex/ir/arch.rs (pub enum Endness, ~line 138) after vex/ir.rs was split into the vex/ir/ directory (commit 327f12229).
gotcha-multi-cell-callback-rarely-fires
forgotten
Phase 1.4 wiring of MultiwriteAnnotation in _cb_memory_store_symbolic_full landed but in practice rarely fires today: Rust's default write_range_limit=128 matches Python's MultiwriteAnnotation default, so the symbolic-store path inside Rust hits ConcretizationResult::Multiple (eager ITE) BEFORE escalating to TooLarge / the Python callback. To exercise the Phase 1.4 path in production, either raise write_range_limit so TooLarge fires more, or wait for Phase 2 (angr-qh5u) which flips Multiple/Strided to use Multi cells directly in Rust — bypassing the callback entirely and making the annotation check moot for the common case. Implication: do NOT expect a benchmark improvement from Phase 1.4 alone; the win is Phase 2.
gotcha-pyo3-methods-readonly
forgotten
PyO3-generated pyclass methods are read-only attributes on the Python wrapper — raises 'attribute is read-only'. To monkey-patch in tests, wrap the call in a thin Python method on the higher-level Python class (e.g. _rust_state_memory_store_symbolic_multi → calls _rust_mgr.state_memory_store_symbolic_multi). Pattern used in angr/exploration/rust_manager.py for Phase 1.4 (angr-5zw8) test fakes.
gotcha-stride-detection-two-addrs
forgotten
AddressConcretizer with default settings has enable_stride_detection=true. Two solutions with a regular delta (e.g. {0x1000, 0x2000}) get classified as ConcretizationResult::Strided{base=0x1000, stride=0x1000, count=2}, NOT Multiple. Tests that need a Multiple result must use 3+ non-strided candidates or relax the assertion to accept either variant. Bit me in test_store_symbolic_unified_multi_multiple_round_trip (commit 405f4c3db); fix was matches!(.., Multiple(_) | Strided{..}). Both variants route through install_multi_for_candidates so functional behavior is identical, only the type tag differs.
grub-z3-crash-fixed
forgotten
grub Z3_solver_translate panic (angr-xtk/angr-6vg) is FIXED by lazy solver materialization — we no longer call Z3_solver_translate at all. However grub now fails with 'list index out of range' error, which is a separate 32-bit x86 handling bug. Python engine completes grub in 2.07s successfully.
grub-z3-translate-panic
forgotten
grub crash is z3-rs panic at Z3_solver_translate (Solver::clone) returning None during state fork. This is in the z3 crate (z3-0.19.7/src/solver.rs:663), not our code. Happens during solver context fork via SymContext::fork(). May be use-after-free or context mismatch in Z3 C API.
guest-value-indexing-structural-not-panic
remembered
Guest-value panic sweep of interpreter/.rs + vex/ops_vec_.rs (angr-qwyti.17): the raw [] indexing there is on IR-STRUCTURAL indices, NOT guest values — self.temps[tmp as usize] (IRTemp id, bounded by tyenv), lane loops (0..count), GetI/PutI offset = base + (idx+bias)%nElemselem_size (nElems from the VEX RegArray descriptor, not guest), elem_width/count from the op type. So the qwyti.16 arithmetic-fold panic class has NO raw-indexing analog in this layer — casts like (i as u8)/(count as u32) are structural. The ONE guest-value overflow was the arithmetic-shift sign-fill mask: (elem_mask << (elem_width - shift)) & elem_mask in vec_sar_n + vec_shift_vec(Sar), which shifts a u128 by 128 when elem_width==128 && shift==0 (panic=abort SIGABRT / release mod-128 wrap). Guarded via pub(super) VEXOps::sar_fill_mask (ops_vec_shift.rs): returns 0 when fill_bits>=128 (shift-0 fills nothing, matches reachable <128 path). Latent (NEON per-lane sar routes only <=64-bit lanes) but same class as sign_extend_to / low_bit_mask_u128 guards. Test the guard directly (VEXOps::sar_fill_mask); it is unreachable through binop dispatch.
h0dv-callback-chain-removal
forgotten
angr-h0dv (commit 24311a484, 2026-05-22): Rust→Python constraint-sync FFI chain fully removed. Python: _cb_sync_constraints, sync_rust_constraints_to_python, set_sync_constraints registration, and cb_sync*/pending_ast_sync_calls counters all gone. Kept: rust_ctx_missing counter (defensive Path A telemetry). Rust: sync_constraints field/setter/call_sync_constraints gone from callbacks.rs; engine.rs post-loop sync removed; sync_before_callback in interpreter_cb/constraints.rs reduced to clearing tracked constraints. The Rust-internal tracker (pending_python_constraints, track_concretization_constraint, track_branch_constraint, PendingConstraint struct) is RETAINED — it backs in-process unit tests in constraints.rs but no longer crosses the FFI. Path A (rust_solver_ctx attach in rust_callback_dispatch.py::_install_rust_solver_on_callback_state) is the single live path; Python's state.solver is monkey-patched to delegate to the Rust solver context, so constraints never need to be replayed across the FFI.
hackcon-pure-rust-slow
forgotten
hackcon2016 (pre-optimization era, 67s) had ZERO Python overhead — 0 callbacks, 1 FFI crossing, 0s in callbacks. All time was spent in pure-Rust solving (see hackcon-z3-ast-structure for breakdown). Note: current rust_time is ~11.7s post-optimization (baseline_timings.json 2026-05), so the 67s figure is historical. Constraint AST structural mismatch with Python remained the root of the residual 0.9x gap.
hackcon-regression-root-cause-fced54a07
forgotten
hackcon2016 11.7s -> 30.6s regression (2026-05-17, bisected 2026-05-18, fix landed in commit f54bbba93): root cause is fced54a07 (angr-9maq, 'skip eager allocation of all-zero filler pages') which made all-zero pages lazy-only in _sync_extra_python_pages. For hackcon2016 (~35 zero pages per state), keeping those pages lazy structurally slows the FINAL Z3 solve from ~10s to ~28s — explore phase stays fast and fetch_page callbacks are 0, so the lazy path's slowness is NOT FFI roundtrip cost. The slowdown must come from the constraint AST built by the Rust solver picking up different shape when its memory model has fewer concretely-mapped pages during exploration. Exact AST-structure delta unmeasured. The fix (commit f54bbba93) is a per-state cap: if total zero-candidate pages <= 200, eager-map all (hackcon path); else lazy (mma_howtouse path which has ~2058 per-state and was the original reason for the optimization). Cap sits with headroom on both sides. Page counts measured 2026-05-18 (used to size the cap): hackcon2016 = 66 zero pages over 2 state-syncs, max 35/state; mma_howtouse = 184860 zero pages over 90 state-syncs, ~2054 avg/state. Post-fix samples (3-run median): hackcon=15.22s (still above pre-regression 11.7s — residual cost is the Z3 Extract/Reverse AST mismatch from hackcon-z3-ast-structure, not page-allocation related). Not investigated further: WHY the lazy-zero-page path produces worse Z3 ASTs when no fetch_page callbacks fire during exploration. If future work hits another regression here, instrument: (a) symbolic-spans diff between eager and lazy, (b) Z3 assertions added during explore, (c) which pages get queried in the final solve_for_flag step. The current fix is a heuristic, not a root-cause repair.
hackcon-signext-root-cause
forgotten
angr-rbnk (2026-06-02, commit 4dc7fc064): hackcon2016_angry-reverser AST-structure divergence root-caused. Rust BVOp::SignExt to-Z3 emission at native/angr/src/symbolic/value.rs:2353 used a hand-rolled concat loop: 'let sign_bit = inner.extract(w-1, w-1); for _ in 0..bits { result = sign_bit.clone().concat(&result); }'. When inner is itself an Extract(byte_range, flag), this emits nested (extract 7 7 (extract X Y flag)) terms — 2.7x more extracts than Python (19649 vs 7158) and 1.7x more total chars (740K vs 432K) on the 58-assertion hackcon final solve. Fix: 'BVOp::SignExt(bits) => operands[0].to_z3_ast_cached(cache).sign_ext(*bits)'. Result: AST shrunk 13.7x (54K chars, 449 extracts, 0 concats — 8x smaller than Python). Median ~30s → ~22.5s (8-run campaign). Now bimodal: fast tail 9s matches Python solve time, slow tail 35s on Z3 SAT-search nondeterminism. Investigation tool: tools/dump_hackcon_smtlib.py uses rust_ctx.get_all_constraints_str() for Rust and claripy.backends.z3.convert(c).sexpr() for Python. Spike report in docs/advanced-topics/rust_engine.rst. Lesson: when porting symbolic ops to Z3, prefer the z3-rs native operator (sign_ext, zero_ext) over hand-rolled concat/extract — Z3's bv_rewriter handles native ops natively but a hand-rolled equivalent forces it to do extra simplification work AT BEST, and prevents propagation pruning AT WORST.
hackcon-z3-ast-structure
remembered
hackcon2016 69s breakdown: explore=0.6s, constraint_export=0.1s, Z3_solve=66s. The 66s is Z3 solving the synced constraint system. Python engine eval takes only 9s because its constraints have native AST structure. Rust engine constraints are converted via claripy_to_rustbv → rustbv_to_claripy, producing structurally different Z3 ASTs that solve 7.5x slower. Root cause: flag BVS is imported to Rust as symbolic memory, but Rust solver creates Extract/Reverse sub-expressions over it. The resulting constraint ASTs don't match Python's natural structure. Fix requires Rust solver to natively share Z3 context with Python, or produce structurally identical constraints.
hang-divergence-addr-trace-method
remembered
Method for a 'bench hangs under config X but not Y' divergence (angr-s0x0v, unmapped_analysis under the callback-memory-proxy gate). Do NOT mgr.run()/explore() — step with a hard cap and print BOTH mgr.stash_counts() AND [hex(s.addr) for s in mgr.active] each step, once per config, then diff the ADDRESS traces. The stash counts alone said 'no explosion' but nothing more; the addr column pinpointed the exact divergent step (a single state that should have errored instead entered a 2-block loop) in one run. The bead's a-priori hypothesis (symbolic-bytes fidelity => wide forking => state explosion) was WRONG — the active stash never exceeded 5. Lesson: for a hang, always check 'one state looping' before 'many states forking'; they look the same from a wall-clock timeout. Run the script under a nested capped scope: systemd-run --user --scope -p MemoryMax=4G -p MemorySwapMax=0.
hfr0-macro-shape
forgotten
angr-hfr0 (commit b40768356, 2026-05-21): opcode_map.rs width-family macros use strip_prefix + inner match + 'return Some(...)' from inside the enclosing parse_* fn. Each macro is a statement that falls through on miss, so multiple macros chain naturally. Macros are local (macro_rules! at top of file, no #[macro_export]). The 'return' inside an if-let block returns from the function — works because parse_* return Option. Macros sharing a prefix (e.g. 'Iop_And' for both And and VAnd arms) are fine because the inner match selects unique suffixes. For arms with multiple suffix-to-same-elem (e.g. InterleaveLO 8x8 and 8x16 both -> elem=I8), the macro repeats the arm — works because each match arm is independent. Pattern saves ~50% LOC on hand-written N×M dispatch tables.
high-level-mgr-errored-stash-producer
forgotten
The ONLY non-executable page the high-level RustExplorationManager maps into Rust is the STACK page (perms=6, via rust_state_sync.py _setup_stack_region/_sync_stack_page). All loader pages (_build_loader_pages_cache_entry batch_pages) and lazy-fetched pages hardcode perms=7 (RWX), and unmapped/bad accesses get swallowed to zero buffers or routed to deadended — NOT errored. So the deterministic way to drive the HIGH-LEVEL manager into the errored stash from Python is: entry_state(add_options={STRICT_PAGE_ACCESS, ENABLE_NX}), set state.regs.pc = state.solver.eval(state.regs.sp) (PC into the eagerly-mapped stack page), then mgr.run(). The NX fetch fails before lift and lands a RustErrorRecord (error_class='memory') in errored. Used to de-vacuify test_errored_returns_error_records (angr-9drh4).
hitcon-sakura-separate-bug
forgotten
hitcon2017_sakura 'list index error' is NOT the same as csgames2018. sakura uses address-based find/avoid with multi-stage exploration (loop: explore(find=addr), found[0], new simgr(found)). The crash is simgr.found[0] on empty found list. The Rust engine doesn't find the first address-based target. This is a separate issue from the callable predicate re-evaluation bug.
hk7k-materialize-cost-distribution
forgotten
Materialize cost on hot benches: per-bench breakdown from 2026-05-21 hk7k research spike (run_single.py --counters-json). HIGH cost (24-31% of wall): defcamp_r100 (14 mats, 60.5ms, 24.2%), ais3_crackme (50 mats, 244ms, 28.4%), google2016_unbreakable_0 (52 mats, 261ms, 28.4%), defcon2016quals_baby-re (30 mats, 138.7ms, 30.8%). LOW cost (<2%): fauxware (2 mats, 3ms, 1.6%), mma_howtouse (45 mats, 0.34ms, <0.01%), securityfest_fairlight (16 mats, 60ms, 0.3%). Per-mat cost is bimodal: ~5ms when fork happens with substantial constraint prefix, ~10us when prefix is small (mma). Key insight: high-mat-cost benches are all ALREADY ≥1.5x speedup; the sub-1.0x benches that the rust-symex epic actually cares about (mma, fairlight, hackcon-likely, sokohashv2-likely) do NOT have materialize as a meaningful bottleneck. hk7k optimization is general-perf, not a lever for closing slow-bench gaps. See angr-hk7k notes.
hook-length-userhook-honored
forgotten
Hook length parameter is correctly honored in Rust dispatch via UserHook semantics, NOT via dispatch logic.
When user calls proj.hook(addr, fn, length=N) with a callable, project.py wraps it: UserHook(user_func=fn, length=N). UserHook.run() calls self.successors.add_successor(self.state, self.state.addr + length, claripy.true(), Ijk_Boring|Ijk_NoHook). The successor's address (hook_addr+length) propagates through dispatch as new_pc = succ_state.addr and into Rust via resume_after_simprocedure(new_pc, ...) -> state.set_pc(new_pc).
The dispatch's hook_length variable in rust_callback_dispatch.py:375-378 is only used for is_zero_length_hook (skip_hook_addr decision); the actual PC advancement happens via the UserHook's add_successor call. This is functionally correct and matches canonical angr's behavior.
Why: Bead angr-03ej claimed this was broken, but the diagnosis was wrong. Verified via test_hook_length_advances_pc_userhook (fauxware) and instrumented run loop logging. How to apply: When debugging hook-related issues with length>0, do NOT assume the dispatch logic needs to plumb hook_length to advance PC; UserHook does it. If a hook re-fires repeatedly, look elsewhere (e.g., unconstrained PC concretization, basic block boundaries, succ.addr extraction).
hook-sync-unregister-live-manager
forgotten
Live-manager hook sync (angr-969g): _sync_hooks_before_step in rust_state_sync.py must DIFF the registered set against proj._sim_procedures, not just add. proj.unhook() on a live RustExplorationManager mutates _sim_procedures (removes the key) but the old add-only sync + 'len==len' fast path never propagated removals to the Rust hook table -> stale hooks keep firing. Fast path is now 'registered == sim_procedures.keys()' (set==dict_keys, no copy); removals call unregister_simprocedures FFI (exploration/mod.rs, removes from self.hooks and self.simprocedures) + registered.difference_update. Also: when _find_simprocedure returns None (stale/unknown hook) the dispatcher (rust_callback_dispatch.py) must set_skip_hook_addr(addr) + resume at addr, NOT resume_after_simprocedure(addr+1) which lands a misaligned PC.
hv22-overlay-relocated-bottleneck
forgotten
Profile of _overlay_relocated_sections (fauxware, 2026-05-11, 20 iters wall-clock): per-call mean 1.79ms / median 1.475ms / max 7.74ms (cold). NOT the 6.1ms cProfile claim — cProfile inflates because the function makes thousands of attribute accesses through angr's 14-layer memory-mixin chain. Cost breakdown across 29 sections (25 concrete, 4 symbolic, all <4KB): state.memory.load = 1.250ms (97%), solver.eval+to_bytes = 0.026ms (2%), rust map FFI = 0.016ms (1%). Same root cause as z8xa register sync — fundamental angr memory-model cost. Naive manager-scope caching of section_patches is unsafe because the current code gates each overlay on per-state val.symbolic — caching would lose this gate and overwrite symbolic page contents in forked successors. The disk cache + _try_fast_memory_sync already handles the safe case (states without user-symbolic). Decision: rationale, no code change. Wrote profile harness at tests/benchmarks/profile_overlay_relocated.py.
hzd9e-corpus-add-redecision
forgotten
angr-hzd9e corpus-add re-decision (iter50, commit 94918464b): expanded tests/benchmarks/collect_simproc_fallbacks.py CANDIDATE_BENCHES beyond CTF-heavy x86_64 with libc-I/O (write_stream_heavy, cli_ctype_fprintf, busybox_static, xmllint_getenv) + non-x86 arch-smoke (arm/aarch64/mips LE+BE branch). FINDINGS: (1) dominant closable native gap = write-side stdio fileno fallback fwrite(144)+fputc(48)=192 of 247 closable simproc fallbacks, ALL from write_stream_heavy — already tracked+human-triaged modest-cost in angr-csyy9 (WON'T-FIX-leaning). (2) Non-x86 arch workloads surface ZERO VEX-op fallbacks: native VEX coverage holds for ARM/AArch64/MIPS LE+BE. (3) No FP/SIMD fixture exists in corpus (all integer/string-bound) — a dedicated one needs a new fixture. CONCLUSION per aq26d charter: native coverage is NOT the perf bottleneck; redirect to z3_/ffi_crossings/concretize_ per mgr.stats().
i386-struct-stat-and-angr-syscall-map
remembered
i386 struct stat support landed (angr-11djq.5.1, commit bf81b4ff5): write_i386_stat in native/angr/src/syscalls/file_path.rs mirrors fstat64.py::_store_i386 (struct stat64, NOT the legacy old struct stat). Shared write_stat_for_arch(state,arch_name,buf,size) dispatcher fans AMD64->write_amd64_stat / ARM64->write_aarch64_stat / X86->write_i386_stat / ARM->write_arm_stat / MIPS32->write_mips32_stat; each handler keeps its OWN arch guard (fstat+newfstatat: AMD64/ARM64/X86/ARM/MIPS32; stat+lstat: AMD64/X86/ARM/MIPS32 -- ARM64 dropped legacy stat/lstat). PITFALL: angr's i386 syscall-number map (angr/procedures/definitions/linux_kernel.py ~line 4285) DIVERGES from the kernel syscall_32.tbl. fstatat64 is 327 in angr (kernel says 300; 300=semctl in angr). angr's i386 map has NO 106/107 (legacy stat/lstat), only 108->fstat. Register the numbers angr USES, not raw kernel numbers: i386/ARM stat64=195, lstat64=196, fstat64=197, fstatat64=327. Always grep that linux_kernel.py number map for the target arch before registering. The _store_i386 layout writes 64-bit value widths (from posix Stat tuple) with intentional overlapping stores (st_ino 0x0C overlaps st_mode 0x10) replayed in Python's exact order; all fields but st_mode/st_size/st_blksize are 0. ARM (angr-11djq.5.2, commit 0ef7ab5f7): write_arm_stat mirrors _store_arm, SAME LFS numbers as i386 per angr's 'arm' map (~line 4089). STAT FAMILY NOW COMPLETE: MIPS32 (angr-11djq.5.3, commit e4fade99b) -- see memory mips32-struct-stat-and-omitted-mode for its quirks (big-endian, OMITS st_mode, numbers 4213/4214/4215/4293).
i9f2-mma-howtouse-attribution
forgotten
mma_howtouse 0.6x residual gap is attributed (angr-i9f2, 2026-05-19, HEAD 2870a3429).
Per-Callable RustExplorationManager perf_report() breakdown:
Init total 43.3ms (Memory sync 36.7ms, Register sync 3.1ms, Python init 1.2ms);
SimProcedure CallReturn callback 8.4ms; Lift 1.0ms total; Z3 7.1ms/check.
Per-Callable Rust extras ≈58ms → 45 × 58ms = 2.6s, matches observed 3s gap.
Dominant cost: _sync_memory_to_rust slow path runs every Callable because
_run_python_init_if_needed (rust_manager.py:2317) returns immediately for
state.addr in a real binary, so _mem_cache is never populated and
_try_fast_memory_sync (rust_state_sync.py:229) returns False. Each manager
re-iterates loader.all_objects, re-loads pages via loader.memory.load, and
re-issues map_memory_batch.
Follow-up fix: angr-bzsc — cache _extract_loader_pages output class-wide
keyed by (binary_path, arch_name). Output is a pure function of loader state.
Expected impact: ~36ms × 44 ≈ 1.6s recovered (closes most of the residual 3s).
Doc updated: docs/advanced-topics/rust_engine.rst mma_howtouse section.
iaol-audit-no-hashmap-leaks
forgotten
Determinism audit (angr-iaol, commit 2038bd4ea, 2026-05-25): every std::HashMap in native/angr/src/ classified for iteration-order leaks into exploration output. Result: ZERO output-affecting sites. Caches (claripy_bridge AST_CACHE, vex/lifter IRSB cache, symbolic/registry, symbolic/table) and registries (syscalls/handlers, procedures, vex/dirty handlers, state.simprocedures*) are lookup-only. State.symbolic_pages / hook_symbolic_memory / addr_to_ast are iterated into PyDict (state_api.rs:261/292/323) but consumers read by key — data-equivalent run-to-run. Stash maps (stashes/state_index/state_roots): per-stash order is the VecDeque (FIFO/LIFO honored); cross-stash iteration only writes same value to every state (set_max_history) or finds unique state_id. So FxHashMap conversion is NOT needed anywhere; remaining nondeterminism is Z3-only. See rust_engine.rst 'Determinism contract' section for the full table.
iaol-z3-seed-gap
forgotten
Z3 random_seed gap (angr-iaol audit, 2026-05-25): build_solver_params at native/angr/src/symbolic/context.rs:888 sets timeout + bv_extract_prop + mul2concat but NEITHER smt.random_seed NOR sat.random_seed. Two consequences: (1) defcamp_r100 trailing-byte mismatch — unconstrained stdin bytes get different fill values run-to-run; run_regression.py papers over with output normalization. (2) test_model_stability_constraint_order only verifies order-independence WITHIN a process — not run-to-run. Pin smt.random_seed=0 + sat.random_seed=0 via z3::Params::set_u32 in build_solver_params; that closes most of the gap. Residual: Z3 4.13 reserves heuristic latitude (eval() answer stability across rebuilds NOT guaranteed even with seed pinned). AVOID parallel.enable=true (correctness-breaking per avoid-z3-parallel-enable). Tracked in iaol.1 (pin seed) -> iaol.2 (deterministic=True flag + e2e test).
iaol1-seed-pin-empirically-broken
remembered
angr-iaol.1 finding (commit 364325237, 2026-05-25): the parent iaol audit's hypothesis that pinning smt.random_seed / sat.random_seed in build_solver_params (native/angr/src/symbolic/context.rs) would deliver model stability across fresh RustSolverContext instances is EMPIRICALLY WRONG. Three param-name forms tried via z3-0.19.7 Params::set_u32 + Z3_solver_set_params: (1) 'smt.random_seed' and 'sat.random_seed' CORRUPT the solver — eval() returns models that VIOLATE the asserted constraints (x=0 for x>=100 on a fresh RustSolverContext). Both forms hit the same failure mode, suggesting Z3 4.13 routes these only via Z3_global_param_set, not the solver-level params path. (2) 'random_seed' (no module prefix) is accepted by Z3 without corruption but delivers MORE variation across instances than no pin AND breaks test_model_stability_constraint_order (which passes with no pin). Conclusion: model determinism across fresh Solver instances is NOT reachable through Z3_solver_set_params for these keys. To make any seed pin take effect the call has to route through Z3_global_param_set BEFORE the first Solver::new — and even then Z3 4.13 reserves variable / restart heuristic latitude that is not pinned. build_solver_params left unchanged on code; docs + test_z3_seed_pin_not_attempted guard against re-introduction. AVOID parallel.enable=true (avoid-z3-parallel-enable).
ilq3-integration-done
forgotten
angr-1ilq.3 (scheduler<->run_loop integration) is DONE/closed as of iter49 (verify-and-close, no code change). The full parallel integration lives on HEAD: run_loop.rs dispatches RUST_PARALLEL_WORKERS>1 to run_loop_parallel (wave loop) or run_loop_parallel_steady (nkoct persistent worker-local frontiers), workers<=1 stays on verbatim run_loop_single_threaded. Real counters: parallel_tasks/parallel_migrations wired for the live parallel path (record_migration_sample in helpers.rs is a SEPARATE single-threaded ANGR_PARALLEL_WORKERS cost-estimate MODEL, not the real path). Canonical env is RUST_PARALLEL_WORKERS -> parallel_real_workers field. Determinism gate + real-counter tests: tests/engines/rust/test_parallel_wave.py (7 tests, green). REMAINING open work is 1ilq.5 (perf ship-gate: >=1.5x@2 workers) which does NOT yet materialize — workload-bound per vh834-phase6-go-not-materialized + parallel-per-wave-migration-dominates memories.
ilq8-post-cancel-waste
remembered
angr-1ilq.8 measure-first result: post-find speculative waste in parallel steady mode. Added parallel_post_cancel_steps counter (SchedulerCounters::post_cancel_steps -> SchedulerStats -> manager -> stats() dict). A step counts when cancel is set on process() return but the step did NOT itself raise it (!request_cancel) — i.e. an in-flight step committed before a peer's find-cancel became visible. Both worker_loop (wave) and worker_session_loop (steady) increment it. WASTE TABLE (RUST_PARALLEL_STEADY=1): fork_solve_W6_S8 W6 -> 5/9 tasks = 55.6%; xmllint_getenv W6 -> 0/23; ais3_crackme W4/W8 -> 1/98 = 1.0%; csaw_wyvern W8 -> 0/33; fauxware W4 -> 0/18; defcamp_r100 W4 -> 0/15. KEY: waste is bounded by (workers-1) in-flight steps because cancellation lands at the NEXT TASK BOUNDARY, not mid-solve — so absolute waste is tiny (<= workers-1) even on the pathological wide-fork bench. num_find=1-on-a-wide-frontier is anti-parallel (confirms rust_parallel_design.rst) but the fix's upside is small in absolute steps; the win is on synthetic simultaneous-wide-fork shapes, negligible (0-1%) on real CTF first-find benches. Feeds the find-aware dispatch bead angr-1ilq.9.
import-error-exception-redundancy
forgotten
'except (ImportError, Exception)' is a bug shape — Exception is a base class of ImportError so the ImportError clause is redundant AND the Exception catches everything else (too wide for an optional-import pattern). Correct fix: '(ImportError, AttributeError)' covers 'module missing' AND 'attribute missing from module' which is the actual concern when probing 'from angr import sim_options as o; ... o.LAZY_SOLVES'. Found 2 cases in rust_manager.py (sim_options probes); fixed in angr-z1fb.
import-z3-constraint-ptrs-hardening
forgotten
import_z3_constraint_ptrs hardening (angr-33t9, 2026-06-01, commit 6c15b9c19): Z3_get_sort + Z3_get_sort_kind sanity check rejects null, foreign-context ptrs, and non-Bool sort kinds as PyValueError before reaching unsafe add_constraint_raw. Validation is atomic — runs over full list before any constraint is added, so on failure the state's solver is unmutated. RESIDUAL UB: truly arbitrary integers (e.g. 0xdeadbeef) still segfault inside Z3's deref before the API call returns. Z3 does NOT magic-byte-validate AST headers before dereferencing them; closing that gap needs either an opaque Py newtype or a thread-local side-table of blessed exported ptrs. The sanity check covers realistic misuse (wrong sort, stale export, null mid-list) at cost of one Z3 C call per ptr.
imported-addrs-filter
remembered
get_state_symbolic_z3_asts must skip addresses in SymbolicMemory.imported_addrs. These are addresses imported from Python via import_symbolic_value(). Even if the binary modifies them (Symbolic→Expression via XOR etc), Python has the correct original BVS. Overwriting breaks post-exploration constraint solving (flareon5 root cause). Commit 0036e629e.
indexing-slicing-audit-2026-07
remembered
clippy::indexing_slicing repo-wide audit (angr-qwyti.19, 2026-07-25): swept all 7 subsystems for GUEST-VALUE-DERIVED raw []/slice indexing. Result: exactly ONE genuine site across the whole crate -> getopt.rs format_string's caller loop 'arg[optchar as usize]' (the getopt short-option engine). optchar is ENGINE cursor state restored via load_cursor from a prior getopt call; a guest that shrinks its own argv element in memory between calls leaves optchar > arg.len() -> panic-index. Fixed: 'let Some(&c) = arg.get(optchar as usize) else { return Err(ProcedureError::Other(...)) }' (defer to Python), test test_shrunk_argv_element_defers_not_panics in getopt_tests.rs. Everything else structural: claripy_bridge args[N] op-arity-guarded, memory page-offset masked (& PAGE_MASK) + Result-returning byte-lane extraction, procedures/syscalls dispatcher-arity (extract_procedure_args len==num_args) + fmt.len()-bounded cursors + [_;256] tables, interpreter/vex IR-structural (qwyti.17). DECISION: do NOT land scoped #![deny(clippy::indexing_slicing)] — ~575 residual structural sites would need #[allow] for zero panic gain. Complements panic-reachability-audit-2026-06 (j60q0) and guest-value-indexing-structural-not-panic (qwyti.17).
init-memory-scan-bottleneck
forgotten
BOTTLENECK FOUND: sync_memory_to_rust scanned ALL memory pages (2000+) via memory.load() looking for user symbolic data. After Python init, most pages are unconstrained fill (mem*/unconstrained) with empty symbolic_data dicts. memory.load() costs ~0.3ms/page = 670ms total. Fix: use get_symbolic_addrs() or page.symbolic_data (O(1)) to filter. Memory sync: 684ms->19ms for fauxware. Init: 917ms->265ms.
inline-audit-value-rs-2026-06
forgotten
When auditing value.rs for #[inline] candidates: by 2026-06-01, ALL arithmetic _into variants and operator pairs (add/sub/mul/udiv/sdiv/urem/srem/and/or/xor/shl/lshr/ashr/rotl/rotr/eq/ne/zero_extend/sign_extend/truncate/extract/concat/ite/clz/ctz/popcount/neg/not/reverse + their *_into counterparts) ALREADY have #[inline]. So do width(), is_concrete(), is_symbolic(), is_expression(), as_u128(), as_u64(), to_u128(), to_u64(), op(), operands(), into_arc(), concrete(), zero(). Remaining trivial candidates that lacked it as of angr-l4bs: RustBV::ones, BitWidth::{bits,bytes,mask,from_bits}, CallbackInterpreter::get_pc. Larger matches like BVOp::is_unary/is_binary/is_ternary deliberately left alone (could bloat compile output at every call site). Skip RustBV::symbolic/symbolic_with_id constructors — they allocate Z3 ASTs under the vex-engine-z3 feature, not trivial.
inspection-system
forgotten
InspectionManager lives in state/inspection.rs (state.rs split into state/ dir module, angr-zel8z.1): bitmask u8 for enabled events (bit N = InspectEvent N), ring buffer Vec (max 1024), event_counts [u64; 6]. InspectEvent: 0=MemRead, 1=MemWrite, 2=RegRead, 3=RegWrite, 4=Fork, 5=Exit. is_active() checks bitmask != 0, is_enabled() checks single bit. State has inspect_mem_read/write/fork/exit convenience methods (in state/mod.rs) that check bitmask before recording. Cloned on all 5 fork methods (state/fork.rs). The interpreter drives inspection (statements_inspect.rs/expressions.rs/execution.rs use InspectEvent). NOTE (angr-04tw3.13, 2026-07-30): the manager-level Python FFI surface (enable_state_inspection/enable_all_inspections/get_state_inspection_counts/get_state_inspection_events pymethods + _-prefixed helpers in state_api.rs + InspectionEventInfo type alias) was DELETED as dead — zero callers anywhere, since state.inspect raises NotImplementedError on the Rust engine (docs/advanced-topics/rust_engine.rst). The core InspectionManager machinery + interpreter wiring stays live and is exercised by state_tests.rs. Original wiring commit a07a999f4.
internal-patches-offline-prepare-pattern
remembered
Pattern: when a bd task is gated on a runtime measurement / CI artifact (not network access), the offline-prepare slice still works but lands under tools/draft_patches/ (parallel to tools/upstream_patches/). Difference vs the upstream variant: blocker class is data not access; placeholder is a measured value not a github URL; convention is sed-substitute-then-apply-then-delete-draft. Same slice-bead bookkeeping: slice bead closes on artifact commit, parent stays open until real value lands. First applied by angr-zlzw (2026-06-06, slice of angr-ywu7) for the [tool.coverage.report].fail_under CI gate — patch shape is trivial but threshold X requires running pytest+coverage (~30-60min) or downloading a CI artifact (no github). Pattern family now: aggregation-doc-v1-partial-pattern (doc blocked on 1 sibling), upstream-pr-offline-prepare-slice-pattern (PR blocked on github), internal-patches-offline-prepare-pattern (this one — patch blocked on measurement). All three convert stuck-queue items into productive drainage.
invalid-handle-id-helper
forgotten
Symbol-table handle-lookup misses (RustSolverContext op_/add_constraint_handle in native/angr/src/solver.rs + pending_api.rs set_pending_register_symbolic) MUST raise via solver::invalid_handle_id(&[u64]) -> PyValueError. Standardized in angr-ghwsd.1: was PyRuntimeError in 34 op sites (no id in msg) vs PyValueError in 1 site. Pass every handle id the op dereferenced; helper formats single id ('invalid handle id: N') or all ('invalid handle id (one of [..])'). New op_* methods must use the helper, not a hand-rolled ok_or_else, to keep the type from diverging. BEHAVIOR CHANGE: callers excepting RuntimeError on a bad handle now get ValueError.
inventory-validation-cross-check
remembered
angr-f8je learning: validating an inventory by reading source alone misses mixin-supplied methods. RustExplorationManager inherits eval_memory, found_states, get_state_by_id from RustStateSyncMixin (rust_state_export.py); RustInspectProxy.SUPPORTED_EVENTS is a class constant set outside the class body. Pattern for future inventory tasks: combine source grep with runtime dir(cls) + re.finditer(r'self.(\w+)\s*=', getsource(cls)) so mixins, class constants, and init instance attrs all show up. The 3 omissions caught by this cross-check would have been silent gaps in the v1 API contract.
iop-avg-vs-hadd-distinction
remembered
VEX Iop_Avg* != claripy _op_generic_HAdd: Iop_Avg{N}{S/U}x{M} is ROUNDING halving add ((a+b+1)>>1, ARM URHADD/SRHADD, SSE PAVG). claripy only has _op_generic_HAdd which is TRUNCATING ((a+b)>>1) — different op. There is no _op_generic_Avg in claripy/angr/engines/vex/claripy/irop.py. So angr falls back to symbolic-only for Iop_Avg* and the Rust engine cannot use Python parity tests — must use a spec-replay test (see z3-universality-parity-test-template variant 2). Trap: the OP_ATTRS_PATTERN regex parses Iop_Avg* → generic_name='Avg' but no handler matches, so the op silently degrades to a fresh BVS in pure-Python angr. The Rust engine is now stricter — it implements the rounding semantics directly.
irsb-deserialize-bottleneck
forgotten
VEX IRSB JSON deserialize is NOT a bottleneck (spike angr-3ijo, 2026-05-03). Microbench on 500 captured codegate_2017-angrybird IRSBs (avg 3KB JSON each): full deserialize_irsb = 17 µs/call, of which 12.8 µs is serde_json parse and 4.2 µs is PyVex→IRSB convert. For codegate (most lift-heavy benchmark, 1282 lifts): total deserialize = 21.8 ms = 0.8% of 2.72s total runtime. Even bincode at 5x faster parse would save only ~13 ms = 0.48% of total. Lift_time-python_callback_time delta in profiler = 70% deserialize, 30% Arc::new+cache.put. Across all benchmarks, deserialize is ~10% of lift_time, lift_time is ~10% of total = ~1% total. Spike fails its threshold of '>20% of lift'. Verified by running native/angr/examples/bench_deserialize on captured samples.
irsb-serializer-module
forgotten
VEX IRSB serialization lives in angr/exploration/rust_irsb_serializer.py (extracted 2026-05-03 from RustExplorationManager._serialize_irsb). Module-level: serialize_irsb(irsb)->str (entry point), with _serialize_const/_serialize_descr/_serialize_cee/_serialize_expr/_serialize_stmt helpers. Constants: CONST_TYPES (frozenset of Ico tags), _EXPR_FIELDS (table of RdTmp/Get/Load/Unop/Binop/Triop/Qop/ITE field schemas), _STMT_FIELDS (IMark/WrTmp/Put/Store/StoreG/AbiHint/MBE/NoOp). Class method _serialize_irsb just forwards. Output is JSON, consumed by Rust VEX interpreter on lift_block callback.
iter-88-checkpoint
forgotten
Iter 88 start: planning angr-4scu step 2 (symbolic-address store via lazy Multi-cell)
j4hl-counter-dump-cli
forgotten
tests/benchmarks/run_single.py supports --dump-counters (categorized aligned table) and --counters-json (raw stats dict as JSON, suppresses curated sections so JSON is the only structured stdout payload). Both Rust-engine only. Time keys ending in time_ns and time_in* floats auto-render as ms. dict-valued keys (only simprocedure_fallback_by_name today) print inline as {name=count,...}. Counter categorization is data-driven in run_single.py: explicit per-key sets for exploration/python-side/fallbacks; prefix-based for rust_/z3_/vex_/mem_/concretize_/bvop_/zext_*. New counter prefixes land in a 'misc' bucket — update _DUMP_PREFIX_GROUPS in run_single.py when adding a new family. Commit dcf821df7, 2026-05-20 (angr-j4hl).
j4hl-stats-key-count-snapshot
forgotten
mgr.stats() returns 132 keys as of 2026-05-20 (after angr-2j5v instrumentation landed). 11 logical categories (see j4hl-counter-dump-cli). simprocedure_fallback_by_name is the only dict-valued key — needs explicit type-handling in any non-JSON printer. reset_solver_stats() in native/angr/src/symbolic/context.rs:408 resets ALL angr-2j5v counters (vex_, mem_, concretize_, bvop_, zext_) in addition to z3_; called automatically at explore() start, so end-of-bench values are effectively delta-since-explore-start. Multi-explore() solve.py scripts will overwrite earlier counter values — known limitation.
je2xt-phase2-reseed-root-cause
remembered
angr-je2xt root cause: RustExplorationManager._phase_activate stored the angr-027h phase-2 seed states as REFERENCES (self._initial_seed_states = list(active_states)). The callback-dispatch path re-uses a seed SimState object as the Python-side mirror of its root Rust state, so every Python bounce MUTATES the seed in place (pc walks to the bounced SimProcedure; memory/constraints follow the path). _maybe_phase2_eager_retry then re-seeded copies of that consumed mid-path state: it never re-runs read(0), so it has no seed-binding equalities and no branch constraints, drifts to the find address, and lands in 'found' carrying ONLY the find gate — trivially satisfiable, posix.dumps(0) all zeros. Fix (commit 675439d33): copy at seed time, [s.copy() for s in active_states]. INVARIANT: any Python-side cache of a SimState that outlives a step must hold a copy — the bounce machinery treats the root SimState as mutable scratch.
kkpr-canonicalization-scope
forgotten
angr-kkpr (commit 65b97cec9, 2026-05-21): canonicalize commutative-op operand order at RustBV construction in add_into/mul_into/and_into/or_into/xor_into/eq_into/ne_into. Z3 mk_bv* hash-cons by AST identity but NOT by commutative-arg permutation, so add(x,y) and add(y,x) used to produce distinct Z3 ASTs. The canonical_sort_key in native/angr/src/symbolic/value.rs returns (variant_priority, sub_key): Symbolic=0/by-id, Constrained=1/by-id, Expression=2/by-Arc-ptr, Concrete=3/by-value. Concretes always sort to the right. Canonicalization runs AFTER constant-folding short-circuits. SLT/SLE/ULT/ULE/Concat/Sub/etc are NOT canonicalized (not commutative). Regression test: native/angr/src/symbolic/value.rs::tests::commutative_ops_canonicalize_operand_order.
kkpr-zdho-impact-overestimated
forgotten
Surprising measurement (angr-kkpr, 2026-05-21): commutative-arg canonicalization at RustBV construction was expected to reduce zdho's structural-duplicate rate on benches with high dup rates. ACTUAL impact: ais3_crackme 39.7%→38.1% (45 fewer ptrs, 44 fewer dups out of 1685); fauxware/csaw_wyvern/defcamp_r100 UNCHANGED (exact same numbers). Root cause: VEX lifters emit operands in CONSISTENT order from a given binary — they don't randomly permute. Permutation noise mostly arises from cross-binary symbolic-constraint shape variance, which only shows up in benches with diverse symbolic interactions like ais3_crackme. The bead's premise that 'most of zdho's 27-85% dups are commutative permutations' was OVER-OPTIMISTIC — most dups are different RustBV Arcs that Z3 then hash-cons via to_z3_ast. The actionable subset is small but real and the fix is cheap (one helper call per commutative-op fallback).
kol7-batched-constraints-api
forgotten
Batched add_constraints (angr-kol7, 2026-05-20): SymContext exposes add_constraints_raw_batch(Vec<(z3_ast_ptr, RustBV, is_true)>) at native/angr/src/symbolic/context.rs ~line 720. Single local_constraints.lock(), single solver() guard, single sat_cache invalidate, short-circuiting batched model-invalidation. Use this when you have N>1 constraints to assert from already-extracted Z3 AST pointers. Fast path in RustSolverContext::add_constraints (solver.rs:165) collects pointers up front then dispatches in one call. If ANY ast fails extract_z3_ast_ptr/claripy_to_rustbv, the whole batch falls back to the per-constraint slow path so semantics stay identical.
kwpi2-baseline-drift-pre-existing
forgotten
angr-kwpi.2 benchmark verification (2026-05-25): the failing regression benches (google2016_unbreakable_0 1.04s vs baseline 0.88s; whitehatvn2015_re400 1.43s vs 1.23s; ais3_crackme 0.97s vs 0.84s) are STALE-BASELINE drift, not regressions caused by lazy materialization. Confirmed by stashing the change and running the same 5-sample medians on commit 80046806f — identical results. Sub-second benches still noise-dominated as baseline-timings-stale-2026-05-14 and benchmark-regression-noise-floor noted. Validation pattern: stash diff, re-run 3-5x, compare medians.
l9h7-add-state-profile-2026-05-11
forgotten
REAL warm _add_rust_state on fauxware (cProfile, 50 samples, 2026-05-11): 17.0ms total per call, dominated by _sync_registers_to_rust 7.8ms (46%) + _sync_memory_to_rust 7.2ms (42%, of which _overlay_relocated_sections=6.1ms). _extract_symbolic_pages was only 1.56ms (9%) — the bead's '0.95ms = 79%' claim from add-state-init-breakdown-2026-05-11 was wrong about share but right about being wasted work. Optimizing symbolic_pages saves ~1.3ms (7.5%); the next big wins are register and memory sync.
latent-nightly-bench-broken
forgotten
Before commit 88c5b60b5, .github/workflows/nightly-ci.yml::benchmark_regression checked out angr/binaries but NOT angr/angr-examples — and tests/benchmarks/run_regression.py resolves the example corpus from ~/repos/angr-examples/examples via run_single.EXAMPLES_DIR. So in CI $HOME/repos/angr-examples did not exist and the step exited with sys.exit(2) 'ERROR: Missing examples: ...'. The fix adds an angr/angr-examples checkout + sets ANGR_EXAMPLES_DIR (now honored by run_single.py). If anyone re-introduces a CI bench step elsewhere, replicate that env var or it'll silently fail.
lazy-memory-load-overlay-fails
remembered
Lazy memory (pending_writes ITE overlay on loads) does NOT work for sym-write. Every VEX load iterates all pending writes, building ITE chains. Since pending writes have symbolic addresses, concrete loads can't rule out overlap without Z3. Net effect: O(n) work per load where n = pending writes count, vs O(n) per store in eager mode. Loads outnumber stores → worse performance. The right approach for sym-write is NOT a per-load overlay but either (1) Python-style lazy symbolic memory with native MultiwriteAnnotation support, or (2) keeping eager ITE chains and optimizing the Z3 concretize calls.
lazy-memory-sidecar-architecture
forgotten
Lazy symbolic memory (angr-czph Phase 1) follows a sidecar architecture, NOT an enum-per-byte design. The design doc says 'Add Multi variant to byte-cell enum in memory/page.rs' but the actual byte representation in page.rs is concrete bytes + Option<Box>, with symbolic data stored in SymbolicMemory.symbolic_objects (FxHashMap<addr, RustBV>). To match that pattern, Phase 1.1 (commit ca509f925) added: (a) a separate multi_objects: FxHashMap<u64, MultiPayload> sidecar on SymbolicMemory, parallel to symbolic_objects, and (b) a multi_bitmap on MemoryPage parallel to symbolic_bitmap. A byte is Multi when its page bit is set AND multi_objects has an entry at that address. set_multi_alternatives() is the only public path that installs Multi cells; it auto-maps the page, clears any conflicting symbolic_objects/spans entry, and calls record_mem_ite_depth(len) to satisfy invariant-mem-ite-depth-counter.
lazy-solver-materialization
forgotten
SymContext::fork() now uses lazy solver materialization. solver field is Optionz3::Solver, starts as None. Materialized on first access via self.solver() helper method (uses parking_lot::MutexGuard::map for ergonomic access). Z3 assertions are cached in z3_assertions_cache and replayed during materialization. This eliminates O(n) replay cost for states that never query the solver (pruned/avoided/deadended). Committed cb3dac5bd.
lazy-solves-late-binding
forgotten
LAZY_SOLVES set after RustExplorationManager construction (e.g. sm.one_active.options.add(LAZY_SOLVES)) was not propagated to Rust interpreter. Fixed by re-checking _state_cache at explore() time (commit 25b3244f4). Also: init cache (in-process and disk) must preserve user globals and LAZY_SOLVES option from original state.
lazy-store-gate-removed-phase43
forgotten
Lazy symbolic memory store gate is GONE as of HEAD. Phase 2 (angr-qh5u, bce90fef4) added SymbolicMemory::use_multi_cell_stores default-OFF, but Phase 4.3 (angr-mmdh.3, commit e5c594fe1, 2026-05-15) ENABLED Multi-cell stores by default and removed the flag + its getter/setter/PyO3 bindings + the dead eager store_conditional_multiple helper. Multi-cell lazy stores are now the ONLY path for Multiple/Strided symbolic-address store results. Phase 4.1 (wider-load cache, angr-mmdh.1) + 4.2 (flush coalescing, angr-mmdh.2) closed the load-time/Z3 gap that justified the gate. Any doc/task citing 'use_multi_cell_stores' or 'gated default-OFF' for stores is stale -- the angr-miac task description itself was.
lazy-z3-ast
forgotten
Lazy Z3 AST for Expression nodes: removed ast field, added build_z3_ast() that recursively builds from op+operands. Also binary_regions use Arc<Vec> for O(1) clone per step. Commit 779a10442. No performance regression on fauxware/ais3/defcamp.
leak-check-baseline-mma-howtouse
forgotten
leak-check baseline (mma_howtouse, 2026-06-03 commit 9eb1edf52): peak_rss after each main() across N=10 iters is ~231 MB throughout, ratio iter10/iter1 = 1.002. Wall-time per main() ~4.3-4.6s. Three iters gives 1.001; five iters gives ~1.001; ten iters gives 1.002. Confirms post-293aa8163 cleanup() path holds RSS flat across repeated Callable workloads. Use as reference baseline when tuning --threshold for new Callable-heavy benches added to run_leak_check.py.
legacy-engine-unused
forgotten
Legacy non-callback interpreter (interpreter.rs + engine.rs RustVEXEngine PyO3 class) is unused by production code — angr/exploration/rust_manager.py calls interpreter_cb's CallbackInterpreter exclusively. RustVEXEngine is registered to PyO3 module but no Python imports it. When cleaning up dead code in interpreter.rs / engine.rs, only the cargo unit tests + engine.rs's from_result() actually consume ExecutionResult.
legacy-vex-interpreter-deleted
forgotten
Legacy VEX interpreter (VEXInterpreter in interpreter.rs + RustVEXEngine pyclass) deleted 2026-05-31 via commit 25c3a3dd3 (angr-febn / B26). Replacement for the three typed-error tests is execute_irsb_for_test (in engine.rs, exposed as angr.rustylib.vex_engine.execute_irsb_for_test) which constructs a CallbackInterpreter with empty PythonCallbacks::new() and runs a single block via the new pub fn CallbackInterpreter::execute_block in interpreter_cb/execution.rs (thin delegate to the private execute_block_with_callbacks). CbExecutionError -> RustExecError mapping lives in engine.rs::cb_execution_error_to_typed (replaces the prior execution_error_to_typed). RustVEXEngine had no live callers in angr/ Python source — was only exercised by the three TestRustExecutionErrorHierarchy cases at test_rust_exploration.py:11498/11543/11585. CallbackInterpreter was a strict superset already (CAS/DCAS, dirty helpers, VECRET/GSPTR, pending stores, prefetch, deferred forks; safer CCall fallback via NeedPythonFallback instead of legacy concrete-0 silent corruption — defensive fix in angr-ppgx was made superfluous by this deletion).
libafl-0153-api-migration
forgotten
libafl 0.15.3 API drift fixed in fuzzer feature (angr-ckzr): (1) the separate SetTimeout executor trait was removed — set_timeout is now a required method on HasTimeout itself (libafl src/executors/mod.rs trait HasTimeout has both timeout() and set_timeout()); merge the impl. (2) libafl::inputs::NopToTargetBytes was renamed NopBytesConverter (StdFuzzer's 3rd type param IC; StdFuzzer::new yields StdFuzzer<CS,F,NopBytesConverter,NopInputFilter,OF>). CI clippy + .claude/hooks/cargo-check-stop.sh now run --all-features again.
libvex-callee-addr-normalize
forgotten
IRCallee.addr must be normalized to 0 in the native libVEX marshaller (marshal_callee in native/angr/src/vex/libvex_lifter.rs). The C IRCallee.addr is the HOST address of the VEX helper (e.g. the in-process pointer to amd64g_calculate_condition) -- non-deterministic across loads and UNUSED by the Rust interpreter, which dispatches CCalls by cee.name. The pyvex serializer (rust_irsb_serializer._serialize_cee) hardcodes addr:0, so the native path must too or corpus IRSB parity diverges on any block with a CCall (surfaced by angr-3s5js.4 on fauxware block @0x400490, amd64g_calculate_condition). Same applies to Dirty callees. See [[libvex-corpus-parity-gate]].
libvex-corpus-parity-gate
forgotten
libVEX-FFI corpus parity gate (angr-3s5js.4): native/angr/src/vex/libvex_corpus_tests.rs replays a checked-in block corpus through BOTH NativeLibVEXLifter.lift(bytes) AND pyvex_bridge::deserialize_irsb(json), asserting structural IRSB equality. Corpus is MULTI-ARCH since angr-qwyti.20: 77 AMD64 + 1 each ARM/ARM64/MIPS32/MIPS64 blocks (fn arch_from_str maps the corpus arch string to VexArch and panics on anything else). IRSB/IRStmt/IRExpr do NOT derive PartialEq, so equality is compared via derived Debug: format!("{:#?}", irsb) string-compare (both paths target the same vex::ir shape -> identical trees render identically; also catches field-order/variant drift). Fixture native/angr/tests/fixtures/libvex_corpus.json generated by gen_libvex_corpus.py: lightweight recursive-descent (project.factory.block(addr) + irsb.constant_jump_targets, no CFG) over fauxware/ais3_crackme/r100, dumping {addr,arch,bytes(hex),pyvex_json} where pyvex_json comes from the SAME serialize_irsb production _cb_lift_block uses. Gate: 100% parity. Test is feature-on-only: cargo test --features libvex-ffi corpus (needs Z3_SYS_Z3_HEADER + PYVEX_FFI_LIB_DIR + LD_LIBRARY_PATH per libvex-lift-params memory). See invariant-libvex-native-lifter-arch-support.
libvex-corpus-pr-lane
remembered
libVEX corpus parity gate (native-vs-pyvex IRSB equality, native/angr/src/vex/libvex_corpus_tests.rs) now runs in TWO places: nightly-ci.yml::libvex_ffi_tests (unconditional) AND a dedicated PR-time lane .github/workflows/libvex-corpus.yml gated on 'pull_request: paths: native/angr/src/vex/**' + corpus fixture + build.rs (angr-qwyti.21). Rationale: the corpus test is a single sub-second #[test]; the entire cost is the --features libvex-ffi build (~2min cold) + pyvex link, so path-gating to VEX-op PRs (not a corpus subset) is the lever. If you edit VEX lift/opcode code, expect this extra PR check.
libvex-corpus-synthetic-blocks
forgotten
libvex_corpus.json (the libVEX-FFI Stage-1 parity gate fixture) is generated by a recursive-descent walk of a real AMD64 binary, which reaches ZERO SSE/AVX-const blocks — so the corpus alone never exercised V128/V256 const marshalling and the gate was green against a broken marshaller (angr-op0dn.2.1). gen_libvex_corpus.py now appends SYNTHETIC_BLOCKS: hand-assembled byte sequences lifted via pyvex.lift + the same serialize_irsb, merged into the fixture by (addr, bytes). When adding coverage for an IR shape a normal binary never emits, add a SYNTHETIC_BLOCKS entry rather than hunting for a binary that contains it. Real AMD64 only emits the all-zero / all-ones restricted-vector patterns, so mixed patterns need direct unit tests (libvex_lifter_tests.rs::test_expand_v128_pattern).
libvex-ffi-build-scaffolding
forgotten
libVEX FFI Stage-1 build scaffolding (angr-3s5js.1, commit d4902afac) is DONE. Non-default cargo feature 'libvex-ffi' in native/angr/Cargo.toml. build.rs::configure_pyvex_ffi (gated on CARGO_FEATURE_LIBVEX_FFI) calls find_pyvex_lib_dir (mirrors find_z3_pkg_from_python: honors PYVEX_FFI_LIB_DIR override, else python3 -c 'import pyvex' + /lib, checks libpyvex.so/.dylib) and emits rustc-link-search + link-lib=dylib=pyvex + rpath. setup.py::_resolve_pyvex_libdir sets PYVEX_FFI_LIB_DIR (mirrors _resolve_z3_header). To build the feature via direct cargo: set Z3_SYS_Z3_HEADER=/usr/include/z3.h + PYVEX_FFI_LIB_DIR=/site-packages/pyvex/lib, cargo build --features libvex-ffi. NEXT: angr-3s5js.2 = FFI decls (bindgen) from pyvex.vex_ffi.ffi_str cdef.
libvex-ffi-ci-lane
remembered
libVEX-FFI parity gate now HAS a CI lane (angr-kl5qt, commit eb960a28b): .github/workflows/nightly-ci.yml job 'libvex_ffi_tests' runs 'cargo test --release --features libvex-ffi --lib libvex' after 'pip install pyvex'; locally use 'make test-libvex'. Nightly, not PR-time, because of the pyvex install + libpyvex.so link cost — PR-time coverage is only 'cargo clippy --all-features', which COMPILES vex/libvex_lifter.rs + libvex_corpus_tests.rs but never executes them. build.rs::find_pyvex_lib_dir auto-locates libpyvex.so via 'python3 -c import pyvex' (no PYVEX_FFI_LIB_DIR needed when a venv pyvex is importable). Gate is 8 tests: 7 libvex_lifter::tests + corpus_tests::test_corpus_native_vs_pyvex_structural_parity. NOTE the fuzzer feature's corpus tests (src/fuzzer/corpus.rs) are still unexecuted by any lane.
libvex-ffi-decls-bindgen
forgotten
libVEX FFI Stage-1 .2 DONE (angr-3s5js.2, commit 85efd11a9): FFI decls generated by bindgen, all behind the libvex-ffi feature (DEFAULT-ON since 807bfd74d / angr-3trr7 — setup.py::rust_features appends it; opt out with ANGR_LIBVEX_FFI=0). Vendored cdef: native/angr/vendor/pyvex_ffi.h (from pyvex.vex_ffi.ffi_str, pin 9.2.209; regen via tools/regen-pyvex-ffi-header.py — prepends #include <stddef.h> for size_t). NOTE: the script's emitted banner text and vendor/pyvex_ffi.h must be edited in LOCKSTEP or the vendored file drifts from what regen writes (angr-656mp). Cargo.toml: libvex-ffi = ["dep:bindgen"] + [build-dependencies] bindgen={version="0.72",optional=true} (same major z3-sys already locks; only prettyplease added to Cargo.lock). build.rs::generate_pyvex_ffi_bindings (cfg libvex-ffi; no-op cfg(not) sibling) runs bindgen with allowlist roots vex_lift/vex_init/register*/VEXLiftResult/IRSB/VexArch/VexArchInfo + allowlist_recursively + NewType enums (FFI-safe vs rustified) + layout_tests(false), writes OUT_DIR/pyvex_ffi_bindings.rs. vex/libvex_ffi.rs include!()s it; wired in vex/mod.rs #[cfg(feature=libvex-ffi)]. Generated file ~1914 lines: _VEXLiftResult{exits[400],const_vals[1000]}, IRSB, IRStmt/IRExprTag, ExitInfo/DataRef/ConstVal. Build recipe: Z3_SYS_Z3_HEADER=/usr/include/z3.h PYVEX_FFI_LIB_DIR=/pyvex/lib cargo build --features libvex-ffi.
libvex-ffi-default-on
forgotten
libvex-ffi is ON by default as of angr-3trr7 (commit 807bfd74d, human GO 2026-07-15). setup.py::_rust_features returns ['libvex-ffi'] UNLESS ANGR_LIBVEX_FFI in {0,false,off,no} (opt-OUT escape hatch) OR PYVEX_FFI_LIB_DIR is unset (no libpyvex.so next to pyvex, e.g. Windows -> graceful degrade to callback lift). Stock pip install -e . and released wheels now link the native libVEX lifter on ELF/Mach-O; use_native_lift default True + libvex_ffi_enabled() both satisfied on a stock AMD64 .so. Wheels: wheels.yml excludes libpyvex.so from auditwheel (Linux) / delocate (macOS) repair and relativizes RUNPATH to $ORIGIN/../pyvex/lib (@loader_path/../pyvex/lib on macOS), exactly mirroring the libz3 handling -- so the wheel resolves the USER's pyvex, not a vendored 2nd libVEX. Build-system gotchas from the opt-in era still hold: setuptools-rust pyproject table cannot express a conditional feature (mutate ext.features in build_rust.run()); do NOT declare via setup(rust_extensions=[...]) in this hand-assembled venv.
libvex-ffi-enum-parity-tripwire
remembered
native libVEX loadg_op (vex/libvex_lifter.rs) cannot mirror pyvex_bridge::parse_loadg_op's defensive ILGop_{16,32}{U,S}to64 arms: the JSON path matches strings so it can accept tags that don't exist yet, while the native path matches bindgen constants generated from native/angr/vendor/pyvex_ffi.h -- which declares only 8 ILGop_* tags (INVALID + IdentV128/Ident64/Ident32/16Uto32/16Sto32/8Uto32/8Sto32, implicitly numbered from 0x1D00). Naming a nonexistent variant is a compile error, so parity there is unachievable by construction. The tripwire instead: test_vendored_header_ilgop_variant_set_is_unchanged in libvex_lifter_tests.rs include_str!s the vendored header, scrapes every ILGop_* token, and diffs against VENDORED_ILGOP_TAGS -- so a tools/regen-pyvex-ffi-header.py pin bump that grows the enum fails loudly instead of silently degrading LoadG to IRLoadGOp::Unknown (which drops the widening). Same pattern applies to any other bindgen-enum-vs-string dual-path divergence in vex/.
libvex-ffi-feature-gated-verification
forgotten
vex/libvex_lifter.rs + vex/libvex_ffi.rs are gated behind the non-default cargo feature 'libvex-ffi' (Cargo.toml: libvex-ffi = [dep:bindgen]; vex/mod.rs #[cfg(feature)]). The default clippy gate (cargo clippy --all-targets -D warnings, no --features) and the pytest suite do NOT compile or exercise these files. To verify any change to the NativeLibVEXLifter marshallers, run: cargo clippy --manifest-path native/angr/Cargo.toml --features libvex-ffi --lib -- -D warnings, and cargo test ... --features libvex-ffi --lib libvex (6 unit+corpus tests). All 11 marshaller fns are private unsafe fns so clippy::missing_safety_doc never fires on them; the only missing_safety_doc allow is in libvex_ffi.rs guarding include!'d bindgen output (module-scoped, non-narrowable).
libvex-ffi-stage1-done
forgotten
libVEX-FFI Stage-1 (angr-3s5js) DONE + closed iter47. NativeLibVEXLifter lives behind non-default cargo feature 'libvex-ffi' (Cargo.toml: libvex-ffi=["dep:bindgen"]) in native/angr/src/vex/libvex_lifter.rs, slotting into the VEXLifter trait beside the pyvex-callback path. Verify with: cargo test --manifest-path native/angr/Cargo.toml --features libvex-ffi --release test_corpus_native_vs_pyvex_structural_parity (in vex::libvex_lifter::corpus_tests; source native/angr/src/vex/libvex_corpus_tests.rs). Gate: 100% structural IRSB parity on 65-block AMD64 corpus (fauxware/ais3/r100) vs pyvex_bridge::deserialize_irsb. marshal_callee normalizes IRCallee.addr->0 to match pyvex. NEXT: umbrella angr-z087y still open for Stage 2 (flag-gated engine use) + Stage 3 (AMD64 default); downstream gorvf.4 zero-bounce milestone. Feature is NOT wired into the engine yet.
libvex-ffi-stage1-feasibility
forgotten
libVEX FFI Stage-1 (angr-z087y) is feasibility-GO: a native NativeLibVEXLifter needs NO from-scratch lifting and NO separate libVEX build. (1) The installed pyvex ships pyvex/lib/libpyvex.so which exports pyvex's exact-config shim vex_lift + vex_init AND the raw LibVEX_* API -> link the SAME .so via build.rs (search=pyvex/lib, link-lib=dylib=pyvex, rpath), gated behind a NON-default cargo feature so the default build/bench-gate takes no new libpyvex.so rpath dependency. (2) The wheel ships no C headers, but pyvex.vex_ffi.ffi_str (~44KB) is the AUTHORITATIVE cdef: vex_lift signature (VexArch guest, VexArchInfo archinfo, insn bytes, max_insns/max_bytes, opt_level, traceflags, ...px_control, lookback) returning VEXLiftResult* {IRSB* irsb; ...; ExitInfo exits[400]; inst_addrs[200]; DataRef data_refs[2000]; ConstVal const_vals[1000]} plus IRSB/VexArchInfo/ExitInfo layouts -> use as bindgen source. (3) Link target == the in-process lifter pyvex loads -> byte-for-byte IRSB parity, no opt/arch/endness divergence. Marshal VEXLiftResult->irsb into vex::ir::IRSB (same shape as pyvex_bridge.rs::PyVexIRSB but from the C struct not JSON), via the existing VEXLifter trait seam in vex/lifter.rs. RISKS: libVEX arena clobbers VEXLiftResult* on next vex_lift (copy out first); cdef ABI is pinned to pyvex==9.2.209 (regen on any bump); libVEX globals are non-reentrant (per-worker lock for parallel). Verifier: tools/probe-libvex-ffi.py (asserts all 3, exits nonzero on NO-GO). Full recipe+plan: docs/advanced-topics/rust_libvex_ffi.rst. Impl remainder = bead angr-3s5js.
libvex-lift-params
forgotten
NativeLibVEXLifter::lift calls vex_lift with pyvex _lift defaults (disassembled from pyvex/lifting/libvex.py): opt_level=1, traceflags=0, allow_arch_optimizations=1, strict_block_end=0, collect_data_refs=0, load_from_ro_regions=0, const_prop=0, px_control=VexRegUpdUnwindregsAtMemAccess (cross_insn_opt=True branch), lookback_amount=0, max_insns=99, max_bytes=min(slice_len,5000). AMD64 archinfo from archinfo.ArchAMD64().vex_archinfo: hwcaps=0, endness=VexEndnessLE, x86_cr0=0xFFFFFFFF, hwcache_info{num_levels:0,num_caches:0,caches:NULL,icaches_maintain_coherence:1}, rest 0. For the .4 parity harness these MUST match whatever the Rust engine's actual pyvex lift_block callback passes; if parity diverges, re-check allow_arch_optimizations and hwcaps first (hwcaps=0 means baseline SSE only, no AVX). V128/V256 IRConst are marshalled as raw u16/u32 zero-extended (Stage-1 stub) — exact expansion deferred to .4.
libvex-marshal-enum-name-bridge
forgotten
NativeLibVEXLifter (native/angr/src/vex/libvex_lifter.rs, libvex-ffi feature) marshals C VEXLiftResult->irsb into vex::ir::IRSB by reusing opcode_map.rs string parsers (parse_opcode/parse_type/parse_jumpkind/parse_endness). The bridge: build.rs::generate_pyvex_ffi_enum_names parses the vendored cdef (vendor/pyvex_ffi.h) enum blocks and emits reverse discriminant->name tables (irop_name/irtype_name/ijk_name/iend_name) into OUT_DIR/pyvex_ffi_enum_names.rs, included as libvex_ffi::enum_names. Marshaller reads raw C tag (.0 on bindgen NewType), looks up name, calls parser. Single source of truth = same header bindgen consumes, so parity-safe. Structural target shape mirrors pyvex_bridge::convert_* node-for-node.
libvex-multiarch-corpus
remembered
libVEX multi-arch corpus (angr-qwyti.20): the non-AMD64 corpus was NOT network-blocked as prior ralph handoffs claimed — pyvex.lift(bytes, addr, archinfo.ArchX()) lifts ARM/ARM64/MIPS locally, no network. Two non-obvious facts made Stage-2 easy: (1) across all arches archinfo.ArchX().vex_archinfo is identical except endness (LE for ARM/ARM64/AMD64, BE for MIPS), so archinfo_for(arch) in native/angr/src/vex/libvex_lifter.rs just varies VexEndness; (2) marshal_irsb() hardcoded arch: VexArch::AMD64 — it must thread the real arch or IR bodies match but the arch field diverges. gen_libvex_corpus.py dedup key must include arch (MIPS32/MIPS64 share addr+bytes encoding). Corpus tests are #[cfg(feature=libvex-ffi)], default-off, so ralph's default cargo-test gate does NOT cover them — run 'make test-libvex'.
libvex-stage3-default-on
forgotten
z087y Stage-3 DECISION (angr-op0dn.2.2, commit 63d201a88): RustExplorationManager use_native_lift now defaults TRUE, still double-guarded by libvex_ffi_enabled() + arch==AMD64, so a stock build (libvex-ffi is NOT a default cargo feature) is inert. Evidence, feature-on cargo build, run_single.py --native-lift vs --no-native-lift, 3+ reps: codegate_2017-angrybird 4.33-4.42 -> 3.68-3.94s (~13%); ekopartyctf2016_rev250 2.53-2.75 -> 2.40-2.47s (~7%, 221 native / 0 fallback, gil_work 356ms->198ms); xmllint_getenv 3.84 -> 3.79s (135/0, gil 96->65ms); fauxware/mma_howtouse/cow_fork_scaling flat; unbreakable_1 within bimodal noise. NOTHING regressed, found unchanged. Real payoff is the 30-45% GIL-work drop (parallel-warmup serialization point). Full table in docs/advanced-topics/rust_libvex_ffi.rst 'Stage-3 verdict'. To A/B: cargo build --release --features libvex-ffi (Z3_SYS_Z3_HEADER=/usr/include/z3.h) then cp target/release/librustylib.so angr/rustylib.cpython-312-x86_64-linux-gnu.so.
libz3-soname-mismatch-root-cause
remembered
Rust .so linking against wrong libz3 SONAME (libz3.so.4 from /lib/x86_64-linux-gnu vs libz3.so from venv) causes silent segfaults when Rust touches Python claripy AST via shared Z3 context. Two libz3 mappings = two contexts = invalid AST handles. Fix: -L native=/lib/python*/site-packages/z3/lib in .cargo/config.toml so z3-sys links the venv copy with SONAME=libz3.so. Verify via: ldd target/release/libangr_native.so | grep z3 → should be libz3.so (NOT libz3.so.4). RPATH alone is not enough — link-time SONAME determines what dlopen looks for at runtime.
license-timeout-fread-root-cause
forgotten
asisctffinals2015_license rust TIMEOUT — FINAL root cause + fix (supersedes license-timeout-fread-root-cause's 'z3 churn' theory, which was WRONG). The hang is NOT z3/130-asserts and NOT the Python fread. Real cause: the Rust manager's Python-init-to-main path (_run_python_init_if_needed) hits the init cache (_try_disk_init_cache / try_in_memory_init_cache), which is built from a plain blank_state with an EMPTY filesystem. apply_state_metadata copies constraints/options/globals but NOT the fs plugin, so the cached callback state's state.fs is empty. fopen then takes the ALL_FILES_EXIST branch and mints a fresh symbolic-size SimFile (filesize_file_1* unbounded). fseek(SEEK_END)+ftell returns that unbounded symbolic size (max 2^63-1); fread(dst, size*nm) over-reads, spawning tens of thousands of fill BVS -> 4GB RLIMIT_AS blowup -> timeout. Pure-Python keeps the user-inserted SimFile (size 0x22=34) so fread size is CONCRETE 34 and finishes ~1.7s. FIX (commit 00a1bb3dc): rust_disk_cache._state_has_user_symbolic now also returns True when state.fs._files is non-empty, disabling the init cache when the user inserted SimFiles. RESOLVED 2026-07-03 (angr-0xyq2 Phase 3+4): the bench now PASSES under Rust ~0.9s vs Python ~1.7s (~1.9x), catalog rust_ok=True. Two unlocks: (1) bounded-symbolic-file export (_export_fs_files_to_rust) registers the 34-byte SimFile in the Rust FileSystem, so read/fread serve natively and fstat/ftell see concrete sizes (effective_len) — the symbolic-fread-size explosion is gone by construction; (2) solve.py's inline strlen (SIM_PROCEDURES execute on the found state) needed _LazySimStateRef to delegate _inspect — fixed via _MATERIALIZING_PRIVATE allowlist in rust_state_export.py.
lineage-chase-final-verdict
forgotten
Lineage chase finally closed 2026-05-25 (angr-0hdq + 2 children + angr-1gfa). All four beads converged on the verdict that materialize-cost win from angr-hk7k is NOT capturable in current engine architecture. What landed and survived:
-
Simple-variant dismantle (commit 971d35545, angr-v5ht) — opt-in via use_shared_lineage_solver=True only. Detects baby-re-shaped (LOSE) workloads via hot-cache hit rate <35% within 10/20-window, then disables the lineage solver path. 35% threshold justified by N=4 data: LOSE=30.2%, WINs=44.7/51.0/84.2% (4.8pp/9.7pp margins; see v5ht-threshold-justification-2026-05-25 memory).
-
Doc verdict in docs/advanced-topics/rust_engine.rst:456 'Shared-lineage Z3 solver — rejected' (175 lines, commit 14709cea8).
-
Behavioral tests in native/angr/src/exploration tests (cold/hot/overhead).
What was REMOVED: native/angr/src/symbolic/lineage_assumptions.rs (alt-d spike; recover via git show 561aa838b if ever needed).
Default state: use_shared_lineage_solver=False in rust_manager.py:753. Don't reopen this chase without new measurement data showing the architectural prior has changed (BFS-thrash + 15.6% hot-cache hit rate ≈ theoretical 1/N for N=5-10 active states).
lineage-hot-ratio-measurement-recipe
forgotten
Recipe for measuring hot-cache hit ratio under use_shared_lineage_solver=True: python tests/benchmarks/run_single.py <example> --engine rust --use-shared-lineage-solver --counters-json | grep -E 'lineage_switch_count|lineage_switch_hot_count|lineage_dismantle|lineage_sample'. Ratio = lineage_switch_hot_count / lineage_switch_count. If lineage_dismantled=1 the ratio reflects the pre-dismantle window; if 0 the ratio is the natural workload hot-cache rate over the whole run. lineage_sample_call_count / lineage_sample_decision_count tell you how many sampler decisions fired (sampler hooks every 10 ticks, decides once min_switches=20 events have accumulated).
lineage-rejection-doc-location
forgotten
User-facing SharedLineageSolver rejection rationale lives in docs/advanced-topics/rust_engine.rst, section 'Shared-lineage Z3 solver — rejected' (top-level, between ANGR_Z3_TACTIC and SimOption matrix). Any future lineage-architecture proposal must cite this section and explain how it discriminates WIN vs LOSE on multiple benches (not just one). Bead trail: angr-v5a5 (spike rejected), angr-3ms1 (alternatives enumerated), angr-v5ht (simple-variant landed), angr-0dgq (full-variant reverted). Landed in commit 14709cea8 (2026-05-25, angr-0hdq.1).
link-register-subcall-blocker
remembered
RESOLVED 2026-08-01 by angr-9ke6b.3 (commit 5d2a37db2) — link_register() IS now wired on ARMEABI/AArch64CC/MipsO32/MipsN64, and native sub-calls work there. The historical blocker: RustExplorationManager::get_return_addr (exploration/helpers.rs) read [sp] unconditionally, and the serial NativeProcDisposition::SubCall arm in run_loop_single.rs feeds it into NativeSubcall::caller_return_addr, which the resume path installs as the PC — on a link-register ABI that is stack garbage, and the SubcallSetupError::UnsupportedAbi bail was the only guard. FIX: get_return_addr now calls calling_convention.get_return_addr(state.registers(), None, &ctx) first and only falls back to reading [sp] when pops_return_addr() is true, so amd64 is byte-identical (the CC declines with memory: None, same as the Returned arm of step_one). INVARIANT to preserve: any new consumer of a caller return address on the serial path must go through RustExplorationManager::get_return_addr, never a raw [sp] read. Regression coverage: exploration::stepping::subcall_tests::link_register_abi_subcall_roundtrip_resumes_at_lr (poisons [sp] on all four LR arches).
list
forgotten
list
live-pending-callback-test-harness
forgotten
To de-vacuify a test that needs a LIVE pending_callback (with_pending succeeds only while self.pending_callback is Some): use _RustExplorationManager directly, register a SimProcedure at a HOOK addr, set the arg register SYMBOLIC (set_register_symbolic on rdi via claripy.backends.z3.convert(BVS).as_ast().value), then mgr.run(N). The run loop's SimProcedure Python fallback (run_loop.rs sets PendingCallback::with_context) fires ONLY for symbolic args — a symbolic arg makes extract_concrete_arg yield SymbolicArgument, so the native callable is NOT invoked and a live pending_callback is established. After run returns event.callback_reason=='simprocedure', pending_memory_load_symbolic_page reads the forked active state's memory. GOTCHA: add_state (state_lifecycle::_add_state) FORKS the supplied state and mints a fresh monotonic id, so state.state_id is NOT the manager's id — use mgr.get_state_ids('active')[0] for any state_memory_store_symbolic_multi/query call. PyRustSimState does NOT expose memory_store_symbolic_multi to Python; only the manager does (state_memory_store_symbolic_multi).
load-lazy-inner-extract-le-bias
forgotten
memory/load.rs site 2 (~line 675-683) and site 1 (~line 209-227) use DIFFERENT extract semantics for the wider-symbolic-containment case. Site 1 honors endianness (Endness::Big and Endness::Little branches, mirror of angr-v1q2 fix). Site 2 uses ONLY the LE-style extract: high=(byte_offset+size)8-1, low=byte_offset8, regardless of memory endianness. This is a pre-existing inconsistency in load_concrete_lazy_inner. angr-uwtj preserved both behaviors when introducing the containing_wider_sym helper. If a BE memory ever hits site 2 with a wider sym containment, the extract returns wrong bytes. Real reachability of site 2 is also rare (same dead-code analysis as site 1).
load-prefetch-removed
remembered
Load-prefetch machinery (PrefetchedLoad, load_prefetch_cache, use_load_prefetch, set_load_prefetch, prefetch_loads_for_block, scan_loads_in_, try_eval_expr_concrete, get_prefetched_load, clear_prefetch_cache, 4 prefetch__scratch bufs, prefetch_time_ns stat) was REMOVED as dead code in commit f806633d0 (angr-4xaga.4). It was default-off with zero non-test callers so prefetch_loads_for_block early-returned every block. The LIVE page-prefetch path (fetch_page_with_prefetch, get_nearby/eager_prefetch_list, page_prefetch_count, get_region_prefetch_list) is DISTINCT and untouched. Store-side SMC-invalidation helpers formerly update_prefetch_on_store/invalidate_loads_at were stripped of cache lines and renamed invalidate_code_on_store/invalidate_code_at_store (statements_store.rs) — they still do IRSB self-modifying-code invalidation.
load-shellcode-blob-binary-none
remembered
load_shellcode creates a cle Blob object whose .binary attribute is None (in-memory bytes, not from a file). The default _load_binary_regions in rust_manager.py SKIPS Blob objects via the 'if obj.binary is None: continue' guard at line 1647-ish. Effect: shellcode-based tests don't get binary regions registered with the Rust engine, so is_in_binary returns False everywhere and JMP/Ret targets are treated as UnmodeledCalls (P21 generic skip). Workaround for tests: call mgr._rust_mgr.load_binary_regions([(base, bytes)]) AFTER manager init to register the shellcode manually. Discovered while writing an SMC integration test for angr-k67f.
loop-gate-bench-status
forgotten
As of 2026-05-07 16:24 the benchmark gate (run_optimization_loop.run_benchmark_gate, called after each commit) works end-to-end: passed=12, failed=0, gate_broken=False, duration ~19.6s. PYTHONPATH=REPO_DIR is set in the systemd-run scope env so multiprocessing spawn workers can import angr despite the editable install having no .pth file. Verified by direct invocation 2026-05-07 16:24 against the current source. Premise of angr-p5hl ('fix didn't land') was wrong — the fix did land; the previous orchestrator process was running stale bytecode.
loop-head-policy-caplg
forgotten
angr-caplg LoopHeadRoundRobin selection policy (selection_policy.rs): round-robin over (loop-head, callstack-class) buckets to prevent a looping state starving siblings. bucket_key() folds loop-head (most-frequent addr in state.history(), matching LoopBound's exceeds_loop_bound frequency test; falls back to state.pc() when no addr repeats) with the return-addr chain from state.call_stack() (same signal record_reconvergence_sample uses). select() picks the least-dispatched bucket (Mutex<HashMap<u64,u64>> counts, strict < → FIFO tie-break) and bumps it. KEY: both signals are per-state accessible inside the two-hook seam — NO seam widening needed, same lesson as m9fpp (see coverage-guided-policy-m9fpp, selection-policy-seam-limits). Opt-in: set_state_selection_loop_head (mod.rs, single #[pymethods] block) + set_exploration_strategy('loop_head') (rust_manager.py). Never default. Completes the a32jl.2 policy trio (random/coverage/loop_head).
loop-stdout-buffering
forgotten
run_optimization_loop.sh output is block-buffered when piped to tee, so .claude/loop-logs/iteration-*.log files show 0 bytes until the process exits. Fix: use 'stdbuf -oL bash run_optimization_loop.sh 2>&1 | tee logfile' to force line-buffered output. Or use 'script -c' as alternative. Without this fix, you cannot monitor loop progress in real-time.
loopseer-native-wiring
forgotten
angr-4arn: the native LoopSeer wiring was mostly pre-existing. register_loop_bound (mod.rs) + NativeTechnique::LoopBound enforcement (helpers.rs exceeds_loop_bound: any single addr appearing > bound times in history) already existed; only the Python translation in rust_techniques.py use_technique() was missing (LoopSeer branch was a debug-log no-op). Wired LoopSeer/LocalLoopSeer.bound -> mgr._rust_mgr.register_loop_bound(bound, discard_stash). bound=None registers nothing; bound_reached callback disables native path (can't invoke from Rust) with warning. See concretization-config-readback for the use_technique() dispatch pattern.
lru-unbounded-clone-overflow
remembered
lru-0.12.5 LruCache::unbounded() is a zero-preallocation empty cache (cap=usize::MAX, HashMap::default(), no buckets) vs LruCache::new(cap) which eagerly does HashMap::with_capacity(cap). Use unbounded() for THROWAWAY placeholder caches (e.g. the swap-dance placeholders in exploration/step_core.rs::run_interpreter_step_core). But NEVER for a cache that will be cloned: lru's Clone impl does LruCache::new(self.cap()), so an unbounded cap=usize::MAX overflows HashMap::with_capacity -> 'Hash table capacity overflow' panic. VEXInterpreter::with_config's default block_cache must stay bounded (new(BLOCK_CACHE_CAPACITY)) precisely because VEXInterpreter::fork() clones it; caught by interpreter::smc_tests::fork_inherits_dirtied_pages.
m1-presolver-tier-class-vs-site-crosstab
remembered
M1.b single-var interval decide (angr-op0dn.9.2) is KILLED, and the reason generalizes: the S1 query CLASS census (query_class.rs QueryClass) and the CheckSite census (stats.rs Z3_CHECK_SITE_COUNT) are independent tallies of the same checks, so a class bucket being non-trivial corpus-wide does NOT mean it is addressable at the sites a given tier intercepts. Cross-tab them per bench before funding any pre-solver tier: bound the addressable set by min(class_count, site_count) per bench, not by the corpus class total. For single_var_range: 27 checks corpus-wide (2% of 1320) but AT MOST 10 of the 549 min/max/eval-site checks (1.8%) — the benches with single-var queries (android_arm_license_validation 16, ais3_crackme 3) issue almost no min/max/eval checks at all; their single-var queries are branch/satisfiable checks, which is 9.1's territory and 9.1 is VOID. Numbers in the m1b_kill_gate block of tests/benchmarks/query_class_numbers.json.
make-bv-from-bytes-lsb-chunking
remembered
make_bv_from_bytes (native/angr/src/symbolic/bv_codec.rs) must chunk the right-aligned big-endian byte array from the LSB, NOT the front: a single ragged top chunk of width%64 bits (top_bytes=div_ceil(top_bits,8)) followed by width/64 aligned 64-bit chunks, mirroring make_bv_const's hi/lo split. Front-chunking (min(bits_remaining,64) from bytes[0..]) misaligns every chunk by 8-(width%8) bits when width%8!=0 (65/125/129), corrupting the wide eval_upto exclusion constant so ast!=val_ast never excludes the real witness. Round-trip make_bv_from_bytes(extract_bv_value_wide(x,w),w)==x guards it; extract_bv_value_wide needs a SIMPLIFIED numeral literal (model-eval output), not a raw concat expr. Fixed angr-ph300.34.
make-test-python-baseline-gate
forgotten
test-python-baseline make target (added 2026-06-03 in angr-r0di) is the per-PR 'we didn't break Python angr' gate. Curated 64-test subset spans tests/test_load_shellcode.py + tests/engines/{test_actions,test_hook,vex/test_lifter}.py + tests/state_plugins/{solver,posix}/* + tests/procedures/test_sim_procedure.py + tests/factory/test_callable.py. CI job python_baseline in ci.yml clones angr/binaries beside angr/ (matches tests/common.py's ../../binaries lookup) and runs uv --directory angr run make test-python-baseline. Does NOT build the Rust extension. When adding a test file: keep the total under the ~5min soft budget; update PYTHON_BASELINE_TESTS in Makefile only (CI just calls the make target). Locally requires angr/binaries cloned at $(REPO_ROOT)/../binaries; if missing, the target prints a clear git clone hint and exits 1 — CI=true env var bypasses the check (same as tests/common.py's CI gate).
mass-rename-mechanical-pattern
forgotten
Mass-rename pattern used in angr-sy4g (interpreter_cb -> interpreter + CallbackInterpreter -> VEXInterpreter, ~23 files): 1) git mv directory, 2) 'git grep -l PATTERN | xargs sed -i s/OLD/NEW/g' for each token, 3) git grep PATTERN to verify zero remaining. Works across .rs/.py/.rst/.md uniformly. Only manual cleanup needed: lines that referenced a separate deleted file (CLAUDE.md had both interpreter.rs AND interpreter_cb/ — first one was stale since angr-febn deleted interpreter.rs). cargo check + pytest verify correctness.
max-active-states-test-pattern
remembered
Test pattern that verifies max_active_states is wired up vs silently no-op: set limit=1 on a forking binary (e.g. fauxware), explore, then check counts.get('pruned', 0) > 0. Just checking active <= limit will pass even when the limit is a no-op if the binary doesn't fork enough.
medium-bench-counters-deterministic
remembered
MEDIUM-tier bench counters are deterministic (angr-lagp, 2026-06-15, commit ad716b164). 5x-per-bench soak via run_single.py --counters-json on all 7 MEDIUM COUNT_EXEMPT benches (sym-write, flareon2015_5/10, ekopartyctf2016_rev250, csaw_wyvern, codegate_2017-angrybird, mma_howtouse): 0% variance in callback_count/state_creations/steps across 5 runs. All 7 removed from run_regression.py COUNT_EXEMPT; the --check-counts gate now enforces them. Only the 4 BIMODAL_BENCHMARKS stay exempt (Z3 multi-solution path nondeterminism; hackcon2016_angry-reverser also OOM-risky so never soaked). Same silent drift bq9v found in fast tier appeared here: sym-write callback 22->20/state_creations 1->0/steps 18->20, ekopartyctf2016_rev250 callback 5->1/steps 3->7 — count baselines rot during dev; always refresh to measured values before enabling a metric gate.
mem-ite-depth-baseline-2026-05-14
forgotten
Baseline metrics for lazy symbolic memory work (angr-czph / angr-qh5u). After adding mem_ite_depth_max/mem_ite_depth_total counters in symbolic/context.rs (Phase 0 / angr-0nme): sym-write rust run = depth_max=2 depth_total=16; strcpy_find = 0/0; fauxware = 0/0. Counters track eager ITE chains built by store_strided / store_conditional_multiple / store_symbolic Multiple-branch. Phase 1 Multi cells (angr-czph) should record the same metric on Multi insertion so before/after comparison is direct. Surfaced through get_solver_stats() (Python) and printed by run_single.py under 'symbolic memory ite-depth' section.
memcpy-memset-python-boundary-test
forgotten
Native memcpy/memmove/memset had ZERO Python-boundary test coverage despite shipping extensive Cargo unit tests for symbolic byte/size/address paths (cudgw.4, e4qwq, 1yv58, 3l72p, 7ifq6). TestNativeMemoryCopyAndSet in tests/engines/rust/test_procedures.py now covers the concrete fast paths via fauxware .ctors stub + get_state_memory readback. Pattern: num_args=3 stub, rdi=dst/rsi=src(or fill-value)/rdx=size SysV, single-step, assert call_counts + dst bytes + rax==dst. get_state_memory(sid,addr,size) returns memory-order bytes and is the readback API for buffer-writing procedures. memset value arg uses only low 8 bits. The symbolic paths (symbolic addr/size) remain Cargo-only — bounded candidate-pair ITE logic is exercised directly in memcpy_tests.rs/memset_tests.rs, not cheaply reachable from a clean Python integration test.
memcpy-symbolic-address-native
forgotten
memcpy/memmove handle a SYMBOLIC dst and/or src address natively via module-level fn copy_symbolic_addr in native/angr/src/procedures/memcpy.rs (follow-up to memset-symbolic-address-native). Both NativeMemcpy/NativeMemmove now declare args dst_bv:bv, src_bv:bv (was concrete); body extracts both with extract_concrete_arg and routes to copy_symbolic_addr when EITHER is Err. Pattern: ctx.eval_upto(dst_bv, CAND+1) and eval_upto(src_bv, CAND+1) for the two bounded candidate sets; reject empty/>cap (unbounded). Cross product: for each (dst d, src s) pair build guard = dst_bv.eq(d).and(src_bv.eq(s)) (eq returns 1-bit bool, .and is logical AND on 1-bit), then per byte i: store(d+i, guard.ite(src_byte[s+i], orig)). ALL src candidate bytes snapshotted into a HashMap<u64,RustBV> BEFORE any store so overlapping src/dst copy pre-store values (memmove contract) — same reason both procs share the helper, like copy_symbolic_size. Composition correct because dst/src each equal at most one candidate so <=1 guard true per physical addr; orig reload per pair layers ITEs. Caps: MAX_SYMBOLIC_ADDR_CANDIDATES=64 each, MAX_SYMBOLIC_ADDR_COPY_SIZE=256, MAX_SYMBOLIC_ADDR_STORES=4096 (|dst|*|src|*size budget, saturating_mul). Falls back to Python (Err SymbolicArgument) on symbolic size (addr+size out of scope), unbounded candidates, or budget exceeded. Pre-existing test_memcpy_symbolic_dst (unconstrained dst, no mem mapped) still asserts Err via the unbounded-fallback branch.
memcpy-symbolic-size-native
forgotten
memcpy/memmove symbolic-SIZE handled natively (native/angr/src/procedures/memcpy.rs, fn copy_symbolic_size; NativeMemcpy/NativeMemmove). Mirrors memset-symbolic-size-native: size arg is 'bv' not 'concrete'; concrete size -> fast 8-byte-chunk copy_forward (untouched, corpus is concrete-size); symbolic size -> ctx.max(size,false) upper bound, Err(SymbolicArgument) fallback if unknown or > MAX_SYMBOLIC_COPY_SIZE (4096), else per-byte ITE(i<n, src[i], dst[i]). KEY for memmove overlap: copy_symbolic_size snapshots ALL source bytes into a Vec BEFORE any store, so it needs no backward-copy special case (concrete memmove path still uses backward copy when dst>src && dst<src+size). dst/src stay 'concrete' arg type so a symbolic ADDRESS still auto-falls-back to Python via extract_concrete_arg (symbolic-address is a separate, unstarted design). Commit 3b4aeeab7.
memory-citation-audit-iter19
forgotten
Memory-citation staleness audit (iter19, 2026-06-23, HEAD 5eeb1f45b): bd memory store is CLEAN — zero stale citations. Method (bounded, read-only, no compute): dump 'bd memories' to a file (USE grep -a / -aoE — bd dump trips grep's binary-file heuristic so plain grep silently returns nothing), regex-extract path citations matching (native|angr|tests|docs|tools)/....(rs|py|rst|json|txt|toml|sh) -> all 73 unique paths exist on disk; same check on CLAUDE.md Key Files -> all exist; extract 'fn ' symbol citations -> all real ones resolve in native/angr/src (check_ast_passthrough is a Python def in tests/smoke/wheel_ast_passthrough.py; get_or_lift_block in interpreter/execution.rs; splice_lane0_u128 in vex/ops_vec_set_lo.rs). Caveat: bd dump truncates bodies with '...' so paths/symbols past the cutoff are invisible (get_or_lift_blo / splice_lane0_u12 were truncation artifacts, not rot) and 'fn ' (arg/in/defined/signature) are prose false-positives. Re-run only after a large rename sweep; routine iterations need not repeat it.
memory-citation-rot-path-scan
remembered
Periodic memory citation-rot audit. Dump 'bd memories --json' (NOT the plain list — it truncates each body to a snippet and hides path tokens), then scan every full body. TWO passes are needed: (1) full-path tokens (native/angr/src/.rs, angr/.py, docs/.rst, tests/.{py,rs}) tested with os.path.exists; beware regex artifacts — '.rst' truncates to '.rs' if 'rs' precedes 'rst' in the alternation, and 'native/angr/Cargo.toml' substring-matches a bare 'angr/Cargo.toml' rule (both false positives). (2) CRITICAL GAP found iter28: a path-prefix-only regex MISSES bare-basename citations of a renamed file (e.g. 'test_rust_exploration.py' written without the 'tests/engines/' dir). iter27's scan used prefix-only and fixed only 3 of 20 live citations to the angr-yg2m-split monolith; iter28 caught the other 17 by also grepping the bare basename of any known-renamed/deleted file. So: for each file a refactor renamed/deleted, grep its BARE basename across all bodies too, not just full paths. Classify each hit: LIVE citation (names a current test/class/cmd that still exists -> repair to new module via 'grep -rl tests/engines/rust/', names are unique) vs HISTORICAL narrative ('formerly in monolithic X before the split', dated audit receipts citing old line numbers) -> KEEP. Repair in place with 'bd remember --key ' driven from a python script using subprocess list-args (bodies have backticks/quotes/newlines that break shell quoting). Run when offline queue is dry. Complements refactor-memory-sweep-rule.
memory-crossref-audit-clean
forgotten
Memory->memory cross-ref audit (iter62, 2026-06-25): CLEAN. Audited all 918 bd memory bodies for citations to OTHER memory keys that no longer resolve. Method: dump 'bd memories --json' (flat key->body dict), extract refs via patterns [[wikilink]], 'bd recall KEY', 'bd memory KEY', 'see KEY', 'key KEY', plus a broad pass for backtick kebab tokens ending in memory-key suffixes (-pattern/-rule/-invariant/-root-cause/-decision/-method/-audit/etc). Cross-check each ref against the valid-keys set. Result: 0 unresolved key citations. The only 3 raw hits were false positives (generic words 'both'/'each' after 'bd recall', and a literal '[[wikilink]]' mention inside docs-rst-crossref-audit-clean). Completes the citation-hygiene sweep series: docs file-paths (iter58), bd-memory file-paths (iter59), bd-memory symbol anchors (iter60), docs .rst cross-refs (iter61), memory->memory cross-refs (iter62) -- ALL CLEAN. No remaining un-swept hygiene surface absent a rename wave.
memory-load-store-variant-matrix
remembered
The memory load_/store_ entry-point families are now classified in a four-axis matrix (permission check / unmapped-page handling / auto-map / Multi-aware) living in the memory::load and memory::store MODULE DOC HEADERS (angr-9ke6b.102, commit ac5a468ac); memory::mod points at both. Read it before adding a variant or a cross-cutting feature and add a row when you do.
The three name traps it records:
load_concrete_automapandstore_concrete_automapdo NOT auto-map — the names are historical (speculative zero pages diverged from Python backer data). Onlystore_concrete_automap_internal,store_concrete_le_bytes_automap_internaland the BAREinstall_multi_for_candidatesactually map missing pages (zero RW).store_concrete_lazyandstore_concrete_automapcurrently have byte-identical bodies (check_pages_mapped_lazythenstore_concrete); the names record intended caller class, not behavior.load_concrete_or_unconstrainedswallows EVERY error includingMemoryError::Permission(it is the ITE-leaf filler) — it is the one row where a perm check runs but cannot reject the load. Never use it on a guest-visible load path.
The funnels that keep cross-cutting features on every path: load_concrete_common (owns the range_has_multi dispatch + the angr-jvjf/angr-3zhl partial-overwrite guards) and store_concrete (owns the W check, the Multi clear, the mem_store counter) plus install_multi_for_candidates{,_safe}. The .96 Multi gate and the .94 permission bypass were both a feature landing on one copy only.
memory-mod-layout-2026-05-07
forgotten
memory/mod.rs structure after angr-0lre close (commit 12d8caaa2): mod.rs 762 lines (was 2881 at start of bead). Sub-modules: load.rs 662, store.rs 495, tests.rs 992, page.rs 251, ite_builder.rs 219, symbolic_objects.rs 115, concretize_glue.rs 75. mod.rs now contains: PendingWrite + MemoryError + SymbolicMemory struct/new, perms helpers (check_perms_range, check_executable, set/get enforce_permissions, set/get zero_fill_unconstrained, endness), map/unmap/is_mapped/map_data, fork+Clone, pending_writes API (count, get, add, drain, flush_pending_writes — flush is the only non-trivial body remaining at ~60 lines), page-mgmt (page_count, mapped_size, dirty pages, load_page_concrete, get_page_data, page_permissions, set_page_permissions), lazy regions (add/clear/is_in/is_addr_in/lazy_region_count, get_region_prefetch_list, get_nearby_prefetch_list), map_page, auto_map_zero_page, merge. If a future split is needed: pending_writes::flush + merge are the two largest standalone chunks left.
memory-perm-rust-vs-python
remembered
Rust SymbolicMemory store check uses Permission::W (W-only), not Permission::RW. Python angr's privileged_mixin.py also gates only on perm_write (not also perm_read). A write-only page should be storable. Permission::W and Permission::X added as single-bit constants alongside existing R/RW/RX/RWX.
memory-prune-round-history
remembered
bd memory prune round-4 (2026-07-20): 1082 -> 1000. Process mirrored round 3 (archive ~/repos/angr-memories, commit 33efab7) but PIVOTED mid-run from full re-review to delta scope after 4 of 21 batches: 278 already-scrubbed round-3 survivors yielded exactly 1 forget (~0.4%), while the 5 delta batches over the 229 new/updated keys ran 9-26% forget rates. RULE for round 5+: default to delta scope (new + body-changed keys since last prune, computed against roundN/snapshot-post-prune.json); full re-review only after a major refactor sweep. Second lesson: the status-shape+closed-only cheap sweep false-matches durable invariants whose bodies contain fixed/resolved phrasing — 8 of 30 sweep rows were hand-pulled and all survived agent review; always eyeball the sweep manifest before applying. Round-4 additions: round4/validate_manifest.py (per-batch manifest validator), manifest-R4-autokeep.jsonl pattern for recording auto-kept survivors so the combined apply still covers the full corpus. Zero promotes for the third consecutive round.
memory-store-symbolic-full-not-registered
forgotten
memory_store_symbolic_full IS now registered in production (key name is historical — formerly 'not-registered'). angr/exploration/rust_manager.py:1589-1590 calls callbacks.set_memory_store_symbolic_full(self._cb_memory_store_symbolic_full) when the Rust callbacks object exposes the setter; the callback cb_memory_store_symbolic_full is defined at rust_manager.py:2104. So has_memory_store_symbolic_full() returns true in production and the richer multi-addr StoreG path is wired up. (Prior state, pre-angr-tfic era: setter unwired, has*=false, multi-addr concrete-guard StoreG fell back to the ITE chain via memory_store_symbolic_value.)
memory-sync-phase-instrumentation
forgotten
Effective method for instrumenting RustExplorationManager memory sync phases without rebuilding Rust: monkeypatch RustStateSyncMixin._sync_memory_to_rust in Python to wrap each substage (._find_user_symbolic_pages, ._map_loader_pages, ._overlay_relocated_sections, ._overlay_python_state_pages, ._add_loader_lazy_regions, ._setup_stack_region, ._sync_stack_page, ._sync_extra_python_pages, ._scan_user_symbolic_pages) with time.perf_counter_ns() and aggregate into a per-manager log. Mma_howtouse Callable workflow makes the LAST manager's perf_report (built-in) ambiguous because each Callable is a fresh manager — averaging across 2nd-onward managers (skipping cache-miss first) gives cache-hit steady-state. Skip manager 0 to factor out the loader-pages-cache miss.
memory-sync-skip-symbolic
remembered
Memory sync MUST skip pages with symbolic_offsets. Syncing all pages overwrites Python's symbolic BVS values (e.g. password bytes in flareon2015_5) with concrete zeros from Rust's fallback representation. Only sync fully-concrete pages.
mempcpy-stpcpy-integration-coverage
forgotten
mempcpy/stpcpy native procs (memcpy.rs NativeMempcpy / strcpy.rs NativeStpcpy) now have Python-boundary integration tests in TestNativeMemoryCopyAndSet (test_procedures.py): test_mempcpy_returns_dst_plus_n + test_stpcpy_returns_pointer_to_written_nul. Key contract distinction tested: mempcpy returns dst+n (vs memcpy's dst), stpcpy returns dst+strlen (ptr to written NUL, vs strcpy's dst). Modeled on test_memcpy/test_strcpy via shared _store_cstr/_run/_make_stub harness. These were the LAST untested native copy procs — the test-hardening seam for the copy/string family is now fully drained.
memreplay-bottleneck-root-cause
remembered
sc_memreplay bottleneck (angr-gorvf.10, commit e6f28e7c8) — the SimProcedure bounce's dirty-page replay was slow for a reason NOBODY predicted, and two plausible hypotheses were WRONG. WRONG #1: 'Rust never clears dirty_pages between crossings, so the replay is O(crossings^2)'. Rust indeed never clears it (there is no Python caller of clear_pending_dirty_tracking, and memory/mod.rs resets it only on fork) — but the set stays TINY: csaw_wyvern replays 48 pages across 30 crossings = 1.6 pages/crossing. A per-page content memo keyed on the owning state was built and REVERTED: with 1.6 pages/crossing there is nothing to dedup. Do not re-derive this. WRONG #2: 'the FFI page fetch is the cost' — it is 13ms of 325ms. The ACTUAL cost, measured with the callback_simprocedure_replay{pages,ffi_ns,store_ns,sym_ns,sym_entries} counters (added by this bead, keep them), was the Python-side store: 311ms of 325ms, split (a) UltraPage.store writes CONCRETE bytes with a byte-at-a-time loop that right-shifts the whole value (self.concrete_data[subaddr] = ival & 0xFF; ival >>= 8) — QUADRATIC in store size, ~6.5ms for ONE 4096-byte page (this is an upstream angr perf bug, still live in angr/storage/memory_mixins/paged_memory/pages/ultra_page.py::UltraPage.store); (b) the real bulk — a page carries ~169 SYMBOLIC objects (8107 entries over 48 page-replays) and each generic state.memory.store cost ~32us of memory-mixin stack = 260ms. FIX (RustStateSyncMixin._acquire_ultrapage + _blit_concrete_page in angr/exploration/rust_state_sync.py): acquire the page ONCE via memory._get_page(pageno, writing=True) (writing=True routes through acquire_unique() so copy-on-write still forks a shared page — do NOT touch state.memory._pages directly) and then (a) blit concrete bytes as a bytearray slice assignment + zero the symbolic_bitmap over the range, matching UltraPage.store's concrete branch (which also leaves symbolic_data alone — loads consult the bitmap), (b) store symbolic entries via page.store(offset, ast, size, endness, memory=..., page_addr=..., cooperate=False), which runs the same decomposition the mixin would have. RESULT: 10.8 -> 2.7ms/crossing (-75%). GENERALIZABLE RULE: any engine-internal SimState memory write that is already passing inspect=False/disable_actions=True is a candidate for this bypass — the only store-side mixin with real state is ConvenientMappingsMixin, and it is inert unless MEMORY_SYMBOLIC_BYTES_MAP / REVERSE_MEMORY_NAME_MAP / REVERSE_MEMORY_HASH_MAP is set (the helper checks these and declines). Residual: sym stores are still 66.8ms of csaw_wyvern's 81.6ms; next lever is coalescing contiguous byte-granular symbolic objects or exporting only objects dirtied since the last crossing.
memset-symbolic-address-native
forgotten
memset SimProc handles a SYMBOLIC destination address natively via the module-level helper fn memset_symbolic_addr in native/angr/src/procedures/memset.rs. Pattern: extract_concrete_arg(dest) Err -> enumerate concrete address solutions with ctx.eval_upto(dest_bv, MAX_SYMBOLIC_ADDR_CANDIDATES+1); reject if empty or >cap (unbounded pointer). For each candidate a, build cond=dest_bv.eq(concrete(a)) once, then per byte i in 0..size: orig=memory_load(a+i,1); store(a+i, cond.ite(byte_bv, orig)). Distinct candidates compose correctly because dest equals at most one — a later candidate's store at a shared position layers another guarded ITE and only one guard is true (same composition reasoning as memcpy-symbolic-size-native overlap). Caps: MAX_SYMBOLIC_ADDR_CANDIDATES=64, MAX_SYMBOLIC_ADDR_MEMSET_SIZE=256, MAX_SYMBOLIC_ADDR_STORES=4096 (candidates*size budget). Falls back to Python (Err SymbolicArgument) when size is symbolic (addr+size both symbolic out of scope), candidate set unbounded, or budget exceeded. Used candidate-enumeration in the proc (NOT state.memory_store_symbolic/store_symbolic_unified concretizer) because the main VEX store path routes symbolic stores back to Python (statements.rs call_memory_store_symbolic); self-contained enumeration is deterministic+fully testable. memcpy/memmove symbolic-address (src×dst cross product) is follow-up angr-7ifq6.
memset-symbolic-size-native
forgotten
memset symbolic-size is handled natively (native/angr/src/procedures/memset.rs, NativeMemset). The size arg is now 'bv' not 'concrete'. Concrete size -> fast 8-byte-chunk path (untouched, no bench impact since corpus is concrete-size). Symbolic size: query ctx.max(&size_bv,false) for an upper bound on n; fall back to Python (Err(SymbolicArgument)) if unknown or > MAX_SYMBOLIC_MEMSET_SIZE (4096); else store ITE(i<n, fill, original_byte) per byte up to the bound, preserving contents past the symbolic length. The bound MUST be the true solver max (loop must cover every byte that COULD be filled). Pattern reusable for memcpy/memmove symbolic-size. Symbolic ADDRESS still falls back. See commit d2e7856ed.
memset-symbolic-value-native
forgotten
memset native SimProc (procedures/memset.rs) handles a SYMBOLIC byte value without Python fallback: arg is declared 'value_bv: bv'; if value_bv.as_u64() is None it builds an 8-bit byte = value_bv.extract(7,0,&ctx) and a 64-bit chunk = concat_balanced of 8 identical copies (endianness-agnostic), then stores chunks+remainder. Concrete values keep the fast precomputed-u64 chunk path. dst (extract_concrete_arg) and size stay concrete-required. strlen already preserves symbolic contents via an ITE null-position chain; memcpy/memmove copy symbolic BVs verbatim. Remaining mem* gap = symbolic size/address (tracked angr-e4qwq, high-risk).
merge-and-range-seeded-test-pattern
forgotten
Testing SymContext::merge (snapshot_fork_ops.rs) in a Rust unit test: create two SEPARATE fresh SymContexts but give the shared variable the SAME name (e.g. RustBV::symbolic(&s1,"x",32) and &s2) — Z3 name identity makes both constraints reference the same const, which is what forked-state merges rely on. merge_conditions are 1-bit BVs (cond.to_z3_bool() == (bv==1)); pass one per context, self first. To verify branch-guarding, just call merged.solution(&x, v): it implicitly solves for the merge-flag assignment, so x==5 and x==9 are each admissible while x==7 is rejected (Or(flags) forces one branch). No need to pin flags. range_seeded(&bv, smallest_known, largest_known): min searched in [0,smallest_known], max in [largest_known,2^w-1] — pass seeds that are valid solutions; concrete bv short-circuits to (v,v); UNSAT returns None. See context_tests/solver.rs test_merge_guards_each_branch_under_its_condition / test_range_seeded_*.
merge-cow-skip-soundness
remembered
SymbolicMemory::merge CoW skip soundness (angr-op0dn.11.2.1): the production skip predicate MemoryPage::is_shared_identical requires data Arc ptr-eq AND NO symbolic/Multi overlay on either page. It is deliberately NARROWER than the cfg(test) shares_data_with (ptr-eq + bitmap-eq). Reason: a symbolic store (store.rs SymbolicMemory::store, value.is_symbolic() branch) only touches the symbolic_bitmap + symbolic_objects map and does NOT break the data Arc (no store_concrete/make_mut). So two forked arms can share data ptr AND have equal symbolic bitmaps yet hold DIFFERENT symbolic_objects values at an already-symbolic byte (arm overwrites a byte that was symbolic in the parent). shares_data_with would wrongly skip that page and drop the divergence. is_shared_identical rejects any symbolic page, making the skip a strict subset of the existing value-equality early-out (s_data==o_data && !has_symbolic) -> provably semantics-preserving. Merge cost is now O(divergent pages). Test test_symbolic_overlay_not_skipped in memory/tests/merge_divergence.rs locks this in.
merge-memory-test-pattern
forgotten
SymbolicMemory::merge (memory/mod.rs) test pattern: per-byte ITE selection needs a real solver — gate under #[cfg(feature="vex-engine-z3")], build self/other mems with differing concrete bytes at same addr, call merge with a fresh 1-bit symbolic cond, then ctx.fork()+assume_true(cond==1/0)+eval(byte) asserts other vs self. The 3 structural branches (page-only-in-other adoption, non-page symbolic_objects union, pending_writes extension) need NO z3 — new_mock suffices since they skip the ite() build. KEY: to isolate the final non-page symbolic-object union loop, insert into other.symbolic_objects WITHOUT page.mark_symbolic so op.has_symbolic()=false and the s_data==o_data short-circuit skips the per-byte loop. Tests in memory/tests/merge_prefetch.rs (lib unit tests, no .so rebuild).
merge-multi-cell-pending-semantics
forgotten
SymbolicMemory::merge Multi-cell + pending_writes semantics (angr-op0dn.11.2.2, M3-2b, commit bd966b737). BOTH-Multi bytes: a byte in multi_objects on both arms unions into ONE lazy Multi cell — self alts guarded And(!m,cond), other alts guarded And(m,cond), count==n_self+n_other. Sound because each arm's exactly-one-cond-true invariant + the m-partition makes the two arm groups mutually exclusive, so the concrete page default byte is a don't-care (never the else-leaf under valid models). MIXED (Multi on one arm, plain/concrete on other): collapse the Multi side via MultiPayload::collapse(page_byte,ctx) and ITE into symbolic_objects — the existing path, generalized by the merge_byte_value helper. CRITICAL: the byte loop drove off is_symbolic()/symbolic_objects only, which are SEPARATE from multi_bitmap/multi_objects (Multi and Symbolic are mutually exclusive per-byte); before this fix a both-Multi divergent byte was merged as concrete = silently dropped. Added !has_multi() to the page concrete-equality early-out (s_data==o_data early-out is blind to multi_objects divergence) + MemoryPage::has_multi. PENDING_WRITES: was a blind extend (unsound — other's deferred store fired on self's paths, and self's own writes fired on other's). Now GUARDED not flushed (flush needs an AddressConcretizer merge doesn't have): self writes get condition=And(!m,prior), other get And(m,prior), via guard_pending_write. Tests memory/tests/merge_multi.rs (vex-z3-gated).
merge-waiters-err-unreachable
remembered
merge_waiters_by_callstack's _merge_states Err arm (native/angr/src/exploration/native_technique.rs) is defensive/unreachable via the current call graph: groups are built from wait_stash contents so len>=2 is guaranteed, and StashManager::find_state (stash.rs) has a linear-scan fallback after the index miss, so a state present in any stash is always found. The only true Err would be a state absent from ALL stashes, which cannot arise from ids sourced from the stash itself. Hardened it to release waiters to STASH_ACTIVE anyway (angr-1yge9.1). Consequence: cannot black-box unit-test the Err path; document reachability instead of testing dead code.
migration-count-is-the-lever
remembered
The parallel-exploration blocker is migration COUNT, not transport cost (angr-t3l5o pivot, 2026-06-30). The overhead gate's shadow probe models a LEVEL-SYNCHRONOUS wave migrating EVERY dispatched state; a real steal-on-imbalance work-stealing scheduler migrates only a fraction f of states (an actual cross-worker steal). run_parallel_overhead_gate.py now reports the BREAK-EVEN steal fraction f* (and accepts --steal-fraction to scale O_migrate). codegate goes GO at f* ~= 10.5-13.3% (migrate < ~1 in 9 states). Phase 1's 4.2x cheaper transport WIDENED that budget from ~3% (old 60ms transport) to ~13% (14.5ms) -> made the scheduler target ~4x easier, did NOT flip the gate alone. CONSEQUENCE for 2c (angr-vh834/angr-1ilq.3): the next blocker is a scheduler whose deque holds worker-LOCAL LIVE states (home Z3 context, never serialized) and serializes a StateMigrationPayload ONLY when a state is offered for stealing (surplus/imbalance), keeping steals <= f*. The current exploration/scheduler.rs does the OPPOSITE: it detach_for_migration's EVERY child onto the deque (eager serialization = the level-synchronous worst case). Full finding in docs/advanced-topics/rust_parallel_design.rst (t3l5o important-note). Related: parallel-is-transport-bound.
mimalloc-global-allocator
forgotten
mimalloc as #[global_allocator] in native/angr/src/lib.rs delivers NO measurable speedup for the Rust engine. Evaluated 2026-06-22 (angr-kq43): mimalloc 0.1.52 vs system allocator, 5-sample medians same-host on defcamp_r100/ais3_crackme/csaw_wyvern/ekopartyctf2016_rev250/mma_howtouse. All deltas within run-to-run noise (mma_howtouse, the only non-bimodal slow outlier, +0.5%). NOT adopted; dep removed. Root cause: engine wallclock is Z3 check()/PyO3-state-sync FFI bound, not malloc-throughput bound, so a faster allocator has no headroom. The earlier FxHash win (state_fork -20%, benchmark-memory-fxhash) was a per-op microbench, not an end-to-end allocator effect. Do NOT re-attempt without a profile showing allocation as a hot path. Negative result in docs/advanced-topics/rust_engine.rst 'Global allocator ... evaluated, not adopted'. NOTE: crates.io network is reachable in-sandbox as of 2026-06-22 (cargo add/search work), lifting the long-standing kq43 offline blocker.
min-max-bottleneck
remembered
min/max binary-search bottleneck pattern: bench impact is dominated by (a) how many min/max calls happen and (b) how tight the witness is. flareon2015_5 had 4074 min_search calls and saw a 1.74x wall-time speedup. csaw_wyvern with 980 min_search calls saw 1.04x. sym-write/mma_howtouse have ZERO min/max calls (sym-write uses eval_upto; mma_howtouse uses satisfiable) — the bead author's hypothesis that those were 'state-export heavy = chains eval->min->max' was wrong; state-export paths in rust_state_export.py do call min/max but the actual angr workloads in these benches do not. When evaluating future model-cache or extrema optimizations, grep for 'z3_site_min_search_count' on candidate benches first.
mips-o32-syscall-audit-clean
forgotten
Audit (angr-cudgw.1) confirmed the native MIPS-O32 syscall table in syscalls/mod.rs register_syscalls!("MIPS32", ...) is FULLY consistent with angr's mips-o32 table (block 8 in linux_kernel.py, line ~6181). All ~82 native numbers match angr's by number, including the +2-divergent *at family: mkdirat=4289 (upstream 4287), unlinkat=4294 (upstream 4292), renameat=4295 (upstream 4293); openat=4288/readlinkat=4298/faccessat=4300 match upstream. No de-registration hazard found. Regression guard: test fn mips32_at_family_uses_angr_divergent_numbers in syscalls/mod_tests.rs pins divergent numbers and asserts upstream slots (4287/4292/4293) stay unregistered. Re-run the parser in the commit msg if angr deps bump. Note old_mmap registered at 4090 which angr's table calls 'mmap' (O32 old-style); correct by number.
mips32-register-dispatch-status
forgotten
MIPS32 register dispatch coverage status (angr-w2gj.1, 2026-06-01): All entries in offsets32 module at native/angr/src/arch/mips.rs:18 (R0-R31, PC, HI, LO, F0-F31, FIR/FCCR/FEXR/FENR/FCSR, GUEST_STATE_SIZE) are wired into CANONICAL_MIPS32 and ALIASES_MIPS32 dispatch tables. Removed stale '#[allow(dead_code)] // see angr-w2gj' attribute — cargo check is clean without it. set_register/get_register Python paths go through arch.register_offset(name) -> RegisterFile.set/get(offset, size) which handles 32-bit and 64-bit ops uniformly. Pattern for new arch register-family tests: assert round-trip on canonical mnemonic AND aliases (e.g. f0 AND $f0; k0 AND $26). Test added: test_mips32_full_register_family_dispatch in TestMultiArchSupport. Note: dead-code audit flagged this because no test had EXERCISED these families end-to-end through Python, even though dispatch was complete — placeholder was a test-gap signal, not a wiring-gap signal.
mips32-struct-stat-and-omitted-mode
remembered
MIPS32 struct stat64 (angr-11djq.5.3, commit e4fade99b) completes the stat-family across all arches. write_mips32_stat in native/angr/src/syscalls/file_path.rs mirrors fstat64.py::_store_mips32. TWO quirks differ from i386/ARM writers: (1) _store_mips32 uses endness=memory_endness (MIPS32 is BIG-endian, MIPS32EL little) NOT a hardcoded Iend_LE -- but memory_store already applies the state's arch endness so plain concrete stores reproduce Python's bytes on both; (2) the MIPS layout writes NO st_mode and NO st_nlink field (angr's _store_mips32 simply omits them; struct flagged 'NOT CORRECT' upstream), so unlike the other Rust writers there is NO S_IFREG|0o755 substitution -- every field but st_size(0x30) and st_blksize(0x50) is zero. The 96-bit zero stores at 0x04/0x24 plus overlapping field stores fully cover bytes 0x00..0x5F with no gap. Registered LFS numbers from angr's mips-o32 map (linux_kernel.py ~6051): stat64=4213, lstat64=4214, fstat64=4215, fstatat64=4293 (no newfstatat on O32). DROPPED the stale legacy lstat 4107 registration -- angr's fstat.py defines _store for AMD64/PPC64/MIPS64/AARCH64 only, NOT MIPS32, so legacy stat-family on MIPS32 errors in Python too; leave 4106/4107/4108 to Python. mmap2 (4210) O32 stack-arg extraction was ALREADY done (angr-tvod). GOTCHA: angr's mips-o32 *at numbers are +2 above upstream (renameat angr=4295 upstream=4293), and 4293 = angr fstatat64, so the mod_tests mips32_at_family upstream-collision check now asserts the handler NAME differs rather than the slot being empty.
mips64-be-validated
forgotten
MIPS64 Big-Endian works end-to-end through the Rust engine with no new code changes. Verified 2026-06-01 via test_mips64_explore_be_real_elf (real ELF64 with EI_DATA=MSB, ELF64+EM_MIPS+EF_MIPS_ARCH_64) and the mips64_be_branch synthetic bench (0.48s rust_time). archinfo's 'mips64be'/'MIPS64'/'mips64'/'MIPS64BE'/'mipsbe64' aliases all resolve to MIPS64 with both memory_endness and instruction_endness = Iend_BE; cle's ELF backend picks up EI_DATA=MSB correctly. To build a MIPS64 BE inline ELF: ELF64 header packed with '>HHIQQQIHHHHHH', PT_LOAD phdr '>IIQQQQQQ', instruction words '>I' (big-endian). Solution shape used: a0<<1 + 16 == 100 ⇒ a0=42. Same opcode encodings as MIPS32 (registers still 5 bits; ADDIU sign-extends 16-bit imm to 64 bits on MIPS64).
mips64-canonical-n64-naming
remembered
MIPS64 register name tables have an N64-vs-O32 subtlety that must not be 'fixed' by copying MIPS32: in arch/mips.rs, offsets64 $8-$11 (R8-R11) canonicalize to a4-a7 (N64 argument regs), NOT the O32 temporaries t0-t3 -- t0-t3 are ALIASES_MIPS64 entries pointing at the same offsets. $30 canonicalizes to fp, s8 is its alias. CANONICAL_MIPS64 must stay in exact agreement with REGISTER_NAMES_MIPS64 (one name per offset) so register_name reverse-lookup (lookup_register_name searches canonical only) resolves all 35 GPRs like CANONICAL_MIPS32. Widened from 7->35 entries in commit f25159ea9 (angr-1yge9.13). register_name has no non-test Rust callers; the Python register dict is built from register_names()+register_offset() in state/export.rs, which use forward lookup (searches aliases too).
mips64-register-dispatch-status
forgotten
MIPS64 register dispatch (post angr-w2gj.2 + angr-mpln0 offset fix): ALIASES_MIPS64 covers GPRs (numeric rN + $N), HI/LO, FPU F0-F31 (canonical + $f-prefixed, 8-byte), FPU control regs (FIR/FCCR/FEXR/FENR/FCSR, 4-byte). Offsets in mod offsets64 (native/angr/src/arch/mips.rs) MUST match the VEX guest_mips64 layout: F0=296..F31=544, FIR=552, FCCR=556, FEXR=560, FENR=564, FCSR=568 (were +8 each pre-mpln0 => name-based FPU access aliased adjacent reg, fcsr clobbered guest_ULR@576). Because get_register/set_register share the table, in-isolation round-trip tests can't catch offset drift — guard with a ground-truth parity test vs archinfo.ArchMIPS64().registers (test_mips64_fpu_offsets_match_archinfo in test_multiarch.py: raw register file is host-native little-endian). CANONICAL_MIPS64 stays narrow (reverse-lookup only).
miri-executed-hitmap-clean
remembered
miri EXECUTED (qwyti.22, 2026-07-25) on the sole pure-Rust unsafe block: 'cargo +nightly miri test --manifest-path native/angr/Cargo.toml --features fuzzer hitmap_raw_pointer' => icicle::tests::hitmap_raw_pointer_aliases_the_slice PASSES clean under miri (9.13s, no UB, no warnings) — provenance+aliasing of 'unsafe{*ptr.add(3)=0xAB}' via Hitmap::as_mut_ptr() into Pin<Box<[u8]>> is sound. Setup: nightly toolchain + cargo-miri already present; only 'rustup +nightly component add miri' + one-time sysroot build (~39s) needed. DECISION: NO nightly CI miri lane (single block, already covered by passing #[test]; other 70 unsafe sites cross a C ABI miri can't run). Feasibility spike that established this (qwyti.8): of 71 unsafe sites across 21 files, exactly ONE is pure-Rust/miri-executable — the hitmap_raw_pointer_aliases_the_slice site above, behind #[cfg(feature=fuzzer)]. All 70 others bottom out in a Z3 (from_borrowed_raw->Z3_inc_ref, {Bool,Float,BV,Ast}::wrap, Z3_get_sort, Z3_simplify), libVEX (vex_lift), or PyO3/CPython (raw py-ctx ptr, CStr::from_ptr) C ABI call that miri aborts on. migration.rs/scheduler.rs unsafe hits are comment-only (unsafe-free designs). Verdict: miri technically feasible but low-value — not worth a CI gate for one already-#[test]-guarded block.
miri-feasibility-rust-core
forgotten
miri feasibility on the rust-symex core (qwyti.8 spike): of 71 unsafe sites across 21 files, exactly ONE is pure-Rust/miri-executable — icicle_tests.rs::hitmap_raw_pointer_aliases_the_slice (the 'unsafe { *ptr.add(3)=0xAB }' write through Hitmap::as_mut_ptr() into a Pin<Box<[u8]>>, behind #[cfg(feature=fuzzer)]). All 70 others bottom out in a Z3 (from_borrowed_raw->Z3_inc_ref, {Bool,Float,BV,Ast}::wrap, Z3_get_sort, Z3_simplify), libVEX (vex_lift), or PyO3/CPython (raw py-ctx ptr, CStr::from_ptr) C ABI call that miri aborts on. migration.rs/scheduler.rs unsafe hits are comment-only (unsafe-free designs). Verdict: miri technically feasible but low-value — not worth a CI gate for one already-#[test]-guarded block. Actual run is network-gated (nightly+miri not installed) -> angr-qwyti.22.
mma-howtouse-cache-clear-speedup
forgotten
Calling clear_all_caches() (thread-local AST/CLARIPY_AST/EXPRESSION caches) between callable() invocations in mma_howtouse reduced wall time from 6.62s to 5.07s (~23% improvement) without affecting correctness. The caches stay populated even after a manager goes out of scope because they are thread-local, not per-manager. Did NOT affect memory (still 1606MB peak). Suggests AST cache lookup overhead grows with total entries across all managers — likely O(n) hashmap probes hitting cold cache lines.
mma-howtouse-leak-source
forgotten
mma_howtouse 0.65x slowdown vs Python is dominated by Python-side memory leak when using Rust engine. Diagnostic: Python-only run is FLAT (RSS 207MB constant across 45 callable() invocations, py_heap 21MB constant). Rust engine grows ~24MB Python heap + ~8MB Rust per call, reaching 1606MB RSS / 1108MB py_heap by call 45. Top tracemalloc growth (since i=0): UltraPage bytearray(page_size) at ultra_page.py:30 and :34 (+359MB each = 181368 page allocations total, ~4030 fresh pages PER call), dirty_addrs_mixin.py:10 set updates (+170MB / 2.7M blocks). Each new RustExplorationManager creates a fresh angr state via factory.call_state, that state's pages aren't being freed across callable invocations even with explicit gc.collect(). Suspected cycle: state -> solver.eval (bound method) -> RustSolverFallback wrapper -> state via attach() in rust_state_export.py:52 (sets state.scratch.rust_mgr and patches solver methods to bound wrapper methods). Thread-local AST caches stay empty for this benchmark (concrete-only), global symbolic registry stays at 0 — those are NOT the cause. Clearing AST caches between calls reduces TIMING by ~1.5s (6.6s -> 5.1s) but does not affect memory.
mmap-map-fixed-divergence
forgotten
mmap MAP_FIXED divergence from procedures/posix/mmap.py: the Python procedure returns -1 on MAP_FIXED collision (procedures/posix/mmap.py:107-109), but real Linux mmap(2) atomically discards colliding pages and remaps at the requested addr. angr-ttr7 (commit 7d517864e) chose the POSIX semantics for the native fast path because (a) angr's whole-program analyses generally want kernel-faithful behavior, (b) MAP_FIXED+collision is rare enough that diverging from Python here doesn't affect existing tests, and (c) the native unmap is page-granular and trivial. Implication: if a test ever pins the Python -1 behavior, it will now see addr returned natively. Test code added in TestMmapMapFixedNative locks the new contract.
model-stability-eval-nondeterminism-root-cause
remembered
test_model_stability_constraint_order (TestSolverOperations) flake root cause: NOT cross-test Z3 pollution as iter-24 labeled it. It asserted eval(x_a)==eval(x_b) across two RustSolverContexts with the same constraints inserted in reversed order. Z3 gives NO guarantee about WHICH satisfying model eval() returns, so the equality flaked ~1-in-4 gate-on under -p no:randomly (which leaves PYTHONHASHSEED random per process). Gate-independent in mechanism: the only global-Z3-state tests (set_z3_global_param / deterministic=True, ~line 6863) run AFTER the model-stability test (~line 6803) in collection order, so there is no in-order polluter. INVARIANT: never assert cross-instance eval() model identity in tests; use deterministic min()/max() (unique values) as order-independence witnesses instead — they still catch a bridge reorder/dedup bug that changes the satisfiable range. Fixed commit 0c1ab2fe8. This cleared the last named test blocker for promoting the register-proxy write-through gate (angr-4rq7 fixed the addr corruption); promoting the gate default / adding to nightly-ci proxy_gates_on is still a separate decision.
module-split-visibility-and-import-mechanics
remembered
Splitting an impl-heavy Rust module in exploration/ (run_loop.rs -> run_loop_{single,wave,steady,worker}.rs, angr-9ke6b.49): mechanics that cost time. (1) Inherent-impl methods are private to their DEFINING module, so every method a sibling module calls needs pub(crate) — the compiler names them one at a time (E0624); expect a widen pass for struct fields too (ParallelShared's root_map/kind_map/counters/worker_found_hint/num_find/stepped all became pub(crate) because run_loop_wave/run_loop_steady read them). (2) Copy the parent's whole import block into each new file, then let 'cargo check --all-targets --message-format short | grep unused' dictate the prune per file — do NOT hand-guess, and note lib vs lib-test disagree (an import used ONLY by the co-located #[path] tests shows as unused in the lib pass; move it into the test file instead of keeping it in the module). (3) TRAP: if the new file starts with a hand-written 'use super::;' AND you slice the original's import block (which begins with the same line), you get a duplicate glob that rustc reports as 'unused import: super::' — confusing because the names DO come from it. Same shape for '#![deny(clippy::unwrap_used, clippy::expect_used)]' when the sliced header range already contains it: clippy::duplicated_attributes fires. (4) Each new file must RE-STATE the module-level deny; it does not inherit. (5) A struct's #[path] test submodule can still build it with private fields, so keep a struct and its literal-constructing tests in the same file.
multi-stage-manager-reuse
remembered
Multi-stage explore (hitcon2017_sakura pattern: simgr.explore→found[0]→new simgr) MUST reuse the old RustExplorationManager via reset_for_stage() instead of creating a new one. Constraint transfer between managers is lossy: Z3 deduplicates assertions on import (UNSAT at stage 49), claripy round-trip drops constraints (wrong branches at stage 62). Detection: state.scratch.rust_mgr in init. Loader page overlay + extra page sync also needed.
multiarch-parity-only-decision
forgotten
Multi-arch Rust-engine showcase (angr-4n26m.6) ships PARITY-ONLY, not a speed claim — this is a deliberate, evidence-backed decision (angr-4n26m.2 closed 2026-06-19). Why: the only real non-x86 ELF in the corpus, android_arm_license_validation, runs ~0.8x (SLOWER) under Rust (baseline_timings.json python_time=0.2 / rust_time=0.25); synthetic arch ELFs are ~124B with python_time=null; the firmware corpus (../binaries) is absent offline so no real MIPS/ARM busybox baseline can be sourced. Conclusion recorded in show_multiarch.py docstring, multiarch_numbers.json ('claim' field), and docs/blog/rust_symex_showcase.md sec 2. Do NOT make a non-x86 SPEED claim without first sourcing a real firmware ELF AND a Python baseline that actually wins.
multiarch-test-harness-table
remembered
Multi-arch test harness (angr-9ke6b.215): per-arch facts live in ONE table, tests/engines/rust/arch_specs.py::ARCH_SPECS — six ArchSpec rows (amd64/x86/arm/arm64/mips32/mips64) mirroring ALL_ARCHES in native/angr/src/arch/mod.rs. Each row carries archinfo id, ELF e_machine/e_flags, base addr, endianness variants (EndianVariant: armel vs armeb, mips32be vs mips32le, mips64be vs mips64le), a hand-assembled 'solve for 42' program, probe register names, and a ProcSpec (arg/ret-addr/ret regs) for the link-register arches. arch_specs.build_elf() replaced five copy-pasted minimal-ELF builders inside test_multiarch.py. test_multiarch.py sweeps that table with test_explore_solves_for_42 (18 cells = 9 endian variants x {blob, elf} loader), test_state_creation_round_trips_registers / test_fork_isolation / test_exploration_manager_creation (6 each) and test_native_procedure_round_trip (4); arch_specs.ARCHES feeds test_arch_offset_parity.py. Rust side: ARCH_EXPECTATIONS in native/angr/src/arch/mod_tests.rs is the matching table, swept by test_all_arches_report_expected_bits_name_and_endianness / _special_register_offsets / test_all_arches_resolve_their_alias_spellings; per-arch *_tests.rs files now hold only genuinely arch-specific tests (segment bases, NEON, MIPS64 reverse lookup). ADD A ROW, NOT A TEST FILE, when adding an arch.
mv08h-addr-pin-root-cause
remembered
angr-mv08h TooLarge Any/Max address-concretization fallback: the fix is to pin addr==chosen on the path via concretize::pin_fallback_addr (asserts ctx.assume_true(addr == chosen)) at EVERY fallback Single materialization site — concretize_read/concretize_write, interpreter/concretize_cache.rs cache-hit read+write arms, and the raw ctx.eval() symbolic-addr fallbacks in memory/load.rs::load + memory/store.rs::store. Genuinely-unique Singles never route through those arms (parity with Python AddressConcretizationMixin's trivial skip). SURPRISE: the 'unconditional pin' was reverted 3x (iters 41-43, 2026-07-20) for >15% fast-tier bench regression, but on the 2026-07-25 codebase it NO LONGER reproduces — run_regression --skip-bimodal --threshold 0.15 = 22/22, xmllint_getenv 3.33s vs 3.28 baseline (+1.5%). Lesson: re-measure a perf-reverted correctness fix on current HEAD before assuming a lazy/narrow redesign is still required; the surrounding solver/caching changes since the revert absorbed the cost.
mwbp-assume-dedup-design
forgotten
angr-mwbp (2026-06-03): extending dedup_set to assume_true/assume_false revealed a quadratic-in-exploration-depth regression. Unconditionally seeding the dedup_set from assume_*/false made flareon2015_2 timeout at 30s (5x baseline). Root cause: every fresh fork's first symbolic assume triggers an O(N) walk over shared+local z3_assertions, repeated per-fork. Fix: only consult dedup_set when ALREADY seeded (by an earlier add_constraint_raw). On unseeded contexts, fall through to legacy push-only. Counters: z3_assume_dedup_scanned/hit. Current fast-tier activation rate: 0% (the contexts where assume runs are generally not the same ones where add_constraint_raw seeds). Future work: heuristic that seeds when shared.len() crosses a threshold could lift the activation rate without re-triggering the regression.
native-access-parity
forgotten
Native access() parity: procedures/access.rs NativeAccess mirrors libc/access.py — mint RustBV::symbolic(name,32), build Or(ret.eq(concrete(0,32)), ret.eq(concrete(0xFFFFFFFF,32))) where 0xFFFFFFFF is 32-bit -1, then state.add_constraint(or). Build the constraint INSIDE a solver().borrow() scope, drop ctx, THEN state.add_constraint (mirrors read.rs/fgets.rs — add_constraint re-borrows). path+mode declared bv and discarded. Constraint feasibility tested in cargo via ctx.can_be_true (default features include vex-engine-z3 so it's meaningful). The linux_kernel/access.py FS-walking variant stays Python. Registered in procedures/mod.rs after rand. Committed c3034e059.
native-asprintf-getline-getdelim-parity
remembered
native-asprintf-getline-getdelim-parity: ae54t.3 (commit 7e7479a59). KEY GOTCHA: getdelim.py existed but its class was named __getdelim, so SIM_PROCEDURES['libc'] registered it under key '__getdelim' and the public 'getdelim' SYMBOL was UNHOOKED (the native-libc-no-parity-family audit's grep missed it — it grepped for class names matching the symbol). Fix = rename class __getdelim->getdelim. RULE: libc proc registration is keyed by CLASS name, not filename; a class named differently from the symbol leaves the symbol unhooked. getline = subclass getdelim with delim=BVV('\n', byte_width) (DRY). asprintf.py = FormatParser; mallocs len+1, stores out_str+NUL, writes buf ptr to *strp with arch.memory_endness, returns len. NATIVE: NativeAsprintf in sprintf.rs reuses format_string()+heap_alloc()+memory_store(strp, RustBV::concrete(dst,bits)) (mem honors endness, same pattern as strtol endptr store). getline/getdelim native DEFERRED to Python — unbounded realloc byte-loop over SimFileDescriptor (concrete+symbolic) not faithfully reimplementable from a SimProc ctx; same precedent as native fgets deferring non-stdin. Registered NativeAsprintf in procedures/mod.rs next to NativeSprintf.
native-bzero-parity
forgotten
Native bzero (procedures/memset.rs NativeBzero): Python angr's posix/bzero.py subclasses memset and forwards memset(s,0,n). Native bzero is the same DRY pattern as the fortify _chk wrappers — a declare_proc! whose call body does NativeMemset.call(state, &[dest_bv, RustBV::concrete(0,8), size_bv]). The .call method on a proc unit-struct is an INHERENT method generated by declare_proc! (no trait import needed). Registry test API is registry.has_native("bzero") (bool), NOT .lookup(). Remaining simple sandbox-safe parity gaps from the libc/posix-vs-native name diff: abort (NO_RET exit), putchar/getchar (need posix fd write/read), access/time (return stubs), memccpy (needs memory.find symbolic scan — bigger). strcasecmp/strncasecmp/strdup/strndup also Python-only.
native-char-io-unlocked-aliases
forgotten
Native char-IO stdio procs (fputc/putc in puts.rs; fgetc/getc/getchar in fgets.rs) now register their _unlocked aliases via declare_proc! 'aliases' field: fputc_unlocked, putc_unlocked, fgetc_unlocked, getc_unlocked, getchar_unlocked. All five _unlocked names resolve in SIM_PROCEDURES['libc'] (fputc.py/fgetc.py/getchar.py define x_unlocked=x as module attrs), so without the alias a glibc-heavy binary emitting the unlocked symbol round-trips to Python. Same gap class as stdio _unlocked (native-stdio-unlocked-aliases) and fileops fseeko/ftello (native-fileops-largefile-aliases). fgetpos/fsetpos and _IO_putc/_IO_getc are NOT in SIM_PROCEDURES['libc'] so no native parity gap there. Test: mod_tests::test_char_io_unlocked_aliases_dispatch.
native-coverage-matrix-location
forgotten
Native coverage matrix in docs/extending-angr/simprocedures.rst (added angr-3u87, commit 24a43dc1f): two list-tables tracking which libc procedures and Linux syscalls have native fast paths today vs Python fallback. Format follows rust_vex_ops.rst 'Unsupported op coverage matrix' but uses fractional M/N counts in the 'Native' column (since every row has a Python fallback — meaningful axis is how many named members reach the fast path, not implemented/placeholder/stubbed). When closing an angr-f16h.* (procedures) or angr-0hif.* (syscalls) child, flip Native column for affected members and bump M/N. Sources of truth: native/angr/src/procedures/mod.rs lines 176-299 (proc registrations), native/angr/src/syscalls/mod.rs register_ blocks. Validation: codepoint heading-underline scanner + list-table cell-count scanner (both in .ralph/state/session.md from prior iters).
native-coverage-real-binary-tail
forgotten
Native libc real-binary coverage tail = epic angr-tx7ec (sibling of angr-11djq). Curated ~12 high-confidence procs that real CLI binaries hit: fortify _chk printf/mem/str families (.1/.2/.3 — DRY: wrap base proc + bound check, never reimplement), locale ctype __ctype_b_loc/_loc (.4), getopt (.5, fork-safe optind/optarg state) + getopt_long (.6), fscanf (.7), vfprintf/vprintf (.8), vsnprintf/vsprintf (.9), stpcpy/mempcpy (.10), memrchr/strchrnul (.11), alloc family getline/getdelim/asprintf (.12, GATED on angr-um39j heap_brk fix). fprintf stays in existing angr-884yn (not duplicated). CUT as architecturally impossible: qsort/qsort_r/bsearch (no guest-callback infra) + strtok (static saveptr not fork-safe); CUT as zero-corpus-demand: mbtowc/realpath/strftime/abs/isascii tail. Gate is blind to these (FAST_SUITE only hits fprintf via sharif7_rev50) -> angr-11djq.20 adds a getopt+fprintf+ctype CLI micro-bench fixture. Real-binary gaps also under angr-11djq: format %n/%[]/float (.17/.18/.19), stat-family i386/ARM/MIPS32 (.5.1-.3), symbolic-file+symlink (.6.1-.2). DRY/KISS/SOLID now in .ralph/prompts/_footer.md CODE QUALITY section. No self-replenishing factory by design: when curated set drains, ralph idling is correct (real bottleneck was human-gating not bead scarcity, per iter77 zero-fallback corpus audit).
native-ctype-loc-tables
remembered
Native locale ctype tables (__ctype_b_loc/__ctype_tolower_loc/__ctype_toupper_loc, angr-tx7ec.4, commit aca498296): the TABLES are still built by Python _libc_start_main init pass (mallocs+fills LOCALE_ARRAY/TOLOWER_LOC_ARRAY/TOUPPER_LOC_ARRAY in shared memory, records ptrs on state.libc.ctypeloc_table_ptr) which runs BEFORE Rust takes over. Native side only FORWARDS the pointer values: new CtypeLocPtrs{b,tolower,toupper: Option} struct on RustSimState (state/mod.rs), getter ctype_loc()/setter set_ctype_loc(), 3 PyO3 #[setter]s on PyRustSimState (set_ctype_b_loc_table_ptr etc), pushed in rust_manager.py near posix_brk/heap_brk push (read state.libc.ctype_loc_table_ptr ints). Native procs in ctype.rs delegate to shared ctype_loc_ptr() helper; None->ProcedureError->Python fallback. Did NOT reimplement table-building (DRY). NOTE: any new scalar field on RustSimState must be added to ALL struct literals: 3 ctors in state/mod.rs, 6 in fork.rs (one uses merged_stdin not self.stdin_symbols.clone() so replace_all misses it), 1 snapshot field+2 sites in snapshot.rs (needs serde derive).
native-ctype-parity-complete
forgotten
Native ctype parity gap CLOSED (angr-ae54t.7, iter21): Python angr ships isascii/isblank/iscntrl/isgraph/ispunct as libc SimProcedures but native procedures/ctype.rs originally only had isdigit/isalpha/isspace/isalnum/isupper/islower/isxdigit/isprint/tolower/toupper — the 5 were silently falling back to Python. Added them via the existing ranges_predicate (isascii [0,127]; iscntrl [0,31]+[127,127]; isgraph [33,126]; ispunct [33,47][58,64][91,96][123,126]) and set_predicate (isblank space|tab) helpers; registered in procedures/mod.rs NativeProcedureRegistry::new. PITFALL: clippy::byte_char_slices fires on a 2-element &[b' ', b'\t'] literal passed to set_predicate — use a byte-str b" \t" instead (existing isspace has 5 elems so escapes the lint). All native ctype procs truncate arg to low 8 bits (arg_byte) per the established 'as u8' convention, so faithfulness is byte-level not full-int — same as the pre-existing procs.
native-dispatch-gate-prefer-native
remembered
Native-dispatch gate for hooked PCs is execution_env::prefer_native_dispatch (native/angr/src/exploration/execution_env.rs), shared by run_loop_single.rs (the hook block in the single-worker run loop) and core_outcome_handlers.rs::handle_simprocedure_core. Semantics: hook OUTSIDE every loaded object (extern stub) -> native ALWAYS; hook inside the MAIN object -> Python ALWAYS (preserves user proj.hook() overrides); hook inside a NON-main loaded object (libc under auto_load_libs+use_sim_procedures) -> native when prefer_native_library_hooks is on. That flag is default ON as of 2026-07-14 commit 622666808 (angr-gorvf.6) — it was default OFF from its landing (9e3454509, angr-gorvf.3.2/angr-a8epx) until the native string procs matched Python's symbolic-scan bound. Escape hatch: ANGR_RUST_PREFER_NATIVE_LIBRARY_HOOKS=0 (or kwarg prefer_native_library_hooks=False) forces the old all-Python dispatch; rust_manager.py::_resolve_env_flag takes a default= param so an unset env var yields the default and only a SET var is parsed for truthiness. Rust-side ExecutionEnvironment::new also defaults the field true, though Python sets it explicitly on every manager. Main-object span comes from rust_manager.py::_load_binary_regions -> rustylib set_main_object_range(min_addr, max_addr+1) (cle max_addr is INCLUSIVE). Do NOT reintroduce a raw is_in_binary check at a dispatch site — route through the helper. Two prose copies of this rule must stay in sync with the helper: the 'Dispatch priority' list in procedures/mod.rs's module doc and the 'Dispatch priority (native vs Python)' section of docs/extending-angr/simprocedures.rst — both said 'ANY in-binary hook runs Python' (pre-gorvf.6 semantics, plus dead run_loop.rs/stepping.rs line refs) until angr-9ke6b.106 fixed them in 750b9fa28. If you flip the default again, edit those two.
native-dispatch-skipped-for-loaded-libs
remembered
Native SimProcs (strcmp/malloc/calloc/getenv...) are BYPASSED on dynamically-linked real binaries loaded with auto_load_libs=True + use_sim_procedures=True. Mechanism: load_binary_regions (angr/exploration/rust_manager.py) puts the executable sections of ALL loaded objects -- including libc.so -- into environment.binary_regions (only cle## pseudo-objects are skipped). The native-vs-Python dispatch gate in run_loop.rs and stepping.rs (the 'is_in_binary' check, both ~'if !is_in_binary && native_procedures.get(&name)') skips the native registry entirely when the hook PC is inside any binary_region. use_sim_procedures hooks libc symbols AT their addresses inside libc .text, so is_in_binary==true and native is never consulted -> the Python SimProc runs and native_proc*_fallbacks counters stay 0 (native never even tried). This is why xmllint_getenv showed 14/27 native-implementable fallbacks running in Python (angr-11djq.4 iter17). The gate was designed to honor USER proj.hook() overrides but conflates them with use_sim_procedures library hooks. Native procs only fire today when the hook addr is OUTSIDE all binary regions (extern/PLT-less hooks, or auto_load_libs=False externs). See angr-oppte.
native-fgets-fgetc-resolves-stream-fd
forgotten
NativeFgets/NativeFgetc (native/angr/src/procedures/fgets.rs) used to declare _stream: bv and IGNORE it, always synthesizing stdin_* symbols (recorded as stdin input) for ANY FILE* stream. This diverged from Python fgets/fgetc, which resolve stream->_fileno via posix.get_fd and read from that SimFileDescriptor (returning -1 when missing). Same bug class as iter53/54/58 NativeFwrite/Fputs/Fputc arbitrary-fd fixes. FIXED (commit 399958bb0): args changed to stream: concrete, resolve fd via read_fileno (fileops.rs); fd==0 keeps native symbolic-stdin path, fd>0 (real file) returns Err->Python fallback, fd<0 returns -1, symbolic FILE*/_fileno falls back. getchar unchanged (always stdin, no arg). getc/getc_unlocked/fgetc_unlocked/fgets_unlocked delegate to the fixed bases. read.rs and fread.rs (NativeFread) were already correct (resolve fileno). NOTE: native stdin fgets still does NOT add Python's EOF/newline path constraints — pre-existing, out of scope.
native-fileops-largefile-aliases
remembered
Python angr aliases glibc large-file seek variants fseeko=fseek and ftello=ftell (libc/fseek.py, libc/ftell.py tails). NativeFseek/NativeFtell (procedures/fileops.rs) now register those aliases via the declare_proc! 'aliases = ["..."]' field (added iter57). Same DRY parity mechanism as the stdio _unlocked aliases (native-proc-unlocked-aliases): without it, _FILE_OFFSET_BITS=64 binaries emitting fseeko/ftello round-trip to Python through rust_manager's SIM_PROCEDURES name-fallback. declare_proc! aliases field generates NativeSimProcedure::aliases(); registry.register() inserts under primary+each alias. Remaining Python-only fileops with NO native base (not alias candidates): fgetpos/fsetpos.
native-fprintf-impl
forgotten
Native fprintf = NativeFprintf in procedures/printf.rs (registered after NativePrintf in procedures/mod.rs). Stream variant of NativePrintf: read_fileno_for_stream(stream) -> scan_concrete_lossy(fmt) -> write_fd. Writes RAW format string (no %-substitution), mirroring printf's CTF asymmetry vs Python fprintf which does out_str=fmt.replace(va_arg). Returns -1 on fd<0 (Python returns -1 when simfd is None). Unblocks __fprintf_chk half of angr-iv0hv (now has a native base to DRY-forward to). Closed sharif7_rev50's 44 fprintf fallbacks (0.32s->0.17s). angr-884yn / commit a3e30a789.
native-fputc-putc-resolves-stream-fd
forgotten
NativeFputc and NativePutc (native/angr/src/procedures/puts.rs, declare_proc! macro) previously IGNORED the FILE* stream arg (declared _stream: bv) and always wrote to stdout via write_stdout. Fixed in commit dd4f8d5a4 (angr-15eod): now declared 'stream: concrete', resolve stream->_fileno via procedures::stdio::read_fileno_for_stream (made pub(crate)), write to that fd via state.write_fd, return -1 (0xFFFFFFFF, 32-bit) on negative fd. A symbolic FILE* now falls back to Python (concrete extraction fails) instead of silently writing stdout. putchar correctly stays stdout-only (no stream arg). Same fix class as NativeFwrite/NativeFputs arbitrary-fd (see stdio-fwrite-fputs-arbitrary-fd) — keep all four stdio byte/buffer write shims in sync. write_stdout is just write_fd(1,..) so stdout case is unchanged.
native-fscanf-stream-variant
forgotten
Native fscanf/__isoc99_fscanf (procedures/scanf.rs NativeFscanf; __isoc99_fscanf is an aliases() entry, not its own struct — see invariant-libc-alias-not-duplicate-proc) is the stream variant of scanf, exactly mirroring how NativeFprintf extends NativePrintf: resolve stream->_fileno via fileops::read_fileno (pub(crate), uses io_file_for_arch offset e.g. AMD64=112), return -1 (concrete -1 at arch bits) on fd<0 (matches Python fscanf when simfd is None), else route through shared do_scanf core. do_scanf was generalized with (source: &str, record_stdin: bool) params: scanf/__isoc99_scanf/sscanf pass ("stdin", true); fscanf passes ("stdin", true) only when fd==0 else ("file", false) so non-stdin file reads do NOT call record_stdin_symbol and thus do not pollute posix.dumps(0). Like NativeSscanf, parsed values are minted as fresh unconstrained symbolic BVs — the file CONTENT is not parsed (documented simplification, less accurate than Python interpret(simfd=...) for concrete files but consistent with existing sscanf).
native-getopt-cursor-fields-landed
forgotten
Native getopt (bead angr-bhk0a) is now DECOMPOSED into 3 ordered drainable children — iter38 firsthand 350-line getopt.py read confirmed the full port is genuinely multi-session, so it was sliced. ID/role map (bd auto-suffixes do NOT match creation order): bhk0a.3 = cursor-fields FOUNDATION (DONE iter38, commit 3e491c484): getopt_optind/getopt_optchar:u32 on RustSimState, exact posix_brk mirror (struct field decl + 3 constructors + 6 fork.rs sites + snapshot.rs field/to_snapshot/from_snapshot + inner getopt_cursor()/set_getopt_cursor() + RustStateProxy PyO3 getter/setter passthrough getopt_optind/getopt_optchar), defaults (optind=1,optchar=0) match glibc/Python state.libc; tests = test_getopt_cursor_default_and_fork_isolation + assertion threaded through test_state_snapshot_round_trip_buckets_a_b_c. bhk0a.1 = NEXT, the Python->native extern-addr init-push (optind/optarg/optopt resolved Python-side via loader.find_symbol, pushed into 3 new u64 RustSimState fields mirroring the cursor plumbing) — the no-template EXTRA-RISK driver. bhk0a.2 = LAST, port getopt.py _getopt() short-opt engine to a native proc consuming .3+.1, faithfulness-or-ProcedureError-fallback (getopt_long/long_only/symbolic-argv/ambiguous defer to Python). Adding a posix_brk-style state field = ~13 edit sites: struct decl + 3 ctors + 6 fork + 3 snapshot points + getter/setter; for PyO3 exposure add RustStateProxy passthrough.
native-getopt-design-no-elf-resolution
forgotten
Native getopt (bead angr-bhk0a) — iter11 NEW risk + touch-point map. (1) NOVEL-CHANNEL RISK that iters9/10 missed: the posix_brk/mmap_base precedent syncs Rust->Python (native bumps the field, _sync_rust_posix_brk_to_state in rust_state_export.py pushes it back to angr_state). getopt needs the OPPOSITE direction — Python (the only side with loader.find_symbol) must push the 3 project-constant extern addrs (optind/optarg/optopt) INTO native RustSimState at init. There is NO existing template for a Python->native init-push of a resolved project constant; simprocedure addrs go to the ProcedureRegistry (register_simprocedures), NOT to state. This new channel (a setter on RustStateProxy called once at export/init) is the genuinely novel infra and the true source of bhk0a's EXTRA-RISK P4 framing — not the cursor fields. (2) TOUCH-POINT MAP for the 5 state fields, mirror posix_brk exactly: state/mod.rs = 1 field decl (:273 region) + 3 constructors (:468/516/574) + getter/setter pair + 2 RustStateProxy passthrough methods (:1611/1620); state/snapshot.rs = 1 snapshot-struct field (:69) + 2 sync points (to_snapshot ~:143, from_snapshot ~:192); state/fork.rs = 6 fork sites (grep 'posix_brk' fork.rs = 6). So ~13 edit sites PER field for the 2 cursor fields; the 3 addr fields additionally need the novel Python->native init-push (no template). Faithfulness is LOW-risk by construction: ProcedureError->Python fallback for getopt_long/getopt_long_only + symbolic argv + any unported edge = native can only ever match Python or defer, never diverge. CONCLUSION (5th consecutive iter): genuine multi-session feature, correctly NOT force-started under scope discipline.
native-getopt-engine-bhk0a2
forgotten
Native getopt(3) engine (NativeGetopt in procedures/getopt.rs, bhk0a.2, commit e41c8375b) ports ONLY the short-option path of getopt.py _GetOptBase._getopt. Registered for plain 'getopt' (3 args) in procedures/mod.rs; getopt_long/getopt_long_only stay Python (the proc never sees longopts). Faithfulness = match-or-defer: symbolic argc/argv/optstring/argv-element-ptr/scanned-byte -> ProcedureError -> Python (which produces the unconstrained BVS itself). Reads (optind,optchar) cursor via state.getopt_cursor()/set_getopt_cursor (bhk0a.3 fields), and the optind/optarg/optopt guest addrs via state.getopt_extern() -> GetoptExternAddrs (bhk0a.1 init-push). None extern addr just skips that store, mirroring Python _store_int/_store_ptr when _global_addr returns None. load_cursor honours a guest optind reset (0 -> full rescan to (1,0)). KEY mechanics learned: state.memory_load(addr,size).as_u64() is endness-correct (None=symbolic); state.memory_store(addr, RustBV::concrete(v,bits)) honours mem endness; int returns use width 32 (-1 = 0xFFFFFFFF). Python e2e test pattern: _RustExplorationManager raw class -> mgr.get_state_memory(sid,addr,n) DIRECTLY (NOT mgr._rust_mgr, that's only on the Python RustExplorationManager wrapper); hook getopt at out-of-binary PC so native dispatch fires (in-binary hooks force Python per dispatch rule 1).
native-getopt-extern-addr-pushdown
forgotten
Native getopt extern-addr init-push (bead angr-bhk0a.1, DONE iter40, commit 505ae229d). KEY CORRECTION to native-getopt-design-no-elf-resolution's 'NO existing template' claim: the ctype table-ptr push IS the exact template for the Python->native init-push direction (NOT posix_brk, which syncs the OPPOSITE Rust->Python way). Pattern mirrored verbatim: GetoptExternAddrs{optind/optarg/optopt:Option} struct (Clone+Copy+serde, like CtypeLocPtrs) -> getopt_extern field on RustSimState -> 3 ctors default + 6 fork.rs sites + snapshot.rs (field+to_snapshot+from_snapshot) + inner getopt_extern()/set_getopt_extern() + RustStateProxy PyO3 #[getter]get_getopt_optind_addr/#[setter]set_getopt_optind_addr pairs (PyO3 strips get_/set_ -> property getopt_optind_addr). Python init-push lives in rust_manager._build_seed_state right after the ctype block: loader=self.project.loader; for (name,setter) in optind/optarg/optopt: sym=loader.find_symbol(name); if sym: setattr(rust_state, 'getopt'+name+'_addr', sym.rebased_addr). None default = native proc defers to Python. Consumer is bhk0a.2 (_getopt engine port, LAST/still open). NOTE venv pip is broken (no .venv/bin/pip, only pip3); rebuild via tools/rebuild-rust.sh --cargo-only --keep-cargo-cache.
native-getopt-family-parity
remembered
getopt/getopt_long/getopt_long_only now HAVE Python SimProcs (angr/procedures/libc/getopt.py, class names match symbol names so auto-registered into SIM_PROCEDURES['libc']; landed angr-ae54t.6, commit 56440475b). Engine: faithful concrete-argv getopt — short opts (grouped '-abc', inline '-bval' / separate next-argv required args, '::' optional args, '?'/':'/leading-colon-mode errors), '--' terminator, POSIX NON-PERMUTING (stops at first non-option operand; argv never reordered — a documented simplification vs glibc default permutation), and long options ('--name'/'--name=value', unique-prefix abbreviation, struct option {name,has_arg,flag,val} parsed via stride=4*arch.bytes / has_arg@ps / flag@2ps / val@3ps, flag!=NULL stores val & returns 0, longindex written). getopt_long_only tries single-dash as long first then short. CURSOR: per-state SimStateLibc.getopt_optind (mirrors guest optind, 1-based) + getopt_optchar (internal within-arg offset, not guest-visible); both added to init and copy(). optind/optarg/optopt written back to their guest EXTERN-SYMBOL addresses (self.project/state.project.loader.find_symbol) so the program reads them. SYMBOLIC argc/argv/optstring/relevant-bytes -> unconstrained return (no state mutation), preserving pre-SimProc behavior, never branches on a symbolic scan. NATIVE parity deferred to angr-bhk0a (per-state guest-memory globals = the EXTRA-RISK half). Tests: tests/procedures/libc/test_getopt.py (load_shellcode + loader.extern_object.make_extern for the globals).
native-htonl-htons-parity
remembered
Native htonl/htons live in native/angr/src/procedures/byteorder.rs (NativeHtonl/NativeHtons, host_network_swap helper). They register under SIM_PROCEDURES['posix'] — matching the Python source dir angr/procedures/posix/htonl.py + htons.py (the registration namespace tracks the dir, same as strndup/strncasecmp which are also under posix/, see native-strndup-parity). The only surprise: these byte-order C functions are filed under 'posix', NOT 'libc' where a contributor might first look. Impl mirrors Python: on LE arch extract low 32/16 bits, RustBV::reverse (byte-swap), zero_extend to arch.bits(); identity on BE. ntohl/ntohs are aliases (byte-order swap is symmetric; Python ships neither). reverse/extract/zero_extend fold concrete operands so one path covers concrete+symbolic. Integration test must use SIM_PROCEDURES['posix']['htonl'] or KeyError.
native-identity-proc-vs-syscall-dispatch
remembered
Native POSIX identity getters getuid/geteuid/getgid/getegid live in TWO places with DIFFERENT dispatch paths: (1) syscalls/identity.rs as NativeGetuidSyscall etc. (kernel syscall path), and (2) procedures/getid.rs as NativeGetuid etc. (PLT libc-call path, added angr-ae54t.9). A binary calling getuid() via PLT dispatches through the PROCEDURE registry, NOT the syscall path — so a native syscall handler alone does NOT prevent a Python fallback for the libc call. Both return constant 1000 (DEFAULT_UID_GID). The proc return BV is sized to state.arch().bits() to match angr SimProcedure.ret() building BVV(value, arch.bits). When auditing native parity, check the procedure registry (mod.rs register() calls), not just syscall coverage.
native-libc-alias-parity-audit
remembered
Native libc alias-parity audit: cross-ref Python 'x_unlocked = x'/'fseeko = fseek' style aliases against native declare_proc! aliases=[...]. fgets_unlocked was a real GAP (Python fgets.py:70 had it; NativeFgets did not) fixed iter36 (commit 869342aa8). Method that WORKS: a python script regex-diffing libc proc names+aliases vs native fn name()/name=/aliases= strings exposes these. Remaining REAL non-trivial gaps (faithfulness obstacles, NOT clean ports): ungetc (native fgets mints fresh symbolic bytes, has NO fd read-position model to decrement -> must stay Python), err/error (best-effort format-string write + NO_RET/DYNAMIC_RET), gets (needs libc.max_gets_size state + newline semantics + symbolic constraint gen), strtok (per-state saved ptr), memccpy (symbolic memory.find scan), tmpnam (host-random filename = faithfulness risk). The void/return-0 and simple-alias seam is now drained.
native-libc-no-parity-family
remembered
Native-coverage leaf tasks tx7ec.11 (memrchr/rawmemchr/strchrnul), tx7ec.12 (getline/getdelim/asprintf), fortify wrapper nl5v1 (__vsnprintf_chk), and the getopt family tx7ec.5/tx7ec.6 hit the parity wall. UPDATE 2026-06-25: most of this family is NO LONGER a wall — Python SimProcs now ship for memrchr/rawmemchr/strchrnul, getline/getdelim/asprintf, vprintf/vfprintf/vsprintf, __vsnprintf_chk, AND getopt/getopt_long/getopt_long_only (angr-ae54t.6, see native-getopt-family-parity). What REMAINS native-only (Python defers): see per-family memories. RULE for future native-libc tasks: grep angr/procedures/*.py for an actual SimProcedure CLASS (not just a glibc_decls.txt/glibc.json TYPE DECLARATION) before claiming parity status — a decl alone means the symbol resolves to ReturnUnconstrained. getopt EXTRA risk (fork-unsafe optind/optarg/optopt/opterr globals needing per-state guest memory) was handled in the Python proc via SimStateLibc.getopt_optind/getopt_optchar + extern-symbol writeback; native getopt parity (angr-bhk0a) still owes that per-state guest-memory work. FORTIFY NUANCE: a __X_chk wrapper needs its OWN Python __X_chk class — base X existing is NOT enough (chk symbol resolves independently to ReturnUnconstrained if absent).
native-library-hooks-default-on
remembered
prefer_native_library_hooks is DEFAULT ON since 2026-07-14 (angr-gorvf.6, commit 622666808). Evidence at flip time, all with the flag UNSET: concrete_strops canary (tests/benchmarks/synthetic_examples/concrete_strops/measure.py) = 50 native_proc_calls / 0 Python fallbacks, 0.09s — and with ANGR_RUST_PREFER_NATIVE_LIBRARY_HOOKS=0 it flips back to 50 Python fallbacks / 0.36s, proving the escape hatch. xmllint_getenv symbolic canary (run_single.py, --timeout 180 --mem-limit 3000) = found=1, 3.33s/363MB, exactly 1 SimProcedure->Python bounce (the forced explore(find=getenv) target); before the MAX_SYMBOLIC_SCAN_BYTES fix this same config timed out at 180s/3GB. Gates: 1825 Rust unit tests, 1348 pytest tests/engines/rust/, fast-tier bench 22/22, medium-tier 29/29. Two caveats for whoever revisits this: (1) make test-python-baseline was NOT run — ../binaries is not cloned on the loop box and there is no network; the pure-Python engine never reads this flag (it is a RustExplorationManager kwarg only), so it is not a real risk surface. (2) The 'is_heap_layout_proc carve-out' the bead told us to re-evaluate DOES NOT EXIST anywhere in the tree — grep for heap_layout returns nothing in .rs/.py. Heap parity is instead handled by rust_callback_dispatch.py::_sync_state_heap_to_rust / _install_lazy_heap_sync, which push a bounced Python malloc's heap bump back into Rust so a later native malloc cannot overlap it. Nothing was silently inherited.
native-library-hooks-parity-cleared
remembered
prefer_native_library_hooks PARITY GATE IS CLEARED as of 2026-07-14 (angr-gorvf.5 closed, iter122) — the flag is safe to turn on; only the default flip itself remains (angr-gorvf.6). Root cause of the old xmllint fork-storm was NOT missing pruning constraints (strings::null_exists_constraint already existed via angr-sgcye) but the SYMBOLIC SCAN WINDOW: native collected every symbolic position to MAX=4096, so the Or(...) disjunction and ITE chain ran thousands of terms wide and kept thousands of downstream branches feasible. Python caps at state.libc.buf_symbolic_bytes=60. Fix: MAX_SYMBOLIC_SCAN_BYTES=60 enforced in strings::scan_concrete_then_collect (commit c94941fff); concrete positions do not draw down the budget. MEASURED with ANGR_RUST_PREFER_NATIVE_LIBRARY_HOOKS=1: xmllint_getenv 3.29s/363MB/found=1 vs all-Python 3.89s/276MB/found=1 (pre-fix: 180s timeout, 3GB cap); concrete_strops 50 native_proc_calls / 0 Python fallbacks flag-on vs 50 fallbacks flag-off, ~4x wall. This SUPERSEDES avoid-default-on-native-library-hooks, whose OFF rationale is now obsolete. Evidence fixture: tests/benchmarks/synthetic_examples/concrete_strops/ (self-building measure.py).
native-lift-fallback-counter-trap
remembered
libVEX native-lift counters: rust_native_lift_fallback_count counts EVERY miss, including lift attempts at addresses outside all binary regions (a state returning into unmapped memory, e.g. ret-to-0x0). Those are deadend probes — the pyvex callback can't lift them either, it returns the '{}' sentinel and the state deadends — so they are NOT lost native-lift wins. Misreading them cost a whole bead (angr-op0dn.2.3): cow_fork_scaling reads 21 native / 256 fallback and looks like a 7% hit rate, but all 256 are 0x0 probes and the native path serves 21 of 21 real blocks. Use rust_native_lift_deadend_probe_count (subset of fallback, bumped in execute_block's native-lift arm in interpreter/execution.rs) — real lost wins = fallback_count - deadend_probe_count, which is 0 across the fast-tier corpus. Counters only publish under mgr.enable_profiling().
native-lift-feature-not-default
forgotten
The 'native-lift' Cargo feature is NOT in the default feature set in native/angr/Cargo.toml (default = ['vex-engine', 'vex-engine-z3', 'automaton'] — native-lift is opt-in). Effect: any code under #[cfg(feature = 'native-lift')] in native/angr/src/interpreter_cb/execution.rs is dead code in default builds. The fallback path is _cb_lift_block (Python lift via project.factory.block). Important consequence for angr-k67f: my native-lift SMC fast path (read_concrete_bytes_for_lift from rust_memory for dirty pages) only activates when native-lift is enabled. For default builds, SMC support requires Python-side state.memory sync.
native-locale-string-parity
forgotten
Native locale string parity (ae54t.8): strcoll/strxfrm now native. In C/POSIX locale (angr default) strcoll==strcmp (registered as alias on NativeStrcmp in strcmp.rs) and strxfrm==strncpy(dest,src,n) + return strlen(src) UNTRUNCATED (NativeStrxfrm in strcpy.rs, reuses scan_concrete_until_null for the strlen + write_concrete_bytes for the bounded NUL-padded n-byte window; return can exceed n, snprintf-style). Remaining string/stdio parity gaps still falling back to Python: memccpy (needs memory.find symbolic semantics), strtok (stateful static ptr, fork-unsafe like getopt/bhk0a), gets/fputs/feof/fflush/setbuf/ungetc/perror, vsnprintf/vsprintf (format). Productive seam: continue per-family Python-SimProc vs native parity audit.
native-memrchr-rawmemchr-strchrnul
forgotten
ae54t.2 (memrchr/rawmemchr/strchrnul) implemented by extending strchr.rs scan cores with bool flags rather than new scan functions: (1) scan_for_byte gained nul_returns_addr — when stop_at_null hits a NUL before the target, return the NUL's address (strchrnul) vs concrete 0 (strchr); threaded through build_ite_chain's stop_at_null ITE arm AND the concrete fast-path Stop. (2) scan_for_byte_last gained stop_at_null — false (memrchr) scans the full min(n,MAX_SCAN) window ignoring NULs and returns last_match-or-0 on Exhausted, true (strrchr) stops at NUL and errors on Exhausted. rawmemchr = scan_for_byte stop_at_null=false MAX_SCAN. Python procs reuse strlen+strchr (strchrnul=If(strchr==0, s+strlen, strchr)), memchr (rawmemchr, n=libc.max_buffer_size=48), and a forward-ITE backward-override loop (memrchr). Same option-a pattern as ae54t.1 stpncpy.
native-merge-fast-path
remembered
RustExplorationManager.merge() native fast path (angr-op0dn.11.4, fn _merge_native in rust_manager.py): taken only when merge_func AND merge_key are both None. Groups state_ids by pc via _rust_mgr.get_state_pc_by_id (no export), calls _rust_mgr.merge_states(group, stash) per multi-member group, then drops sources through the _merge_drop stash. Returns bool: False if a pc lookup misses -> caller falls back to the exporting Python path. KEY DESIGN: native merge_states (state_lifecycle.rs::_merge_states) forks+merges in-Rust and never exports, so the angr-qluof demoted-symbolic-file re-demotion dance is UNNECESSARY on this path -- the merged state inherits its base arm's demoted paths directly (no _export_fs_files_to_rust re-arms them). merge_states does NOT remove source states from their stash; the Python wrapper must drop them. states_merged_native counter lives on the native manager (mod.rs), incremented by state_ids.len() in _merge_states, surfaced in stats_api.rs. Note stats is a @property (mgr.stats not mgr.stats()).
native-mergepoint-technique
remembered
Native MergePoint technique (angr-op0dn.11.5, ManualMergepoint parity): NativeTechnique::MergePoint { address, wait_counter_limit, counter, wait_stash } in native_technique.rs; registered via register_merge_point pymethod (manager_methods.rs, creates per-address merge_waiting_{addr:#x} stash). Applied post-round in apply_native_techniques (helpers.rs) but HANDLED OUT OF BAND above the match: merging calls _merge_states (needs &mut self) which conflicts with the &mut self.native_techniques[tech_idx] the match arm holds -- caller copies immutable fields out first, writes counter back via short reborrows keyed by tech_idx. apply_merge_point: park_states_at_address moves active states with pc==address to wait stash (fresh arrival resets counter to 0); once active drains OR wait_counter_limit post-step rounds elapse, merge_waiters_by_callstack groups waiters by call_stack return_addr chain (first-appearance order for determinism), merges each >=2 group via _merge_states (forks+pushes merged to active, bumps states_merged_native; sources dropped via drop_states_by_id), lone-callstack waiters released to active unmerged. IMPORTANT: _merge_states does NOT consume sources (it forks), so the technique must drop them. Python: rust_techniques.py routes ManualMergepoint->register_merge_point and 'ManualMergepoint' is in _NATIVE_STEP_TECH_NAMES so its Python step() hook is suppressed (no double-merge). Native serial loop applies techniques after EACH single-state step (not per full round), so counter ticks faster than Python's per-step()-of-whole-stash; primary merge trigger is active-empty, counter is the fallback. Uses RustSimState::push_call in tests (not push_call_frame -- that's a pymethod).
native-only-procedures-pattern
remembered
When a libc procedure is decl-only in glibc.json with no Python implementation (ferror, posix_memalign, memalign, aligned_alloc, strtoll/strtoull/strtod), the native registry is the ONLY dispatch path. The integration test pattern (TestNativeMemoryAlignedAllocators, TestNativeStringToNumericProcedures, TestNativeStdioStatusAndWrite) uses _make_stub() to mint a fake angr.SimProcedure subclass with the matching class name so proj.hook(HOOK_ADDR, stub_class(), replace=True) creates a hook that the native dispatcher will recognize by name. The stub's run() body never executes — the dispatcher intercepts before it.
native-proc-call-counts-keyed-on-hook-name
remembered
Native proc call_counts is keyed on the HOOKED SimProc display_name (the lookup key), NOT the base proc.name(). In stepping.rs the dispatch does self.native_procedures.get(&name) then call_counts.entry(name.clone()) — name is the hooked name. So an aliased proc (declare_proc! aliases=[...], e.g. vprintf->NativePrintf, vfprintf->NativeFprintf) records under the ALIAS name (call_counts['vprintf']), even though registry.get('vprintf').name()=='printf'. Integration tests that assert native dispatch of an alias must check stats['call_counts'][ALIAS], not the base name. Verified by TestNativeStdioStatusAndWrite vprintf/vfprintf tests (angr-u1gtc).
native-proc-extern-region-and-ctype-table-timing
remembered
Native SimProcs only fire when the SimProcedure hook lands in angr's EXTERN region — gate on this when building benches/fixtures that should exercise native procs. (1) auto_load_libs=True loads real libc and the hooks fall INSIDE a loaded-libc binary region; run_loop.rs is_in_binary check then SKIPS the native fast path and forces the Python SimProcedure -> the fallback counter (native_proc_other_fallbacks_by_name / simprocedure_fallback_by_name) lights up. Use auto_load_libs=False (fauxware/cow_fork pattern) so hooks land extern. (2) SEPARATELY, native __ctype_b_loc/_tolower_loc/_toupper_loc fall back with ProcedureError::Other 'table not initialized' (ctype.rs ctype_loc_ptr) in ANY end-to-end RustExplorationManager run that starts from entry_state: the classifier table is malloc'd+populated by the __libc_start_main SimProc DURING exploration, but the Rust seed captures CtypeLocPtrs at construction (BEFORE init runs) -> null ptr. So native ctype is effectively unit-test-only; end-to-end benches gate the dispatch+fallback path, not native execution. To fix you'd need the Python state to run libc init before Rust seed capture, or eagerly build the ctype table into the seed.
native-proc-heap-store-lazy
forgotten
Native SimProcedures that heap_alloc a struct then write to it (fopen writing _fileno into a fresh _IO_FILE, also calloc/realloc/posix_memalign/strdup) used to fail with Memory(Unmapped) and fall back to Python, because the heap range [0xC0000000,0xC1000000) was never registered as a lazy region (only loaded objects + stack were) and RustSimState::memory_store called plain store_concrete (no auto-map). Fix (state/mod.rs): new_state_memory() registers the heap as a lazy region in all 3 constructors via HEAP_REGION_START/SIZE consts, and memory_store now uses store_concrete_automap_internal so writes to fresh heap auto-map a zero page. Unwritten heap reads stay Unmapped -> Python fallback, preserving symbolic-fill semantics. Diagnosed via symmetric handler-Err logging added at the proc dispatch fallback site in exploration/stepping.rs (mirrors the existing arg-extraction log). Cascading win: sharif7_rev50 fseek=86/fputc=43/fopen=1/fclose=1 all went native, 175->44 SimProc fallbacks (remaining 44 = unregistered fprintf), 0.9s->0.32s. Verified: 1441 rust unit tests + 968 python rust tests pass, bench gate 20/20 (flareon2015_2 'regression' was CPU-contention noise: identical op counts, identical timing under equal load).
native-proc-unlocked-aliases
remembered
Native SimProcedure dispatch names come from proc.class.name (rust_manager._cb_resolve_function), so Python aliases like 'fwrite_unlocked = fwrite' usually resolve to the base class name 'fwrite' and hit the native impl. BUT the SIM_PROCEDURES name-fallback path (rust_manager.py ~line 1972) can surface the literal _unlocked symbol name, so native procs need explicit alias registration to avoid a Python round-trip (this is why NativeFreadUnlocked existed as a separate struct). NativeSimProcedure::aliases() (default &[]) + register() inserting under each alias is the DRY mechanism; fwrite/fputs/feof/fflush carry their _unlocked alias as of commit 9c6badea1. fputc/putc/fgetc/getchar/fgets _unlocked have NO native base impl so they aren't candidates.
native-procs-no-simoption-access
forgotten
RESOLVED by angr-kzjv6: native SimProcedures CAN now query SimOptions via RustSimState::has_option (see native-simoption-has-option). The old blocker (options were Python-only) is fixed for the symex-relevant subset (_NATIVE_SIMOPTIONS, currently SHORT_READS). efvao (fgets short-read) is now unblocked.
native-pthread-mutex-noop
forgotten
Native pthread_mutex_lock/unlock are pure no-ops returning 0 (SUCCESS), word-width, matching Python (both 'return 0'). Live in native/angr/src/procedures/pthread.rs as NativePthreadMutexLock/NativePthreadMutexUnlock via declare_proc!, registered in NativeProcedureRegistry::new. Mutex ptr arg declared 'bv' (cloned, never inspected) so a symbolic pointer does NOT fall back. Single-path symex has no real concurrency so locks always succeed. pthread_once is NOW ALSO NATIVE as NativePthreadOnce (hand-written, not declare_proc) using the sub-call mechanism — call_ex reads the once-guard byte, sets the done-bit (value 2) BEFORE the CallAndResume{target:func} sub-call (recursion-safe), resume returns 0; symbolic control/guard/SP fall back to Python. Done in adea42e52 (xxukz, 2026-06-26). Still NOT native: pthread_create (spawns symbolic branch). __ctype_b_loc native as NativeCtypeBLoc. Justified by angr-11djq.4 xmllint_getenv: lock x4 + unlock x3 + pthread_once x4 fallbacks eliminated.
native-resume-path-live
forgotten
Native CallAndResume sub-call machinery is LIVE (not dead/unfinished): RustExplorationManager::setup_native_subcall (exploration/stepping.rs) is called from run_loop.rs sequential path; production resume runs via core_outcome::handle_native_resume_core (angr-vh834 refactor). The stepping.rs handle_native_resume method is a TEST-ONLY direct-call harness for subcall_tests.rs — gated #[cfg(test)] (angr-0mqkc.2), not #[allow(dead_code)]. SubcallSetupError is live too; its #[allow(dead_code)] is the 'fields read only via Debug' idiom, not scaffolding. When wiring gorvf.3 ADDS_EXITS/sub-call work, the primitives already exist — do not rebuild.
native-returnunconstrained-bvv0-gate
forgotten
CONFIRMED root cause for angr-8mjd native ReturnUnconstrained explosion (was 'unconfirmed' in simproc-fallback-statecreate-bottleneck): Python's angr.procedures.stubs.ReturnUnconstrained calls state.solver.Unconstrained(name,size,key=('api','?',display_name)), and SimSolver.Unconstrained (angr/state_plugins/solver.py) returns a CONCRETE BVV(0) unless sim_options.SYMBOLIC_INITIAL_VALUES is in state.options — which is OFF by default and absent on csaw_wyvern. So for the ~24 pointer-returning C++ stubs, Python writes concrete 0 (null), NOT a symbolic value: downstream null-checks and address-uses resolve concretely with zero forks. The reverted native fast-path (iter46) minted a FRESH symbolic BVS per call, so every null-check forked and every store/load through the symbolic pointer paid concretization cost -> 2.7s->86s explosion. CORRECTED RECIPE for any re-land: gate on SYMBOLIC_INITIAL_VALUES — when absent (common case) write Ok(Some(BVV(0, returnty_bits))) to the return reg, only mint a symbolic BVS when the option is set; void-return stubs (returnty None -> Ok(None)) always safe. This makes 8mjd tractable again (no longer a dead-end). Documented in docs/extending-angr/simprocedures.rst (Native section, .. warning:: after the ProcedureError enum para).
native-scanf-variadic-arg-truncation
remembered
Native SimProcedure dispatch (run_loop_single.rs hook path + core_outcome_handlers.rs::handle_simprocedure_core) historically extracted args using ONLY the Python SimProcedure's registered num_args (the FIXED-arg libc prototype count — scanf=1, sscanf/fscanf=2), which EXCLUDES variadic pointers. This truncated variadic native procs: NativeSimProcedure::num_args() (7 for scanf, 8 for sscanf/fscanf) was never consulted, so do_scanf got an empty ptr_args slice and wrote nothing — the entire scanf family was a silent end-to-end no-op (missed feasible paths). FIX (angr-8onrp, commit 1d224d7cf): both sites now extract max(num_args, native_proc.num_args()). Any future variadic native proc MUST declare its full arg count in num_args() and rely on this max(). Note ZERO_FILL_UNCONSTRAINED_MEMORY makes over-extraction of absent stack args harmless (reads 0). Separately: NativeSscanf::call now returns Err to DEFER to Python — sscanf reads a CONCRETE in-memory buffer that must be parsed to constrain outputs (FormatString.interpret addr path); a native free-BVS mint made impossible paths feasible. scanf/fscanf keep native mint (stream/stdin sources are symbolic, so free BVS is a reachability superset).
native-setbuf-parity
remembered
Native void no-op SimProcedure pattern (ae54t.19/ae54t.20): stdio::NativeSetbuf (commit 356ec6d8d) mirrors procedures/libc/setbuf.py (run(stream,buf): return) -- lives in stdio.rs next to NativeSetvbuf (same buffering family). procedures/syslog.rs (angr-ae54t.20) adds NativeOpenlog (3 args ident/option/facility) and NativeCloselog (0 args) via declare_proc!, void parity with libc/{openlog,closelog}.py: return; syslog(3) itself is NOT modeled (FormatParser subclass, posix/syslog.py, left to Python). All of the above: call() returns Ok(None) -> dispatch (run_loop.rs) does the return-addr dance but writes NO return register (only Ok(Some(rv)) sets rax); every arg is declared 'bv' kind and discarded (never inspected) so a symbolic argument stays symbolic-safe rather than forcing concretization or a Python fallback -- this is the general reusable trick for any no-op native proc that ignores its args. PITFALL THAT BIT ME: cross-referencing native procs by *.rs FILENAME is unreliable -- setvbuf has NO setvbuf.rs file, it lives in stdio::NativeSetvbuf; I built a duplicate setvbuf.rs before catching it via a cargo test name-clash. ALWAYS grep the proc NAME string (e.g. '"setvbuf"') across native/angr/src/procedures/, not just ls *.rs. Verified-absent simple gaps remaining (name-grep-verified): time (forwards to the linux_kernel time syscall, needs the native syscall path), strtok/strtok_r (per-state saved ptr, fork-unsafe like getopt).
native-simoption-has-option
remembered
Native SimProcedures can now read symex-relevant SimOptions via RustSimState::has_option(name)->bool (set via set_option). The option set lives in RustSimState.sim_options (Arc<HashSet>, CoW on fork, sorted-Vec in snapshot). Python wiring is TWO-PART and BOTH are required: (1) rust_manager._add_rust_state mirrors the _NATIVE_SIMOPTIONS subset (currently just SHORT_READS) onto the rust_state via rust_state.set_option; (2) those same option names MUST be added to _apply_state_metadata's allow-list tuple — otherwise _run_python_init_if_needed strips them from state.options BEFORE _add_rust_state runs (the apply-state-metadata-strips-options caveat), and the mirror silently no-ops. To expose a NEW native-visible option: add its name string to _NATIVE_SIMOPTIONS (rust_manager.py) — it auto-flows into both _add_rust_state and the allow-list (which splats *_NATIVE_SIMOPTIONS). Manager-level test accessor: mgr._rust_mgr.state_has_option(state_id, name).
native-sleep-usleep-parity
forgotten
Native sleep/usleep parity (angr-ae54t.15, commit ac0f203df): NativeSleep/NativeUsleep in procedures/sleep.rs return concrete 0 sized to arch.bits(), mirroring getid.rs constant-return form. KEY TRICK for no-op procs that ignore their arg: declare the arg as 'bv' kind (not 'concrete') and bind it to _name — this discards it WITHOUT forcing concretization, so a symbolic duration returns 0 natively instead of falling back to Python. Registered under SIM_PROCEDURES['posix'] (same gotcha as strndup/htonl/perror — see native-htonl-htons-parity). 5 cargo + 2 parametrized Python integration tests.
native-stdio-cle-stream-fileno-unmapped
forgotten
Native read-side stdio procs (fgets/fgetc in fgets.rs) cannot resolve stream->_fileno for cle standard streams (stdin/stdout/stderr): their _IO_FILE lives in the cle##externs object, mapped LAZILY, and lazy pages are NOT fetchable from a SimProcedure context (only the VEX interpreter can fetch them). So read_fileno() hits an unmapped page and returns ProcedureError::Memory for the common fgets(buf,n,stdin) call. iter59 added naive read_fileno and this forced a ~100ms Python fallback per call -> defcamp_r100 regressed +92-117%. Fix (resolve_stream_fd fn, commit 878093d15): on Memory error, serve fd 0 natively (an unmapped FILE* can only be a cle standard stream, and a read from one is overwhelmingly stdin; matches Python stdin._fileno==0). Symbolic _fileno still falls back to Python; fopen'd files allocate _IO_FILE in Rust memory so resolve normally. Write-side procs (fwrite/fputs/fputc/putc) may have the SAME latent issue if they ever pass a cle stdout/stderr FILE* - verify before trusting their native fd-resolution on standard streams.
native-stpcpy-mempcpy-parity
remembered
Native stpcpy/mempcpy bases live in strcpy.rs (NativeStpcpy) and memcpy.rs (NativeMempcpy); stpcpy returns dest+strlen(src), mempcpy returns dest+n. Both reuse the existing strcpy/memcpy copy cores (DRY) and the fortify __stpcpy_chk/__mempcpy_chk wrappers in fortify_str.rs/fortify_mem.rs forward to them. PARITY RULE for the str/mem 'returns-end-pointer' family: only add a native base when angr ships a matching Python SimProcedure. stpncpy was once SKIPPED (tx7ec.10) for lacking a Python SimProc, but is now DONE (angr-ae54t.1, commit cf1b37cb2): a Python stpncpy.py was added first (angr/procedures/libc/stpncpy.py), then NativeStpncpy (strcpy.rs, registered procedures/mod.rs) -- both REUSE the existing strncpy copy core (DRY, mirrors how stpcpy reuses strcpy). stpncpy semantics: bounded copy of n bytes + NUL-pad, RETURN = dst + min(strlen(src), n) (ptr to written NUL, or dst+n if no NUL fits); native captures the ret offset = buf.len() BEFORE the NUL-pad resize. KEY GOTCHA (pre-existing, out of stpncpy's own scope): native NativeStrncpy NUL-pads the full n-window (buf.resize(n,0)), but Python strncpy only copies min(limit, src_len+1) bytes (no full-window pad); stpncpy inherits this per-engine divergence in strncpy itself -- only the return-pointer (stpncpy's actual point) matches both engines.
native-stpncpy-parity
forgotten
stpncpy parity (angr-ae54t.1): added Python SimProc angr/procedures/libc/stpncpy.py + native NativeStpncpy (strcpy.rs, registered procedures/mod.rs). Both REUSE the existing strncpy copy core (DRY, mirrors how stpcpy reuses strcpy). stpncpy semantics: bounded copy of n bytes + NUL-pad, RETURN = dst + min(strlen(src), n) (ptr to written NUL, or dst+n if no NUL fits). Native captures ret offset = buf.len() BEFORE the NUL-pad resize. KEY GOTCHA: pre-existing strncpy padding divergence — native NativeStrncpy NUL-pads the full n-window (buf.resize(n,0)), but Python strncpy only copies min(limit, src_len+1) bytes (NO full-window pad). stpncpy inherits this per-engine; only the return-pointer (stpncpy's actual point) matches both engines. This divergence is in strncpy itself, out of stpncpy scope. Resolves the tx7ec.10 SKIP noted in native-stpcpy-mempcpy-parity.
native-strncasecmp-parity
forgotten
strncasecmp native proc lives under SIM_PROCEDURES['posix'] not ['libc'] (same as strcasecmp). NativeStrncasecmp in procedures/strcmp.rs is a 1-block declare_proc! combining strncmp's max_len=n.min(MAX_STRCMP_LEN) bound + strcasecmp's case_insensitive=true through the shared compare_bytes helper. Python procedures/libc/strncasecmp.py == strncmp with ignore_case=True. Registry test API: NativeProcedureRegistry::new() then .has_native(name)->bool (NOT with_defaults).
native-strndup-parity
remembered
Native strndup (NativeStrndup in procedures/strcpy.rs) is DRY = scan_concrete_bounded(src, min(n,MAX_STRLEN)) for strnlen semantics + heap_alloc(len+1) + write_cstr (appends NUL). Always NUL-terminates even when truncated at n. Matches Python posix/strndup.py (strnlen+malloc+memcpy). Registers under SIM_PROCEDURES['posix'] like strdup. Registered in procedures/mod.rs after NativeStrdup.
native-subcall-dispatcher-design
remembered
Native sub-call (CallAndResume) mechanism — IMPLEMENTED and LIVE in production (S1+S2+S3, 5gf0s/xxukz; not dead/unfinished scaffolding). Native procs were return-only (NativeSimProcedure::call -> Result<Option>); now a proc can invoke a guest routine and resume via an Option-B sibling-trait pair: ProcOutcome enum {Return(Option), CallAndResume{target,args,resume_tag}} + default call_ex (wraps call) + default resume (NotImplemented), in procedures/mod.rs. State carries a serializable NativeResumeFrame{proc_name,resume_tag,saved_args,caller_return_addr} Vec (state/mod.rs), fork/snapshot-plumbed. Dispatcher (exploration/stepping.rs): handle_simprocedure recognises a resume sentinel (native_resume_sentinel/NATIVE_RESUME_SENTINEL_NAME, top-of-space addr) before native lookup; setup_native_subcall overwrites [sp] (stack ABI) or LR with the sentinel and jumps to target; RustExplorationManager::setup_native_subcall is called from the run_loop.rs sequential path, and production resume runs via core_outcome::handle_native_resume_core (angr-vh834 refactor) -- the stepping.rs handle_native_resume method is a TEST-ONLY direct-call harness for subcall_tests.rs, gated #[cfg(test)] (angr-0mqkc.2), not #[allow(dead_code)] scaffolding; SubcallSetupError's #[allow(dead_code)] is likewise just the 'fields read only via Debug' idiom, not dead scaffolding. BOTH dispatch paths route through call_ex: handle_simprocedure (Path B, interpreter mid-block) AND the run_loop.rs inline fast path (Path A, fresh entry — wired in 96dfd2994, captures caller_return_addr via get_return_addr before the sentinel overwrite). First consumer: NativePthreadOnce. Commits: S1 abd0c1544, S2 26f2bf6fc, S3 adea42e52+96dfd2994, e2e 2a1df9d9f. static_exits/CFG OUT OF SCOPE (no CFG in pure symex). Design doc: tools/decisions/native_subcall_dispatcher_design.md (verified present). IMPORTANT for future work (e.g. wiring gorvf.3 ADDS_EXITS/sub-call work): these primitives already exist — do not rebuild them.
native-subcall-enum-vs-trait-blastradius
forgotten
Native sub-call dispatcher (angr-5gf0s / xxukz) risk-#5 enum-vs-trait is QUANTIFIED, not just KISS-asserted. Measured HEAD over NativeSimProcedure scope (procedures/ only; NativeSyscall is a separate trait, untouched by either option): Option A = ProcOutcome enum replacing Option in call() signature = ~198 mechanical edits (24 impl NativeSimProcedure call signatures + 148 Ok(Some)+24 Ok(None) return sites across 6 files + trait def + stepping.rs Ok(ret_val) arm). Option B (RECOMMENDED) = sibling-trait pair: keep call() as-is, add enum ProcOutcome + default call_ex() wrapping self.call into ProcOutcome::Return + default resume()->Err(NotImplemented) = ~4 new items, ZERO edits to the 24 existing procs. Both express CallAndResume identically; B is more SOLID. Data appended to tools/decisions/native_subcall_dispatcher_design.md 'Addendum (iter 44)'. The go/no-go itself remains human-gated.
native-subcall-resume-stack-s1
forgotten
Native sub-call resume-stack S1 (bead angr-pn3w8, commit abd0c1544) LANDED: per-state Vec<NativeResumeFrame{proc_name:String, resume_tag:u32, saved_args:Vec}> LIFO field on RustSimState, foundation for the ADDS_EXITS dispatcher (design tools/decisions/native_subcall_dispatcher_design.md). Plumbing mirrors getopt-cursor bhk0a.3 EXACTLY: struct decl in state/mod.rs + 3 ctors + 6 fork.rs sites (fork/translate_state/fork_true/fork_false/fork_from_snapshot/merge — Clone delegates to fork) + snapshot.rs (field with #[serde(default)] for forward-compat + to_snapshot clone + from_snapshot move) + accessors native_resume_stack()/push_native_resume_frame()/pop_native_resume_frame(). NativeResumeFrame is Clone+Serialize+Deserialize but NOT Copy (holds Vec); RustBV already derives serde (symbolic/value.rs RustBVData). NO RustStateProxy/Python passthrough added — S2 dispatcher is pure Rust stepping.rs, Python never reads the stack (unlike getopt which needed proxy). Empty default = zero behaviour change. NEXT: S2 dispatcher core (bead angr-xxukz chain) needs spike angr-5gf0s human go/no-go (enum-vs-trait CallAndResume return) before the hard interpreter work.
native-syscall-name-is-diagnostic
forgotten
NativeSyscall::name() is diagnostic-only and has ZERO callers: the production dispatcher in exploration/stepping.rs keys on (arch_name, syscall_num) and never reads handler.name(). angr-9ke6b.214 confirmed this with rustc's dead_code lint once syscalls/ was demoted to pub(crate) — the trait method is now tagged #[allow(dead_code)] at its declaration in syscalls/mod.rs with that rationale, which also covers the $label argument of the stub_syscall!/constant_syscall! macros. It was retained rather than deleted because the ~50 impls are the only in-tree mapping from handler type to syscall name and a diagnostic log line is the obvious consumer. Implication: handler types that vary only by syscall number can be unit structs (single registration class, multiple numeric keys) — see exit::NativeExitSyscall, registered at 11 sites across 5 arches but only one impl. Lookup is via NativeSyscallRegistry::get(arch: &str, num: u64), not by name.
native-syscall-registration-discipline
remembered
Native syscall registration pattern (refined angr-0hif.3): for any new syscall family, register ONLY the syscalls that have an explicit Python SimProcedure or a well-defined stub semantics. The 'identity getters' family is a clean fit: getpid/getppid/gettid/getuid/geteuid/getgid/getegid all return constant defaults the Python proc would return (1337/1336/1000). When a sibling syscall (setuid/setgid) lacks a Python proc, file a follow-up bead and add an 'X_intentionally_absent' test pinning the gap rather than registering a guess. See identity.rs and syscalls/mod.rs::setuid_setgid_intentionally_absent.
native-syslog-parity
forgotten
Native syslog-family parity (angr-ae54t.20): openlog/closelog are void no-op stubs. Added procedures/syslog.rs via declare_proc! macro: NativeOpenlog (3 args ident/option/facility, all 'bv' & discarded -> symbolic-safe), NativeCloselog (0 args). Both 'call |_state| { Ok(None) }' -> run_loop does return-addr dance, writes NO rax (void parity with libc/{openlog,closelog}.py: return). syslog(3) itself NOT modeled (FormatParser subclass, posix/syslog.py, left to Python). Registered in procedures/mod.rs after system::NativeSystem. 5 cargo tests (syslog_tests.rs) + TestNativeSyslog (2 dispatch tests, _dispatch helper). Same void-noop pattern as setbuf. Remaining NAME-grep-verified absent simple gaps: time (forwards to linux_kernel time syscall -> needs native syscall path), strtok/strtok_r (per-state saved ptr -> bigger).
native-system-parity
forgotten
Native system() (procedures/system.rs NativeSystem) mirrors angr/procedures/libc/system.py: mints a fresh 8-bit symbolic BV ('system_returncode_N' via symbol_counter) and zero_extends to 32 (sizeof int), NOT arch.bits — same idiom as rand.rs which returns a 32-bit int. cmd pointer declared 'bv' arg and discarded so a symbolic command pointer stays native (no concretization/Python fallback). Registered under registry by name='system' (categorization libc-vs-posix only matters Python-side, not for native registry). Return is symbolic so Python integration test asserts call_counts['system']==1, not a concrete rax.
native-technique-architecture
remembered
NativeTechnique enum lives in exploration/native_technique.rs (split out of mod.rs per angr-zel8z.3) with 4 variants: LengthLimiter(max_length, drop), Timeout(timeout_secs, start_time), LoopBound(bound, discard_stash), MergePoint(address, wait_counter_limit, counter, wait_stash); re-exported pub(crate) from exploration/mod.rs so siblings' use super::* still resolve it. Stored in native_techniques: Vec. RustExplorationManager::apply_native_techniques() (exploration/helpers.rs) dispatches them, called alongside apply_uniqueness_filter(); returns bool (true = complete, triggers early exit from run loop) and ONLY the Timeout arm ever returns true. MergePoint is handled out-of-band BEFORE the match because merging needs &mut self. PyO3 API now lives in exploration/manager_methods.rs (moved out of mod.rs, angr-nbim4.1): register_length_limiter, register_timeout, register_loop_bound, register_merge_point, native_technique_count, clear_native_techniques. Python side: use_technique() sets _native_length_limiter/_native_timeout flags; filter/complete callbacks skip flagged techniques. Unit tests: exploration/helpers_tests.rs, section 'MergePoint native technique' plus 'Timeout / LengthLimiter / LoopBound native techniques' (angr-9ke6b.77).
native-time-proc-forwards-to-syscall
forgotten
Native libc time() proc (procedures/time.rs::NativeTime, angr-ae54t.21) forwards to the linux_kernel time SYSCALL model, NOT a fresh impl. The shared core is fn fresh_monotonic_time(state) in syscalls/sim_time.rs: fresh symbolic time_t constrained sge(last_time or 0), set_last_time. Both NativeTimeSyscall and NativeTime call it (DRY); each does the optional *pointer store + outcome-wrap itself because the error types differ (SyscallError vs ProcedureError). KEY enabler: a native proc CAN return a symbolic rax — call() returns Result<Option, ProcedureError>, so Ok(Some(symbolic_bv)) sets rax (mirrors the syscall's ContinueSymbolic). pointer is 'concrete' arg -> symbolic *tloc falls back to Python, matching the syscall's extract_concrete_arg gate. This pattern (libc proc forwarding to an existing native syscall core) generalizes to any libc fn that inline_calls a kernel syscall in Python.
native-uniqueness-filter
remembered
Native uniqueness filter: register_uniqueness_filter(Vec) enables per-step check in Rust. compute_register_tuple_hash() uses DefaultHasher over register values (u64 or sentinel for symbolic). apply_uniqueness_filter() called at 3 sites: end of run() loop, resume_after_simprocedure, resume_after_symbolic_branch. Python CheckUniqueness detected by class name in rust_techniques.py, auto-registered with arch-appropriate register names.
native-veritesting-dispatch
remembered
Veritesting under the Rust manager: it overrides step_state() (not step()), dispatched per-state by dispatch_step_state_with_hooks in angr/exploration/rust_techniques.py (angr-op0dn.11.7). Mechanism: export each active state via get_state_by_id, run the HookSet-composed step_state chain on the proxy whose BASE step_state is monkeypatched to a _StepStateDeclined-raising sentinel. APPLIED (Veritesting's nested Python analysis returns a merged successor-dict of angr SimStates, no rust id) -> re-import via _add_rust_state; live (None/'active') ones parked in _vt_hold stash first so the follow-up native run() does NOT double-step them, then restored to active. DECLINED (falls back to simgr.step_state -> sentinel raised) -> leave source in active, advance via native run() which (unlike proxy step_state) drives SimProcedure/syscall bounces. KEY: Veritesting merges inside its OWN nested Python SimulationManager, so states_merged_native stays 0 -- use the veritesting_applied counter (or veritesting_dispatches) to confirm routing/merge fired. use_technique() must set technique.project=mgr._project BEFORE setup() or the first dispatch AttributeErrors. Wired at all 3 run-loop step-hook sites (predicate batch + addresses loop + n-step run), checked AHEAD of _has_technique_step_hooks. Pure Python, no Rust rebuild. Gotcha: fauxware never triggers a Veritesting merge (SimProc-heavy blocks -> always declines); the merge is pinned by test_merges_on_veritesting_a which needs angr/binaries (skipped locally). Exercise the applied re-import path locally with a synthetic identity-then-decline step_state technique.
native-vprintf-family-aliases
forgotten
Native printf-family va_list variants vprintf/vfprintf are declare_proc! aliases on NativePrintf/NativeFprintf (printf.rs) -- byte-identical to their bases because the native printf core writes the RAW format string with NO substitution (trailing va_list never read; format/stream at same arg slots). Pattern mirrors fseeko=fseek. Test: mod_tests.rs::test_vprintf_family_aliases_dispatch. UPDATE (ae54t.4, 2026-06-25): Python SimProcs for vprintf/vfprintf/vsprintf NOW EXIST (angr/procedures/libc/, raw-write, no substitution) -- the prior 'angr has NO Python vprintf/vfprintf SimProcedure' note is OBSOLETE. vsprintf got its own native struct NativeVsprintf (can't alias NativeSprintf, which substitutes). See native-vsprintf-vprintf-vfprintf-parity.
native-vsnprintf-chk-parity
forgotten
native __vsnprintf_chk parity: NativeVsnprintfChk (fortify_printf.rs) num_args=6 [dest,maxlen,flag,slen,fmt,ap], drops args[2] flag + args[3] slen, forwards [dest,maxlen,fmt,ap] to NativeVsnprintf. Python __vsnprintf_chk (vsnprintf.py) subclasses vsnprintf, run(s,maxlen,flag,slen,fmt,ap) calls super().run(s,maxlen,fmt,ap). Base vsnprintf is the degenerate stub (size==0->0, else single NUL + return 1) so fmt/ap pass through unused. Same drop-flag/slen pattern as __snprintf_chk/__sprintf_chk. Drained the last ae54t fortify-printf leaf (ae54t.5).
native-vsprintf-vprintf-vfprintf-parity
forgotten
ae54t.4 SHIPPED (commit follows 7e7479a59): Python vprintf/vfprintf/vsprintf SimProcs in angr/procedures/libc/ + native NativeVsprintf in procedures/sprintf.rs. ALL THREE write the RAW format string with NO %-substitution (va_list unmodeled). vprintf=puts-style raw write to stdout(fd1, no newline) returns len; vfprintf=resolve FILE* fd then raw write returns len(-1 if simfd None); vsprintf=inline strcpy(dst,fmt)+strlen(DRY). Native NativeVsprintf(num_args=3: str,fmt,va_list) reuses read_string+write_cstr (NOT format_string), registered after NativeVsnprintf in mod.rs. KEY DECISION: vsprintf writes RAW format (consistent w/ vprintf/vfprintf raw-write family) rather than copying vsnprintf's degenerate NUL-stub -- vsnprintf is the grandfathered odd-one-out. Native vprintf/vfprintf were already declare_proc! aliases (printf/fprintf). Tests: sprintf_tests.rs test_vsprintf_writes_raw_string + test_vsprintf_no_substitution. Before this, Python had NO v-printf procs -> ReturnUnconstrained -> diverged from native aliases (latent faithfulness gap, now closed).
neon-epic-tukg-complete
forgotten
angr-tukg NEON epic complete as of iter 37 (2026-05-31, commit 6c5c6dec5). All 8 sub-beads landed: .1 QAdd/QSub, .2 PwAdd/PwAddL/PwMin/PwMax, .3 Avg, .4 Reverse, .5 RecipEst/RSqrtEst (integer URECPE/URSQRTE), .6 Cnt/Clz/Cls/PolynomialMul, .7 Shl/Shr/Sar/Sal-by-vector, .8 QShl/QSal saturating-by-vector. Only Iop_PwAdd32Fx2 (FP pairwise add) remains in parse_neon_unimplemented — parked indefinitely with no known consumer. The pytest canary in test_raise_unsupported_neon_op_via_execute_irsb has been retargeted to that op as a Binop. When/if PwAdd32Fx2 is implemented, the canary needs another retarget OR (more likely) deletion since no NeonUnimplemented variant would remain.
neon-mla-blob-test-encoding
forgotten
AArch64 NEON integration test in tests/engines/test_rust_exploration.py (TestMultiArchSupport::test_aarch64_neon_mla_blob) uses these encodings (verified individually via pyvex.IRSB before committing): FMOV S0,W0=0x1E270000 (lifts to PUT(q0)=0; PUT(s0)=w0_low32 — zeros upper 96 bits), MLA V0.16B,V0.16B,V1.16B=0x4E219400 (lifts to t=Mul8x16(v0,v1); v0=Add8x16(v0,t)), UMOV W0,V0.B[0]=0x0E013C00 (lifts to GET:I32 from byte alias). Pattern for verifying new ARM/AArch64 encoding before adding to integration tests: 'pyvex.IRSB(struct.pack("<I",encoded)+b"\x00"*16, 0x400000, archinfo.ArchAArch64(), num_inst=1).pp()' — confirms the lifter emits the expected NEON Iop.
neon-saturating-narrow-already-done
forgotten
NEON saturating-narrowing VEX ops (Iop_QNarrowUn*, Iop_QNarrowBin* signed/unsigned for 16->8, 32->16, 64->32) were implemented in angr-hzs0 (commit 7630b1d66, 2026-05-14). 21 ops total mapped in native/angr/src/vex/opcode_map.rs (9 VQNarrowUn variants in 1115-1167 + 12 VQNarrowBin in 1171-1244) and dispatched in vex/ops.rs (concrete fast path + symbolic ITE fallback). Five unit tests in vex::ops cover the variants. Bead angr-h1uh, filed 2026-05-19 to do exactly this work, was closed as duplicate without code changes. Future 'NEON XYZ unimplemented' beads — grep parse_neon_unimplemented first; the ~145 remaining placeholders are QAdd/QSub (32), QShl/QSal (16), RecipEst/RSqrt (16), Avg (12), Reverse (14), Pw* (32), Cnt/Clz/Cls (14), vector shifts Shl/Shr/Sar/Sal (32), PolynomialMul (3). QNarrow is NOT in that list anymore.
neon-unimplemented-test-canary-retarget
forgotten
When implementing a previously-unimplemented opcode that pytest uses as the canary in test_raise_unsupported_neon_op_via_execute_irsb (tests/engines/test_rust_exploration.py L11652), retarget the test to another still-unimplemented op of the same shape (currently Iop_RecipEst32Ux2 — Ity_I64 unary, remains placeholder under angr-tukg.5). Otherwise pytest fails because the canary now succeeds. Same dance applies to the cargo test_neon_unimplemented_routing sanity list in opcode_map.rs L1473 — keep at least one still-unimplemented op in the for-loop.
network-reachability-2026-06-06
forgotten
Network reachability snapshot 2026-06-06: pypi.org reachable (verified via /usr/bin/pip download archinfo, both 9.2.209 and 9.2.221 fetched). github.com unreachable (curl https://github.com/angr/archinfo returns 000 within 5s). venv pip (.venv/bin/pip) is broken for download — hits pip._vendor.resolvelib internal ImportError ('cannot import name RequirementInformation'). System /usr/bin/pip works. Implication: bd tasks gated on 'fetch upstream source' (e.g. angr-b3sc archinfo PR prep) ARE actionable offline if they only need source review/patch-prep; tasks gated on 'open PR' / 'check CI result' / 'clone github' are NOT. Apply the upstream-pr-offline-prepare-slice-pattern when feasible.
no-default-features-build-broken
remembered
The no-default-features (non-vex-engine-z3) 'mock' build of the native/angr crate compiles again as of 2026-07-20 (angr-ph300.79). Fixes: (1) thiserror is now an UNCONDITIONAL dep in Cargo.toml (was gated behind the vex-engine feature, but ConstraintSyncError in symbolic/context.rs derives thiserror::Error even in the mock build) — do NOT re-gate it. (2) SymContext::next_id() is a PROCESS-GLOBAL allocator method (NEXT_SYMBOL_ID fetch_add in bv_id_ops.rs), NOT a per-context field — merge()/fork() do NO id reconciliation (the vex-engine-z3 merge documents 'No id reconciliation needed'); never write self.next_id.load()/store(). When adding/moving a z3-only fn, still gate it AND its callers with #[cfg(feature = "vex-engine-z3")]; parallel non-z3 mocks live in solving_ops.rs/transaction_ops.rs. A CI cargo-check --no-default-features lane (angr-kl5qt) can now be added without red-failing.
no-default-features-build-broken-2026-07
forgotten
As of 2026-07-30 (iter 82, angr-1yge9.14) the rust-symex --no-default-features build was BROKEN, and had been silently red because ralph commits directly to the branch (PR-time rust_feature_flags_check gate never runs) and nobody watches nightly rust_feature_flags. Two layers: (1) LIB: symbolic/value_ops.rs (extract_no_ctx, drive_extract) called bv_codec::concrete_extract_u128 but the whole bv_codec module is #[cfg(feature=vex-engine-z3)]; and snapshot_fork_ops.rs (to_snapshot, restore_from_snapshot, the not-vex-engine-z3 merge variant) read/wrote SymContext.deterministic / .use_shared_lineage_solver which are vex-engine-z3-gated AtomicBool fields. FIXED in angr-1yge9.14: moved the Z3-free concrete_extract_u128 into a new ungated symbolic/bv_concrete.rs; gated the 3 snapshot sites (struct-literal cfg defaults to false, restore store in a cfg block, dropped the no-z3 merge's meaningless stores). This restored cargo check/build --no-default-features + the feature_flag_smoke target. (2) TESTS still broken (angr-cagbn, P2): in-crate #[cfg(test)] modules reference z3 / bv_codec / add_constraint / to_z3_ast ungated, so cargo test --no-default-features (what nightly runs) still fails ~16 errors. CORRECTION to bug-class-elimination-2026-07: its claim that nightly has 'run cargo test for all 4 combos since 2026-05-01' is only true when the build compiles -- it has NOT actually been green. Lesson: a nightly-only gate on a branch with no PR-gate enforcement can rot unnoticed; the angr-1yge9.14 ci.yml change adds a PR-time feature_flag_smoke execution step for the '' combo as a partial backstop.