angr-memories

catch / 4

78 remembered, 164 forgotten in this chunk.

no-default-features-test-gating remembered

no-default-features / no-z3 nightly cargo test combos: any #[cfg(test)] module or example that touches z3 (RustBV::to_z3_ast, SymContext::add_constraint, the z3 crate, bv_codec, StateMigrationPayload, exploration scheduler) must be gated. Test modules: gate the '#[path=..] mod' decl on '#[cfg(all(test, feature = "vex-engine-z3"))]' (symbolic/value.rs value_tests, value_ops.rs value_ops_property_tests, context.rs context_tests_constraints). Examples: declare '[[example]]' in native/angr/Cargo.toml with required-features (leak_probe->vex-engine-z3, bench_deserialize->vex-engine) since cargo auto-discovers examples and cargo test builds them. PR gate ci.yml rust_feature_flags_check runs full 'cargo test --no-default-features' for the '' combo. NOTE vex-engine-no-z3 combo still lib-broken (angr-rk5tw).

no-full-simstate-sync remembered

AVOID full SimState sync for callbacks: Generated 11 caches, identity conflicts, constraint desync, dual-solver bugs. Use RustStateProxy. Only create full SimState for SimProcedure execution.

no-native-exit-simprocs forgotten

Updated: native exit/abort SimProcs ARE now safely registered (commit 7cd3d7431). Required honoring no_return in both native dispatchers — see invariant-no-return-deadend. Supersedes the no-native-exit-simprocs memory which warned against registration.

forgotten 2026-06-04T21:57:53.266364+00:00 — Superseded by invariant-no-return-deadend (cited as out-link); itself says 'supersedes' prior memory.

nt4q-proxy-writes-deferred forgotten

angr-nt4q (commit 23f1d1016, 2026-05-25): RustStateProxy.memory.store NotImplementedError documented as a deferred limitation, not implemented. Reason: rust_proxy_writes_design.rst (existing design doc) evaluated proxy-backed register/memory writes (Option A) end-to-end and recommended Option C (status quo). Measured per-callback diff-and-push cost: 1-14 ms; aggregate at most ~7% wall on the fastest bench, <1% on the rest. Risk surface (plugin substitution) is the same class that 'avoid-state-copy-optimization' caught silently regressing ais3 3s->11s. Reopen conditions: callback density 10K+/bench OR angr-2k64 (per-state plugin restoration) revives Option A as a prerequisite. The bead's 'common Python angr idiom (mutate memory in a hook)' framing was mismatched: proj.hook() routes to SimProcedure which DOES get a full SimState (memory.store works via diff-and-push); the proxy is only used for find/avoid predicates and inspect BPs where mutation isn't idiomatic.

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

offline-loop-queue-blocker-map remembered

Offline autonomous-loop queue blocker map (verified iter24; FxHash spike consumed iter25; FxHash FULLY EXHAUSTED iter26). bd ready surfaces only epics + externally-blocked leaves; NO fully-offline engine-code close from the standing queue. Map: (1) angr-75mc xmllint -> human a/b decision (flip Cargo 'fuzzer' default vs additive non-fuzzer solve_symex.py; brief recommends b); blocks 6d3l,p65i xmllint,4n26m.12. (2) angr-ig3o.2 ARM64 BE -> upstream archinfo PR angr-b3sc. (3) angr-kq43 mimalloc/jemalloc -> crates absent from ~/.cargo/registry, needs operator network pre-stage. (4) angr-iwmp peak_memory warn->hard -> 3-night soak. (5) angr-439q/ywu7 coverage -> coverage.yml CI artifact. (6) angr-3gjm wheels.yml -> CI/Docker. SELF-DIRECTED SPIKE STATUS: FxHash-on-stash DONE (angr-dziec, benchmark-memory-fxhash-stash-done); dedup_set candidate evaluated+REJECTED iter26 (avoid-fxhash-dedup-set-spike). The std->FxHash maps are now genuinely exhausted -- do NOT re-suggest any. RECOMMENDATION: NO offline perf spike remains; surface to human for a/b (75mc) + crate-prestage (kq43) + CI-soak (iwmp/439q/3gjm) decisions, or authorize genuinely net-new measured tracked work.

offline-workflows-doc-2026-06-06 remembered

tools/OFFLINE_WORKFLOWS.md (created iter 283, angr-xll3) is the contributor-facing entry point for the three offline-prepare patterns: (A) upstream-PR slice -> tools/upstream_patches/, (B) internal-patches slice -> tools/draft_patches/, (C) aggregation-doc v1-partial -> doc with stub. Decision matrix keyed by what's MISSING (access vs data vs sibling result), not what the work looks like. Both subdir READMEs cross-link to it. Why: previously the only discovery path was bd memories, which meant a future contributor needed bd-recall to learn the pattern existed; now tools/ grep surfaces it. How to apply: when adding a new pattern variant, extend OFFLINE_WORKFLOWS.md decision matrix + reference the bd memory key as authoritative; keep the prose tight and lean on the memory for the why.

okca-statesid-scoping forgotten

angr-okca (StateId newtype for exploration/) is a single ATOMIC refactor of 419 non-comment state_id sites across 8 files, NOT incrementally splittable. Unlike borb.1/memory/Address (self-contained: addresses defined+keyed inside memory/), the state-id identifier is DEFINED in state.rs (next_state_id/state_id()->u64), STORED in stash.rs (StashManager.state_roots: HashMap<u64,u64>, set_root/get_root/find_state keyed on u64), and ENTERS exploration/ via PyO3 pymethods as u64. An exploration/-only StateId newtype thus needs conversions at BOTH edges (Python-in via FromPyObject, stash.rs-out via .raw()) with type safety only in the thin middle. Doable in address.rs style (FromPyObject extracting u64 lets pymethods take StateId directly; .raw() at stash edges) but the all-or-nothing nature + slow cargo-only rebuilds (venv pip broken) make it unfit for a single capped loop iteration. Recommend: do off-loop in one focused sitting, OR re-scope borb's StateId half to land StateId in stash.rs+state.rs together (more correct home than exploration/).

forgotten 2026-08-05T04:34:40Z — Stale: this memory recommends deferring the StateId newtype refactor as unfit for a single loop iteration, but the refactor has since landed -- native/angr/src/exploration/state_id.rs now defines `pub(crate) struct StateId(pub u64)` and it is used throughout exploration/mod.rs. The scoping analysis no longer reflects the current codebase.

oom-rust-side-aborts-not-propagates forgotten

RustOomError exists in the Rust exception hierarchy (errors.rs:63) but is currently only raised by an explicit test hook at engine.rs:312 ('oom' test variant). Real Rust allocation failures via Vec/Box default to alloc::handle_alloc_error which ABORTS the process in cargo release profile — not propagate through PyO3 as RustOomError. Python-side MemoryError DOES propagate normally through the FFI boundary. Documented in docs/advanced-topics/rust_engine.rst 'Memory pressure and OOM > Behavior on exhaustion' (angr-zidj, commit c714e9e15). Implication: if we want graceful Rust-side OOM recovery, need to switch to fallible allocations (Vec::try_reserve etc.) and surface them through RustExecError::Oom — not a trivial refactor.

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

oom-safety-fix forgotten

run_single.py was running in-process with no memory isolation — OOM-killed Claude orchestrator on 8GB/0-swap machine. Fixed: now uses multiprocessing.spawn with RLIMIT_AS=4GB. run_comparison_10.py reduced from 6GB to 4GB limit. Loop prompt forbids running run_comparison_10.py entirely.

forgotten 2026-06-04T21:57:53.606983+00:00 — Operational fix already encoded in run_single.py and CLAUDE.md (4GB RLIMIT_AS).

op0dn-ralph-sizing-review forgotten

op0dn ralph-sizing review (2026-07-11): every moonshot bead audited against the ralph-iteration constraint (1 agent, <=1h, tree clean+committed). Rubric: right-sized = one seam, one deliverable, crisp gate, ~1-3 iterations; too big = bundles independent deliverables, mixes investigation with productization, or carries an unresolved design fork. RESULT: 32 beads unchanged; 9 split into 19 children (E1 .1.1/.1.2; E2 .2.1/.2.2; S-C1 .6.1/.6.2; S5 .11.1.1/.11.1.2; B9 .11.2.1/.11.2.2; HC-2 .12.2.1/.12.2.2; M6.5 .14.1.1/.14.1.2; M6.3 .14.4.1/.14.4.2; M6.4 .14.5.1/.14.5.2); Bug M1 drain-on-cancel extracted to .13.8 (kill-proof, was buried in .13.3/.13.6); ype54 rewritten + split into ype54.1 decision spike / ype54.2 fix (guard-serialization refuted, see avoid-ype54-guard-serialization). Umbrella parents retyped to epic so ralph does not claim them (bd rejects task->epic dep edges; spike gates therefore point at terminal verdict LEGS: .6.2, .11.1.2). PREMISE CORRECTIONS found: E2 wiring already DONE (NativeLibVEXLifter fully wired behind libvex-ffi; NativeVEXLifter in vex/lifter.rs is a dead stub — E2 is verify+promote); E1 stepper exists (step_state_with_skip pub(crate)) — E1 is pyo3 exposure + extra_stop_points + proxy stubs. Borderline beads .11.5/.13.4/.9.2 left whole with stall-split notes (user decision). HUMAN GATE: angr-75mc (xmllint) blocks S6+S7; PR staged at tools/upstream_patches/angr_examples_xmllint_solve_symex.patch — needs the user to file it cross-repo.

forgotten 2026-07-20T05:01:43.517405+00:00 — Bead-sizing status snapshot (2026-07-11); tracker since taken to 0 open and statuses superseded by roadmap-2026-07-18-housekeeping (same batch); premise corrections live on the beads themselves

ops-tests-split-design forgotten

vex/ops_tests.rs split (angr-9hleg): the 5288-line monolithic test file is now 14 by-family ops_tests_.rs siblings + shared ops_test_helpers.rs. Design that worked: helpers (pack_lanes_, unpack_lane, assert_lanes, eval_v128/i64/i32, make_v128_lane0*, pack_4xf32/2xf64/2xf32_64) hoisted to ops_test_helpers.rs as pub(super) fns with 'use super::'; each family file does 'use super::' (reaches ops private items via the child-of-ops rule) + 'use super::ops_test_helpers::*' ONLY when it uses a helper (else unused-glob warns under -D). FCmpKind import lives ONLY in ops_tests_float_cmp.rs. Module decls in ops.rs are '#[cfg(test)] #[path=...] mod tests_'. Split was scripted (brace-match on ^}$, classify by fn-name prefix); 188 tests, all green under 'cargo test vex::ops'.

forgotten 2026-08-05T04:34:40Z — Narrow, closed refactor-completion narrative (ops_tests.rs file split) pinned to native/angr/src/vex/ops_tests*.rs; adds nothing a commit message wouldn't for future unrelated sessions.

otjw-top-fallback forgotten

angr-otjw spike (2026-05-20) ranked SimProcedure Python-fallbacks across 14 benches with callback_count>0. Top contributors: __libc_start_main (50 calls, 3 benches — ais3_crackme=47, sym-write=2, csgames2018=1); ReturnUnconstrained (26, 2 benches — angr generic catch-all); fwrite (22, csgames2018); fflush (14, 2 benches); my_scanf (13, defcon-baby-re — user-named binding). Aggregation script at tests/benchmarks/collect_simproc_fallbacks.py (committed 59ded1c93). Follow-ups: angr-yrhh (libc_start_main native), angr-70no (fwrite/fflush/setvbuf), angr-ilsr (audit symbolic-arg fallbacks). KEY: ais3_crackme's entire 47-callback budget is __libc_start_main; killing that fallback drops ais3 to 0 callbacks. strcpy_find has 0 fallbacks despite 97-callback budget — all native dispatch.

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

ovqja5-qfbv-smt-route-infeasible remembered

angr-ovqja.5 spike (route qfbv_smart push/pop loops to incremental smt) is NOGO/DEFERRED. SymContext holds ONE solver (context.rs solver: Mutex<Optionz3::Solver>); under ANGR_Z3_TACTIC=qfbv_smart it IS the tactic2solver. All queries (is_sat, check_branch_feasibility, eval_upto/_wide, min, max in solving_ops.rs) route through one with_z3_solver closure (snapshot_fork_ops.rs::with_z3_solver). FAITHFULNESS PARTITION: is_sat/branch return booleans (solver-invariant, but already one-shot, no loop); min/max return the unique optimum (solver-invariant result -> the ONLY faithfulness-safe loop reroute); eval_upto/_wide return a SET of distinct witnesses (solver-DEPENDENT -> rerouting to smt changes the candidate set vs qfbv_smart = the exact gate violation forbidden). So the spike's 'keep smt for push/pop loops' is faithfulness-infeasible for eval_upto. The min/max-only slice IS mechanically feasible (transient in-closure z3::Solver::new + get_assertions re-assert, get_assertions exists z3-patched/src/solver.rs) but DEFERRED because: opt-in only (zero default-gate/ovqja-attribution value, the bead's own lowest-value-survivor note) and acceptance needs the bimodal trio timing proof which is memory-flagged-avoid in the loop. Sibling of per-fork-solver-share-infeasible (angr-2yyao).

p15-conservative-fork-undercount forgotten

P15 conservative-fork fallback in stepping.rs (lines 524-545 and 1198-1207, identified 2026-05-31 angr-95up.2): when a deferred fork lacks both a stored condition AND a reconstructed condition, the 'else' branch creates a fork via successors[0].fork() to explore the target without a constraint. This DOES clone the Z3 solver (state.fork() at state.rs:1672-1708 invokes solver.borrow().fork()) but does NOT increment solver_fork_count. So solver_fork_count is a structural UNDERCOUNT of actual solver clones for the rare P15 path. Fix is small — add 'self.profiling.accumulated_stats.solver_fork_count += 1' inside the P15 else-branch in both stepping.rs locations, profiling-gated to match the other sites. Not fixed in angr-95up.2 (task was documentation-only); leave a bead if drift becomes a measurement problem. Code search anchor: 'P15: Missing condition for deferred fork'.

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

p1b-harness-pass-results forgotten

P1b bounded-harness pass (angr-9w6ad.2) results — 15 benches, rust engine, 100s timeout, 3500MB cap (driver /tmp/p1b_driver.py, raw /tmp/p1b_results.jsonl). OUTCOMES: OK(3)=hackcon2016_angry-reverser 26.8s, tumctf2016_zwiebel 56.9s*, 0ctf_trace 27.7s. TIMEOUT(6)=sharif7_rev50, 0ctf_momo_3, hitcon2017_sakura, asisctffinals2015_fake, defcamp_r200, CADET_00001. OOM(1)=insomnihack_aeg (~92s MemoryError@3500MB). FAIL(5). FAIL buckets: (a) unsupported SimOption NotImplementedError SYMBOL_FILL_UNCONSTRAINED_REGISTERS — fast-fail, NOT a perf bug: b01lersctf2020_little_engine, whitehat_crypto400, simple_heap_overflow. (b) 'IndexError: list index out of range' — REAL rust-engine bug, actionable: asisctffinals2015_license (0.4s fast-fail), ekopartyctf2015_rev100 (84s fails LATE). Z3-vs-engine split (OK benches via counters-json): angry-reverser ~84% Z3-bound (z3_check_time_ns 22.6s of 26.8s) -> P2c profiling target is Z3 not engine; 0ctf_trace ~0 z3/engine timing counters (tracing/concrete path, symbolic engine barely exercised); zwiebel BIMODAL (56s under driver, TIMEOUT@180s on standalone re-run). CATALOG DRIFT: hitcon2017_sakura rust_ok=True but TIMES OUT@100s (stale-optimistic); zwiebel notes '~2.5h' but sometimes finishes ~1min (bimodal, overly pessimistic).

forgotten 2026-07-04T00:33:51.341241+00:00 — One-off bounded-harness run-results snapshot (angr-9w6ad.2); superseded by perf-campaign-p5-deferral-decision; no external citers.

p1s02-symbolic-addr-store-fallback forgotten

