LyraCore codebase audit: open findings

Audited 2026-09-03 at main f731664. Eight auditors, one per category, read-only. Only the fast test tier was run. Findings already fixed by the seven follow-up PRs (#407 to #413) are omitted from this report.

Scorecard

CategoryScoreVerdict at audit time
Architecture and boundaries7Two-tier design is real and build-enforced by tripwires. Flat 91-method store and stringly cross-tier errors.
Correctness and error handling7Trust boundaries checked with care. Weak spots in cross-database flows and error classification.
Testing and verifiability7About 3,500 behaviour-named tests through real Fakes. Durable module tests all ignored.
Documentation and onboarding7Clone-to-client works from docs alone. Half of docs/ is a lab notebook.
Code quality and simplicity6Typed decision cores. Essay comments, long functions, string errors.
Build, tooling, workflow6Every pin exact and self-explaining. Format and lint drift on main.
Performance and scalability6Movement path measured and well designed. Everything else fans out shard-wide.
Security5Coherent trust model. No rate limits, no session expiry, debug reducers ungated.
Overall (unweighted mean)6.5
Expected movement once the seven PRs merge: security 5 to 6, code quality 6 to 7, tooling 6 to 7, testing 7 to 8. The items below are what stands between the repo and the next point in each category.

Method

1. Security (5/10)

FindingWhereSeverityStatus
Unlimited SRP proof attempts per connection, no per-IP cap, no delay. Each attempt costs a server modexp. Online password guessing plus CPU burn.gateway/src/logon/mod.rs:112-133mediumExploitable now, documented in quickstart
Session expires_at is written but never read, and rows are never deleted. A stolen session key authenticates world sessions indefinitely.module/src/auth.rs:518-535, gateway/src/stdb/world_store.rs:1141mediumHardening gap
claim_operator is trust-on-first-use. After a publish -c, whoever reaches port 3000 first owns the realm. The dev publish ships 124 debug reducers, 76 in debug/mod.rs with one gate.module/src/auth.rs:171, module/src/debug/mod.rsmediumHardening gap, mitigated by loopback bind
Installer pipes rustup and SpacetimeDB installers to sh and fetches Binaryen with no checksum. SpacetimeDB systemd unit has no NoNewPrivileges or ProtectSystem. No gateway unit in the repo.install.sh:242,271,327, deploy/systemd/lowHardening gap
The SRP library compares proofs with !=, not constant time. Impractical over the network.wow_srp dependencylowUpstream

Next point: per-IP connection and proof-attempt limits on the logon port with a delay after failure. Enforce expires_at in the session lookup and reap session rows.

2. Performance and scalability (6/10)

Hot spotWhereScales withImpact
Combat events, auras, casts, impacts, chat and emotes enqueue one boxed job per viewer on the shard. The visibility gate runs inside the job on the writer thread. Work per event is O(Vshard), event rate grows with players in combat.gateway/src/stdb/world_view.rs:1615-1625, 1814-1895, 1915-1927O(V²)At 1000 players and 300 swings/s: about 300k closure allocations and writer wakeups per second, mostly discarded.
Each visible aura event scans the entire shard aura cache twice per viewer, plus once on the pump.gateway/src/stdb/subscriptions.rs:1633-1641, world_view.rs:677-690O(A × Vvisible)Dominates writer threads in buff-heavy raids with tens of thousands of aura rows.
Every 500 ms tick iterates all of game_world_entity and calls an indexed find per row, players included, in one transaction.module/src/creatures/tick/mod.rs:326-370Spawned creatures per shardA full continent import makes this the tick's floor cost, independent of player count.
panicked scans all of game_aura every firing, including non-sense ticks.module/src/creatures/cycle/ctx.rs:864-871O(A) per 500 ms
Two OS threads per session for its whole life, capped by the blocking pool. Thread count, not CPU, is the seat ceiling. Measured 477 seated of 600 offered.gateway/src/world/mod.rs:1737, 805Sessions
Each coordinator subscribes to about 51 SELECT * tables per shard. Every gateway holds every shard's full cache in RAM and receives every delta. Sharding spreads database writes but not gateway memory or pump work.gateway/src/stdb/connection.rs:563-770Shards × rows

Measured vs assumed. The movement path is measured: batch and motion stats, fan-out ratio, writer occupancy, the acceptance doc. Combat, aura and chat fan-out, module tick duration per creature count, and gateway RSS per shard cache are asserted only. No load test lives in the repo.

Next point: route combat, aura, cast and chat through viewers_of(cell) so the gate runs before the enqueue, and index the gateway aura cache by target. Replace the per-tick entity scan with a players-only index. Land an in-repo load test reporting per-tick module duration and per-session writer CPU at 500, 1000 and 2000 players.

3. Correctness and error handling (7/10)

DefectWhereTriggerSeverity
Every delayed Auto Shot damage log spawns an OS thread inside a subscription callback. The late sender clone keeps the writer channel alive past logout for the impact delay.gateway/src/stdb/subscriptions.rs:1157-1163Many huntersmedium
Item handler treats any error not containing "reducer transport disconnected" as a gameplay refusal. A 10 s reducer timeout is shown as an inventory failure while the module may still commit the move.gateway/src/world/handlers/item.rs:58-67Slow reducermedium-low. The auction handler got the typed fix in #411; item, social and trainer still match strings.
Viewer and shard registries use .read().unwrap() in callback paths, while the connection and session code recovers poison. One panic under the write guard breaks every later relay.gateway/src/stdb/world_view.rs:226, 235, 245-362Any panic under writelow-medium
p.xp += amount is unchecked. Level 60 players are paid for level 55+ mobs and next-level XP is 0, so XP accrues with no cap. No overflow-checks in any profile, so release wraps.module/src/xp.rs:349, 100-1104.29e9 XP at caplow
LYRACORE_MAX_SESSIONS=1,000 parses as unlimited and silently disables the login queue. The blocking-threads parser shows the safer posture.gateway/src/config.rs:748-750, 801-805Operator typolow

Weak patterns

Next point: apply the #411 shape (shared refusal enum, tag at the reducer edge, classify on the error variant in the gateway) to item, social, trainer and loot. Replace the per-hit thread with a timer queue on the writer. Cap XP at 60.

4. Architecture and boundaries (7/10)

FindingWhereWhy it matters
A gameplay Gate is duplicated in the Gateway. interaction_allowed re-implements the twelve-condition auction check from the module. It is not one of the documented cross-shard exceptions.gateway/src/world/handlers/auction.rs:562-587 vs module/src/auction.rs:259-283The two can drift; architecture.md §2.3 says gates live in the module.
WorldStore is wide and flat: 91 methods, 58 with default no-op bodies. Narrow per-family traits sit beside it, so handlers use two shapes. handle_* (writes to socket) and dispatch_*_action (returns a batch) coexist by admission.gateway/src/world/store.rs:15-1011, handlers/mod.rs:6-11Two ways to write a handler. The dispatch chain in world/mod.rs:1125-1215 is order-dependent pass-through.
Mega-files mix concerns. subscriptions.rs holds relay builders plus two unrelated coordinator impls (viewer registration at :3161, bot-invite relay at :3379). importer/src/main.rs embeds about 40 inline column-index modules at :82-518. auction.rs (2.7k production lines, 8 traits) has not been split into a directory like transfer/ and mail_escrow/.gateway/src/stdb/subscriptions.rs, importer/src/main.rs, module/src/auction.rs#413 shortened the longest functions but the files keep their mixed responsibilities.
209 source-text shape assertions across 59 files pin literal impl strings.e.g. module/src/mail_escrow.rs:1057, gateway/src/realm_core.rs:1771Enforces structure but couples tests to formatting.
lyracore-shared stragglers: values_mask.rs is gateway-only, region.rs (595 lines) is module-only and belongs to the retired region tier.crates/lyracore-shared/src/The crate's bar is "shared by more than one crate".

Next point: delete the Gateway auction gate or turn it into a Store read that returns the module's verdict. Collapse handlers onto the dispatch_*_action shape. Split subscriptions.rs and auction.rs into directories.

5. Code quality and simplicity (6/10)

Remaining after #412 and #413

MeasureValue
Issue references in gateway, importer and crates comments (module is now clean)~180
ALL-CAPS heading comments149
Comment line share: gateway / module / importer36% / 19% / 12%
Functions over 200 lines (now warned by clippy)43
Longest remaining: assemble (eventai), parse_args_from, aura_apply468 / 377 / 356
allow(clippy::too_many_arguments)68
Lines nested 7+ levels825
Result<_, String> fn signatures in module (116 are reducer edges)316

Findings

Next point: fix clippy in the three red crates and flip the CI job to blocking. Convert one feature per PR from string errors to a shared refusal enum. Replace the argument parser with the existing dependency or a table of flags.

6. Build, tooling and workflow (6/10)

FindingWhere
Format drift on main (61 files, 280 hunks) and clippy red in three crates. Both CI jobs are non-blocking until this is cleared.workspace
module/build.rs is a 2,033-line hand-written Rust tokenizer that regex-scans sources for macro markers and lints the Package API. syn is already in the tree.module/build.rs
Gateway Rust bindings (961 files) are regenerated by a raw spacetime generate line in danger-zones.md, not by a CLI command, unlike the TypeScript bindings.docs/danger-zones.md
Five importer shell scripts run without set -e, including the 1,010-line import-world.sh. Three shellcheck warnings across scripts/ and the launcher.importer/scripts/, scripts/movement-batch-acceptance.sh:38
16 crates at duplicate versions in the lock: thiserror 1+2, rand 0.8+0.9, getrandom 0.2+0.3+0.4, hashbrown ×3, syn 2+3, wow_world_base 0.2+0.3.Cargo.lock
Two TypeScript versions in datascripts (7.0.2 root, 6.0.2 in runtime-scripts). No rustfmt.toml or .editorconfig.datascripts/
lyracore-package-delta tests are not in the CI test job because danger-zones.md does not list the crate..github/workflows/rust.yml, docs/danger-zones.md

Next point: one PR that formats the tree and fixes clippy, then flip both jobs to blocking. Wrap bindings regeneration in the CLI. Give import-world.sh set -euo pipefail or split it.

7. Testing and verifiability (7/10)

AreaTestsNote
module/src (lib)~1,490creatures 358, spell 148, transfer 96, package_import 95, combat 69. seed/ has zero.
module/tests (durable)3937 ignored (need SpacetimeDB CLI and wasm). Wall-clock sleeps up to 1.3 s.
gateway/src/world602UnixStream sessions with real cipher. 9k-line file with an 11-trait in-memory store.
gateway/src/codec224 + 9 propertyGolden byte vectors, fixed-seed property tests.
importer3392 ignored (need classic DB dump). build_spell_rows has no coverage.
lyracore-package-delta / lyracore-shared287 / 148Green. Refusal-first integration tests.
datascripts (bun)46Only one file runs in CI.

Next point: a CI job with the SpacetimeDB CLI that runs the ignored durable suite, with sleeps replaced by reducer-observable polling. Split the 9k-line gateway test file per handler. Tests for seed/.

8. Documentation and onboarding (7/10)

FindingWhere
Half of docs/ is verification write-ups (9 reference docs, 9 probes and checks). #408 indexed them but they still sit beside the reference docs, and their filenames use glossary Avoid words (probe, harness).docs/
Rustdoc on public items: module 26%, shared 45%, gateway 76%. Module-level //! docs are near total.workspace
No gateway systemd unit, backup procedure, or front-door firewall doc, though danger-zones.md refers to one.deploy/systemd/, docs/danger-zones.md:243
publish_safety.rs and test_scan.rs still name the maintainer-local publish script. They treat it as optional so they do not lie, but the code path exists for a file that is never in the tree.module/src/publish_safety.rs, module/src/test_scan.rs
Glossary Avoid words in source identifiers: guard 602, reject 510, validat 281, tripwire 198.src

Next point: move the write-ups to docs/verification/ and rename per the glossary. Add the gateway unit and a backup note. Retire the publish-script code path or check the script in.

Suggested order for the next fan-out

  1. Format and clippy sweep, then blocking CI. Unlocks every later refactor. One PR, mechanical, large diff.
  2. Logon rate limits and session expiry. Closes the two remaining exploitable-now security findings. Small, gateway and auth.rs.
  3. Relay fan-out through the cell index. The one performance change that turns O(V²) into O(V). Needs a load test alongside it.
  4. Typed refusals for item, social, trainer, loot. Four PRs from the #411 template. Removes the last substring matching and the timeout misclassification.
  5. Delete the duplicated auction gate. Small, restores the tier rule.
  6. Durable module tests in CI. Larger. Needs the SpacetimeDB CLI on the runner and sleep removal.