28 KiB
General review — wave 8 / pre-release (whole workspace)
- Reviewer: opencode (glm-5.3-flash)
- Scope: a general review of the full workspace as it stands at
wave-8 completion (2026-10-11, post
review-wave-8, pre-publish) — the same lens family as reviews 001/002/003 (correctness, security, code smells, classic dev issues), applied to all four published crates plus the contract suite and the fuzz sidecar, rather than a single wave's delta. Numbering note: reviews 001 (waves 1–2), 002 (wave 4), and 003 (wave 5) already occupy the first three slots, so this record is 004 by the directory's sequence. - Input: four parallel deep-read agents (one per crate: core; sqlite engine + substrate; postgres; mem) plus a cross-cutting sweep (panics/unsafe/logging/security greps, metadata, README / CHANGELOG spot-checks, fuzz/shared), followed by session-level verification of every finding that drove a change — each fixed item below was re-verified by direct read and, where behavioral, by a live run.
- Gates at review time (all run this session, after the fixes):
cargo build;cargo test --workspaceserver-less (green; core 27, sqlite 194, mem 102, pg lib SKIP-posture, suite 25 on the sqlite+mem columns); the full pg column live vs the harness server (pglo-poc: 127 lib + 25 suite + 9 schema, all green — the run that re-verified the pg fixes below through the live server);cargo clippy --all-targets -- -D warnings;cargo fmt --check; the fuzz corpus replay (6/6); the wasm32 check (cargo check --target wasm32-unknown-unknown -p alkstore-mem); the doc gate (RUSTDOCFLAGS="-D warnings" cargo doc --no-deps, zero warnings). Harness env for the record (review 003's op note):ALKSTORE_PG_HOST=localhost ALKSTORE_PG_PORT=15432 ALKSTORE_PG_USER=postgres ALKSTORE_PG_PASSWORD=poc ALKSTORE_PG_DB=blobs.
Findings fixed inline
| # | Severity | Where | Finding | Fix |
|---|---|---|---|---|
| 1 | HIGH | alkstore-sqlite/src/scheduler.rs |
A failed tick COMMIT/ROLLBACK left the writer slot wedged with a stuck-open transaction. The tick ran inside with_writer, which releases the connection on both arms; SQLite's documented behavior is that a failed COMMIT leaves the transaction active — so the released connection carries a live BEGIN IMMEDIATE forever: every later auto-commit op silently executes inside an uncommitted transaction (writes vanish at close) and every later BEGIN IMMEDIATE errors. Every other commit site has the drop-+-reopen-replenish arm (the tx handle's commit, ConnLease, the drop paths — pinned by sqlite-commit-error-arm tests); the tick was the one site missing it. Also, the tick's error arm swallowed rollback failure (let _ = ROLLBACK). |
The tick takes the connection by value now and returns a (result, conn, poisoned) disposition: COMMIT failure or ROLLBACK failure marks the connection poisoned — dropped, not released, slot replenished via a fresh bootstrapped open (the established arm's posture). The cfg(test) commit-fault seam reaches the tick (the store's flag is threaded into run_schedules), and a new test pins the property: failed_tick_commit_replenishes_the_writer_slot (store/scheduler_tests.rs) injects a SQLITE_FULL-shaped COMMIT failure and asserts the error chain, slot grantability, and full store usability after. |
| 2 | MED (contract-observable) | alkstore-postgres/src/tx.rs, alkstore-mem/src/tx.rs |
The wave-8 gate's pg negative-seed fix missed the tx seam. The gate fixed pg's auto-commit save (raw seed, CHECK dropped) against ADR-023 §2's 2026-10-11 annotation ("a negative is stored as the checkpoint") and pinned save_offset's totality through the direct form — but save_offset_tx still clamped (GREATEST($3, 0)), so the same consumer's first save landed −5 through StreamHandle::save_offset and 0 through TxHandle::save_offset_tx depending on entry point, with the tx path's doc still citing the pre-annotation clamp rationale. The suite's new legs did not pin the tx form, which is why the gate's audit read passed. Mem had the same family defect in-tx: merged_get_offset reported max(staged, committed) against a phantom zero for an absent committed checkpoint, so a staged −5 seed read 0 in-tx but committed as −5 (save_offset_row seeds raw) — the in-tx read contradicted the merge outcome; SQLite (uncommitted in-tx SQL) was correct. |
Both fixed: pg save_offset_tx stores $3::bigint raw (comment updated to the annotated posture); mem's merged read now distinguishes an absent committed row (new has_offset_row probe) — a staged seed reads raw in-tx, an existing checkpoint rides the monotone max as before. A new suite leg pins the tx form end-to-end (staged −5 → raw in-tx read-back → stored −5 at commit → anchored reads resume forward) in in_tx_reads_see_own_writes, stamped with the ADR-023 §2 (annotated 2026-10-11) accumulation; green on all three columns against the live pg server. core-contract.md's numeric-domain verification row names the tx-form leg; core-contract's save_offset_tx prose was already uniform ("monotone upsert" — the divergence was pg/mem implementation, not contract text). |
| 3 | MED | alkstore-postgres/src/forwarder.rs, src/notify.rs, src/stream.rs |
A failed register leaked a phantom refcount. register increments the channel registry first, then faults on three paths (mid-reconnect, forwarder-gone, ack dropped). listen() (notify.rs) propagated the error without unregistering — and since no receiver was created, no future Drop would ever decrement: the last-subscriber UNLISTEN rule could never fire and the reconnect re-issue kept LISTENing a channel nobody consumes. The subscribe-side failure arm two lines below already unregisters ("the acked LISTEN must not leak") — the register-side arm was the miss. spawn_bridge (stream.rs) had the same shape. |
Both call sites now unregister on the register error before returning it (mirroring the existing subscribe-failure arm), and register's doc is corrected: the failure disposition is the call site's (no receiver exists to drop the reference), replacing the old "the registry entry stays either way" claim that codified the leak. |
| 4 | MED | alkstore-postgres/src/forwarder.rs |
Nine non-test expect()s in the fanout/registry/config locks — a panic while holding any of them poisoned the mutex and converted every later register/subscribe/shutdown into a panic in unrelated tasks, despite the workspace's no-expect-outside-tests rule (and the crate's own seam docs). |
Converted to the mem crate's established poison-recovery idiom (unwrap_or_else(std::sync::PoisonError::into_inner)) — degraded-but-alive continuation matching alkstore-mem/src/state.rs's locked-discipline posture. The one remaining pg expect (stream.rs wake-runtime build, a OnceLock fixed-config invariant) is recorded, not converted: its failure is a bug, not a condition, and converting it would change the function's signature for no path. |
| 5 | MED | alkstore/src/payload.rs (+ fuzz/shared/src/payload_codec.rs, engine enqueue paths implicitly) |
Deeply-nested payload Values aborted the process at encode time. The doc claimed "Unreachable for serde_json::Value in practice", but serialization's recursion is unguarded: a caller-supplied Value nested ~10⁵ levels overflows the stack inside serde_json::to_vec — an abort, not an Err — and encode_payload runs at every enqueue/publish/notify site on every engine, so one bad Value from one consumer kills the runtime. Worse for the pre-change state: the fuzz invariant explicitly pinned that a value nested past the decode recursion limit encodes successfully and only "decodes loud" later — i.e. storing bytes no reader could ever read back, silent data loss at read time. A scratch probe pinned serde_json's decode boundary empirically: 127 nested containers read back, the 128th nested open fails (RECURSION_LIMIT 128 counts the failing open, not the last success). |
encode_payload now measures nesting iteratively (an explicit-stack walk — a recursive checker would itself overflow on exactly the deep inputs) and rejects anything deeper than 127 containers with typed Error::Codec: exact encode/decode symmetry (everything decodable encodes; nothing undecodable stores) and no reachable abort path. Applied pre-publish, so the class-1 window covers it as a failure-set addition within ADR-023 §1's typed-failure pin — flagged here for the record's honesty. The fuzz invariant's encode expectations were corrected (past-limit encode fails typed; a boundary round-trip leg at 127 containers added), and two core tests pin the boundary + no phantom behavior (value_type_tests.rs). payload_as's decode-side behavior is unchanged. |
| 6 | MED (platform-conditional) | alkstore-mem/src/scheduler.rs, alkstore-sqlite/src/scheduler.rs, alkstore-postgres/src/scheduler.rs |
Instant + Duration panic / unchecked overflow on far-future boundaries. The three leader loops' sleep deadlines computed tokio Instant + Duration::from_secs(soonest − now) with soonest a consumer-reachable i64 (a legal @every 9999999999999999s interval); on macOS (u64-nano Instant, ~584 y) or Windows (~29 k y) the addition panics for waits past the platform's representable span (the mem agent verified the panic class; Linux tolerates it). The sleep is a wake-latency optimization — correctness never rides it. Same family: the sqlite tick's claim stamping did an unchecked now + s (expires_s consumer i64) — debug panic / release wrap, in the one spot whose neighbors all use saturating_add. |
All three sleeps cap one slice at MAX_SLEEP_S (30 days): a far-future boundary sleeps, ticks (a no-op), and sleeps again — the tick recomputes the soonest every iteration and the renewals continue, so a 30-day-cap is behavior-preserving on any platform. The sqlite claim stamp is now.saturating_add(s) matching resolution.rs's own posture. |
| 7 | LOW | alkstore-mem/src/wakes.rs |
Stale module-level #![allow(dead_code)] whose own comment said "until the mechanism tasks wire it" — the mechanisms wired the bus in wave 6; the suppression was masking the module's real dead-code signal. |
Removed (with the now-covered #[allow(dead_code)] on new(), which the Default impl exercises); compiles clean — the surface is genuinely live. |
| 8 | LOW (test hygiene, core) | alkstore/src/value_type_tests.rs |
Two test smells: payload_as_undecodable_target_yields_codec exercised a map_err the test itself wrote instead of the function under test (it proved nothing); two serialization asserts compared against to_vec(...).unwrap_or_default() — a failure in the reference encode would diff against an empty Vec and misdirect. |
The undecodable-bytes case now goes through Job::payload_as on a real undecodable job; the reference encodes panic with their cause. |
| 9 | LOW (doc accuracy) | several | Doc-vs-code mismatches found by the read, all public or contract text: core's Error::Closed doc scoped the variant to wake receivers while the stream receiver's try_recv produces it too; validation.rs's shared-kind bullet was self-contradictory about the whitespace rule and NameKind::Local omitted it; alkstore-sqlite's lib header promised "durability knobs … on SqliteOpts" although SqliteOpts carries only the watcher cadence and reader-pool size (synchronous/busy-timeout are bootstrapped constants); pg lock.rs claimed the acquire sequence "auto-committed as a unit" though the statements commit individually (correctness lives in the PK constraint, which the doc itself cited second); mem's tx module doc said the commit-merge is "one guarded operation" while the merge is two sequential guarded sections (queues → streams) on a non-awaiting path, and mem's stream doc said publishes fire "one wake per event" where the tx commit path coalesces per stream. |
All reworded to match the code. The pg lock.rs and mem merge statements now state the real mechanism honestly; the sqlite lib doc now matches SqliteOpts's actual fields; the Error::Closed doc covers both receiver families. |
Findings recorded, not fixed (follow-up material)
These are real but either need their own design pass, touch lineage or distributed machinery whose repair exceeds a review fix, or are conscious postures to record. None blocks the release; each names its shape so a future session can decompose without re-deriving.
(Dispositions, 2026-10-11: all sixteen items decomposed into the
review-004-fixes fix batch — the sixteen tasks/* files below each
carry the per-item decisions of record and verification — and the
batch's gate audited them all landed. The record reads closed.)
- sqlite seam panic-safety (defense-in-depth) —
with_writer/with_reader/blocking_read(alkstore-sqlite/src/seam.rs,substrate/schema.rs) release their slot/pool connection after the closure returns only. A panic inside the closure skips the release: the single writer slot wedges forever (Writer::acquirewaits on a condvar with no timeout), and an abandoned reader connection permanently shrinks the pool (Readers.outstandingnever decrements on abandonment). The tx seam already owns this class (ConnLease); the auto-commit seams don't. No reachable panic path today (the library is panic-free by the standing gates, and these closures take no consumer code), so this is armor, not exposure — but the fix needs abandonment plumbing on bothWriter(drop + path-based replenish) andReaders(an outstanding decrement), which is a deliberate small design task, not an inline patch. Disposition: landed viasqlite-fix-seam-panic-safety(RAIIWriterLease/ReaderLeaseguards, drop-not-release, replenish/ abandon dispositions; injected-panic tests instore/seam_tests.rs). - sqlite post-watcher-death subscribe hang — after the watcher
thread exits (db-file replacement),
subscribe()still enrolls new senders with no poller alive (substrate/watcher.rs:391-396): alisten()or streamsubscribe()on the dead store parks forever, contradicting engine-sqlite.md's "on watcher death every subscriber closes" posture (enforced only for pre-death subscribers). Needs a dead-store/dead-watcher sign that new subscription paths consult — a state-machine change acrossstore.rs/notify.rs/stream.rs. Disposition: landed viasqlite-fix-watcher-death-subscribearm (a) — the dead sign set before the fatal exit, falliblesubscribefailing closed under the senders lock. - sqlite watcher initial-stat failure self-kill — an
identity-
statfailure at watcher start seeds(0, 0)(watcher.rs:164-170); the first successful later stat mismatches it and drives the death path, spuriously closing every subscriber on recovery from a transient stat hiccup. Rare, but the fix (an identity-unknown state instead of a false identity) is another watcher-state change — bundle with item 2 if either is taken up. Disposition: bundled with item 2 insqlite-fix-watcher-death- subscribearm (b) — theOption<(u64, u64)>identity-unknown baseline; the first successful stat captures without driving death. - pg forwarder register/unregister TOCTOU — the registry refcount
and the LISTEN/UNLISTEN command emission are not one critical
section (
forwarder.rs:131-176vs438-482): interleaved register/unregister across tasks can leave a live refcount with the server-side LISTEN cleared (both queued commands share a generation, so no stale check saves it) until the next reconnect re-issue. The fix is a decide-and-queue restructure inside the forwarder — forwarder surgery that should ride its own session with the existing test-seam vocabulary. Disposition: landed viapg-fix-forwarder-hardening(a) —ChannelSet::enqueue_register/enqueue_unregistermake the refcount decision and the command enqueue one critical section under the registry guard; replay-proven both ways. - pg server-side BIGINT overflow on consumer stamps — the claim,
heartbeat, retry-reschedule, and retention-sweep statements compute
server-side sums over consumer-supplied i64 values
(
queue.rs:386/912/1000/714,lock.rs:91): one row stamped withvisibility_timeout_s = i64::MAX(or retentioni64::MIN) makes every claim/heartbeat UPDATE fail with "bigint out of range", and the failure is sticky per row. The client-size resolution is saturated (resolution.rs); the row-stamp arithmetic is not. Fix shape: clamp/validate at the enqueue seam (orLEAST/GREATESTguard the server expressions) — but the which side question is contract-adjacent (ADR-023 §2's totality wording) and deserves its own decision, not an inline patch. Disposition:pg-decide-stamp-arithmetic-sidedecided the server expression owns the guard (result-domain saturation, no input clamp);pg-fix-bigint-stamp-overflowlanded it assaturating_stamp_exprat the six sites, pinned live. - pg forwarder hang-shaped arms — two narrow ones: a failed
generation-verify/LISTEN batch is treated as connection death and
parks on
poll_task.awaitforever even though the session may be alive (forwarder.rs:698-702/803-808); andclose()/Dropcan leak the listener session past shutdown when the serve loop is awaiting a command on a black-holed connection (therun_commandawait sits outside the shutdownselect,forwarder.rs:781, no timeout on this connection). Both want the same treatment family — timeouts/bounded waits on the listener path — bundle into the forwarder-hardening session (item 4). Disposition: both bundled intopg-fix-forwarder-hardening(b/c) — thebounded_waitprimitive bounds every listener-path await (LISTENER_QUERY_TIMEOUT5 s /LISTEN_SETTLE_TIMEOUT2 s, shutdown-selected), the failed-verify settle aborts at the bound; the black-hole proxy harness test pins the close-time reclaim. - pg forwarder diagnosability — the reconnect arm's connect
failures are swallowed with
.ok()and the loop's exit discards the poll error (forwarder.rs:825/855-869): an operator sees nothing before wake loss. Related cross-cutting posture: the family has no log facade and 17 library-patheprintln!sites carry its diagnostics (verified secret-free — no DSN/password material ever printed). Alog/tracingadoption decision is a family-level call; recorded rather than made here. Disposition:decide-log-facade-adoptionadoptedtracing(ADR-025, engine crates only, user sign-off 2026-10-11; all 19 library-patheprintln!sites migrated, substrate sites riding delta D-41) andpg-fix-forwarder-diagnosabilitysurfaced the two arms (warnon failed reconnect connects with attempt/backoff context,erroron the poll-exit ending). - sqlite listener-bridge asymmetry —
listen()parks onespawn_blockingthread per subscription (notify.rs:98-104, tokio blocking-pool ceiling ~512) while the stream bridge does the identical job on a dedicatedstd::thread(stream.rs:368-379); past ~500 concurrent listeners the whole engine's seams queue behind parked bridges. Aligning the two bridges (std-thread shape) is a small contained change deferred alongside item 2's subscription-path work. Disposition: landed viasqlite-fix-listener-bridge-alignment— the listen bridge takes the stream bridge's named std-thread shape exactly (including the spawn-failure fail-closed arm). - sqlite run_schedules error flattening —
lease.renew(...).await.unwrap_or(false)maps any renew failure (including the closed-storeDatabaseerror) toLeadershipLost(scheduler.rs:205-211), misdirecting the documented respawn recipe at a dead store. Small, but the right arm set (closed vs lost vs storage failure) is a contract-adjacent error-classification change on a stable surface — record, don't rush. Disposition:sqlite-fix-scheduler-error-classificationlanded the loss-vs-failure arm set (Ok(false)staysLeadershipLost; a failed renewal propagates its opaque classification) with the dated ADR-009 §1 annotation; the pg twin discovered mid-batch landed viapg-fix-scheduler-error-classification, implementing to match. - mem growth postures (N-1 family) — two perf/posture notes to
carry into the mem spec's growth section when next touched: the
subscribe path drains the full backlog into the receiver's pending
deque before the first yield, and pending grows at publish rate for
a stalled consumer (
stream.rs:314-331— the documented "drains to the tail then parks" shape, but unbounded on the consumer side); the claim path is an O(total-live-rows) scan with full-row clones per claim (queue.rs:122-181) — blessed economics for the ephemeral tier, worth one sentence inengine-mem.mdif ever revisited. Disposition: landed viamem-doc-growth-outbox-notesarm (a) — the growth postures pinned inengine-mem.md's new growth section, anchored to the sites. - mem outbox outcome swallow —
outbox run_oncedrops the ack/ retry row-refs' booleans and returnsOk(true)even when delivery outlived the visibility window and nothing landed (outbox.rs:104-122). Inherent to the no-heartbeat outbox design (at-least-once reclaim is the recovery), but adebug_assert!or doc sentence pinning the "processed means delivered-or-reclaimable" semantic would prevent future confusion. Disposition: bundled intomem-doc-growth-outbox-notesarm (b) — the "processed means delivered-or-reclaimable" semantic pinned inengine-mem.md's outbox section; thedebug_assert!declined with the false-positive rationale recorded in the task. - core
offset()initial value —EventReceiver::offset()pins the post-yield value only; the pre-yield return (the seeded checkpoint vs 0) is unpinned in the contract text. A doc-only fix once the three engines' pre-yield values are confirmed equal (not verified this session; left as a named gap). Disposition: landed viacore-pin-offset-initial-value— the three engines verified equal (0 on every pre-yield arm, live on all three columns) and the doc-pin branch applied; pinned in core-contract.md, the trait doc, and the suite rowreceiver_offset_pre_yield_is_zero. - pg identifier truncation cross-channel collision —
LISTEN/UNLISTENinterpolate the channel as a quoted identifier (truncated at 63 bytes server-side) whilepg_notifycarries it as a parameter; two distinct names sharing a 63-byte prefix can silently collapse into one wake channel, with names unbounded by the validation contract (the no-cap "length-independence" verdict is deliberate). The honest cheap fix is a deployment-matrix/ docs note ("names longer than the PG identifier limit may collide"); deferred here because the matrix was just finalized by the gate — fold it into whichever doc pass runs next (or into the publish session's reading). Disposition: landed viadocs-pg-identifier-truncation-note— the note in deployment.md (dated) and engine-postgres.md, the mechanism sharpened (the server rejects ≥ 64-bytepg_notifychannels; the collision is on the registration side), the no-cap verdict untouched. - core error
Displaylog-forging —InvalidName/
ReservedName/InvalidSpecinterpolate raw consumer strings intoDisplay; control characters can forge log lines in naive consumers. Typical-idiom risk, low; recorded. Disposition: landed viacore-fix-display-log-forging— thedisplay_safeescape helper in the three variants'Displayarms (plusError::Codec, the one same-class neighbor); taxonomy untouched. - unsafe without
// SAFETYin the substrate — the eightunsafe { ctx.get_connection() }UDF blocks (substrate/ops.rs:147-236) carry no justification comments. The pattern is the standard rusqlite UDF shape and provably constrained, but AGENTS.md's own carve-out ("inline comments when a non-obvious safety constraint would otherwise be missed") says this is where they'd live. Not added this session: the substrate is lineage-port code under ADR-012 §3's diff-fidelity posture — comments must ride a PROVENANCE delta. Recorded for whenever the substrate is next touched. Disposition: landed viasqlite-substrate-safety-comments— all ten same-class sites (the eight +schema.rs's notify registration + the test-module probe) carry concrete-invariant// SAFETYcomments, registered as delta D-40. - Cosmetic metadata asymmetry — the three engines' dev-dep tokio
features disagree (
rt-multi-thread+macros+time/test-util/rt+macros); harmless (the contract suite carries the test runtime) but an easy-to-trip asymmetry when copying test code. Recorded, not normalized mid-release. Disposition: landed vianormalize-tokio-dev-deps— all three engines normalized tort-multi-thread + macros + time + test-util, dev-dep metadata only.
False positives caught on verification (for the record)
- mem
WakeBus::attachcheck-then-act race (reported as a finding): disproven by read — theclosedcheck happens under the registry guard, andclose()'sstore(true)is sequenced before its guardedclear(), so the guard's acquire/release supplies the happens-before edge; no interleaving leaks a subscription. No fix needed; noted here so a future audit doesn't re-derive it. - The "tx-path negative seed divergence" was real (fixed, item 2), but the analogous claim against SQLite's tx form was not — its in-tx SQL is already raw-seed-and-monotone; the divergence was pg's clamp and mem's phantom-zero merge read only.
What was checked and clean (summary)
- Security: SQL injection — every consumer-supplied string rides
bound parameters across all engines (the
format!-built SQL interpolates only engine-owned identifiers through the quoting helpers; savepoint/table keys are compile-time constants);env::varconfined to test harness plumbing (re-grepped); nofswrites outside tempdirs; no process spawning; no secret/DSN material in any of the 17 library-patheprintln!s. - Core crate: zero panics/unsafe/
unwrapoutside tests, its three-dependency posture intact,#[non_exhaustive]placement and the hidden-constructor seam exactly per ADR-017, object-safety and validation coverage as pinned. (Post-fix, pre-abort-armor caveat in item 5 above.) - Mem engine: wasm32 subset verified (
cargo checkclean, tokio sync+time only, nospawn_blocking/fs/thread); all deadline arithmetic saturating; queue state machine, trim semantics, three-way arithmetic, scheduler boundaries/catch-up/leadership re-read clean. - pg engine: injection-clean,
in_txpool accounting exact on every arm, forwarder reconnect invariants sound outside the recorded TOCTOU/hang arms,FOR UPDATE SKIP LOCKEDclaim semantics and scheduler serialization consistent with sqlite/mem. - Cross-cutting: no
todo!/unimplemented!/dbg!/.ok()anywhere; metadata coherent (versions, path-dep bounds, publish fields,exclude = ["fuzz"]); README/CHANGELOG claims spot-read true against code; fuzz/shared targets thin and invariant-correct.
Gates re-run after the fixes (this session)
cargo build, cargo test --workspace (green server-less), the full
live pg column (127 + 25 + 9 — including the new in_tx_reads_see_own_writes
negative-seed leg against the real server), cargo clippy --all-targets -- -D warnings, cargo fmt --check, the fuzz corpus
replay (6/6 — including the corrected encode/invariant), the wasm32
check, and the doc gate — all green.