avoid
67 remembered, 112 forgotten.
avoid-491g-already-implemented
forgotten
Task angr-491g (Concrete-address fast path before memory_*_symbolic_full callback) closed 2026-05-08 (160th loop session) as no-longer-motivated. The proposed optimization 'if address is fully concrete after one solver query, route to concrete fast path instead of fallback callback' is ALREADY IMPLEMENTED via concretize.rs read_fallback_any=true (line 156) and write_fallback_max=true (line 157), both always-on defaults (not configurable). concretize_read returns TooLarge only when eval() ALSO returned None — a state-is-unsat case. concretize_internal's Failed branch only fires after eval() already returned None upstream. Therefore fallback_load_symbolic_full and fallback_store_symbolic_full ONLY fire for genuinely unsat / broken states; adding another ctx.eval() before them would be a no-op. Acceptance criteria already met independently: sym-write 0.42s (target <1s) per benchmark-2026-05-05-sweep; mma_howtouse 0.65x dominated by UltraPage Python memory leak per mma-howtouse-leak-source, not these callbacks. Do not re-open without finding a real call site where address is unique under current constraints but cached concretize returned TooLarge or Failed AND eval succeeds — none exists in current code.
avoid-add-constraint-for-context-rebuild
remembered
PITFALL when rebuilding SymContext constraints (e.g. SymContext::translate_into, snapshot replay): SymContext::add_constraint (-> install_constraint, constraint_ops.rs) asserts on the LIVE z3 solver but does NOT append to the z3_assertions LOG (z3_assertions_shared + local_constraints.z3_assertions). add_constraint_raw DOES seed the log (via seed_and_check_z3_dedup). So a context rebuilt with add_constraint evaluates correctly ONCE, but the next operation that reads the z3_assertions log (fork-freeze, to_snapshot, or a SECOND translate_into = the A->B->A path) silently sees ZERO constraints and returns unconstrained models. restore_from_snapshot uses add_constraint_raw for exactly this reason. ALWAYS route rebuilt/translated Bool constraints through add_constraint_raw (wrap the Z3_translate'd Bool's raw ptr via Z3AstPtr::from_borrowed_raw), never add_constraint. Caught by the A->B->A round-trip test in test_symcontext_translate_into_cross_context; a single-translate test would have passed and hidden it.
avoid-addr-parity-on-predicate-find
remembered
Rust-vs-Python found-state PARITY: compare the SOLVED INPUT (posix.dumps(0)), never found[0].addr, when the explore find= is an output-content predicate (e.g. b"Welcome" in s.posix.dumps(1)). The two engines batch steps differently, so the predicate trips at different block boundaries: on fauxware the Python found state sits at 0x4008... while the Rust-routed one has already returned (addr 0, plus a benign "Lift error at 0x0" warning). Both solve to a SOSNEAKY-bearing stdin. Bit me writing TestParityUnderTheFlag in tests/engines/rust/test_default_engine_flag.py. Use an ADDRESS find= if you need addr equality.
avoid-any-on-bytes-for-zero-check
remembered
AVOID any(bytes_of_size_N) for zero-detection — Python's any() iterates each byte through PyObject_IsTrue and is ~14.65us per 4KB. Use b == ZERO_BYTES (memcmp via bytes.eq) at ~0.25us per 4KB, or bytearray == bytearray at ~0.11us per 4KB. memoryview != bytes is surprisingly ~8us (does NOT use memcmp). Always microbench compare strategies — the obvious ones (any, memoryview compare) are often the slowest. Used to fix 86% of _sync_extra_python_pages cost in angr-b58a.
avoid-arithmetic-cc-op-decoder
forgotten
angr-1yw9 (ccall.rs unification) cleanup yielded -91 LOC (2142->2051), below the bd issue's '150-200 LOC' target. The constraint: keeping the explicit per-op match table (one arm per cc_op u64 value) in amd64_cc_op_info/x86_cc_op_info is ~150 lines and resilient. An alternative arithmetic decoder ((cc_op-1)/4 -> category lookup table, (cc_op-1)%4 -> width) would save ~120 lines but encodes an undocumented assumption about VEX's enum value layout. If VEX ever reorders, it would silently miscompute. Explicit table chosen for safety. Future refactors of similar VEX-encoded tables: prefer explicit over arithmetic decoders unless the order is documented as stable in VEX headers.
avoid-arm-rust-engine-mgr-run
forgotten
ARM (armel) shellcode through the high-level RustExplorationManager.run() silently drops states (active=0, deadended=1, addr=0x0, history=[]) without invoking ANY of the manager's cb* spies. Cause not diagnosed (could be ARM lifting, or condition-flag handling, or the run-loop bailing on an unsupported VEX shape). Avoid ARM-based tests that rely on observing callback fire counts — use AMD64 + hand-built IRSB JSON via lift_block callback instead. Investigated during angr-2q5k. Same pattern reproduced with both LDREQ + symbolic r1 and a plain MOV nop block.
avoid-assigning-simgr-active
remembered
SimulationManager stash assignment trap: 'sm.active = [...]' does NOT filter the active stash — angr exposes stashes via getattr, so the assignment creates an instance attribute that shadows the dynamic lookup with a FROZEN list while sm.step() keeps stepping sm.stashes['active']. Symptom: a stepping loop that reads sm.active sees the same states forever and any address check inside it never matches. Use sm.move(from_stash=..., to_stash=..., filter_func=...) (or mutate sm.stashes['active']) instead. Bit us in _replay_preinit_prefix_for_find (angr-bdeqa).
avoid-background-run-at-iter-end
remembered
Ralph loop pitfall: a background run launched in the LAST turn of an iteration gets KILLED when the iteration ends — its child procs die with the agent. Iter 14 launched the full N=5 show_raw_speed.py run in background then ended the turn, leaving raw_speed_numbers.json empty (0 bytes); iter 15 had to re-run it. Rule: if a long background bench must finish THIS iteration (e.g. to commit its artifact), block-wait on it in the foreground (a Bash poll loop on pgrep with a generous timeout) before the turn ends. Only fire-and-forget background work that the NEXT iteration will collect.
avoid-bail-on-symbolic-prefix
forgotten
When adding symbolic-byte support to libc procedures with concrete-only prefix-handling code (atoi/strtol whitespace+sign+base), do NOT return SymbolicArgument when the prefix loop encounters a symbolic byte — that defeats the entire symbolic-digit feature for the most common shape (input is digits-only). Instead, BREAK OUT of the prefix loop and let the symbolic accumulator handle from idx=0. First-pass implementation that bailed broke 6/7 of my own new tests; fix was straightforward but easy to miss when porting from a 'concrete fast path' design.
avoid-bare-git-stash-in-loop
remembered
To verify a test fails pre-fix, do NOT use bare 'git stash' + 'git stash pop': when changes are already committed, 'git stash' saves nothing and a later 'git stash pop' pops a PRE-EXISTING unrelated stash (stash@{0} is an old native/*.rs WIP on this branch), causing merge conflicts across many files. RELATED HAZARD (iter19): if the fix is still UNCOMMITTED, 'git checkout ' to restore after temporarily neutering it will DISCARD the uncommitted fix too — you must reapply it. Safe pattern: COMMIT the fix first, then neuter a copy / 'git show HEAD1:path' to get the old version, verify the test fails against that, and the committed fix is never at risk. Instead of stash: test pre-fix on a clean tree BEFORE committing (git stash push ), or 'git show HEAD1:path > /tmp/old'. Recovery from a stash accident: 'git reset --hard HEAD' when the fix is committed. Always run 'git stash list' before any stash op in this loop.
avoid-bare-ruff-check-fix
remembered
Do not run bare 'ruff check --fix ' in this repo: without pre-commit's config it applies rules the repo doesn't gate on (RET504/SIM108/B904) and will silently autofix PRE-EXISTING code outside your diff, and it floods stderr with F811 false-positives on the pytest-fixture import pattern used across tests/engines/rust/ (importing a fixture from tests.engines.conftest and taking it as a test arg). Use 'make lint' (pre-commit) or, if pre-commit is not on PATH, just 'ruff format ' + git diff --stat to confirm no stray hunks landed.
avoid-bd-remember-key-only
forgotten
Don't run 'bd remember KEYNAME' without a body string — it silently replaces the existing memory's contents with the literal key. The correct form is 'bd remember "insight text" --key keyname' (positional arg is the body; --key controls the key). Same gotcha bites bd update --notes when its argument expansion fails silently (e.g. a python heredoc with bad backslash escaping that emits empty) — the bead's notes get REPLACED with whatever string materialized, including empty. Recovery: 'bd history --json' shows previous notes content from prior commits; copy from there, write to a tmpfile, then 'bd update --notes="$(cat /tmp/file.txt)"'. Witnessed 2026-05-22 iter 17 when 'bd remember v5a5-slice-3b-landed' turned the slice-3b memory body into the string 'v5a5-slice-3b-landed' itself, and a follow-up '--notes="$(... | python -c ...)"' with bad python escaping replaced angr-v5a5 notes with the slice-3c block alone (slice 2/3a/3b lost). Both recoverable from history.
avoid-bd-remember-positional
forgotten
PITFALL: bd remember with no body argument silently writes the key string AS the body, wiping previous content. The CLI does NOT warn or refuse. Why: the positional argument is the body; --key is the key. The correct invocation is: bd remember "" --key . Encountered 2026-05-22 during angr-ymoe; corrupted 2j5v-counter-architecture by running 'bd remember 2j5v-counter-architecture' (intended as a recall). Use bd recall for lookups, never bd remember.
avoid-bd-remember-without-key-flag
remembered
Never run bd remember <text> WITHOUT the --key <name> flag. When <text> looks like a key name (no spaces, hyphenated), bd silently UPDATES the memory whose key equals <text> with value = <text> — clobbering its content with the literal key string. Always pass --key explicitly, and always pass the actual memory body as the positional arg. To read a memory, use bd recall <key> or bd memories <key>. To write/update: bd remember "<body>" --key <key>. The same overwrite footgun applies to bd update --notes when its argument expansion produces an empty/garbled string (e.g., a python heredoc with bad escaping) — see bd-update-notes-overwrites.
avoid-bindgen-newtype-bare-const
remembered
bindgen NewType enum style (default_enum_style NewType in build.rs) emits enum VALUE constants as ASSOCIATED consts on the newtype, NOT module-level: access as ffi::VexArch::VexArchAMD64, ffi::IRConstTag::Ico_U1, ffi::IRExprTag::Iex_Binop, ffi::IRStmtTag::Ist_IMark, ffi::IRLoadGOp::ILGop_8Uto32, ffi::IREffect::Ifx_Read, ffi::VexEndness::VexEndnessLE, ffi::VexRegisterUpdates::VexRegUpd*. The newtype itself is struct T(pub c_uint); read the discriminant via .0. First attempt using bare ffi::VexArchAMD64 fails E0425.
avoid-cache-cached-state-predicates
remembered
AVOID caching predicate results for states in _state_cache (Python-created states from SimProcedure callbacks). Their stdout comes from Python's posix plugin (written during callback execution), not from Rust's stdout_buffer. Rust's stdout_len is 0 for these states even when they have significant output. Only cache predicate results for Rust-only states where stdout_len is authoritative.
avoid-cadet-phase3-sticky-eager-retry
remembered
angr-ckdy (CADET solve.py phase-3 step-loop egg hunt): the 'while True: sm.step(); break if any active.addr==0x804833E' idiom CANNOT be made to converge under the Rust engine by a manager-level sticky-eager retry (option b in the bead). Two independent blockers, both proven iter69:
(1) OBSERVABILITY: the Rust VEX interpreter CHAINS basic blocks within one step(n=1), so a state executes THROUGH 0x804833E mid-step and never stops with pc==0x804833E. explore(find=0x804833E) works only because angr-027h (commit ed600427b) breaks the block chain AT find/avoid addrs; a bare step-loop sets no find addr, so the egg block is never observable as an active addr. Python angr is block-granular so its phase-3 works.
(2) EXPLOSION/CRASH: flipping to eager mode (set_use_deferred_forks(false)) on active_empty does re-seed and progress (active 1->27 by step 38), BUT without a find-target to STOP at, eager BFS keeps forking — active jumps 49->310 around step 120 and SEGFAULTS via C-stack overflow in claripy _ast_serialize/_calc_hash (recursive AST hash on giant trees), through rust_callback_dispatch._handle_symbolic_branch_callback_inner. explore() masks this by stopping at find (step 38, active27).
Conclusion: a real phase-3 fix needs BLOCK-GRANULAR stepping (stop the chain at every basic-block boundary when no explore is active) so the egg is observable AND the loop breaks before explosion. That is a large engine change, not the eager retry. iter69 shipped only the diagnostic counter deferred_forks_dropped() (RustExplorationManager::deferred_forks_dropped, incremented at the stepping.rs UnconstrainedJump deferred-drop arm). The Python _maybe_step_eager_retry was implemented then REVERTED because it turns a benign infinite-spin into a crash.
avoid-cargo-fmt-cross-file-leak
forgotten
When deleting old VEX interpreter dead code (or similar large surgeries), cargo fmt run via 'make fmt' reformats the WHOLE workspace, not just touched files. The unrelated fmt changes will get mixed into the bead's diff if you stage with git add -A. Workflow: after make fmt, revert the unrelated files with git checkout -- before staging. Files that should stay touched are only the ones you explicitly edited. Keeping the commit focused makes the bead diff readable and the revert reversal trivial if needed.
avoid-cargo-fmt-scope-creep
remembered
cargo fmt reformats THE ENTIRE workspace, not just the file you touched — and pre-commit has NO rust hooks (.pre-commit-config.yaml = ruff/pyupgrade only), so the committed tree is NOT rustfmt-clean against rustfmt 1.8.0/toolchain 1.94; a global cargo fmt reformats 50+ unrelated files, creating massive commit noise. Three known footguns: (1) make fmt after a subsystem refactor pulls in unrelated formatting cleanup (e.g., context.rs, vex/ops.rs) and bloats the diff. (2) cargo fmt --manifest-path X.toml -- <file> does NOT scope rustfmt to ; the arg is silently ignored. (3) Cleanup after large surgeries (e.g., deleting old VEX interpreter dead code) mixes unrelated fmt changes into the bead's diff if you stage with git add -A. Workflow: re-apply edits by hand matching surrounding style, or scope formatting to only the lines you added (run rustfmt directly on the single file); after make fmt / cargo fmt, do git status and git checkout -- the files you didn't intend to touch. Encountered 2026-05-21 in angr-borb.1 (Address newtype) — touched 12 unrelated files — and angr-hfr0 — touched 13 unrelated files.
avoid-cargo-fmt-whole-crate
forgotten
cargo fmt --manifest-path X.toml -- does NOT scope rustfmt to ; it re-formats the WHOLE workspace and the arg is silently ignored. When doing a focused refactor and only the touched file needs fmt, either run directly, or accept the side-effects and revert other modified files via git checkout. Learned 2026-05-21 during angr-hfr0 — cargo fmt touched 13 other files with unrelated style nits.
avoid-cargo-only-rebuild-without-libvex-ffi
remembered
cargo-only rebuild leaves a degraded .so: tools/rebuild-rust.sh --cargo-only (and make rebuild-cargo) do NOT pass --features libvex-ffi, so after using them angr/rustylib*.so has libvex-ffi OFF even though it is default-ON since angr-3trr7. The pip path is not an alternative in this venv (it fails building unicornlib: no libvex.h). Symptom is silent — tests still pass, just on a different lift path. Always restore with the manual recipe in rebuild-with-libvex-ffi-recipe and verify python -c 'import angr.rustylib as r; print(r.vex_engine.libvex_ffi_enabled())' -> True before ending an iteration.
avoid-check-assumptions
remembered
AVOID z3 check_assumptions() for branch feasibility. It caused 2x regression (fairlight 14s→28.6s). check_assumptions prevents Z3 from reusing its internal bit-blasting cache between calls, making each subsequent check more expensive. push/assert/check/pop is MUCH faster because Z3 can reuse the preprocessed formula across push/pop boundaries.
avoid-cibw-export-in-before-build
remembered
cibuildwheel pitfall: an 'export FOO=...' inside CIBW_BEFORE_BUILD_* does NOT reach the build (cibuildwheel runs the before-build hook in its OWN shell, then invokes pip/cargo in a fresh one). Build-time env vars that the compiler needs — notably Z3_SYS_Z3_HEADER for z3-sys bindgen — must go in CIBW_ENVIRONMENT_, not be exported from a hook. Hit while adding the macOS leg to .github/workflows/wheels.yml (angr-c4xcs.3): the first draft did 'export Z3_SYS_Z3_HEADER=$(brew --prefix z3)/include/z3.h' in CIBW_BEFORE_BUILD_MACOS, which would have silently left bindgen guessing. Fix: hardcode the Apple-silicon brew path in CIBW_ENVIRONMENT_MACOS ('/opt/homebrew/include/z3.h' — the same path setup.py::_resolve_z3_header probes) and keep only the 'test -f || fail loud' assertion in the before-build hook.
avoid-claripy-not-on-bv-guard
remembered
claripy.Not(ast) on a 1-bit BV AST returns the NotImplemented singleton instead of raising — it does NOT negate. The Rust engine's assumed-constraint log stores (guard: RustBV(1-bit), is_true) pairs; rustbv_to_claripy lowers a comparison-derived guard to a claripy Bool (Not works) but an Extract/Ite-derived guard to a 1-bit BV (Not silently yields NotImplemented, which used to get pushed into the exported constraint list). Always go through claripy_bridge::export::assumed_guard_to_claripy, which checks 'length is None' to distinguish Bool from BV and compares a BV guard against BVV(1|0, 1). Consumers: RustExplorationManager::_export_state_constraints (state_api.rs) and PythonCallbacks::call_inspect_constraints (callbacks/inspect.rs).
avoid-clippy-approx-constant
forgotten
avoid clippy approx_constant in tests: float literals like 3.14, 2.71828 trigger deny-by-default clippy::approx_constant (they're close to PI/E). For tests, use 'boring' values like 42.5, 1.5, 3.0 — or use the constant directly (std::f64::consts::PI). Hit this in strtod tests during angr-f16h.3 — fixed by switching to 42.5.
avoid-collect-fallbacks-without-callbacks
forgotten
collect_simproc_fallbacks.py only surfaces useful signal for benches with callback_count > 0. The 4 synthetic-arch smokes (aarch64_le_branch, arm_le_branch, mips32_le_branch, mips64_le_branch) and the slow benches with callback_count=0 (codegate_2017-angrybird, defcamp_r100, flareon2015_2, google2016_unbreakable_1, hackcon2016_angry-reverser) contribute zero rows to the aggregation, so the script's default bench list deliberately skips them. The 14-bench default takes <90s end-to-end on the 8GB box; expanding to all 26 baseline benches adds ~60s with no extra signal.
avoid-conflating-per-call-cache-vs-hashcons
forgotten
AVOID conflating per-call to_z3_ast_cached hit-rate with what construction-time RustBV hash-cons (angr-behq) would dedupe. They measure DIFFERENT things:
-
Per-call hit-rate (z3_ast_cache_hit) catches Arc reuse WITHIN one to_z3_ast() invocation. The cache is reset every call. Hits = same Arc pointer revisited inside one tree walk.
-
Structural-duplicate rate (from analyze_constraint_sharing) is the population of distinct Arc allocations that have a structurally identical sibling — these are pointers hash-cons WOULD have merged. Across the whole exploration, not per-call.
A bench can have 0% per-call hit-rate (no within-call sharing) but 30%+ structural-duplicate rate (lots of cross-call would-be-shared nodes). The first says "the per-call cache is idle on this bench"; the second says "construction-time hash-cons would still help". Treat them as complementary, not redundant.
Source: angr-zdho measurement (2026-05-20). fauxware/defcamp_r100/ ekoparty_rev250/whitehatvn2015/codegate are exactly this case: 0% per-call hit but 25-85% structural dups.
avoid-content-fingerprint-on-found-states
remembered
vh834 content_fingerprint (tests/benchmarks/content_fingerprint.py::fingerprint_terminals) is NOT usable as a found-set gate at the RustExplorationManager integration level. Measured iter16 (angr-op0dn.13.1): a Rust-engine found state exports a nearly-EMPTY Python-side constraint log — fauxware entry_state found states export exactly ONE constraint; the fork_solve_pbounce synthetic (blank_state OR entry_state, symbolic stdin) exports ZERO, and state.solver.eval on the seeding BVS / posix.dumps(0) both return all-zero bytes even though the 8 leaves are demonstrably distinct (8 satisfiable finds). The path condition lives in the RUST solver and only surfaces through the eval/dumps fallback for states whose stdin was created by angr's own entry_state plumbing. Fingerprinting state.constraints therefore collapses every found state to a single fingerprint and any cross-worker set-equality assertion passes VACUOUSLY. Use instead: (a) a solver-eval projection (test_parallel_wave._path_set: pc + is-backdoor from posix.dumps), or (b) the exhaustive drain COUNT (num_find == 2^W; a lost path reads < N, cancel over-collection > N). content_fingerprint remains valid for RustStateProxy objects (which expose .pc/.constraints from the Rust side) and for the Rust unit test scheduler::tests::test_determinism_result_set.
avoid-csgames2018-as-regression-signal
forgotten
csgames2018 was failing on master with 'list index out of range' through 2026-05-07 due to a wrong-return-register bug in Cdecl (offset 16 = EDX instead of 8 = EAX). Fixed in commit 5329d8222 — csgames2018 now passes at 0.97s/294MB on the rust engine. Re-treat it as a true regression signal.
avoid-deep-ite-arm-grouping
forgotten
angr-269l originally framed as 'group addresses by loaded-value into Or-of-equalities conditions, then emit one arm per group'. Going with a simpler sibling-collapse approach (ite(c,v,v)->v after recursive split) was deliberate: (a) handles the bead's stated target case (zero pages, repeated initializers) by recursively bubbling identical leaves to the root, (b) preserves the existing balanced bisect tree shape (no need to re-key or sort groups), (c) adds zero work when nothing matches. The Or-of-equalities approach would handle weirder patterns like [v1,v2,v1,v2] better, but those aren't the documented hot case and the implementation effort/risk is much higher. Acceptable to revisit if profiling shows a benchmark dominated by alternating-content fan-out.
avoid-deferred-4j5u-rustexploration-decomposition
forgotten
Task angr-4j5u (decompose Rust RustExplorationManager into ExplorationStateStorage/ExecutionConfiguration/ProcedureDispatcher/HookRegistry/ProfilingCollector) was deferred 2026-05-07 after audit. (1) Bead description claims '95-field god struct' but actual count is 45 (NOTES correction said 41 — both are ~2x lower than title). (2) Sub-struct pattern is already in use: ExecutionConfig, NativeProcedureRegistry, NativeSyscallRegistry, NativeProcStats, StashManager already sub-structs. File at native/angr/src/exploration/mod.rs already has 8 logical impl-block sections with comment dividers (PyAPI/Native Proc/Uniqueness/Native Techniques/State Export/Run loop/Resume/Solver Stats). (3) Refactor cost is high: 47 callsites for profiling fields, 16 for hooks, 28 for procedure-dispatcher fields = 91+ mechanical rewrites for cosmetic gain. (4) No bug class motivates: 146 tests + 16 benchmarks pass, no memory entries for god-struct or rust-manager-mod-rs incidents. (5) High invariant-breakage risk: invariant-stepping-decomposition, invariant-no-return-deadend, avoid-fixing-only-one-native-dispatch-path all run through these fields across mod.rs + stepping.rs. Same deferral template as angr-borb/angr-ja0b/angr-x3xu/angr-m2hf/angr-prem/angr-fk0m. DO NOT re-open without (a) a concrete bug showing field-coupling drift OR (b) a refactor that genuinely changes responsibilities (e.g., splitting orchestration from execution), not just renames field groups.
avoid-deferred-borb-newtypes
forgotten
Task angr-borb (Introduce StateId/Address newtypes for type safety) was deferred 2026-05-03. Reasons: (1) bead description references pyapi.rs which was deleted in commit 15960a013 — bead is partly stale, (2) full refactor scope is 283 sites across 25 files (state_id/address bare u64), too large for a single session, (3) PyO3 boundary doesn't auto-convert custom newtypes — every Python-exposed method needs FromPyObject impls or u64-then-wrap, (4) P4 backlog and no documented bug class motivating it. Half-measures (just a few helpers) don't deliver the type safety promise. Revisit only if a real state_id/address mixup bug surfaces.
avoid-deferred-csd1-pythoncallbacks-trait-split
forgotten
Task angr-csd1 (Split PythonCallbacks into MemoryCallbacks/ExecutionCallbacks/ProcedureCallbacks traits) deferred 2026-05-07 after audit. Same template as wqao/4j5u/borb/fk0m/m2hf/prem/x3xu/ja0b. (1) Bead description out of date — points at lines 298-400 calling out '15+ methods' but file is 1780 lines, struct has 18 callbacks, 19 set_, 18 call_, plus GC traversal helpers and a CallbackHandle wrapper. (2) Today's Option<Py> already supports partial wiring; the avoid-silent-no-op-callback-fallbacks invariant (each call_* uses ok_or_else(PyRuntimeError) for None hooks) provides the safety the trait split would. (3) 146/146 tests pass and no current test is blocked by lack of mocking granularity. (4) PyO3 surface preservation requires a shim because rust_manager.py:696-716 calls set_memory_load/etc on a single PythonCallbacks. (5) Real benefit requires switching 30+ &PythonCallbacks call sites in engine.rs, exploration/, interpreter_cb/ to multiple &dyn refs — many sites use multiple groups together (expressions.rs uses memory + register; statements.rs uses memory + sync_constraints). (6) Hot-path perf risk on call_memory_load via Arc dispatch with no perf criterion in acceptance. (7) Recent bug memories (deadend-drops-deferred-forks, shared-solver-for-callbacks, symwrite-rustbv-to-claripy-bottleneck) all point to dispatch/sync logic, not PythonCallbacks shape — no bug class motivates the work. Reopen ONLY if (a) a concrete test needs pure-Rust mocking of one callback group, OR (b) a measured perf win requires monomorphizing one group, OR (c) a real bug shows the trait boundary would have caught it.
avoid-deferred-fk0m-state-mixin-unification
forgotten
Task angr-fk0m (unify rust_state_sync/cache/export mixins into RustStateCoordinator) deferred 2026-05-07, audit reconfirmed 2026-05-16 in angr-fk0m.1. (1) Mixins are phase-organized by direction: sync (Python→Rust, 46 methods), cache (metadata management, 14 methods), export (Rust→Python, 36 methods). Total ~3700 LoC + 96 methods (~18% growth since prior audit). Proposed phases (Synchronization/Caching/Export) match current boundaries exactly — pure renaming. (2) Cross-mixin coupling is minimal and clean: sync→cache 8 sites all calling _register_handle (AST handle registration at sync.py:1282,1430,1453,1543,1808,1873,1888,1949); cache→export 1 site calling _attach_rust_solver_fallback (cache.py:404 when materializing Python state for predicate eval); export→sync/cache 0 direct calls. 9 edges total are dependency edges, not duplicated logic. (3) Multiple documented invariants run across these files: invariant-callstack-sync-export-pipeline (4 export paths), invariant-rust-solver-fallback-class (RustSolverFallback in export), disk-cache-register-filter (sync), callstack-rebuild-pattern (export), flareon5-post-exploration-constraints (solver fallback). Unifying risks breaking these without test signal. (4) Cache invalidation is centralized in StateMetadata dataclass (per invariant-state-metadata-dataclass), not duplicated across mixins — drift-risk claim unsupported. (5) No documented bug class. Tests + benchmarks correct under current structure. Same deferral template as angr-borb/ja0b/x3xu/m2hf/prem. Re-open only if (a) a concrete bug shows mixin drift, OR (b) a refactor genuinely changes responsibilities (not just class renames).
avoid-deferred-fork-base-mismatch
remembered
Deferred fork base mismatch in stepping.rs: MaxBlocks/BlockEnd path forks from successors[0] (carries accumulated taken-path constraints from prior forks in same block) — SemiProc-native success path forks from a saved fork_base = successors[0].fork() taken BEFORE any constraints. process_deferred_forks_into matches MaxBlocks/BlockEnd's behavior. The MaxBlocks/BlockEnd semantics is correct (deferred forks share a common prefix of prior taken-path constraints, since they all happened during the same VEX block). SimProc-native may have a subtle correctness bug — needs audit before unification. Affected file: native/angr/src/exploration/stepping.rs lines 196–281 (MaxBlocks) vs 380–415 (SimProc-native).
avoid-deferred-implementation-quality-review-b-beads
forgotten
2026-05-30 implementation-quality review beads angr-v6yy (SymContext split B9), angr-0o7j (CallbackInterpreter split B31), and parent angr-a2br all match the avoid-deferred-csd1/wqao/borb/fk0m/m2hf/prem/4j5u/ja0b/zo refactor template. Deferred 2026-06-02 to 2026-08-01. v6yy is a strict dup of already-deferred angr-a2br.2 (both target context.rs split, different cut lines). 0o7j description is stale (interpreter_cb -> interpreter renamed, mod.rs is 1781 LOC already split into 8 files). a2br parent has no actionable work (children .1/.3 closed, .2 deferred). General rule: 'B-numbered' implementation-quality review beads proposing god-object splits that arrived AFTER the original avoid-deferred batch should be cross-checked against deferred siblings before being treated as actionable. The B-numbering (B9, B31) suggests they came from a single review document, not from concrete bug evidence — so apply the same reopen criteria: (a) concrete bug class the split would prevent, OR (b) refactor that genuinely changes responsibilities (not just moves code).
avoid-deferred-m2hf-error-trait
forgotten
angr-m2hf (CommonErrorReason refactor) re-deferred 2026-05-11 after detailed variant audit. Actual Rust error-enum variant overlap is too thin to motivate the refactor: only Unsupported(String) appears in 3+ enums (CbExecutionError, ExecutionError, LiftError). SymbolicArgument(String) and Other(String) each only in 2 (SyscallError + ProcedureError). Even narrowed, would touch dozens of call sites to share 2-3 string variants. Audit memo's 'weak fit, no motivating bug' conclusion stands. Revisit only if a real Rust-error-typing incident surfaces.
avoid-deferred-prem-memory-layer
forgotten
Task angr-prem (MemoryLayer trait so interpreter is generic over backend) deferred 2026-05-07 after audit. Same template as angr-borb / angr-ja0b / angr-x3xu / angr-m2hf. KEY FINDINGS: (1) SymbolicMemory is NOT 'pure data' — its MemoryError variants drive control flow at the call site. UnmappedPageInRegion triggers fetch-page-from-Python and retry; Unmapped and SymbolicAddress mean 'fall through to Python'; other variants propagate as CbExecutionError. (2) try_rust_memory_load (interpreter/expressions.rs:619) is NOT a peer to load_from_callback (mod.rs:1015) — it is a richer control-flow gate that returns Result<Option>: Some=Rust handled, None=fall through, Err=hard error. The Python callback path returns plain Result. (3) The Load expression in expressions.rs:45 walks 6 sources in order: try_rust_memory_load, pending_symbolic_stores, pending_stores buffer, all_flushed_symbolic_stores, all_flushed_stores, prefetch cache, try_read_concrete_memory, load_from_callback. Not 2 backends. (4) try_rust_memory_load takes &self.concretizer and load_start timing — both interpreter-level, not backend-level. (5) PythonCallbackMemory cannot share these signals because Python IS the page authority and concretizer. (6) No bug class motivates the trait. (7) Half-measures (trait wrapping only SymbolicMemory) deliver no value. DO NOT REOPEN without (a) a concrete bug class showing the dual path causes wrong behavior, OR (b) a third backend that needs the trait.
avoid-deferred-qrhl-vex-trait-dispatcher
forgotten
Task angr-qrhl (Trait-based VEX op dispatcher to replace match registry in vex/ops.rs) deferred 2026-05-07 (132nd loop session). Same template as angr-prem/csd1/m2hf/wqao/ja0b. KEY FINDINGS: (1) vex/ops.rs has 5 #[inline] entry points (unop/binop/binop_with_rm/qop/unop_with_rm), 4 hot-path call sites in interpreter_cb/expressions.rs (lines 161/180/242/261). Every IRStmt op flows through this — replacing match with Box+HashMap is a perf regression on the hottest dispatch in the engine. (2) IROp enum (vex/ir.rs:517) has 224 variants, MOST parameterized by IRType ty. Many arms compile to width_binop!(left, right, ty, add_into, ctx) → direct RustBV intrinsics. A trait keyed on op kind cannot access the IRType inside the variant without re-matching, defeating the abstraction. (3) Acceptance criterion 'dispatch overhead within 5%' is structurally hard to meet — match-→-jump-table beats virtual dispatch. (4) 'Adding a new op' workflow keeps the same touch-point count: enum variant in ir.rs (still needed for opcode_map.rs) + handler-impl-or-match-arm + opcode_map entry. Trait moves work, doesn't reduce it. (5) Hypothetical 'external-crate plugin' use case has no real downstream consumer documented. (6) Recent VEX bug fixes (angr-n28w transcendentals, angr-3ekz vec FP cmp) added match arms — the exact workflow the trait would complicate. REOPEN ONLY IF: concrete external consumer appears, OR prototype shows measured perf win, OR a bug class shows the match is structurally unsafe.
avoid-deferred-v2cl-binop-trait
forgotten
angr-v2cl (BinOpMethod trait + binop submatches in vex/ops.rs) released 2026-05-31 iter 13 without claiming. Same risk profile as deferred angr-qrhl: replacing the 273-line binop() match with trait dispatch on the hot VEX op path is what the prior memory avoid-deferred-qrhl-vex-trait-dispatcher explicitly warns against. width_binop!/width_unop! macros + flat match are an intentional design (match → jump-table beats virtual dispatch). Submatch-by-class doesn't help — IROp variants don't cluster in the enum, splitting needs fallthrough chains which are uglier than the original. Reconsider only if a concrete bug class motivates it.
avoid-deferred-wqao-rust-manager-decomposition
forgotten
Task angr-wqao (Split angr/exploration/rust_manager.py into single-responsibility components: StateLifecycleManager / DiskCacheManager / ConstraintBridge / CallbackRouter) was deferred 2026-05-07 (127th loop session) after detailed audit. (1) Bead description ~2.8x stale — claims 'rust_manager.py + 4 mixins ~2800 lines / ~165 funcs', actual 7820 lines / 202 methods (rust_manager.py 2963 + rust_callback_dispatch.py 1915 + rust_state_sync.py 1587 + rust_state_cache.py 400 + rust_state_export.py 955). (2) Three of four proposed extractions already exist as mixins on RustExplorationManager (rust_manager.py:291): RustStateCacheMixin+RustStateExportMixin map to StateLifecycleManager; RustStateSyncMixin maps to ConstraintBridge; RustCallbackDispatchMixin maps to CallbackRouter (event-side). (3) The 15 FFI cb* low-level callbacks at rust_manager.py lines 734-1206 must stay FFI-bound to the manager identity — extracting them adds indirection (self.x → self._router.mgr.x) without changing responsibilities. (4) The ~700-line disk-cache section (1248-1957) is being decomposed incrementally already (aed1ec175 split disk-cache load into 3 phases, eece5e3c5 split _save_init_to_disk_cache, 9bbc301b0 split cache version, 7def453e3 extracted IRSB serializer). (5) Refactor cost: 33 candidate methods, 40 internal callsites, 102 cross-mixin field references for 6 shared fields (_perf_stats, _state_metadata, _current_callback_state_id, _register_handle, _get_per_fork_state, _get_effective_state_id), 38 instance fields tightly coupled in init. (6) Multiple documented invariants run through proposed-extraction code without regression test coverage: invariant-disk-cache-key-axes, invariant-init-cache-lazy-regions-order, disk-cache-register-filter, disk-cache-symbolic-guard, explore-predicate-loop-termination. (7) No bug class motivates the work — bug memories (deadend-drops-deferred-forks, shared-solver-for-callbacks, symwrite-rustbv-to-claripy-bottleneck) all point to rust_callback_dispatch.py which has already been extracted. 39 recent commits on rust_manager.py show active maintenance not rot. 146/146 tests + 16/16 benchmarks pass under current structure. Same deferral template as angr-borb/ja0b/x3xu/m2hf/prem/fk0m/4j5u. Do not re-open without (a) a concrete bug showing field-coupling drift in rust_manager.py, OR (b) a refactor that genuinely changes responsibilities (e.g., disk-cache backends become pluggable), not just renames or rehoming.
avoid-deferred-wqao1-disk-cache-save-extract
forgotten
Task angr-wqao.1 (Extract disk-cache save into RustDiskCacheManager mixin) released 2026-05-09 (184th loop session) without execution. Inherits parent angr-wqao deferral verdict (avoid-deferred-wqao-rust-manager-decomposition memory). The proposed move is mechanical (~200 lines into a new mixin file) with no motivating bug and no responsibility change. Disk-cache code is already being decomposed incrementally inside rust_manager.py (aed1ec175 split disk-cache load into 3 phases, eece5e3c5 split _save_init_to_disk_cache into module-level extract* helpers, 9bbc301b0 split cache version, 7def453e3 extracted IRSB serializer to angr/exploration/rust_irsb_serializer.py). The save-side helpers (_disk_cache_dir, _disk_cache_key, _state_has_user_symbolic, _save_init_to_disk_cache, _save_init_state_to_caches, _compute_disk_init_key) share state (_disk_key_cache class attr) and metadata-application logic with load-side, and _save_init_state_to_caches saves to BOTH in-memory (_init_cache) and disk caches — extraction would either split this or duplicate the path. No bug class motivates the work. Do not re-open without (a) a concrete bug showing the disk-cache code's coupling causes drift, OR (b) a refactor that genuinely changes responsibilities (e.g., disk-cache backends become pluggable).
avoid-deferred-wqao2-disk-cache-load-extract
forgotten
Task angr-wqao.2 (Extract disk-cache load into RustDiskCacheManager mixin) deferred 2026-05-09 (202nd loop session). Inherits parent angr-wqao deferral (avoid-deferred-wqao-rust-manager-decomposition memory) and matches sibling angr-wqao.1's deferral (avoid-deferred-wqao1-disk-cache-save-extract memory). Proposed move is mechanical (~200 lines into a new mixin). Disk-cache LOAD code shares state with SAVE code (_disk_key_cache class attr, metadata-application logic) — extracting only one half of the load/save pair would either split the shared state or duplicate path code. Disk-cache code already being decomposed incrementally inside rust_manager.py (commits aed1ec175 split disk-cache load into 3 phases, eece5e3c5 _save_init_to_disk_cache split, 9bbc301b0 cache version split, 7def453e3 IRSB serializer extracted to angr/exploration/rust_irsb_serializer.py). No bug class motivates the work. Reopen ONLY if (a) a concrete bug shows the disk-cache code's coupling causes drift, OR (b) a refactor that genuinely changes responsibilities (e.g., disk-cache backends become pluggable).
avoid-deferred-wqao4-init-pipeline-phases
forgotten
Task angr-wqao.4 (Sequence rust_manager.py init pipeline into explicit phases) deferred 2026-05-09 (202nd loop session) after audit. (1) Description proposes 'Rename helpers step_N to phase_*' but no such _step_N methods exist in rust_manager.py — grep 'step' yields only _step_python_to_main (line 2155, multi-stage handler — different concern). Three init phases (_setup_callbacks line 1053, _load_binary_regions line 1639, _register_simprocedures line 1676) already exist as named methods with clear scopes; _perf_stats.set_init_phase calls already track them at lines 738/743/748. (2) Acceptance criterion 'rust_manager.py under 1000 lines' is unachievable via renaming alone — file is 3758 lines, mostly large method bodies (_setup_callbacks ~580 lines, callback dispatchers, disk cache 1248-1957). Going from 3758→<1000 requires the extractions in parent angr-wqao that were explicitly deferred (avoid-deferred-wqao-rust-manager-decomposition memory). (3) Sibling angr-wqao.1/.2 (disk-cache mixin extractions) named in the description as preconditions are ALSO deferred (avoid-deferred-wqao1-disk-cache-save-extract memory). Without those, the init pipeline stays embedded and renaming changes nothing structural. (4) No bug class motivates this. Same template as the parent. Reopen ONLY if (a) the disk-cache mixin extractions land successfully, OR (b) the acceptance criterion is rewritten to be size-agnostic and the renaming target actually exists in the code.
avoid-deny-on-mod-rs-parent
remembered
deny-sweep must NOT add module-level #![deny(clippy::unwrap_used, clippy::expect_used)] to exploration/mod.rs: mod.rs declares every sibling submodule (scheduler, selection_policy, helpers, step_core, ...) via 'mod x;', and an inner-attribute deny propagates hierarchically into all those child modules — many carry dozens of un-allowed unwrap/expect and the build would break. Deny only the LEAF py-boundary body files individually (manager_methods/pending_api/state_api/stats_api/event/state_id/state_lifecycle/resume/run_loop). Same reason mirror-applies to any parent 'mod.rs' anywhere in the tree.
avoid-dispatch-next-drain-loop-in-tests
remembered
Unit tests that call exploration::scheduler::worker::dispatch_next MUST NOT drain with while let Some(..). dispatch_next only returns None at quiescence (injector empty AND WorkTransport.pending == 0) or on cancel; a real worker retires each task with pending -= 1 after processing, so a test that absorbs children via absorb_continues (which counts them IN) and never decrements pending will hang FOREVER inside steal_from_injector's backoff sleep loop — a silent deadlock that looks like a slow cargo test --release (test binary alive, load average ~0). Dispatch a fixed count instead, or pre-set pending. Symptom-to-diagnosis: if cargo test --release runs >10min with near-zero CPU, pgrep the target/release/deps/rustylib-* binary — it is blocked, not compiling.
COROLLARY (angr-9ke6b.58, iter124): the same 'pending must be retired or the pool hangs' rule is what makes dispatch_next's reattach-failure arm correct — its pending.fetch_sub(1) is not just accounting hygiene, it is the ONLY thing that lets the following steal_from_injector see pending==0 and report quiescence. Any future worker-side path that DROPS a stolen payload must fetch_sub too. Both directions are now pinned by test_dispatch_next_corrupt_only_payload_reaches_quiescence (sole corrupt task -> None, pending 0) and test_dispatch_next_drops_a_corrupt_payload_and_keeps_stealing (corrupt ahead of a good payload; injector_dispatches counts steals, reattaches counts only successes), both driving StateMigrationPayload::corrupt_for_test() and relying on Injector::steal being FIFO so the corrupt payload is stolen first.
avoid-eager-symbolic-register-export
remembered
Eager full-AST recovery of EVERY symbolic register on the plain-state export hot path (rust_state_export.py register sync) regresses perf badly — flareon2015_2 +38% (5.4 vs 3.94s). WORKING ALTERNATIVE (commit 0b6b0ea1d, see clz-ctz-symbolic-concretization-bug): attach a LAZY RustRegisterProxy only when snapshot.get_symbolic_register_names() is non-empty. The proxy recovers each register's AST per-READ via get_state_register_ast, so it pays only for registers the caller actually inspects — measures +3.3% on flareon2015_2 (noise). Rule: never eagerly materialize symbolic register ASTs on a per-export path; defer to a proxy/on-demand read. Most states have zero symbolic registers so the gate keeps the plain register file untouched.
avoid-early-return-on-empty-deferred-forks
remembered
tests/engines/rust/test_manager_core.py::test_fork_counters_exposed_and_non_summable asserts exec_stats['deferred_fork_time_ns'] > 0 on fauxware, and on fauxware that value comes ENTIRELY from find/avoid callbacks that carry ZERO deferred forks — the legacy step_one site started/charged the batch timer unconditionally. Adding an early to materialize_deferred_forks (helpers.rs) is a natural-looking optimization that reddens this test. cargo test cannot catch it; only the Python suite does. Keep the unconditional batch-timer charge.
avoid-enabling-native-read
forgotten
NativeRead/NativeWrite are now REGISTERED BY DEFAULT (angr-3tek.2, 2026-05-10) at native/angr/src/procedures/mod.rs:240-241. The cache-sync fix is in place: _replay_rust_dirty_pages (angr/exploration/rust_state_sync.py:1675) walks Rust's dirty_pages and writes both concrete bytes and symbolic ASTs into the cached Python SimState during _create_state_for_callback (rust_callback_dispatch.py:1880). If you need to disable them temporarily, comment those two lines, but do NOT do so silently — the read+strcmp interaction in fauxware is the canonical regression. Replay ordering invariants live in invariant-3tek2-replay-ordering.
avoid-entry-state-for-seed-push-tests
remembered
Testing the Python->Rust seed-time push of a state field in _add_rust_state: do NOT bump the field on an entry_state() — for a state at proj.entry, _run_python_init_if_needed runs Python-init-to-main and returns a FRESH state-at-main (or cached), discarding any bump on the seed (and resetting heap_location to default / posix.brk to loader value). Use proj.factory.blank_state(addr=proj.entry+0x10): a non-entry addr inside the main binary is returned by _run_python_init_if_needed unmodified (line 'In a real binary (not entry), no init needed'), so the seed bump survives to _add_rust_state. Caught while writing TestHeapBrkSync.test_init_push (angr-um39j).
avoid-fast-path-skipping-filters
remembered
AVOID: when adding fast-path bypasses that skip the slow path's iteration logic, do not also skip the slow path's filtering. The slow path's choice of WHICH items to process is part of its contract — not just 'how to read them quickly'. Concrete case: 13e84581f saved 0.5ms by skipping the per-register read from SimState, but also skipped the 'iterate only the AMD64 reg_names list' filter, exposing cr0/ymm/seg registers that the Rust engine never modeled. Pattern: factor the filter (here _supported_register_names) so both paths share it.
avoid-fixing-only-one-native-dispatch-path
remembered
AVOID: assuming the exit hook is hit only via the top-of-loop run() check in mod.rs. The VEX interpreter has its OWN native SimProcedure dispatcher in stepping.rs:handle_simprocedure that is invoked when a block ends with a call to a hooked extern address. Both paths must implement no_return deadending; only fixing one leaves the bug.
avoid-flareon-5rjbq-store-visibility-lead
forgotten
iter64's flareon-5rjbq-store-visibility-disambiguated conclusion is WRONG/SUPERSEDED. It claimed the native VEX hash loop read CONCRETE-ZERO ENC and never saw the proxy pw stores (a callback-state<->execution-state coherence gap). iter65 empirically DISPROVED this: instrumenting SymbolicMemory store_concrete+load_concrete showed store AND native load both hit the same memory at 0x0..0x21 with sym_hit=true. The native read DOES see the pw. Real cause is address-witness collision — see flareon-5rjbq-address-witness-collision-root-cause. Do not chase store-visibility/resume-snapshot leads (pre_callback_snapshot etc.) for this bug.
avoid-fresh-symbolic-on-unsupported-binop
forgotten
Before fix: symbolic DivMod returned OpError::UnsupportedVectorOp, which the binop handler in interpreter/expressions.rs:329 catches and replaces with a FRESH unconstrained symbolic (unsup_binop_). This is sneakily unsound — a code path with symbolic divisor would produce a result unrelated to dividend/divisor. The 'falls back to Python' in the task title was misleading; nothing actually fell back to Python.
avoid-frozenset-intersection-on-simstateoptions
remembered
SimStateOptions does not implement the set protocol — frozenset.intersection(state.options) raises 'option does not exist' because Python's set algorithm probes each rejected name as if it were a state-option key. Use 'name in state.options' membership iteration instead: '{n for n in MY_FROZENSET if n in state.options}'.
avoid-full-lineage-teardown
forgotten
angr-0dgq full-variant teardown is a NET LOSS vs angr-v5ht simple variant on baby-re. Measurement (5-sample, 2026-05-24, branch rust-symex, HEAD 971d35545): simple variant median 1.00s peak_mem 552MB, full variant median 1.13s peak_mem 509MB. The teardown walks all stashes after the dismantle sampler fires (lineage_dismantle_count=1, lineage_teardown_count=13) and (a) drops each state's lineage Arc, (b) invalidates the cached per-context z3::Solver, (c) clears scope_path/scope_savepoints, (d) resets sat/model caches. Next solver() access rebuilds from z3_assertions_shared + local_constraints.z3_assertions — that rebuild is expensive (13 fresh Z3 solvers seeded with ~600 assertions each). The savings from skipping switch_to/lineage-cache traffic on 13 in-flight states is smaller than the rebuild cost. Memory drops modestly (552→509MB = 8%) but wall time worsens (1.00→1.13s = +13%) and high-sample variance grows (1.40, 1.63 outliers in 5-sample run). KEEP angr-v5ht simple variant as the only dismantle strategy. DO NOT retry the full teardown without first proving that the per-context solver rebuild can be made lazy/incremental — e.g. by handing the existing Z3 solver to the per-context path instead of dropping+rebuilding. See angr-0dgq for the implementation that was reverted; the unit tests test_teardown_lineage_drops_arc_and_preserves_constraints and test_teardown_lineage_no_op_when_absent verify the mechanics work, but the run_loop integration is the regression.
avoid-fuzzing-below-parser-contract
remembered
Fuzzing pub(super) parser internals directly (below their caller contract) yields FALSE POSITIVES. Seen in angr-qwyti.9 cargo-fuzz: (1) parse_binary_to_bytes has a dev-only debug_assert!(bits<=byte_len*8) tripwire — real callers derive s and width from the SAME Z3 BV so it holds; feeding width=0 with non-empty bits trips it (release is panic-free via saturating_sub). (2) parse_hex_to_bytes/parse_binary_to_bytes assume ASCII (Z3 Display is always ASCII). Harness must respect these: pass ASCII-only payloads and width>=implied-bit-count so a reported crash is a REAL defect, not a breached-but-unreachable contract. cargo-fuzz builds WITH -Cdebug-assertions so debug_assert!s fire.
avoid-fxhash-dedup-set-spike
remembered
Evaluated+REJECTED the last std-hash hot-path FxHash candidate for the autonomous loop: symbolic/context.rs LocalConstraints.dedup_set (HashSet of Z3 AST ptrs, seeded in constraint_ops.rs seed_and_check_z3_dedup, consulted per add_constraint_raw). NOT a measured-win spike because: (1) the only seed trigger add_constraint_raw is in NO cargo bench timed loop (symcontext_fork/_scaling only time fork(), and fork rebuilds dedup_set lazily so it's never cloned/populated there; assume_true/false use check_z3_dedup_if_seeded which never seeds); (2) within add_constraint_raw the dominant cost is the Z3 solver.assert + lineage scope work, not a usize-keyed lookup over tens of ptrs -- bottleneck is NOT the hash. Building a faithful isolating bench needs a bench-only pub reseed method (pollutes public API; benches are an external crate). CONCLUSION: std->FxHash swaps are now genuinely exhausted for cleanly-measurable wins; do NOT re-investigate dedup_set. See benchmark-memory-fxhash-stash-done (last real one) and offline-loop-queue-blocker-map.
avoid-getattr-private-dunder-recursion
remembered
LazySimStateRef.getattr MUST raise AttributeError for names starting with '' or '__'. Otherwise debugger probes (class, reduce, getstate), pickle, copy.deepcopy, and inspect.getmembers all silently materialize the SimState, defeating the lazy goal. The two slot attributes (_lazy_mgr, _lazy_state_id) are stored via object.setattr so they never invoke getattr in the first place. See angr/exploration/rust_state_export.py _LazySimStateRef.getattr.
avoid-git-stash-during-edits
forgotten
PITFALL (2026-05-25, angr-tkbr.3 iter 7): 'git stash' silently saved AND reverted in-progress unstaged edits across multiple files. After 'git stash pop' the changes were restored. Lesson: NEVER 'git stash' mid-task to peek at base-branch state. Use 'git show HEAD:path' or 'git diff' instead. If you must stash, run the diff/check on the worktree FIRST, then immediately 'git stash pop' before doing any other work. The Edit tool worked off in-memory file state so subsequent edits silently no-op'd until I realized + popped.
avoid-global-cargo-fmt
forgotten
Do NOT run 'cargo fmt' (global) on this repo: pre-commit has NO rust hooks (.pre-commit-config.yaml = ruff/pyupgrade only), so the committed tree is NOT rustfmt-clean against rustfmt 1.8.0/toolchain 1.94. A global cargo fmt reformats 50+ unrelated files, creating massive commit noise. Re-apply edits by hand matching surrounding style; do not auto-format. If you must format, scope to only the lines you added.
avoid-hasattr-lazy-init
remembered
AVOID using hasattr(state, 'heap') or hasattr(state, plugin_name) on angr SimState objects. SimState.getattr triggers lazy plugin initialization. The 'heap' plugin takes ~80ms to initialize on first access. Use 'plugin_name in state.plugins' for O(1) dict check instead.
avoid-id-keyed-global-symbol-cache
remembered
The Rust symbolic identity registry (symbolic/registry.rs, global_registry()) is PROCESS-GLOBAL and never cleared between RustExplorationManager instances, while symbol ids come from SymContext::next_id() — a PER-CONTEXT counter that restarts at 0. A second exploration in the same process mints the same ids and can alias entries the first left behind: export's get_claripy_ast(id) can hand back a foreign symbol's claripy AST. Consequence for TESTS: any test depending on Rust-minted symbol identity (e.g. the Python-bounce round trip) must run in a FRESH interpreter — see tests/engines/rust/test_wide_eval.py::TestPythonBouncePreservesIdentity, which shells out via subprocess; in-process it silently falls back to pre-fix behaviour, so it passes or fails depending on file ordering. AVOID fixing this by guarding the export cache hit on a name match (tried in angr-izov2): it rejects legitimately-cached seeded-stdin ASTs and loses a found leaf in test_stdin_seed / test_parallel_wave. Open follow-up on the related constraint-wipe: angr-je2xt.
avoid-impl-into-arc-str
forgotten
Don't use 'impl Into<Arc>' for parameters that get called with '&String'. The std lib does NOT impl From<&String> for Arc, so callers passing &unique_name (where unique_name: String) fail to compile. Use 'impl AsRef' instead — it accepts &str, &String, String, Arc uniformly and Arc::from(name.as_ref()) produces an Arc. Hit during k9hr (2026-05-05): 19 call-site errors with first attempt at impl Into<Arc>. Fixed by switching to AsRef.
avoid-import-before-usage-ruff-strip
forgotten
The PostToolUse ruff --fix hook runs after EVERY Edit. If you add an import in one Edit and add its first usage in a LATER Edit, ruff strips the import as unused in between (you'll get F821 'Undefined name' at the end). Fix: add the usage first, or re-add the import after the usage edit. Seen consolidating RustFactoryPatch into tests/engines/conftest.py (angr-k8pc).
avoid-import-before-use-formatter-strip
forgotten
When adding 'import contextlib' (or any new import) to a Python test file in a SEPARATE edit BEFORE the edit that uses it, the PostToolUse ruff-format hook runs ruff --fix between the two edits and DELETES the still-unused import, leaving F821 'Undefined name'. Fix: add the import and its first use in the same operation, or re-add the import after writing the usage. Hit during angr-ge7l SIM105 cleanup (try/except/pass -> contextlib.suppress).
avoid-inline-callback-timing-wrappers
forgotten
sluuj.1 (inline callback timing wrappers into _inner) was DECLINED, not done. The 5 handle_callback/inner pairs in rust_callback_dispatch.py (syscall/find_predicate/avoid_predicate/symbolic_branch/python_vex_fallback) intentionally keep the split: the wrapper holds the perf_counter_ns timing (record_call in a finally), the _inner holds 38-98 lines of logic each already containing 2-3 nested try-blocks. Inlining adds a 3rd-5th try-nesting level on the hottest callback paths = WORSE, not cleaner. Do not re-open as a simplification. Closing sluuj.1 auto-closed epic sluuj (last child).
avoid-inline-elf-base-addr-confusion
forgotten
PITFALL when writing inline-ELF integration tests (encountered angr-gxhf.2): branch target addresses must be relative to ENTRY (BASE + EHDR_SIZE + PHDR_SIZE), NOT BASE. The Blob backend places code at base_addr directly, but ELF code lives at the file offset following ehdr+phdr — so for an ELF32 (52+32 header) code starts at vaddr = BASE+0x54, not BASE. First MIPS32 LE test attempt used 'find=BASE+0x14' and got 0 found / 2 deadended because targets were inside the ELF header bytes that wouldn't execute. Fix: compute targets as ENTRY + relative_offset.
avoid-inspect-state-id-negative
forgotten
AVOID passing state_id=-1 (or any < 0) to mgr.cb_inspect_mem{read,write}: _make_inspect_state_for falls back to _get_default_state() which returns the cached SimState, NOT a RustStateProxy. The user's BP action then reads state.inspect.mem_read_length from the SimState's SimInspector (None — never written to) instead of the manager's RustInspectProxy (where the dispatcher set the attrs). Symptom: user action runs but every state.inspect.* read returns None. Tests must use a real state_id from mgr._rust_mgr.get_state_ids('active')[0]. The production VEX-instrumentation path (uq4n.3) will always pass a valid state_id.
avoid-lazy-fork-assume-no-impact
forgotten
angr-hk7k 'extend lazy-solver materialization to assume_true/assume_false/add_constraint_raw' approach FAILS to deliver any speedup. Z3_MATERIALIZE_COUNT is unchanged with-and-without on both targets (fauxware: 2/2, mma_howtouse: 45/45). Reason: every fork that adds constraints immediately queries the solver via branch-feasibility check, eval, or SimProcedure callback — there are no forks that accumulate assume_* constraints and get pruned/dropped before any solver query. The save-by-not-materializing-early is paid back on the very next call. Confirms 'avoid-lazy-solver-fork' for branch-fork constraint paths too, not just SimProcedure-callback forks. How to apply: future angr-hk7k attempts need a different lever — e.g. push/pop scoping for branch-feasibility (where the solver IS materialized but the scope is short-lived) rather than skipping materialization entirely. Profile the fork+pop+drop pattern: that is where the push/pop scheme already wins for SimProcedure callbacks.
avoid-lazy-solver-fork
remembered
Lazy solver fork for callback states does NOT help. __libc_start_main and other SimProcedures always trigger solver.eval/check, so the lazy materialization adds no benefit there. The Rust-side optimization wins came elsewhere (e.g. native SimProcedures eliminating the callback round-trip). Why: tested in early lazy-solver work; callback paths invariably evaluate. How to apply: when designing a new lazy-X-on-fork optimization, profile first to confirm the fork path never queries the lazy state — if it does, the laziness is pure overhead.
avoid-memory-proxy-for-fetch-page
forgotten
ZeroPy fetch-page residual is NOT retired by the memory-proxy gate (measured, angr-gorvf.4 acceptance run, iter131): re-running the four sole-site benches with ANGR_RUST_USE_CALLBACK_MEMORY_PROXY=1 still FAILed on batch_fetch_pages, same magnitude. Root cause: callbacks.memory_is_rust_proxy() is only consulted in interpreter/statements_store.rs (the memory-STORE path); interpreter/prefetch.rs (fetch_page / fetch_pages_batch) never checks it. STATUS 2026-07-14 (iter132): the fetch-page site is now RETIRED by a different mechanism entirely — the setup-time servable-page snapshot, see zeropy-fetch-page-servable-snapshot — so this memory is kept only for the still-live fact about where memory_is_rust_proxy is read, which matters to the angr-grji4 proxy-flip work.
avoid-mgr-active-after-syscall-step
forgotten
When testing native syscall dispatch with a single-syscall shellcode + RustExplorationManager.run(max_steps=1), checking len(mgr.active) post-step can be 0 — the state goes to deadended/etc. The Rust unit tests in syscalls/.rs already pin per-handler return semantics; the Python integration test only needs to verify stats['syscall_python_fallback_count'] == 0 to confirm cross-FFI dispatch. Don't read back regs from mgr.active[0] after the syscall step. See TestNativeFdAllocatingSyscalls pattern (tests/engines/rust/test_syscalls.py, TestNativeFdAllocatingSyscalls).
avoid-mislabel-stale-test-as-isolation
remembered
DIAGNOSIS PATTERN: a Rust lib test that fails BOTH standalone AND in its full module run is a STALE/broken test, NOT a test-isolation flake. True isolation bugs pass standalone and fail only in the full suite (shared global/threadlocal/env state). Always confirm by running both: 'cargo test --release --lib ' (standalone) vs '... --lib ' (full module). Iter-15 handoff mislabeled test_phase4_wider_load_cache_fork_independence as 'test-isolation' when it failed unconditionally (stale assertion vs the 94c015db5 cold-cache change; fixed in angr-c9ovf / commit 5eeb1f45b). The other test it flagged (newfstatat_unsupported_arch_falls_back) passes both ways. As of commit 5eeb1f45b the full Rust lib suite is green (1510 passed, 0 failed).
avoid-monkeypatching-pyo3-classes
forgotten
PyO3 classes do not allow attribute monkeypatching: 'attribute is read-only' from unittest.mock.patch.object. To assert which FFI accessor a Python wrapper uses, build a stand-in Python recorder object that mirrors the FFI surface and pass it into the wrapper instead. Pattern used in tests/engines/test_rust_exploration.py::test_recent_bbl_addrs_uses_tail_ffi_not_export_state — see _RecordingMgr inner class. Validated 2026-05-25 on angr-kwpi.1.
avoid-multivalued-read-behavioral-test
remembered
AVOID_MULTIVALUED_READS behavioral test surface: PyRustSimState::probe_symbolic_load_value_range(addr,size) (native/angr/src/state/pymethods.rs — the #[pymethods] block moved there from state/mod.rs by god-object decomp angr-0mqkc.5 inc9) builds a STRUCTURALLY-symbolic 64-bit addr (RustBV::symbolic), pins it via ctx.assume_true(addr.eq(concrete)), then memory_load_symbolic + returns (min,max). KEY INSIGHT: a BVS constrained ==C in the solver still has as_u64()==None (structure, not solver state), so should_avoid_multivalued_read (concretize.rs) still fires while the OFF path deterministically concretizes to C. ON -> fresh unconstrained (min=0,max=2^(8*size)-1); OFF -> pinned to backer (min==max). For raw RustSimState tests, memory_store needs the page mapped first — use map_memory_data(addr,bytes) (auto-maps+writes), NOT memory_store (raises unmapped). Test: test_avoid_multivalued_read_behavioral_unconstrained_vs_pinned in test_misc.py.
avoid-mv-restore-stale-cargo-build
remembered
Restoring a source file with mv file.rs.bak file.rs after a temporary negation-verification patch makes cargo SKIP the rebuild: mv preserves the .bak's mtime, which predates the negated build, so cargo sees an unchanged file and the test binary still contains the negated code. Symptom: your just-restored tree 'fails' the tests that passed minutes earlier. Always touch the file after any cp/mv-based restore (or use git checkout -- <file>, which writes fresh). Cost 2 confused test runs in angr-9ke6b.97.
avoid-naive-max-merge-sync
forgotten
When implementing Rust↔Python state field sync, the max(rust, python) merge rule only works when both sides start at the same default. If the Python loader can set the field to a value different from Rust's hardcoded default (e.g., posix.brk via SimUserland), max() will silently corrupt Python's value on the very first export — Rust's untouched default may be larger than Python's loader-set value. Why: I tried max(rust, python) for posix_brk and the end-to-end test failed because fauxware's loader sets brk to 0x602000 < Rust default 0x1B00000, so max() returned the wrong answer. How to apply: before adding a Rust→Python sync helper, check whether Python's loader (angr/simos/simos.py) overrides the field on state creation. If yes, also add an init-push (Python→Rust) in angr/exploration/rust_manager.py::_add_rust_state so both sides agree at state construction. This is the pattern used for posix_brk in angr-as3c.
avoid-needless-pass-by-value-sweep
remembered
clippy::needless_pass_by_value nursery lint is NOT a viable autonomous sweep on native/angr (unlike redundant_clone, which is drained to 0 prod+test). Re-audited iter65: 98 hits on --lib, overwhelmingly PyO3 false-positives — #[pymethods] signatures REQUIRE by-value PyRef<'_,Self>/Bound/Py/Python tokens (e.g. segmentlist.rs::iter takes PyRef by value because the pyo3 macro mandates it; callbacks/mod.rs has ~11 hits all on pyclass method sigs). cargo clippy --fix would break the PyO3 boundary. Do NOT chase this lint wholesale. iter35's 'prod-only sweep found 1 real + 1 pyo3-false-positive' was a narrower/stale baseline; current reality is ~98 hits dominated by required pyo3 sigs. If ever revisited, hand-filter to non-#[pyclass] free functions only.
avoid-nop-padding-syscall-return-test
remembered
When testing native-syscall return register values in the Rust engine via load_shellcode, do NOT pad the shellcode with executable nops after the syscall instruction. A 'Continue' syscall successor is stepped further within the same run() call (max_steps=1 does not stop it), and post-syscall instructions clobber the return register ($v0 on MIPS) before you can read it. $a3-style error flags happen to survive nops but $v0 does not. Map ONLY the 4-byte syscall so the successor lift-errors immediately and deadends, freezing the post-syscall register values (angr-pfbu test design).
avoid-pending-api-never-loop-clippy
forgotten
Pre-existing clippy never_loop error at native/angr/src/exploration/pending_api.rs:339-342 (a 'while let Some(p) = current_parent { ancestry.push(p); break; }' pattern). Exists on rust-symex HEAD as of 2026-06-01, not caused by recent work. Reachable via 'cargo clippy --manifest-path native/angr/Cargo.toml --release --all-targets'. If a task's acceptance criteria asks for 'cargo clippy clean', either prefilter this error or fix it as a tiny prerequisite refactor (most likely the loop should be an 'if let' since it always breaks).
avoid-pending-store-partial-overwrite
forgotten
pending_stores load forwarding has a known but pre-existing semantic quirk: the reverse-scan-first-fully-covering-store-wins logic returns wrong bytes when a smaller more-recent store partially overwrites a previously stored larger range. E.g. store [0x100..0x108) then store [0x102..0x104) followed by load [0x100..0x104): the 4-byte load returns the OLD bytes from the larger store, not the new bytes from the partial overwrite. PendingStoreBuffer preserves this exactly via the slow-path reverse-scan fallback. Fixing this would require returning byte-by-byte composed values; out of scope for the indexing optimization.
avoid-per-crossing-cost-without-warmup-split
remembered
Single-crossing benches LIE about per-crossing SimProcedure cost — always subtract warmup (angr-gorvf.9). The process's FIRST simprocedure bounce pays one-time cost (lazy angr/claripy imports, SimProcedure machinery, page-cache fill) that steady-state crossings do not. Raw counters made 'strncpy' look like ~197ms and 'open' ~60ms on their single crossings, which drove a wrong 'port these procs natively' conclusion in iter136. RustPerfTracker.add_simprocedure_phase (angr/exploration/rust_perf_tracker.py) now banks first-crossing twins: callback_simprocedure_first_{state_create,execute,sync_back,state_copy}_ns. Subtract them from the totals before dividing by crossing count. On the multi-crossing benches the first crossing's execute is only ~2ms, proving the bodies are cheap; warmup is 26% of the whole class.
avoid-per-tree-lineage-dismantle
remembered
angr-g1fev (2026-07-13) KILL: the shared-lineage dismantle decision cannot be made per-lineage-tree. Measured with the new lineage::tree_census_stats() counters (emitted from get_solver_stats; a 'tree' = one SharedLineageSolver Arc minted by the SymContext::fork gate), run with --use-shared-lineage-solver. google2016_unbreakable_0 (the +10% anti-case): 16 trees minted, 9 ever switch, MAX 6 switches on any tree — no per-tree window (even 4) ever collects a sample, so a per-tree valve can never fire; its cost is paid at FORK time (fresh solver mint + loss of per-context incremental state; 22 switches / 4 checks but +57ms check time), not at switch time. ekopartyctf2016_rev250 (the -10% win): 59 trees, 44 switch, only 9 reach 8 switches, 1 reaches 24, max 47 — the 39% global hot ratio is an AGGREGATE over many short-lived trees, and the single long-lived tree is COLD at its late window, so a re-evaluating per-tree valve would dismantle the tree carrying the win. Population finding: minted trees are overwhelmingly stillborn (75% of rev250's never reach 8 switches), so 'the tree' is the wrong decision unit — no per-tree steady state exists to detect. Do NOT retry any hot-ratio-shaped valve (global, per-tree, or re-evaluating); only a PRE-FORK predictor of mint cost could move this. Full table in tools/decisions/solver_pool_design.md sec 7b.
avoid-pip-editable-rust-rebuild
forgotten
Editable install (pip install -e . --no-build-isolation --no-deps) does NOT rebuild the rust extension. After Rust changes you must run cargo build --manifest-path native/angr/Cargo.toml --release and then cp target/release/librustylib.so angr/rustylib.cpython-312-x86_64-linux-gnu.so. Symptom: pip install reports 'Successfully installed' but angr/rustylib.*.so timestamp unchanged, native procedures not registered when imported.
avoid-pip-install-deps
remembered
NEVER run 'pip install ' (pyvex, claripy, archinfo, cle, z3-solver) in the angr venv. These are pinned to 9.2.209 in pyproject.toml and the venv is hand-assembled via cargo-direct-copy (pip list only shows pip/platformdirs/rust-demangler — angr deps live in site-packages but pip doesn't track them). 'pip install' would trigger a reinstall cascade that has historically downgraded the dev pins (9.2.210.dev0 → released) and broken the build. SONAME audit (angr-3mkf, 2026-06-05): claripy 9.2.209 and 9.2.221 (latest PyPI) BOTH pin z3-solver==4.13.0.0, so the originally-feared SONAME mismatch is dormant — the actual current risk is venv corruption from cascade reinstalls, not a different libz3. Upstream master pins 9.2.222.dev0 (not on PyPI, requires source builds from angr/{archinfo,claripy,cle,pyvex} git checkouts) and bumps pypcode 3.x→4.x. Re-evaluate when upstream cuts a stable 9.2.222+ release.
avoid-pip-install-no-rust-build
remembered
If 'pip install -e . --no-build-isolation' silently produces no .so file (rustylib*.so missing, ImportError on 'from angr.rustylib import SegmentList'), check whether setuptools-rust's metadata is missing: 'ls .venv/lib/python3.12/site-packages | grep -i rust' should show BOTH 'setuptools_rust' AND 'setuptools_rust-X.Y.dist-info'. If the dist-info is gone, the entry-point hooks (build_rust command, finalize_distribution_options) aren't registered and 'running build_ext' never fires the cargo step. Fix: 'pip install --force-reinstall setuptools-rust' restores the metadata. The pyc files alone (without dist-info) load fine but no entry points are visible — verify with 'python -c "from importlib.metadata import entry_points; [print(e) for e in entry_points() if "rust" in e.value.lower()]"'.
avoid-porting-full-simprocedure-body-without-checking-entry-vs-continuation
forgotten
When asked to implement a native SimProcedure as a perf optimization, do NOT default to porting the full Python body. Check first: (1) which addresses (entry vs continuations) the procedure fires at during Rust exploration, (2) whether rust_manager._step_python_to_main caches the init path. For procedures whose entry path runs only at init time and gets cached, the high-frequency cost is the after-X continuation reached via main RET. A minimal native that mirrors the continuation's semantics (often just exit→deadend) can deliver the entire perf win at a fraction of the implementation cost. Example: angr-yrhh — Python __libc_start_main has ~150 LOC of malloc-table + call-main setup, but during Rust exploration only after_main fires, so a 5-line no_return=true Rust deadend captures all 47 callbacks/run on ais3_crackme.
avoid-post-cancel-test-flake
forgotten
The Rust scheduler unit test test_post_cancel_steps_counts_peer_speculation (scheduler.rs) is TIMING-FLAKY: it asserts post_cancel_steps>=1 by racing a 50ms peer sleep against the finder's immediate cancel on a 4-worker ParallelScheduler. Under contended CPU (running the full 'cargo test scheduler' suite in parallel) the finder's cancel can quiesce the pool before any peer commits a step, so it intermittently panics 'at least one peer must commit a step'. Passes reliably in isolation (cargo test --lib ). Not a correctness bug and unrelated to policy changes (uses run_instrumented/Lifo default). If it flakes in the loop, re-run isolated to confirm before investigating.
avoid-pre-pinning-recovered-symbols
forgotten
Page-recovery pre-pin (rust_state_export.py _apply_symbolic_constraints) and _replace_with_rust_snapshot were both holdovers of the pre-pinning recovery era. Closed by angr-uwfs (8d80a689c reverts dead method; 70d84f909 deleted zombie). The canonical eval-time mechanism is RustSolverFallback monkey-patching state.solver.eval; never reintroduce 'state.solver.add(ast == BVV(rust_eval))' patterns — they overconstrain and were the source of the fauxware/flareon2015_5 wrong-answer regression (2f0e8164f rollback).
avoid-prepinning-export-constraints
forgotten
On rust-symex state export, never add ast==BVV(concrete) constraints for tracked symbols to 'help' the Python solver. Tried in pre-2f0e8164f and rolled back — concrete values from get_state_memory() can disagree with the actual model the rest of the solver expects, overconstraining the state and breaking fauxware + flareon2015_5. The accepted fix is _attach_rust_solver_fallback monkey-patching state.solver.eval to route through fork_state_solver(state_id) — it leaves the constraint set alone. Wired in rust_state_export.py, rust_state_proxy.py, rust_state_cache.py; must be attached on every export path (cached, root-copy, stepping, snapshot — the snapshot path was originally missed). Page-recovery pre-pin (rust_state_export.py _apply_symbolic_constraints) and _replace_with_rust_snapshot were both holdovers of the pre-pinning recovery era; closed by angr-uwfs (8d80a689c reverts dead method; 70d84f909 deleted zombie). The canonical eval-time mechanism is RustSolverFallback monkey-patching state.solver.eval; never reintroduce 'state.solver.add(ast == BVV(rust_eval))' patterns. See angr-wvxj (2026-05-13) for the memory distillation. Cross-ref: constraint-export-no-pre-pin (mandatory keep).
avoid-print-stdout-in-run-single-probes
forgotten
tests/benchmarks/run_single.py swaps sys.stdout for a BufferedStringIO (test_utils.BufferedStringIO) while the solve script runs, so a print() inside an angr callback VANISHES. Any ad-hoc probe inside RustExplorationManager callbacks must print with file=sys.stderr (and be read via '2>&1 >/dev/null | grep'). Cost ~10 wasted turns in iter130 before this was spotted.
avoid-proxy-addr-for-frontier-fingerprint
forgotten
mgr.active proxy .addr can LIE: it is served from the Python _state_cache and can lag the real Rust pc by a block (repro at workers=1 on the pbounce synthetic; clearing mgr._state_cache makes the proxy view match Rust exactly). Any structural fingerprint / parity check over the frontier must read PCs from Rust (mgr._rust_mgr.get_state_pc_by_id over get_state_ids(stash)), NOT from proxy .addr — see _fingerprint in tests/engines/rust/test_parallel_wave.py. Bug tracked as angr-ibx8j.
avoid-proxy-test-addr-eq-source-state
forgotten
When testing RustStateProxy.addr against a source SimState passed to RustExplorationManager, do NOT assert proxy.addr == source_state.addr. The Rust manager internally advances PC during state initialization (e.g. fauxware: source.addr=0x400580 (_start) but post-init mgr.active[0].addr=0x40071d). Instead, assert proxy.addr == mgr.active[0].addr (or mgr.found[0].addr, etc.) — the full-SimState path agrees with the proxy because both read from the same Rust state. Discovered while writing TestStashProxyAccessors.test_active_proxies_returns_rust_state_proxy in angr-kwpi.3.
avoid-pyo3-init-test-failures
forgotten
Pre-existing cargo test failures (unrelated to changes) that surface in cargo --lib runs without Python::initialize(): procedures::malloc::tests::test_heap_metadata_cloned_on_fork, procedures::getenv::tests::test_getenv_env_preserved_on_fork, syscalls::brk::tests::fork_preserves_posix_brk, syscalls::mmap::tests::fork_preserves_mmap_base. All four panic with 'The Python interpreter is not initialized and the auto-initialize feature is not enabled' because they use Python::attach() / fork-and-resync paths that touch PyO3 inside cargo test where auto-initialize isn't on. Verified pre-existing on e6c81f093. Don't treat as regressions when verifying refactors.
avoid-python-cross-process-snapshot-test
remembered
Cross-process snapshot tests in tests/engines/rust/ ARE fine to write — the earlier order-fragility report was a bad assertion, not real (see foreign-snapshot-zero-constraints-root-cause). Pattern that works: subprocess dumps the snapshot via a module-level dumper string, parent seeds+steps a manager, load_snapshot, then assert on mgr._rust_mgr.export_state_constraints(sid) — never on the SimState mirror's claripy constraints. Live example: tests/engines/rust/test_misc.py::TestForeignProcessSnapshot. The Rust-level guard stash_tests.rs::test_foreign_envelope_rebases_symbol_ids still covers the id rebase.
avoid-python-gate-for-cancel-drain
forgotten
Bug M1 (parallel cancel drops the frontier) is NOT observable on fauxware or the 8-leaf pbounce synthetic, so do not try to gate it with a real-binary Python test asserting parallel_residual_drains > 0. Reasons: fauxware reaches accepted with an empty worker-local queue; on pbounce the trap_point bounce means only ONE bounce is dispatched per run(), so each wave is seeded with 1-3 states; unhooked, the DFS has already dispatched most of the 8-leaf tree by the time a leaf is found. A seeded-wide-frontier variant (40 entry-state copies with find == the seed PC) does not work either — the Rust engine never routes those to FOUND and the test explores all 40 states (~98s). The real gate is the Rust unit test scheduler::tests::test_wave_cancel_drains_residual_frontier (WIDTH=40 fan-out, exact conservation).
avoid-python-solver-fallback-on-unsat
remembered
On a Rust-engine state the constraints live in the RUST solver context, not in the Python one (state.solver.constraints is often EMPTY on a found state). So whenever a Rust solver entry point returns None, falling through to the Python solver is NOT a safe fallback: with an empty constraint set every expression looks unconstrained and eval happily returns ZEROS. That is how an unsat found state silently produced an all-zero "solution". RustSolverFallback.eval (angr/exploration/rust_state_export.py) now disambiguates the way min()/max() already did — if rust_ctx.satisfiable() is false, raise SimUnsatError instead of delegating. Apply the same check to any new original delegation you add there. (angr-ue4ro)
avoid-record-stdin-symbol-mismatch
forgotten
NativeRead procedure and the new NativeReadSyscall (angr-0z34) DO NOT call state.record_stdin_symbol when creating fresh stdin bytes — but NativeFgets, NativeFgetc, NativeGetchar (procedures/fgets.rs) DO. Result: posix.dumps(0) sees fgets-introduced bytes but not read-introduced bytes. This is a pre-existing gap inherited from when NativeRead was first written — not introduced by 0z34. If a benchmark/user complains that 'posix.dumps(0) returns empty after a read syscall', the fix is to add record_stdin_symbol calls in BOTH NativeRead (procedures/read.rs:80ish) AND NativeReadSyscall (syscalls/read.rs:90ish). Symbol names today: 'stdin_' for the procedure, 'sys_read_' for the syscall.
avoid-redundant-thrash-tests
forgotten
angr-0hdq.2 (commit ec9984782, 2026-05-25): Acceptance criteria (i) cold-workload-fires and (ii) hot-workload-stays-on were ALREADY covered by tests added in angr-v5ht (test_sample_for_thrash_cold_workload_triggers, test_sample_for_thrash_hot_workload_stays_on at native/angr/src/symbolic/lineage.rs ~line 909/942). When evaluating a 'add behavioral tests for X' bead, grep for existing tests first — the cold/hot 'baby-re-shaped'/'ais3-shaped' framing is a re-articulation of test coverage that already shipped with the feature.
avoid-reset-solver-stats-in-tests
remembered
reset_solver_stats() must never be called from a Rust unit test. Six files delta-assert process-global stats counters inside measurement windows -- symbolic/context_tests/constraints.rs (plain after-before on ADD_CONSTRAINT_RAW_DEDUP_HIT_COUNT, underflows), syscalls/fd_io_tests.rs (symfile_reads_native, plain subtraction), memory/tests/symbolic.rs, memory/tests/ite_dedup.rs, symbolic/context_tests/solver.rs, symbolic/context_tests/lineage.rs (post>pre style, fails rather than underflows) -- so no lock short of a crate-wide one makes a reset call safe under the parallel runner. Cover reset structurally instead: stats.rs's MEASUREMENT_COUNTERS table is looped over by BOTH get_solver_stats() (insert_measurement_stats) and reset_solver_stats() (reset_measurement_counters), which makes 'emitted but never cleared' unrepresentable, and each loop is unit-tested against local AtomicU64s. Pair that with a key-list assertion (test_measurement_counter_keys_are_stable) -- mutation-verified as the only test that catches a typo'd key string, since the marker-bump wiring test reads keys from the same table.
avoid-run-single-parallel-fauxware-false-alarm
forgotten
run_single.py fauxware under RUST_PARALLEL_WORKERS=2 FAILs with 'IndexError: list index out of range' at ~0.27s. This is PRE-EXISTING on HEAD (verified via git-stash + rebuild against b9fc5f002), NOT a regression from parallel-policy changes. Root cause is harness-level: the fauxware solve-script main() indexes into found[] assuming a serial-path count/order that differs under parallel dispatch. The pytest test_parallel_wave.py suite (uses fauxware_project with workers) passes fine, so the engine parallel path is healthy — only the run_single benchmark harness assumes serial result shape. Do NOT treat this run_single parallel FAIL as a scheduler bug.
avoid-rust-mgr-extra-pages-symbolic-skip
forgotten
When a Python angr blank_state has user-stored pages (via state.memory.store) that haven't been fully concretized, RustExplorationManager._sync_memory_to_rust SKIPS them as 'symbolic_pages' because the pages' symbolic_bitmap still has unfilled bits. Even with ZERO_FILL_UNCONSTRAINED_MEMORY, the bitmap stays True for untouched bytes (the option only changes resolution, not the bitmap). To force Rust to see the page contents, fill the entire 4 KiB page with concrete bytes — then symbolic_bitmap-any becomes False and _sync_extra_python_pages overlays the page into rust_state. This is why integration-style tests through the manager couldn't be used for angr-jdz9; switched to a Rust unit test against SymbolicMemory.load_symbolic_unified directly.
avoid-rust-mips-symbolic-accumulation-bench
forgotten
[FIXED 2026-05-14 by angr-g9hy commit ceba7816b] Was: Rust engine intermittently collapses $t0 to concrete zero in MIPS32 symbolic accumulation chains. Root cause turned out to be the disk init cache invalidation in _state_has_user_symbolic — it only scanned MEMORY for user symbolic data, not REGISTERS, so 'state.regs.a0 = BVS()' didn't invalidate the cache, the cached blank_state replaced the user's state, and the symbolic a0 was silently lost. Fix: extended _state_has_user_symbolic to also scan state.registers._pages.symbolic_data. The 'workaround' (single ADDIU/SLL chain, no multiple blocks) is no longer required — multi-block accumulation chains with symbolic registers work correctly on MIPS32 (and presumably all other arches, since the bug was arch-agnostic).
avoid-rust-tracking-actions-silent-ignore
forgotten
When users rely on state.history.actions or unsat_core() under the Rust engine, they will silently get empty/missing data. The TRACK_*_ACTIONS, TRACK_ACTION_HISTORY, TRACK_MEMORY_MAPPING, and CONSTRAINT_TRACKING_IN_SOLVER options are silent-ignored — Rust produces no SimAction stream and does not surface unsat_core. Recommend explicitly rejecting these on add() rather than silent-ignore. See docs/advanced-topics/rust_engine.rst SimOption coverage matrix.
avoid-rustbv-enum-field-add
remembered
Adding fields to RustBV enum is a 67-callsite refactor: RustBV::Expression is constructed in 67 places across native/angr/src/{symbolic/value.rs+value_ops.rs+value_z3.rs, vex/ops.rs, ...}. Avoid adding fields to RustBV variants — prefer side-tables keyed by Arc::as_ptr(&operands) (operands Arc is stable across RustBV::clone() refcount-bumps and unique per builder call). When the side-table value also holds a clone of the BV, pointer reuse is structurally impossible while the entry is live (the clone keeps the Arc refcount > 0).
avoid-sampling-sweep-complete-iter508
forgotten
avoid-* memory sampling sweep complete as of iter 508 (2026-06-06). All 41 keys sampled across iters 504-508. NO merge candidates remain — every avoid-* describes a distinct mechanism / failure mode. Pairs that look similar by title were verified orthogonal: (1) avoid-hasattr-lazy-init [caller side: SimState plugin lazy init via hasattr → 80ms heap init] vs avoid-getattr-private-dunder-recursion [implementer side: _LazySimStateRef.getattr must filter dunder/private], (2) avoid-check-assumptions [Z3 push/pop antipattern] is a 4th distinct Z3 trap alongside avoid-z3-optimize-for-min-max / avoid-z3-parallel-enable / avoid-z3-solver-set-params-for-random-seed, (3) avoid-state-copy-optimization [extern SimProc register sync correctness] vs avoid-weakref-callbacks [per-call closure overhead], (4) avoid-deferred-fork-base-mismatch [stepping.rs SemiProc-native vs MaxBlocks divergence] vs avoid-deferred-prem-memory-layer [MemoryLayer trait audit/rejection]. Skip re-sampling unless a new avoid-* memory is added — track count via 'bd memories | grep " avoid-" | wc -l' (was 41 on this date).
avoid-scratch-scripts-in-tmp
remembered
Scratch/debug Python scripts must NOT live in /tmp for this repo. A stale /tmp/attr.py (a hand-written bench-classifier script from an earlier session) SHADOWS the real 'attr' package for any python script run from /tmp, because sys.path[0] is the script's directory. rich/pretty.py does 'import attr' during 'import angr', so the shadow file EXECUTES — in that case it launched an 11-bench run_single.py sweep and made 'import angr' appear to hang for minutes. Cost ~15 tool calls of misattributed debugging in iter90. Use ~/probe/ (or tests/benchmarks/) for scratch harnesses instead, and if 'import angr' ever hangs, dump the traceback with PYTHONFAULTHANDLER=1 + 'kill -ABRT ' and look for a /tmp frame.
avoid-self-state-during-setup-callbacks
forgotten
When refactoring _setup_callbacks in angr/exploration/rust_manager.py to drive registration from a data table, DO NOT read self. — _setup_callbacks is called from init BEFORE many instance attributes are set (lines 882 vs 995+). Symptom: AttributeError: _INSPECT_EVENT_BITS via getattr fallback at rust_manager.py:4366. Fix: read directly from the module (e.g. from angr.exploration.rust_state_proxy import _RUST_INSPECT_EVENT_BITS) rather than from self. This keeps _setup_callbacks independent of init ordering and avoids fragile move-the-block-up surgery.
avoid-set-of-simstateoptions
forgotten
set(state.options) on an angr SimStateOptions raises SimStateOptionsError (not all internal _options keys are valid Boolean switches — iteration triggers per-key validation). Pull enabled options from state.options._options dict directly: {name for name, value in state.options._options.items() if value is True}. Hit during angr-t3l3 (RustStateProxy options seeding); silent except-block hid this until DEBUG logs surfaced 'The state option "0" does not exist.'
avoid-silent-cc-fallback
forgotten
ANTIPATTERN: returning a fallback default in a CC-/arch-dispatch function. The latent bugs angr-gzk8 (MIPS64→AMD64 silent miscompile) and 5329d8222 (x86 Cdecl return-register routed to RDX instead of EAX) both hid for months precisely because default_cc_for_arch returned SystemVAMD64 on unmatched lookup instead of failing loudly. Future arch/CC dispatch functions should panic, error, or assert on unknown input — silent fallbacks are a deferred correctness landmine.
avoid-silent-fallback-masks-coverage-gaps
forgotten
PATTERN (confirmed twice — angr-bkcs NEON, angr-tkbr.2 unmapped opcodes): a 'silent fallback' path (log::warn! + fresh-symbolic / Raw(0)) is dangerous because it converts coverage gaps into wrong results that aren't attributed to the real cause. Symptoms: test 'just happens to pass' because the resulting wrong value doesn't break the assertions; failures show up later, far from the cause, looking like state-management bugs. Fix pattern: route the silent path to a typed error and forward it past the silent-fresh-symbolic catch-all in interpreter_cb/expressions.rs. The conversion will expose latent gaps (in angr-tkbr.2 it surfaced Iop_CasCmpEQ string-path miss); plan to fix one or two before merging.
avoid-silent-no-op-callback-fallbacks
remembered
AVOID silent no-op fallbacks in PythonCallbacks call_* methods (e.g., the removed call_memory_store_symbolic_ast in callbacks/dispatch.rs returned Ok(()) doing nothing when its hook was unset). They mask wiring bugs: the engine appears to run but stores are silently dropped, leading to divergent memory between Rust and Python. Prefer hard errors that surface 'no such callback' to debugging silent corruption. Pattern: match the explicit-error idiom in PythonCallbacks::call_lift_block (callbacks/dispatch.rs) -- ok_or_else into PyRuntimeError::new_err(" callback not set") when the hook is None. The enforcement site is dispatch_tests::unset_dispatch_callbacks_hard_error, which probes every call_* on the struct; add a probe there for any new callback. NOTE (angr-9ke6b.18, 2026-08-01): the old exemplar in this memory and in the callbacks/mod.rs module docs was call_on_hook, which was DEAD -- no Rust caller and no Python registration -- and has been deleted along with call_on_syscall and the CallbackSite::{OnHook,OnSyscall} profiling variants. Pick exemplars that production actually exercises; a dead exemplar cannot regress-test the invariant it advertises.
avoid-silent-zero-raw-fallback
forgotten
Before angr-n28w, IROp::Raw(opcode) for unhandled FP ops returned RawOpcode err from VEXOps::binop, then expressions.rs IRExpr::Binop/Triop or_else fallback substituted RustBV::concrete(0, 64) for concrete inputs and fresh-symbolic for symbolic inputs. So transcendentals on concrete f64 silently returned 0 — semantically WRONG, not a Python fallback as the bead description claimed. sokohashv2 (only fast-tier benchmark with x87 fyl2x/fscale/f2xm1) was passing in 12s because the codepath using these instructions wasn't on the find-trace OR the fresh-sym substitution happened to constrain correctly. Surprising root cause for any future correctness regression: silent zero from Raw fallback. Memory written 2026-05-07 commit ef020d101.
avoid-simfile-assumptions-on-posix-streams
remembered
angr's default posix stdout/stderr streams are SimPackets(Stream), not SimFile: no .size BVV and no .load(). SimPackets keeps .content as a list of (data_ast, length_ast) packets; total length = sum of packet lengths, and the payload of a packet is the LEADING length bytes of data_ast (see SimPackets.concretize slicing x[0][:size - i*byte_width]). Any code that measures or slices a posix stream must handle both flavors — rust_state_export::_concrete_stream_len / _concrete_stream_bytes / _posix_stream are the shared helpers (module-level, imported by rust_callback_dispatch). First bite: the angr-op0dn.14.1.4 outbound sync used stream.load() and blew up with 'SimPacketsStream object has no attribute load'.
avoid-snapshot-overlay-sidecar
forgotten
Bucket-D Py overlays (symbolic_pages / hook_symbolic_memory / addr_to_ast) are NOT the cause of snapshot search-truncation, despite the dump_snapshot docstring flagging them as the known gap. Measured (angr-op0dn.13.14, iter 37): every pbounce frontier state carries 45 symbolic_pages + 45 addr_to_ast entries that the snapshot drops; capturing them into a pickled sidecar and restoring them via set_state_symbolic_pages / set_state_addr_to_ast changed the resumed leaf count by ZERO (4/8 before and after). The Rust interpreter reads its own native symbolic memory (SymbolicMemorySnapshot, which IS serialized); the overlays are a Python-export-side mirror. Don't spend another iteration on an overlay sidecar.
avoid-snapshot-pc-and-name-counter-theories
forgotten
angr-op0dn.13.14 falsified theories (do not re-litigate). (1) claripy's process-global BVS name counter is NOT the coupling behind the warm-vs-cold resume gap: burning 64 dummy claripy.BVS before constructing the resumed manager leaves a cold resume at exactly 6/8 leaves. (2) Restored PCs and state_ids are byte-exact — comparing _rust_mgr.get_state_ids(stash) + get_state_pc_by_id() pre-dump vs post-load matches perfectly. An apparent PC mismatch seen through the Python state proxy (.addr) is a proxy/materialization artifact, NOT a snapshot bug — always confirm PC claims against the Rust core accessors, not the proxy. (3) Constraints round-trip intact: analyze_constraint_sharing() reports identical states_analyzed/constraints_analyzed/unique_shapes pre vs post (310/115 on fork_solve_pbounce_W3_S2_M8_B1); only unique_pointers rises, which is expected Arc-sharing loss. Reference numbers on that bench: fresh run 8/8 leaves, dump-then-continue-in-same-manager 8/8, cold resume 6/8 and exhausts (active=0). Remaining suspect: fork/branch bookkeeping that lives outside self.sm and never reaches the envelope — same class as the .13.10 parked-bounce queue. See [[snapshot-bucket-d-overlay-loss]].
avoid-snapshot-ptr-dedup-py-vs-rustbv
forgotten
Avoid ptr-keyed dedup between Python-claripy ASTs and RustBV-rebuilt ASTs (angr-82g6, 2026-06-02): The first approach for snapshot constraint capture tried to dedup the SMT-LIB2 solver dump against assumed_constraints replay using Z3 hash-cons ptr-equality (parsed-assertion.get_z3_ast().as_ptr() vs assumed-replay-rebuild.get_z3_ast().as_ptr()). This FAILED for the Python claripy-sync path. Specifically: in _add_constraints_to_state, the fast path calls add_constraint_raw(z3_ast) with the Python claripy backend's Z3 AST AND assumed_constraints_push(bv, true) where bv came from claripy_to_rustbv (a parallel BV→Z3 translation). The two Z3 ASTs are structurally different (Python uses one canonical form, claripy_to_rustbv uses another via build_z3_ast_cached), so they hash-cons to different pointers. Result: dedup misses, snapshot round-trip over-counts. Correct design (which we landed): replay the two captures independently — SMT-LIB2 dump via add_constraint_raw, BV log via assumed_constraints_push directly. See snapshot-solver-smtlib2-design.
avoid-snapshot-registry-rewarm
forgotten
Snapshot resume leaf-loss (angr-op0dn.13.14) is NOT a claripy-bridge cache problem. Both candidate fixes were built and measured as exact no-ops: (a) eagerly re-registering every restored leaf (id,name,width) in the global SymbolicIdentityRegistry at load time by exporting it through rustbv_to_claripy, and (b) calling claripy_bridge::reset_for_new_exploration() inside RustExplorationManager::load_snapshot_bytes. Drained-leaf count stayed 4/8 (warm, in-process fresh manager) and 6/8 (cold, separate interpreter) under every combination. Do not re-litigate registry warmth. The live differentiator is COLD-vs-WARM PROCESS, and cold is BETTER — suspect claripy's process-global BVS name counter: a cold resume re-mints the same symbol names the dumping run used, a warm one does not. Probes: /tmp/pb_dump.py + /tmp/pb_resume.py.
avoid-solver-bridge-protocol
forgotten
angr-x3xu proposed defining a SolverBridge Protocol so the manager codes against an abstract solver interface (eval/satisfiable/min/max) instead of a concrete Rust type. Rejected after code review (2026-05-07). Why: angr/exploration/rust_manager.py contains ZERO references to RustSolverContext / RustSolverFallback / RustSolverProxy — the manager already has no direct coupling to decouple. The Rust solver is reached either via self._rust_mgr.fork_state_solver(state_id) (returns a PyO3 RustSolverContext used as a duck-typed object) or via RustSolverFallback (rust_state_export.py:20) which patches state.solver post-exploration. Both already act as duck-typed wrappers around the same set of methods. No tests substitute or mock the solver — Grep on tests/ for mock.solver / FakeSolver / StubSolver finds nothing. Adding a Protocol would document the existing duck-typed contract but provide no decoupling gain because there is no caller to redirect away from a concrete import. The bead's value-prop ('lets us swap the backend in tests') is hypothetical — without an actual second implementation or test harness, the Protocol is busywork.
avoid-space-separated-git-rev-parse-arg
forgotten
Bug found while wiring run_regression.py save_history_record (commit 540313127, 2026-05-15): _git_revparse('--abbrev-ref HEAD') passed a single space-separated string as one argv entry, so git parsed '--abbrev-ref HEAD' as a literal ref and returned that string verbatim instead of the branch name. Pattern: subprocess argv lists need flag/operand split — _git_revparse('--abbrev-ref', 'HEAD') is correct. Caught only when sanity-checked the JSON output (branch field looked obviously wrong). Lesson: any subprocess.run wrapper that takes user-supplied 'arg' strings should use *args and document the splitting requirement.
avoid-spawn-pythonpath-broken
remembered
run_regression.py / run_single.py spawn subprocesses via multiprocessing 'spawn' context — this DOES NOT inherit the venv site-packages. The venv has _editable___angr*_finder.pyc but no .pth file, so 'import angr' fails in spawn workers. Workaround: prefix with PYTHONPATH=/home/ubuntu/repos/angr. Symptom: 'No module named angr' from every benchmark. Tests via 'python -m pytest' work because they don't use spawn.
avoid-split-import-edit-ruff-autofix
remembered
GOTCHA (edit-hook autofix): the PostToolUse ruff hook (ruff --fix / ruff-format) runs after EVERY Edit. If you add an import in one Edit and its first USAGE in a LATER Edit, ruff's F401 unused-import autofix DELETES the import between the two edits (it was momentarily unused), leaving F821 'Undefined name' / NameError at runtime. Fix: add the import and at least one usage in the SAME edit, or re-add the import after writing the usages. Hit several independent times: adding 'from unittest import mock' to test_plugins.py (angr-szg45.1), consolidating RustFactoryPatch into tests/engines/conftest.py (angr-k8pc), adding 'import contextlib' during the angr-ge7l SIM105 cleanup (try/except/pass -> contextlib.suppress), and adding 'import sys' to angr/exploration/rust_manager.py for the find-all num_find=None sentinel (ralph iter1, 2026-07-16) — 3 tests failed with NameError until re-adding the import after the usage existed.
avoid-split-import-usage-edits
forgotten
PostToolUse ruff autofix strips a just-added 'import X' if X is not yet USED in the file at that moment. When adding an import plus its first usage across SEPARATE Edit calls, the hook runs after the import-only edit (import unused -> deleted), so the later usage edit then NameErrors at runtime. Fix: add the import and its usage in the SAME edit, OR add the usage first. Bit me adding 'import sys' to angr/exploration/rust_manager.py for the find-all num_find=None sentinel (ralph iter1, 2026-07-16) — 3 tests failed with NameError until re-adding the import after the usage existed.
avoid-stale-bug-bead-acceptance
forgotten
Pattern: when picking up an in_progress bead, FIRST grep bd for closed beads with similar symptoms (here: angr-fv81 closed 2026-05-14 with same IndexError symptom that angr-y95g 2026-05-30 described). Stale beads accumulate when a memory promised 'open as a new bead' but the underlying bug was actually fixed shortly after under a different ID. Same lesson as avoid-stale-perf-bead-acceptance but for bug beads. Cheapest check: bd show ; bench-run to verify current behavior matches the original symptom before any code work.
avoid-stale-orchestrator-bytecode
forgotten
Long-running run_optimization_loop.py processes do NOT pick up source-file edits — Python doesn't reload modules. If you fix a bug in the orchestrator while it's running, the fix only takes effect when the process restarts. Symptom: log entries from after the fix's commit time still show pre-fix behavior. The angr-p5hl confusion came from this: angr-lbze landed correctly but iter 48-50 of the still-running orchestrator (started before the commit) kept executing pre-fix bytecode. Detection: as of 6b21f72a5, the orchestrator computes _orchestrator_source_hash() at startup and at each iteration; a 'ORCHESTRATOR SOURCE STALE' warning fires once when the on-disk hash diverges. Fix: kill and re-launch run_optimization_loop.py whenever you commit changes to it.
avoid-stale-parking-beads
forgotten
Pattern observed 2026-05-23 iter 13: parking-spot beads (the P4 'place to land if X comes up' tier) accrue stale descriptions because subsequent work landed without back-references. Example: angr-86fa filed 2026-05-19 claiming Tactic/Goal/Probe were not exposed in native/z3-patched; one day later angr-ya00 (commit 2e27425ba) used z3::Tactic + z3::Probe + Tactic::cond at native/angr/src/symbolic/context.rs:810-819, fully satisfying both acceptance criteria, but the bead was never refreshed. Before claiming a P4 parking bead, do a 60-second sanity check: grep the codebase for the named missing API and skim git log -S for the keyword. Many parking beads are already satisfied by intervening epic work. Closing them is a high-value, low-risk session move.
avoid-stale-perf-bead-acceptance
remembered
Before claiming an optimization-spike bead, grep the codebase + git log for the central artifact named in the description. angr-eyt7 (2026-05-30) was filed claiming RustExplorationManager._loader_pages_cache was 'assigned but never read', but angr-bzsc (commit f7873d9de, 2026-05-19) had wired it up 11 days earlier via _get_loader_pages_cache in rust_state_sync.py:348. Verifying first turns a 'do the work' task into a 'document the closure' task. Pattern: grep -rn '<symbol>' angr/ + git log --oneline -S '<symbol>' -- angr/ + bd memories <subject-keyword> BEFORE touching any code.
avoid-stash-z3-leak
forgotten
BOTTLENECK: Avoided states hold full Z3 solver clones in Rust VecDeque. hackcon2016 accumulates hundreds → 7x slowdown + OOM risk. Drop immediately at all push sites in exploration.rs (lines 994, 1075, 1400, 1638, 1645, 1670).
avoid-state-copy-optimization
forgotten
AVOID removing state.copy() for extern SimProcedures without replacing it with register snapshots. When orig_state == state (no copy), _extract_register_changes sees no changes because the procedure modifies the state in-place. The fix requires capturing register values as a dict before execution AND modifying _extract_register_changes to accept this alternate form. Without both changes, register sync breaks silently (ais3: 3s → 11s).
avoid-state-regs-iter-in-init
forgotten
Avoid iterating 'getattr(state.regs, name)' over all arch registers in any init/sync hot path. Each access triggers the regs plugin's default fill (a fresh BVS allocation per uninitialized reg) AND emits an 'unspecified value' warning per reg. For x86_64 with ~80 regs (including cr0..8, ymm0..15, segment regs, fp/sse) this can add tens of ms per state init, multiplied by hundreds of forks. Initial angr-g9hy fix tried doing exactly this in the _sync_registers_to_rust precomputed_regs fast path and caused 20-130%+ regression on ais3/csgames/defcamp/etc. CORRECT pattern: iterate state.registers._pages.items() and look at page.symbolic_data — that contains only actually-written entries (no lazy fill). This is what _state_has_user_symbolic does for both memory and now registers.
avoid-stdin-tracking-fix-for-codegate
forgotten
Do NOT attempt to 'fix stdin BVS tracking' as the codegate_2017-angrybird fix. The parent angr-kcf description ('Rust engine doesnt track symbolic stdin BVS variables created during entry_state(); posix.dumps(0) returns b""') is STALE. Why stale: (a) the current symptom is dumps(0) returns 20 WRONG bytes, not b'' — so injection IS happening (b) NativeFgets DOES call state.record_stdin_symbol() for each byte (procedures/fgets.rs:86-88) (c) inject_rust_stdin (rust_callback_dispatch.py:327) correctly retrieves and evaluates them via Rust solver and appends concrete bytes to stdin.content (d) Verified 2026-05-17: SMT-LIB output from Rust solver references stdin_fgets_0* in constraints. How to apply: when angr-kcf or codegate-divergence work resurfaces, start from the constraint/path divergence angle (symbolic-memory branch decisions at 0x1000-0x1018), NOT stdin variable tracking.
avoid-stdout-probes-in-run-single
remembered
run_single.py (tests/benchmarks/) swaps sys.stdout for a BufferedStringIO (test_utils.BufferedStringIO) while the solve script runs, so a print() inside an angr callback or angr/exploration/*.py probe VANISHES and looks like 'the code path never ran'. Any ad-hoc probe inside RustExplorationManager callbacks must print with file=sys.stderr (and be read via '2>&1 >/dev/null | grep'). Cost ~10 wasted turns in iter130 before this was spotted. Method that localized angr-5rjbq in ~5 runs: put an env-gated stderr probe at EVERY resume_after_simprocedure call site in rust_callback_dispatch.py (_resume_with_state ~line 'Resume Rust with the changes', _resume_with_skip_hook, and the no-successor/non-zero-length path that passes reg_changes=None), printing new_pc + the decoded reg_changes list. Seeing 'regs=[]' for a hook that provably writes ebx/ecx pinpoints the extraction as the culprit, not Rust.
avoid-step-outcome-trait-refactor
forgotten
angr-ja0b proposed replacing the StepError enum match in native/angr/src/exploration/mod.rs:2735-2947 with a StepOutcome trait (action()/state()/reason()). Rejected after real code review. Why: the four variants carry different data shapes — NeedCallback(PendingCallback) needs 7 fields (reason, state, deferred_forks, fork_snapshots, stored_conditions, pre_callback_snapshot) and its handler is ~180 lines (deferred-fork replay against stored conditions). Deadended/Error/Unconstrained are 1-5 lines each. A trait with state()+reason() cannot expose pending.deferred_forks etc., so the run loop would either lose type info or have to downcast — neither improves on the existing match. The existing memory invariant-step-error-not-thiserror already documents that StepError is a control-flow signal, not a pure error. Pattern-matching is the natural Rust expression; closing wontfix.
avoid-store-side-fix-for-wider-sym-drift
forgotten
When fixing sidecar drift in store_concrete (e.g., angr-1tes for multi_objects, angr-jvjf attempted for symbolic_objects/spans): the obvious 'clean up sidecars in store_concrete' approach only works for sidecars based AT addr. For wider syms based at a DIFFERENT address (where store_concrete writes inside the wider sym's range), the cleanup needs to either (a) drop the wider sym + its spans (losing surviving-byte tracking, breaks loads) or (b) explode the wider sym into per-byte entries (requires &SymContext for RustBV::extract — store_concrete signature does not have one, and 10+ callers would need to thread it through). The pragmatic fix is load-side: consult page bitmap before trusting the wider-sym sidecars. See angr-jvjf fix at d2c4078cf.
avoid-storing-extract-asts-in-guest-buffers
remembered
AVOID storing a seeded stdin AST directly into the guest buffer in read_stdin_symbolic (procedures/read.rs). Tried first for angr-mb09c: write_bv_bytes(buf, seeded_bytes) where seeded_bytes are Extract(hi,lo, harness_BVS_256). Result on the 8-leaf pbounce synthetic: only 7 of 8 leaves reached the find gate and two found states evaluated to a leaf pattern that was not their own -- the two leaves that take TWO Python bounces (trap_point at both levels) lost their branch constraints. Root cause: the Python-bounce memory round-trip (Rust symbolic memory -> claripy -> back) preserves identity for plain 8-bit LEAF symbols but not for a non-leaf Extract expression over a wide BVS. The working fix keeps plain stdin_N_i leaf symbols in the buffer and ties them to the seed with an equality constraint (sym.eq(seed)) -- solver-level binding, memory shape unchanged. Invariant for any future 'serve the harness's own symbol' work on a Python-bounceable path: bind via constraint, do not store the composite AST.
avoid-strdup-heap-alloc-off-by-one
forgotten
PITFALL when refactoring strdup-like procedures: the original strdup's heap_alloc size argument was buf.len() where buf included the null terminator. If switching to scan_concrete_until_null (which excludes the null), the alloc must become buf.len() + 1 to preserve the actual byte count. The test test_strdup_empty_string asserts alloc_size == 1 (one byte for null) — easy to break by forgetting the +1. Same applies to any heap_alloc sized off scan_concrete_until_null output.
avoid-strict-2-cache-bound-without-overlay
forgotten
AVOID interpreting angr-qm7w's 'cache size ≤ 2' literally without a plugin-overlay map. The current implementation got down to cap=8 by pinning roots + current callback state and using LRU eviction beyond that. Going strictly to ≤ 2 would require capturing per-state plugin diffs (posix, libc, heap, fs, log, globals) into a separate overlay dict and evicting full SimStates after each callback — significantly larger refactor. Reason: VEX-execution memory_load callbacks (_get_per_fork_state) read state.memory directly from the cached state; without that path, root state's memory misses callback-induced writes. How to apply: when extending qm7w cleanup further, plan for a plugin-overlay dict keyed by state_id and verify _cb_memory_load behavior under heavy symbolic-write workloads.
avoid-stuck-angr-32ky
forgotten
Task angr-32ky (Improve test coverage for edge cases) was auto-deferred after 3 consecutive dirty iterations. The agent could not complete build/test/commit cycle within session time limits. May need to be broken into smaller subtasks or done manually.
avoid-stuck-angr-3tek
forgotten
Task angr-3tek (Enable native read/write SimProcedures (currently disabled due to symbolic_objects stale cache)) was auto-deferred after 3 consecutive dirty iterations. The agent could not complete build/test/commit cycle within session time limits. May need to be broken into smaller subtasks or done manually.
avoid-stuck-angr-3uye
forgotten
Task angr-3uye (Unconstrained PC after ret silently concretized to 0 instead of unconstrained stash) was auto-deferred after 3 consecutive dirty iterations. The agent could not complete build/test/commit cycle within session time limits. May need to be broken into smaller subtasks or done manually.
avoid-stuck-angr-3zhl
forgotten
Task angr-3zhl (load_concrete returns full sym1 ignoring partial overlap by sym2) was auto-deferred after 3 consecutive dirty iterations. The agent could not complete build/test/commit cycle within session time limits. May need to be broken into smaller subtasks or done manually.
avoid-stuck-angr-8s4b
forgotten
Task angr-8s4b (Invalidate concretize cache on store and reuse prefetch cache on writes) was auto-deferred after 3 consecutive dirty iterations. The agent could not complete build/test/commit cycle within session time limits. May need to be broken into smaller subtasks or done manually.
avoid-stuck-angr-bkcs
forgotten
Task angr-bkcs (ARM / AArch64 NEON SIMD support) was auto-deferred after 3 consecutive dirty iterations. The agent could not complete build/test/commit cycle within session time limits. May need to be broken into smaller subtasks or done manually.
avoid-stuck-angr-hjs2
forgotten
Task angr-hjs2 (SLOW_STMT / SLOW_BLOCK eprintln! → log::warn! in interpreter_cb/execution.rs) was auto-deferred after 3 consecutive dirty iterations. The agent could not complete build/test/commit cycle within session time limits. May need to be broken into smaller subtasks or done manually.
avoid-stuck-angr-is4x
forgotten
Task angr-is4x (Reduce per-constraint lock acquisitions in Z3 context) was auto-deferred after 3 consecutive dirty iterations. The agent could not complete build/test/commit cycle within session time limits. May need to be broken into smaller subtasks or done manually.
avoid-stuck-angr-mmdh
forgotten
Task angr-mmdh (Phase 4: wider-load collapse + per-byte flush coalescing for Multi cells) was auto-deferred after 3 consecutive dirty iterations. The agent could not complete build/test/commit cycle within session time limits. May need to be broken into smaller subtasks or done manually.
avoid-stuck-angr-ufez
forgotten
Task angr-ufez (Compare-and-swap: support symbolic address and DCAS (128-bit) variants) was auto-deferred after 3 consecutive dirty iterations. The agent could not complete build/test/commit cycle within session time limits. May need to be broken into smaller subtasks or done manually.
avoid-stuck-angr-uq4n.3
forgotten
Task angr-uq4n.3 (inspect.3: mem_read event dispatch + Python integration test) was auto-deferred after 3 consecutive dirty iterations. The agent could not complete build/test/commit cycle within session time limits. May need to be broken into smaller subtasks or done manually.
avoid-stuck-angr-vt0t
forgotten
Task angr-vt0t (Categorize remaining 250+ Python bridge except blocks) was auto-deferred after 3 consecutive dirty iterations. The agent could not complete build/test/commit cycle within session time limits. May need to be broken into smaller subtasks or done manually.
avoid-symbolic-addr-store-for-concrete-readback
remembered
INVARIANT: state.memory_store_symbolic(symbolic_addr, val) is NOT reliably concrete-loadable afterwards — a subsequent state.memory_load(concrete_addr, n) over a region written by a symbolic-address store errors with MemoryError::SymbolicAddress{description:"symbolic bytes not fully tracked"} (store_symbolic_unified routes into a sidecar the concrete load path doesn't fully reconstruct). When you need a symbolic-SIZE/position write whose result must be read back concretely (e.g. NUL-at-symbolic-offset in fgets short reads), emulate it as concrete-position ITE stores instead: for each fixed position p, store ite(idx_cond(p), new_val, old_val). This also avoids symbolic-address concretization enumerating up to N addresses (explosion). See fgets-short-read-design (angr-efvao) for the worked example.
avoid-sync-exported-constraints
forgotten
NEVER call _sync_exported_constraints on state export. It's O(n^2) on constraint ASTs (5.9s for sym-write), causes identity mismatches (Python/Rust symbol names differ), and is unnecessary since the Rust solver fallback handles all solver operations directly. Skipped in all 4 export paths (cached, parent-copy, stepping-copy, snapshot).
avoid-synthetic-loop-blob-for-rust-tests
forgotten
Rust engine + tiny load_shellcode/blob: angr-1lzq + angr-g6dg both now FIXED. (1) [commit b78d99dc4] Blob main object binary=None -> _load_binary_regions skipped it -> zero concrete regions -> in-bounds jumps misclassified as unmodeled calls -> 0x0; plus off-by-one truncating last byte (cle Region.max_addr INCLUSIVE; size=max-min+1). (2) [commit f761d0493] amd64g_calculate_rflags_c symbolic path lacked INC/DEC arms -> dec's carry-preservation ccall returned fresh unconstrained symbolic carry (see ccall-inc-dec-carry-preserve). With both fixed, cold conditional countdown loops in a blob run to completion. Regression tests: test_load_shellcode_blob_in_binary_control_flow (unconditional jmp $) and test_load_shellcode_blob_conditional_loop_cold_flags (mov/dec/jnz countdown parking on a jmp-$ pad at 0x1009). Real binaries unaffected (concrete flag-setting ops precede branches).
avoid-test-helper-page-remap
forgotten
Test helper for inserting a symbolic byte into pre-mapped memory: do NOT call state.map_memory_data(page_addr, &[0u8; 4096], ...) before storing — that overwrites prior content on the same page. Just do: let ctx = state.solver().borrow(); let sym = RustBV::symbolic(&ctx, name, 8); drop(ctx); state.memory_store(addr, sym.clone()).unwrap(). Page must already be mapped by the caller. Spent debugging cycles on tests like test_strcmp_symbolic_byte_solver_evaluation_equal where the helper was zeroing 'a\0\0' to '\0\0\0' before placing the symbolic byte, causing a position-0 mismatch with s2='ab\0' that returned -97 instead of 0.
avoid-trait-over-macro-for-uniform-binops
remembered
The bd task angr-v2cl suggested replacing width_binop! macro with a BinOpMethod trait. This was rejected as net negative: width_binop! is a 4-line macro shared by both unop() and binop() expanding to 'debug_assert widths match, call BV::method_into(...)'. A trait replacement would require: (1) a struct per op (40+ same-width binops), (2) impl BinOpMethod for that struct (4 lines), (3) replace the macro-call arm with a type-parameterized helper call (1 line). Net: ~6 extra lines per op for zero functional benefit. Pattern: if a refactor suggestion in a bd task swaps a macro for a trait, count the per-instance line delta before adopting — macros for same-shape boilerplate often beat trait dispatch on code size.
avoid-trusting-dead-code-rationale-docs
remembered
AVOID trusting an #[allow(dead_code)] retention doc's own account of its subject. angr-9ke6b.218 decided 8 such items; SIX had a rationale that misdescribed itself. Failure modes seen, in order of nastiness: (1) claims 'No caller' while the module's sibling *_tests.rs already asserts it (NativeSyscall::name) -- ALWAYS grep the sibling test file first; (2) claims a value comes from config when the constructor hardcodes a literal (VEXInterpreter.use_memory_callbacks, 'set from ExecutionConfig' vs literal true); (3) states the OPPOSITE of a documented decision elsewhere (gil_profile module doc vs set_profiling's deliberate process-cumulative accumulators); (4) asserts a reset that never happens (claripy_bridge::clear_global_registry); (5) blocks a cleanup on an API concern that does not exist (SimProcedureInfo::no_return claimed deleting it 'changes the Python-facing 4-tuple' -- the tuple's flag is consumed one layer UP, in run_loop_single, and the interpreter copy was a pure duplicate); (6) gives a reason that does not distinguish its subject (MipsO32::fp_arg_registers cited 'no canonical register_name entry' for $f12/$f14, but ARM d0-d7 and AArch64 q0-q7 are alias-only too and ARE populated). Decide from the code, then repair the doc in the same commit.
SAME RULE APPLIES TO THE AUDIT BEAD'S OWN DEAD-ITEM LIST, not just in-source rationale docs. angr-9ke6b.188 listed 9 'zero-caller' pub items in automaton/; 2 were live: StateSet::remove is called by DFA::add_transition's reverse-edge retirement (a later commit than the audit's snapshot) and is asserted in state_tests.rs, and DFA::transitions is what PyDFA::to_networkx iterates -- the bead had matched the EpsilonNFA namesake of the same name. Both traps are mechanical: an audit bead's line-number citations go stale as the file moves under it, and a bare method-name grep conflates same-named methods on sibling types. Before deleting, re-grep each item by name across native/angr/src + the Python tree and read the hit's receiver type; a 2-of-9 false-positive rate is normal, not exceptional.
avoid-trusting-gc-noisy-subcounters
remembered
GC pauses land on whichever line allocates next, and they WILL corrupt a fine-grained Python perf counter. Seen in angr-gorvf.11: csgames2018's callback_simprocedure_replay_sym_ns read 0.65ms / 7.07ms / 7.69ms across three runs of IDENTICAL code, and an in-place split showed the whole spread sitting on one line (page.symbolic_data = SortedDict(...)), which does ~0.24ms of real work. It is a gen2 collection tripped by that allocation, not work. The tell, and the rule: cross-check against the enclosing TOTAL before believing a sub-counter delta — csgames2018's callback_simprocedure_total_ns was flat at 167-170ms vs 165-171ms baseline, i.e. no regression existed. Benches with a small entry count and a big live claripy AST graph (csgames2018: 370 entries) are the exposed ones; a bench with enough real work per crossing (csaw_wyvern: 8107 entries) averages the pause out and reads stable. Do not chase a sub-counter regression whose parent total did not move.
avoid-trusting-stale-cache-clear-speedup
forgotten
The mma-howtouse-cache-clear-speedup memory (claripy.clear_all_caches gave 23% gain) is STALE — claripy no longer exposes clear_all_caches(). It moved to WeakValueDictionary caches that GC naturally. Don't use that memory as a planning input for future optimizations; the predicted gain is no longer realizable via that mechanism. The Rust-side clear_ast_cache() (PyO3 export, from angr-518z) is NOT equivalent — it only clears the Rust translation-side LRUs, not Python claripy state. Validated 2026-05-17 via angr-gra3 — angr-518z hook delivered 0% measurable gain.
avoid-unbounded-extraction-fallback
forgotten
When walking UltraPage.symbolic_data as a fallback for filler-materialised values, cap per-entry extraction at ~64 bytes. SYMBOL_FILL_UNCONSTRAINED_MEMORY produces single entries that can cover entire 4 KB pages (e.g. stack page filler). Removing the cap regresses ais3_crackme by ~40% and google2016_unbreakable_0 by ~43% because each page sync extracts 4096 bytes via per-byte memory.load. The 64-byte cap covers all user-seeded init.memory.load(addr, N) patterns observed so far (sokohashv2 uses 8) while skipping page-fillers.
avoid-unconditional-addr-pin-constraint
forgotten
angr-mv08h (Any/Max TooLarge concretization pin) reverted 3x. The logically-correct fix — pin_fallback_addr calling ctx.assume_true(addr.eq(chosen)) on EVERY read_fallback_any/write_fallback_max path in AddressConcretizer (native/angr/src/concretize.rs) — is sound (matches Python AddressConcretizationMixin adding Or(addr==c)) but adds a solver constraint per fallback, causing fast-tier bench timing regressions >15% (3.48s vs 3.02s; 0.56s vs 0.48s) that trip run_regression. LESSON: adding an unconditional ctx.assume_true per hot-path concretization is too expensive for the bench gate. A resurrection needs a cheaper design — pin lazily only when the concretized addr later feeds a guard/eval, or otherwise avoid touching the solver on the common fallback path. Do not re-attempt the unconditional-pin form.
avoid-unconditional-rust-memory-buffering
forgotten
angr-5rjbq perf trap: making the interpreter buffer callback-dispatched stores into rust_memory UNCONDITIONALLY regressed four fast-tier benches 15-25% (flareon2015_2, unmapped_analysis, defcamp_r100, android_arm_license_validation) — gate-off the Python shadow already absorbs the store, so the rust_memory write is pure duplicate cost. Fix: guard on PythonCallbacks::memory_is_rust_proxy (Arc, set via set_memory_is_rust_proxy at callback registration in rust_manager._register_callbacks). Pattern: when adding correctness work for the proxy gate, gate it ON the proxy flag rather than on use_rust_memory, which is true in BOTH gate states.
avoid-unguarded-run-one-harness-forkbomb
remembered
A one-off benchmark harness that calls run_regression.run_one MUST have an 'if name == "main": main()' guard. run_one uses a multiprocessing spawn pool, and spawn re-imports the script as main in every child — without the guard the module body re-runs there and the script FORK-BOMBS the 7GB box (seen 2026-08-01, angr-018m9: a 6-rep run produced zero output in 400s and a 43MB traceback log while recursively spawning). /tmp/audit_baselines.py has the guard; copy from it. Second gotcha from the same session: do NOT pipe the harness through 'tail -N' — the pipe buffers everything, so a timeout kill loses ALL partial results. Redirect to a file and poll it instead.
avoid-uniform-page-policy-rust-sync
remembered
When a Rust-state-sync optimization 'always X' or 'never X' decision benefits one benchmark workload (mma_howtouse: thousands of small filler pages per state) but penalizes another (hackcon2016: tens of pages per state with complex constraint solving), a per-state cap with headroom on both sides is usually right. Specific case (angr-8t45 fix in commit f54bbba93): zero_eager_cap=200 picks hackcon (35) vs mma (2054) cleanly. General principle: count first, then decide eager-vs-lazy. The original fced54a07 commit decided eager-vs-lazy purely on 'is page all-zero' which conflates two distinct cases. Before applying a uniform 'is X' policy across all states/pages/states, measure the distribution across the benchmark suite — the bimodal distribution between hackcon (35 pages) and mma (2000+ pages) is enormous and means uniform policies WILL hurt one side.
avoid-unwrapping-z3-simplify
remembered
sample_simplify_skip (symbolic/solver_build.rs) is a DIAGNOSTIC counter that used to call the z3 crate's Ast::simplify, which unwrap()s Z3_simplify. Z3_simplify returns NULL when the context has an error latched — reachable with a claripy-imported Bool coming through add_constraint_raw — so a counter could abort the whole interpreter (repro before the fix: pytest -k UnsatCore tests/engines/rust/test_solver_ops.py). It now calls z3_sys::Z3_simplify directly and drops the sample on NULL. Rule: never let a sampling/diagnostic path unwrap a nullable Z3 FFI return.
avoid-update-baseline-without-verification
remembered
Running run_regression.py --full --update writes a single run's measurements as the new baseline. For Z3-nondeterministic benchmarks (rust_only=True), a single fast measurement bakes in a too-tight baseline that future noisy runs trip with false-positive regressions. Workflow: --update first, then run 3-5 verification rounds and bump any flagged entries above their slow-mode value. Confirmed 2026-05-09 on angr-6a7u: --update set google2016_unbreakable_1=1.683 (off a fast run) but max observed across 8 runs was 3.26 -- bumped to 3.5. RECONFIRMED 2026-07-16 (commit f804ba410): defcon2016quals_baby-re baseline was refreshed 0.53->0.33 by the libvex-ffi flip (807bfd74d) --update, but reproducible steady-state is 0.40-0.42s over 6 runs -> phantom 23% gate regression that survived --retry-failures. No code regression (bisect: reverting fork perf f5824dadc left it unchanged; only Python explore()/reporting edits landed since). Loosened to 0.42. Lesson: a fast bench (<0.5s) refreshed by a single --update run is especially prone to this; verify with 6 runs before trusting the --update value.
avoid-venv-pip-resolvelib-broken
forgotten
Venv pip is fragile in this repo. As of 2026-05-07 commit 0c90d4962, .venv/lib/python3.12/site-packages/pip/_vendor/resolvelib/resolvers/ contained only .pyc files (newer resolvelib version) while structs.py was the older v1.0.1 sans RequirementInformation, so pip install crashed with 'cannot import name RequirementInformation'. Fix: rm -rf .venv/lib/.../resolvelib/resolvers/ — Python falls back to the v1.0.1 resolvers.py file. Also setuptools/_vendor/jaraco/text/ is missing source .py files including 'Lorem ipsum.txt'; pip install -e . crashes. Workaround: use cargo build --manifest-path native/angr/Cargo.toml --release, then cp target/release/librustylib.so angr/rustylib.cpython-312-x86_64-linux-gnu.so. To run benchmarks: PYTHONPATH=/home/ubuntu/repos/angr /home/ubuntu/repos/angr/.venv/bin/python tests/benchmarks/run_regression.py
avoid-vsnprintf-real-formatting
remembered
vsnprintf native SHIPPED (commit dd300abf4, tx7ec.9): NativeVsnprintf in procedures/sprintf.rs matches Python angr/procedures/libc/vsnprintf.py no-op stub exactly (size==0 -> return 0; else write one NUL at str[0], return 1; NO %-substitution; format/va_list args ignored). num_args=4. Registered in procedures/mod.rs after NativeSnprintf. This is the FAITHFUL impl — a 'real' formatter via the sprintf core would diverge (va_list is arch-specific SysV reg_save_area, unmodeled by angr) and explore different symbolic states. The vsprintf/vfprintf/vprintf siblings have NO Python handler at all -> still product-decision-blocked, folded into angr-ae54t. RULE: when a Python SimProcedure is itself a stub, the faithful native impl matches the STUB, not the real libc behavior.
avoid-weakref-callbacks
remembered
AVOID wrapping Rust engine callbacks (RustExplorationManager.cb*) in weakref-based closures to break the bound-method cycle. Approach failed: caused 19-72% regression across 8 benchmarks (defcamp_r100 +38%, ais3_crackme +72%, csgames2018 +38%, etc.) due to per-callback overhead from weak_self() + attribute lookup + extra Python frame. The Rust engine fires callbacks at very high frequency (memory_load, get_register, etc.). The correct fix is to add traverse/clear to the PyO3 pyclass on the Rust side, leaving the Python callbacks as plain bound methods (zero per-call overhead). See pyo3-pyclass-cycle-leak memory.
avoid-wiring-up-dead-reset-hooks
remembered
The two 'unwired reset()' items from angr-9ke6b.218 (items 3 and 4, commits 8ddb25f36 / 58c13322b) were NOT defects — both were doc contradictions, and in both cases wiring the reset up would have been the actual bug. gil_profile::reset(): RustExplorationManager::set_profiling deliberately keeps the GIL/wall accumulators process-cumulative across managers so the LAST manager's stats() reports the whole-process GIL fraction; resetting at profiling-enable would silently truncate it. claripy_bridge::reset_for_new_exploration + symbolic::clear_global_registry: symbol identity is process-global on purpose (managers coexist — a bench builds one per simulation_manager() call — and sibling parallel workers share it), so wiping it at a second manager's start strands rust ids the first manager's live states/RustBVs hold, giving lookup_name_by_id -> None -> rustbv_to_claripy mints a renamed BVS no constraint binds (angr-izov2). Both are now #[cfg(test)] rather than #[allow(dead_code)] — the gate that makes a future production caller a compile error. GENERAL RULE: before 'wiring up' a dead reset/GC/clear hook found by a dead_code audit, read the sibling call site's rationale comment first; in this crate the absence of the call is usually the design, and the stale doc is the bug.
avoid-with-z3-context-for-stale-ctx-test
forgotten
Testing a stale-context guard for any cached z3::ast::BV (e.g. expression-z3-ast-memo) CANNOT use z3::with_z3_context: its closure has a Send+Sync bound that exists precisely to forbid smuggling a Z3-bearing value (RustBV is !Send/!Sync) across the boundary — but the stale path needs the SAME RustBV converted in two contexts. Instead save Context::thread_local(), call Context::set_thread_local(&new_ctx) (Config::new()+Context::new(&cfg) for a distinct Z3_context), convert, then restore set_thread_local(&original) BEFORE asserting so a failure doesn't poison sibling tests. Use an Extract-over-concrete RustBV (raw_extract_node helper in value_tests.rs) so the rebuilt AST is a fresh context-local const with NO Symbolic leaves — Symbolic.ast has its own pre-existing cross-context staleness that would confound the check. See test_expression_memo_no_stale_context_leak.
avoid-ype54-guard-serialization
forgotten
ype54 SymbolicBranch-guard-serialization fix is INSUFFICIENT and REGRESSES test_parallel_wave — do NOT re-attempt the description's '## Fix' as written. Verified iter43 (post-HEAD 02ce2aae): implemented exactly the described fix — carry the parked SymbolicBranch guard (last_condition RustBV) across worker->coordinator migration via a serde pending_branch_guard: Option field on RustSimState/RustSimStateSnapshot, stamped in parallel_process_state's CoreReturn::NeedsPython arm (run_loop.rs, where last_condition was dropped as ) and read back in process_parallel_bounce_queue to seed last_condition before dispatch_bounce. RESULT: cleanly REMOVES the 'condition N not found in stored_conditions' ValueError, BUT (a) workers=2 phase2=off STILL finds only the backdoor (path loss unchanged), and (b) REGRESSES test_parallel_wave 4/7->fail: phase2=on now recovers a DUPLICATE backdoor instead of the feasible non-backdoor path. CONCLUSION: the ValueError is benign (as lq9rz originally noted); the real path loss is UPSTREAM (validates iters36-42 native constraint-drop direction). ROOT REASON the guard-carry produces a WRONG fork: parallel_process_state sets state.set_pc(step.new_pc) BEFORE the NeedsPython bounce (run_loop.rs ~305), so the migrated bstate has already advanced PAST the branch point; the coordinator's fork then constrains a state already committed to one direction, collapsing both forks to the backdoor. In seq, dispatch_bounce forks a state still AT the branch. So even a correctly-carried guard cannot reproduce the seq fork on the par path. NEXT: fix must address the upstream constraint drop OR fork at the correct pre-branch state, NOT serialize the guard. All iter43 code was REVERTED (git checkout); tree clean, .so rebuilt, 7/7 green.
avoid-z3-optimize-for-min-max
remembered
AVOIDED Z3's Optimize API (Optimize::minimize/maximize from z3-0.19.7) for symbolic-address concretization. While Optimize gives exact min/max in 1 check() call each (vs ~64 SAT calls in bisection), creating an Optimize solver requires re-asserting all current constraints — paying that cost per concretization defeats the savings. Seeded bisection on the existing solver (push/assert/check/pop) is simpler and avoids the constraint-copy overhead. Reconsider only if we cache an Optimize solver across calls AND have a way to keep its assertions in sync with the main solver's stack.
avoid-z3-parallel-enable
remembered
Z3 parallel.enable=true (set via params.set_bool on the solver) is CORRECTNESS-BREAKING in our setup, not merely null/negative on performance. Tested in angr-gfay (closed 2026-05-21, no code committed): adding params.set_bool("parallel.enable", true) to build_solver_params (native/angr/src/symbolic/context.rs:719) breaks 4 of 6 sampled benches with different error modes: fauxware/ais3_crackme fail with IndexError, mma_howtouse fails with AngrCallableError "No paths returned from function", sokohashv2 fails with KeyError. Errors are consistent with Z3 returning Unknown / non-deterministic models under parallel SAT, which downstream consumers do not handle. The two passing benches (defcamp_r100, fairlight) show no measurable speedup. Do NOT re-attempt without first auditing every solver consumer for Unknown handling AND pinning smt.random_seed + parallel.threads.max for determinism. Distinct from angr-59jk (per-thread Z3 contexts for parallel exploration) which is a separate, orthogonal approach.
avoid-z3-sat-preprocess-tactic
forgotten
ANGR_Z3_TACTIC=sat-preprocess:qfbv tactic pipeline produces WRONG ANSWERS on real angr workloads — failed csgames2018 and securityfest_fairlight with 'IndexError: list index out of range' during post-solve goal unwrap (angr-ya00, 2026-05-20). Wall times were suspiciously fast (~0.25s) which is the tell. Avoid sat-preprocess as a pipeline prefix until that interaction with claripy AST passthrough is debugged.
avoid-z3-sat-test-for-lane-independence
forgotten
test_virecip_est_lanes_independent attempted to verify lane independence via SAT-distinguishability (push() + add_constraint(lane0._eq(&lane1).not()) + assert ctx.is_sat()) but FAILED unexpectedly even though two RustBV::symbolic calls with the same name produce monotonically-numbered unique BVS — Z3 returned unsat. Root cause not investigated; the failure pattern was REMOVED from the test because shape tests + the structural guarantee that RustBV::symbolic produces unique IDs already establish independence. If you need to verify lane independence in future tests, prefer to extract each lane individually and check the underlying RustBV IDs match the expected fresh-symbol count, rather than going through Z3. Potential explanation: ctx might be returning a cached unsat result from a prior local-constraint, OR extract() on a concat tree might return ASTs that share structure in some unexpected way at the Z3 level when both lanes are at the same position offset.
avoid-z3-solver-set-params-for-random-seed
remembered
AVOID pinning Z3 smt.random_seed or sat.random_seed via z3::Params::set_u32 + Z3_solver_set_params in native/angr/src/symbolic/context.rs::build_solver_params. Both dotted forms CORRUPT the solver (eval returns models violating asserted constraints). The bare 'random_seed' form does not corrupt but produces MORE nondeterminism than no pin. Determinism via solver-level params is unreachable in z3-0.19.7 / Z3 4.13. If a future task needs seed pinning, route through Z3_global_param_set (which sets module-level params) before the first Solver::new call — verify against test_model_stability_constraint_order. Tested 2026-05-25 in angr-iaol.1 (commit 364325237). See also iaol1-seed-pin-empirically-broken.
avoid-z3-solver-translate-same-context
forgotten
Z3_solver_translate (Solver::clone) is BLOCKED as a fork-acceleration strategy when source-context == dest-context: returns a solver with 0 assertions (per existing memory z3-solver-translate-same-context-bug-z3-solver). Even though native/z3-patched/src/solver.rs:663 still wraps Z3_solver_translate via Solver::clone, the same-context bug rules out using it for SymContext::fork() — which is the path we'd want to optimize. Future hk7k attempts must NOT propose clone-based fork as a solution; the spike must use either shared-solver-with-push/pop (Option A) or per-state cross-context translate (Option D, expensive due to AST replication). Validated 2026-05-21 during angr-hk7k research spike.
avoid-z3-tactic-cond-overhead
forgotten
Z3 probe-conditional tactic (Tactic::cond(probe, t1, t2).solver()) ADDS per-check overhead that can wipe out gains and regress small-problem benches. Concretely (angr-ya00, 2026-05-20): cond(num-consts > 20, qfbv, smt) regressed defcon2016quals_baby-re 0.50->1.54s (+208%) even though most baby-re checks should fall to the smt branch. Hypothesis: smt-via-Z3_mk_solver_from_tactic is not identical to z3::Solver::new() — the default Solver wraps an additional combined_solver portfolio. Implication: per-call tactic dispatch via Tactic::cond is not free; prefer static tactic choice or accept the bimodal-only opt-in.