fix: reject non-integer tile refs in GameMap.isValidRef (#4720)

`GameMap.isValidRef` only checked that a `TileRef` was in range — not
that it was an integer. Since the simulation runs on every client and
intents carry raw tile refs, a single ~60-byte malformed intent with a
fractional ref (e.g. `1.5`) passed validation in executions
(`ConstructionExecution`, `TransportShipExecution`,
`MoveWarshipExecution`) and could corrupt typed-array indexing / freeze
the game for the whole lobby.

Require `Number.isInteger(ref)` in `isValidRef`, matching what
`isValidCoord` already does for x/y. This also rejects `NaN` and
`±Infinity` explicitly.

Overhead is ~3 ns per call, and `isValidRef` is boundary validation (per
intent / per path request), not inner-loop code.

New `tests/core/game/GameMap.isValidRef.test.ts`:
- in-range integer refs accepted
- out-of-range refs rejected
- in-range fractional refs rejected (the attack vector)
- `NaN` / `±Infinity` rejected

All existing tests for consumers (pathfinding, transport ship,
construction) pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Evan
2026-07-25 10:39:16 -07:00
committed by evanpelle
co-authored by Claude Fable 5
parent dd1277e245
commit decf8ce355
2 changed files with 34 additions and 1 deletions
+1 -1
View File
@@ -166,7 +166,7 @@ export class GameMapImpl implements GameMap {
}
isValidRef(ref: TileRef): boolean {
return ref >= 0 && ref < this.refToX.length;
return Number.isInteger(ref) && ref >= 0 && ref < this.refToX.length;
}
x(ref: TileRef): number {
@@ -0,0 +1,33 @@
import { describe, expect, it } from "vitest";
import { GameMapImpl } from "../../../src/core/game/GameMap";
describe("GameMap.isValidRef", () => {
const map = new GameMapImpl(10, 8, new Uint8Array(10 * 8), 0);
it("accepts integer refs within range", () => {
expect(map.isValidRef(0)).toBe(true);
expect(map.isValidRef(1)).toBe(true);
expect(map.isValidRef(10 * 8 - 1)).toBe(true);
});
it("rejects refs outside the map", () => {
expect(map.isValidRef(-1)).toBe(false);
expect(map.isValidRef(10 * 8)).toBe(false);
expect(map.isValidRef(Number.MAX_SAFE_INTEGER)).toBe(false);
});
it("rejects non-integer refs even when in range", () => {
// A fractional ref passes a pure range check but corrupts anything that
// indexes typed arrays or iterates from it — a single malicious intent
// could freeze the simulation on every client.
expect(map.isValidRef(1.5)).toBe(false);
expect(map.isValidRef(0.0000001)).toBe(false);
expect(map.isValidRef(79.999)).toBe(false);
});
it("rejects NaN and infinities", () => {
expect(map.isValidRef(NaN)).toBe(false);
expect(map.isValidRef(Infinity)).toBe(false);
expect(map.isValidRef(-Infinity)).toBe(false);
});
});