# Bug audit — 2026-05-03

Multi-pass multi-agent bug hunt across `net/crates/net/` (~117k LOC, 158 Rust files). Findings are real, demonstrable defects (not style or speculation). Numbered sequentially within severity buckets; near-duplicates from successive passes have been merged.

Items prefixed with `[FIXED in <sha>]` have been resolved on `bugfixes-9`. Each fix lands as a single commit with a regression test where reasonable.

## Status

- **Fixed:** #2, #3, #4, #5, #9, #10, #11, #12, #14, #15, #16, #17, #18, #19, #20, #21, #22, #26, #27, #28, #29, #30, #31, #32, #33, #34, #35, #36, #37, #38, #40, #41, #42, #43, #44, #45, #46, #47, #48, #49, #50, #51, #52, #53, #54, #55, #57, #58, #59, #60, #61, #62, #63, #65, #66, #67, #69, #70, #71, #72, #74, #75, #76, #77, #78, #79, #80, #82, #83, #84, #85, #86, #87, #88, #89, #90, #91, #92, #93, #94, #95, #96, #98, #99, #100, #101, #103, #104, #105, #106, #107, #108, #109, #110, #111, #112, #114, #115, #116, #117, #119, #120, #122, #123, #124, #126, #129, #130, #131, #132, #133, #134, #135, #136, #137, #138, #139, #140, #141, #142, #143, #144, #145, #146, #147, #148, #149, #150, #151, #152, #153, #154, #155, #156, #157, #158, #159, #160, #161, #162, #163, #164, #165, #166, #167, #168, #169, #170, #171 (149 of 171)
- **Obsolete (already addressed pre-audit):** #113
- **Remaining Critical:** none
- **Remaining High:** none (Cluster C #23/#24/#25 fully ported across all 11 cortex+mesh+redis handle types)
- **Remaining Medium / Lower:** none — every actionable audit item is fixed, obsolete, or skipped with documented reasoning.
- **Obsolete (audit misread or addressed by prior refactor):** #73 (rollback+override already correct; pinned by regression), #113 (already addressed pre-audit), #118 (current behavior is correct rate-limit semantic), #121 (degenerate case is the algorithm), #127 (NetAdapter is per-peer by design; concurrent-init flood is an application-layer concern), #128 (notify_one + permit-stash semantics are sound; audit's `notify_waiters` suggestion would regress)
- **Skipped:** #39 (requires persistent-sequence feature work, not just a bug fix); #97 (audit-suggested reorder conflicts with the credit-window invariant — see `f41d9c36`)

## Critical

1. **[FIXED — durable_rename helper]** **`adapter/net/redex/disk.rs:1322` — `compact_to` is non-atomic on Windows.** `fsync_dir` is a no-op on `cfg(not(unix))`. Combined with the cross-file rename sequence (idx + dat + ts), a power loss can leave the directory pointing at mismatched inodes. Fix: introduced `durable_rename` helper. On Windows, routes through `MoveFileExW` with `MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH` so each rename blocks until the NTFS journal commits — closing the per-call durability hole that the stdlib `fs::rename` leaves open. The cross-file mixed-state window between two of the three renames is unchanged on either platform; that remains the deferred manifest-pointer rework.

2. **[FIXED in `f18dc900`]** **`bus.rs:686-711` (`remove_shard_internal`) — Batch worker awaited before drain worker, contradicting the function's own doc-comment.** Lines 506-520 explicitly document the correct teardown order ("drain first, so the in-progress `scratch` and `sender.send(batch).await` are flushed before the batch worker exits"); `add_shard_internal` rollback (533-569) and `shutdown_via_ref` (1329-1349) follow that order. The actual code awaits batch first, then drain. Drain worker still owns a sender clone, so `recv()` doesn't return `None` until drain exits — and there is no `tokio::time::timeout(...)` shell here (unlike the rollback path), so a wedged adapter on the shard being removed pins this function indefinitely. Fix: swap the await order to drain → batch, or wrap in the same timeout the rollback path uses.

3. **[FIXED in `781620b5`]** **`ffi/mod.rs:1322-1331` (`net_poll_ex`) — Panic across FFI on OOM.** `event.id.as_bytes().to_vec().into_boxed_slice()` and `event.raw.to_vec().into_boxed_slice()` can panic mid-loop, leaking earlier `Box::into_raw`s + the `std::alloc::alloc(layout)` array. No `catch_unwind`. Fix: wrap entry points in `catch_unwind`, or use `try_reserve`.

4. **[FIXED in `3efa4a9d`]** **`ffi/mod.rs:805-841` (`net_poll`) — Buffer-size check runs after events have been consumed.** `bus.poll(request)` advances the consumer cursor *before* the response is serialized; if `response_json.len() + 1 > buffer_len`, the function returns `BufferTooSmall` and drops the entire `response`. The caller's contractual retry fetches starting from the now-advanced cursor — every event in the failed serialization is silently lost. The C-API doc itself shows fixed-size buffers, so the trigger is the recommended usage pattern. Fix: serialize first and size-check before advancing the cursor, or buffer the response so a retry can resume.

## High

5. **[FIXED in `41be4ff2`]** **`adapter/net/redex/disk.rs:1356-1413` — Temp-file leak on `compact_to` failure.** Cleanup at 1411-1413 runs *after* `open_or_poison(...)?` (1356-1358) and `clone_or_poison(...)?` (1396-1398). On any post-rename reopen failure the three placeholder files in `/tmp` leak forever.

6. **[FIXED — applied/folded watermark split]** **`adapter/net/cortex/adapter.rs:486-499` — Watermark advances past unfolded events under `Stop` policy.** `recoverable_decode` causes `folded_through_seq.store(seq)` for events whose state mutation never landed; `wait_for_seq(seq)` returns true incorrectly. Combined with snapshot-restore (#7) this produces permanent state divergence. Fix: introduced `applied_through_seq` as a strict-prefix watermark — advances only on `Ok(())` AND only when this seq is the immediate successor of the current value. Any skip (recoverable_decode under Stop, or any error under LogAndContinue) caps it at the prior consecutive seq forever. `folded_through_seq` keeps current semantics so live consumers (DoS-resistance contract) don't deadlock; `wait_for_seq` doc tightened to surface the truth.

7. **[FIXED — same change as #6]** **`adapter/net/cortex/adapter.rs:374-386` + `tasks/adapter.rs:340-344` — Snapshot persists `last_seq` for skipped events.** When (#6) fires, `snapshot()` returns `last_seq = Some(N)` for events whose state was never applied; restore tails from `N+1` and the skipped seq is never re-attempted. Violates "log is the source of truth." Fix: `snapshot()` now returns `applied_through_seq` (strict-prefix); restore tails from there and re-attempts every seq past the gap. Trade-off documented on `snapshot`: non-idempotent folds will double-apply intervening successful seqs, which is a strictly-better failure mode than the pre-fix permanent loss.

8. **[FIXED — compute_checksum_with_meta]** **`adapter/net/cortex/meta.rs:135-137` — Checksum doesn't cover meta header.** `compute_checksum` hashes only `tail`. A bit-flip in `dispatch` byte (e.g. `STORED` → `DELETED`) is undetected and silently re-routes the event to the wrong fold arm. Fix: introduced `compute_checksum_with_meta(meta, tail)` that hashes the 20-byte header (with the `checksum` slot zeroed) followed by the tail. Memories + tasks producers stamp v2 checksums on every new write; corresponding fold consumers verify v2 first and fall back to legacy v1 (tail-only) for records written by pre-fix adapters. New writes detect dispatch / flags / origin_hash / seq_or_ts bit-flips; old records keep working with the original tail-only-coverage limitation. Migration is one-way (downgrade to a pre-fix adapter binary will skip every event written by a v2-capable producer because the legacy verifier expects checksum == xxh3(tail)).

9. **[FIXED in `6bf190c6`]** **`adapter/net/state/log.rs:175-211` (`prune_through`) — `base_link` not updated on partial prune.** Anchor only refreshes when `events.is_empty()` post-prune. After a partial prune followed by a full prune, `base_link` points at the first prune anchor, not the current head — chain validation breaks.

10. **[FIXED in `8d15aa1f`]** **`adapter/net/redex/file.rs:1100-1126` (`sweep_retention`) — In-memory index corrupted on panic between drain and renormalize.** Lines 1100-1102 evict prefix; 1118-1126 rewrite `payload_offset`. A panic between them leaves rebased `base_offset` with absolute offsets — every subsequent read misses. Fix: build the renormalized index in a temp `Vec`, then atomically replace.

11. **[FIXED in `005e2fb9`]** **`adapter/net/redex/disk.rs:251-261, 305-311` — Truncate-on-recovery without `sync_all`.** `set_len` for torn-tail repair has no `sync_all`/`fsync_dir`; a crash before the next durable write reverts the recovery and re-reads the same torn bytes.

12. **[FIXED in `6d11354c`]** **`adapter/net/redex/disk.rs:735, 951` — Best-effort rollback silently swallows errors.** `if let Ok(f) = OpenOptions::new().write(true).open(...)` — if the rollback open fails, orphan dat bytes / extra idx records persist. Subsequent appends produce permanent dat/idx divergence.

13. **[FIXED — DaemonError::Stale + per-mutator guard_identity]** **`adapter/net/compute/registry.rs:71-74` (`replace`) — Not safe for stateful in-flight work.** A caller holding an old `Arc<Mutex<DaemonHost>>` from `get_arc` keeps mutating the old host while new traffic hits the new one; old chain's writes are dropped. `replica_group::on_node_failure`, `fork_group::on_node_failure`, and `standby_group::on_node_failure` all use this pattern without quiescing. Fix: introduced `DaemonError::Stale(u32)` and a private `guard_identity(origin, &held_arc)` re-check that every public mutator (`deliver`, `snapshot`, `restore_from_snapshot`, `stats`, `with_host`, `daemon_keypair`) calls AFTER acquiring the inner `Mutex`. A caller whose held `Arc` no longer matches the map's current entry (because a `replace`/`unregister` landed during the get_arc → lock window) bails before mutating, surfacing `Stale` so the caller can retry against the current registered host. Residual: pre-acquired-lock mutators (already inside the inner Mutex when the swap arrives) still write to the orphan; fixing that requires either snapshot/restore-mid-flight on the host trait or a `try_lock`-with-timeout protocol that would deadlock with the existing `with_host` re-entrancy contract — see the `replace` doc for the trade-off.

14. **[FIXED in `348ba6c3`]** **`adapter/net/compute/orchestrator.rs:1301-1323` (`abort_migration_with_reason`) — Doesn't propagate to `MigrationSourceHandler`.** Source-side `migrations` map retains the entry; `is_migrating()` stays true, `buffer_event` keeps buffering into an undrained vector, retries trip `AlreadyMigrating`.

15. **[FIXED in `fc2797ce`]** **`adapter/net/compute/standby_group.rs:354-389` — Replaced standby marked healthy with `synced_through = 0`.** If the active fails before next `sync_standbys`, `promote()` (`max_by_key(synced_through)`) may pick the fresh zero-state standby and lose all pre-buffer state. Fix: keep replaced standby unhealthy until after a successful sync, or filter promote candidates to `last_sync.is_some()`.

16. **[FIXED in `d7c1089e`]** **`adapter/net/compute/bindings.rs:117-118` — Unbounded `Vec::with_capacity` from attacker u32.** `count = cursor.get_u32_le() as usize; Vec::with_capacity(count)` — peer-supplied snapshot can declare ~4B entries, ~96 GiB allocation before truncation checks. Fix: bound by `data.len() / MIN_BINDING_SIZE`.

17. **[FIXED in `040e6bdc`]** **`adapter/net/cortex/tasks/watch.rs:64-86`, `memories/watch.rs:95-117` — Doc says strict bounds, code is inclusive.** `created_after` / `created_before` etc. published as `>` / `<`; underlying `…FilterSpec::matches` uses `>=` / `<=`. Strict-bound consumers receive boundary events.

18. **[FIXED in `668c8a31`]** **`adapter/net/reliability.rs:308-323` (`on_send`) — Silent reliability drop.** When `pending.len() >= max_pending`, the oldest *unacknowledged* packet is `pop_front`-ed; subsequent NACK can never recover that seq because the entry is gone (`on_nack` at 408-417 finds nothing to match). Fix: backpressure callers; don't drop tracking for in-flight packets.

19. **[FIXED in `d3ae1fb7`]** **`adapter/net/router.rs:559-580` (`NetRouter::start`) — No re-entry guard.** Calling twice spawns two competing dequeue loops on the same `FairScheduler`, racing `pop()`. Fix: `compare_exchange` on `running`.

20. **[FIXED in `24e51a00`]** **`adapter/net/behavior/safety.rs:1378-1407` — AuditOnly mode silently drops violation logs.** `check_rate_limits` only logs the global-RPM violation when `mode == Enforce`; AuditOnly's documented "log violations but don't block" doesn't actually log. Fix: log unconditionally; gate only the `return Err` on enforce.

21. **[FIXED in `4e3bf4bd`]** **`adapter/net/behavior/proximity.rs:316` (`update_from_pingwave`) — Worse path overwrites better.** Updates whenever `pw.seq > self.last_seq` regardless of hop count. A flooded high-seq pingwave through a long route demotes the cached direct route. Fix: separate freshness (always take latest seq) from path quality (only update `hops`/`addr`/`latency_us` when `new_hops <= self.hops`).

22. **[FIXED in `0a79c368`]** **`adapter/net/subnet/gateway.rs:144-152` — `ParentVisible` over-permissive direction.** Predicate accepts both `dest.is_ancestor_of(source)` and `source.is_ancestor_of(dest)`; the second clause leaks parent-region traffic *down* into descendants, contradicting the documented "visible to ancestors" framing. Tests cover only child→parent and sibling→sibling.

23. **[FIXED — all 7 cortex handle types ported]** **`ffi/cortex.rs` (~30 sites: lines 207, 275, 323, 339, 354, 366, 388, 417, 487, 519, 571, 597, 622, 643, 667, 836, 871, 908, 970, 1000, 1062, 1103, 1132, 1154, 1175, 1196, 1390, 1421, 1453) — Handle-lifetime contract unenforced.** Unlike `ffi/mod.rs`, the cortex FFI uses bare `&*handle` with no `FfiOpGuard`/`active_ops` analog. A caller that races `net_redex_free` against `net_redex_file_append` produces use-after-free: `Box::from_raw` in `_free` deallocates while another thread holds `&*handle`. The Go cgo SDK is exactly this multi-thread pattern. Fix: extracted shared `ffi::handle_guard::HandleGuard` + `HandleOp` + `begin_free` protocol mirroring `ffi::mod`'s `FfiOpGuard`/`active_ops`/`bus_taken` design (5 helper unit tests pin the protocol). Applied to all seven cortex handle types: `RedexHandle`, `RedexFileHandle`, `RedexTailHandle`, `TasksAdapterHandle`, `TasksWatchHandle`, `MemoriesAdapterHandle`, `MemoriesWatchHandle`. Each handle's inner lives in `ManuallyDrop`; every public entry point gates on `try_enter`; every `_free` drives `begin_free(FFI_HANDLE_FREE_DEADLINE)` and takes the inner only after the drain (logs+leaks on timeout). Outer Box is intentionally leaked across all of them — the atomic memory backing `try_enter`'s `fetch_add` must outlive concurrent ops, same soundness rule as `NetHandle`. Three regression tests on `RedexFileHandle` (the audit's specific call-out) pin: post-free entry points return `ShuttingDown`; `_free` is idempotent; `_free` waits for in-flight ops to drain. The other six handle types share the recipe by construction.

24. **[FIXED — all 4 mesh + redis_dedup handle types ported]** **`ffi/mesh.rs:498-505, 1131-1138, et al.` — Same lack of shutdown/free synchronization across all mesh handles.** `net_mesh_free` / `net_mesh_stream_free` / `net_identity_free` / `net_redis_dedup_free` are unsynchronized vs concurrent uses of the same handle. Fix: applied the same `HandleGuard` recipe extracted in 30fe5e09. All four handle types (`MeshNodeHandle`, `MeshStreamHandle`, `IdentityHandle`, `RedisStreamDedupHandle`) wrap their inner Arcs/Mutexes in `ManuallyDrop`; every public entry point gates on `try_enter`; every `_free` drives `begin_free(FFI_HANDLE_FREE_DEADLINE)` and takes the inner only after the drain (logs+leaks on timeout). Outer Box intentionally leaked across all of them. Non-`c_int`-returning entry points (e.g., `net_mesh_node_id` u64, `net_identity_origin_hash` u32, `net_mesh_arc_clone` `*mut Arc<MeshNode>`) bail with the same shape as the absent-handle path on `try_enter` failure (0 / null_mut) rather than a typed error.

25. **[FIXED — same change as #24]** **`ffi/mesh.rs:1078-1079` — `MeshStreamHandle._node` keeps the node alive but not the handle box.** `_node: Arc<MeshNode>` pins the node, but `net_mesh_free(node_handle)` deallocates the handle's `Box` while `net_mesh_send` is dereferencing `&*node_handle` (e.g. through `handles_match` / `Arc::ptr_eq`). UAF on the handle pointer. Fix: closed by the same `HandleGuard` work as #24. Send-family entry points (`net_mesh_send`, `net_mesh_send_with_retry`, `net_mesh_send_blocking`) now gate on **both** `MeshStreamHandle.guard.try_enter()` AND `MeshNodeHandle.guard.try_enter()` — either being concurrently freed would otherwise UAF the inner deref. The outer Boxes for both handles are intentionally leaked across `_free`, matching the soundness rule of the parent `NetHandle`.

26. **[FIXED in `7c27af9f`]** **`adapter/net/mesh.rs:2745` (`handle_routed_handshake` Case 2) — Routed-handshake replay nukes the live session, no rate limit.** Unconditionally overwrites `ctx.peers[peer_node_id]` with the fresh session derived from any inbound msg1; no in-progress-session guard, no `HandshakePacer`. Noise NKpsk0's responder uses a fresh ephemeral on each reply, deriving a brand-new session key per replay. A passive attacker who replays a captured msg1 replaces the legitimate session keys; the legitimate sender keeps the old keys, every subsequent packet fails AEAD and is dropped → trivial DoS of any node that ever handshook on a routed path. Fix: if a live session for `peer_node_id` already exists with the same `remote_static_pub`, drop the new handshake (or require it from the same `source`); add the same `HandshakePacer` the legacy adapter uses.

27. **[FIXED in `9239b1c3`]** **`adapter/net/pool.rs:567-575, 597-720` (`ThreadLocalPool`) — Per-thread cache leaks forever.** No `Drop` impl removes the per-thread `LOCAL_BUILDERS[pool_id]` slot. Each new `NetSession` gets a fresh `pool_id` via `NEXT_POOL_ID`. Every connect/disconnect / NAT-rebind / mesh-rebuild cycle leaks ~16 KB × `local_capacity` × `num_threads`. Long-lived daemons OOM in proportion to lifetime peer-churn count. Fix: `impl Drop for ThreadLocalPool` walks every thread's `LOCAL_BUILDERS` to evict its `pool_id` slot, or trade the bare thread-local for a per-thread map weakly keyed off `Arc<ThreadLocalPoolInner>`.

28. **[FIXED in `53de9ecb`]** **`bus.rs:545-570` (`add_shard_internal` rollback) — Stranded batch dispatched with stale `next_sequence` after worker-timeout.** On the worker-shutdown timeout path the JoinHandle is leaked (acknowledged in comment), but the next line still loads `workers.next_sequence.load(Acquire)` and dispatches a stranded batch — the still-running detached worker may not have published its final flush yet, so the rollback emits overlapping msg-ids with the detached worker. Adapter sees two batches claiming the same `sequence_start..sequence_end` range. Fix: on timeout, refuse to dispatch (or use a sentinel `sequence_start = u64::MAX/2`) to avoid colliding with the still-running worker.

29. **[FIXED in `321e2a0e`]** **`ffi/mod.rs:570, 615, 670, 1253` (and similar in `ffi/mesh.rs`) — `slice::from_raw_parts(ptr, len)` lacks `len <= isize::MAX` validation.** Affects `net_ingest`, `net_ingest_raw`, `net_ingest_raw_batch`, `net_ingest_raw_ex`, plus `mesh.rs::collect_payloads`, `net_mesh_publish`, `net_redex_file_append`, `net_identity_sign`, `net_identity_install_token`, `net_parse_token`. A C caller passing a stray sign-extended `-1` (or any `len > isize::MAX as usize`) triggers immediate UB before any guard fires. Fix: reject `len > isize::MAX as usize` at every entry point before constructing the slice.

30. **[FIXED in `7ad49321`]** **`ffi/mod.rs:1134-1145, 1154-1160` — `net_generate_keypair` / `net_free_string` feature-gated, but `net.h` declares them unconditionally.** `net.h:266-271` exposes both with no feature gate; both Rust definitions are `#[cfg(feature = "net")]`. A consumer linking against a cdylib built without `net` gets load-time missing-symbol errors despite the header promising the symbol. Fix: add `#[cfg(not(feature = "net"))]` stubs (NULL-returning / no-op), mirroring the `nat-traversal` pattern at `mesh.rs:975-1058`.

31. **[FIXED in `6ca848f1`]** **`adapter/redis.rs:503-520` (`is_transient_error`) — Cluster `MOVED` / `ASK` / `READONLY` / `CLUSTERDOWN` classified as fatal.** Substring set is `LOADING | BUSY | TRYAGAIN | MASTERDOWN`. After any Redis Cluster failover, every batch fails permanently until process restart. Fix: add `MOVED`, `ASK`, `READONLY`, `CLUSTERDOWN`, `NOREPLICAS` to the substring set; prefer redis-rs's `is_cluster_error()` / `is_io_error()` if available.

32. **[FIXED in `258296c1`]** **`adapter/jetstream.rs:347-356` (`on_batch`) — "Pipelined" publish actually awaits each enqueue serially.** Loop `await`s `publish_with_headers` per event before moving on; only the server-ack `try_join_all(acks)` is parallel. On a 1ms-RTT link a 1k-event batch costs ~1s instead of the documented "~1 RTT per batch." Fix: push the publish-future itself into `acks` (no inner `.await`) and join, or use `js.send_publish(...)`.

33. **[FIXED in `cc96aed6`]** **`adapter/jetstream.rs:104-123` (`poll_shard`) — Per-event `serde_json::Value` allocation violates "MUST NOT allocate per-event" contract.** `adapter/mod.rs:88` documents the contract; the Redis adapter uses `RawValue` for zero-copy. JetStream parses the entire payload tree then re-serializes the `r` subtree per event. At 100k events/poll this is measurable; on a hot replay potentially OOM. Fix: mirror Redis's `RawValue` borrow + `Bytes::copy_from_slice(parsed.r.get().as_bytes())`.

34. **[FIXED in `fd3f73d4`]** **`adapter/jetstream.rs:537-644` (`poll_shard`) — One RTT per sequence in steady state.** `direct_get(seq)` per sequence — a 100-event poll on a 1ms-RTT link costs ≥100ms wall, bounded by latency not bandwidth. Fix: replace per-seq `direct_get` with `direct_batch_get` or a pull consumer's `fetch(N)`.

35. **[FIXED in `6c0b935c`]** **`bus.rs:1393-1400` (`manual_scale_up`) — Loop invariant violated whenever `cooldown > 0`.** Each iteration calls `add_shard_internal` → `mapper.scale_up_provisioning(1)`, which bumps `last_scaling = Instant::now()` on success and rejects with `InCooldown` while `last_scaling.elapsed() < cooldown`. Default `ScalingPolicy::default_for_cpus` has `cooldown = 30s`. Iteration 0 succeeds and stamps the timestamp; iteration 1 immediately fails. The first shard is left half-added (workers spawned, routing entry installed) while `?` propagates `InCooldown` to the caller — partial-state failure with no rollback. Fix: pass `count` through to a single `scale_up_provisioning(count)` call, or bypass the cooldown gate for explicit operator-initiated `manual_scale_up`.

36. **[FIXED in `b53f2d55`]** **`adapter/jetstream.rs:387-410` (`shutdown`) — `self.jetstream` / `self.client` not cleared after `drain()`.** `shutdown` flips `initialized = false`, drains the client, and clears `streams` — but `self.jetstream = Some(...)` and `self.client = Some(...)` survive. `on_batch` checks `self.jetstream.as_ref()`, not `initialized`, so a post-`shutdown` `on_batch` proceeds against a drained client (typically erroring, sometimes hanging). Fix: in `shutdown`, set `self.jetstream = None; self.client = None;` after drain completes, or have `on_batch` consult `initialized`.

37. **[FIXED in `83e0b09e`]** **`adapter/redis.rs:421-426` (`shutdown`) — Pure advisory flag; `conn` retained.** Only flips `initialized = false`; `self.conn = Some(ConnectionManager)` is never cleared, and `get_conn` doesn't consult `initialized`. Subsequent `on_batch` calls write to Redis silently after the operator believes the adapter has shut down. No flush-before-return either. Fix: drop `self.conn` in `shutdown`, and have `get_conn` return `Err(Fatal)` when `initialized == false`.

38. **[FIXED in `50b9f43c`]** **`adapter/jetstream.rs:602` (inner poll loop) — `Fatal` decode error discards already-decoded events without advancing the cursor.** When `deserialize_event` returns `Fatal`, the function returns immediately, dropping every event accumulated in `events` so far. The cursor never advances past the good prefix. After operator recovery the consumer re-polls from the previous cursor and re-emits the entire prefix — duplicate delivery. Fix: return `Ok(SubmitOutcome { events, next_id: Some(last_good_id), .. })` first, then surface the corruption on the next poll.

39. **`adapter/jetstream.rs:327-365` — `(process_nonce, shard_id, sequence_start, i)` msg-id is unsafe across restart unless `sequence_start` is monotonic.** Persistent `process_nonce` (via `dedup_state`) ensures the prefix doesn't change across process restart, but the `(shard_id, sequence_start, i)` suffix must be globally unique-per-batch within the dedup window. If `Batch::sequence_start` ever resets per-restart (e.g. counter reinit instead of reading from `bus.events_dispatched()`), post-restart batches collide with pre-restart ones in the JS dedup window and the second batch is silently dropped server-side. Pin a unit test that asserts `sequence_start` is monotonic across simulated bus restarts.

40. **[FIXED in `09b63c5e`]** **`ffi/mod.rs:1304-1320` (`net_poll_ex`) — Allocation failure after `bus.poll` returns drops the entire batch.** Same shape as #4 for the structured API: `Layout::array::<NetEvent>(count)` and `std::alloc::alloc(layout)` failures both return `NetError::Unknown` and drop `response`. Caller sees a generic error, has no signal that data was lost, retries from the cursor that was already advanced. Fix: pre-validate `count` against a max event-count, or require the caller to pre-allocate the events buffer.

41. **[FIXED in `6483aac4`]** **`ffi/cortex.rs:186-188, 470-473, 956-958, 1402-1404` — `Arc<Inner...>`-wrapped FFI handles share state across threads with no compile-time `Send + Sync` audit.** `RedexFileHandle { inner: Arc<InnerRedexFile> }`, `TasksWatchHandle { stream: TokioMutex<Option<BoxStream<...>>> }`, etc. The handle is returned to C as `*mut HandleType` and routinely shared across goroutines / Python threads (the docstrings advertise this pattern). Soundness rests entirely on the inner type's auto-`Send + Sync` impl. A future refactor that adds a `Cell` / `RefCell` / `Rc` / `*mut` field to `InnerRedexFile` (or analog) compiles cleanly because the FFI layer doesn't typecheck `Send + Sync`. Fix: place `static_assertions::assert_impl_all!(InnerRedexFile: Send + Sync);` next to each handle.

42. **[FIXED in `ffa12891`]** **`adapter/net/swarm.rs:599` (pingwave `strict_progress` predicate) — Address-poisoning via `hops < n.hops` arm.** `strict_progress = pw.seq > n.last_seq || hops < n.hops`. The OR lets an attacker who has observed pingwaves spoof `(origin_id=Y, seq=K, hop_count=0)` for a *not-yet-observed* `K` strictly less than `n.last_seq`. The dedup filter at line 538 admits the entry, `hops=1 < n.hops=2` flips `strict_progress` true, and `n.addr` is overwritten to the attacker's UDP source. The existing regression test `on_pingwave_likely_restart_must_not_overwrite_addr` covers only seq-regression-with-equal-hops. Fix: AND the conditions — require `pw.seq >= n.last_seq` *and* `hops <= n.hops` before honoring the address update.

43. **[FIXED in `62e23855`]** **`adapter/net/compute/orchestrator.rs:1021-1027` (`start_migration` local-source path) — Always emits a single `SnapshotReady` regardless of size.** Returns `chunk_index: 0, total_chunks: 1` whether `snapshot_bytes` is 12 B or 12 MB. The wire encoder rejects any chunk larger than `MAX_SNAPSHOT_CHUNK_SIZE = 7000`; the receiver-side reassembler also rejects via `ChunkTooLarge`. Any locally-initiated migration of a daemon whose serialized state exceeds 7 KB cannot be sent — easily hit by stateful daemons with cached models, large bindings, or non-trivial behavior history. Fix: route the local-source path through `chunk_snapshot(daemon_origin, snapshot_bytes, seq_through)`.

44. **[FIXED in `e6c4fefc`]** **`adapter/net/contested/reconcile.rs:174-196` — `unreachable!()` is reachable on signed but divergent input.** The divergence loop at lines 105-117 enters the conflict branch on payload mismatch *or* `(parent_hash, sequence)` mismatch. When divergence is by parent-hash/sequence only and payloads happen to be byte-identical (e.g. after pruning + re-issue of the same payload under a different parent), the equal-length tiebreak at 174-196 falls into `Ordering::Equal` and panics the reconciliation thread. A malformed-but-signed remote chain crashes the worker. Fix: replace `unreachable!` with a deterministic tiebreak on `parent_hash` (or sequence).

45. **[FIXED in `243fceb8`]** **`adapter/jetstream.rs:692-705` (`is_transient_error`) — `PublishErrorKind::Other` matched as transient.** `Other` is async-nats's catch-all — it includes auth failures, permission denied, account-misconfig, and malformed-subject errors. Classifying all of them as transient drives the bus's outer retry loop into an infinite tight retry storm against a backend that will never succeed. This is a production-down scenario when a NATS account is misconfigured at deploy time. Fix: enumerate the truly transient `PublishErrorKind` variants explicitly and treat `Other` as fatal.

46. **[FIXED in `70573fd1`]** **`bus.rs:1056-1106` (`flush` Phase 2 barrier) — Inflated `dispatched` counter satisfies the inequality while pre-flush events are still in flight.** `target_ingested` is snapshotted at flush entry, but `dispatched` is the running global counter — events ingested *after* `flush()` returns also bump it. With asymmetric per-shard latency and concurrent ingest on a fast shard, the inequality `dispatched + dropped_during_flush >= target_ingested` can be satisfied by post-flush traffic while pre-flush events are still queued in a slow shard's mpsc channel or its batch worker's `current_batch`. `all_shards_empty()` only inspects ring buffers, not the per-shard mpsc channel or the batch worker's pending batch. The documented at-least-once delivery barrier silently gives up. Fix: also snapshot `dispatched + dropped` at flush entry and gate on the *delta* of dispatched/dropped exceeding the *delta* of ingested, or expose a per-shard "fully-flushed" predicate that includes the channel + batch worker.

47. **[FIXED in `8dc646f1`]** **`bus.rs:1437-1446` (`manual_scale_down`) — Shards finalized by other callers are silently leaked.** `mapper.finalize_draining()` is destructive — every qualifying `Draining` shard is transitioned to `Stopped` in one shot. If the scaling monitor (or a parallel `manual_scale_down` on a different target set) drains a shard concurrently, those shard ids appear in the returned `stopped` list, but the `if target.contains(&shard_id)` filter drops them on the floor. The non-target shards are now stuck in `Stopped` with their workers and routing-table entry intact — `remove_shard_internal` is never called. Subsequent polls iterate the orphan, the ring buffer keeps consuming memory, and `num_shards()` reports an inflated count. Fix: still call `remove_shard_internal` for the non-target ids, or stop calling `finalize_draining` from non-monitor paths.

## Medium

48. **[FIXED in `f2207bdd`]** **`adapter/net/cortex/adapter.rs:122-141` (`wait_for_seq`)** — When `start_seq == 0` and no events fold, the `u64::MAX` sentinel + always-true `running` makes `wait_for_seq(0)` block forever. Direct `CortexAdapter` users on an empty file hang.

49. **[FIXED in `7df111c4`]** **`adapter/net/cortex/memories/fold.rs:67-86` — Asymmetric upsert semantics under `STORED`.** Existing-id store partial-updates (preserves `pinned`/`created_ns`, overwrites the rest); missing-id inserts with `pinned: false`. The `MemoriesAdapter::store` doc reads "Store a new memory" — silent overwrite is surprising. Combined with absence of tombstones (`memories/fold.rs:115`), a STORED→DELETED→STORED sequence resurrects deleted entries.

50. **[FIXED in `abe99ff9`]** **`adapter/net/cortex/memories/query.rs:77-86` — Empty-vec filter is a footgun.** `Some(vec![])` for `require_any_tag` excludes everything (`any` over empty = false → reject); `Some(vec![])` for `require_all_tags` excludes nothing (`all` over empty = true). UI forms emitting empty multi-selects break silently.

51. **[FIXED in `929da149`]** **`adapter/net/failure.rs:783-805` (`RecoveryManager::on_failure`)** — `failed_nodes.insert(...)` overwrites existing `FailedNodeState`, resetting `failed_at` and `retry_count` to 0. Flapping peers never hit `max_retries`. Fix: `entry().or_insert(...)` and bump `retry_count`.

52. **[FIXED in `db57ae34`]** **`adapter/net/session.rs:543-565` (`evict_idle_streams`) — LRU vs concurrent open race.** `streams.iter().min_by_key(...)` then `remove` is non-atomic. A freshly-opened stream can be torn down between selection and removal. Fix: `remove_if` with a freshness predicate.

53. **[FIXED in `e5a8dfbf`]** **`adapter/net/mod.rs:451` — Handshake retry has no upper bound.** `tokio::time::sleep(Duration::from_millis(100 * attempt as u64))` over `MAX_HANDSHAKE_RETRIES = 1024` ⇒ ~14 hours total, last attempt sleeps ~102s. Fix: cap the per-attempt sleep at e.g. 5s.

54. **[FIXED in `bded1c07`]** **`adapter/net/redex/file.rs:402-462` (`append`) — Observer inconsistency.** `next_seq` is bumped before `segment.append`, so a reader between (`len()`, `next_seq()`) sees `len + 1 > next_seq_seen`. Fix: maintain a separate `committed_seq` for observers.

55. **[FIXED in `465173e4`]** **`bus.rs:1337` — `JoinHandle` errors silently dropped.** `let _ = futures::future::join_all(drains).await;` — a panicked drain worker isn't logged unless the runtime's panic hook is configured (default Tokio doesn't). Stranded events.

56. **[FIXED — wire-format widening to u64]** **`adapter/net/identity/origin.rs:42-43` — `origin_hash: u32` collision floor.** ~65k peer birthday-collision; cross-channel accounting keyed by `origin_hash` aliases distinct entities. Fix: widened `EntityId::origin_hash` and all application-layer types (`EntityKeypair`, `OriginStamp`, `CausalLink`, `EventMeta`, `CausalChainBuilder`, all `DashMap`/`HashMap`/`HashSet` keys, all migration-message wire encoders/decoders, `DaemonError::NotFound`/`Stale`, `EntityLog::origin_hash`, `MigrationError::DaemonNotFound`/`AlreadyMigrating`, FFI `net_identity_origin_hash`, etc.) to u64 — eliminates the ~65k birthday-collision floor across cross-channel accounting and chain-integrity checks. Wire format bumps: `CAUSAL_LINK_SIZE` 28→32, `EVENT_META_SIZE` 20→24, `CONTINUITY_PROOF_SIZE` 36→40, `ForkRecord` wire 25→33. `NetHeader::origin_hash` STAYS u32 (per-packet routing fast-path; collisions there are benign, bumping per-packet header has a much larger blast radius); callers downcast `kp.origin_hash() as u32` at the `NetHeader` boundary. The low 32 bits of the new u64 derivation are byte-identical to the prior u32 derivation (same BLAKE2s domain label `b"net-origin-v1"`), so `NetHeader` carries the same routing bytes as before. No backwards compat — migration cliff documented in release notes. C header `net.go.h` updated: `net_identity_origin_hash` returns `uint64_t`.

57. **[FIXED in `bd3df772`]** **`shard/mapper.rs:1024` (`finalize_draining`) — Wrong atomic ordering.** Reads `pushes_since_drain_start` with `Relaxed` while the field's docstring requires `Acquire` to pair with the writer's `SeqCst` reset.

58. **[FIXED in `3e14600e`]** **`adapter/net/identity/token.rs:565-594` (`insert_unchecked`) — Slot-cap race unbounded.** `contains_key` then `entry()` overshoot is bounded by concurrent calls, not shards. Fix: `entry().or_insert_with()` then drop on overflow.

59. **[FIXED in `897e7a1d`]** **`adapter/net/behavior/diff.rs:241-243` — `to_bytes` returns `Vec::new()` on cap-violation.** Indistinguishable from a legitimate empty diff. Senders silently transmit zero bytes; receiver drops. Fix: deprecate `to_bytes` in favor of `try_to_bytes`.

60. **[FIXED in `45f04b3e`]** **`adapter/net/contested/propagation.rs:62-106` — Lossy `f64 → u64` poisons EWMA.** A pathological RTT clamps `per_hop` to `u64::MAX`, locking `base_hop_latency_nanos` near max permanently. Combined with the `multiplier == 0.0` check (line 96) which doesn't catch NaN.

61. **[FIXED in `2747f7e0`]** **`adapter/net/contested/correlation.rs:148` — `Instant` subtraction can panic.** `now - self.config.correlation_window` panics if `correlation_window > now.elapsed()` (configurable to hours).

62. **[FIXED in `d1748217`]** **`adapter/net/contested/superposition.rs:107-111` — `target_replayed` only advances from `Superposed`.** If called while in `Spreading` (target catches up before `advance(Replay)`), phase stalls in `Spreading` forever; `ReadyToCollapse` never fires.

63. **[FIXED in `d03e773d`]** **`adapter/net/contested/observation.rs:62-73, 115` — Unbounded `HashMap` growth + `seq_diff_sum` overflow.** No eviction in `last_observed`/`estimated_delay`; `u64::abs_diff` accumulator overflows on long chains. Fix: LRU + `saturating_add`.

64. **[FIXED — query host's real chain head]** **`adapter/net/compute/orchestrator.rs:1177-1182` (`on_replay_complete`) — Forged `target_head` with `parent_hash: 0`.** Synthetic link doesn't match any real hash; downstream verifiers can't reconcile. Fix: introduced `DaemonHost::head_link()` accessor that exposes the chain's real `CausalLink` (origin_hash, horizon_encoded, sequence, parent_hash). `on_replay_complete` now queries the freshly-replayed daemon via `daemon_registry.with_host(...)` and stamps the host's actual head into `superposition.target_replayed(...)` instead of the synthetic `parent_hash: 0`. Falls back to the synthetic link with a `tracing::warn!` if the daemon isn't registered locally — same downstream-unverifiable failure mode as pre-fix but with operator visibility. Also logs (without overriding) when the host's head sequence disagrees with the caller's `replayed_seq` so a pipeline divergence is observable.

65. **[FIXED in `f14a2361`]** **`adapter/net/compute/migration_target.rs:244-261` (`buffer_event`) — No phase guard.** Inserts/delivers post-cutover; combined with normal-path delivery yields duplicate execution.

66. **[FIXED in `1c592746`]** **`adapter/net/compute/migration_source.rs:107-145` (`start_snapshot`) — Snapshot side-effect race.** `contains_key` → `entry()` window allows two concurrent snapshots of the same origin via user `MeshDaemon::snapshot()`. Trait API doesn't enforce idempotency.

67. **[FIXED in `55eeeff0`]** **`adapter/net/compute/standby_group.rs:213-253` (`sync_standbys`)** — Partial-failure leaves standbys at mixed states with `synced_through` unchanged and buffer not cleared; subsequent `promote()` may double-deliver.

68. **[FIXED — same change as #13]** **`adapter/net/compute/registry.rs:91-96` (`unregister`)** — In-flight `Arc<Mutex<DaemonHost>>` clones survive past unregister; a fast `register` of the same origin produces two parallel Arcs locking independently, splitting writes. Fix: same `DaemonError::Stale` + `guard_identity` re-check pattern as #13. Mutators with an OLD Arc whose origin has since been `unregister`ed (entry now absent) or replaced via `unregister`+`register` (entry now NEW) detect the orphan and bail with `Stale` instead of silently mutating the orphan and splitting writes. Same residual hazard for pre-acquired-lock mutators applies.

69. **[FIXED in `fdd45ec6`]** **`adapter/net/state/causal.rs:248` — Doc says `CausalLink: 24 bytes`, constant is 28.** Wire-format spec contradicts code; external parsers reading per the doc go off-by-4.

70. **[FIXED in `c07976cc`]** **`adapter/net/contested/partition.rs:241-246` — `NaN >= threshold` blocks healing.** When `other_side.is_empty()`, ratio is NaN and partition can't heal automatically.

71. **[FIXED in `1890d5d8`]** **`adapter/net/redex/disk.rs:1196` — `saturating_sub(dat_base) as u32` for stale heap entry silently writes offset 0.** Currently safe by `dat_base` derivation, but masks corruption rather than failing.

72. **[FIXED in `0d30c819`]** **`adapter/net/traversal/portmap/natpmp.rs:537-606` (`remove`)** — Silent early returns on `bind`/`connect`/`send` failures; operator never sees that the prior mapping wasn't revoked.

73. **[OBSOLETE — addressed by rollback+override refactor; regression test pinned]** **`consumer/merge.rs:384` — Per-shard cap clamps cursor advancement.** When `unclamped_per_shard > PER_SHARD_FETCH_CAP`, cursor rolls back instead of advancing to the last fetched event id; single-shard requests with filters can stall in re-fetch loops. The audit's stall scenario doesn't reproduce on the current code: the `rollback + override` logic at lines ~696-760 rolls the cursor back to the original position only for shards with truncated matches (the un-returned matches re-appear next poll), then overrides to the last *returned* match's id so each poll makes `request.limit` worth of forward progress. Added `poll_merger_does_not_stall_on_single_shard_filter_under_cap` regression that drains 50 events at limit=10 in 5 polls and asserts the watchdog never trips — pins the no-stall property going forward.

74. **[FIXED in `5af9cfc7`]** **`bus.rs:1245` — `std::time::Instant` mixed with tokio time.** Wall-clock `5s` in shutdown breaks `tokio::time::pause()`-based tests.

75. **[FIXED in `1990f863`]** **`adapter/net/route.rs:461-481` — Doc/code mismatch on tie-break.** Doc says "preserved if strictly better"; code uses `<=`, allowing equal-metric overwrite.

76. **[FIXED in `90249bc4`]** **`ffi/mod.rs:563` (and similar handle accessors) — `&*handle` without alignment check.** Misaligned `*mut NetHandle` from C is immediate UB before the null check; only `is_null()` is validated.

77. **[FIXED in `41ebbf8f`]** **`ffi/mod.rs:1075-1081` — Concurrent `net_shutdown` callers race the `bus_taken` swap.** The drain protocol guards in-flight FFI ops but not concurrent shutdown callers themselves. A second/third caller can return `Success` while the first is still inside `runtime.block_on(bus.shutdown())` — falsely signaling completion of an in-progress shutdown. The doc admits this; nothing enforces it.

78. **[FIXED in `55d0bc59`]** **`ffi/cortex.rs` and `ffi/mesh.rs` (every `runtime().block_on(...)` site: cortex.rs:419, 670, 910, 1199, 1455; mesh.rs:475, 639, 660, 676, 698, 810, 829, 902, 1209, 1242, 1277, 1360, 1689) — No `Handle::try_current()` guard.** `ffi/mod.rs:21-53` documents the contract; the cortex/mesh modules don't enforce it. A `block_on` panic unwinds across `extern "C"` → UB. `catch_unwind` shim is missing.

79. **[FIXED in `aea77a09`]** **`adapter/jetstream.rs:267-380` (`on_batch`) — Partial-failure can produce duplicate publishes.** Mid-batch publish error drops in-flight `PublishAckFuture`s but bytes are already on the wire; retry re-publishes. `Nats-Msg-Id` dedups *only within the dedup window* (default 2 min). Slow caller / long retry backoff lands the same event at two distinct seqs.

80. **[FIXED in `9b9279ec`]** **`adapter/jetstream.rs:336-371` — Async-cancellation hazard mid-loop.** No timeout wraps `publish_with_headers`. Tokio cancellation drops `acks` without awaiting; bytes already sent persist. Same wire/local-cancel split as #79. Compare `redis.rs:396` which does wrap.

81. **[FIXED — one-time-per-process warn at adapter init]** **`adapter/redis.rs:316-405` — Pipeline timeout duplicate hazard documented but unfixed by default.** Lines 376-394 document the race. The `dedup_id` mitigation depends on the consumer running `RedisStreamDedup`, which is *not* wired up by default — Go consumers that skip the helper see duplicates. Fix: emit a one-time-per-process `tracing::warn!` from `RedisAdapter::init` pointing at the consumer-side helper (`net_sdk::RedisStreamDedup`) and the at-least-once-with-retry-duplicates contract. The producer side cannot enforce consumer behavior (Redis Streams decouple them), so the warn is the minimum-mechanism, maximum-visibility surfacing the audit asks for. Deployments running without the helper see the deployment guidance; explicit ignore is the user's choice. The producer's `dedup_id` field is unchanged — the contract was always documented, just easier to miss.

82. **[FIXED in `6d4ce914`]** **`adapter/net/mesh.rs:4191` — Lone non-saturating `hop_count += 1` on attacker-controlled byte.** Every other hop-count increment in the crate uses `saturating_add(1)` (`swarm.rs:122`, `route.rs:254`). Bounded today by the `< MAX_CAPABILITY_HOPS - 1` (15) guard above it, but one constant change from a debug-panic / release-wraparound on attacker-controlled input. One-line hardening.

83. **[FIXED in `90aa66de`]** **`shard/mapper.rs:838-856` (`activate`) — `active_count.fetch_add` outside the held write-lock allows transient budget overshoot.** Sequence is `load → check → state-mutate → drop(shards) → fetch_add`. Two activates each pass the budget gate under their respective held locks; a third caller landing between either `drop(shards)` and the corresponding `fetch_add` reads the still-stale count and also passes. `active_count` transiently exceeds `max_shards`. Fix: move `self.active_count.fetch_add(1, Release)` *before* `drop(shards)`.

84. **[FIXED in `434b2f6e`]** **`bus.rs:1200-1212, 1246-1274` (`shutdown_via_ref` and in-flight wait loops) — Bare `tokio::task::yield_now` thrashes the runtime.** `yield_now()` doesn't park; it just re-queues the task. Tight loop under contention starves the workers it's waiting on, prolonging shutdown. Fix: `tokio::time::sleep(Duration::from_millis(1)).await` (or backoff).

85. **[FIXED in `e954a7f6`]** **`consumer/merge.rs:681-685` (`PollMerger::poll`) — Stalled poll loses cursor context.** `next_id` is `None` when no shards make progress, even if the caller passed a valid `request.from_id`. Caller may interpret `None` as "no events" and re-fetch from the beginning — silent regression of pagination across a stalled stream. Fix: when `request.from_id.is_some()` and no progress was made, echo back the original `from_id`.

86. **[FIXED in `5cba14be`]** **`bus.rs:1471-1528` (`Drop`) — Stranded events not surfaced in stats.** When the bus is dropped without `await shutdown()`, ring-buffer events are lost (acknowledged in comment) but `events_dropped` is never incremented and `shutdown_was_lossy` is never set. Operators reading post-mortem stats see no record of the loss. Fix: snapshot `shard_manager.shard_stats()` in `Drop`, increment `events_dropped` by each shard's ring-buffer length, set the flag.

87. **[FIXED in `cb58131c`]** **`adapter/net/failure.rs:297, 569, 692` (`FailureDetector::cleanup`, `CircuitBreaker`) — `unwrap()` on poisoned `std::sync::Mutex`.** The rest of the crate uses `unwrap_or_else(|p| p.into_inner())` (see `crypto.rs:691`). A single panic anywhere holding these locks turns every subsequent `unwrap()` into a runtime panic that takes down the failure-detection loop. Fix: switch to the `into_inner` pattern, or `parking_lot::Mutex`.

88. **[FIXED in `51875b4c`]** **`adapter/net/router.rs:461-546` (`route_packet`) — No source/loop suppression.** Decrements TTL based purely on `routing_header.dest_id`. No `routing_header.src_id == self.local_id` check (drop own packets coming back) and no LRU duplicate cache. Misconfiguration / `add_route_with_metric` flap / malicious peer can build a 2-hop loop only broken by TTL exhaustion. Fix: drop in `route_packet` when `routing_header.src_id == routing_table.local_id`; consider a small `(src_id, stream_id, sequence)` LRU.

89. **[FIXED in `624fd7ec`]** **`adapter/net/linux.rs:110-197` (`BatchedTransport::send_batch`) — Silently truncates above `MAX_BATCH_SIZE = 64`.** `let total = packets.len().min(MAX_BATCH_SIZE)` then returns `Ok(<= 64)`. Caller compares against `packets.len()` to detect partial sends; a "successful" return of 64 hides that packets `[64..]` never reached the wire. Reliable streams already stashed those via `on_send` and only learn via NACK/RTO retransmit. Fix: return `io::Error::InvalidInput` when over the cap, or chunk internally and loop until exhausted.

90. **[FIXED in `40e81406`]** **`adapter/net/config.rs:104` + `pool.rs:646-657` — `MAX_PACKET_POOL_SIZE = 1<<20` can OOM on first session.** `with_local_capacity` pre-allocates `size` builders × ~16 KB. At the configured ceiling that's ~16 GiB up-front per session — guaranteed OOM. The cap was intended to *prevent* OOM. Fix: lower to a few thousand, or lazy-allocate buffers on first use.

91. **[FIXED in `3982cc54`]** **`ffi/mod.rs:1409-1425` (`net_free_poll_result`) — Not idempotent, easy double-free.** Frees `events` and `next_id` but leaves the struct fields holding the freed pointers. C header just says "free a poll result" with no contract that the caller must null fields. A second call double-frees. Fix: after freeing, set `(*result).events = ptr::null_mut(); (*result).count = 0; (*result).next_id = ptr::null_mut();`.

92. **[FIXED in `48377fbc`]** **`ffi/redis_dedup.rs` (every `extern "C"` entry; representative for the crate-wide gap) — No `catch_unwind` shim.** With `panic = "unwind"` (Rust default), any panic inside an `extern "C"` body is UB across the cgo / N-API / cffi boundary. Partly overlaps with #78 but applies to *all* panic sources, not only runtime-context conflicts. Fix: set `panic = "abort"` for the cdylib, or wrap every FFI body in `catch_unwind`.

93. **[FIXED in `cd3661eb`]** **`adapter/redis.rs:489-498` (`is_healthy`) — `tokio::time::timeout` cancellation desyncs `ConnectionManager`.** Same race the `on_batch` doc warns about. When timeout fires, the in-flight PING future is dropped while bytes are still on the wire; a subsequent caller's response can be served the leftover PING reply, producing spurious "wrong type" errors on the next real command. Fix: dedicated health-check connection, or use `ConnectionManager`'s native timeout config.

94. **[FIXED in `6312cc20`]** **`adapter/jetstream.rs:475-535` (`poll_shard`) — Cold-stream bail enabled on transient `info()` failure.** On `info()` error, fallback uses `first_seq = 0`, which *enables* the cold-stream bail. A populated stream returning NotFound in a deletion gap then bails after 64 NotFounds; consumer believes the stream is drained when there are events ahead. Fix: propagate `AdapterError::Transient` rather than fabricating `first_seq = 0`.

95. **[FIXED in `eaad4d83`]** **`adapter/jetstream.rs:236-242, 387-410` — Init-after-shutdown silently overwrites client without `drain()`.** `shutdown` clears `initialized = false`; `init` no-ops only on `initialized = true`. A reconnect path that calls `init` post-shutdown overwrites `self.client` without first draining, losing in-flight publishes piggybacking on the prior client. Fix: in `init`, if `self.client.is_some()` and `initialized == false`, drain the prior client before overwriting.

96. **[FIXED in `4b56749a`]** **`adapter/net/crypto.rs:483-494` (`shift_bitmap_up`) — Forward-jump > 1024 zeroes anti-replay state instead of refusing.** `MAX_FORWARD = 65_536` and `WINDOW_SIZE = 1024`. A single authenticated packet whose counter jumps `1025..65_536` past `rx_counter` is accepted by `is_valid`, the bitmap is zeroed, and any previously-seen counter inside `rx_counter - 1024 .. rx_counter` (whose "seen" bit just got dropped) becomes replayable. The warning log fires but the slide proceeds. Fix: refuse to slide more than `WINDOW_SIZE` and require a fresh handshake; or shrink `MAX_FORWARD` to `WINDOW_SIZE`.

97. **[SKIPPED in `f41d9c36`]** **`adapter/net/session.rs:1153-1154` (`apply_authoritative_grant`) — Clamp to a racy `tx_bytes_sent` watermark truncates legitimate grants.** `try_acquire_tx_credit` decrements `tx_credit_remaining` (AcqRel CAS) *before* `fetch_add`-ing into `tx_bytes_sent`. The grant's `total_consumed.min(sent_watermark)` clamp Acquire-loads `tx_bytes_sent` and can observe a value that pre-dates an in-flight admission's `fetch_add`, while the receiver-reported `total_consumed` already reflects bytes from the same admission. Truncates a legitimate grant; sender stalls briefly until the next grant unsticks it. Self-healing per the comment, but each truncation is a measurable send-side hiccup. Fix: bump `tx_bytes_sent` *before* decrementing `tx_credit_remaining`.

98. **[FIXED in `f1f21fd3`]** **`adapter/net/session.rs:892-911` (`RxCreditState::on_bytes_consumed`) — `consumed` and `granted` are not jointly atomic.** Two threads each calling `on_bytes_consumed(100)` `fetch_add` independently; thread A's `consumed` store can be globally visible before its `granted` store. A concurrent `outstanding()` reader computes `granted - consumed` and transiently sees `consumed > granted`. `saturating_sub` masks it to 0, so admission is unaffected, but observability/metrics show transient zeroes during contention. Fix: take a single AcqRel CAS on a packed `u128` (consumed:64 | granted:64).

99. **[FIXED in `571b6301` + age-sweep follow-up]** **`adapter/net/compute/orchestrator.rs:782-790` (snapshot reassembly `pending` map) — Unbounded chunk hold via repeated `seq_through == latest`.** Eviction at 782-790 only fires when the new chunk's `seq_through` is *strictly greater* than `latest_seq`. The reject gate at 770 only blocks `seq_through < latest`, so `==` is admitted. Combined with `MAX_TOTAL_CHUNKS = 700_000` × `MAX_SNAPSHOT_CHUNK_SIZE = 7000` per pending entry, an attacker can park up to ~4.3 GiB of unfinished reassembly per `(origin, seq)` and refresh forever. There's no per-peer cap, no time-based sweep. Fix: per-entry byte cap (`MAX_PENDING_REASSEMBLY_BYTES = 64 MiB`) lands in `571b6301`; per-entry age sweep (`MAX_PENDING_REASSEMBLY_AGE = 5 min`, opportunistic at the head of every `feed` plus a public `sweep_stale` for external timers) is the follow-up that closes the at-cap-and-quiet residual hole the byte cap couldn't reach.

100. **[FIXED in `36381e18`]** **`adapter/net/state/log.rs:88-99` (`EntityLog::append`) — Genesis-shaped event accepted with un-validated payload.** When `events.is_empty()` and `current_head.is_genesis()` and the new event's `link.is_genesis()`, the append is taken without any check on `event.payload` (any byte string passes). A peer-injected genesis-shaped event seeds the log with an attacker-chosen payload; if no successor ever arrives, the log permanently carries an unverified anchor. Fix: pin a canonical genesis payload (often the empty vector) and reject anything else.

101. **[FIXED in `60d88d12`]** **`adapter/net/identity/token.rs:55-59` (`TokenScope::contains`) — `contains(NONE)` returns true unconditionally.** `(self.bits & NONE.bits) == NONE.bits` simplifies to `0 == 0`. Any token authorizes the no-op action; for any caller that builds an `action: TokenScope` from external input (e.g. a wire `u32` masked into a smaller subset and passed to `authorizes`) and the input happens to mask to `NONE`, the call returns `true` for *every* token. Compounds with `authorizes(NONE, ch)` returning unconditional `true`. Fix: short-circuit `if other == TokenScope::NONE { return false; }` at the top of `contains`.

102. **[FIXED — wire-format version byte; v0 fallback removed]** **`adapter/net/identity/envelope.rs:361-396` (v0 fallback path) — Doubled AEAD CPU under attacker-shaped traffic.** Every `open` runs an AEAD decrypt with `aad_v1 = chain_link.to_bytes()`; on failure it retries with `aad = &[]` (the v0 envelope shape). Integrity is preserved by the outer signature, but the verifier pays two AEAD verifies per probe under any traffic that doesn't match v1 AAD — a passive attacker submitting random ciphertext can double the verifier's CPU per probe. After re-reading the code: `open` runs the ed25519 `verify_strict` signature check (line 323) BEFORE any AEAD attempt. Random ciphertext cannot forge a valid signature over `target_pub || chain_link`, so it never reaches the AEAD-with-fallback path. The 2× AEAD only fires on envelopes that already passed signature verification — i.e., legitimate-but-replayed v0 envelopes during a rolling upgrade. The doubling is the documented cost of the v0 fallback (`open_accepts_v0_envelope_for_rolling_upgrade_compat` test pins this) and not a DoS amplifier on random traffic. Wire-bump fix landed: added `IDENTITY_ENVELOPE_VERSION = 1` byte at the head of every envelope (size 208 → 209); `from_bytes` rejects anything other than v=1; the v0 AEAD fallback path is gone (single AEAD attempt, deterministic AAD). `SNAPSHOT_VERSION` bumped 1 → 2 to reflect the embedded envelope size change; v1 snapshots and pre-magic v0 snapshots are rejected at `StateSnapshot::from_bytes`. No backwards compat — migration cliff documented in the project release notes. Three regression tests pin: 209-byte size, version-byte at offset 0, post-bump rejection of pre-bump v0 envelopes (and a parallel snapshot-side rejection test).

103. **[FIXED in `da92f04c`]** **`ffi/mesh.rs:199-205` (`c_str_to_str`) — Lifetime elision binds returned `&str` to the `*const c_char` reference, not the underlying C buffer.** The signature `unsafe fn c_str_to_str(p: &*const c_char) -> Option<&str>` makes the borrow checker treat the result as if it lived as long as the local stack slot. Today's call sites are stack-only, but a future refactor that moves the result into a `tokio::spawn(async move { ... })` compiles silently and hands a dangling pointer to the spawned task. Fix: rebind explicitly via `unsafe fn c_str_to_str<'a>(p: *const c_char) -> Option<&'a str>` and force every caller to assert the lifetime, or always copy into `String`.

104. **[FIXED in `c43208bf`]** **`ffi/mod.rs:662-680` (`net_ingest_raw_batch`) — Silent drop of null and invalid-UTF-8 entries.** A null entry pointer `continue`s; an invalid-UTF-8 entry is dropped. Function returns the number of accepted events without a per-entry error array. A binding that passed `[]string` with one garbled element sees `count - 1` accepted, attributes the drop to back-pressure, retries the wrong indices, and double-publishes the good ones. Fix: accept an `out_failed_indices: *mut size_t, out_failed_len: *mut size_t` pair to surface the exact dropped indices.

105. **[FIXED in `928bd520`]** **`ffi/mod.rs:180-186, 1048-1053` — `bus_taken` defense-in-depth claim is doc-only.** The doc on `bus_taken` says "FFI ops also check this before touching `bus`/`runtime`," but a grep finds the field read only inside `net_shutdown`. Soundness today is rescued by `shutting_down` + `active_ops`, but the doc-vs-code gap is a future-refactor trap. Fix: either gate every `enter_ffi_op` against `bus_taken` to make the doc match, or remove the misleading sentence.

106. **[FIXED in `64dd771c`]** **`bus.rs:621-624 / 779-782` (`poll_merger` swap) — Lost-update race between concurrent add/remove.** Two callers (e.g. scaling-monitor `add_shard_internal` racing `manual_scale_down → remove_shard_internal`) read `shard_manager.shard_ids()` and then race on `self.poll_merger.store(...)`. T1 reads `{0..5}`, T2 reads `{1..4}`, T2 stores `{1..4}`, T1 stores `{0..5}` — final published view includes a removed shard 0. The merger then routes polls to a removed shard until the next topology change repairs the snapshot. Fix: serialize the `shard_ids() → store` block under a mutex.

107. **[FIXED in `e42dcc15`]** **`bus.rs:1245-1271` (lossy-shutdown bookkeeping) — `events_dropped` incremented for events that may still be delivered.** On the in-flight deadline path, `stranded = self.in_flight_ingests.load(SeqCst)` is added to `events_dropped`. But many of those producers will still successfully complete `shard_manager.ingest()` (their event lands in the ring buffer) AND be drained on the final two-pass sweep. Those events get counted in both `events_ingested` and `events_dropped`, violating the invariant `events_ingested = events_dispatched + events_dropped` and making `shutdown_was_lossy` a false positive on every deadline-triggered shutdown. Fix: subtract the events the final sweep actually drained, or replace the heuristic with an explicit "events still in ring at deadline" count.

108. **[FIXED in `10f61560`]** **`shard/mapper.rs:172-177` (`record_flush`) + `246-247` (`collect_and_reset`) — Latency-sum/count desync zeroes the average that drives scale-up.** `record_flush` does two independent `fetch_add`s; `collect_and_reset` does two independent `swap`s. If a metrics tick runs between the two `fetch_add`s, the swap of `flush_latency_sum_us` captures a sum that includes a flush whose count is captured in the *next* window. The doc-comment acknowledges the inaccuracy as "an accepted trade-off," but the real impact is that `avg_flush_latency = sum.checked_div(count).unwrap_or(0)` returns 0 in window N (sum without count) and 0 in window N+1 — the average that `evaluate_scaling`'s flush-latency trigger reads silently zeros out under sustained load, suppressing the scale-up decision. Same race for `push_latency_sum_ns` / `push_count`. Fix: pack `(sum, count)` into a single `u128` and CAS together.

109. **[FIXED in `288571ef`]** **`ffi/mod.rs:372-380` (`parse_config_json`) — Unknown `backpressure_mode` silently falls back to `DropNewest`.** `parse_poll_request_json` was specifically fixed (with a regression test) to surface unknown / wrong-typed enum strings as `InvalidJson`; this parallel parser silently swallows typos and selects `DropNewest`. A caller writing `"DropOldset"` (typo) or `"FailProduce"` gets a different durability profile with no error at deploy time. Same parser also has no support for `BackpressureMode::Sample { rate }`. Fix: error on unknown values, and add a `Sample` arm with `rate` validation.

110. **[FIXED in `426d325b`]** **`ffi/cortex.rs:225-239` (`net_redex_open_file`) — `retention_max_*` accept zero, fsync params do not.** Three lines earlier the parser explicitly rejects `(Some(0), _)` / `(_, Some(0))` for fsync; `retention_max_events`, `retention_max_bytes`, and `retention_max_age_ms` are propagated unchecked. Setting `retention_max_events = 0` means "evict everything immediately on first append" — almost certainly a config mistake intended as "no limit," especially since most JSON config schemas use `null` or omission for "unlimited." Inconsistent and silent data loss. Fix: reject `Some(0)` for the retention triple at the same gate that rejects fsync zeros.

111. **[FIXED in `811e4ff3`]** **`ffi/mod.rs:490-498` and `ffi/mesh.rs:411-417` — Net `heartbeat_interval_ms` / `session_timeout_ms` and mesh `heartbeat_ms` accept `0`.** `EventBusConfig::validate` is careful to reject zero `Duration`s for `cooldown`, `metrics_window`, `scale_down_delay`, `adapter_timeout`, etc. The Net adapter and mesh JSON parsers have no equivalent zero-check at the FFI boundary — a `0` flows through to `Duration::from_millis(0)`. Heartbeat-every-0ms in particular busy-loops the heartbeat task and saturates a CPU. Fix: validate the values before constructing the `Duration`.

## Lower-priority

112. **[FIXED in `1bc5adc1`]** **`bus.rs:1064`** — sync `parking_lot::Mutex::lock` inside `async fn flush`.

113. **[OBSOLETE]** **`adapter/jetstream.rs:604, 628`** — unchecked `current_seq += 1` overflow on long-lived streams. Already addressed by an earlier `saturating_add` change at the cited line — current code uses `current_seq = msg_seq.saturating_add(1)`.

114. **[FIXED in `56d370c5`]** **`shard/batch.rs:60`** — `total_events` unchecked add interacts with `saturating_sub` velocity → 0 across wraparound boundary.

115. **[FIXED in `a595dfaf`]** **`adapter/net/behavior/loadbalance.rs:984-985`** — NaN slips past `total_weight <= 0.0` (use `!(total_weight > 0.0)`).

116. **[FIXED in `7e0b426e`]** **`adapter/net/behavior/safety.rs:1259-1283`** — `release` uses `Relaxed` while `acquire` uses `AcqRel` — observable counter drift on weakly-ordered cores (ARM/RISC-V).

117. **[FIXED in `25e55f40`]** **`adapter/net/behavior/safety.rs:1200-1206`** — audit-only token counter `fetch_add` without saturating.

118. **[OBSOLETE — current behavior is correct rate-limit semantic]** **`adapter/net/behavior/rules.rs:1247-1253`** — window reset to `1` not `0` admits `max-1` further firings. After tracing the code: `record_execution` increments `execution_count` then, on window-expiry, overrides to 1 (the current execution counts as the first of the new window). Subsequent gating allows `max-1` more firings before `count >= max` blocks — total `max` firings per window, which is the documented rate-limit semantic. The audit's suggested "reset to 0" would allow `max+1` firings per window (this one + max more before block), exceeding the cap.

119. **[FIXED in `47005c6f`]** **`adapter/net/proxy.rs:317-322`** — telemetry counters incremented before send succeeds.

120. **[FIXED in `3ed63b78`]** **`adapter/net/failure.rs:818-841`** — `get_action` returns `Retry { delay_ms: 0 }` for healthy nodes, busy-loop footgun.

121. **[OBSOLETE — degenerate case is the algorithm with 2 inputs]** **`adapter/net/behavior/loadbalance.rs:1183-1187`** — `select_power_of_two` degenerates to deterministic pair when `len == 2`. With 2 endpoints there are no other choices to sample — both are read, the lower-load one is picked. That IS Power-of-Two-Choices for `len==2`. Callers wanting alternation should pick `LoadBalancingStrategy::RoundRobin` instead.

122. **[FIXED in `563ae8b0`]** **`adapter/net/channel/roster.rs:80-91`** — TOCTOU between `is_empty()` and `remove_if`; idempotent today but fragile.

123. **[FIXED in `1e3442eb`]** **`adapter/net/channel/guard.rs:300-302`** — `revoke()` clears verified cache but bloom stays dirty until manual `rebuild_bloom`; false-positive rate climbs.

124. **[FIXED in `82fc5232`]** **`adapter/net/identity/token.rs:411-422`** — `signed_payload()` allocates 95 bytes on every verify; hot-path waste.

125. **[FIXED — packed-atomic per-entry self-reset]** **`adapter/net/behavior/safety.rs:451-466`** — `per_source.clear()` at minute boundary drops just-incremented counters; brief per-source RPM cap exceedance. Fix: introduced `RateBucket` with packed `(window_floor: u32, count: u32)` in a single `AtomicU64`. Each per-source bucket self-resets on access via CAS — `try_acquire`'s `fetch_update` closure compares the stored `window_floor` against the current floor and atomically resets count to 1 (or refuses if at cap). Eliminates the global `clear()` race window: pre-fix, an in-flight `record_request` could fetch_add into the OLD AtomicU64 just as the global `clear()` removed it from the map, orphaning the increment on the next request's freshly-inserted bucket. `RateLimiter::maybe_reset` no longer touches `per_source`; it just runs a periodic `gc_per_source_stale` sweep that evicts entries older than 5 windows so the map doesn't grow unbounded under long-tail source churn. Two regression tests pin the contract: `rate_bucket_self_resets_on_window_rollover` (bucket-level invariant) and `per_source_cap_respected_under_contention_no_clear_race` (16 threads × 100 acquires never exceed cap of 50; pre-fix would intermittently over-commit at the minute boundary).

126. **[FIXED in `8132bbf7`]** **`adapter/net/mod.rs:495-522`** — handshake recv loop allocates `BytesMut::with_capacity(MAX_PACKET_SIZE)` per iteration; allocator pressure under stray traffic.

127. **[OBSOLETE — misread of architecture]** **`adapter/net/mod.rs:495-522`** — initiator handshake path lacks `HandshakePacer`-style rate limit. After tracing the code: `NetAdapter` is *per-peer* by design — `self.config.peer_addr` pins exactly one initiator target per adapter instance. There can't be a "flood of concurrent inits to the same peer" within a single adapter because there's only one initiator code path that runs once per `init()`. The retry-backoff fix in #53 (`e5a8dfbf`) already bounds per-init CPU at 5s × max_attempts. The audit's concern would only apply if the same process spawned many `NetAdapter` instances configured for the same peer — that's an application-layer concern (each adapter is a 1-1 session by design, mirroring the QUIC/Noise per-connection model). Responder-side `HandshakePacer` exists because the responder is N-to-1 (one bind socket fielding many sources); initiator side is 1-to-1 by construction.

128. **[OBSOLETE — pattern is sound; audit suggestion would regress]** **`adapter/net/router.rs:198-222`** — `notify_one` lost-wakeup window bounded to 1ms by polling fallback. After tracing the code: `tokio::sync::Notify::notify_one` stores a permit when no waiter exists; the next `notified().await` consumes it immediately. Enqueues that race the `dequeue()→None` transition are observed on the next loop iteration regardless of which side won the race. The 1ms polling fallback is documented as defensive paranoia, not load-bearing for correctness. The audit's suggested `Notify::notify_waiters` would actively regress the design — `notify_waiters` does NOT store a permit when no waiter exists, so it would re-introduce the genuine lost-wakeup case the permit-stash semantics close. Closed via in-code documentation pinning the rationale; no code change needed.

129. **[FIXED in `02756abf`]** **`adapter/net/behavior/proximity.rs:611`** — self-edge `insert_or_update_edge` per-pingwave is hot-path noise.

130. **[FIXED in `b420d5ef`]** **`netdb/db.rs:103-114`** — `close()` swallows the second adapter error on double failure.

131. **[FIXED in `7108c023`]** **`shard/mapper.rs:609`** — `overloaded_count` uses stale-metric placeholders for freshly-added shards.

132. **[FIXED in `9911bd29`]** **`shard/mapper.rs:681`** — `next_shard_id.store(first_id + count)` lacks `checked_add`.

133. **[FIXED in `9911bd29`]** **`shard/batch.rs:135`** — `f64 → usize` `as` cast without `clamp`.

134. **[FIXED in `52cb2efe`]** **`consumer/merge.rs:91`** — JSON cursor key `"00"` parses to `0`, colliding with shard 0.

135. **[FIXED in `af571e6f`]** **`adapter/jetstream.rs:107`** — missing `r` field stores `b"null"`, may surprise downstream consumers.

136. **[FIXED in `af830cc2`]** **`ffi/cortex.rs:443-450, 929-938`** — non-success paths don't write `*out_json`/`*out_len`; pre-zero is contract.

137. **[FIXED in `7c1ffad8`]** **`ffi/mod.rs:1349-1358`** — `CString::new` failure reported as `InvalidUtf8` but actual cause is interior NUL.

138. **[FIXED in `cc9ce87f`]** **`ffi/mod.rs:1192-1209`** — `NetEvent`/`NetReceipt` `#[repr(C)]` lacks cross-arch alignment pinning.

139. **[FIXED in `1fc92b6c`]** **`ffi/cortex.rs:419-453`** — JSON serialization runs while `TokioMutex` held; per-cursor latency stall.

140. **[FIXED in `6e830672`]** **`adapter/net/compute/migration_source.rs:202-213` (`take_buffered_events`)** — no phase guard; misuse-prone.

141. **[FIXED in `b1deaa0a`]** **`adapter/net/compute/migration_target.rs:362-368` (`abort`)** — `completed` index not cleared; minor leak.

142. **[FIXED in `126589dc`]** **`adapter/net/compute/orchestrator.rs:1052-1066`** — `MigrationError::TargetUnavailable(0)` invents a fake node id.

143. **[FIXED in `60723b2f`]** **`adapter/net/compute/orchestrator.rs:1280-1293` (`buffer_event`)** — returns `false` at Cutover; downstream caller may route to source post-handoff.

144. **[FIXED in `4dfff703`]** **`adapter/net/compute/migration.rs:139, 153`** — `started_at: u64` saturates on clock jump backward; use `Instant`.

145. **[FIXED in `6c3bf139`]** **`adapter/net/compute/fork_group.rs:225-231`** — `forks.pop()` and `coord.remove_last()` invariant unenforced; brittle.

146. **[FIXED in `687eaa57`]** **`adapter/net/redex/file.rs:474-497`** — `next_seq` rollback skipped if `disk` is `None` (currently safe path).

147. **[FIXED in `06f6bb63`]** **`timestamp.rs:163`** — `raw_to_nanos(raw)` uses `delta_as_nanos(0, raw)`; quanta semantics unspecified.

148. **[FIXED in `f35f21c2`]** **`shard/batch.rs:73`** — `let window_start = now - self.config.velocity_window;` panics on `Instant - Duration` underflow. On Windows `Instant` is QPC-relative to boot; with a velocity_window of seconds and a process started immediately after boot, this aborts the batch worker task. Use `checked_sub`.

149. **[FIXED in `bd9dbbcf`]** **`shard/mapper.rs:875-904` (`drain_specific`)** — Doesn't bump `last_scaling`. `scale_down` always writes `last_scaling = Some(Instant::now())` so a follow-up `scale_up` is gated by cooldown; `drain_specific` removes a shard from Active without touching `last_scaling`, so `drain_specific(id) → scale_up(N)` bypasses the cooldown floor.

150. **[FIXED in `bd5fb2dd`]** **`bus.rs:2060-2064`** — Drain worker `mem::replace` swaps `scratch` *before* the `sender.send(batch).await`. If the channel closes while the await is parked, the batch is silently dropped and the loop breaks — events never reach the adapter. Safe under documented shutdown ordering today; fragile to reorder.

151. **[FIXED in `fd17565c`]** **`adapter/net/linux.rs:138-139`** — `iov_base: packet.as_ptr() as *mut _` launders `*const u8` to `*mut` for `sendmmsg`. Sound (kernel reads only, `&[Bytes]` keeps storage alive) but the kind of pointer-provenance laundering Miri flags.

152. **[FIXED in `898978e6`]** **`adapter/net/pool.rs:198, 265`** — `(payload.len() - 16) as u16` truncation is safe under current `MAX_PAYLOAD_SIZE = 8112`, but unchecked. If anyone ever raises the cap past `u16::MAX + 16` without revisiting these lines, the wire `payload_len` silently truncates and the receiver mis-frames. Add `debug_assert!(payload.len() - 16 <= u16::MAX as usize)`.

153. **[FIXED in `2b2684bf`]** **`adapter/net/session.rs:644`** — `verify_and_touch_heartbeat` doesn't pre-check `parsed.payload.len() == TAG_SIZE` before AEAD decrypt. AEAD catches the mismatch, but a cheap length check shortcuts cleartext-flood probes before they touch the cipher.

154. **[FIXED in `79c7a980`]** **`shard/mod.rs:456-462`** — Static-mode `select_shard_by_hash` uses raw modulo while dynamic-mode (`mapper.rs:549`) was converted to Lemire's unbiased `(hash * len) >> 64`. Same bias, same fix should apply to both paths.

155. **[FIXED in `ad16ffae`]** **`shard/batch.rs:142`** — `current_batch_size * 3 + target` overflow. `BatchConfig::validate` doesn't bound `max_size` from above; debug build panics, release wraps. Default `max_size = 10_000` is fine; latent under hostile config. Add `max_size <= 1_000_000` to validate.

156. **[FIXED in `71bf549f`]** **`event.rs:148, 255, 471`** — `RawEvent::from_value` / `InternalEvent::from_value` / `StoredEvent::from_value` use `serde_json::to_vec(...).expect("Value serialization is infallible")`. Holds for `serde_json::Value` today, but the panic crosses into bus-level paths where the caller has reasonable expectations of fallibility. Defense-in-depth: return `Result`.

157. **[FIXED in `bc83f8da`]** **`adapter/net/router.rs:486-522`** — `RouterError::TtlExpired` recheck after `forward()` calls both `record_in` and `record_drop`, double-counting per-stream stats. Fix: skip `record_in` until after the post-decrement TTL check, or `record_drop` only.

158. **[FIXED in `4f7e5008`]** **`adapter/net/transport.rs:341-353`** — `BatchedPacketReceiver` thread spins at 1 ms intervals on persistent socket errors (bad fd, permission revoke). No exponential backoff, no break on hard errors. Add `EBADF` / `ENOTSOCK` early return.

159. **[FIXED in `091896a1`]** **`adapter/net/crypto.rs:441-453` (`ReplayWindow::commit`)** — A first authenticated packet with `received == u64::MAX` saturates `rx_counter` to `u64::MAX` (after zeroing the bitmap) and rejects every subsequent counter. Permanent receive-path poisoning from a single hostile authenticated packet. Reject `received == u64::MAX` at `is_valid` instead.

160. **[FIXED in `c9ac05ea`]** **`adapter/net/cortex/watermark.rs:100-101` (`WatermarkingFold`)** — A peer publishing `seq_or_ts == u64::MAX` saturates our `app_seq` to `u64::MAX`; the next legitimate ingest's `fetch_add(1)` either panics in debug or wraps to 0 in release, breaking per-origin monotonicity. Cap inputs at `u64::MAX - 1`.

161. **[FIXED in `13a84019`]** **`adapter/net/continuity/chain.rs:343-348` (`assess_continuity`)** — `(0, Some(non-empty payload))` accepted as genesis-shaped without rejection; downstream parent-hash is computed against junk and the chain is reported `Forked` rather than rejected with a typed error. Add `if sequence == 0 && payload != &[] { return Unverifiable }`.

162. **[FIXED in `8534345a`]** **`adapter/net/contested/correlation.rs:235-244`** — Capability-index parent walk loops forever if `SubnetId::parent` ever returns a non-self, non-global subnet that loops back. Defensive depth cap (4 iterations matches the 4-level hierarchy).

163. **[FIXED in `2b93fbfe`]** **`ffi/mesh.rs:2447-2449`** — `g.fp16_tflops_x10.map(|tf| tf as f32 / 10.0)` is lossy for `u32` values ≥ 2²⁴; the neighboring `tops_x10` already adopted `saturating_u16_cap` for the same reason. Apply matching saturation here.

164. **[FIXED in `bfa16d70`]** **`ffi/mesh.rs:2279-2290` (`parse_modality_cap`)** — Unknown modality strings silently fall back to `Modality::Text`; used for both capability announcements and capability filters. A typo in `require_modalities` returns wrong nodes with no error. Switch to `Option<Modality>` and surface `NET_ERR_CHANNEL` on unknown.

165. **[FIXED in `f13a9e4f`]** **`adapter/dedup_state.rs:169`** — Startup nonce is `xxh3_64` of a `(pid, tid, ns, stack_addr_as_usize, ...)` mix. Non-cryptographic; on 32-bit targets the `as usize` capture of stack address narrows entropy. Two co-located pods restarting from the same checkpoint at the same wall-clock instant carry tighter margins than the comment suggests. Use a 128-bit hash or a /dev/urandom seed mixed in.

166. **[FIXED in `e2994f17`]** **`adapter/redis_dedup.rs:178-208`** — `RedisStreamDedup::new()` defaults to capacity 4096 with FIFO eviction by insertion order. Module docs describe the use case as "a few thousand ids, ~minutes of in-flight at moderate throughput." At 10 K events/sec the 4096-entry window is ~0.4 s — two orders of magnitude too small for the documented usage. Either bump the default to ~600 K or change the doc to demand explicit sizing at construction.

167. **[FIXED in `5a24cd1b`]** **`event.rs:496-513` (`StoredEvent::Serialize`)** — Re-encodes `self.raw` through `serde_json::Value`. Round-trip discards original whitespace, normalizes number formatting (`1.0` → `1`), and discards key ordering (serde_json's default `Map<String, Value>` is `BTreeMap` unless `preserve_order` is on). Any downstream that hashes or signs the serialized form and expects byte-equality with the original raw bytes silently fails verification. Fix: serialize the raw bytes as a `&serde_json::value::RawValue`.

168. **[FIXED in `098fd036`]** **`timestamp.rs:103-145` (`TimestampGenerator::next`)** — `now` is read once outside the CAS loop; on contention, retries reuse the stale `now` and the returned timestamp is `last + 1`, drifting arbitrarily far behind real time. Re-read `raw` inside the loop on CAS failure (cheap, ~1 ns) to keep the wall-clock contract.

169. **[FIXED in `46e15810`]** **`consumer/merge.rs:49-60` (`compare_stream_ids`)** — Mixed-format fallback to lex compare wedges the cursor across backend migrations. After a JetStream → Redis switch, a new `"1700-0"` (Redis) compared with an old `"42"` (numeric) falls to lex: `"1700-0" < "42"` (because `'1' < '4'`), so `update_from_events` refuses to advance the cursor. Silent stall requiring manual cursor reset. Fix: persist the id format alongside the cursor and refuse migration without explicit reset.

170. **[FIXED in `b2348706`]** **`consumer/merge.rs:449-457` (`PollMerger`)** — Per-shard adapter errors are silently swallowed. When a shard's adapter call errors, the merger logs at `warn` and continues. The consumer sees a normal `ConsumeResponse` with events from the surviving shards; **no field on `ConsumeResponse` reports failed shards**, in contrast to `stalled_shards` which IS surfaced. Add `failed_shards: Vec<u16>` to `ConsumeResponse`.

171. **[FIXED in `9d4d8c3f`]** **`adapter/redis.rs:458-466` (`poll_shard` XRANGE)** — No `command_timeout` wrapper. `on_batch` and `is_healthy` both wrap their pipelines in `tokio::time::timeout(self.config.command_timeout, ...)`; `poll_shard`'s XRANGE relies entirely on the `ConnectionManager`'s implicit timeout. On a slow or wedged Redis node, `poll_shard` can block indefinitely while the rest of the adapter is honoring the timeout contract. Wrap in the same timeout shell.

## Audit notes

- Three subsystems are notably well-defended: `crypto.rs` (nonce/replay/HKDF/AEAD all sound), `pool.rs` (TX nonce coordination correct), and most `FailureDetector`/`CircuitBreaker` paths.
- `mesh.rs` (~7953 lines) was not fully read — only spot-checks. Worth a dedicated follow-up.
- The codebase has aggressive regression tests (`test_regression_*`) that pin many historical bugs; the findings above are not covered by those.
- **Triage priority (remaining):** Critical bucket cleared. Top High remaining: `compute/registry.rs::replace` quiescence (#13), cortex/mesh FFI handle-lifetime gaps (#23, #24, #25), then watermark/snapshot durability (#6, #7, #8). Items marked `[FIXED]` above are landed on `bugfixes-9`.