RustMemoryProxy.store() (angr/exploration/rust_state_proxy.py) unbounded-symbolic-address fallback (angr-p1s02, commit 58daadb20): when state_memory_store_symbolic_multi returns not-ok (address has too many/zero satisfying solutions, e.g. an unconstrained operator new/malloc return pointer with a 0x7fff.. high prefix), the store NO LONGER raises NotImplementedError. It concretizes the addr via self._mgr.fork_state_solver(state_id).eval(addr) (angr's Max write-concretization strategy) and re-issues the store with a concrete claripy.BVV addr through the SAME state_memory_store_symbolic_multi entry point (its concrete fast path -> store_concrete_automap). KEY GOTCHA: store_concrete_automap in native/angr/src/memory/store.rs is MISNAMED -- it does NOT auto-map; it errors on unmapped non-lazy pages so Python can handle the store. So a concretized garbage high page (0x7fff00000000) still fails -> the fallback SILENTLY DROPS it (matches AVOID_MULTIVALUED_WRITES + observable gate-off outcome: nobody reads back a garbage pointer) rather than raising, keeping the SimProc state alive. New counter mgr._stats_proxy_mem_symbolic_addr_fallback (stats key proxy_mem_symbolic_addr_fallback). Fixed csaw_wyvern gate-on regression (3.66s vs gate-off 3.59s, was 0.86s dead). flareon2015_5 still regresses under the gate with a SEPARATE SimUnsatError (angr-xs4rj), so callback-memory-proxy default-on is STILL not fidelity-safe. Diag method: temporarily print addr/eval/eval_upto in the not-ok branch, run ANGR_RUST_USE_CALLBACK_MEMORY_PROXY=1 run_single.py --engine rust.

forgotten 2026-08-05T04:34:40Z — Fixed-in-commit implementation detail pinned to angr/exploration/rust_state_proxy.py and native/angr/src/memory/store.rs (store_concrete_automap misnaming); relocate the misnaming gotcha as a comment at store_concrete_automap.

page-clear-symbolic-helper forgotten

Page::clear_symbolic(offset, size) added to native/angr/src/memory/page.rs: clears symbolic_bitmap bits in [offset, offset+size) WITHOUT touching the data buffer, and drops the bitmap allocation when empty (matches the in-line block inside store_concrete). Use this when you've already lost wider-sym tracking and need to reclassify orphaned symbolic bytes as concrete — NOT when you want to actually overwrite data (use store_concrete for that). Sister method to mark_symbolic. Only public method for clearing arbitrary bitmap ranges as of 2026-06-03.

forgotten 2026-08-05T04:34:40Z — Single-function API note pinned to native/angr/src/memory/page.rs (Page::clear_symbolic); relocate as a doc-comment on that method.

panhl1-killgate-pass forgotten

angr-panhl.1 Phase-0 kill-gate RESULT = PASS (proceed to Increment 2 angr-panhl.2 RustBV::translate_into). Profiled with the work-stealing migration model (see parallel-migration-model): Z3-bound trio all clear the gate -> hackcon2016_angry-reverser 710ms/task 0 migr, securityfest_fairlight 319ms/task 0 migr, ekopartyctf2016_sokohashv2 173ms/task 0 migr (all >100ms/task AND <10 migr/bench). GIL/callback fraction tiny everywhere (0.2-1.3%). State-heavy unmapped_analysis is fine-grained (39.6ms/task, 1 migr) so that class would NOT amortize cross-thread translate cost - but only >=1 favourable target class needed. Coarse-task low-migration shape confirmed for the Z3-bound class. Full table in bd note on angr-panhl.1.

forgotten 2026-08-05T04:34:40Z — Bead-progression status snapshot (Phase-0 kill-gate PASS) for the parallel-exploration epic; superseded by substantial further parallel implementation work visible in git history (scheduler.rs, scheduler_worker.rs, persistent worker pool, angr-op0dn.13.*/ph300/nkoct/vh834) well beyond this gate.

panhl2-killgate-pass forgotten

angr-panhl.2 (parallel kill-gate Increment 2) RESULT = PASS-leaning GO (2026-06-23, commit e4033ac0e). Cross-context RustBV translate measured at 355 ns/node / 709 ns/leaf-translate on a 64-byte memory-load-shaped tree (Reverse(Concat(64 BVS))) — WITHIN the 390-677 ns/node synthetic spike, far from the 10x kill ceiling. CRITICAL favorable finding: only Symbolic LEAVES do Z3_translate; Expression nodes rebuild lazily (near-free), so real-state translate cost scales with LEAF count not total AST nodes — better than the spike's per-node assumption. Both panhl.1 (migration cadence) + panhl.2 (translate_into) now PASS. Remaining before reopening angr-1ilq (Phases 3-5): whole-state translate_state + real-trio capture = follow-up angr-ahypj. See [[translate-into-primitive]], [[panhl1-killgate-pass]], [[parallel-go-nogo-panhl]].

forgotten 2026-08-05T04:34:40Z — Bead-progression status snapshot (Phase-2 kill-gate PASS) for the parallel-exploration epic; same staleness as panhl1-killgate-pass -- superseded by the epic's substantial subsequent implementation.

panic-abort-landmine-layer remembered

panic=abort landmine audit (angr-qwyti.11): the files that directly carry #[pyclass]/#[pymethods]/#[pyfunction] are NOT where input-reachable panics live — their unwrap/expect sites are overwhelmingly documented internal invariants (mutex poison, session-live, kind_map guards; run_loop.rs alone has 36, all of this pattern). All 3 known panic=abort bugs (arch_from_name n0irt.6; shift-clamp narrowing; div-by-zero MIN/-1) lived in TRANSITIVELY-CALLED value/interpreter helpers (value_z3.rs, value_ops, RustBV), which qwyti.11's py-attr-file scope explicitly excluded. Future sweeps should target the value/interpreter arithmetic layer for guest-computed-value panics, not the py-boundary files.

panic-abort-no-poison-invariant remembered

Under [profile.release] panic='abort' (workspace Cargo.toml, angr-1cue), mutex POISONING IS IMPOSSIBLE crate-wide: poisoning requires a thread to unwind out of a live MutexGuard, but abort tears down the process at the panic site with no unwind. So every .lock().expect('X poisoned') is a provably-unreachable invariant guard, not an error path. Same logic: a worker thread cannot 'die' into a live-pool mpsc disconnect mid-wave without the process already aborting, so scheduler run_wave's channel .expect('worker died ...') can only observe disconnect at clean teardown. Consequence for panic-hardening (CQ .8, angr-0mqkc.8): there is NO fallible .expect site to propagate in exploration/scheduler.rs or run_loop.rs and NO way to 'surface a worker failure as a Python exception' -- catch_unwind is useless under abort (symbolic::value_z3::fresh_unconstrained_raw already documents this for its own path). Documented as a 'Panic policy' section in scheduler.rs module doc, cross-ref'd from run_loop.rs. To change this you'd have to switch to panic='unwind' (perf/binary-size cost).

panic-abort-release-profile forgotten

panic=abort is set in workspace [profile.release] (Cargo.toml:33, commit 493b67547, 2026-06-02). Implication: Rust panics no longer cross the PyO3 FFI boundary as catchable PanicException — they SIGABRT the Python process. Tests / external code that previously caught PanicException via 'except BaseException' will now see process death. None of the 661 existing tests broke; only documentation references PanicException (lines 7316, 13319 of test_rust_exploration.py are historical commentary, not assertions). The subprocess-style test_unknown_pipeline_name_fails_fast at line 8857 accepts either non-zero exit OR Exception/Error in stdout, so both unwind and abort paths satisfy it. Dev/test profile is unchanged — only release builds (pip install -e .) get panic=abort, so cargo test still uses unwind.

forgotten 2026-06-04T16:36:54.851552+00:00 — Implementation detail / closed-bead status note with no durable rule

panic-audit-2026-05-03 forgotten

panic-audit-2026-05-03: After 2026-04-20 audit + 72 commits, native/angr/src has 509 unwrap() calls but only 4 in production code (rest inside #[cfg(test)]). All 4 fixed in commit 9192bc506. Verification command: 'awk "/^#[cfg(test)]/{in_test=1} in_test==0 && /.unwrap()/{print FILENAME":"NR}" file.rs'. Codebase is and remains well-disciplined about error handling — test-bound unwraps dominate the count.

forgotten 2026-06-04T16:36:55.196934+00:00 — Implementation detail / closed-bead status note with no durable rule

panic-hardening-symbolic-value-pattern forgotten

RustBV::to_u128() (panicking as_u128().expect) was removed in angr-j60q0.1; use as_u128()->Option instead. The 3 prod callers degrade gracefully: memory/store.rs::store_concrete returns new MemoryError::UnexpectedSymbolic (concrete path is is_symbolic-guarded so unreachable, defense-in-depth); state/mod.rs::memory_load + exploration/pending_api.rs::_get_pending_memory return PyValueError. PITFALL when hardening the symbolic/ value->concrete panics: the value_z3.rs FPA NULL .expect sites (item 1) CANNOT be cheaply converted — to_z3_ast/to_z3_ast_cached return z3::ast::BV (not Result) and ripple to solver.rs+claripy_bridge; split to angr-j60q0.3.

forgotten 2026-08-05T04:34:40Z — Implementation detail pinned to native/angr/src/symbolic/value_z3.rs, memory/store.rs, state/mod.rs (specific panic-hardening call sites); relocate as comments at those sites. The overarching audit conclusion is preserved in kept panic-reachability-audit-2026-06.

panic-hardening-vec-float-pattern forgotten

Panic-hardening pattern (angr-j60q0.2): in vec_float dispatch, kept the dev-time debug_assert AS a tripwire AND added a graceful Err return for release. Two distinct gaps: (1) unreachable!() arms in elem/kind match expressions — replace with _ => return Err(...) (the ! of return unifies with the arm's value type, so no signature change needed; lane.rs uses InvalidFloatType(elem), scalar.rs uses UnsupportedVectorOp(format!)). (2) a debug_assert!(arity <= FLOAT_LANE_OP_MAX_ARITY) guarding fixed-size buf32/buf64 arrays indexed by arity — promote to a real if arity > MAX { return Err } BEFORE the loop, since over-arity is an index-panic abort even in release. No InvalidIR/InvalidArity variant exists in OpError (ops.rs:1589); use UnsupportedVectorOp(String) for arity/kind, InvalidFloatType(IRType) for elem.

forgotten 2026-08-05T04:34:40Z — Implementation detail pinned to native/angr/src/vex ops lane.rs/scalar.rs (unreachable!->Err, debug_assert->real arity check); relocate the arity-check-before-indexing principle as a comment at those sites.

panic-reachability-audit-2026-06 remembered

panic/unwrap reachability audit of rust-symex (2026-06-20, bead angr-j60q0). Swept native/angr/src/{procedures,syscalls,vex,memory,symbolic,interpreter} for panic/unwrap/expect/unreachable reachable from symbolic/malformed-binary/Python-boundary input. Of ~2382 raw matches only 62 are non-test production sites (proc=5,sys=2,vex=8,mem=3,sym=39,interp=5); ALL invariant-guarded, FFI-init, or test-only => 0 reachable bugs. Engine is hardened on this axis. Filed only defense-in-depth hardening (angr-j60q0.1 symbolic Z3-FPA expects + value.rs to_u128/u64 footguns + parse.rs underflow; angr-j60q0.2 vex release-mode debug_assert gaps). REJECTED/invariant (do not re-flag): mutex-poison expects (vex/opcode_map.rs:21, procedures/mod.rs:138, symbolic/context.rs:550, value.rs solving witness unwraps), all sprintf/scanf/syscall-arg/interpreter-temp slice indexing (dispatcher guarantees args.len==num_args; temp writes bounds-checked), memory/multi.rs coalesce-guarded expects, DCAS is_dcas-guarded unwraps. Don't re-run a generic panic grep expecting bugs.

parallel-2worker-ab-gate-failed forgotten

Parallel 2-worker A/B ship-gate (angr-1ilq.5) FAILED (iter69). Measured RUST_PARALLEL_WORKERS=1 vs 2 via run_single subprocess on the retargeted gate corpus: cmu_binary_bomb_partial 1.42s->2.25s (1.58x SLOWER), codegate_2017-angrybird 3.68s->4.46s (1.21x SLOWER), CADET_00001_partial (high-width stress) 7.75s->did-not-finish-in-150s across 3 attempts (>19x/hang, SIGTERM not OOM). Gate needed >=1.5x speedup at 2 workers; got net regressions everywhere. Do NOT ship parallel default-on. Signal: translate_state-on-steal + per-worker Z3 context overhead dominates at these widths; codegate w=2 main-proc peak_mem collapses 2049MB->~325MB (wide frontier held worker-local, not accumulated by coordinator) = per-steal migration churn, not useful parallelism. The CADET high-width hang is a distinct pathology (see new bead). Entry points: exploration/scheduler.rs (num_workers, steal path), helpers.rs parallel_num_workers homing.

forgotten 2026-08-05T04:34:40Z — Historical A/B ship-gate result (angr-1ilq.5, iter69) for the parallel-exploration epic; superseded by substantial subsequent parallel implementation work (scheduler.rs, persistent worker pool, steady-state coordinator, many angr-op0dn.13.*/ph300/nkoct/vh834 commits) -- current status belongs in docs/advanced-topics/rust_parallel_design.rst, not this stale gate snapshot.

parallel-cliff-was-bimodal-not-superlinear remembered

CORRECTION to earlier same-session claim of a '>26x super-linear cliff' in the parallel path: that was BIMODAL Z3 VARIANCE, not a deterministic pathology (classic benchmark-bimodal-variance-rules trap — treated single-run timeouts as signal). Verified 2026-07-01 on fork_solve_trap (identity-SimProc-hook bounce bench), trivial-solve isolation variant W5/S2/M8 at workers=4: wall scales LINEARLY with trap-call count T (T=1 2.36s, T=2 3.26s, T=3 4.25s, T=4 ~5.0s across 3 repeats 4.97/5.05/5.20s), peak_mem flat ~621MB. vs workers=1 ~1.7s => deterministic parallel overhead is ~3x on a callback-heavy workload, LINEAR in callback count, NOT super-linear. The earlier T=3/T=4 'TIMEOUT >90-180s' observations were bimodal bad-mode outliers. cProfile (with the run_single Rust monkeypatch installed — WITHOUT it you profile the vanilla Python engine, a trap I hit) shows the cost is inside RustExplorationManager.run tottime (2.3s/65 calls at T=2, ~1 run() re-entry per bounce) + resume_after_simprocedure; reattaches modest (~1/bounce), gil_work sub-ms. So the real parallel issue is a MODEST linear per-callback run()-re-entry/migration overhead (~3x on callback-heavy, less on solve-heavy), amplified by bimodal wall variance. Combined with bounce-reduction-low-roi-corpus, the honest picture: the parallel engine is in reasonable shape; the dramatic 'migration dominates / >4x timeout' framing (bd parallel-per-wave-migration-dominates) was bimodal-inflated.

parallel-drain-depth-ground-truth remembered

Ground truth for 'which arm drains right' disputes between the serial and parallel Rust arms: run the VANILLA Python SimulationManager on the same setup and count terminal states. On the 8-leaf synthetic fork_solve_pbounce_W3_S2_M8_B1 (run to quiescence, no find => no cancel token to skew the comparison) Python ends with 14 unconstrained states, and so does a Rust drain at EVERY worker count (1/2/4) when it is the FIRST exploration in the process. CORRECTION (2026-07-13, angr-op0dn.13.16): the '16 under parallel' number that once looked like a parallel fork bug was an artifact of measuring the serial baseline first IN THE SAME PROCESS — a warm process drains 16 at workers=1 too. See warm-process-exploration-divergence. Methodology rule: measure each arm in a FRESH process; a same-process A/B of two exploration arms is confounded. Also: only compare arms in the no-find/quiescent regime — a num_find cancel makes the residual frontier (and hence every terminal count) depend on which worker hit the target first (see snapshot-resume-spread-is-dump-side).

parallel-exploration-recommendation forgotten

Per angr-59jk.1 design comparison (commit 5ad385aef, docs/advanced-topics/rust_parallel_design.rst), the recommended threading model for Rust-engine parallel exploration is Option A: shared-nothing per-worker Z3 contexts with Z3_translate at coarse task boundaries. Migration cost stays <5% of runtime when scheduler-task granularity is >100 ms (10K-node state translates in 4-7 ms per angr-59jk.2 spike). Option B (single Z3 context + mutex) is rejected: every Z3 call must hold the lock, so a slow solver.check() on a bimodal bench (~21s slow mode on fairlight) would block all other workers for the full check duration; locks all forward progress, not just parallel speedup. Z3 parallel.enable=true is OUT OF SCOPE here — it's a fan-out inside SAT, not parallel state stepping, AND it's correctness-breaking in this codebase (see avoid-z3-parallel-enable memory). Migration path proposed: 5 phases — instrumentation, per-context cache partitioning, translate helper, work-stealing pool gated by RUST_PARALLEL_WORKERS env var (default 1, single-worker remains current path), Python callback dispatch handling (GIL is the cap for SimProcedure-heavy benches like mma_howtouse), A/B regression. Single-worker fallback is free under Option A but expects a small regression under Option B from unconditional lock acquisition. PARENT angr-59jk should only be promoted from deferred when parallelism is committed to; current single-threaded gaps (mma_howtouse Callable per-init memory sync, sokohashv2 Z3 nondeterminism, hackcon angr-tlvl Z3 shape) have lower-risk single-threaded fixes.

forgotten 2026-08-05T04:34:40Z — Superseded design-phase recommendation for the parallel-exploration epic; the actual implementation has since progressed far beyond the Option-A proposal stage (scheduler.rs, scheduler_worker.rs, RUST_PARALLEL_WORKERS live in native/angr/src/exploration/, referenced in CLAUDE.md's architecture map) -- current design doc is docs/advanced-topics/rust_parallel_design.rst.

parallel-go-nogo-panhl forgotten

Parallel-exploration GO/NOGO sharpened (2026-06-23): design is DONE (angr-59jk closed, docs/advanced-topics/rust_parallel_design.rst) — verdict Option A (shared-nothing workers + per-task Z3_translate), Option B (single-context-mutex) rejected (slow check() blocks all workers), Z3 parallel.enable out of scope (avoid-z3-parallel-enable). Implementation bead angr-1ilq stays DEFERRED. Sharpened recommendation: CONDITIONAL GO — commit to learning not building. The GO assumption (>100ms/task, <10 migrations -> translate <5%) is UNVERIFIED on real workloads (spike 390-677ns/node used SYNTHETIC AST shapes; CTF-reverser tasks are 10-50ms where 5ms translate = 10-50% and kills economics). Filed de-risk epic angr-panhl (blocks 1ilq) with 2 threading-free kill-gated increments: panhl.1 migration-cadence profile (Phase 0; KILL if Z3-bound trio fairlight/sokohashv2/angry-reverser + unmapped_analysis lack coarse-task shape), panhl.2 translate_into + real-state validation (Phase 2; KILL if real-state cost >> spike or lazy-memory Arc-ancestor translation unsafe). panhl.1 blocks panhl.2. Full GO (reopen 1ilq, Phases 3-5, ship-gate >=1.5x at 2 workers on trio + zero single-threaded regression) ONLY if both pass. Sequencing: complementary to ovqja z3-hotpath (that lowers per-check cost; parallelism distributes independent checks = only lever beating the bimodal Z3 FLOOR). Parallelism is the one genuine 'beyond what Python angr can do' (GIL precludes in-process parallel symex) per beyond-parity-no-motivator-closure-pattern's carve-out (bjk8 clear future path).

forgotten 2026-08-05T04:34:40Z — Bead-progression GO/NOGO decision snapshot for the parallel-exploration epic; superseded by the epic's substantial subsequent implementation (see panhl1/panhl2-killgate-pass staleness note).

parallel-is-transport-bound remembered

Parallel Rust exploration (angr-1ilq, Option A): the 2b' gate (angr-1obng) first showed per-state cross-Z3-context migration serde was the bottleneck (~60ms/state, ~1MB/state codegate; ~14ms/state cmu; both NO-GO at workers=2). angr-t3l5o then attacked transport: Phase 0 attribution (bench_migration_phases + ANGR_MIGRATE_PHASE_TIMERS) proved the Z3 SMT-LIB2 TEXT round-trip dominated (codegate emit+parse 88.8%, emit alone 66%). Phase 1 (commit 1283d6c4a) rebuilds the assume class from RustBV IR via assume_true/false on reattach and carries only the residual no-RustBV class (add_constraint_raw/add_bv_constraint/merge guards) as residual_smtlib2 (empty common case); see SymContext.non_bv_assertions + SymContextSnapshot.reassert_assumed. Result: text round-trip -> ~0, codegate per-state tax 60->14.5ms (~4.2x), gate 0.07x->0.25x but STILL NO-GO. KEY REFINEMENT: the residual ~14.5ms is ~40% serde + ~60% an IRREDUCIBLE Z3-AST-rebuild floor (~8ms/state) that every sound cross-thread transport must pay (Z3_translate too; unsound on steal anyway). codegate work budget is only ~4.2ms/state (3.5s/844 states) < the rebuild floor, so it is migration-bound at ANY transport cost IF every state migrates. So the real lever is migration COUNT, not per-state cost -> see migration-count-is-the-lever.

parallel-migration-model remembered

parallel-migration model (angr-panhl.1, record_migration_sample in exploration/helpers.rs): models a work-stealing scheduler over the active frontier with NO real threading, sampled once per step at the run_loop dispatch site (alongside record_reconvergence_sample). Each dispatch=1 task. N workers (ANGR_PARALLEL_WORKERS, default 4); homes sticky across steps (rebuilt each sample from surviving frontier to bound memory to active width); new states -> least-loaded worker; a migration (steal) counted when dispatched state's home worker has backlog (>=2 queued) while another worker is idle (load 0). Exposed via stats(): parallel_migrations/parallel_tasks/parallel_num_workers/parallel_max_active_width + python_callback_{count,time_ns}. Counters only, no behaviour change. ms/task = wall_s*1000/parallel_tasks computed Python-side.

parallel-numfind1-speculative-waste remembered

num_find=1 (first-find) exploration on a WIDE frontier is a worst case for the level-synchronous parallel wave loop, independent of migration cost. Single-threaded BFS short-circuits at the first satisfiable find (~1 expensive Z3 satisfiable()); the parallel wave speculatively dispatches the WHOLE frontiers find-checks across workers before the num_find cancellation lands, doing many unnecessary satisfiable() checks. Measured on fork_solve_W6_S8 num_find=1: workers=1=59.7s vs workers=2 timed out >180s. Implication: the parallel wall-clock win requires NO-EARLY-EXIT / exhaustive workloads (find-all, coverage, bug-sweep) where every states expensive work is necessary. Even then, per-wave full-frontier migration currently swamps the win (see parallel-per-wave-migration-dominates).

parallel-per-wave-migration-dominates remembered

The persistent worker pool (commit fa441e13a) and warm per-worker block cache (ec0a0b07d) landed and are correct, but do NOT yield a wall-clock GO. Root cause measured 2026-07-01: run_loop_parallel (native/angr/src/exploration/run_loop.rs, fn run_loop_parallel) drains the ENTIRE STASH_ACTIVE into a shared crossbeam Injector every wave and re-seeds it, so on a wide-and-slow EXHAUSTIVE workload the whole frontier is detach_for_migration/reattach-ed across worker Z3 contexts on EVERY wave. Measured on fork_solve_trap_W5_S8_M12 exhaustive (found=32, frac_ge3~0.99, max width 45, ~119 steps): workers=1=32s, workers=2 and workers=4 both timed out >140s (>4x SLOWER). Warm cache near-perfect (15 misses / 590k hits) so block-lifting is NOT the cost; the per-wave Z3-AST reattach of deep-constraint states dominates. Persistent pool killed thread/context churn and warm cache killed re-lifts, but neither touches per-wave full-frontier migration. Next lever: persistent worker-LOCAL frontiers ACROSS waves (states stay in their owning worker context wave-to-wave; migrate only on genuine steal/callback), which the persistent pool now enables but run_loop_parallel does not yet do. See angr-vh834, rust_parallel_design.rst.

parallel-snapshot-load-sensitivity remembered

Parallel snapshot resume is load-sensitive, so leaf-count gates must be measured under CPU contention, not just idle. TestParallelCheckpointFrontier[4] (tests/engines/rust/test_parallel_wave.py) drains all 8 synthetic leaves on an idle box after angr-op0dn.13.10 + .13.11, but with the box saturated (4 busy-loop procs) the SAME resume drains 6, 7, 8 or 9 across runs: which states are in flight vs parked at the dump point depends on worker scheduling, and the 9 is the .13.12 phase-2 eager-retry re-seed inflation. Method: before tightening any parallel-path assertion to an exact count, stress it — 'for i in 1 2 3 4; do python -c "while True: pass" & done' then run the test ~6x. An == gate that only ever ran on an idle box will flake in CI. angr-op0dn.13.13 owns tightening this one back to ==.

parallel-stats-aggregation forgotten

ParallelProfiling (core_outcome.rs) holds a Mutex step_stats accumulator: parallel workers call accumulate_step(&step.step_stats) after each dispatch so ALL sum-typed interpreter counters (lift_time_ns, cache_hit/miss, load/store/expr timings, block_exec) surface in mgr.stats() at RUST_PARALLEL_WORKERS>1, mirroring single-threaded accumulated_stats.merge(&step.step_stats) in stepping.rs. Solver fork/sat/deferred counters stay on the SEPARATE atomics in ParallelProfiling because the POST-STEP arms (run_post_step_core, core_outcome.rs) populate them, NOT the interpreter step_stats -- so the full-stats merge cannot double-count them. INVARIANT for future counters: if a counter is set inside the VEX interpreter step it flows automatically via the step_stats merge; if it is set in a post-step coordinator arm it needs an explicit atomic + fold_into/drain_into line. blocks_executed reads 0 in mgr.stats() on BOTH W=1 and W=2 (interpreter never populates that step_stats field) -- pre-existing, not a parallel-path gap.

forgotten 2026-08-05T04:34:40Z — Implementation-level invariant about which counters flow automatically through step_stats merge vs need an explicit atomic (f); relocate as a comment near ParallelProfiling::accumulate_step in native/angr/src/exploration/core_outcome.rs.

parallel-step-mode-routes-serial remembered

Step-mode (run(n=N) / step_func) exploration is SERIAL under RUST_PARALLEL_WORKERS>1 by design since angr-9ke6b.221 (commit fa11aea77). Python's RustExplorationManager.run(n=N) maps to N native run(1) calls, and run_loop_parallel caps each wave at the call's remaining budget (angr-9ke6b.52) — so a wave with budget 1 dispatched ~one state, tripped its CancelToken, and drained the ENTIRE resident frontier back through serde (detach_for_migration + reattach) to keep it resumable in STASH_ACTIVE. Measured on the 8-leaf pbounce synthetic, mgr.run(n=4096), identical found/steps at every worker count: serial 0.190s / 0 residual_drains, workers=2 2.923s / 178, workers=4 2.600s / 169 — a ~15x penalty for ZERO parallelism. Mitigation is a guard at the top of run_loop_parallel (next to the find_needs_python fallback): max_steps < workers => run_loop_single_threaded. Safe because that loop honors the budget exactly, leaves the frontier in STASH_ACTIVE by construction, and calls flush_parked_bounces_to_active on entry (so a pending_parallel_bounces queue parked by a prior large-budget wave is not stranded). Only the WAVE loop is guarded; run_loop_parallel_steady is exempt (frontier residency across run() calls is its whole premise). CONSEQUENCE for tests/benchmarks: any harness that wants to exercise the wave must use a BATCH entry point (explore(), whose native budget is max_steps_per_run or a 50-step batch), never mgr.run(n=...). tests/engines/rust/test_parallel_wave.py::_drain_pbounce was silently testing the serial loop until it was switched to explore(num_find=None) with drop_terminal_states pinned off (the address explore path, unlike run(), leaves terminals droppable). Pinned by TestStepModeBudgetRoutesSerial. Related: [[anti-migration-scheduler-delivered]] [[benchmark-findall-dispatch-balance]]

parallel-tasks-not-a-parallelism-signal forgotten

parallel_tasks in RustExplorationManager stats() is NOT a 'real parallel ran' signal — the panhl.1 work-stealing MIGRATION MODEL (record_migration_sample, native/angr/src/exploration/helpers.rs) bumps parallel_tasks (and parallel_migrations) on the SINGLE-THREADED run loop too (run_loop.rs run_loop_single_threaded calls record_migration_sample at dispatch). The unambiguous discriminator for whether the real work-stealing pool ran is parallel_worker_dispatch: an EMPTY list on the serial path, populated only by the two real parallel loops (stats_api.rs comment 'empty list on the serial path'). Use sum(stats['parallel_worker_dispatch'])>0. Learned in angr-op0dn.13.5 when a callable-find fallback test asserted parallel_tasks==0 and got 84.

forgotten 2026-08-05T04:34:40Z — Implementation detail about which stats field is the real parallelism discriminator (f); relocate as a comment near parallel_worker_dispatch in native/angr/src/exploration/stats_api.rs.

parallel-wave-fauxware-hang forgotten

RESOLVED (angr-q1mwl, fix 8906de8a5): test_parallel_wave.py hang was NOT the scheduler backoff commit 97b0cbdd7 and NOT a thread deadlock. Root cause: the run-loop 'active exhausted' path returned a 'found' event with a partial count (found_count<num_find), which the Python _explore_with_addresses loop can never break on, so run() spun forever on an empty active stash. See invariant-active-empty-not-partial-found. Underlying trigger: the parallel wave surfaced only 1 of fauxware's 2 accepting paths (found_count stuck at 1) — the angr-027h phase-2 eager retry now recovers the 2nd, so the test passes, but the wave-mode completeness gap is tracked separately.

forgotten 2026-07-19T19:50:55.855851+00:00 — closed-only AND status-shape

parallel-wave-technique-hang remembered

run_loop_parallel waves run their frontier to QUIESCENCE with the GIL released; native techniques (LoopBound/Timeout/LengthLimiter) are coordinator-side and only run BETWEEN waves via apply_native_techniques. So parallel + any coordinator-side technique on a non-terminating frontier = wave never returns = run() hangs, ignoring both the technique and the run(n) budget. Guard: RustExplorationManager::must_run_serial() (run_loop.rs) returns true when parallel_real_workers<=1 OR !native_techniques.is_empty(), and run_loop routes to run_loop_single_threaded (applies techniques per-step). The Python parallel_eligible gate (_engage_parallel_workers, rust_manager.py) covers the kwarg path. The RUST_PARALLEL_WORKERS env path is ALSO handled there now (angr-ph300.77): _engage_parallel_workers no longer early-returns unconditionally on the env path — it records the parsed env count (_parallel_workers_env_value) and downgrades to setter(1) for an ineligible explore (callable predicates or until), restoring the env count when a later explore is eligible. Rust cannot see until (Python-batched), so this Python-side downgrade is the fix for the until residual; older Rust builds lacking set_parallel_workers keep the pre-fix behaviour.

parallel-width-audit-verdict remembered

Parallel-exploration favorability is decided by SUSTAINED concurrent stash width, not peak width or per-task duration. The panhl.1 kill-gate PASSED the Z3-bound trio on ms/task>100 + <10 migrations but OMITTED concurrent width — and the trio are width-1 single deep paths (Z3-DEPTH-bound), so state-level threading cannot speed them up at all. The panhl.3 audit (commit 4f76f8d31) added parallel_width_hist[==1,==2,3-4,5-8,>=9] (per dispatch in record_migration_sample, helpers.rs) + tests/benchmarks/run_width_audit.py. Verdict (re-validated 2026-06-28 on a CLEAN .so, a8epx WIP parked on branch a8epx-wip): QUALIFIED GO. Wide-and-slow NON-BIMODAL targets exist: CADET_00001_partial 79% steps width>=3 (peak30, 7.3s), codegate-angrybird 51% (844 tasks), ekopartyctf2016_rev250 30%, cmu_binary_bomb_partial 86% — all explore(find=) forking workloads, statically linked, width-stable across clean/WIP. Trio confirmed NARROW (hackcon width-1, sokohashv2 width-1, fairlight 0% width>=3). CORRECTION: xmllint_getenv (bounded find=getenv harness) is NARROW on clean engine (3.8s, peak width 2, 0% width>=3); its earlier 'explodes wide/180s-timeout' was a WIP find-bypass ARTIFACT, NOT clean behavior — do NOT cite xmllint_getenv as wide-real-binary evidence. Real binaries explode wide only under UNBOUNDED/BFS exploration (the 11djq premise), not this bounded bench. Retarget any parallel ship-gate to the wide-and-slow non-bimodal CTF class; RustSimState !Send (Rc<RefCell>)->Arc is a prerequisite. LESSON: a parallelism favorability gate MUST measure step-weighted width, AND must run on a clean engine (a WIP that changes dispatch can confound exploration shape).

parallel-worker-stack-overflow-root-cause forgotten

CADET W=2 collapse root cause (angr-h92bx, NOT offload serde): scheduler worker threads in PersistentPool::new (native/angr/src/exploration/scheduler.rs) were spawned with Rust's DEFAULT 2 MiB stack, while the serial run loop runs on CPython's main thread (8 MiB). The stepping path recurses over AST shape (claripy_bridge::import self-recurses per operand; Z3 emission walks the RustBV tree), so deep symbolic-stdin ASTs (CGC/CADET_00001) overflow 2 MiB on a worker at RUST_PARALLEL_WORKERS>=2. Symptom is NOT an exception: dmesg shows 'angr-worker-N ... segfault ... error 6 in rustylib...so' with the fault address one word below sp (guard-page write). A dead worker never reports wave completion, so the coordinator's first wave never quiesces -> the 'zero completed waves in 100s' observation that looked like an intra-wave perf collapse. Fix: WORKER_STACK_SIZE=16 MiB via thread::Builder::stack_size (commit 4131a8e27). CADET_00001_partial W=2: >180s timeout -> 7.34s (W=1: 7.0s). DEBUG LESSON: when a parallel Rust run 'hangs' or is mysteriously slow, check 'sudo dmesg -T | grep segfault' FIRST — a worker-thread stack overflow presents as a hang, not a crash trace.

forgotten 2026-07-19T19:50:56.291021+00:00 — closed-only AND status-shape

param-struct-refactor-0mqkc6 forgotten

Crate-wide clippy too_many_arguments allows are DONE (angr-0mqkc.6 got to 9; angr-inieg.2 finished the tail). Only 2 remain and BOTH are API-boundary-constrained keeps, not debt: fuzzer.rs::py_new (every arg is a distinct Python kwarg — bundling breaks the Python API) and the module-level allow in vex/libvex_ffi.rs (generated FFI bindings). Do NOT 'fix' those. The retired 8 are the pattern reference: CasArgs + CasWriteback (statements_cas.rs x3), LoadGArgs (statements.rs::handle_loadg), [&IRExpr; 4] instead of arg1..arg4 (expressions.rs::eval_qop), StridedPattern (memory/ite_builder.rs::build_strided_ite_tree), ForkBundle for the deferred_forks/stored_conditions/fork_snapshots trio (exploration/callback_types.rs, consumed by PendingCallback::with_context + stepping.rs::handle_unmodeled_call), and SimProcCall (MOVED core_outcome.rs -> callback_types.rs, now pub(crate), shared by handle_simprocedure_core + ExplorationEvent::need_simprocedure). Reuse rules that worked: (1) prefer bundling into an EXISTING struct over inventing one (parallel_process_state took the CoreCtx it already built; setup_native_subcall reused NativeSubcall); (2) a fixed-arity operand list is better as an array than a struct; (3) when a struct moves into a module a child glob-imports (core_outcome_handlers.rs does 'use super::*'), the child still resolves it via the parent's re-import — no edit needed there; (4) struct-field init IS a deref-coercion site, so 'prof: &wave_prof' with wave_prof: Arc coerces fine.

forgotten 2026-08-05T04:34:40Z — Closed refactor receipt (clippy too_many_arguments cleanup done, angr-0mqkc.6/angr-inieg.2) pinned to specific structs that will drift (a/c).

parity-census-matrix-is-positional-not-prose forgotten

parity_census.py's docs-matrix parser is POSITIONAL: it classifies a SimOption by which list-table section its row physically sits in (docs/advanced-topics/rust_engine.rst), not by what the row's prose says. So an option whose row still lives in the 'Ignored -- divergence-risk' table counts as a flip blocker even when the row text already reads 'Honored as of angr-XXXX'. That is exactly why 3 of the 4 angr-op0dn.14.9 options looked like open work but needed zero code: AVOID_MULTIVALUED_READS/_WRITES (honored since angr-tfic) and ZERO_FILL_UNCONSTRAINED_REGISTERS (matches-by-default since angr-rhe2). The intended fix is an entry in parity_census.py's _EXONERATED dict with a live-code citation -- NOT moving the row -- because _EXONERATED is the audited subtract-from-silent list. Before treating a census 'silent' option as work, always read its docs row AND grep the bridge for the option name; the matrix is stale in the conservative direction.

forgotten 2026-08-05T04:34:40Z — Implementation-level gotcha about one script's parser (f); relocate as a comment near the _EXONERATED dict in tests/benchmarks/parity_census.py (confirmed still present).

parity-gap-diff-false-positives forgotten

Parity-audit gap-diff has a false-positive trap: many native procs use the hand-rolled 'impl NativeSimProcedure { fn name(&self) -> &'static str { "x" } }' form (stdio.rs: fwrite/fputs/fflush/setvbuf/feof/ferror; sprintf.rs: sprintf/snprintf/scanf) NOT the declare_proc! 'name = "x"' macro form. A diff that only greps 'name = "..."' (or even +aliases) FALSELY flags these as gaps — wasted an iter starting a duplicate fwrite. Correct native-name extraction must union THREE forms: declare_proc 'name = ".."', hand-rolled 'fn name(&self)' return string (grep -A2), and 'aliases = [..]'. Even then, ALWAYS confirm a candidate is truly absent with 'grep -rl ""PROC"" native/angr/src/procedures/ | grep -v tests' before implementing.

forgotten 2026-08-05T04:34:40Z — Implementation-level gotcha pinned to specific procedure files and a grep recipe (f); relocate as a comment near the native-name extraction logic in the gap-diff tooling.

parity-silent-option-resolution-routes remembered

angr-op0dn.14.7: the 3 default-bundle silent SimOptions (EXTENDED_IROP_SUPPORT, TRACK_CONSTRAINT_ACTIONS, TRACK_MEMORY_MAPPING) were all resolved WITHOUT implementing a feature — the census had over-counted them. Three distinct resolution routes, worth reusing when auditing any remaining silent option: (1) inverse-polarity gate — the option is honored when set and only diverges when unset, so gate on the absence (see invariant-required-option-inverse-polarity-gate); (2) attribution at the CONSUMPTION site — TRACK_CONSTRAINT_ACTIONS cannot raise or warn at manager construction (it rides the symbolic bundle, so every entry_state() would fire), but its only observable effect is the SimActionConstraint stream and _RustOwnedSimStateHistory (angr/exploration/rust_state_export.py) already warns once when state.history.actions is read on a Rust-owned state — so the divergence IS attributable, just later; recorded in _CONSUMPTION_WARNED in tests/benchmarks/parity_census.py; (3) vestigial — TRACK_MEMORY_MAPPING has NO reader anywhere in angr, claripy, cle or pyvex; the sole in-tree reference ADDS it to an option set (analyses/identifier/runner.py). Before writing a parity feature for a 'silent' option, grep for a READER: 'is it in an options set' is not the same as 'does anything branch on it'.

parse-decimal-right-align remembered

parse_decimal_to_bytes (symbolic/parse.rs) must right-align: copy low min(byte_len,16) bytes of v.to_be_bytes() into the tail of the width-sized result. The pre-fix code used offset=byte_len.saturating_sub(16) and copied the LEADING (high, zero) bytes for byte_len<16, silently zeroing every real decimal value. Mirrors parse_hex_to_bytes/parse_binary_to_bytes right-alignment. Dormant because Z3 Display emits #x/#b (decimal branch effectively dead) but a silent-zeros trap on any Z3 format change.

pattern-libc-symbolic-integration-test remembered

Pattern for symbolic libc-procedure integration tests via RustExplorationManager: (1) hook fauxware's mapped .fini area 0x4008c0 with the SimProcedure (proj.hook(0x4008c0, angr.SIM_PROCEDURES['libc'][name](), replace=True)); (2) blank_state at HOOK_ADDR with ZERO_FILL_UNCONSTRAINED_{REGISTERS,MEMORY}; (3) memory.store the symbolic bytes at BUF_ADDR=0x601100 (past .bss, lazy-mapped); (4) set rdi/rsi/rdx for SystemV; (5) memory.store(rsp, RET_ADDR=0x4008b0, endness=Iend_LE) so post-call dispatch lands somewhere mapped; (6) mgr.run(max_steps=1); (7) assert solver.min/max on rax (or rax[31:0] for int returns). See TestSymbolicLibcProcedures in tests/engines/rust/test_procedures.py.

pattern-pyobject-fork-clone-via-attach remembered

When a Rust struct holds Py and must be cloned/forked from a non-PyO3 context, Py::clone_ref needs a Python token, so the naive pattern is Python::attach(|py| v.clone_ref(py)). USE THAT ONLY OFF THE HOT PATH. On a hot path (state fork, per-step work) prefer storing the value as Arc<Py> and cloning the Arc — an atomic bump that needs no token, and the deferred pyo3 decref makes dropping the last Arc GIL-free too. RustSimState::clone_py_metadata (native/angr/src/state/fork.rs) USED to be the attach-based exemplar; as of 2026-07-14 (angr-gorvf.4.2, commit 27f44e7f4) it is the Arc exemplar instead — see [invariant-fork-metadata-arc-not-py]. Still true either way: never rely on pyo3's py-clone feature for Py::clone() — it can panic when the GIL is missing and gives no compile-time signal, which is also why Arc<HashMap<..,Py<..>>> + Arc::make_mut does not work for Py values (make_mut needs Clone).

pattern-stub-python-module-in-rust-tests remembered

python_bindings.rs to_networkx (native/angr/src/automaton/) is unit-testable without networkx installed: python_bindings_tests.rs::test_to_networkx_emits_every_node_and_labels_edges_with_python_symbols injects a stub module into sys.modules ('import sys, types; mod = types.ModuleType("networkx"); mod.MultiDiGraph = ; sys.modules["networkx"] = mod') whose MultiDiGraph records add_node/add_edge calls. This makes the cargo-test interpreter dependency-free AND lets the test assert on the raw call sequence (node list == dense id range, every edge endpoint present) rather than on networkx's own view. Reuse this pattern for any pyclass method that py.import()s an optional third-party package. Note the bead angr-9ke6b.189 premise was partly stale: the test module already existed (added by .187) — verify before assuming 'no tests at all'.

pbounce-synthetic-only-6-feasible-leaves forgotten

The pbounce synthetic (tests/benchmarks/synthetic_examples/fork_solve_pbounce_W3_S2_M8_B1) has only 6 FEASIBLE leaves, not 8 — do not write a test or acceptance criterion asserting all 2^W=8 found states are solvable. Leaves s=0 and s=4 both force b[0]==0 and b[1]==0 in the width region, which makes the accumulator a CONSTANT whose low byte is 0x66 (s=0) / 0x36 (s=4); the find gate is (acc & 0xff) == 0xee, so neither can ever hold. The engine nevertheless reports 8 found states — the 2 extra ones have solver.satisfiable()==False (false-positive find, filed as angr-3ag1l). A Python-mirror of main() for checking whether a candidate stdin really reaches reach_target lives in tests/engines/rust/test_wide_eval.py::_reaches_target. angr-ue4ro built its acceptance on the wrong "all 8 leaves" premise; check feasibility before trusting a leaf count.

forgotten 2026-08-05T04:34:40Z — Stale: angr-3ag1l (verified CLOSED) fixed the false-positive-find bug; the engine no longer reports 8 found states with 2 unsat, so the memory's central claim is now incorrect (e).

pbounce-synthetic-six-feasible-leaves remembered

The pbounce synthetic (fork_solve_pbounce_W3_S2_M8_B1, used by TestParallelExhaustiveSynthetic / TestParallelSteadyFoundCap / TestParallelFoundContentSynthetic in tests/engines/rust/test_parallel_wave.py) has only SIX feasible find-paths, not eight. Leaves 0 and 4 pin b[0]==0 AND b[1]==0, so both LCG mixing rounds fold in zero and acc is a CONSTANT determined by the leaf index s alone; its low byte is not 0xee, so the find gate ((acc & 0xff) == 0xee) is unsatisfiable there. Every other leaf leaves b[0] and/or b[1] free (167-256 reachable low bytes). Consequence: the long-standing 'exhaustive drain == 8 leaves' assertion is NOT a distinct-path count — the engine drains 8 found states over 6 real paths, two of them byte-identical duplicates, single-threaded included. Gate on the feasible leaf SET (_FEASIBLE_LEAVES = {1,2,3,5,6,7}) instead. Verify feasibility by enumerating the C source's arithmetic in Python, never by trusting a run.

pdq8-counter-overhead-validated remembered

angr-pdq8 (2026-05-25, A/B verification): the 2j5v 'zero-cost when not read' counter claim is VALIDATED. Methodology: patched native/angr/src/symbolic/{context,lineage}.rs to comment out all pure-stats fetch_add calls (53 in context.rs, 11 in lineage.rs); kept functional fetch_adds intact (self.next_id, self.push_level, self.constraint_count, self.bare_z3_push_depth, SAMPLER_TICK_COUNT, SIMPLIFY_SAMPLE_TICKER, NEXT_FRAME_ID). Rebuilt via tools/rebuild-rust.sh --cargo-only. Measured 5-10 sample medians per bench, both conditions. Results (n=10 on long benches): flareon2015_2 LIVE 4.6300s vs STUBBED 4.6150s = -0.32%; ais3_crackme LIVE 0.9500s vs STUBBED 0.9500s = 0%; strcpy_find LIVE 0.4100s vs STUBBED 0.4100s = 0%; defcamp_r100 (n=5) 0.24s == 0.24s; defcon2016quals_baby-re (n=5) 0.49 vs 0.50 (+2%, but direction wrong: stubbing strictly removes work, so stubbed-slower-than-live = noise). All directional, meaningful deltas are below the 0.5% threshold. CONCLUSION: AtomicU64::fetch_add(N, Ordering::Relaxed) on single-threaded uncontended cache lines costs <0.5% on every fast-tier bench. The 'stats' feature flag from the bead description is NOT NEEDED. Patch script at /tmp/pdq8_stub.py (also kept the design — sed-style line comment-out, no API churn — for future re-verification).

pending-writes-scaffold-inactive-not-the-path remembered

PendingWrite scaffolding in native/angr/src/memory/mod.rs (PendingWrite struct + add_pending_write/drain/flush methods) is currently INACTIVE in execution paths. The only call sites for add_pending_write are tests (native/angr/src/memory/tests/symbolic.rs, at lines 704, 723, 905, 950) — production stores still go through store_symbolic_unified -> eager concretize+ITE in store_conditional_multiple. apply_pending_writes_concrete and apply_pending_writes_symbolic in memory/load.rs are stubs that return base_value unchanged. Memory lazy-memory-load-overlay-fails explains why: per-load overlay is O(n) Z3 per load, worse than eager. The infrastructure is kept for export-time flushing but is NOT the path forward for in-flight laziness. The lazy memory design (pogf) recommends extending MemoryPage byte cells instead.

per-arch-syscall-numbers-from-angr remembered

Per-arch syscall registration (syscalls/mod.rs register_syscalls! blocks): pull the authoritative per-arch numbers from angr itself, not memory. SIM_LIBRARIES['linux'][0].syscall_number_mapping[] is {num:name}; arch keys are amd64/i386/arm/aarch64/mips-o32/mips-n64. Reverse it to look up a name's number. CAVEAT: angr's tables are TRUNCATED for some arches — getrandom is present only on amd64+i386 (absent on arm/aarch64/mips-o32/mips-n64, whose tables stop below its number). Register a handler on an arch ONLY where angr's table defines that number, so native dispatch never diverges from Python's supported set. The 7 angr-6ylm handlers + pread64/pwrite64 were added per-arch in angr-dbb1 by appending grouped blocks before each table's closing bracket (anchored on each arch's unique trailing prlimit64/renameat2 line, since X86/ARM regions are otherwise identical and break unique-match edits).

per-fork-solver-share-infeasible remembered

Per-fork Z3 solver materialize (context.rs::solver()) re-asserts the Arc-shared prefix per fork (Z3_MATERIALIZE_TIME_NS ~3ms/fork on eko rev250 / unbreakable_1). The 'share the prefix via push/pop across sibling forks' idea (angr-2yyao, DEFERRED iter9) is SEMANTICALLY INFEASIBLE for angr's stash model: a z3::Solver push/pop is one sequential stack on one object, but sibling forks are alive simultaneously, each caching its own materialized solver+delta. Sharing one base forces pop(prev)+push(this)+re-assert + sat_cache/model_cache invalidation on EVERY state switch = net loss. CRITICAL (spike angr-mawkv, iter48, doc tools/decisions/solver_pool_design.md): the 'only viable design = solver POOL with single active base + swap + cache-invalidation' that 2yyao recommended ALREADY EXISTS and is wired — it is SharedLineageSolver (lineage.rs, spikes hk7k->v5a5->v5ht, wired by angr-3ms1). switch_to() does the swap-in/out via longest-common-FrameId-prefix push/pop, O(local_diff). It is opt-in (use_shared_lineage_solver, default off) and v5ht-auto-enabled by workload shape (35% hot threshold). So 2yyao's prefix-share win is a SUBSET of SharedLineageSolver when enabled. Do NOT build a new pool (DRY violation). The only open question is gating policy (auto-enable on rev250/unbreakable_1), blocked on the same profiling-evidence gate as T3 beads, and constrained by avoid-dfs-coupling-for-shared-lineage. NOGO. Distinct from ovqja.5 (qfbv_smart tactic2solver re-blast).

perf-attr-fork-benches-z3-bound forgotten

Fork-bench perf attribution (angr-dva9j.6, measure-first): fork_solve_W6_S8 (73s/9 steps) is 99.5% z3_check_time_ns (78 satisfiable checks, ~0.94s each) — the M=20-mask/S=8-mix hard SAT the bench is built to be slow at; fork machinery is ~free (rust_deferred_fork_count @100ns, rust_solver_fork_count=0, z3_materialize 0.2%). cow_fork_scaling (2.6s/513 steps) is 47% z3_materialize (256 leaf solvers lazily minted on first access), forks deferred @0ns. Wide-fork redundant-work hypothesis REFUTED: child_lineage=None by default (use_shared_lineage_solver off, SymContext::fork snapshot_fork_ops.rs), freeze_into_shared (context.rs) is Arc::clone when local empty. Both bounded by inherent Z3 solve, not fork/snapshot/lineage — any speedup belongs to z3 hotpath epic angr-ovqja not the fork path.

forgotten 2026-08-05T04:34:40Z — Superseded by the broader, later campaign-closing conclusion in perf-campaign-p5-deferral-decision (the engine is not the bottleneck anywhere) (d).

perf-campaign-p5-deferral-decision remembered

Perf-campaign P5 deferral decision (angr-9w6ad.12, commit on docs/advanced-topics/rust_engine.rst 'Deferral decision (P5)' subsection after P4-spike-B). Closes the angr-9w6ad campaign: the Rust symex engine is NOT the bottleneck anywhere in the corpus, so no further engine work is scheduled. Bucket B (hackcon2016_angry-reverser, securityfest_fairlight, ekopartyctf2016_sokohashv2, google2016_unbreakable_1) = DEFERRED architectural: Z3 check()/eval_upto floor (45-134% z3-fraction), confirmed by P4-spike-A (deterministic seeding narrows cv 0.97->0.61 but does NOT collapse; Z3 4.13 restart-heuristic latitude unbound by random_seed) and P4-spike-B (lazy solves correctly disengaged; Rust active path already solves LESS than Python). Same class as prior epic angr-34w arch limits (hackcon 0.16x, sym-write 0.13x 'Z3 AST'). Permanent absent a Z3-version/constraint-model change; do NOT re-open as engine work. Bucket E (8) = excluded by construction NOT deferred-for-effort: CADET_00001 (>280s leak angr-027h, correctness bug), defcamp_r100__dfs (no own solve.py), cow_fork_scaling (synthetic no py driver), 5 *_branch arch smoke benches (python_time null by design). Tallies A=2 (both NO-OP non-engine) B=4 defer C=18 faster D=0 E=8 excluded.

perf-counter-fishing-exhausted remembered

Perf-counter fishing on safe benches (fauxware/ais3_crackme via run_single.py --counters-json) is EXHAUSTED as a source of new offline perf leads, not just FxHash. iter29 (2026-06-19) confirmed: fauxware's top counters are callback_simprocedure_total_ns (59ms, ~80% of run) split into execute_ns/sync_back_ns/state_create_ns — i.e. the Python<->Rust SimProcedure boundary round-trip. That cost is architectural and already analyzed: sync-back direction has a standing 'keep status quo (Option C diff-and-push)' design verdict (see proxy-writes-design-verdict), and mma_howtouse callback-heavy slowness was re-measured post-d1dr (see invariant-cache-python-callback-states cluster). z3_check_time_ns is the next tier (Z3, externally bounded). CONCLUSION: do not re-run counter dumps hunting for a fresh lead unless a NEW bench lands that exercises a different path (e.g. nonzero syscall_python_fallback_count, still zero across the whole corpus per bench-syscall-fallback-zero-coverage). The offline engine-code queue being dry is corroborated by structure, not just absence of beads.

perf-dashboard-architecture forgotten

perf dashboard pipeline (angr-myty, commit 540313127): nightly-ci.yml emits bench_history_record.json per run, uploaded as artifact bench-history- (90-day retention). perf-dashboard.yml uses workflow_run trigger after Nightly CI, downloads last 60 artifacts via 'gh run list' + 'gh run download', aggregates via tools/aggregate_bench_history.py to docs/dashboard/data.json (gitignored), deploys static Chart.js site to GitHub Pages via actions/deploy-pages@v4. Featured tiles: fauxware/ais3_crackme/csaw_wyvern. CRITICAL: workflow_run fires on any conclusion — the job 'if:' guard checks event.workflow_run.conclusion=='success' before proceeding.

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

perf-dashboard-schema remembered

perf dashboard data schema (commit 540313127): tests/benchmarks/run_regression.py --history-record emits {schema_version, timestamp (ISO Z), commit, branch, results} per run. tools/aggregate_bench_history.py expects exactly that shape and SKIPS records with unknown schema_version. INVARIANT: when adding fields, keep schema_version=1; bump to 2 only on a breaking change AND update the aggregator's accept list, or every old record will silently drop out of the dashboard.

phase2-retry-masks-snapshot-loss forgotten

angr-op0dn.13.12: the angr-027h phase-2 eager retry (RustExplorationManager._maybe_phase2_eager_retry) re-seeds self._initial_seed_states = the states the manager was CONSTRUCTED with. After load_snapshot those are the pre-load PLACEHOLDER, not the restored frontier, so a resumed find-based explore that exhausts would silently re-run the whole exploration from a path the caller never asked to resume (and flip use_deferred_forks off globally, which drops stored branch conditions on the parallel path -> unconstrained fallback forks). Fix: load_snapshot now sets _initial_seed_states=None and _phase2_retried=True, retiring phase 2 for any resumed manager. THE TRAP: this silently MASKED snapshot frontier loss. TestParallelCheckpointFrontier's serial arm asserted 8/8 leaves and passed — but only because phase 2 re-ran the exploration from the placeholder (a valid pbounce entry state) and back-filled the leaves the snapshot had lost. With phase 2 retired the true restored-frontier yield is 4/8 (angr-op0dn.13.14). Lesson: any test whose green depends on an implicit retry/fallback path is not testing what its name says — suppress the fallback before trusting the assertion.

forgotten 2026-08-05T04:34:40Z — Debugging narrative pinned to rust_manager.py::_maybe_phase2_eager_retry and a fixed bug (f); the QA lesson (a green test masked by an implicit fallback) belongs as a comment near that function/test.

phase3-cache-invariant forgotten

MultiPayload::collapse(default_byte, ctx) caches the right-folded ITE keyed on default_byte. Cache is RefCell<Option>, invalidated by push(). Clone propagates cache (Z3 ASTs are refcounted, cheap). set_multi_alternatives always installs a FRESH payload so the merge path in install_multi_for_candidates is automatically safe. Future code that mutates alternatives via any path other than from_alternatives + set_multi_alternatives MUST call invalidate_cache or break this invariant.

forgotten 2026-08-05T04:34:40Z — Implementation-level cache-invalidation invariant for one struct (f); relocate as a comment near MultiPayload::collapse in native/angr/src/memory/multi.rs (confirmed location).

phase3-cache-residual-gap forgotten

Phase 3 collapse cache (angr-j0n4, commit 3bf156896) landed but the Phase 2 gate stays off. Cache helps gate-on sym-write 1.76s→1.72s but gate-off remains 1.55s — ~10% residual gap. Profiling: z3_check≈144ms either way, load_stmt only 8ms of 1.55s. The bottleneck is NOT load-time ITE rebuild (that's what the cache fixes). It is (1) per-byte iteration in assemble_load_with_multi — even cache hits do N page lookups + N byte concats, vs eager's single symbolic_objects.get(addr) returning a width-N BV; (2) flush_multi_cells writes per-byte symbolic_objects so state export volume 4-8x larger than eager. Fixing the gate requires wider-load collapse cache + per-byte flush coalescing — do NOT re-attempt the gate flip based on collapse caching alone.

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

phase41-bottleneck forgotten

Phase 4.1 (angr-mmdh.1, commit a1d9f5593) closed the load-time portion of the sym-write gate-on regression: gate-on load_stmt 10ms vs gate-off 9ms (1ms gap). But wall-time gap is still ~10% (1.78s gate-on vs 1.62s gate-off measured 2026-05-15). The remaining gap is in Z3 work, NOT in load assembly: z3_site_eval_upto 55→136ms (+81ms), z3_check 86→158ms (+72ms), z3_check_count 126→310. Root cause: flush_multi_cells writes per-byte symbolic_objects entries that downstream concretization (state export) iterates over individually. Each per-byte entry triggers its own eval_upto + sat check. Fix is Phase 4.2 (angr-mmdh.2: coalesce adjacent per-byte entries on flush). DO NOT try wider-load cache tweaks to close the residual — load_stmt is already <1ms over baseline.

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

phase42-bottleneck forgotten

Phase 4.2 (angr-mmdh.2, commit 278fc5223) coalesces adjacent Multi bytes in flush_multi_cells into a single wider symbolic_objects entry (capped at 16 bytes per run). Detection: cond-fingerprint match across consecutive byte addresses (Arc::as_ptr of MultiAlternative::cond's operands — install_multi_for_candidates clones one cond per candidate into each byte, so neighbours from the same store-call share Arcs). For each run: per-alt wider value = concat per-byte alt.value endian-correctly; wider default = concat page bytes; right-fold (cond, wider_value) into one ITE of width 8N; install at symbolic_objects[run_start] with symbolic_spans for interior bytes. sym-write gate-on 1.78s -> 1.70s (avg 4 runs), z3_check 158->147ms, z3_site_eval_upto 136->125ms. Closed ~35% of the gate-on/gate-off gap (1.78->1.62 baseline). Residual ~10% gap is in state-export downstream eval that doesn't scale linearly with symbolic_objects count — Phase 4.3 candidate.

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

phase43-default-flip forgotten

Phase 4.3 (angr-mmdh.3, commit e5c594fe1, 2026-05-15) made Multi-cell lazy stores the only path for symbolic-address Multiple/Strided store concretizations. Removed the use_multi_cell_stores gate field, getter/setter on SymbolicMemory + State, PyO3 bindings (set_use_multi_cell_stores / use_multi_cell_stores), and the now-dead store_conditional_multiple helper. store_symbolic_unified and store_with_concretization always call install_multi_for_candidates_safe now. Old eager store_symbolic (line 168 of store.rs) is still used by tests and is unchanged — it doesn't go through the unified path.

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

pmovmskb-binary-free-test forgotten

Iop_GetMSBs8x16 (x86 PMOVMSKB) IS testable binary-free, contrary to the iter-50/51 handoff claim that it 'needs an SSE-strlen/PMOVMSKB binary'. A single shellcode blob 660fd7c0 (pmovmskb eax, xmm0) + c3, with xmm0 set via state.regs.xmm0 = claripy.BVV(v,128), drives the op end-to-end through the Rust interpreter. Read s.regs.rax & 0xFFFF for the 16-bit MSB mask (bit i = byte i's bit 7). Pattern: TestVexGetMSBsInstruction in tests/engines/rust/test_strategy_vex.py (commit 657b1b211, bead angr-xhbka). Setting xmm0 from the Python state DOES export into Rust on the input side (not just readable post-procedure as in the strtod test). Same load_shellcode template as test_misc.py TestRust*MemoryStore.

forgotten 2026-08-05T04:34:40Z — Narrow, single-opcode test-technique note correcting an old handoff claim (f/c); relocate as a comment near TestVexGetMSBsInstruction in tests/engines/rust/test_strategy_vex.py.

pogf-design-recommendation-2026-05 forgotten

Lazy symbolic memory design (angr-pogf, 2026-05-14) recommends Option A: per-byte MultiValues port (Python-equivalent), NOT Option B (Z3 array primitive) or Option C (Python fallback hybrid). Rationale: A reuses existing balanced ITE builder, concretization strategy chain, fork/merge plumbing; only the page byte cell representation changes. B has unknown perf cliffs with no in-codebase template. C caps gains at Python's number. Two-phase plan: Phase 1 (angr-czph) introduces Multi cells + read-time collapse, Phase 2 (angr-qh5u) makes store_symbolic_unified emit Multi cells by default. Phase 0 prerequisite: ite_depth_max counter as gate metric. See docs/advanced-topics/rust_lazy_memory_design.rst.

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

posix-callback-counter-export-only forgotten

posix callback counters (callback_posix_count/total_ns, angr-afbx) only increment during state EXPORT/materialization to Python SimStates (rust_state_export._sync_cached_state + snapshot_to_angr, rust_state_cache), NOT during the Rust step loop. A bare mgr.run() leaves the count at 0; it goes nonzero only once terminal states (deadended/found) are materialized (e.g. accessing state.posix). Implication for angr-6zxx's '>5% wall in posix' DEFER->GO trigger: the wall measured is export-time posix injection, not per-step posix work. Counter feeds python_callback_count via the callback-prefix loop in RustExplorationManager.stats.

forgotten 2026-08-05T04:34:40Z — Implementation detail about counter increment timing for one subsystem (f); relocate as a comment near callback_posix_count in angr/exploration/rust_state_export.py.

postsync-sat-removal forgotten

Removed _sync_exported_constraints post-sync satisfiable() check (commit 5d22a70a5). Was a diagnostic that called Z3 on full constraint set — took 66s for hackcon (LAZY_SOLVES 20x20 system) with zero benefit. The same Z3 solve happens in eval() anyway. Net savings: 0-1.5s (Z3 solve moves from sat_check to eval, no double-solve).

forgotten 2026-06-04T16:36:55.551381+00:00 — Implementation detail / closed-bead status note with no durable rule

pr-bench-gate-jitter-risk remembered

PR-time benchmark_regression gate in .github/workflows/ci.yml uses --rust-only --skip-bimodal --threshold 0.15. Dry-run on the dev box (with system load) flagged sub-second benches (defcamp_r100 0.23s baseline, google2016_unbreakable_0 0.88s, strcpy_find 0.39s) at 13-29% deltas — i.e. 60-140ms absolute. Even flareon2015_2 (3.62s baseline) bounced to 4.29s once (+18%). On a quiet CI runner this is typically fine, but if the PR gate flakes, options are: (a) rebaseline tests/benchmarks/baseline_timings.json, (b) add an --abs-threshold flag (regression requires both relative AND absolute delta — sketch: rust_time > baseline*(1+threshold) AND rust_time-baseline > abs_threshold), (c) raise --threshold to 0.20 for PR-only. Do NOT drop --skip-bimodal — those benches have known 2x bimodal Z3 variance.

pre-existing-dcas-test-failure forgotten

RESOLVED 2026-05-13 (angr-ruay). The DCAS test_dcas_cmpxchg16b_no_match_keeps_memory + 2 native FD tests (pipe/dup2) were failing because they predated commit 974e2daeb (angr-3uye.2), which changed ret-with-empty-callstack routing from 'deadended' to 'unconstrained'. Fixed by passing save_unconstrained=True and including 'unconstrained' in the survived/inspected stash list. See invariant-ret-empty-callstack-test-fixture.

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

pre-pinning-dangerous forgotten

AVOID pre-pinning constraints: Adding ast == BVV(concrete) for tracked symbols overconstrained states. REVERTED in v2 commit 2f0e8164f. Use Rust solver eval fallback instead.

forgotten 2026-06-05T17:23:37.163919+00:00 — Subsumed by constraint-export-no-pre-pin (mandatory-keep covers same rule).

predicate-eval-uncached-states forgotten

CRITICAL BUG FIXED: _evaluate_predicates_on_active only iterated self._state_cache, which only contains states created during SimProcedure callbacks. States forked purely in Rust (e.g., symbolic branch at cmp instruction) were NEVER evaluated by callable predicates. Fix: also query Rust stashes (active + deadended) for all state IDs and create lightweight Python states from cached ancestors for uncached ones via _create_state_for_predicate(). This fixed sym-write from FAIL to correct (70 solutions).

forgotten 2026-08-05T04:34:40Z — Closed bug-fix receipt (CRITICAL BUG FIXED, sym-write now correct) (a).

predicate-reevaluation-bug forgotten

Callable predicate evaluation (find/avoid as callables) had a bug: _evaluated_state_ids permanently prevented re-evaluation of states. When stdout grew between batches (e.g. puts executes after state first seen), predicate was never re-checked. Root cause: _evaluate_predicates_on_active used a permanent set. Fix: replaced with _predicate_matched_ids that only tracks states already moved to found/avoid. States in active/deadended are always re-evaluated. Fixed csgames2018 (0 found → 1 found).

forgotten 2026-08-05T04:34:40Z — Closed bug-fix receipt (fixed csgames2018 0->1 found) (a).

prefer-reading-write-only-diagnostics remembered

Dead-code triage rule for WRITE-ONLY DIAGNOSTIC data in the rust engine: prefer READING it over deleting it. A field/method that only ever carries a human-readable label or a diagnostic value is usually dead because a log line was never written, not because the data is worthless — and the adjacent code path is typically silent in a way that hurts debugging. Two instances from angr-9ke6b.218: (1) item 6, NativeSyscall::name — wired into handle_syscall_core (exploration/core_outcome_handlers.rs) on both Python-fallback paths; the handler-declined arm (Err(_) => fall through) had NO log at all, so 'a registered native handler declined' was indistinguishable from 'no native handler for this (arch, num)'. (2) item 8 DEFERRED, the RunResult jumpkind/min_target/max_target/limit fields populated from ConcretizedJump::TooMany — core_outcome.rs's UnconstrainedJump arm is still silent; a state reaches the unconstrained stash with no log line. Contrast with genuinely dead symbols (BlockResult::{Continue,Error}, use_memory_callbacks) which carry no information and should just be deleted. Test: does the value answer a question a debugger would ask at that site? If yes, log it and drop the #[allow(dead_code)].

prefetch-disabled-by-default forgotten

Load prefetch (use_load_prefetch in VEXInterpreter) defaults to false at interpreter/mod.rs:729 (fn with_config) with comment 'Disabled by default - adds overhead for most workloads'. set_load_prefetch (prefetch.rs:6) is the only toggle but is never called from anywhere — Python doesn't enable it. So prefetch_loads_for_block early-returns on every block in practice. Optimizations targeting only the prefetch path (e.g., expr-eval LRU, scan-loads memoization) have ~zero measurable impact. The prefetch_loads/unique/dedup/callback scratch buffers added in angr-dtiy still help if anyone re-enables prefetch, but don't move benches today.

forgotten 2026-08-02T21:31:27Z

prefetch-sp-offset-bug forgotten

prefetch.rs get_stack_pointer had wrong VexArch->offset table for ARM (used 52, actual sp_offset 60) and ARM64 (used 52, actual 264). The bad offsets meant is_stack_region() never matched real ARM/ARM64 stack addresses, silently disabling stack-aware prefetch on those archs. Lesson: hand-coded arch register tables drift from the canonical arch impls. Anywhere you see a 'match self.arch { VexArch::... => offset }' pattern in interpreter_cb/, prefer routing through self.registers.arch().sp_offset() / .ip_offset() / etc.

forgotten 2026-06-04T21:58:01.375668+00:00 — Iteration receipt of a fixed bug; lesson (route through arch helpers) is general practice already discoverable in code

prexisting-public-api-drift-failures forgotten

3 tests in tests/engines/test_rust_public_api.py::TestClassPublicAttrs::test_class_public_attrs_match_inventory (params RustExplorationManager, RustRegisterProxy, RustMemoryProxy) FAIL on rust-symex HEAD as of 2026-06-13 (commit fd3cc6325/743834cb1) with 'public surface drifted' — the runtime proxy classes expose methods (copy, merge, set_state, store, etc.) not listed in angr/exploration/_public_api.py inventory. These are PRE-EXISTING, not caused by test refactors. Fix = update _public_api.py inventory. Don't blame an unrelated change for these.

forgotten 2026-07-04T00:33:51.729796+00:00 — Stale status snapshot: the 3 failing tests were fixed in 1d488466a (angr-ytu9 CLOSED); superseded by public-api-proxy-protocol-surface.

printf-family-symbolic-format-boundary forgotten

printf/scanf/sprintf symbolic-format boundary is asymmetric on symbolic format BYTES (address is uniform: all extract_concrete_arg the format ptr -> SymbolicArgument fallback). scanf::read_format_string and sprintf::read_string call extract_concrete_arg on every byte, so the FIRST symbolic byte raises SymbolicArgument and falls back to Python. But NativePrintf::call (printf.rs) does NOT fall back: its byte loop just breaks on a symbolic byte, writes the concrete prefix to stdout, and returns success (it only does raw-copy, no specifier substitution). No symbolic-format substitution path exists for any of them. Documented in docs/extending-angr/simprocedures.rst 'Worked example 4: format strings'.

forgotten 2026-08-05T04:34:40Z — Verbatim duplicated in docs/extending-angr/simprocedures.rst 'Worked example 4: format strings' (verified via grep) (b).

procedure-syscall-error-memory-variant forgotten

ProcedureError and SyscallError both carry Memory(crate::memory::MemoryError) variant (added angr-7xhx 2026-05-31). thiserror's #[from] attr provides From, so .memory_load()? / .memory_store()? propagate directly. Don't reintroduce .map_err(|e| Pe::MemoryError(e.to_string())) — that was the boilerplate this refactor removed. Display still shows 'memory error: '. Pattern-match consumers can now drill into MemoryError::Unmapped { addr, size } / Permission / Symbolic etc.

forgotten 2026-06-04T21:58:01.725769+00:00 — Refactor receipt with commit citation; current code shows the pattern

procedures-cluster5-scan-driver forgotten

procedures cluster-5 dedup (commit a444fa02f): the concrete-fast-path-then-symbolic-collect string scans (scan_for_null_symbolic in strings.rs, scan_for_byte/scan_for_byte_last in strchr.rs, compare_bytes in strcmp.rs) share one generic driver scan_concrete_then_collect<B,R> in strings.rs. B=per-position load (RustBV for single stream, (RustBV,RustBV) for strcmp's two streams); R=concrete early-return result. concrete closure returns ConcreteStep::{Continue,Stop(R),BeginCollect}; collect_stop decides when to halt collecting (concrete null). Returns ScanResult::{Stopped(R),Exhausted,Collected(Vec<(u64,B)>)}. GOTCHAS: (1) memory_load returns MemoryError not ProcedureError so the load closure must wrap Ok(...?); (2) symbolic-target paths use ConcreteStep::::BeginCollect turbofish since R is otherwise unconstrained, and Stopped(_)=>unreachable!; (3) scan_for_byte_last threads last_match via a &mut capture in the concrete closure, readable after the driver returns (NLL).

forgotten 2026-08-05T04:34:40Z — Implementation-level refactor detail (generic driver, gotchas) pinned to strings.rs/strchr.rs/strcmp.rs (f); relocate as a comment near scan_concrete_then_collect in native/angr/src/procedures/strings.rs.

procedures-format-common-shared-helpers forgotten

procedures/format_common.rs is the canonical home for printf/scanf leaf parsers. parse_width_digits returns (width, bytes_consumed); parse_length_modifier returns (LengthModifier, bytes_consumed). LengthModifier enum has 8 kinds (None/Char/Short/Long/LongLong/SizeT/IntMax/PtrDiff) with the single width helper LengthModifier::int_conv_bits(arch_bits) — it replaced the old boolean is_64bit() in angr-9ke6b.111 because l/z/t are arch-dependent. sprintf and scanf each keep their own top-level state machine — only leaves are shared. When adding a new printf/scanf-family procedure (e.g. fscanf, vsprintf), use these helpers; do NOT inline new ad-hoc digit-loops or length-modifier matches.

forgotten 2026-08-05T04:34:40Z — Implementation-level helper inventory for one module (f); relocate as a comment in native/angr/src/procedures/format_common.rs.

procedures-strings-shared-helpers forgotten

procedures::strings (created angr-arf5, commit b10f0785c) is the canonical home for null-terminated-string scanning helpers shared by libc string procedures. Two concrete-only variants: scan_concrete_until_null (errors on max) and scan_concrete_bounded (returns null_found bool). Two symbolic-aware: scan_for_null_symbolic + build_strlen_chain. find_null_addr returns the null position for strcat-style use. Add new str* procedures here, not as ad-hoc loops in the procedure file.

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

procedures-symbol-counter forgotten

Symbol-counter unification (angr-alhy, 2026-05-31, commit 82df4781b): native SimProcedure fresh-symbol counters now live in procedures::symbol_counter(prefix: &'static str) -> u64, backed by OnceLock<Mutex<HashMap<&'static str, u64>>>. To add a new procedure with symbolic outputs, call symbol_counter("my_prefix") instead of defining a per-procedure static AtomicU64. Picked per-prefix (each prefix has its own counter at 0) over single-global because changing IDs would alter every existing symbol name and could break tests that assert specific names. Lock contention is negligible at procedure-call rates (microseconds vs milliseconds per state).

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

procedures-write-and-memcommon-helpers forgotten

procedures/ write-side + symbolic-addr shared helpers (angr-24pv4.7, rust-symex). strings.rs now owns the write counterparts to its read/scan helpers: write_concrete_bytes(state,addr,&[u8]) (the per-byte memory_store(addr+i,RustBV::concrete(byte,8)) loop) and write_cstr(state,addr,&[u8]) (bytes + trailing NUL at addr+len). Use write_cstr for always-NUL-terminating C-string writers (strcpy/strdup/strcat/strncat/getenv/sprintf); use write_concrete_bytes for no-terminator or caller-placed-terminator writers (strncpy/snprintf truncated slice/fread/read). NEW module procedures/mem_common.rs owns the symbolic-DEST-address preamble shared by memset+memcpy: consts MAX_SYMBOLIC_ADDR_CANDIDATES=64/MAX_SYMBOLIC_ADDR_SIZE=256/MAX_SYMBOLIC_ADDR_STORES=4096, check_symbolic_addr_size(size_bv)->Result<Option> (None=>size0 noop, Err on symbolic/over-cap), enumerate_addr_candidates(state,ptr,name)->Result<Vec> (eval_upto cap+1, empty/unbounded reject). The candidatessize (memset) vs |dst||src|size (memcpy) store-budget guard stays caller-side. Do NOT re-add per-file MAX_SYMBOLIC_ADDR_ consts.

forgotten 2026-08-05T04:34:40Z — Implementation-level helper inventory with specific consts (f); relocate as comments in native/angr/src/procedures/mem_common.rs and strings.rs.

profile-python-bench-harness forgotten

profile_python_bench.py harness (tests/benchmarks/profile_python_bench.py, landed 2026-06-06 commit 60bbac47d for angr-trsg). Runs one angr-examples bench in-process under cProfile, then dumps {pstats, text top-50 cum+tot, folded collapsed-stack}. Use: python tests/benchmarks/profile_python_bench.py
--engine {python,rust} --cprofile --out /tmp/PREFIX For perf record on Rust frames, run WITHOUT --cprofile: perf record -F 99 -g -- python tests/benchmarks/profile_python_bench.py
--engine rust Why the harness exists: profile_rust_bench.sh wraps the criterion micro-benches in native/angr/benches/vex_engine.rs, NOT actual bench solve.py scripts. cargo-flamegraph/py-spy were unavailable in the build env (no network for cargo install, venv pip broken). The folded format is the input format for flamegraph.pl — render SVG offline.

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

proxy-baby-re-corruption-mem-aliasing forgotten

angr-5aj8 investigation findings (2026-06-06, iter 4 incomplete): With ANGR_RUST_USE_CALLBACK_MEMORY_PROXY=1 on defcon2016quals_baby-re, the deadended state has PC=0xf2aff9eee1eeffff (corrupted). Root-cause narrowed to: between two consecutive Python SimProc callbacks (e.g. printf then fflush), the value at the stack return-address slot (mem[sp=0x7ffffffefef8]) goes from the correct concrete value (e.g. 0x402614 from the call's push) to a specific concrete witness 0xf2aff9eee1eeffff. The corruption window is during VEX interpreter execution BETWEEN callbacks. Key observations: (1) the corruption value is a stable CONCRETE BVV, suggesting a Z3 witness for some unconstrained symbolic that got hardened into Rust memory. (2) Adjacent stack addresses (0xfee0, 0xfee8, 0xfef0, ...) show ALTERNATING pattern 0xc0aefffffffeffff/0xf2aff9eee1eeffff at 16-byte period. (3) No _cb_memory_store / _cb_memory_load fires for stack addresses during the corruption window (verified by Python-side instrumentation wrapping the methods) — it's NOT a Python-mediated write. (4) Same state_id (1) across all callbacks — no fork. (5) Bug reproduces even with a no-op my_scanf SimProc — the symbolic write at flag_0/flag_1 slots is NOT the trigger. (6) Reproduces with just a TracePrintf hook (return 0) and TraceFflush hook (return 0), so default angr SimProc behavior isn't the trigger either. The unconstrained fill at _start writes 3976 bytes at 0x7ffffffef000..0x7ffffffefF88 (filler_mixin) — this region INCLUDES 0xfef8. Hypothesis: during VEX interpreter execution between callbacks, the rust_memory (taken from state.take_memory) ends up with UNCONSTRAINED symbolic at 0xfef8 instead of the concrete value the call instruction's STle pushed. Maybe the interp.flush_stores_to_rust_memory at end of step (stepping.rs:1148) is not committing the pending concrete writes, or the state.replace_memory(mem) at stepping.rs:104-106 is restoring stale memory. Next investigation: add Rust-side eprintln in store_concrete and store_concrete_automap_internal to log writes at addr=0x7ffffffefef8, and verify that the call's STle actually stores 0x402623 there. Also check if RustSimState memory differs from interp.rust_memory at the moment of recovery. Repro: ANGR_RUST_USE_CALLBACK_MEMORY_PROXY=1 python tests/benchmarks/run_single.py defcon2016quals_baby-re --engine rust → FAIL with 'Lift error at 0xf2aff9eee1eeffff' in 0.16s. Reference: bd-key gate-baby-re-attribute-error-triage (referenced in task description, doesn't exist anymore).

forgotten 2026-07-04T00:33:52.116359+00:00 — Incomplete-investigation snapshot (iter 4); angr-5aj8 later root-caused (u128 shift-overflow) and CLOSED/fixed, superseding these hypotheses/next-steps.

proxy-callstack-manage-required forgotten

RustCallStackProxyPlugin._manage() must exist (return None) as a no-op stub — angr.engines.successors.add_successor (~line 199) unconditionally calls state.callstack._manage() during SimProc successor add. If the proxy lacks _manage, every callback-driven explore (e.g. fauxware find=0x4006ed) AttributeErrors before any state reaches the found stash. The Rust interpreter drives the canonical call stack so a write-through _manage would diverge — keep it inert. Same logic: push/pop/call/ret should also return self / be no-ops since SimProcs don't manually push or pop frames in practice.

forgotten 2026-08-05T04:34:40Z — Already documented verbatim as a docstring on RustCallStackProxyPlugin._manage in angr/exploration/rust_state_proxy.py (verified) (b).

proxy-gate-audit-all-default-off remembered

Proxy-gate audit verdict (angr-sluuj.3, iter41 2026-06-22): ALL 8 RustExplorationManager proxy/fork gates are default-OFF in production — none is 'always-on in practice'. _resolve_env_flag (rust_manager.py) returns False when kwarg=None AND env unset; every init param defaults None/False; no production call site passes True (only nightly proxy_gates_on forces 4 clean gates, and run_single.py --use-shared-lineage-solver is canary-only). CONSEQUENCE: the sluuj epic's premise is INVERTED — the PROXY paths are the speculative default-OFF code, the FALLBACK paths (register snapshot, constraint diff-and-push, CallbackMemoryTracker) are production. Deleting fallbacks now would break the default config, so NO delete-fallback beads were filed. Real simplification (promote proxies, delete fallbacks) is gated on angr-4rq7 (register_proxy corrupts state.addr/PC) so all gates can promote together. Gap found: export_callstack/export_memory proxies lack a nightly gates-on soak (only _GATE_TOGGLES precedence tests) -> filed angr-5x6la. Cross-ref write-through-gate-promotion-keep-off-verdict.

proxy-gate-cache-pollution-pattern forgotten

Cache-pollution pattern under proxy gates: angr/exploration/rust_callback_dispatch.py:1301 and :1143 do self._state_cache[state_id] = state/succ_state after a SimProc callback. The state was created from cached_state.copy() at line 2045 and had its memory plugin REPLACED with RustMemoryProxy at line 2270 (via _install_callback_memory_proxy). The successor inherits the proxy via plugin copy() (RustMemoryProxy.copy returns another RustMemoryProxy). So the cached state persists with the proxy installed, and any subsequent code that reads state.memory (or state.registers if register-proxy gate is on, state.callstack if callstack-proxy gate is on) gets a proxy plugin pointing at Rust. This is fine during async callbacks (where mgr is unborrowed) but breaks synchronous Rust→Python callbacks invoked from inside run(). Don't 'fix' the cache pollution by restoring the original plugins — the proxy is what makes the gate's write-through semantics work during the NEXT SimProc callback. The right fix is at the consumer side: detect the proxy and bypass.

forgotten 2026-08-05T04:34:40Z — Implementation detail pinned to specific line numbers in rust_callback_dispatch.py (f); relocate as a comment near _install_callback_memory_proxy / the state-cache assignment.

proxy-gate-default-on-flip-pattern remembered

Default-on proxy gates use _resolve_env_flag(kwarg, ENV, default=True) so the env var becomes an opt-OUT (=0 forces off), and must be pulled OUT of the default-off _GATE_TOGGLES parametrized table in tests/engines/rust/test_proxy_gates.py (its TestProxyGateToggles.test_gate_default_off asserts default False for every row). Give the flipped gate dedicated default-on/=0-optout/kwarg-false-beats-env tests in its own Test*Gate class. First applied to _use_callback_memory_proxy (angr-grji4, 2026-07-15, commit 2d9bb6598). FLIP DISCIPLINE (angr-92e17): before defaulting a gate on, run the FULL tests/engines/rust suite under gate-on, not just benches -- that is how ryf6-style clobber hazards surface. Example (commit 5fafd7915): RustMemoryProxy.store rejected a bare-int value without size=, but angr's SimMemory.store infers a bare int's width from the arch word size (asprintf writes a malloc'd pointer back via memory.store(strp, dst, endness=memory_endness) with NO size) -> TypeError under the gate, diverging from the tracked-writes path. Fix defaulted size to self._arch.bytes in BOTH the concrete-store int branch AND _data_to_ast (symbolic-addr path), matching the in-file precedent (load path + RustRegisterProxy.store). The old test encoded the wrong contract (int always needs size) and was rewritten to test_memory_write_int_value_defaults_to_arch_word -- a failing gate-on test may be pinning the divergence itself.

proxy-int-store-arch-word-default forgotten

callback-memory-proxy gate (ANGR_RUST_USE_CALLBACK_MEMORY_PROXY=1) fidelity bug: RustMemoryProxy.store (rust_state_proxy.py) rejected a bare-int value without size=, but angr's SimMemory.store infers a bare int's width from the arch word size. asprintf (angr/procedures/libc/asprintf.py) writes a malloc'd pointer back via memory.store(strp, dst, endness=memory_endness) with NO size -> raised TypeError under the gate, diverging from the tracked-writes path. FIX (commit 5fafd7915, angr-92e17): default size to self._arch.bytes in BOTH the concrete-store int branch AND _data_to_ast (symbolic-addr path). Precedent already in-file: load path + RustRegisterProxy.store default size=arch.bytes. Test test_memory_write_int_value_requires_size encoded the OLD wrong contract (int always needs size) -> rewrote to test_memory_write_int_value_defaults_to_arch_word. Full tests/engines/rust green under gate-on (1093 passed, was 1 fail). Lesson for 92e17 default-on: run the FULL rust suite under the gate to surface these clobber hazards (ryf6-style), not just benches.

forgotten 2026-07-20T05:01:49.688689+00:00 — Fixed-commit receipt (5fafd7915, pinned by test_memory_write_int_value_defaults_to_arch_word); durable flip-discipline lesson folded into the gate-flip pattern canonical (merged into proxy-gate-default-on-flip-pattern)

proxy-memory-load-endianness forgotten

RustMemoryProxy.load() must default to Iend_BE, NOT arch.memory_endness. angr's standard memory.load() defaults to BE regardless of architecture. Using arch.memory_endness (Iend_LE on x86) byte-reverses loaded data, breaking callable predicates that compare memory contents (e.g. strcpy_find 'HAHAHAHA' becomes 'AHAHAHAH'). Fixed in commit 5af22cde6.

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

proxy-stale-addr-root-cause remembered

proxy-stale-addr-root-cause: When chasing a 'stale mid-run proxy value' bug in the Rust engine (mgr.active[i].addr, register reads), the intuitive story — 'the Python mirror lags the Rust pc by a block' — was WRONG. The real mechanism (angr-ibx8j, iter 76-77) was object ALIASING inside _state_cache, not temporal staleness: several ids shared one SimState, so the first sync bound the shared RustRegisterProxy to one id and the others inherited its pc. Diagnostic that settles it fast: compare sorted([hex(mgr._rust_mgr.get_state_pc_by_id(sid)) for sid in mgr._rust_mgr.get_state_ids('active')]) against sorted([hex(s.addr) for s in mgr.active]) — if the proxy list has DUPLICATES that Rust does not, it is aliasing; if the values are merely one block behind, it is staleness. Also: a pc of 0x0 on a blank_state(addr=main) harness is legitimate (ret from main into a zeroed return address), not a symptom. See [[invariant-state-cache-mirror-id]].

proxy-writes-design-verdict remembered

DESIGN VERDICT (angr-g7ug, 2026-05-22, commit 45c2c7fdf): Recommend Option C (status quo diff-and-push) for SimProcedure callback boundary. Measured per-callback sync-back cost via run_single.py --dump-counters: fauxware 13.9ms (24% of cb time), mma_howtouse 7.2ms (86%), unbreakable_0 7.7ms (4%), flareon10 2.4ms/cb (21%), csaw_wyvern 0.89ms/cb (1.3%). Aggregate sync_back is at most 7% wall on the fastest bench (fauxware) and <0.2% wall on heavy benches. Option A (proxy plugins on SimRegisters/SimMemory) recovers 100% of sync_back but introduces N+M PyO3 calls per callback plus wide plugin-contract risk surface. Option B (tracked-writes log) recovers ~50-70% of sync_back, lower risk, but savings don't move bench matrix. Reopen conditions: callback density grows >10K/bench (current max 30 in csaw_wyvern) OR angr-2k64 (per-state-id plugin restoration) revives needing proxy-write architecture as dependency OR mma/hackcon residual gaps are localized to diff-push (not the case today — 9maq points at state_create).

public-api-proxy-protocol-surface forgotten

test_rust_public_api drift for Rust{Register,Memory}Proxy is the SimMemory plugin protocol (STRONGREF_STATE, SUPPORTS_CONCRETE_LOAD, category, copy, init_state, merge, set_state, set_strongref_state, store, widen; +compare/find/permissions on memory) added under the write-through gate angr-qj30; RustExplorationManager drift is drop_copy/fork_state_for_copy CoW plumbing. All INTENTIONAL public surface -> add to angr/exploration/_public_api.py CLASS_PUBLIC_ATTRS, not rename-to-underscore. Precedent: get_state_globals_py/get_state_options_py plumbing already pinned. Fixed in 1d488466a (angr-ytu9).

forgotten 2026-08-05T04:34:40Z — Closed receipt naming a fix commit (1d488466a) (a).

pufm-current-state forgotten

Per angr-pufm audit (2026-05-06): the three originally-described gaps are largely closed. StoreG/CAS/Load all check has_memory_*_symbolic_full(). Remaining session-sized gaps (now closed by angr-8mh1, commit 0c90d4962): LoadG symbolic-addr Failed, Load expr Failed, fallback_to_python_store Failed. The bigger work remaining for pufm: lazy guarded entries in symbolic_objects+spans for >N solutions (so symbolic-store doesn't have to enumerate addresses) — this needs Z3 array/lambda theory and is multi-session. Recommend opening that as a separate child rather than reopening pufm.

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

pyo3-module-constants forgotten

PyO3 supports exporting raw constants via m.add("NAME", VALUE). E.g. m.add("PAGE_SIZE", crate::memory::PAGE_SIZE)? in #[pymodule] makes angr.rustylib.vex_engine.PAGE_SIZE importable. Used in 71st loop session to centralize PAGE_SIZE/PAGE_MASK across Rust↔Python boundary (angr-cr8y). Rust struct constants (BITMAP_WORDS) can guard derived array sizes via const _: () = assert!(BITMAP_WORDS * 64 == PAGE_SIZE as usize) — compile-time check, no runtime cost.

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

pyo3-pyclass-cycle-leak remembered

PyO3 #[pyclass] does NOT implement traverse/clear by default. If a pyclass holds Py refs that participate in a cycle through Python objects (common: bound methods captured in callback structs), the cycle is invisible to Python's cycle-GC and the entire cycle leaks permanently — even gc.collect() does NOT help. Diagnostic in angr: mma_howtouse leaked 1.3GB across 45 callable() invocations because RustExplorationManager.callbacks (PyO3 pyclass holding cloned PythonCallbacks with bound-method Py refs) and mgr._callbacks (the Python-visible PythonCallbacks pyclass) both held strong refs to bound methods on mgr, forming a cycle mgr -> callbacks -> bound method -> mgr. Fix: add fn traverse(&self, visit: PyVisit<'>) -> Result<(), PyTraverseError> { for opt in [&self.field1, ...] { if let Some(o) = opt { visit.call(o)?; } } Ok(()) } and fn clear(&mut self) { self.field1 = None; ... } to BOTH pyclasses. Imports: use pyo3::class::{PyTraverseError, PyVisit};. PyO3 0.27.2 syntax.

pyo3-pymethods-child-module-split remembered

PyO3 pyclass with a giant #[pymethods] block can be split file-wise WITHOUT the multiple-pymethods feature: the ONE #[pymethods] impl may live in a #[path] child module while the #[pyclass] struct stays in the parent. Pattern (angr-nbim4.1, exploration/manager_methods.rs): child starts with 'use super::*' to inherit the parent's imports, pub(crate) fields, and type aliases, then holds the verbatim '#[pymethods] impl RustExplorationManager { ... }'. multiple-pymethods is only needed for >1 block for the SAME type; a single block anywhere in the crate registers fine via PyO3 inventory. GOTCHA: 'super::X' paths inside the moved block shift meaning — in parent mod.rs 'super::state' meant crate::state (exploration is a crate-root child), but from the child module 'super' is exploration, so retarget 'super::state'->'crate::state' (or super::super::state). 'super::helpers' etc still resolve (child can access ancestor-private sibling modules). This drops the god-file cheaply (mod.rs 2790->458) but relocates rather than truly splits the block, since the single-block rule forbids peeling cohesive method groups into separate #[pymethods] blocks.

pyo3-pymethods-private-fn-still-bridged remembered

PyO3 #[pymethods] blocks pick up EVERY fn in the impl (including private 'fn foo' without pub). If you need a helper method that uses non-PyO3-bridgeable types (like Z3AstPtr, which owns a Z3 refcount and cannot be reconstructed from a Python value), move it OUT of #[pymethods] into a separate 'impl RustSolverContext { ... }' block. Symptom: error E0277 'Type cannot be used as a Python function argument' / 'ExtractPyClassWithClone is not implemented'. Pattern used at native/angr/src/solver.rs:1083+ for eval_z3_ast_ptr (consumes Z3AstPtr) — kept the public method 'eval' inside pymethods (it does extract_z3_ast_ptr internally) and the helper outside.

pyo3-trust-model-audit forgotten

PyO3 FFI trust model (angr-9l9j audit, 2026-06-01): the Rust engine assumes cooperative non-adversarial Python. State IDs go through find_state() → Option/PyValueError, never panic. Callbacks are Py (owning refcount, no dangle); cb.call1(py, ...)? propagates exceptions and type/arity errors. RustBVHandle is a pure value (id/width/concrete, no raw ptr). Address args reach SymbolicMemory (guest-VM addr, not host ptr). Stash names silently create new stashes on typo (footgun, not unsafe — filed angr-630x).

ONE unsafe Python-callable FFI surface: import_z3_constraint_ptrs(state_id, ptrs: Vec) at exploration/state_api.rs:102. Body calls unsafe { ctx.add_constraint_raw(*ptr) } if *ptr != 0. add_constraint_raw (symbolic/context.rs:1524) requires a valid Z3_ast Bool in active thread-local context — NonNull::new_unchecked + Ast::wrap = UB on any non-Z3 integer. Production caller (rust_manager.py:2876) always pairs with prior export_z3_constraint_ptrs so trust holds today. Filed angr-33t9 (P3) to harden via Z3_get_ast_kind sanity check or opaque newtype.

add_constraint_raw on RustSolverContext is NOT directly Python-callable; only reachable through add_constraint_ast which extracts via claripy's z3 backend (solver.rs:26 extract_z3_ast_ptr) — safe by construction.

Spike report in docs/advanced-topics/rust_engine.rst 'PyO3 API trust model' section.

forgotten 2026-06-04T21:58:02.079806+00:00 — Audit findings already promoted to docs/advanced-topics/rust_engine.rst 'PyO3 API trust model' section per body text

pyo3-unsendable-drop-content-independent remembered

pyo3 unsendable drop warning is content-INDEPENDENT (angr-87e56.1): pyo3-0.27.2 ThreadCheckerImpl::can_drop (src/impl_/pyclass.rs ~L1091) refuses the drop + write_unraisable purely on thread::current().id() != creation_thread — it never looks at the pyclass payload. So RustSolverContext::close() (dropping Box on the owner) frees the Z3 solver memory but the EMPTY shell STILL warns when GC'd off its creation thread. Consequence: any 'drain owned solvers before worker join' scheduler-side fix CANNOT silence the 'is unsendable, but is being dropped on another thread' warning — it only frees the payload. The residual warning is a PYTHON caching gap: RustSolverProxyPlugin._get_rust_ctx / RustSolverProxy._solver_ctx / RustStateExportManager._cached_rust_ctx cache a fork created ON a worker thread (Python callback under RUST_PARALLEL_WORKERS>1); the plugin/state outlives the worker, so tp_dealloc runs off-creation-thread. Only real fix: do NOT cache a fork created off the coordinator (main) thread — fork transiently + release in finally like RustPosixProxy._eval_stdin. Detect via threading.current_thread() is not threading.main_thread() (Rust workers = _DummyThread). Warning is leak-safe (unraisable, no crash/test-fail); downgraded P4.

pytest-pyo3-method-spy-pattern remembered

PyO3 methods on Python objects (e.g. RustExplorationManager._rust_mgr.register_simprocedures) cannot be monkey-patched in pytest — assignment raises AttributeError 'object attribute is read-only'. Workaround: wrap the entire _rust_mgr attribute with a Python proxy class that forwards via getattr and overrides just the methods you want to spy on. Then monkeypatch.setattr(mgr, '_rust_mgr', proxy). Pattern used in test_register_simprocedures_uses_display_name_for_stubs (tests/engines/rust/test_state_sync.py).

python-baseline-tests-audit forgotten

PYTHON_BASELINE_TESTS audit (2026-06-06, iter 503): added tests/sim/test_state.py (10 tests). Coverage gap identified — SimState merge (basic/static/3-way), pickle round-trip, with_condition/global-condition, arbitrary-interrupt handling in SimSuccessors, BYPASS_ERRORED_IRSTMT — not exercised by any of the previous 9 files. test_simsolver/test_symbolic cover eval+concretization but not merge; test_callable runs SimulationManager indirectly but not SimState lifecycle. Estimated ~25s CI cost (4 binary-free tests = ~1s locally; 2 explore-and-merge on state_merge_0 = ~5-10s each in CI). Rejected candidates: tests/sim/test_simulation_manager.py (7 multi-arch fauxware + CFGEmulated would risk 5-min budget), tests/state_plugins/inspect/test_inspect.py (Rust raises NotImplementedError so Python path matters but lower fundamental priority), tests/sim/test_state_customization.py (3 tests, low yield). Periodic re-audit cadence: every ~50 iters or when CI budget changes. Commit: c05cd8586.

forgotten 2026-08-05T04:34:40Z — Point-in-time coverage-gap audit / status snapshot (c).

python-bridge-dedup-shared-helpers forgotten

python-bridge dedup (angr-24pv4.4): shared solver-proxy primitives now live module-level in rust_state_proxy.py — _cast_eval_result, _with_extra_constraints(ctx, fn, *args, extra=(), **kwargs), and _SolutionCountMixin (eval_one/eval_exact/eval_atleast). Both RustSolverProxy and RustSolverProxyPlugin inherit _SolutionCountMixin and assign _cast_result = staticmethod(_cast_eval_result). rust_callback_dispatch.py imports _with_extra_constraints from rust_state_proxy (no circular import: rust_state_proxy only imports claripy + rustylib). GOTCHA: _with_extra_constraints widened to forward *kwargs so min/max can pass signed=. Env-gate boilerplate consolidated via resolve_env_flag(kwarg, env_var) in rust_manager.py (7 ANGR_RUST_USE gates). rust_callback_dispatch helpers: _resolve_next_pc(state, fallback_addr=None) for symbolic-IP eval_one->eval fallback (3 sites), _merge_tracked_writes/_filter_symbolic_addrs/_merge_symbolic_imports for both resume paths.

forgotten 2026-08-05T04:34:40Z — Closed refactor receipt with implementation-level module inventory (a/f).

python-class-reassignment-for-history-plugin remembered

Python class reassignment works cleanly for SimStateHistory: 'state.history.class = _RustOwnedSimStateHistory' promotes a single instance without affecting other states' history plugins. SimStateHistory has no slots (uses dict), and the subclass only adds property overrides plus a class-level _WARNED flag — no new instance state. Pattern enables per-state behavior overrides without monkey-patching the global class or wrapping the plugin with a delegating proxy. Caveat: pickle would need the subclass importable (not an issue here since RustExplorationManager states aren't pickled across processes in normal use).

python-init-prefix-find-replay forgotten

Init-prefix find/avoid under the Rust manager (angr-lyvf2 + angr-bdeqa). The Python init skip (_run_python_init_if_needed, rust_manager.py) fast-forwards an entry-point seed to main BEFORE Rust ever sees it, so the seed's own PC -- and every address in the init prefix -- is invisible to Rust's pre-step find_addrs/avoid_addrs checks (Rust's step_one and parallel_process_state both DO check find/avoid pre-step; the state just never arrives with those PCs). Debug tell: mgr.active[0].addr != proj.entry immediately after construction, with stats()['steps'] == 0. Seed-PC leg (angr-lyvf2): init parks (pre-init addr, un-advanced SimState, advanced sid) in _preinit_seeds and address-based explore() calls _route_preinit_seed_finds to push the seed into FOUND unstepped + drop_state_from_stash the advanced counterpart from active. Find addresses strictly INSIDE the prefix (__libc_csu_init/_init/frame_dummy/.init_array) match via rust_manager.py::_replay_preinit_prefix_for_find (angr-bdeqa). Key design points: (1) NO cache-version bump was needed -- the un-advanced seed SimState is already parked in _preinit_seeds by _phase_activate regardless of whether the init cache short-circuited stepping, so the prefix can simply be re-stepped on demand. (2) The replay is LAZY: it runs only after an address-based explore() ends with an empty found stash, so the common (find past main) case pays zero. (3) Two traps hit while implementing: assigning sm.active=[...] on a SimulationManager shadows the dynamic stash attribute with a frozen list (the manager keeps stepping stashes['active'] but reads never update) -- use sm.move(from_stash=...,to_stash=...,filter_func=...); and the angr-027h phase-2 eager retry re-runs _phase_activate mid-explore, which parked a SECOND copy of the seed and double-reported the find -- guarded with the existing _phase2_reseeding flag. (4) Residual divergence, documented in docs/advanced-topics/rust_engine.rst (init-prefix section) + pinned by TestFindInsideInitPrefix::test_avoid_inside_init_prefix_does_not_match: an avoid address only the init prefix executes does NOT kill the seed under Rust.

forgotten 2026-08-05T04:34:40Z — Substance verbatim duplicated in docs/advanced-topics/rust_engine.rst 'The Python init prefix and find / avoid' section, including the avoid-divergence caveat (verified) (b).

python-init-skip-hides-prefix-addrs forgotten

The Rust manager's Python init skip (_run_python_init_if_needed, rust_manager.py) fast-forwards an entry-point seed to main BEFORE Rust ever sees it, so the seed's own PC — and every address in the init prefix — is invisible to Rust's pre-step find_addrs/avoid_addrs checks. Rust's step_one and parallel_process_state both DO check find/avoid pre-step; the state just never arrives with those PCs. angr-lyvf2 fixed the seed-PC leg only: init parks (pre-init addr, un-advanced SimState, advanced sid) in _preinit_seeds and address-based explore() calls _route_preinit_seed_finds to push the seed into FOUND unstepped + drop_state_from_stash the advanced counterpart from active. STILL BROKEN: a find/avoid address strictly INSIDE the init prefix (e.g. inside __libc_csu_init) is unreachable under Rust — the init-cache short-circuit means the prefix's address trace is not even recorded. Debug tell: mgr.active[0].addr != proj.entry immediately after construction, with stats()['steps'] == 0.

forgotten 2026-07-20T05:01:45.566138+00:00 — Same init-prefix find/avoid cluster; its 'STILL BROKEN' claim is superseded by the bdeqa replay in python-init-prefix-find-replay; mechanism detail folded into canonical (merged into python-init-prefix-find-replay)

python-lazy-memory-is-page-level-not-address-level remembered

Python angr is NOT lazy on address resolution — common misconception. AddressConcretizationMixin (angr/storage/memory_mixins/address_concretization_mixin.py) uses the same Range->Any (read) / Range->Max (write) strategy chain that Rust's AddressConcretizer mirrors. The laziness is at the PAGE level: MultiValues (angr/storage/memory_mixins/paged_memory/pages/multi_values.py) stores a set of alternative values per byte, collapsed to ITE only at LOAD time. MultiwriteAnnotation is an opt-in annotation used by libc/strchr.py, libc/gets.py, libc/fgets.py to UPGRADE the write strategy from Single to Range — it does NOT itself make stores lazy. So the 'MultiwriteAnnotation pattern' phrasing in beads (czph/qh5u/pogf) is shorthand for the combined behavior: concretize to a range AND record alternatives per byte instead of folding into ITE.

python-native-procedure-api forgotten

register_python_procedure(name, num_args, no_return, callable) on RustExplorationManager (PyO3) lets Python attach native procedures. The callable receives list[int] of concrete args and returns Optional[int] (None=no value). Implementation: PythonNativeProcedure in native/angr/src/procedures/python_proc.rs wraps Py, extracts concrete u64 from RustBV args (symbolic args -> SymbolicArgument error, dispatcher falls back to Python's SimProcedure path), then Python::attach + cb.call1((args,)) and converts result to RustBV at arch_bits width. Hooked into the existing dispatcher at exploration/mod.rs:2598 — no dispatch-loop changes needed.

forgotten 2026-08-05T04:34:40Z — API-implementation note pinned to a specific dispatch line (f); relocate as a doc comment near register_python_procedure in native/angr/src/procedures/python_proc.rs.

python-syscall-integration-test-pattern forgotten

Python integration test pattern for native syscall handlers in tests/engines/test_rust_exploration.py: load_shellcode(b'\x0f\x05' + b'\x90'*N, arch='AMD64', load_address=0x1000), blank_state(addr=0x1000), set rax=<syscall_num> and the ABI args (rdi/rsi/rdx/r10/r8/r9), wrap in RustExplorationManager([state]) then call mgr.run(max_steps=1). Verify the native fast path took effect by asserting mgr._rust_mgr.stats()['syscall_python_fallback_count'] == 0. Pre-seed memory in all states via mgr._rust_mgr.active_states_map_memory(addr, bytes, perms_u8). DO NOT depend on post-state register values — max_steps=1 may carry execution past the syscall block, and the state may land in deadended after running through nops. The Python integration test exists to lock in the no-fallback contract; behavior verification belongs in the rust unit test.

forgotten 2026-07-03T23:13:47.047826+00:00 — stale-file-ref

python-vex-fallback forgotten

Python VEX fallback for unsupported Rust ops: CbExecutionError::Unsupported and NeedPythonFallback now return RunResult::NeedPythonVEX instead of RunResult::Error. This creates a PendingCallback with CallbackReason::PythonVEXFallback, dispatched to _handle_python_vex_fallback in rust_callback_dispatch.py. The handler creates a full SimState, steps it via proj.factory.successors(), and syncs register/memory/constraint changes back to Rust. Multiple successors (symbolic branches in fallback blocks) only sync the first successor — others are logged as warnings. Commit 9e5f4600f.

forgotten 2026-06-04T16:36:55.888605+00:00 — Implementation detail / closed-bead status note with no durable rule

python-wrapper-overhead forgotten

ARCHITECTURE: rust_manager.py is 5059 lines with 115 methods, 4 separate caches, and ~50 FFI boundary crossings per exploration step. Per-callback cost is 30-150ms (state creation + register sync + memory proxy + plugin restore). This is the main bottleneck for simple examples.

forgotten 2026-06-04T21:58:02.425067+00:00 — Bare LOC/method counts; out-of-date snapshot of architecture without durable insight

qh5u-soft-blocker forgotten

Phase 2 (angr-qh5u, commit bce90fef4) installed Multi-cell STORE path but gated OFF by default. With gate ON, sym-write 1.76s vs 1.58s gate-off (10% regression). Cause: load-time ITE rebuild — assemble_load_with_multi runs the right-fold per load over every Multi byte's alternatives, so read-heavy workloads pay the collapse cost repeatedly. Eager store_conditional_multiple builds the ITE once at store time and reuses the BV across loads. Fix path: per-load collapse cache (memo collapsed BV in MultiPayload, invalidate when alternatives append). Until that lands, the gate stays off and Phase 2 is plumbing-only. Soft blocker hit was foreseen in the design doc (rust_lazy_memory_design.rst Phase 2 acceptance criteria); the gate is the documented response.

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

range-seeded-bisection-asymmetry remembered

Bisection asymmetry for unsigned min/max in SymContext: seeding min with hi=smallest_known cuts iterations from log2(2^width) to log2(smallest_known+1) — ~50% reduction for pointer addresses. But seeding max with lo=largest_known yields ~no savings: the bisection range [largest_known, 2^64-1] still has log2 ≈ 64 iterations because most of the upper bitvector range is above true_max. Net win on the range path: ~25% fewer SAT calls. Workloads where this matters most: address-heavy benchmarks like google2016_unbreakable_1 (~17-23% faster post-fix). sym-write barely moves because most concretizations stay in the fast-enum (≤16 solutions) path and never call range() at all.

read-short-read-model forgotten

Native read(fd=0) stdin short-read modeling (angr-kf0uy, commit ba598033c): read.rs::read_stdin_symbolic returns a symbolic real_size BVS (read_realsize_N) constrained .ule(count) ONLY under state.has_option("SHORT_READS"); default path returns concrete count. KEY: unlike fgets (efvao), read has NO newline/NUL/buffer semantics — Python's storage/file.py SimPacket path (line ~539) just replaces the returned size with a symbol <= orig_size and still fills the full packet, so the lone change is the symbolic RETURN value (the fork source); the count symbolic stdin bytes filling buf are byte-identical to the default. Default-off path stays non-forking so fast-tier gate unaffected (20/20). This completes stdio read-side SHORT_READS modeling: fgets+fgetc+getchar+getc+read all done.

forgotten 2026-08-05T04:34:40Z — Closed feature-completion receipt (commit ba598033c, 'completes stdio read-side SHORT_READS modeling') (a/f).

ready-queue-fully-blocked-escalate-to-human forgotten

When the ralph clean-state ready queue is fully blocked on external decisions (no concrete code task), do NOT spin re-confirming 'blocked' each iteration (iters 74-78 did this 5x). Instead escalate the decision-blocked beads via 'bd label add <ids...> human' + a crisp 'bd comment' decision question, so they surface in 'bd human list' and the human can unclog 'bd ready'. iter79 flagged csyy9 (won't-fix vs loader-symbol infra, low ROI), xumwf (add paste dep y/n), 75mc (fuzzer-a vs simproc-b + cross-repo PR). Distinguish human-DECISION blocks (use bd human) from DATA/network/upstream blocks (ywu7/439q coverage baseline, 3gjm CI Docker, ig3o.2 upstream archinfo b3sc, kq43 crate-registry) which stay tracked-by-blocker, not human-flagged.

forgotten 2026-07-04T00:33:52.497539+00:00 — Iteration receipt (ralph iters 74-79, stale one-off bead ids); reusable kernel is a single sentence

real-binary-readiness-roadmap forgotten

Real-binary readiness roadmap = epic angr-11djq (15 beads, tiered for ralph). Goal: PRACTICAL on WELL-HARNESSED real binaries, NOT unbounded symex (both engines explode unharnessed; honesty contract angr-4n26m). Two failure modes: grub=deep-loop SEARCH (DFS: angr-xel4/angr-smxp), xmllint=glibc-init+HARNESSING (parser path via fuzzer). Tier 1 (ready): .1 expose uniqueness/search knobs to Python (native exists, unwired), .2 deep-loop harness recipe doc, .3 xmllint bounded find=getenv bench. Tier 2 gated behind .4 T2-MEASURE counter evidence: .5 syscalls/.6 symbolic-files/.7 fd-sync/.8 hot-libc/.9 format gaps. CHOSEN primary anti-explosion track for reachability: DIRECTED/GUIDED SEARCH = CFG-distance state prioritization, .14 spike -> .15 native priority-queue impl (sidesteps merge-fidelity minefield). DEFERRED/contingent (only if directed search insufficient + high reconvergence): auto-merging .10 (needs cost-guard), veritesting .11; also deferred TLS-generality .12, constraint-pruning .13.

forgotten 2026-08-05T04:34:40Z — Project roadmap/status snapshot for epic angr-11djq, superseded by the later 2026-07-18 housekeeping roadmap (c).

real-unmapped-vex-ops-list forgotten

Verified 2026-06-01 (angr-uprs): real VEX opcodes that route to IROp::Unmapped via execute_irsb_for_test's JSON parser AND surface as RustUnsupportedVexOpError include: (1) x87 transcendentals — Iop_SinF64/CosF64/TanF64/AtanF64/Yl2xF64/Yl2xp1F64/ScaleF64/2xm1F64/RecpExpF64/RecpExpF32 (these only get the libm fast path when arriving as IROp::Raw(code) via the FFI lifter — JSON path can't carry the numeric opcode), (2) NEON saturating shift-by-immediate Iop_QShlNsatS{U,S}{8,16,32,64}x{1,2,4,8,16} — note Iop_QShl8x8 / Iop_QSal8x8 etc. (vector-by-vector) ARE mapped to IROp::VQShlSat in angr-tukg.8, (3) FP/decimal conversions like Iop_F128toD32 / F128toI128S / RoundF32x4_R{M,N,P,Z} / SignificanceRound{D64,D128} / F32ToFixed32S* / Fixed32SToF32x*, (4) crypto+poly like Iop_PolynomialMulAdd{8x16,16x8,32x4,64x2} / Iop_CipherV128 / NCipherV128 / SHA256 / SHA512. The lone NeonUnimplemented entry is Iop_PwAdd32Fx2 (binop, arm64-only via Iex_Binop construction). Probe pattern: build single-Unop or single-Binop IRSB JSON with Ity_I64 args; dispatch hits IROp::Unmapped before any width/type validation.

forgotten 2026-08-05T04:34:40Z — Verbatim duplicated in docs/extending-angr/rust_vex_ops.rst, including the identical 'Verified 2026-06-01 (angr-uprs)' citation (b).

rebuild-with-libvex-ffi-recipe remembered

Building the .so WITH libvex-ffi in this hand-assembled venv (needed after angr-3trr7 flipped it default-ON): the pip path (make rebuild / pip install -e .) FAILS because setuptools tries to build unicornlib which needs libvex.h headers absent here; the cargo-only path (tools/rebuild-rust.sh --cargo-only / make rebuild-cargo) does NOT pass --features libvex-ffi. Manual recipe that works: export PATH with cargo; Z3_SYS_Z3_HEADER=/usr/include/z3.h (venv z3/include/z3.h does NOT exist here); Z3_LIBRARY_PATH_OVERRIDE=/lib/python3.12/site-packages/z3/lib; PYVEX_FFI_LIB_DIR=/.../pyvex/lib; then 'cargo build --manifest-path native/angr/Cargo.toml --release --features libvex-ffi' and cp target/release/librustylib.so angr/rustylib*.so. Verify: nm -D angr/rustylib*.so | grep vex_lift (>=1), readelf -d shows RUNPATH with pyvex/lib, and python -c 'import angr.rustylib as r; print(r.vex_engine.libvex_ffi_enabled())' -> True (the fn lives on the vex_engine SUBMODULE, not top-level rustylib).

reconvergence-collision-report-a32jl3 forgotten

angr-a32jl.3 Stage-1 reconvergence collision-frequency report (measure-first gate for state-merging angr-11djq.10). Source: stats_api.rs reconvergence_rate = reconvergence_collision_states/reconvergence_active_observed, sampled per active-stash observation in run_loop.rs::record_reconvergence_sample (a 'collision' = 2+ states sharing (pc, callstack-return-chain)). Measured via run_single.py --counters-json, rust engine, 2026-07-04. RESULTS (rate / colliding / observed / maxgroup): CADET_00001_partial 62.3% 38/61 grp6 (n tiny); csgames2018 18.8% 234/1245 grp2; ais3_crackme 17.8% 46/258 grp2; defcon2016quals_baby-re 0% 0/66; fauxware 0% 0/7; defcamp_r100 0% 0/2; csaw_wyvern ~0% 0/2; cmu_binary_bomb_partial/google2016_unbreakable_0/flareon2015_2/sokohashv2 0 obs (no forking on partial run). mma_howtouse + defcamp_r100__dfs not measurable (mma solve.py bypasses --counters-json path; __dfs is a param variant, no example dir). VERDICT: reconvergence is highly bench-dependent. It exceeds the ~5% Stage-2 threshold ONLY on data-dependent branchy CTF binaries (csgames2018, ais3_crackme) + the CGC CADET partial (tiny n). Every >5% hit is exactly the 'data-dependent CTF' class the a32jl.3/11djq.15 gate EXCLUDES; straight-line/quick-solve benches are ~0%. Stage-2 precondition (>5% on a REAL non-CTF target = xmllint/grub) is UNMET with available corpus (xmllint blocked on 75mc human-gated; grub OOMs). Implication for 11djq.10 state-merging: potential payoff exists on branchy data-dependent search but real-target evidence is missing; do not start merging work until a real target clears >5%.

forgotten 2026-08-05T04:34:40Z — Closed measure-first research receipt gating state-merging work that the later roadmap marks as engine-work-done/deferred (c).

refactor-memory-sweep-rule remembered

Refactor-time memory-hygiene rules so renames stop rotting bd citations. (1) RENAME-SWEEP: any commit that renames/moves a file or public symbol must 'bd memories ' (keyword search has recall gaps, so also grep a full 'bd memories' dump) and repair stale citations in the SAME commit, per bd-memory-citation-repair-pattern. (2) SYMBOL-ANCHOR: when authoring a memory, anchor code refs to symbol names (fn/struct/method) not raw line numbers — 2026-06-11 audit found 11/24 line refs drifted 10-600 lines (e.g. invariant-error-class-taxonomy rust_manager.py:269 vs actual 662; invariant-fork-needs-python-init state.rs ~1496 vs 2092) while every symbol anchor resolved. Codified in CLAUDE.md 'Keeping memories from rotting' subsection + .ralph/prompts/_footer.md (step 10 + Rules rename-sweep bullet). Sibling: docs-dead-memory-key-sweep-method (post-prune dead-key sweep). Source: angr-7tvm, state-review-2026-06.

reference-rustc-hash-dep forgotten

When Cargo.toml's only direct deps are std HashMap/HashSet but rustc-hash is needed, add 'rustc-hash = "2.1"' to native/angr/Cargo.toml. It is already transitively present via z3-sys/syn so the lockfile only adds a single line and there is no extra build cost. Use rustc_hash::{FxHashMap, FxHashSet}; both have ::default() constructors and the same API as std HashMap/HashSet.

forgotten 2026-06-04T21:58:02.771403+00:00 — Trivial Cargo.toml advice discoverable in <1min by any contributor

reg-proxy-overlap-invalidation forgotten

RustRegisterProxy.setattr must invalidate cached aliases whose (offset, size) overlaps the written register. Bug pattern: SimRegArg.set_value (and any other partial-register write) issues two stores — first clears the full 64-bit reg, then writes the 32-bit sub-reg. Without overlap invalidation, the first write caches rax=BVV(0); the second caches eax=symbolic but leaves rax stale. A later state.regs.rax read returns BVV(0) and the symbolic SimProcedure return value never reaches the solver. Fix uses a per-arch overlap map computed from arch.registers; module-level cache keyed by arch.name. Closed angr-yxar; commit 1a5b2243b.

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

register-proxy-gate-empty-state-ids forgotten

angr-4rq7 root cause #2: under the register-proxy write-through gate (ANGR_RUST_USE_CALLBACK_REGISTER_PROXY=1), mgr.found_proxies()/active_proxies() enumerate Rust state-ids that point to GENUINELY-EMPTY states (self.pc==0 AND IP register==0), while the full-export path produces the real successors with valid addrs. So proxy.addr=0x0 is a STATE-ID ENUMERATION DIVERGENCE, not a pc-vs-rip stale read. Suspected mechanism: with simproc_fork gate off, _add_forked_state->_add_rust_state reads succ_state.regs.* through the RegisterProxy that is bound to the PARENT callback_state_id (the state.copy() shares the parent-bound proxy), so the pushed regs come from the parent and Rust mints an id whose successor IP was never written. Likely fix: rebind proxy per-successor OR route _add_forked_state through the simproc_fork-via-rust path. Commit 12aad9ce6 fixed the simpler half (get_state_pc_by_id prefers concrete IP when pc==0).

forgotten 2026-07-04T00:33:52.877183+00:00 — Superseded hypothesis; angr-4rq7 CLOSED with stale-cache as final root cause, which refutes this suspected mechanism

register-proxy-stale-cache-root-cause remembered

angr-4rq7 root cause (FINAL): the register-proxy gate's PC/state.addr corruption was NOT a Rust-side pc-vs-rip divergence (Rust states are fully self-consistent: pc==rip for every id, gate-on). It was a STALE PYTHON-SIDE PROXY READ CACHE. RustRegisterProxy.getattr caches per-name register reads in self._cache and NEVER invalidates on a Rust step. A materialized SimState that inherits a callback RustRegisterProxy (the live frame cached in _state_cache, or a .copy() of a parent-root state — RustRegisterProxy.copy() makes a fresh proxy but the SAME-OBJECT cached-state path keeps the populated cache) read regs.ip from that stale cache, so state.addr returned the callback-time value while the X_proxies() accessor read the live get_state_pc_by_id. FIX (commit 7af4c6617): _sync_rust_registers_to_state, when state.registers is a RustRegisterProxy, now (1) rebinds _state_id to the id being materialized and (2) clears _cache, so reads route live. It still does NOT setattr the concrete snapshot (would clobber symbolic regs the SimProc wrote). Regression: test_full_addr_matches_rust_pc_under_register_proxy. The materialized full state's addr reads via regs.ip -> proxy.load('ip') -> getattr, which DOES route through the proxy (load/store are implemented) — the bug was purely the cache.

register-snapshot-optimization forgotten

Register snapshot for extern SimProcedure callbacks: _snapshot_registers() captures reg values as dict instead of state.copy(). 2x faster for ais3 (57ms vs 118ms for 118 callbacks). Only used when: NOT UserHook, NOT in _memory_writing_procs, IS zero-length hook. Snapshot dict format: {reg_name: (is_symbolic, concrete_val_or_None, offset, size)}. _extract_register_changes accepts either SimState or snapshot dict.

forgotten 2026-06-04T21:58:03.120115+00:00 — Iteration receipt with old measurements; behavior is in code

register-sync-bottleneck forgotten

_sync_registers_to_rust's symbolic register import path (z3_backend.convert + as_ast().value) is NOT a benchmark hot path. Empirical hit/miss for a (hash,length)→ast_ptr cache: fauxware 0/0, ais3_crackme 0/2, csaw_wyvern 0/0, flareon2015_5 0/8, securityfest_fairlight 0/2, sym-write 0/2, strcpy_find 0/2. Reason: SimProcedure callbacks operate ENTIRELY inside Rust state. Register sync from Python only happens during initial state seeding (or disk cache restoration), so per-benchmark there are at most a handful of unique symbolic register conversions, never repeated. Bead angr-nwbx's premise (50µs × 16 regs × N callbacks) was wrong; the actual cost is bounded by the count of distinct seeded states. Keep this in mind before optimizing 'per-callback FFI overhead' in fauxware — the win is in SimProcedure execution itself (open: 59ms, strcmp: 21ms, read: 16.6ms × 4 from rust_profiling), not register sync (~3ms total).

forgotten 2026-08-05T04:34:40Z — Superseded by the broader campaign-closing conclusion in perf-campaign-p5-deferral-decision; specific hit/miss numbers tied to one bead (nwbx) that will drift (d/f).

regression-flakes-defcamp_r100 forgotten

Pre-existing regression-suite flakes on this 8GB box (verified 2026-05-17 against baseline aef3e6ac0): defcamp_r100 occasionally 16-20% over baseline (0.23s); unmapped_analysis occasionally 15-18% over baseline (0.79s). Reproducible BEFORE any working-tree changes — pure ambient noise from Z3 model nondeterminism + system load. When a regression run fails ONLY on these two benches and a new bench's own timing is within threshold, the new change is OK.

forgotten 2026-08-05T04:34:40Z — Tied to one box and a stale baseline commit hash (aef3e6ac0, 2026-05-17); superseded by the broader, more current pr-bench-gate-jitter-risk finding (c/e).

rejected-optimizations-apr12 remembered

REJECTED optimization proposals (adversarial review 2026-04-12): (1) Batch symbolic memory import — only 1-5 imports per run, <1ms. (2) Batch stash queries 3→1 — 15µs/pass, negligible. (3) Native symbolic region scanning — already optimized with fast paths. (4) Constraint list slicing — constraints not append-only. (5) Native LoopSeer counting — unused in all benchmarks. (6) Auto-prefetch regs for predicates — already implemented at rust_techniques.py:218-222. Do NOT re-propose these.

replace-all-too-narrow-neon-comments forgotten

PITFALL (2026-05-25, angr-tkbr.3 iter 7): Edit's replace_all matches EXACT string only. When converting 4 'IROp::NeonUnimplemented(name) => panic!(...)' sites in vex/ops.rs, three sites had the comment 'NEON scaffolding: fail loudly rather than silently fall back.' but the fourth (unop, line 535) had the longer 'NEON scaffolding: fail loudly rather than silently fall back to / a fresh-symbolic result. Implementations land in angr-bkcs.2.' My replace_all snippet only matched 3 of the 4. The remaining panic site was caught only when running an end-to-end test that triggered a PanicException. LESSON: when using replace_all with surrounding-context strings, FIRST run a grep to enumerate match candidates and verify count expected vs. found. Better still: use the narrowest unique anchor (just the panic line) per site, or do explicit per-site Edit calls.

forgotten 2026-06-04T21:58:03.464287+00:00 — Iteration-specific tooling pitfall; general lesson is trivial (verify replace_all matches)

residual-neon-placeholders forgotten

Residual NEON placeholders after angr-tukg campaign (2026-06-01): Iop_PwAdd32Fx2 (the last entry in parse_neon_unimplemented at native/angr/src/vex/opcode_map.rs:945) and Iop_QShlN* (saturating shift-by-immediate, not yet parsed). Documented in the new docs/extending-angr/rust_vex_ops.rst 'Unsupported op coverage matrix'. Promote each to a standalone bead only when a benchmark drives a symbolic path through them.

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

reverse-leaf-emission-root-cause forgotten

Reverse(x) leaf-case Z3 emission in build_z3_ast (value.rs:2099-2113) and build_z3_ast_cached (value.rs:2350-2364) had a latent semantic bug: a stray parts.reverse() before the result.concat(part) accumulator produced x itself instead of byte-reversed x. Z3's bv_rewriter folded the mis-shaped Concat back to x as a single Extract — masking the bug. Most call sites hit Rule 3 canonicalization (extract_into: Extract(Reverse(x)) → byte-reindexed Extract on x) at value.rs:1568 or the Concat-distribution branch before reaching the leaf case, so 389/389 integration tests stayed green even with the bug present. Three round-trip unit tests (test_reverse_z3_emission_{16,32,64}bit_leaf in value.rs:3306-3340) catch this kind of bug — they pin x to a concrete value and assert that ctx.eval(Reverse(x)) returns the byte-reversed value. Fix: drop parts.reverse() in both branches (commit bbdb2451e, 2026-05-19).

forgotten 2026-06-04T21:58:03.807494+00:00 — Fixed-bug receipt with commit hash bbdb2451e; covered by unit tests in code

rlib-hides-dead-code-from-rustc remembered

Why rustc's dead_code lint was blind in native/angr (angr-9ke6b.214, 2026-07-31): [lib] crate-type = ["cdylib", "rlib"] plus 23 'pub mod' declarations in lib.rs made every item externally-consumable, so dead_code reported 0 hits crate-wide while 468 unreachable_pub sites existed. Dropping rlib is impossible (benches/, examples/, tests/ and the fuzz/ cargo-fuzz project link it). The lever that works: keep rlib, but demote to 'pub(crate) mod' every top-level module no external target imports. Re-derive the keep-list with: grep -rhoE 'rustylib::[a-z_]+' native/angr/tests native/angr/benches native/angr/examples native/angr/fuzz | sort -u -- currently automaton, concretize, fuzz_api, memory, stash, state, symbolic, vex. After demoting, 'cargo clippy --fix --lib -- -W unreachable_pub' narrows the module-level items, and only THEN does dead_code fire. CRITICAL SECOND STEP: unreachable_pub does NOT lint methods inside inherent impl blocks, so 'pub fn' in 'impl Foo' stays invisible even in a pub(crate) module -- those 77 sites had to be narrowed by script, and doing so surfaced 11 MORE dead items (SegmentList::len/get_segment, PythonCallbacks::call_on_hook/call_on_syscall/call_get_register/call_put_register/get_inspect_enabled_for_debug, RustSolverContext::sym_context/from_sym_context_with_symbols). Exclude #[pymethods] impls from that pass -- PyO3 trampolines keep those live anyway. Other rustfix blind spots: items declared inside macro_rules! bodies (procedures/macros.rs, syscalls/mod.rs stub_syscall!, syscalls/identity.rs constant_syscall!, interpreter/mod.rs define_execution_stats!) must be hand-edited; narrowing procedures::format_common::{parse_width_digits,parse_length_modifier,LengthModifier} breaks --features fuzzing with E0364/E0446 (they stay 'pub' with #[cfg_attr(not(feature="fuzzing"), allow(unreachable_pub))]). ALWAYS re-check dead_code under --features libvex-ffi before deleting: PythonCallbacks::call_inspect_vex_lift is dead under default features and live under libvex-ffi (which setup.py turns on). lib.rs now carries #![warn(unreachable_pub)] to keep the hole closed. Side effect to expect: clippy style lints that skip exported items (upper_case_acronyms on arch::arm::ARM / calling_conventions::ARMEABI) start firing once a module goes pub(crate).

roadmap-2026-07-18-housekeeping forgotten

Roadmap housekeeping 2026-07-18 (interactive session, plan we-need-to-do-ancient-pony.md, peer-reviewed): tracker taken to 0 open / 0 ready / 0 in_progress; 1733 closed; 47 status-deferred; 3 status-blocked (angr-439q, angr-b3sc, angr-ig3o.2 -- note bd status 'Blocked: 18' is the dep-aware metric, not status). VERDICT: engine work is DONE. Closed as acceptance-met: op0dn.10 (M2 determinism 7/7), op0dn.13 (M5 parallel impl 17/17), gorvf (zero-Python, milestone hit), ovqja (z3 hotpath). Everything else deferred with event-shaped UNDEFER CONDITION notes (no --until dates; use bd defer not bd update --defer). THREE CHOKE POINTS gate all remaining work: (1) angr-75mc xmllint -- human files the staged angr-examples PR in tools/upstream_patches/; unblocks S6/M3 (op0dn.5), S7/M5 (op0dn.7), 6d3l, sbvcx, 4n26m.12 -- highest-leverage single action. (2) angr-udo91 env restore (x86 pypcode sleigh + ../binaries) -- unblocks the whole M4 concolic line (op0dn.6/.12.*); network IS available in interactive sessions, recipe on the bead. (3) angr-4n26m.11 blog draft -- parked, awaiting user review of docs/blog/rust_symex_showcase.md. Chores gh-auth-gated: ywu7/439q coverage, 3gjm wheels CI, b3sc archinfo PR. Ralph loop: nothing to drain; ready is intentionally empty so MaxNoopIters self-terminates. PORTFOLIO CONTEXT (absorbed in the R4 merge; blueprint doc /home/ubuntu/.claude/plans/we-have-started-thinking-smooth-valley.md): the program was 6 spike-gated moonshots + 2 enablers under epic angr-op0dn. Durable framing that outlives the closures: the engine is no longer the bottleneck anywhere -- attack the Z3 check()/eval floor and how OFTEN it is called, not the interpreter; GO gates ranked on structural counts (z3_check_count/saved-check/callback counts), NOT wall-time; no CV<0.1 determinism gate (Z3 restart-heuristic floor is permanent); resume is search-continuity, not byte-identity; checkpoint/resume + LibAFL fuzzer + icicle already ship in-tree -- do not re-scope them greenfield.

forgotten 2026-08-05T04:34:40Z — Point-in-time project status snapshot (tracker counts, bead closures) (c).

rst-doc-citation-pattern forgotten

RST docs in this repo: when adding sections with code references, double-backtick everything (RST inline literal). Default-role single backticks render as :title-reference: under Sphinx, which is visually subtle drift. The errors.rs / engine.rs line citation pattern used in rust_engine.rst's 'User-facing error taxonomy' section is the model: cite a stable file:line range so future variant additions surface as visible doc drift. Pre-existing test_class_public_attrs_match_inventory[RustSimulationManagerProxy] failure (step/step_state/successors added but not in _public_api.py inventory) is independent of doc PRs — confirmed by stash-and-rerun.

forgotten 2026-06-04T21:58:04.163621+00:00 — Trivial RST style hint discoverable from existing docs

rst-heading-validation-no-sphinx forgotten

rST heading underline validation (no sphinx in venv): use .venv/bin/python3 to scan lines with len(set(line))==1 and line[0] in '=-~^"'' then assert len(prev) == len(cur). Found 3 mismatches in angr-gcfa first draft: top-level title with ↔ (U+2194, 1 codepoint, len() matches visual width), and two ~ underlines off-by-one. Sphinx warnings: underline too short → ERROR, underline too long → OK rendered. Fix all to exact-match for consistency. Memory invariant-rst-heading-underline-em-dash also applies but is about em-dash (—) which is also 1 codepoint per len() — no special multibyte handling needed beyond Python len().

forgotten 2026-06-05T17:23:04.914185+00:00 — Narrative anecdote about angr-gcfa first draft; mostly subsumed by invariant-rst-heading-underline-em-dash (the actual rule). Recipe (python len() scan) is trivial enough not to memorialize.

ruff-install-recipe remembered

ruff/pre-commit are NOT preinstalled here. The .venv pip is broken (vendored resolvelib ImportError), so 'python3 -m pip install' via the venv fails — use /usr/bin/python3 -m pip (system, separate), uv if available, or extract the tool's self-contained binary straight from its PyPI wheel. ruff ships as one binary in its wheel; pin 0.15.15 to match .pre-commit-config.yaml: URL=$(python3 -c "import json,urllib.request;d=json.load(urllib.request.urlopen('https://pypi.org/pypi/ruff/0.15.15/json'));print(next(f['url'] for f in d['urls'] if f['filename'].endswith('.whl') and 'manylinux' in f['filename'] and 'x86_64' in f['filename'] and 'i686' not in f['filename']))"); curl -fsSL "$URL" -o /tmp/r.whl; (cd /tmp && unzip -oq r.whl); install -m755 $(find /tmp -name ruff -type f|head -1) ~/.cargo/bin/ruff. (Do NOT record network/proxy reachability here — it is session-mutable.)

run-n-ignored-root-cause forgotten

RustExplorationManager.run(n=N) historically delegated to explore() whenever step_func was None, and explore() drops n into **kwargs and IGNORES it, running to completion. This violated SimulationManager.run(n=N) semantics (=step N times). For scripts doing sm.run(n=4); sm.step(...); sm.active[0] (ekopartyctf2015_rev100; asisctffinals2015_license uses the find/avoid explore path instead) the over-run drove the lone state into a deadend that drop_terminal_states discarded, emptying every stash -> 'IndexError: list index out of range' on active[0]/found[0]. Fix (commit ca6a48637): in run(), bind to N steps via the incremental step loop whenever n OR step_func is given; only n is None AND step_func is None delegates to explore(). Keep drop_terminal_states(False) across the bounded run so deadended states land in their stash like Python. Repro lives in /tmp/rev100_repro.py: rust run(n=4) gave all-stashes-empty, python kept active=1.

forgotten 2026-08-05T04:34:40Z — Closed bug-fix receipt (commit ca6a48637) (a).

run-single-monkeypatch-callable-guard forgotten

run_single.py's engine-swap monkeypatch (patched_simulation_manager in _run_in_child) intercepts proj.factory.simulation_manager and routes it to RustExplorationManager. Its caller-frame guard skips angr-internal callers so they use the Python engine. The guard MUST include /angr/callable.py: static-glibc binaries (e.g. busybox) carry IFUNC/IRELATIVE relocations that angr resolves AT LOAD TIME by executing the resolver through angr.callable.Callable -> perform_call -> simulation_manager. Those internal resolver states carry default SimOptions incl. SYMBOL_FILL_UNCONSTRAINED_REGISTERS, which the Rust manager hard-rejects in _check_raise_options (_RAISE_OPTION_NAMES) -> the project fails to even load (0.08s FAIL, before any Filling/load logs; child stderr is suppressed by run_single so it looks mysterious). Guard now: analyses/ OR exploration_techniques/ OR callable.py. Fixed in commit b9b56bcef (angr-4n26m.1).

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

run-single-spawn-tree-sampling forgotten

When wrapping tests/benchmarks/run_single.py in a process-tree sampler (e.g. RSS profiling), the angr worker is a GRANDCHILD of the Popen target — run_single uses multiprocessing.spawn (ctx='spawn'). Naive sampling of just the Popen pid underreports VmRSS by ~10x (orchestrator stays small while grandchild does all angr work, ~200 MB). Fix: walk /proc//task//children recursively for each tracked root. See _descendants() in tests/benchmarks/characterization/fleet_resource_profile/run_fleet.py.

forgotten 2026-08-05T04:34:40Z — Implementation-level technique pinned to one script (f); relocate as a comment near _descendants() in tests/benchmarks/characterization/fleet_resource_profile/run_fleet.py.

run1-per-step-overhead forgotten

BOTTLENECK: When until predicate or techniques active, Python calls rust_mgr.run(1) per step instead of batched run(). This forces FFI round-trip per VEX block. Line 4492-4493 in rust_manager.py.

forgotten 2026-06-04T21:58:04.509787+00:00 — Bare line citation that drifts; investigation hint without durable insight

rustbv-api-and-syscall-test-gotchas remembered

RustBV API gotcha: the bit-width accessor is .width() on the enum dispatcher, NOT .bits() — .bits() is a method on the inner Concrete/Symbolic variant fields only. Cargo will fail with E0599 if you call .bits() on a RustBV value directly. For Python parity tests in tests/engines/rust/, mgr.run(max_steps=1) does NOT leave a state in mgr.active after a syscall — the state may lift-error on the next block (PC=0 in synthetic shellcode tests). The reliable cross-the-FFI assertion is stats['syscall_python_fallback_count'] == 0; symbolic-return verification belongs in cargo unit tests, not Python tests.

rustbv-binop-clone-not-warranted forgotten

value_ops.rs by-ref binop wrappers (add/sub/...) forwarding to _into cost NOTHING extra vs _into for reference-holding callers: microbench rustbv_clone (benches/vex_engine.rs, angr-dva9j.4) showed add_byref_leaf==add_into_owned_leaf (84.7 vs 84.6ns). The clone-both-operands pattern is only avoidable when a caller ALREADY OWNS the operand and can move it (VEXOps::binop path, already using _into). RustBV::clone cost over a ~48ns criterion harness floor: Expression clone +3ns (Arc bump + empty-memo RefCell, unmeasurable), Symbolic leaf clone +8ns (Arc + Z3 BV refcount bump). The operand must be owned to enter the result node's Arc<[RustBV]>, so the leaf clone is unavoidable node-construction cost. Conclusion: adding more &self ref-taking op variants is NOT warranted.

forgotten 2026-08-05T04:34:40Z — Benchmark data supporting the into/byref pairing rule; folded into the canonical design-rule memory. (merged into rustbv-into-variants)

rustbv-into-variants remembered

RustBV (ops live in value_ops.rs since the angr-7hwz.2 split) exposes paired ops: add(&self,&other,ctx) and add_into(self,other,ctx). The &self form is a thin wrapper that clones then delegates. Hot paths owning operands (VEXOps::binop/unop, vec_binop, vec_mul_lo, vec_cmp, vec_interleave_, vec__n, float_neg/abs, widening_mul) use _into to skip clones. Other callers (ccall.rs, interpreter, tests) keep &self form. When adding new BV ops, pair them: _into is canonical, &self wraps it. Benchmark justification (rustbv_clone, benches/vex_engine.rs, angr-dva9j.4): the &self wrapper costs NOTHING extra vs _into for reference-holding callers (add_byref_leaf==add_into_owned_leaf, 84.7 vs 84.6ns) — the clone-both-operands pattern is only avoidable when a caller ALREADY OWNS the operand and can move it (the VEXOps::binop path, already using _into). RustBV::clone cost over a ~48ns criterion harness floor: Expression clone +3ns (Arc bump + empty-memo RefCell, unmeasurable), Symbolic leaf clone +8ns (Arc + Z3 BV refcount bump). The operand must be owned to enter the result node's Arc<[RustBV]>, so the leaf clone is unavoidable node-construction cost. Conclusion: adding more &self ref-taking op variants beyond the existing pair is NOT warranted.

rustbv-microbench-vs-realworld remembered

When auditing Arc-allocation hot spots in symbolic execution, microbench-only data can mislead. The rustbv_symbolic criterion bench showed 36% drop_in_place + Arc::drop_slow overhead, which sounds catastrophic — but real-world workloads are Z3-bound (fairlight Z3 ~95%, interpreter expr_eval ~14ms/11k calls = <<1%). The Arc<[Arc]>→Arc<[RustBV]> change yielded ~30% on the microbench but zero measurable change on real benchmarks (fauxware unchanged). Investigation tasks should weigh microbench wins against where the bottleneck actually is in production traces before deciding worth-the-effort.

rustbv-symbolic-id-not-in-debug remembered

When writing cargo unit tests that need to assert two successive symbolic-syscall calls minted DISTINCT fresh symbols, do NOT compare format!("{:?}", ret) strings — RustBV::Symbolic's Debug impl prints Symbolic(name, width) and OMITS the internal id field, so two distinct symbols with the same name+width format identically. Pattern-match into RustBV::Symbolic { id, .. } and assert_ne! on the id field instead. See memory_extras.rs and signals.rs tests for the canonical pattern.

rustbv-to-claripy-memoization forgotten

rustbv_to_claripy memoizes recursive conversions by RustBV pointer identity (HashMap<*const RustBV, Py>). RustBV::Expression operands stored inline inside a shared Arc<[RustBV]> have stable per-call addresses, so DAG-shared subtrees are only converted once. Without this, sym-write's 25-unique-subtree DAG fans into a 142k-node tree taking 2.7s; with it, ~tens of ms. Implementation: native/angr/src/claripy_bridge.rs split into rustbv_to_claripy (public, allocates fresh memo HashMap) and rustbv_to_claripy_memo (recursive, takes &mut memo). Memoization is gated on Expression variant only — Concrete/Symbolic/Constrained skip the cache. Commit c88947e65.

forgotten 2026-06-04T16:36:56.233893+00:00 — Implementation detail / closed-bead status note with no durable rule

rustfmt-ci-fix-scope forgotten

When fixing CI rustfmt drift, the actual scope is usually larger than what bug reports cite. CI uses 'cargo fmt --all -- --check' so it flags ALL files; reports may only mention one. Always run cargo fmt --all locally first to see the true scope, then fix everything in one mechanical commit (no rebuild needed — formatting doesn't change compiled output).

forgotten 2026-06-04T21:58:04.852503+00:00 — Trivial CI operational hint; would be discovered immediately by running cargo fmt --all

rustfmt-whole-workspace-hazard forgotten

rustfmt-on-whole-workspace hazard, re-confirmed iter-50 angr-xgef: running 'cargo fmt' (no path arg) reformats 50+ files of unrelated drift (mostly use-statement reordering and trivial chained-call-collapse). This makes review impossible and risks merge conflicts with other branches. AFTER 'cargo clippy --fix' produces machine-applicable edits: (1) immediately 'git stash' if cargo fmt was run by accident, (2) prefer applying rustfmt with explicit file args: 'cargo fmt -- <changed_files>' or 'cd native/angr && rustfmt --edition 2024 src/foo.rs src/bar.rs'. The clippy autofix output is USUALLY rustfmt-clean already; only rerun rustfmt on the changed files if the autofix produced a long line (e.g. 117+ chars from single_match collapsing) or odd indentation. Workflow that worked: 'cargo clippy --fix -- -A clippy::all -W ' → 'git diff --stat' → if any non-clippy file is touched (rustfmt drift), 'git diff --name-only | grep -v <clippy_files>' then 'git checkout HEAD -- <those_files>'. Worked cleanly for the 7-file batch in angr-xgef (single_match + manual_*).

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

rustsimstate-field-buckets forgotten

RustSimState field-bucket map (angr-x04s, state.rs:689): 30 fields grouped as (A) trivially serde — pc, state_id, parent_id, history, detailed_history, max_history, heap_brk, posix_brk, mmap_base, stdin_symbols, call_stack, heap_metadata, no_ip_concretization, no_symbolic_jump_resolution, keep_ip_symbolic, vex_arch, fs, inspection counts/bitmask; (B) concrete-with-symbolic-overlay — RegisterFile (Vec+FxHashMap<u32,RustBV>), SymbolicMemory (OrdMap<u64,MemoryPage>+symbolic_objects/multi_objects/lazy_regions/pending_writes); (C) Arc-shared collapse-on-snapshot — hooks: Arc<HashSet>, environment: Arc<HashMap<Vec,Vec>>; (D) cross-FFI Py handles — symbolic_pages, hook_symbolic_memory, addr_to_ast (each addr->claripy AST, needs Python claripy pickle); (E) Rc solver — SymContext with Vec constraints + ScopePath, all RustBV-tree-serializable. Box rebuilds from arch_name string. Useful map when extending RustSimState — anything new must declare a bucket.

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

ryf6-root-cause-proxy-replay-clobber forgotten

angr-ryf6 (closed, commit 2480b2aca) root cause + durable lesson: under ANGR_RUST_USE_CALLBACK_MEMORY_PROXY=1, _replay_rust_dirty_pages (rust_state_sync.py:1787) and _install_rust_memory_proxy (rust_state_sync.py:1726) issued state.memory.store(...) to copy Rust dirty pages back. SAFE under legacy cached-state model where state.memory is a real SimMemory. UNSAFE under callback-memory-proxy gate: the FIRST callback for a state_id installs RustMemoryProxy as state.memory (_create_state_for_callback line 2231) and caches the state. On SECOND+ callback, the cached state already has the proxy; the replay then calls state.memory.store(page_addr, BVV(4096)) which routes RustMemoryProxy.store -> mgr.set_state_memory_concrete -> 16-byte chunks -> SymbolicMemory.store_concrete -> CLOBBERS symbolic_objects. Next-step symbolic restore (sym_entries from pending_memory_load_symbolic_page) reads from now-clobbered Rust state; AST never re-written; downstream constraints become unsatisfiable for original symbols; solver eval returns the witness (0x20 space) instead of the intended flag chars. Fix: early-return both helpers when _is_rust_memory_proxy(state.memory) is True. Architectural distinction worth knowing: set_state_memory_ast requires pages already mapped and does NOT mark imported_addrs; tracked_symbolic_writes replayed via import_symbolic_to_state DOES create pages on demand and DOES mark imported_addrs (so get_state_symbolic_z3_asts can find the entries). Under the proxy gate, the proxy is the single source of truth — no separate Python state needs syncing. Repro: ANGR_RUST_USE_CALLBACK_MEMORY_PROXY=1 .venv/bin/python tests/benchmarks/run_single.py defcon2016quals_baby-re --engine rust.

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

s1-easy-query-census-verdict forgotten

S1 easy-query census (angr-op0dn.3) verdict: KILL the cheap pre-solver tier as framed. Instrumentation = native/angr/src/symbolic/query_class.rs (classify_bool/classify_eval/classify_eval_many/classify_extrema + ClassScope), attribution hook inside timed_check (solver_build.rs) so per-class counts sum EXACTLY to z3_check_count; enable with ANGR_RUST_QUERY_CLASS=1, counters surface as z3_check_class_* in mgr.stats(). Corpus (11 fast benches, 1320 checks, tests/benchmarks/query_class_numbers.json): addressable = 2.2% (trivial_decide 2, single_var_range 27, forced_value 0) vs KILL gate >=30%. free_witness 25.2%, hard 72.7%. Two load-bearing findings: (1) forced_value == 0 corpus-wide — NO eval query is pinned by an x==const constraint, so there is no soundness-free eval fast path to harvest; every eval win would change concretization (M2 determinism interaction). (2) single-var is bimodal, not rare-uniform: android_arm_license_validation is 94% single_var_range while every other bench is 0-2.5%, so an interval tier is a per-binary-shape win, not a corpus win. Classification runs AFTER the shipped fast paths (as_u128 short-circuits, cached-model hits, constructor folds), so these are NOT double-counted wins.

forgotten 2026-08-05T04:34:40Z — Iteration-receipt verdict for closed bead angr-op0dn.3 (M1 census, epic closed); dense one-off corpus numbers with no standalone generalizable rule.

s10-flip-gate-opt-in-half-drained forgotten

angr-op0dn.14.8 drained the opt-in half of the S10 flip gate: CONSTRAINT_TRACKING_IN_SOLVER -> _RAISE_OPTION_NAMES (Rust adds constraints with no assumption literals, so unsat_core() returns silently empty — worse than refusing); CONCRETIZE_SYMBOLIC_WRITE_SIZES -> exonerated (its ONLY in-tree SimOption consumer is SimFileBase._prep_generic in storage/file.py — the identically-named memory knob is a SimMemory constructor kwarg in size_resolution_mixin.py, NOT the option — and native write paths fall back to Python on a symbolic count); CGC_NON_BLOCKING_FDS -> honored both ways (see invariant-cgc-fdwait-option-polarity). parity_census.py silent surface 13 -> 10; opt-in silent count now 0. Remaining flip gate: the tri-state dispatcher (14.5.2), the 3 default-bundle silent options (14.7), the 63 flip-blocking bridge sites, and the constraints inspect gap (14.4.1). angr-op0dn.14.2 (unsat_core wiring) is now beyond-parity feature work, not a flip blocker.

forgotten 2026-07-20T05:01:47.623411+00:00 — Progress increment on the same S10 flip-gate ledger as the verdict memory; exoneration details folded into canonical (merged into s10-parity-census-flip-gate-verdict)

s10-parity-census-flip-gate-verdict forgotten

S10 (angr-op0dn.8) verdict: the default-engine flip is blocked by SILENCE, not by missing features. Score divergences by loudness, not feature count: anything Rust detects BEFORE it diverges (raise in _RAISE_OPTION_NAMES, raise in RustInspectProxy._check_event, rejected techniques, unsupported arch) is dispatcher-handleable and NOT a flip blocker. 32/44 divergence-risk SimOptions are already loud (14 raise, 10 warn, 8 exonerated -- the docs SimOption matrix is stale in the CONSERVATIVE direction; see _EXONERATED in tests/benchmarks/parity_census.py for the citation clearing each). Silent surface was 13 items. Two inversions: (1) THE RAISE IS THE REGRESSION -- a raise is right for an opt-in engine and fatal for a default one, so the tri-state dispatcher (angr-op0dn.14.5.2) gates all other M6 work; (2) a DEFAULT-BUNDLE option cannot be dispatched around, since routing on it routes everyone -- EXTENDED_IROP_SUPPORT / TRACK_CONSTRAINT_ACTIONS / TRACK_MEMORY_MAPPING ride the 'symbolic' bundle that plain entry_state() hands out, and the latter two are the pair deliberately excluded from the warn set BECAUSE they are default. Minimal parity set = dispatcher + those 3 options + 63 flip-blocking bridge sites (M6.5a) + the constraints inspect gap. The 7 opt-in silent options were ONE-LINE promotions to _RAISE_OPTION_NAMES, not features. Regenerate: python tests/benchmarks/parity_census.py --json. PROGRESS (angr-op0dn.14.8) drained the opt-in half: CONSTRAINT_TRACKING_IN_SOLVER -> _RAISE_OPTION_NAMES (Rust adds constraints with no assumption literals, so unsat_core() returns silently empty -- worse than refusing); CONCRETIZE_SYMBOLIC_WRITE_SIZES -> exonerated (its ONLY in-tree SimOption consumer is SimFileBase._prep_generic in storage/file.py -- the identically-named memory knob is a SimMemory constructor kwarg in size_resolution_mixin.py, NOT the option -- and native write paths fall back to Python on a symbolic count); CGC_NON_BLOCKING_FDS -> honored both ways (see invariant-cgc-fdwait-option-polarity). Silent surface 13 -> 10; opt-in silent count now 0. Remaining flip gate: the tri-state dispatcher (14.5.2), the 3 default-bundle silent options (14.7), the 63 flip-blocking bridge sites, and the constraints inspect gap (14.4.1). angr-op0dn.14.2 (unsat_core wiring) is now beyond-parity feature work, not a flip blocker.

forgotten 2026-08-05T04:34:40Z — dup_of_docs confirmed: docs/advanced-topics/rust_engine.rst 'The default-engine flip gate' section now documents this (and the closure of op0dn.14.5.2/.14.7/.14.9) more currently than this stale status snapshot.

s3-determinism-variance-attribution-verdict forgotten

S3 (angr-op0dn.4) determinism-variance attribution verdict — the residual TIMING variance and the RESULT instability are DISJOINT phenomena, and only the second is boundary-fixable. Harness: tests/benchmarks/run_determinism_census.py (run_repeat/summarize; numbers pinned at tests/benchmarks/determinism_numbers.json). It runs N repeats per bench per mode, fingerprints (a) the WORK each run did — path-pure counters only, WORK_COUNTERS: steps/state_creations/found/z3_check_count/... no ns timings — and (b) the RESULT (found-pc multiset + bench stdout hash), then does a between/within one-way variance decomposition on wall clock grouped by work fingerprint. Findings (n=8/6/5): (1) BOTH bimodal benches measured — google2016_unbreakable_1 (deterministic CV 0.31) and ekopartyctf2016_sokohashv2 (CV 0.17) — have work_groups==1 and IDENTICAL results across every repeat. Same questions to Z3, same answers, same reported flag; only the clock moves. So 100% of the bimodal residual CV is Z3's internal restart/variable-selection path: IRREDUCIBLE at the SymContext boundary, confirming perf-campaign-p5-deferral-decision from a second direction. Canonicalizing eval_upto witness order buys ZERO wall clock. (2) The model choice that IS unpinned shows up somewhere else entirely: csgames2018 (multi-solution keygen) has 6 distinct work groups across 6 runs, diverging ONLY in z3_check/sat/unsat, found_pcs identical, and the printed key list differs run to run — model choice, boundary-fixable, worth ~0.026 CV (i.e. nothing). CONSEQUENCE for M2 (angr-op0dn.10): its acceptance clause 'collapse >=half the residual CV' is UNREACHABLE and must be dropped; the 'make found set + evaluated models identical across runs' clause is reachable and is the only thing worth building (10.1 canonical eval_upto witness order + 10.4 result-equality harness). Do not scope M2 as a variance/perf win — it is a reproducibility win with no timing payoff.

forgotten 2026-08-05T04:34:40Z — Iteration-receipt verdict for closed epic angr-op0dn.10 (M2, closed); its irreducible-Z3-variance finding duplicates perf-campaign-p5-deferral-decision (own text: 'confirming... from a second direction') and its M2 consequence was already executed.

s35o-savings-bottleneck forgotten

angr-s35o (commit 1c6b0fd2b, 2026-05-21): define_unsigned_cmp_pair! + define_signed_cmp_pair! in symbolic/value.rs replaced 8 borrow+consuming method pairs (ult/ule/ugt/uge + slt/sle/sgt/sge). Bead estimated ~250-300 LOC savings; actual net -87 LOC (-231/+144). Same shape as the vec-ops-savings-bottleneck pattern: macro definition overhead (~85 LOC for the two macros with doc strings) plus per-invocation arg-table lines eat into the savings when each method body is small (signed variants were only ~17 lines pre-refactor). Refactor still earns its keep — eliminates 8x duplication and matches house style with width_unop! / define_float_to_float! — but for future similar refactors, target the LOC estimate based on (per-body lines - macro-call lines) * N, not raw body LOC.

forgotten 2026-06-04T21:58:05.190033+00:00 — Iteration receipt with commit hash; lesson is captured in canonical vec-ops-savings-bottleneck (cited)

s5a-merge-cost-shape-divergence-proportional forgotten

S5a memory-merge cost-shape spike (angr-op0dn.11.1.1, commit on rust-symex). Diamond CFG: 16 mapped pages, fork 2 arms, each writes 8 bytes to ONE shared page. BEFORE/AFTER merge cost (measured in native/angr/src/memory/tests/merge_cost_shape.rs): production SymbolicMemory::merge (memory/mod.rs) walks ALL shared pages = 16 pages / 65536 bytes (does load_concrete(0,PAGE_SIZE) x2 + full PAGE_SIZE byte compare per shared page, even ptr-identical ones). A CoW-skip keyed on MemoryPage::shares_data_with (Arc::ptr_eq(data) && bitmaps equal) walks only the 1 divergent page / 4096 bytes = 16x fewer -> merge cost becomes DIVERGENCE-PROPORTIONAL (pages_walked==divergent-page-count). ITE cells already == divergent-byte-count (8==8) because production already does a per-byte continue on equal concrete bytes. KEY FINDING (retires the risk premise behind sibling angr-op0dn.11.2.1): OrdMap<u64,MemoryPage> + Arc<Vec> structural sharing SURVIVES fork->divergent-write->merge; only the written page breaks ptr-sharing (store_concrete uses Arc::make_mut, page.rs). So the primitives for a divergence-proportional native merge ALREADY SHIP; .11.2.1 is impl-only, not a feasibility question. shares_data_with is #[cfg(test)] for now (promote to pub(crate) when .11.2.x wires the skip into merge). FOLLOW-UP (angr-op0dn.11.2.1, commit 71f21bde3): productionized. The skip did NOT promote shares_data_with as this memory originally suggested — that predicate is UNSOUND for merge (admits symbolic-shared pages whose symbolic_objects diverge). Production uses a narrower MemoryPage::is_shared_identical (ptr-eq + no symbolic/Multi overlay). See merge-cow-skip-soundness.

forgotten 2026-08-05T04:34:40Z — Closed spike (angr-op0dn.11.1.1/.11.2.1); its own text says the skip predicate it proposed was superseded — see merge-cow-skip-soundness, which documents the actual shipped (narrower) predicate.

s5b-constraint-merge-cost-shape forgotten

S5b (angr-op0dn.11.1.2, spike 4a347063b) — constraint-merge cost shape + CoW-prefix prototype on a diamond CFG. Sibling of S5a memory spike (s5a-merge-cost-shape-divergence-proportional). FINDING: current SymContext::merge (snapshot_fork_ops.rs::merge, feature vex-engine-z3) iterates shared.iter().chain(ctx_local.z3_assertions.iter()) per arm and emits Or(not merge_cond, assertion) for EVERY constraint -> guarded-Or count == TOTAL constraint count (Sigma shared+local over arms), even though every arm's shared prefix is byte-identical. The fork-freeze-self-invariant is the enabling primitive: fork() drains self local into z3_assertions_shared via freeze_into_shared then hands the SAME Arc<Vec> to the child, so two arms forked from one base hold a PTR-EQUAL frozen prefix Arc to the merge point (verified Arc::ptr_eq across arms==base); post-fork divergence lives only in local.z3_assertions. Because all diamond arms descend from the fork point, the shared prefix holds on every path reaching the merge, so it can be asserted ONCE UNGUARDED, guarding only local suffixes -> guarded == divergent-constraint count + 1 Or. MEASURED on 8-shared/2-per-arm/2-arm diamond: guarded 4 vs 20 (5x), total assertions 13 vs 21 (prefix collapses from N_ARMS copies to 1, saving (N_ARMS-1)*SHARED). CoW-merged stays SAT + admits exactly arm-value union (x=200,300 yes; 100,5 no) == KILL clause passes; matches production merge SAT/eval EXACTLY (test_cow_merge_matches_production_semantics). SOUNDNESS INPUT for .11.3 kill clause: unguarded-prefix is sound ONLY because local is strictly ADDITIVE to the frozen shared prefix (never retracts/shadows) - test_local_never_shadows_shared checks it. VERDICT: GO on the constraint leg - divergence-proportional guarded count is achievable with zero new primitives; both S5 legs (memory=S5a, constraints=S5b) clear. Fidelity gaps deferred to M3-2b impl (angr-op0dn.11.2.2): assumed export pairs (keep assume_class_reconstructible=false), non_bv residual log (unguarded prefix DOES have RustBV form -> could stay reconstructible), memory-side multi_objects/pending_writes/longest-stdout. cf cheap sibling angr-sbvcx (drop-identical dedup).

forgotten 2026-08-05T04:34:40Z — Closed spike (angr-op0dn.11.1.2/.11.3); the prototype it describes was superseded by the productionized design in merge-shared-prefix-aware.

safety-comment-fp-helpers-pattern forgotten

value.rs FP helpers (build_fp_z3_ast_cached + 5 siblings, native/angr/src/symbolic/value.rs lines ~2660–3230) use a uniform Z3 FFI safety pattern that's worth lifting into a single function-level SAFETY block instead of restating 30 times: (1) raw_ctx = z3::Context::thread_local().get_z3_context() (thread-local context, valid for thread lifetime), (2) all Z3_ast inputs come from .get_z3_ast() on live Float/BV/Bool/RoundingMode/Sort wrappers bound in scope, (3) every Z3_mk_fpa_*.expect(...) converts NULL to panic so escaping ptrs are non-null, (4) raw is immediately handed to Float::wrap/BV::wrap/Bool::wrap (takes Z3's construction-time refcount), (5) nothing escapes the calling thread. The per-block SAFETY comment then just names the specific FFI call + operand wrappers. Pattern in commit de5a5666c (angr-59t3).

forgotten 2026-06-04T21:58:05.540949+00:00 — Refactor receipt with commit hash; pattern is in code

sampler-perf-test-pattern forgotten

Sampler-hook perf test pattern (native/angr/src/symbolic/lineage.rs::test_sampler_hook_overhead_under_budget, commit ec9984782): to exercise the DECISION branch (not the short-circuit 'accumulate window' branch) on every iteration, manually bump LINEAGE_SWITCH_COUNT/LINEAGE_SWITCH_HOT_COUNT inside the loop so each sample window sees switch_delta >= min_switches. The natural pattern of 'switch many times once' folds all switches into the first window only; subsequent sample-step calls then see switch_delta=0 and take the early-return. Also requires SAMPLER_TEST_LOCK + reset_dismantle_state_for_test() since the hook touches global atoms shared across all parallel tests in the lineage module.

forgotten 2026-08-05T04:34:40Z — Implementation-level test-authoring detail pinned to one test (native/angr/src/symbolic/lineage.rs::test_sampler_hook_overhead_under_budget); relocate as a comment there.

satisfiable-wrong-answer forgotten

satisfiable() returning False on exception is a wrong-answer bug, not a fallback. False means UNSAT, which is a definitive answer about the constraint system. If the solver itself errored, callers need to know — silently claiming UNSAT will mask flag-finding failures and other solver bugs. Fix in commit 4f16e3792: re-raise from satisfiable_with_fallback in rust_state_export.py. Same pattern existed in _attach_rust_solver_primary fixed in 1e00e4919.

forgotten 2026-06-05T17:23:37.529461+00:00 — Fixed-bug receipt with two commit hashes; principle is general (don't swallow exceptions as UNSAT)

satoutcome-decided-forcing-function remembered

Z3 SatResult interpretation is centralized in SatOutcome::decided (trait in native/angr/src/symbolic/solver_build.rs): Sat->Some(true), Unsat->Some(false), Unknown->None. Every sat/unsat decision in solving_ops.rs + transaction_ops.rs routes through .decided() so a timeout can never silently collapse to a boolean (the angr-ph300.43/angr-n0irt.1 bug class). Only two raw SatResult::{Sat,Unsat} matches remain, both tagged '// satresult-exempt': the per-variant stats counter in timed_check and the SatOutcome impl itself. tools/audit_satresult.py is a hard grep gate banning new raw matches outside the helper (scans native/angr/src, skips *_tests.rs and /_tests/ dirs, honors // satresult-exempt within 2 lines above). When adding a new solver query: use .decided(), don't match SatResult directly. See invariant-z3-unknown-not-unsat.

scanf-now-accepts-zjt-modifiers forgotten

Pre-angr-ryw5, scanf's parse_scanf_format silently ignored %z/%j/%t in the length-modifier match arm (no consume), so they fell through to the spec parser and errored as 'unsupported specifier'. Post-ryw5, scanf accepts them (matches glibc). If a test ever fails because it expected an error from %zd-in-scanf, this is the cause — the new behavior is intentional and correct.

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

scanf-scanset-native-implemented forgotten

scanf %[...] scanset IS implemented natively in procedures/scanf.rs (fn scanset_body_len + the b'[' arm in parse_scanf_format) — handles %[set], %[^set] negation, max-width, assignment-suppression (%*[...]), literal-bracket-as-first-member, and unterminated->ProcedureError fallback. Treated as a string-like conversion: mints fresh symbolic bytes + NUL like %s (no concrete set-membership filtering — faithful to angr's symbolic-byte modeling). 5 tests in scanf_tests.rs: test_scanf_scanset_{basic,negated_newline,with_width_and_literal_bracket,suppressed_then_int,unterminated_falls_back}. This is the IMPLEMENTED third of the format_parser-gaps trio; %n and float %f/%e/%g are won't-implement parity fallbacks (see format-n-no-native-parity, format-float-no-native-parity). Closed angr-11djq.9 (T2-B6) as fully-covered on these three; T2-MEASURE (11djq.4) found 0 format fallbacks on the real xmllint target so no native expansion is motivated.

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

scanf-scanset-unconstrained forgotten

Native scanf %[...] scanset (procedures/scanf.rs, scanset_body_len fn + b'[' arm of parse_scanf_format) is modeled like %s: it mints UNCONSTRAINED symbolic bytes capped at field width, NOT constrained to the bracketed character class. This matches the existing %s/sscanf simplification (file content not parsed). So %[^\n] does NOT actually exclude newlines from the minted bytes — over-approximation, sound for path-finding but may yield spurious solutions. scanset_body_len handles %[^...] negation + the POSIX leading-']' literal-member rule (']' right after '[' or '[^' is a set member, not the closing bracket); unterminated sets return None -> ProcedureError -> Python fallback. If a future bead needs faithful matching, add per-byte set-membership constraints there.

forgotten 2026-08-05T04:34:40Z — Implementation-level correctness note pinned to native/angr/src/procedures/scanf.rs (scanset_body_len / b'[' arm of parse_scanf_format); relocate as a comment there.

scanf-simproc-design forgotten

Native scanf creates symbolic BVS per format specifier: %d/%i/%u/%x/%o=32-bit, %ld/%lld=64-bit, %c=8-bit, %s=N symbolic bytes + NUL. Stdin symbols tracked via record_stdin_symbol(). Format string must be concrete. Pointer args must be concrete. Suppressed specifiers (*) skip pointer consumption. sscanf ignores source string content (creates symbolic values like scanf). Registered for: scanf, __isoc99_scanf, sscanf.

forgotten 2026-08-05T04:34:40Z — Stale: sscanf no longer mints symbolic values (NativeSscanf::call now unconditionally defers to Python per angr-8onrp — verified in native/angr/src/procedures/scanf.rs); current contract is documented in docs/extending-angr/simprocedures.rst and native_coverage_matrix.rst.

scheduler-dead-code-audit forgotten

Scheduler dead-code audit (angr-0mqkc.1): the blanket #[allow(dead_code)] on 'mod scheduler' in exploration/mod.rs was angr-1ilq.3 incremental-landing scaffolding; steady-state has landed so it's removable. Pattern for exploration/scheduler.rs: the Lifo-default *::new() convenience ctors (WorkTransport/WaveJob/RunSession) and the ParallelScheduler compat-wrapper struct are TEST-ONLY (production uses *_with_policy + PersistentPool directly) -> gate with #[cfg(test)], not allow(dead_code). WorkTransport::new had zero callers (deleted). WaveDone is a unit barrier token (coordinator only counts num_workers of them, never reads worker_id) -> field dropped. Lifo import is now #[cfg(test)] since only gated ctors reference it. ActiveResidual MatKind variant (run_loop.rs) stays with justified allow: matched/handled but not yet produced (forward scaffolding).

forgotten 2026-07-19T19:50:56.728812+00:00 — closed-only AND status-shape

scheduler-policy-propagation forgotten

Parallel scheduler policy propagation (angr-x1fya.1, commit 33ea8d1c0): run_loop's SelectionPolicy (mod.rs field self.policy, now Arc not Box) is threaded into the parallel scheduler via WaveJob::new_with_policy / RunSession::new_with_policy (scheduler.rs), called at the two run_loop.rs parallel dispatch sites with Arc::clone(&self.policy). Before this, WorkTransport defaulted to Lifo REGARDLESS of the Python-set policy, so a FIFO/BFS-configured run silently did LIFO/DFS under RUST_PARALLEL_WORKERS>1. Standalone WaveJob::new/RunSession::new keep the Lifo default so run_instrumented + scheduler unit tests stay byte-identical. offload_surplus's pop_front victim choice is worker-local single-threaded, so Fifo select+offload overlap is a load-balancing nuance, not a race.

forgotten 2026-07-20T05:01:48.644718+00:00 — Fix for the latent issue documented in scheduler-selection-policy-seam (same scheduler-policy-routing rule); folded into canonical so the stale 'not yet fixed' claim dies (merged into scheduler-selection-policy-seam)

scheduler-selection-policy-seam forgotten

Parallel scheduler (exploration/scheduler.rs) dispatch routes through the SelectionPolicy seam (angr-1ilq.9, commit b9fc5f002). WorkTransport holds an Arc; dispatch_next calls policy.select(local) (was pop_back), absorb_continues calls policy.on_fork(local, child) (was push_back). Default = Lifo, which reproduces the pre-seam open-coded pop_back/push_back BYTE-IDENTICALLY -> zero regression. POLICY PROPAGATION (angr-x1fya.1, commit 33ea8d1c0) fixed the latent issue where WorkTransport defaulted to Lifo REGARDLESS of the Python-set policy, so a FIFO/BFS-configured run silently did LIFO/DFS under RUST_PARALLEL_WORKERS>1: run_loop's SelectionPolicy (mod.rs field self.policy, now Arc not Box) is threaded into the parallel scheduler via WaveJob::new_with_policy / RunSession::new_with_policy (scheduler.rs), called at the two run_loop.rs parallel dispatch sites with Arc::clone(&self.policy). Standalone WaveJob::new/RunSession::new keep the Lifo default so run_instrumented + scheduler unit tests stay byte-identical. offload_surplus's pop_front victim choice is worker-local single-threaded, so Fifo select+offload overlap is a load-balancing nuance, not a race. A find-aware ordering policy for num_find=1 also plugs in here but its raw waste win is ~0 (see ilq8-post-cancel-waste); justify only as directed search.

forgotten 2026-08-05T04:34:40Z — Parallel-scheduler dispatch integration and the Box->Arc / policy-propagation fix folded into the canonical SelectionPolicy seam memory. (merged into selection-policy-seam-limits)

segmentlist-iter-snapshot forgotten

SegmentListIter snapshots into Vec<(start,end,sort)> at iterator construction (segmentlist.rs:232). RangeMap is BTreeMap-backed so .iter().nth(k) was O(k); the snapshot makes total iteration O(n). Iterator type has no #[new] — only constructed via SegmentList::iter. Snapshot semantics mean concurrent modifications are not observed, which matches typical Python iterator expectations.

forgotten 2026-06-04T21:58:05.910278+00:00 — Implementation detail with line cite; trivial to rediscover from code

selection-policy-matrix-verdict-0r5y3 forgotten

angr-0r5y3 selection-policy measurement matrix VERDICT (a32jl.2d acceptance gate). Ran fifo/lifo/random/coverage/loop_head via run_single.py --strategy (now exposes all 5; commit 0bc3d6393) on the available corpus: fauxware (7 steps/211MB/0.27s/peakW1), defcamp_r100 (2 steps/241MB/0.32s/W1/found1), xmllint_getenv (3.5s/365MB/W2/found1), google2016_unbreakable_1 (~2s/613MB/W1/found1). FINDING: all five policies are byte-identical on steps/found/peakW/peakRSS and within-noise on wall across the ENTIRE corpus, because the active frontier never exceeds width 2 -- with <=1 selectable state, select() returns the same state regardless of policy, so ordering is a no-op. (A one-off 24s loop_head reading on unbreakable_1 did NOT reproduce -- cold-cache/system noise, not a bucket_key O(history) cliff.) The only policy-discriminating workload is a wide/deep divergent frontier (grub/xmllint-parser class), which stays untractable: 75mc human-gated, grub OOMs, xmllint parser path explodes on BOTH engines (xmllint-vanilla-symex-probe). VERDICT: NO policy beats FIFO and NONE regresses -- all are opt-in + default-inert + Rust-unit-tested (selection_policy.rs asserts the ordering divergence on controlled multi-state setups). NO losers deleted: they cost zero on the default path and are the vehicles for a32jl.4 (CFG-distance directed) + lnzcu. The corpus simply lacks a frontier to exploit; the epic's discriminating-target gap is inherited, not a policy defect.

forgotten 2026-08-05T04:34:40Z — Iteration-receipt: closed measurement matrix (a32jl.2d, bead no longer in tracker) showing the available corpus lacks a discriminating frontier; a one-off negative benchmark result, not a durable rule.

selection-policy-seam forgotten

SelectionPolicy trait seam (angr-a32jl.1, commit ae1120f26): active-state selection + fork insertion are pluggable via native/angr/src/exploration/selection_policy.rs. Trait SelectionPolicy{select(&mut VecDeque)->Option, on_fork(&mut VecDeque, state), name()} with Send+Sync supertraits; zero-sized built-ins Fifo (BFS default: select=pop_front, on_fork=push_back) and Lifo (DFS: select=pop_back, on_fork=push_back). Stored as Box field 'policy' on RustExplorationManager (replaced use_lifo:bool); swapped by set_state_selection_lifo/fifo. TWO chokepoints: (1) SELECT is open-coded in run_loop.rs (NOT StashManager::pop_active, which is dead code w/ no non-test callers) as policy.select(deque) via 'let policy=&*self.policy;' bound before the self.sm.get_mut borrow to keep fields disjoint -- and run_loop deliberately does NOT unindex on pop (pop_active does; do not route run_loop through pop_active or you add an unindex behavior change); (2) ON_FORK via new StashManager::push_active(&dyn SelectionPolicy, state) = self.index + policy.on_fork(self.entry(ACTIVE)), called from helpers::push_to_active_or_drop. Future policies (a32jl.2: random-path, coverage-guided, CFG-distance) impl the trait. Byte-identical step traces under default Fifo; run_regression 22/22 green.

forgotten 2026-08-05T04:34:40Z — Original trait definition and chokepoint detail folded into the canonical SelectionPolicy seam memory (its Box<dyn> claim was stale, corrected in the merge). (merged into selection-policy-seam-limits)

selection-policy-seam-limits remembered

SelectionPolicy trait seam (native/angr/src/exploration/selection_policy.rs, angr-a32jl.1): active-state selection + fork insertion are pluggable. Trait SelectionPolicy{select(&mut VecDeque)->Option, on_fork(&mut VecDeque, state), name()} with Send+Sync supertraits. Built-ins: zero-sized Fifo (BFS default: select=pop_front, on_fork=push_back) and Lifo (DFS: select=pop_back, on_fork=push_back), plus RandomState (a32jl.2 pt1: seeded SplitMix64, remove-at-random-index, opt-in via set_state_selection_random — deliberately random-STATE/uniform, not KLEE random-PATH/subtree-weighted; subtree weighting deferred, needs on_fork bookkeeping). UPDATE (angr-m9fpp): coverage-guided new-block-first ALSO fits the seam unchanged — RustSimState::pc() is callable per-state inside select(), and a policy-internal Mutex<HashSet> supplies the seen-set — so the earlier claim that coverage 'CANNOT be done in this seam' was wrong: any policy needing per-state next-block addr fits. loop-head round-robin (angr-caplg) was the remaining open question at the time — it needs LoopBound back-edge bucket data (native_technique.rs), not just pc(), so it may need a signal beyond the deque.

TWO CHOKEPOINTS in the serial path: (1) SELECT is open-coded in run_loop.rs (NOT StashManager::pop_active, which is dead code w/ no non-test callers) as policy.select(deque), bound before the self.sm.get_mut borrow to keep fields disjoint — do not route run_loop through pop_active or you add an unindex behavior change (pop_active unindexes on pop; run_loop deliberately does not); (2) ON_FORK via StashManager::push_active(&dyn SelectionPolicy, state), called from helpers::push_to_active_or_drop. The policy field was originally Box on RustExplorationManager (replaced use_lifo:bool) but is now Arc (native/angr/src/exploration/mod.rs) so it can be shared with the parallel scheduler.

PARALLEL scheduler dispatch (scheduler.rs) routes through the same seam: WorkTransport holds the Arc; dispatch_next calls policy.select(local) (was pop_back), absorb_continues calls policy.on_fork(local, child) (was push_back). Default = Lifo reproduces the pre-seam open-coded pop_back/push_back BYTE-IDENTICALLY. POLICY PROPAGATION (angr-x1fya.1) fixed a latent bug where WorkTransport defaulted to Lifo REGARDLESS of the Python-set policy, so a FIFO/BFS-configured run silently did LIFO/DFS under RUST_PARALLEL_WORKERS>1: run_loop's SelectionPolicy is now threaded into the parallel scheduler via WaveJob::new_with_policy / RunSession::new_with_policy, called at the two run_loop.rs parallel dispatch sites with Arc::clone(&self.policy). Standalone WaveJob::new/RunSession::new keep the Lifo default so run_instrumented + scheduler unit tests stay byte-identical. offload_surplus's pop_front victim choice is worker-local single-threaded, so Fifo select+offload overlap is a load-balancing nuance, not a race.

Byte-identical step traces under default Fifo/Lifo; regression suite green at time of introduction.

selfcall-continuation-regression-test remembered

Binary-free regression for a SimProcedure self.call() continuation (the aca6y FFI push fix): build a load_shellcode AMD64 blob with a lone-ret callee (0xC3) and a SELF-LOOP ret target (0xEB 0xFE, NOT a bare ret -- a bare ret drains to the unconstrained stash with garbage PC and yields an empty active list). Hook the entry with a SimProcedure whose run() does self.call(callee,[],"after_call",prototype="void x()") and whose after_call() stores a sentinel to a fixed mapped addr. After mgr.run, collect states from active+deadended+errored+unconstrained and assert the sentinel is in some state's memory. Trace: step0 PC=callee, step1 PC=rettgt (callee ret popped the continuation -> after_call ran -> top proc ret'd to rettgt). See test_procedures.py::TestSimProcedureSelfCallContinuation, bead angr-2d2h8.

serde-json-byte-keyed-map-trap remembered

Why RustSimStateSnapshot uses Vec<(Vec,Vec)> not BTreeMap for environment: serde_json only allows string-shaped map keys. The angr environment map is byte-keyed (Arc<HashMap<Vec,Vec>>) so BTreeMap<Vec,Vec> serializes through serde_json as Error('key must be a string'). The shadow type is a sorted pair list instead. Bincode/postcard would accept the map shape but the prototype picked JSON for tolerance to BVOp variant churn (see snapshot-rustsimstate-envelope-shape). Same trap applies to ANY byte-keyed map in a serde_json-serialized snapshot — convert to sorted pair list at the shadow boundary. Symptom: test_state_to_from_serialized_round_trip panics at serde_json::to_vec call.

serde-json-unbounded-depth-for-rustbv remembered

serde_json default recursion limit (128) is hit by deep RustBV op-trees when loading snapshots from real benches. Fauxware after step(10) produces sufficiently deep ASTs that load_snapshot fails with 'recursion limit exceeded at line 1 column 330752'. Fix: enable serde_json feature 'unbounded_depth' in native/angr/Cargo.toml, build the Deserializer explicitly, call disable_recursion_limit() before deserialize. Applied at both RustSimState::from_serialized AND StashManager::load_snapshot. The on-disk envelope is trusted (written by our own dump path) so the DoS hardening the limit provides is not load-bearing. Note: disable_recursion_limit does NOT switch to heap-based recursion — for trees >~10k deep you'd still hit thread stack overflow. The fauxware case is in the low-hundreds range.

set-max-history-non-converging-bug forgotten

set_max_history (state.rs) was non-converging before retroactive trim: add_to_history/add_history_entry only remove ONE entry per push when over cap, so a state with 100 entries pushed at cap=5 would oscillate between 100-101 entries forever. Fix: drain the head down to cap immediately on set_max_history. The manager-level set_max_history cascades into state.set_max_history for every existing state, so the fix automatically works retroactively for both the per-state Python API and the manager comment that promised 'applied to every state already in any stash'.

forgotten 2026-06-04T21:58:06.255831+00:00 — Fixed-bug receipt; behavior is in current code

sfp9-dedup-null-result forgotten

angr-sfp9 (commit f13fde0b2, 2026-05-25): HashSet ptr-dedup side-table on add_constraint_raw lands as a documented NULL result. Predicted from angr-1joc measurement: csaw_wyvern 83% hit, flareon2015_5 31% hit, others 0%. After implementation, csaw_wyvern is 2.87s±0.03 (baseline 2.85s±0.02); flareon2015_5 is 3.66-3.79s (baseline 3.63-3.68s). Counters confirm dedup is firing correctly (33/40 hits on csaw_wyvern). Why no win: Z3 internally dedups asserts so the skipped solver.assert call is a no-op; the skipped Bool::clone + Vec push are nanoseconds against millisecond bench noise. Bench-regression 15/15 pass. baseline_timings.json shows 0.94s for csaw_wyvern but current env is 2.85s — baseline is stale env drift; the 'fast-tier' refresh in angr-wnjy did not include csaw_wyvern. Future bench-target reference is the live no-change rerun (2.85s, captured via stash/restore A/B in this session).

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

sgbn-unconstrained-sp-fastpath forgotten

_concretize_stack_registers (rust_state_sync.py) had a 10ms cold-path cost on blank_state because rbp is filled with an unconstrained BVS by default_filler_mixin, and solver.eval(unconstrained) paid full Z3 ctx init + check + model (~10ms) just to get back 0. Fast path (commit 1a233418b): scan state.solver.constraints for variable overlap with reg_val.variables — if no overlap, return default directly (0 for BP, arch.initial_sp for SP). Z3's model for an unconstrained var is arbitrary anyway, and downstream code only requires some concrete value. Measured fauxware blank_state: 10.04ms->0.29ms (34x). Warm entry_state path unchanged (0.08ms). Constraint scan itself costs <1us when solver.constraints is small. Note: scan cost grows with constraint count, so for heavily-constrained states the worst case is slow scan + solver.eval; the unconstrained fast path is for fresh blank/manual states where the optimization actually matters. (Symbol-anchored per refactor-memory-sweep-rule; raw rust_state_sync.py:804 drifted to ~929 by iter52.)

forgotten 2026-08-05T04:34:40Z — Fixed-in-commit (1a233418b) perf narrative pinned to angr/exploration/rust_state_sync.py::_concretize_stack_registers (now drifted to ~line 929 per its own note); relocate as a comment there.

shared-block-cache forgotten

Block cache was per-interpreter (fresh on every step). Fix: swap exploration-level LruCache<u64, Arc> into each interpreter via swap_block_cache(). ais3: 511→55 lift callbacks, 152ms→11ms lift time. Key insight: interpreters are created fresh via with_config() per step, NOT forked from parents — the fork path clone was irrelevant.

forgotten 2026-06-04T21:58:06.607687+00:00 — Iteration receipt with old measurements; current behavior is in code

shared-lineage-gating-decision forgotten

SharedLineageSolver gating policy DECIDED (angr-op0dn.9.3, 2026-07-13): keep use_shared_lineage_solver default-OFF/opt-in. Measured off-vs-on via run_single.py --use-shared-lineage-solver, median-of-5 wall + --counters-json. The feature WORKS: z3_materialize_count halves everywhere (rev250 88->45, unbreakable_0 52->44, unbreakable_1 52->33) so the 'materialize no longer material' VOID gate is NOT met. But it converts to wall time ONLY where the hot ratio (lineage_switch_fast_path_count/lineage_switch_count) is high: rev250 2.74s->2.47s (-10%, 39% hot) vs anti-case google2016_unbreakable_0 0.99s->1.09s (+10%, 4.5% hot; materialize -46ms but z3_check_time +57ms — switch_to push/pop + sat/model-cache invalidation outweighs the saved materialize). KEY: the v5ht dismantle sampler (tick_and_sample_for_thrash in run_loop.rs) CANNOT backstop a default flip — lineage_dismantled==1 on ALL THREE benches, yet (a) unbreakable_0 still regresses 10% because the >=20-switch detection window is most of a 1s bench so the cost is sunk before the valve trips, and (b) it dismantles rev250 despite its eventual 39% hot ratio and 10% win (early window mispredicts steady state; rev250 wins anyway because dismantle only nulls FUTURE forks' lineage per invariant-v5ht-dismantle-child-none). (b) also kills auto-enable-on-workload-shape: the sampler's early hot ratio is the only runtime shape signal and it is wrong on the one workload the feature helps. Full data: tools/decisions/solver_pool_design.md sec 7 + rust_engine.rst 'Gating policy: the kwarg stays opt-in'. Follow-up (angr-g1fev): per-lineage-tree dismantle instead of one global early decision.

forgotten 2026-07-20T05:01:45.045342+00:00 — Fully covered by rust_engine.rst 'Gating policy: the kwarg stays opt-in' section (numbers, sampler failure modes, auto-enable refutation) + solver_pool_design.md sec 7; follow-up is tracked on bead angr-g1fev

shared-solver-architecture forgotten

Shared solver for callbacks: RustSolverContext now supports SolverCtxStorage::Shared(Rc<RefCell>) variant. borrow_pending_solver() returns O(1) Rc clone instead of ~3ms Z3 Solver::clone. export_callback_bundle defaults to shared_solver=true. Python side skips constraint re-sync when rust_ctx.is_shared() returns True. Constraints added via _rust_add go directly to pending state's solver. Forked states (SimProc multiple successors) still use fork_pending_solver().

forgotten 2026-06-04T21:58:06.959853+00:00 — Overlaps heavily with shared-solver-for-callbacks; both describe shared solver mechanism (merged into shared-solver-for-callbacks)

shared-solver-for-callbacks remembered

Shared solver for callbacks (architecture). RustSolverContext supports SolverCtxStorage::Shared(Rc<RefCell>) variant. borrow_pending_solver() returns O(1) Rc clone instead of ~3-42ms Z3 Solver::clone. export_callback_bundle defaults to shared_solver=true. Python side skips constraint re-sync when rust_ctx.is_shared() returns True. Constraints added via _rust_add go directly to pending state's solver. Forked states (SimProc multiple successors) still use fork_pending_solver(). PendingCallback.solver_ctx uses shared Rc clone (O(1)) instead of Z3 Solver::clone. Pre-callback snapshot (state.fork()) only created when deferred_forks is non-empty. 5 callback paths in exploration.rs updated: Hook, SimProcedure, Syscall, dynamic resolution, run-loop SimProcedure.

shared-z3-context-setup-required remembered

RustSolverContext add_constraint_ast + satisfiable/eval ONLY work when Python and Rust share the Z3 thread-local context. This requires calling angr.exploration.rust_manager._setup_shared_z3_context() once per test process. Without it, claripy's Z3 backend and z3-rs (Rust) live in separate Z3 contexts → AST pointers don't share, hash-cons doesn't unify mk_const(name, sort) calls across the boundary, and Rust's eval/satisfiable see only unconstrained variables (returns 0). Symptom: ctx=RustSolverContext(); ctx.add_constraint_ast(x==5); ctx.add_constraint_ast(x==10); ctx.satisfiable() returns True (should be False). pytest TestSolverOperations bootstraps this in setup_class. RustStateProxy tests that round-trip constraints must do the same — TestRegisterProxySymbolicRecovery does. Why: build.rs makes Python and Rust load the same libz3.so, but z3-rs creates its OWN thread-local Z3 context unless explicitly bridged. How to apply: any new test class that exercises RustSolverContext add_constraint_ast + eval/satisfiable identity must add @classmethod setup_class calling _setup_shared_z3_context(). Inheriting from TestSolverOperations is not enough — pytest doesn't share setup_class across siblings.

sharif7-stale-baseline-iter67 forgotten

sharif7_rev50 fast-tier gate flakiness (iter66 failed 0.99s vs 0.84 baseline) was a STALE baseline, not a regression. Root cause: rust_time=0.84/callback_count=132/steps=120 was recorded at promote (6417b2410), ~140 commits before the stdio native-parity cluster (fgets/fgetc/fputc/fwrite/_unlocked aliases; 878093d15 'serve cle stdin', 3cda07010, 399958bb0). Those intentional+tested commits shifted I/O handling: native procs serve stdin/stream reads inside the SimProcedure callback (callback_count 132->175) instead of stepping libc VEX blocks (steps 120->77). Result stays correct. Diagnosis trick: the most-recent FUNCTIONAL commit (878093d15) already claimed 'fast-tier gate 20/20' with this exact behavior, and only docs/test-file commits followed -> failure must be in-suite memory pressure (gate measures benches in one process; standalone sharif7 is rock-stable 0.90s x4 vs in-suite 0.99s). Fix: refresh baseline_timings.json sharif7_rev50 to rust_time=0.90/callback_count=175/steps=77 (commit 831d8918a), matching flareon2015_2 precedent 9067d8a23. The 0.15 gate threshold absorbs the in-suite delta. See [[avoid-update-baseline-without-verification]].

forgotten 2026-07-04T00:33:53.267855+00:00 — Iteration receipt; stale-baseline incident resolved and baseline refreshed in landed commit 831d8918a

shl-mul-const-rhs-rewrite-location forgotten

Concrete-RHS rewrites for Shl/Lshr/Ashr/Mul (angr-5hpn, commit 51ab6ef51) live in native/angr/src/symbolic/value.rs INSIDE the *into methods, NOT in vex/ops.rs as the bead suggested. Reason: every caller benefits (symbolic_pack_eflags in vex/ccall.rs:222-226 chains 4 shl-by-const ops; ccall.rs and claripy_bridge.rs also shift), and the match arm structure was already there for concrete-concrete and identity cases. Pattern: (None, Some(c)) if c < width as u128 → bit-slice form; (None, Some()) → saturated form (0 or SignExt(MSB)).

forgotten 2026-06-04T16:31:59.303194+00:00 — Code location pointer

showcase-blog-draft forgotten

Rust-symex showcase blog DRAFT lives at docs/blog/rust_symex_showcase.md (angr-4n26m.11, commit 03bdb7011). Markdown, NOT in the Sphinx toctree (it's a blog draft, not API docs). Numbers are sourced from the tests/benchmarks/numbers.json artifacts that show.py emit (NEVER baseline_timings.json, stale by design). If a demo's measured numbers change, re-run that show_*.py and update the corresponding table/figure in the blog. Bead is NOT closeable autonomously: acceptance = user-reviewed draft, so it stays claimed/in_progress until the human signs off. Honesty guardrails baked in: speed='now practical' incl busybox 0.33x loss + bimodal non-wins; multiarch=parity-not-speed; checkpoint/resume='resume the search' w/ value-recovery=observed-bonus; solver-instr=genuinely-new bonus.

forgotten 2026-08-05T04:34:40Z — Draft location, numbers-refresh rule, and closure gate folded into the canonical showcase-blog-epic memory. (merged into showcase-blog-epic)

showcase-blog-epic remembered

Showcase epic angr-4n26m ('Showcase the Rust symex engine: blog post + reproducible demos') scopes a BLOG POST + reproducible demos across 4 angles: raw speed, checkpoint/resume, multi-arch, real bug/vuln. Honesty guardrails (load-bearing, from peer review): (1) baseline_timings.json is STALE BY DESIGN - re-measure every number via run_single.py --both N>=5; (2) sym-write is 1.82x not 2.3x; don't quote bimodal benches as wins; (3) checkpoint/resume is NOT result-identical across restore - frame as 'resume the search'; (4) multi-arch is breadth/parity NOT speed (../binaries absent, only real non-x86 ELF bench android_arm is 0.8x slower); (5) snapshot impl is stash.rs+state.rs+rust_manager.py not public_api.py. Children .1-.12; gates: .3 measurement, .4 determinism-boundary, .1 curate-x86. See ~/.claude/plans/i-want-to-show-reactive-engelbart.md. The blog draft itself lives at docs/blog/rust_symex_showcase.md (angr-4n26m.11) — Markdown, NOT in the Sphinx toctree (it's a blog draft, not API docs). Numbers are sourced from the tests/benchmarks/numbers.json artifacts that the show.py demo scripts emit (NEVER baseline_timings.json, stale by design) — if a demo's measured numbers change, re-run that show*.py and update the corresponding table/figure in the blog. The writing bead is NOT closeable autonomously: acceptance = user-reviewed draft, so it stays claimed/in_progress until a human signs off.

showcase-checkpoint-resume-demo forgotten

Checkpoint/resume showcase demo (show_checkpoint_resume.py, angr-4n26m.8) uses a TWO-PROCESS orchestration: the script re-invokes itself via subprocess with --phase dump / --phase resume over a shared on-disk snapshot. This both (a) authentically crosses a process boundary (the whole point of the demo) and (b) is the in-loop memory-safe pattern — each phase runs angr under its own RLIMIT_AS=3GB guard. The 'guaranteed' claim is SEARCH CONTINUITY, proved by a structural frontier fingerprint {stash_counts, sorted(active_addrs)} captured pre-dump and re-captured immediately after load_from_disk; both are deterministic and independent of Z3 model latitude so an exact match proves restore (not re-run). At snapshot_steps=14 fauxware yields a rich frontier (2 active + 1 deadended). Value-recovery is reported as observed-bonus-not-guaranteed (model equality not preserved across re-solve) per showcase-resume-target-classification.

forgotten 2026-08-05T04:34:40Z — Harness-detail memory for one demo script; numbers/framing already baked into the reviewed draft docs/blog/rust_symex_showcase.md. Relocate detail as comments in tests/benchmarks/show_checkpoint_resume.py if needed.

showcase-multiarch-parity-demo forgotten

show_multiarch.py (tests/benchmarks/) is the multi-arch PARITY showcase demo (angr-4n26m.6). Runs the SAME crackme (symbolic input==42 reaches success block) across all 6 Supported arches: AMD64/X86 (blob, 2eax+16==100), ARMEB (BE blob, 2r0+16==100), AArch64 (inline ELF, calls double_it then ==84), MIPS32-LE/MIPS64-LE (inline ELF, ADDIU t0,42 + BEQ). All build in-memory (no external binaries / cross-compiler) — byte encodings lifted verbatim from CI-verified tests in tests/engines/rust/test_multiarch.py. Each run verifies found + solver.eval(sym)==42. Framing: breadth/correctness ONLY, NOT speed (no Python baseline; the one real non-x86 ELF bench android_arm_license_validation is 0.8x slower). Self-caps RLIMIT_AS=3GB; run as subprocess. Durable artifact tests/benchmarks/multiarch_numbers.json for the .11 blog. --quick runs AMD64/AARCH64/ARMEB subset.

forgotten 2026-08-05T04:34:40Z — Harness-detail memory for one demo script; superseded by the blog draft. Relocate as comments in tests/benchmarks/show_multiarch.py if needed.

showcase-number-gate forgotten

Showcase number gate (angr-4n26m.3): tests/benchmarks/measure_showcase.py runs run_single.py --both N>=5 per target, parses 'OK s' lines (regex _OK_RE), reports median+[min,max] per engine + speedup_median=py.median/rust.median. Durable artifact = tests/benchmarks/showcase_numbers.json (consumed by .5/.6/.7/.11). Fresh 2026-06-19 medians, tight ranges: defcamp_r100 3.82x, sharif7_rev50 3.94x, ais3_crackme 3.06x, fauxware 1.74x; busybox_static 0.33x (60-step entry_state breadth demo, ~0.9s PyO3 init tax dominates so Rust LOSES by design - NOT a raw-speed number). BIMODAL set (sokohashv2/fairlight/angry-reverser/unbreakable_1/CADET_00001_partial) is flagged by the harness and excluded from default; heavy winners ekopartyctf2016_rev250(~32s/run)/flareon2015_5(~57s/run) excluded for OOM/time, measure on demand with small -n. NEVER quote baseline_timings.json (stale by design).

forgotten 2026-08-05T04:34:40Z — Harness-detail memory with numbers that will need refreshing anyway; superseded by the blog draft. Relocate as comments in tests/benchmarks/measure_showcase.py if needed.

showcase-raw-speed-harness forgotten

Showcase raw-speed harness: tests/benchmarks/show_raw_speed.py builds a reproducible before(python)/after(rust) speed table, reusing measure_showcase.measure() (the angr-4n26m.3 number gate) verbatim — no duplicate measurement logic, no baseline_timings.json quoting. Three tiers via _targets(): headline (ekopartyctf2016_rev250, flareon2015_5 — heavy CTF, ~9min for full N=5, excluded from --quick), fast (sharif7_rev50/defcamp_r100/ais3_crackme — the --quick set), realsoftware (busybox_static — HONESTY row, init-tax-dominated so Rust LOSES ~0.33x, in NOT_A_SPEEDUP, never quote as a win). --json refreshes durable artifact raw_speed_numbers.json (consumed by blog task .11). Fresh N=5 medians 2026-06-19: eko 14.68x, flareon 7.38x, sharif7 4.00x, defcamp 3.79x, ais3 3.06x. Note flareon re-measured at 7.38x (not the ~10x task-desc estimate) and 25.7s python (not ~57s) — the gate corrects stale estimates, which is the point.

forgotten 2026-08-05T04:34:40Z — Harness-detail memory with numbers already reproduced in the blog draft (docs/blog/rust_symex_showcase.md matches within rounding). Relocate as comments in tests/benchmarks/show_raw_speed.py if needed.

showcase-resume-target-classification forgotten

Checkpoint/resume demo (angr-4n26m.8) target classification, from angr-4n26m.4 probe tests/benchmarks/show_resume_boundary.py (commit 2b84b0b71). fauxware find=0x4006ed is VALUE-REPRODUCIBLE: across N=5 fresh explores in BOTH deterministic and nondeterministic modes the found posix.dumps(0) is identical (the SOSNEAKY backdoor, hex ...534f534e45414b59..), and a dump_snapshot/load_from_disk round-trip reproduces the same bytes. Found-path stdin AST is already CONCRETE (posix.stdin.content[0][0] is non-symbolic). BUT this is NOT global constraint-uniqueness: 0x4006ed is reachable by other inputs (matching creds path) too. So value-reproduction rests on Z3 consistently LANDING on the same path, not on uniqueness. CONSEQUENCE for .8: frame resume as SEARCH CONTINUITY (reaches the same point); value-reproduction is incidental/empirical, not guaranteed (rust_engine.rst: model equality NOT guaranteed across restore; deterministic=True narrows not closes). deterministic=True is safe but unnecessary for this target. SimPacketsStream has no .load — use .content list of (ast,size) or .read (returns >2-tuple).

forgotten 2026-08-05T04:34:40Z — Harness-detail memory for one demo probe; framing already incorporated into showcase-checkpoint-resume-demo/blog draft. Relocate as comments in tests/benchmarks/show_resume_boundary.py if needed.

showcase-runner-pythonpath forgotten

Showcase demos (tests/benchmarks/show_*.py) cannot be run as a bare 'python show_X.py' — angr is an editable/namespace package resolved off the repo root, NOT in site-packages, so a script's sys.path[0]=script-dir misses it. They need PYTHONPATH= (matches the documented 'env PYTHONPATH=$PWD .venv/bin/python' invocation). The single runner show_all.py (struct DEMOS registry, fn run_demo) injects this. Exit-code contract shared by every demo main(): 0=pass, 2=corpus-absent (SKIP, not fail), other=FAIL. Nightly job showcase_smoke runs 'show_all.py --quick'.

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

showcase-solver-instrumentation-demo forgotten

Showcase solver-instrumentation demo (angr-4n26m.9) = tests/benchmarks/show_solver_instrumentation.py. Brackets explore() with mgr.reset_solver_stats()/get_solver_stats() to surface per-call-site Z3 attribution + lazy-fork materialization counts (the 'look inside the solver' angle). KEY GOTCHA: get_solver_stats() returns the FULL process-wide counter dict (bvop_, lineage_, etc.), not just z3_* — the human report filters to z3_check_, z3_site_{count,time_ns} (sites in SITE_ORDER), z3_materialize, z3_assume_, z3_branch_model_. On defcamp_r100 N=1: 25 check() calls, 43ms (~35% of 0.125s search), satisfiable site=98% of check time, 13 lazy-fork materializations. The final-solution eval() happens AFTER the stats window closes (posix.dumps after get_solver_stats), so eval site shows 0 — window covers explore() only, by design. Self-caps RLIMIT_AS=3GB; run via env PYTHONPATH=$PWD. Durable artifact solver_instrumentation_numbers.json.

forgotten 2026-08-05T04:34:40Z — Harness-detail memory for one demo script; superseded by the blog draft. Relocate as comments in tests/benchmarks/show_solver_instrumentation.py if needed.

simoption-wiring-template remembered

SimOption wiring template (angr-zmha, follows angr-yl5n NO_IP_CONCRETIZATION pattern): for any per-state SimOption that gates a Rust execution behavior, the wiring is 8 sites — (1) field on RustSimState in state.rs (default false), (2) constructor inits (new_with_endian, from_vex_arch, with_solver_endian), (3) all fork/merge clone sites (fork, fork_true, fork_false, fork_from_snapshot, merge), (4) set_/getter pair, (5) PyO3 #[pyo3(name=...)] accessors, (6) VEXInterpreter field + with_config init + fork-clone in interpreter/mod.rs, (7) propagation in exploration/stepping.rs run_interpreter_step (after state-handle resolution), (8) state_api.rs state getter + exploration/mod.rs PyO3 export. Plus rust_manager.py: _add_rust_state to set the flag, _apply_state_metadata allow-list to mirror on cache reuse. ~150 lines total per option. Always test with both propagate test (option-to-flag wiring) AND end-to-end test (shellcode where Python and Rust would diverge without the wiring).

simproc-fallback-statecreate-bottleneck forgotten

csaw_wyvern Rust bottleneck is per-fallback Python SimState materialization, NOT the SimProcedure body. Measured 2026-06-15: rust wall=2.68s, time_in_callbacks=2.073s (77%), callback_simprocedure_state_create_ns=1.98s vs execute_ns=0.064s. ~74% of wall is creating a Python SimState for 30 SimProcedure fallbacks (~66ms each). The xtse.1 hypothesis ('2s inside the body') was WRONG. Of the 30 fallbacks ~24 are angr.procedures.stubs.ReturnUnconstrained (operator new/delete, ostream<<, std::string/allocator ctors/dtors). DEAD-END (angr-8mjd iter46, reverted): a native ReturnUnconstrained fast-path writing a FRESH symbolic BV regressed csaw_wyvern 2.7s->86s. ROOT CAUSE NOW CONFIRMED (iter74) — see native-returnunconstrained-bvv0-gate: Python's solver.Unconstrained returns concrete BVV(0) unless SYMBOLIC_INITIAL_VALUES is set (off by default), so Python writes null not a symbol; the native path minted symbols -> downstream null-checks/address-uses fork and pay concretization -> explosion. CORRECTED RECIPE: gate the native fast-path on SYMBOLIC_INITIAL_VALUES and write BVV(0) when absent. Not a dead-end anymore.

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

simprocedure-bounce-service-dominates remembered

The simprocedure bounce is dominated by the SERVICE, not the proc body (angr-gorvf.9, 2026-07-14, HEAD 5f8d936e4). Steady-state split over 49 non-first crossings (fauxware/google2016_unbreakable_0/csgames2018/csaw_wyvern): state_create 65.6%, execute (the Python SimProcedure body) 29.5%, sync_back 4.7%. Inside state_create the top component is sc_memreplay = _replay_rust_dirty_pages (360ms of 542ms; ~10.8ms/crossing on csaw_wyvern), then sc_meminstall (_install_rust_memory_proxy). CONSEQUENCE: growing native/angr/src/procedures/ so fewer procs decline to Python does NOT buy back the 1639ms GIL class — it attacks the 30%. Same shape as angr-gorvf.7 (export dwarfed the store count) and the same root as angr-gorvf.2 (lazy/delta SimState views). Live lever bead: angr-gorvf.10.

SCOPE LIMIT (added iter143 — read before using this to kill work): the claim above is about NS / THROUGHPUT ONLY. It does NOT apply to the ZeroPy gate, whose predicate is gil_work_time_ns == 0 — a COUNT target, where eliminating one crossing entirely flips a bench to PASS and the service/body split is irrelevant. This memory has been misread as 'native procs are pointless' at least twice. See invariant-zeropy-gate-levers-are-count-not-ns; the count-driven levers are beads angr-gorvf.12 / .13.

simprocedure-dispatch-priority remembered

SimProcedure dispatch chain (angr-o0vm, 2026-06-01): native vs Python is decided once per dispatch, never chained except on native error. Order: (1) is_in_binary check in run_loop.rs:~226 / stepping.rs:~586 — user-placed hooks at binary addresses always go Python; (2) NativeProcedureRegistry::get() returns None if globally disabled, per-name disabled, OR has python_override set (any of these route to Python); (3) if native ran and returned Err, fall back to Python (counted in native_proc_stats.python_fallbacks, bucketed by error variant); (4) if no native registered for the name, route to Python (counted in simprocedure_python_fallback_count — DISTINCT counter from native_proc_stats). set_python_override and disable have identical runtime effect; differ only in intent. Override is callable from Python via mgr.set_python_override(name). Regression test: tests/engines/rust/test_manager_core.py::test_python_override_bypasses_native_strlen.

sla-regression-flags forgotten

tests/benchmarks/run_regression.py SLA flags: --sla-warn-threshold (default 1.0x = warn), --sla-fail-threshold (default 0.5x = fail with exit 1), --no-sla disables entirely. Uses py_time from current run, else baseline_timings.json[entry].python_time. To populate python_time for rust_only entries (most of FAST_SUITE), the user must run Python alongside Rust manually since rust_only skips Python in run_regression.py. python_time is now preserved across --update runs (was previously clobbered to None).

forgotten 2026-08-05T04:34:40Z — Narrow CLI-flag documentation for tests/benchmarks/run_regression.py, already self-documented via its own argparse help/docstring; relocate any missing detail as a comment there.

smc-python-lift-bytes-channel forgotten

End-to-end SMC support on the Rust engine has TWO halves: (1) cache invalidation + dirty-page tracking on stores (closed by angr-k67f, commit 3d632bb2b), and (2) lift-side fresh-bytes channel (closed by angr-kwwd, commit 3b0e7690d). The (2) half threads bytes from rust_memory.read_concrete_bytes_for_lift through call_lift_block as a 3rd positional arg into _cb_lift_block, which uses them as factory.block(addr, byte_string=dirty_bytes). Without (2), default builds (no native-lift feature) executed stale cle-binary bytes after a Rust-side store overwrote in-binary code, because backup_state-aware lifting can't see Rust stores until end-of-step _sync_rust_memory_to_state. With both halves landed, default builds now handle SMC binaries (packers, anti-debug, JIT). Native-lift builds had the analogous fast path already (execution.rs:353).

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

smc-rust-engine-architecture forgotten

Self-modifying code on the Rust engine has a multi-part story. The cache invalidation + dirty-page tracking (closed 2026-05-09 by angr-k67f, commit 3d632bb2b) handles the FIRST half: stores to in-binary addresses mark page-numbers (addr >> 12) in CallbackInterpreter.dirtied_code_pages and remove cached IRSBs whose [block_addr, block_addr + irsb.size()) range overlaps the write. invalidate_code_at + is_code_range_dirtied are the public API. update_prefetch_on_store hooks both Rust-native and Python-fallback store paths. SECOND half (followup angr-kwwd) is the lift-side: _cb_lift_block calls project.factory.block(addr), which reads from cle's immutable static binary, not from rust_memory or state.memory. backup_state alone doesn't help because Rust-native stores don't sync to Python state.memory until end-of-step. The native-lift feature has a working SMC fast path (reads from rust_memory.read_concrete_bytes_for_lift for dirty pages) but native-lift is OPT-IN and not in default features. So in default builds, SMC binaries still execute stale bytes.

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

smtlib2-round-trip-floor forgotten

Z3_solver_to_string + Solver::from_string round-trip via SMT-LIB2 has a ~5ms parser-warmup floor regardless of size (measured at 5852us for 5 assertions vs 4812us for 95 assertions — small case is slower because parser setup dominates). Size scales linearly at ~65 bytes/assertion. Verdict: fine for save/restore-on-disk; bad for hot-path serialization. See angr-9o4n.1 + commit 64731bb65.

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

smxp-slice-1-dfs-length-limiter-recipe forgotten

angr-smxp slice 1 (2026-06-06, commit 873a6f549): DFS + LengthLimiter recipe for grub-class deep-input-loop binaries. The infrastructure was already in place — exploration_strategy='dfs' kwarg in RustExplorationManager.init (rust_manager.py:865) routes through set_exploration_strategy → set_state_selection_lifo (Rust FFI), and LengthLimiter technique registers natively via register_length_limiter (rust_techniques.py:254). The slice 1 deliverable was the composition guidance: docs section in rust_engine.rst (Memory pressure section: 'Deep-input-loop binaries (grub-class)') with the angr-xel4 spike table and the recommended recipe, plus regression test TestExplorationStrategy.test_deep_loop_recipe_dfs_plus_length_limiter pinning that DFS + LengthLimiter compose without overriding each other (max_length=32 respected on fauxware active stash). Real grub measurement deferred — needs >4GB RLIMIT_AS. Heuristic auto-detection (option b) and one-call helper (option c) also deferred.

forgotten 2026-08-05T04:34:40Z — dup_of_docs confirmed: docs/advanced-topics/rust_engine.rst 'Deep-input-loop binaries (grub-class)' section documents the DFS + LengthLimiter recipe verbatim.

snapshot-bucket-d-overlay-loss forgotten

angr-op0dn.13.14 root cause #1 (FIXED, 2026-07-13): the snapshot envelope dropped the bucket-D per-state Python-AST overlays. RustSimState::from_snapshot sets symbolic_pages / hook_symbolic_memory / addr_to_ast to EMPTY (they are Py, not serde-able), and the old dump_snapshot docstring waved it off with 'the Rust SimProcedures keep those overlays empty' — true for fauxware, FALSE for any state that bounced through a Python SimProcedure. Measured on fork_solve_pbounce_W3_S2_M8_B1: every active state carries 45 addr_to_ast + 45 symbolic_pages entries at dump time; all were silently discarded, so the resumed state read those addresses as unconstrained. Fix: RustExplorationManager.dump_snapshot writes an outer envelope (_SNAPSHOT_MAGIC + u64 rust-blob len + pickled overlays); capture_bucket_d / restore_bucket_d round-trip them through the existing get_state*/set_state* FFI (manager_methods.rs). Legacy bare-Rust files still load. Pinned by test_snapshot_round_trips_bucket_d_overlays in tests/engines/rust/test_parallel_wave.py. IMPORTANT: this is necessary but NOT sufficient — the serial resume still drains only 6/8 leaves with overlays intact, so the deferred-fork loss has a second independent cause.

forgotten 2026-08-05T04:34:40Z — Superseded: this fixed root-cause (#1 of a since-closed multi-bug saga) is already recapped by snapshot-resume-leaf-loss-root-cause's 'three real but independent defects... fixed along the way' summary.

snapshot-constraint-count-pin remembered

Snapshot round-trip constraint_count pin (angr-kenpr, 2026-07-04). SymContextSnapshot now carries constraint_count (serde(default)=0). SymContext::to_snapshot captures self.num_constraints(); restore_from_snapshot pins it via set_constraint_count() (bv_id_ops.rs) AFTER the assume-class IR replay, only when snap.constraint_count>0 (legacy snapshots read 0 -> skip pin, keep replayed count). WHY: t3l5o Phase-1 rebuilds the assume class from the assumed LOG, but that log preserves entries that were live-deduped away on the SOURCE solver (ptr-dedup in check_z3_dedup_if_seeded fires against a since-freed Z3 AST whose ptr is not reproduced under fresh restore ptrs). Those entries re-assert on restore -> constraint_count inflates (fauxware step(n=10): active state 0 [3,1]->[4,1]). The pin restores the pre-Phase-1 round-trip contract cheaply. CAVEAT: the restored solver still holds one extra (logically-redundant) Z3 assertion; only the COUNT is pinned, not the solver assertion set. Deeper open question tracked separately: is the SOURCE-side assume dedup dropping an INTENDED constraint (stale-ptr false positive)? See snapshot-solver-smtlib2-design (old angr-82g6 full-dump design), avoid-add-constraint-for-context-rebuild.

snapshot-foreign-id-rebase remembered

angr-euw28: symbol-id aliasing across PROCESSES (not contexts) is fixed by rebasing the id space at snapshot load, not by remapping the registry. Ids are process-global (NEXT_SYMBOL_ID in symbolic/bv_id_ops.rs), so a foreign envelope's ids (allocator also started at 0) alias ids the loading process already minted. stash.rs dump_snapshot now writes [version u8][process_token u64 LE][json] (STASH_SNAPSHOT_VERSION=2, process_token() = OnceLock pid+startup-nanos); load_snapshot compares the token BEFORE deserializing and, when foreign, activates a thread-local SymbolIdRebase guard (symbolic/bv_id_ops.rs) whose offset is added to every id in From for RustBV (value.rs::rebase_id, skips the EXPRESSION_ID=u64::MAX sentinel). restore_from_snapshot reserves past next_id+offset. Names are NOT touched (Z3 identity is by name), so replayed constraints still bind. In-process loads (worker migration payloads, same-process round-trip) see offset 0 and are byte-identical to before. Regression test: stash_tests.rs::test_foreign_envelope_rebases_symbol_ids (flips the token byte to fake a foreign writer).

snapshot-path-loss-debug-method forgotten

DEBUG METHOD for 'snapshot restores structurally but loses paths' (angr-op0dn.13.10): the structural fingerprint (stash_counts + sorted Rust PCs) can MATCH while the snapshot still drops states, because states parked outside every stash are invisible to BOTH sides of the comparison. Do not trust it as a completeness check. What actually localized the bug: (1) a phase-marked repro (log markers through the angr logger so they interleave with the WARNINGs — plain stderr prints do NOT order correctly against logging) showed every 'condition N not found in stored_conditions' and 'Lift error at 0x0' fired in the RESUME phase, not the parallel explore; (2) setting mgr._phase2_retried=True to suppress the angr-027h phase-2 eager retry (_maybe_phase2_eager_retry, rust_manager.py) — WITHOUT that, phase 2 re-seeds _initial_seed_states, which after a load_snapshot is the THROWAWAY blank state the manager was constructed with, masking the real loss and inflating/deflating the found count run-to-run; (3) get_state_bbl_history_tail(sid, n) to show the frontier state's history (hook addr then 0x0 = a bounce state whose pc the worker zeroed). NOTE for any post-load_snapshot test: phase-2 eager retry is semantically wrong on a restored manager (it discards the restored frontier and restarts from the constructor's seed) — see angr-op0dn.13.12.

forgotten 2026-08-05T04:34:40Z — Debugging narrative pinned to one now-closed bug hunt (angr-op0dn.13.10/.13.12); the underlying bug is fixed and pinned by regression tests, so this is an iteration receipt rather than a durable rule.

snapshot-py-wrapper-shape forgotten

Snapshot Python wrapper API (angr-x04s.1.4): RustExplorationManager.dump_snapshot(path) / load_snapshot(path). Bytes-level PyO3 bindings (dump_snapshot_bytes() returns PyBytes, load_snapshot_bytes(bytes) takes &[u8]) live on the Rust pyclass; Python wrapper handles file I/O. load_snapshot replaces self.sm wholesale via StashManager::load_snapshot, then invalidates the state-export cache; manager-level config (find/avoid addrs, hooks, simprocedures, solver/memory config) is PRESERVED. Empty envelope and stale version byte both surface as ValueError on Python side ('empty snapshot envelope' / 'version mismatch'). Tests: TestSnapshotRoundTrip in tests/engines/test_rust_exploration.py (3 cases — fauxware structural round-trip + 2 error paths). Round-trip preserves stash_counts, state_ids per stash, per-state pc — does NOT preserve full num_constraints count (see snapshot-add-constraint-raw-not-tracked). 677/677 pytest, 16/16 bench gate green at commit.

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

snapshot-redump-fixed-point-test remembered

Snapshot round-trip fidelity has an exact test: dump, load into a fresh RustExplorationManager, then dump AGAIN and compare the two envelopes BYTE-FOR-BYTE. For the pbounce frontier this is an exact fixed point (2905657 bytes, identical), which proves the Rust state graph survives the round trip intact and exonerates the envelope for any resume-behavior bug. Use this before theorizing about lost states/leaves/constraints. Corollary and pitfall: the Python state proxy (s.addr, s.regs.*, s.memory.load) is NOT ground truth for this comparison — its export of even a LIVE manager can disagree with the envelope (stale entries in the manager's state-export cache showed a duplicate pc where the envelope had two distinct ones), which is what made angr-op0dn.13.14 iter 37 misread an export artifact ('32 distinct claripy vars -> 1') as symbol-identity aliasing. Trust the re-dump, not the proxy.

snapshot-resume-cache-dependence forgotten

angr-op0dn.13.14: the serial snapshot-resume leaf loss (4 of 8 pbounce leaves) is NOT in the snapshot data — dump-load-dump is byte-identical. It tracks process-global/thread-local claripy-bridge cache state that the envelope cannot carry: with the SAME snapshot bytes, a resume yields 4/8 when the states were never exported before the dump and 6/8 when every active state was exported through the proxy first. Suspect the global SymbolicIdentityRegistry + CLARIPY_AST_CACHE: an unregistered restored leaf that bounces through a Python SimProcedure is re-imported as a BRAND NEW rust symbol carrying none of its path constraints (see the angr-izov2 comment in claripy_bridge/export.rs::rustbv_to_claripy_memo) — a silently lost fork. Fix direction: load_snapshot should re-register every restored leaf (id, name, width) in the global registry. Do NOT re-chase envelope-data theories (missing pending callbacks, dropped Py overlays, unserialized next_id) — all three are measured and refuted.

forgotten 2026-07-20T05:01:50.714575+00:00 — Superseded in-cluster: its registry/CLARIPY-cache theory was killed by falsified-theories item 3 (warm same-process resume fails identically) and the real cause is the phase-2 accounting in snapshot-resume-leaf-loss-root-cause; bead angr-op0dn.13.14 closed

snapshot-resume-leaf-loss-falsified-theories forgotten

snapshot-resume-leaf-loss FALSIFIED THEORIES (angr-op0dn.13.14, iters 40-43). Five theories were killed by experiment before the real cause was found — do not re-chase them: (1) solver identity (each restored state gets its own SymContext vs live siblings sharing one via fork) — not load-bearing; (2) the Python SimProcedure bounce — a hook-free repro fails identically; (3) anything process-global (counters/registries) — a WARM same-process resume fails identically; (4) seed-symbol id collision — a seed state that mints no symbols yields the same leaf count; (5) state-id collision on re-mint — fixed in 3ab14cfce, leaf count unmoved. Three real but independent defects WERE found and fixed along the way (bucket-D overlay drop 22c2ba311, SymContext::next_id 13c50741c, NEXT_STATE_ID restore epoch 3ab14cfce) — none of them moved the leaf count either, which is the tell that the framing was wrong. The actual cause is the phase-2 eager retry: see [[snapshot-resume-leaf-loss-root-cause]]. LESSON: when three consecutive 'fixes' each close a real correctness gap but leave the headline metric unmoved, stop fixing and go measure the metric's provenance — here, diffing mgr.stats counters between the live and resumed runs found it in one shot.

forgotten 2026-07-20T05:01:51.223519+00:00 — Same investigation as snapshot-resume-leaf-loss-root-cause (cross-cites it); theory list + lesson folded into canonical (merged into snapshot-resume-leaf-loss-root-cause)

snapshot-resume-leaf-loss-root-cause remembered

snapshot-resume-leaf-loss ROOT CAUSE (angr-op0dn.13.14, iter 43): the 'snapshot resume drains only 6 of 8 leaves' symptom is NOT snapshot data loss. Snapshot fidelity is proven: a warm dump/restore differential shows restored active states are identical to live ones on pc, IP, all GP registers (incl. symbolic chains), symbolic memory objects, bucket-D overlays, constraint sets and satisfiable(). The gap is the angr-027h TWO-PHASE explore: RustExplorationManager::_maybe_phase2_eager_retry (angr/exploration/rust_manager.py) fires when phase 1 (deferred forks) exhausts below num_find, flips set_use_deferred_forks(False), and RE-SEEDS _initial_seed_states, re-exploring the whole tree from scratch in eager mode. On fork_solve_pbounce_W3_S2_M8_B1 that yields found=12 for an 8-leaf binary (only 9 distinct stdin solutions) — phase-1's 6 unique leaves plus duplicates from the re-run. A resumed manager has phase 2 disabled (load_snapshot sets _phase2_retried=True, commit 05e9c8abc) so it drains exactly the deferred ceiling of 6. DIAGNOSTIC SIGNATURE: callback_symbolic_branch_count in mgr.stats is nonzero ONLY in eager mode (statements.rs IRStmt::Exit bounces to Python only under !use_deferred_forks) — live=15 vs resume=0 is what identified this. COROLLARY: a 'found' count from a manager that ran phase 2 is inflated by duplicate paths; dedupe by solved input before comparing leaf counts across engines or across a snapshot boundary. FALSIFIED THEORIES (iters 40-43), each killed by experiment — do not re-chase: (1) solver identity (each restored state gets its own SymContext vs live siblings sharing one via fork) — not load-bearing; (2) the Python SimProcedure bounce — a hook-free repro fails identically; (3) anything process-global (counters/registries) — a WARM same-process resume fails identically; (4) seed-symbol id collision — a seed state that mints no symbols yields the same leaf count; (5) state-id collision on re-mint — fixed in 3ab14cfce, leaf count unmoved. Three real but independent defects WERE found and fixed along the way (bucket-D overlay drop 22c2ba311, SymContext::next_id 13c50741c, NEXT_STATE_ID restore epoch 3ab14cfce) — none of them moved the leaf count either, which is the tell that the framing was wrong. LESSON: when three consecutive 'fixes' each close a real correctness gap but leave the headline metric unmoved, stop fixing and go measure the metric's provenance — here, diffing mgr.stats counters between the live and resumed runs found it in one shot.

snapshot-resume-spread-is-dump-side remembered

Parallel snapshot-resume leaf-count spread (angr-op0dn.13.13) is DUMP-side, not resume-side. Measured on the pbounce W=3 synthetic (tests/engines/rust/test_parallel_wave.py::TestParallelCheckpointFrontier): a parallel num_find=1 explore trips the CancelToken in whichever worker reaches the target first and the other workers stop at their own task boundaries, so the residual frontier drained back to STASH_ACTIVE — the thing dump_snapshot captures — differs run to run (pre_active 7 serial vs 8 at workers=4). A wider residual resumes into more leaves. HOLD THE SNAPSHOT BYTES FIXED and the picture changes completely: serial resume is bit-stable at 4 leaves; workers=4 gives 4-6, i.e. it varies UPWARD, never down, so no restored subtree is lost. Also falsified: the bead's own note that the spread was the angr-027h phase-2 eager-retry artifact — callback_symbolic_branch_count is 0 on EVERY resumed run (phase 2 is retired on a resumed manager since .13.12), so phase 2 cannot be the cause. Method that isolates dump-side from resume-side variance: dump ONCE serially, then resume N times varying only RUST_PARALLEL_WORKERS. Do not compare a workers=1 run against a workers=4 run end-to-end — the dump points are not the same experiment. Open follow-up: angr-op0dn.13.15 (which arm's drain depth is right; parallel also ends with deadended=0-1 vs serial's 8 from the identical frontier).

snapshot-resume-stdin-solve-root-cause remembered

Snapshot resume: the REAL angr-op0dn.13.14 defect was not lost leaves — it was that resumed found-states could not be solved for input. mgr.found[i].posix.dumps(0) returned b"" on a resumed manager, and once a posix was grafted on, the SAME all-zero bytes for every leaf (distinct_stdin=1 vs 9 live). Two stacked causes: (1) a restored state materializes from a bare Rust export, so its posix is angr's DEFAULT EMPTY one — the snapshot carries registers/memory/constraints but no Python plugins; (2) the restored constraints are phrased over the ORIGINAL manager's harness-seeded stdin BVS (stdin_0_256), while the resumed manager's own seed carries a freshly-minted claripy symbol with a different name — so dumps(0) solved an unconstrained variable. Fix (commit ed3b3544b, Python-only): dump_snapshot pickles the seed's posix.stdin.content byte ASTs into the envelope under reserved key _SEED_STDIN_KEY=-1 (real keys are u64 state ids, so a negative key cannot collide and pre-existing envelopes still load); load_snapshot installs them as self._stdin_content. Claripy pickling preserves names, so restored content and restored constraints name the same symbol again. Symbols: RustExplorationManager::_capture_seed_stdin_content / dump_snapshot / load_snapshot / _restore_bucket_d in angr/exploration/rust_manager.py. Regression test: TestParallelCheckpointFrontier::test_resumed_found_states_solve_to_distinct_stdin.

snapshot-rustbv-shadow-type-pattern remembered

RustBV serde shadow type pattern (angr-x04s.1.1, 2026-06-02). The Z3 AST cache on RustBV::Symbolic is non-serializable (feature-gated, raw Z3 pointer). Solution: define RustBVData enum mirroring RustBV without the ast field, use #[serde(from = "RustBVData", into = "RustBVData")] on RustBV. On deserialize, RustBV::Symbolic::ast is rebuilt via BV::new_const(name, width) inside the active thread-local Z3 context — callers MUST run deserialization inside with_z3_context (or equivalent) when vex-engine-z3 is on. Shadow type also handles Arc -> String and Arc<[RustBV]> -> Vec collapse. Identity-cache invariants (EXPRESSION_BY_OPERANDS_PTR keyed on Arc::as_ptr) are NOT part of the wire format; caches rewarm naturally as loaded ops are touched. See native/angr/src/symbolic/value.rs RustBVData + From impls.

The same shadow-type pattern (define a Data mirror struct/enum, derive From/Into, #[serde(from='Data', into='Data')]) was applied to RegisterFile + MemoryPage (angr-x04s.1.2). Collapses there: Arc<Vec> -> Vec, Box<[u64; BITMAP_WORDS]> -> Vec (malformed-length -> None fallback), FxHashMap -> BTreeMap (deterministic ordering + serde-friendly), Box -> arch_name String rebuilt via arch_from_name with AMD64 fallback. RegisterFile needs a Clone derive too (serde-into consumes the value). SymContextSnapshot mirrors get_assumed_constraints() and restore_from_snapshot() replays the (RustBV, bool) log via assume_true/assume_false; Z3 solver, sat_cache, model_cache, scope_path and push stacks all rederive from that canonical log rather than being serialized directly. ScopePath itself is NOT serialized: lineage is opt-in, scope frames hold non-serializable z3::ast::Bool, and the path rebuilds from constraints once a fresh lineage is installed. See native/angr/src/arch/mod.rs RegisterFileData, native/angr/src/memory/page.rs MemoryPageData, native/angr/src/symbolic/context.rs SymContextSnapshot.

snapshot-rustsimstate-envelope-shape remembered

RustSimState snapshot envelope shape (angr-x04s.1.3, 2026-06-02). Format: [VERSION:u8] ++ serde_json(RustSimStateSnapshot). SNAPSHOT_VERSION=1 for per-state, STASH_SNAPSHOT_VERSION=1 for StashManager (independent so each can evolve). Bucket D Py handles (symbolic_pages, hook_symbolic_memory, addr_to_ast, last_time) restore to empty — Python claripy bridge ships in .1.4. SymbolicMemorySnapshot defers multi_objects and pending_writes — they restore empty; callers mid-Multi/Pending must flush_multi_cells + drain before snapshot or accept the lazy queue drop. symbolic_spans reverse index rebuilds from symbolic_objects at restore (per-byte entries keyed by base+offset). state_index on StashManager rebuilds from dumped stash contents so stash_of stays consistent.

snapshot-serialization-design forgotten

RustSimState snapshot/serialization route (angr-x04s spike, 2026-06-01): RustBV op-tree serialization is the right primitive. The enum (Concrete/Symbolic/Constrained/Expression at native/angr/src/symbolic/value.rs:400) is fully serializable without touching Z3 — Symbolic variant's cached Z3 AST drops out, rebuilds lazily on first to_z3_ast() after load via BV::new_const(name, width). Constraint store (SymContext::assumed_constraints, Vec) follows the same route. SMT-LIB2 round-trip is the alternative (~5ms parser floor, validated cross-context in angr-9o4n.1 + angr-rwzi) but redundant for in-process snapshots. Concrete buckets in RustSimState (state.rs:689): (1) plain data fields trivially serde; (2) RegisterFile/SymbolicMemory concrete halves are bytes + symbolic overlays of RustBV; (3) Arc<HashSet/HashMap> fields (hooks, environment) collapse to owned on snapshot; (4) Py fields (symbolic_pages, hook_symbolic_memory, addr_to_ast) require Python claripy.dumps/loads — soft dependency on live Python at load time. Follow-up bead angr-x04s.1 filed for fauxware prototype.

forgotten 2026-06-04T16:32:00.322315+00:00 — Bead/commit closure note

snapshot-solver-smtlib2-design remembered

Snapshot constraint capture design (angr-82g6, 2026-06-02): SymContextSnapshot carries solver_smtlib2: String (full Z3 solver dump via Solver::to_string + format!('{}', tmp)) alongside assumed_constraints: Vec<(RustBV, bool)>. to_snapshot dumps both. restore_from_snapshot: when solver_smtlib2 non-empty, parses via Solver::from_string + get_assertions, walks each Bool, calls add_constraint_raw for each — rebuilds Z3 solver, z3_assertions log, and constraint_count atomically. Then walks assumed_constraints and calls assumed_constraints_push for each (only updates local.assumed, no second solver assert). This separation is what makes it work — the BV log and the Z3 log are restored independently. Field is #[serde(default)] for back-compat with legacy snapshots. Verified end-to-end: state_constraint_count, assumed_constraint_count BOTH round-trip on fauxware step(n=10).

snapshot-state-bucket-shadow-pattern forgotten

RegisterFile + MemoryPage serde shadow type pattern (angr-x04s.1.2, 2026-06-02). Same shape as the RustBV pattern: define ShadowTypeData mirror, derive From/Into, use #[serde(from='Data', into='Data')] on the original. Key collapses for this layer: Arc<Vec> -> Vec, Box<[u64; BITMAP_WORDS]> -> Vec (with malformed-length -> None fallback), FxHashMap -> BTreeMap (deterministic ordering + serde-friendly), Box -> arch_name String rebuilt via arch_from_name with AMD64 fallback. RegisterFile needs Clone derive too (serde-into consumes). SymContextSnapshot mirrors get_assumed_constraints() and restore_from_snapshot() replays via assume_true/assume_false — Z3 solver, sat_cache, model_cache, scope_path, push stacks all rederive from the canonical (RustBV, bool) log. ScopePath itself was NOT serialized: lineage is opt-in, scope frames hold non-serializable z3::ast::Bool, and the path rebuilds from constraints once a fresh lineage is installed. See native/angr/src/arch/mod.rs RegisterFileData, native/angr/src/memory/page.rs MemoryPageData, native/angr/src/symbolic/context.rs SymContextSnapshot.

forgotten 2026-08-05T04:34:40Z — Same serde shadow-type pattern applied to RegisterFile/MemoryPage; folded into the canonical pattern memory. (merged into snapshot-rustbv-shadow-type-pattern)

snapshot-to-angr-ast-recovery-order forgotten

RustStateExportMixin._recover_symbolic_ast lookup order (post-refactor 44ba4fa85): state_id -> parent_id (if >=0) -> root_id (from _state_roots cache, fall back to rust_mgr.get_state_root) -> hook_symbolic_memory[state_id]. ALL lookups require tracked_size == size. AST-only check: hook map is keyed ONLY by snapshot.state_id (not parent/root). The 4 original lookup blocks were near-identical except hook_mem keys only off state_id.

forgotten 2026-06-04T21:58:07.659709+00:00 — Implementation detail of one function; refactor receipt with commit hash

snapshot-v1-public-api forgotten

angr-9o4n (commit 778434d0a, 2026-06-03): snapshot codec was fully implemented but undocumented. The 'missing' v1.0 piece per bd description was actually just (1) a classmethod load_from_disk that doesn't need a placeholder state and (2) docs graduation — rust_engine.rst:2669 still claimed 'no built-in snapshot / restore primitive today' even though dump_snapshot/load_snapshot Python wrappers + STASH_SNAPSHOT_VERSION envelope + round-trip test had been in place for weeks. Lesson: when a bd task description predates intermediate work, verify what currently exists before designing the 'missing' piece. Existing API: dump_snapshot(path), load_snapshot(path), and now classmethod load_from_disk(path, project, **kwargs). Known caveats documented: bucket-D Py overlays not captured, manager config not captured (kwargs pass-through), model equality not guaranteed (Z3 nondeterminism).

forgotten 2026-06-04T16:32:00.664210+00:00 — Bead/commit closure note

sokohashv2-fv81-root-cause forgotten

angr-fv81 (sokohashv2 IndexError after angr-ctct fix) root cause: UltraPage.symbolic_data is a SortedDict keyed by the start offset of each symbolic region — a single 8-byte filler-materialised value stores ONE dict entry but marks 8 symbolic_bitmap bits. angr-ctct's initial fallback walked sd.keys() and extracted only that ONE byte per region; bytes 1..N silently collapsed to concrete zero on the Rust side. For sokohashv2 this turned a 15-term hash AST (using bits [15:0]/[31:16]/[47:32]/[63:48] of every 8-byte input) into a 4-term AST (low byte only) — explored states still reached to_find but the hash conjunction with WIN_HASH was unsat, so solver.eval_upto returned []. Fix walks each entry's contiguous bitmap extent (capped at 64 bytes/entry to skip 4 KB page-fillers that would otherwise tank ais3_crackme/google2016_unbreakable_0 by ~40%).

forgotten 2026-06-04T21:58:08.005813+00:00 — Single-bug detail subsumed by sokohashv2-two-bugs summary (merged into sokohashv2-two-bugs)

sokohashv2-no-transcendental-hits-2026-05-23 forgotten

Survey 2026-05-23 (angr-9l1y closure): counters dump on three sub-1.0x benches shows ZERO x87 transcendental hits — ekopartyctf2016_sokohashv2 (0.4x): vex_op_other=0, vex_op_fp=0, all python_vex_*_fallback_count=0. mma_howtouse (0.6x): vex_op_other=0. securityfest_fairlight (1.0x): vex_op_other=0. solve.py for sokohashv2 hooks every transcendental call site with do_nothing (lines 95-107), bypassing them in the symbolic path. Bottleneck on sokohashv2 is structurally Z3: z3_check_time_ns8.73s + z3_site_eval_upto_time_ns7.62s out of 10s walltime; rust_run_loop_time_ns144ms (Rust interpreter negligible). Implication: angr-9l1y (symbolic transcendental modeling) cannot move any current benchmark; the concrete fast paths in native/angr/src/vex/transcendentals.rs (angr-n28w, 238 LOC) remain useful only as fidelity scaffolding. Updated docs/advanced-topics/rust_engine.rst sokohashv2 section + CLAUDE.md table to reflect the new root cause attribution. Re-open the bead only if a new bench surfaces that actually invokes Iop_SinF64/CosF64/TanF64/AtanF64/Yl2xF64/Yl2xp1F64/ScaleF64/2xm1F64/RecpExpF{64,32} symbolically.

forgotten 2026-06-04T16:37:59.930650+00:00 — originally B4-c3 forget; deferred for citation. Citers now gone.

sokohashv2-rust-hash-divergence forgotten

Rust hash AST has only 4 terms using bits [7:0] of each 64-bit input — Python has 15+ terms covering all 16-bit halfwords of all 4 inputs. The do_repmovsd hook copies 32 bytes from esi to edi via memory.load+memory.store, but Rust callback round-trip preserves only the first byte of each 8-byte symbolic chunk; bytes 1-7 become concrete zero. Root cause likely in callback memory tracker or extraction picking only the symbolic_data SortedDict entries that started at the load-base address (since SortedDict[addr] is keyed by exact start address, not range).

forgotten 2026-06-04T21:58:08.350758+00:00 — Symptom diagnosis subsumed by sokohashv2-two-bugs (merged into sokohashv2-two-bugs)

sokohashv2-two-bugs forgotten

Sokohashv2 had TWO bugs: (1) angr-ctct FIXED 2026-05-14 commit c6b2824cb: _extract_symbolic_pages missed filler-materialised symbolic values. (2) angr-fv81 FIXED 2026-05-14 commit 13bb9f741: _extract_from_ultrapage fallback only extracted the head byte of each symbolic_data entry, dropping bytes 1..N as concrete zero, producing IndexError on sm.found[0]. Fix walks each entry's full bitmap extent (capped at 64 bytes). Verified 2026-06-01 (angr-y95g closure): rust engine succeeds in ~15s vs python ~6s — residual 0.4x gap is Z3 nondeterminism per BIMODAL_BENCHMARKS / benchmark-bimodal-variance-rules, NOT a remaining functional bug.

forgotten 2026-08-05T04:34:40Z — Fixed-in-commit narrative (both bugs fixed 2026-05-14, closure verified 2026-06-01) whose residual-variance pointer duplicates the mandatory-keep benchmark-bimodal-variance-rules memory.

solver-differential-fuzzer-mode remembered

property_fuzzer.py --mode solver (angr-ph300.4) is the solver-layer differential gate: _gen_solver_case builds random constraint sets over 8/16/32/64-bit claripy BVs and _solver_trial_body compares RustSolverContext vs claripy.Solver on satisfiable/min/max/eval_upto/eval_batch. Two design constraints future edits must keep: (1) eval is compared SEMANTICALLY (each engine's returned value must be accepted by the other via solution(); exact set equality only when BOTH returned fewer than n, i.e. exhaustive) -- requiring identical model choice would fail on benign Z3 nondeterminism; (2) eval_batch must be checked for JOINT satisfiability (fresh claripy Solver + constraints + v_i==val_i), because per-variable eval cannot see cross-variable model inconsistency. RustSolverContext is a pyclass and CANNOT be subclassed ('not an acceptable base type') -- to negative-control the harness, pass a delegating getattr wrapper as the RustSolverContext argument of _solver_trial_body. Baseline 2026-07-22: 300 trials, 0 divergences, ~43s, 67% sat / 33% unsat.

solver-equality-contradiction-detected forgotten

Characterization update (angr-e548): the Rust solver NOW correctly detects equality contradictions. RustSolverContext().add_constraint_ast(x==5) + add_constraint_ast(x==10) -> satisfiable() is False. Earlier docstrings claimed add_constraint_ast 'may not preserve == semantics' and could spuriously return SAT — that is stale; equality constraints route through Z3. test_solver_contradictory_constraints now asserts 'is False' to catch a regression back to spurious-SAT. See characterization-vs-fix-pattern.

forgotten 2026-08-05T04:34:40Z — Fixed-in-commit characterization update pointing to the mandatory-keep characterization-vs-fix-pattern memory; the fact itself is now just normal behavior pinned by test_solver_contradictory_constraints.

solver-fallback-architecture forgotten

Rust↔Python solver-fallback architecture. Canonical reference for how the Rust engine integrates with state.solver across export, fork, and eval.

Eval-time mechanism (canonical). RustSolverFallback monkey-patches state.solver.eval/min/max/satisfiable so post-exploration evals run against the Rust solver. NEVER reintroduce state.solver.add(ast == BVV(rust_eval)) patterns — they overconstrain and were the source of fauxware/flareon2015_5 wrong-answer regressions (revert 2f0e8164f; zombie deleted 70d84f909). See pre-pinning-dangerous and avoid-pre-pinning-recovered-symbols memories for the regression history. Constraint export uses Rust solver eval per constraint-export-no-pre-pin.

_attach_rust_solver_fallback callsites in rust_state_export.py (verified 2026-05-22, angr-7ulb). There are 5: lines 319, 358, 386, 414, 980. Only line 414 is redundant (_snapshot_to_angr already attaches at line 980; duplicate is no-op via _ATTACH_FLAG guard). Lines 358 and 386 look redundant against cached states but are NOT — self._state_cache[parent].copy() invokes SimSolver.copy() (state_plugins/solver.py:452) which creates a fresh _stored_solver via _solver.branch(); the new object does not inherit the prior setattr(solver, _ATTACH_FLAG, True) or the eval/min/max/satisfiable overrides (those were on the OLD object's __dict__). Re-attach IS required at 319/358/386/980. Removing 358 or 386 silently makes solver.eval() use Python-only constraints, regressing post-exploration eval perf and risking UNSAT on identity-mismatch cases.

Proxy semantics. RustStateProxy adds constraints to the fork solver only (not the parent). satisfiable() defers extras to Python. solver.eval fast paths that skip the solver entirely are dangerous — see avoid-concretize-fast-path-skips-solver (folded here).

Why: cluster of overlapping invariants was scattered across 5+ memories. How to apply: sole reference for any code review touching the proxy or solver-attach paths.

forgotten 2026-06-05T17:23:05.630083+00:00 — Mostly subsumed by mandatory-keep anchor constraint-export-no-pre-pin (per CLAUDE.md table). Detailed callsite enumeration (lines 319/358/386/414/980) belongs in code comments at rust_state_export.py, not bd. (merged into constraint-export-no-pre-pin)

solver-fallback-cache forgotten

State export fallback (eval_with_fallback, etc.) must cache the forked Rust solver. Each fork_state_solver() call costs ~3ms (Z3 solver clone). Before fix, every solver.eval() call on found states forked again. Fixed by caching in _cached_rust_ctx closure variable, fork once per state.

forgotten 2026-06-04T21:58:08.700445+00:00 — Implementation detail merged into solver-fallback-architecture knowledge cluster

solver-fork-O1 forgotten

INVARIANT: Solver fork must be O(1) via push/pop, not O(n) constraint replay. Fixed in v2 commit c37058a5c.

forgotten 2026-06-04T21:58:09.048255+00:00 — Bare invariant with commit hash; covered by solver-fork-o-n-root-cause-symcontext-fork

solver-fork-o-n-root-cause-symcontext-fork forgotten

Solver fork O(n²) root cause: SymContext::fork() eagerly replays all Z3 assertions into fresh solver. Python angr avoids this via lazy solver materialization (claripy FullFrontend pattern) — constraint list is copied on fork but Z3 solver is only created on first query. Most forked states are pruned/avoided without ever querying the solver, so the replay cost is only paid for states that need it. Fix: make Solver field Option, materialize lazily. Issue: angr-y2o.

forgotten 2026-08-05T04:34:40Z — Fixed-in-commit: bead angr-y2o is closed and the proposed fix (Option<Solver>, lazy materialization) is exactly what shipped, per bd's close reason; the code itself is now the documentation.

solver-test-pattern-fork-witness remembered

Solver test pattern for proving constraint admissibility: rather than calling eval() and checking the value (which only proves ONE solution), fork the solver context, add a test point constraint (e.g. x==51), and assert satisfiable(). This proves the test point is admissible without disturbing the original context. To prove a value is RULED OUT, do the same and assert NOT satisfiable. Used in test_constraint_weakening_through_ite (TestSolverOperations) — see also test_solver_fork_independence which establishes fork() is a deep clone of constraints.

solver-tests-z3-context forgotten

RustSolverContext tests MUST call _setup_shared_z3_context() in setup_class when running in isolation. Without the shared Z3 context, add_constraint_ast uses Python's Z3 variables but eval/min/max create NEW Rust Z3 variables — they don't match, so eval returns 0 for everything. The full test suite hides this because earlier tests (via angr.Project) initialize the shared context as a side effect.

forgotten 2026-06-04T21:58:09.394356+00:00 — Same rule as shared-z3-context-setup-required (test setup_class requirement) (merged into shared-z3-context-setup-required)

spawn-multiprocessing-syspath-leak forgotten

Python multiprocessing spawn workers inherit sys.path from the parent process, NOT just env. When you launch 'python /path/to/script.py' (no -c), Python sets sys.path[0] to dirname(script.py); spawn workers inherit that. So a worker invoked from .../tests/benchmarks/run_regression.py has sys.path[0]=tests/benchmarks/, NOT REPO_DIR — which means 'import angr' fails inside the worker even though it would work in the parent. Fix is to set PYTHONPATH=REPO_DIR in the env (PYTHONPATH is honored on every interpreter startup including spawn workers and is independent of sys.path[0]). Discovered while fixing angr-lbze: the loop benchmark gate had been silently failing for 28+ iterations because the gate worker couldn't import angr.

forgotten 2026-06-04T16:32:01.000256+00:00 — Bead/commit closure note

spike-supersession-triage-pattern forgotten

When closing a spike bead, check if a recently-landed sibling spike already satisfies the acceptance criteria. Pattern (2026-06-01): angr-nfjc was filed 2026-05-30 for 'OOM/MemoryError error-recovery contract' characterization; angr-zidj landed the answer in commit c714e9e15 on 2026-06-01 (rust_engine.rst 'Memory pressure and OOM' subsection — Where memory accumulates / Behavior on exhaustion / Recovery contract sub-blocks). Nfjc's 4 characterization questions and 'graceful-drain follow-up bead' AC were both fully covered (the follow-up is angr-x04s.1). Closure took ~10 tool calls: verify zidj landed, re-read the doc section, cross-check the 4 AC bullets against existing prose, close as superseded. Useful triage heuristic — before claiming a spike, grep recent commits + bd ls --status=closed for related work that may have made the spike trivially closeable.