Skip to content

fix(save,sim): boundary hardening + V13 invariant fixes (closes #99 #101 #102 #103 #104 #105 #106 #107 #108 #109 #110)#111

Merged
LightAxe merged 1 commit into
mainfrom
fix/save-and-sim-hardening-bundle
May 6, 2026
Merged

fix(save,sim): boundary hardening + V13 invariant fixes (closes #99 #101 #102 #103 #104 #105 #106 #107 #108 #109 #110)#111
LightAxe merged 1 commit into
mainfrom
fix/save-and-sim-hardening-bundle

Conversation

@LightAxe
Copy link
Copy Markdown
Owner

@LightAxe LightAxe commented May 6, 2026

Closes #99 #101 #102 #103 #104 #105 #106 #107 #108 #109 #110.

Summary

Two clusters of related hardening fixes that share an entry-point validation theme. 11 issues land together because they all live at one of two boundaries (parseSaveFile/deserializeWorldState, or sim-version-gated invariant restoration) and the test surface is uniform across each cluster.

Cluster A — save-file boundary hardening (8 issues)

All in src/platform/save.ts. Validators run at the parseSaveFile / deserializeWorldState boundaries. Throws route through bootFromSave's catch → bootFresh (consistent with existing #59 / #65 / #66 hardening).

Issue Field Failure mode
#99 grid dims + map cardinality memory-DoS via huge typed-array allocation
#101 chamber.{width,height,posX,posY,foodStored} renderer hang via chamber-flow.ts seed loop
#102 pendingChamber.{width,height,anchor*} renderer hang via tick.ts:382 revert loop
#103 non-array inputLog permanent soft-brick on Continue (TypeError outside try/catch)
#104 s.tick scalar unbounded memory growth via tick++ string concat
#105 null inputLog entry one bad command → entire save deleted
#109 foodPiles array sustained per-frame DoS (scanned every render + tick)
#110 envelope seed silent coerce-to-0 breaks replay-truth contract

44 new tests in save.test.ts cover each validator with a tampered shape and verify legitimate scenarios still round-trip cleanly.

Cluster B — sim-invariant fixes (3 issues, V13 bump)

LATEST_SIM_VERSION bumped 12 → 13 (SIM_VERSION_V13_INVARIANT_FIXES). All three fixes gated so pre-V13 saves replay byte-identically.

Issue Bug Fix
#106 ascent uses colonyId not currentGridColonyId swap + post-ascent writeback to restore invariant; plus skipAscent for Fighting invaders in foreign grids (mirrors descent's isFightingForeigner — without it V13 produced a descent→ascent ping-pong)
#107 killAnt doesn't clear carry slots atomic bidirectional cleanup before alive=0
#108 occupancy zone-mask gap mirror tile-key.ts:56 mask: gridByte = zone===Underground ? raw : 0

8 new tests cover both pre-V13 and V13 paths, including a regression test for the descent→ascent ping-pong bug uncovered during implementation.

Cross-references

Internal review

One fresh-context review pass (no prior session context) returned "Clean — ship it" with one P2 (test gap on the new skipAscent guard). Addressed by adding a regression test that asserts an invader Fighter at tileX matching the enemy entrance does NOT ascend.

Test plan

  • Typecheck clean
  • 1899 unit tests pass (52 new in this bundle)
  • One pre-existing flake on the AI 6000-tick determinism integration test under parallel load — passes in 15s when run isolated
  • UAT: load a fresh game, exercise save/reload round-trip
  • UAT: any combat scenario (V13 changes affect death-cleanup, but no behavioral changes in normal play)
  • UAT: invasion scenario — player Fighters on enemy entrance should descend and stay underground (regression for the ping-pong bug uncovered mid-implementation)

Sim-version gating

Pre-V13 saves replay byte-identically. The V13 changes are gated on world.simVersion >= SIM_VERSION_V13_INVARIANT_FIXES; legacy saves loaded into V13 builds keep V12 behavior (sticky-on-load contract, same as #95/#96 and earlier bumps).

 #102 #103 #104 #105 #106 #107 #108 #109 #110)

Two clusters of fixes that share an entry-point hardening theme.

## Cluster A — save-file boundary hardening (8 issues)

All in src/platform/save.ts. Validators run at parseSaveFile and
deserializeWorldState boundaries; throws route through bootFromSave's
catch → bootFresh, consistent with existing #59 / #65 / #66 hardening.

  • #99  — grid-shape validators (Surface / Underground / Pheromone),
           map cardinality caps (MAX_COLONIES_LOAD = 16, etc), and
           non-numeric / __-prefixed key rejection.
  • #101 — chamber-record validator: chamberType enum-bounded,
           posX/posY in-grid (FP coords), foodStored ≤ FOOD_CHAMBER_CAPACITY,
           width/height MUST equal CHAMBER_DIMENSIONS for the type
           (footprints are PRD-fixed, not player-set).
  • #102 — pendingChamber validator: same canonical-dims rule as #101
           plus footprint-fits-grid check. Closes the CancelDigMark
           revert-loop DoS vector.
  • #103 — non-array inputLog normalized to [] in parseSaveFile so
           game-scene's for-of loop never receives a non-iterable.
  • #104 — snapshot.tick + rngState integer validation; pre-fix a
           string tick caused unbounded memory growth via tick++ string
           concat.
  • #105 — null/non-object guard at top of migrateInputLogCommand;
           pre-fix one bad inputLog entry destroyed the whole save via
           loadSave's swallowing try/catch → deleteSave.
  • #109 — foodPiles array validator: length cap (FOOD_PILE_COUNT × 4),
           per-pile validation, foodPileId + tile uniqueness checks.
  • #110 — envelope seed int32 validation; pre-fix a malformed seed
           silently coerced to 0 on next autosave, breaking the
           (seed, inputLog) → snapshot replay-truth contract.

44 new tests in save.test.ts cover each validator with a tampered shape
and verify legitimate scenarios still round-trip cleanly.

## Cluster B — sim-invariant fixes (3 issues, V13 bump)

LATEST_SIM_VERSION bumped 12 → 13 (SIM_VERSION_V13_INVARIANT_FIXES).
All three fixes gated so pre-V13 saves replay byte-identically.

  • #106 — Underground→Surface ascent uses currentGridColonyId (not
           colonyId) for the entrance lookup, and writes back colonyId
           on success to restore the "Surface ⇒ currentGridColonyId
           === colonyId" invariant. Pre-V13 invader Fighters could warp
           home through coincidental tileX alignment with own-colony
           entrances. Bonus skipAscent guard: Fighting invaders in
           foreign grids do NOT ascend (mirrors descent's
           isFightingForeigner logic) — without it, V13 produced a
           descent→ascent ping-pong loop through the enemy entrance.
  • #107 — killAnt atomically clears bidirectional carry pointers
           (carryingBroodId, carriedBy) before zeroing alive. Pre-V13
           a nurse killed mid-Feeding left the brood with stale
           carriedBy pointing at a dead ant; if the nurse died on a
           Marked or Solid tile, the brood was unreclaimable.
  • #108 — resolveSameColonyOccupancy zero-masks the gridColonyId
           portion of the per-tile key when zone === Surface, mirroring
           combat's tile-key encoding. Same-colony surface ants with
           diverging currentGridColonyId now bucket together; one
           shifts. Closes the surface-stacking vector that #106's
           pre-fix divergence created.

8 new tests in combat.test.ts and ant-system.test.ts cover both pre-V13
and V13 paths, including a regression test for the descent→ascent
ping-pong bug uncovered during implementation.

## Test summary

  • 1899 total tests pass
  • 1 flake on the AI 6000-tick determinism integration test
    under parallel load — passes in 15s when run isolated
    (pre-existing pattern unrelated to this change)
@LightAxe
Copy link
Copy Markdown
Owner Author

LightAxe commented May 6, 2026

@codex review (combined save-file boundary hardening + V13 sim-invariant fixes — 11 issues, ~1100 lines incl. tests)

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Save-file parsing: unbounded grid dimensions / map cardinality enable memory-exhaustion DoS at load

1 participant