Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

8. Campaign Graph (ic-sim / D021)

Surface: CampaignGraph, MissionExecution<S>, roster carryover, story flags.

#Misuse VectorOriginType-System DefenseTest Requirement
C1Complete a mission that is still loading (MissionLoadingMissionCompleted)callerTypestate: MissionExecution<MissionLoading> has no complete() method. Only MissionExecution<MissionActive> does. Transition consumes self.Compile-time enforcement
C2Construct a CampaignGraph with cycles (mission A → B → A)caller/modValidated construction: CampaignGraph::new() validates the DAG property. _private: () prevents bypass via struct literal.T1: cyclic graph → CampaignGraphError::Cycle
C3Transition to a MissionId that doesn’t exist in the graphcaller/modMissionExecution<MissionCompleted>::transition() takes an OutcomeName and looks up the next mission in the validated CampaignGraph. Invalid outcomes return CampaignTransitionError::NoSuchOutcome.T1: dangling outcome → error
C4Roster carryover with fabricated unit stats (inflated health/veterancy)mod/adversaryRoster is extracted from the sim snapshot at mission end — not user-supplied. roster_extract() reads from Verified<SimSnapshot>. Stats come from the actual sim state, not from save data the user can edit.T2: roster extraction from snapshot → matches actual unit state
C5Story flag name collision between different campaign modsmodStory flags are namespaced by CampaignId. Flag access API requires (CampaignId, FlagName). FlagName is a validated newtype. Cross-campaign flag access is impossible without the correct CampaignId.T2: flag set in campaign A → not visible in campaign B

9. Console Commands (ic-ui / D058)

Surface: Command tree, cvar system, BoundedCvar<T>, permission tiers, DevModeFlag.

#Misuse VectorOriginType-System DefenseTest Requirement
X1Non-admin player executes admin-only commandnetwork/adversaryEach command node is tagged with a PermissionTier enum (Player, Admin, Dev). Command execution checks caller.permission >= command.required_tier. The comparison is on the enum, not a raw integer — no off-by-one privilege escalation.T1: non-admin → admin command → permission error
X2Set a BoundedCvar to out-of-range value via consolecallerBoundedCvar::set() clamps to [min, max]. The cvar cannot represent an out-of-range value — it’s structurally impossible after construction. _private: () prevents direct field assignment.T1: set cvar to extreme value → clamped to nearest bound
X3Command injection via chat-crafted / prefix in multiplayeradversaryChat messages beginning with / are parsed through the command tree. Unknown commands return CommandError::NotFound — they are not echoed to chat. The Brigadier-style parse tree has no string interpolation or eval.T2: malformed command string → clean error; no eval
X4Execute dev command during ranked game without flaggingcallerAny command tagged DEV_ONLY sets DevModeFlag on the sim state. DevModeFlag is checked by the ranking system — flagged games are excluded from ranked standings. The flag is write-once-per-session (cannot be unset).T2: dev command in ranked → replay flagged; no leaderboard entry
X5Flood commands to overwhelm the command parseradversaryCommand processing is rate-limited via the same OrderBudget mechanism as orders. Excess commands are dropped with CommandRejection::RateLimitExceeded.T2: 1000 commands/tick → excess dropped; rate recovers

10. Chat & Communication System (D059)

Surface: ChatMessage<S> scope-branded messages (domain/UI layer — lowered to unbranded ChatMessage { channel, text } at the wire boundary; see type-safety.md § Layering note), ping system, chat wheel, minimap drawing.

#Misuse VectorOriginType-System DefenseTest Requirement
M1Team chat message accidentally broadcast to all playerscallerChatMessage<TeamScope> and ChatMessage<AllScope> are distinct types. The team chat handler accepts only ChatMessage<TeamScope>. The routing function for team chat is physically separate from all-chat routing. No implicit From<TeamScope>AllScope conversion.T1: team message → only team members receive; T2: integration test
M2RTL/BiDi override characters injected in chat to disguise message contentadversarySanitizedString replaces String for all user-facing text. Construction requires passing through sanitize_bidi(), which strips/neutralizes BiDi override codepoints per UAX #9. The inner String is private.T3: BiDi QA corpus regression suite
M3Display name impersonation via Unicode confusables (Cyrillic ‘а’ vs Latin ‘a’)adversaryDisplay names are validated via UTS #39 confusable check at registration. DisplayName is a validated newtype — construction runs confusable detection. Existing names are checked for collision with the new name’s skeleton.T3: confusable corpus → all impersonation attempts blocked
M4Ping spam (hundreds of pings per second to obscure minimap)adversaryPing system uses OrderBudget-style rate limiting. Excess pings are silently dropped. Ping rate limit is lower than order rate limit (configurable, default: 5/second).T2: 100 pings/second → excess dropped; rate recovers
M5Minimap drawing exceeds size limit (draw thousands of points)adversaryDrawing commands are bounded (BoundedVec<DrawPoint, MAX_DRAW_POINTS>). Excess points are truncated. Drawing state is cleared each tick.T2: oversized draw command → truncated to limit

11. Double-Buffered State

Surface: DoubleBuffered<T> — used for fog visibility, influence maps, weather terrain effects, global condition modifiers.

#Misuse VectorOriginType-System DefenseTest Requirement
B1Reading from the write buffer during a tick (seeing partial state)callerDoubleBuffered::read() returns &T of the read buffer. DoubleBuffered::write() returns &mut T of the write buffer. Borrow checker prevents aliasing. Additionally, only the designated writer system calls write() — enforced by Bevy system parameter constraints (the writer system takes ResMut<DoubleBuffered<T>>, readers take Res<DoubleBuffered<T>>).T2: determinism test — two sims with same input produce identical fog state
B2Forgetting to call swap() at tick boundary (readers see stale data)callerswap() is called in Simulation::apply_tick() at a fixed point (before system pipeline). This is a single call site, not distributed across systems. Integration tests verify fog state advances per tick.T2: fog update → next tick readers see updated visibility
B3Calling swap() multiple times per tick (can corrupt state)callerswap() is called exactly once in apply_tick(). Debug assertion tracks swap count per tick. Multiple swaps in a single tick triggers debug_assert!(self.swap_count_this_tick == 0).T1: debug assertion catches double-swap in test
B4Writer system mutates the read buffer directly (bypassing double-buffered write)callerThe read buffer’s type is accessed via read()&T (immutable). Without unsafe, the writer cannot obtain &mut to the read buffer. Rust’s borrow system enforces this structurally.Compile-time enforcement

12. UnitTag & Entity Identity

Surface: UnitTag { index: u16, generation: u16 }, UnitPool resource, UnitTag ↔ Entity mapping.

#Misuse VectorOriginType-System DefenseTest Requirement
U1Stale UnitTag used after unit death (use-after-free analog)caller/mod/networkUnitPool::resolve(tag) checks generation. If the slot has been recycled (new unit with higher generation), returns None. Callers handle None explicitly (order validation rejects stale targets).T1: stale UnitTag → resolution returns None; T2: attack order on dead unit → OrderRejection
U2UnitTag Index overflow — more than 65,535 unitscallerUnitPool size is bounded by game module config (RA1: 2048). Allocation beyond capacity returns UnitPoolError::PoolExhausted. The pool size is checked at game start — misconfig is caught early.T2: pool exhaustion → clean error; T3: fuzz with adversarial spawn rates
U3Generation wraparound — generation u16 overflows after 65,535 recycles of same slotcallerAt 30 tps with frequent unit deaths, a single slot recycling every 2 seconds wraps in ~36 hours. In practice, generation overflow is unlikely but handled: UnitPool detects wraparound and retires the slot permanently (marks it unusable).T4: synthetic stress test — force 65,535 recycles on one slot → slot retired
U4Lua/WASM script constructs a UnitTag from raw integersmodHost API returns UnitTag values as opaque handles. Lua represents them as userdata (not plain numbers). WASM receives them as handles through the ABI. Neither can construct arbitrary UnitTag values — they can only use values the host gave them.T3: script forges tag value → host API rejects with ownership error
U5Serialized UnitTag in replay or network message doesn’t match any live entitynetworkUnitPool::resolve() returns None for unknown tags. Order execution handles None gracefully (order is silently dropped or rejected depending on context). No panic path.T2: replay with stale tags → orders rejected gracefully

13. Configuration Loading (TOML/YAML)

Surface: config.toml, server_config.toml, settings.toml, mod YAML, serde deserialization.

#Misuse VectorOriginType-System DefenseTest Requirement
F1YAML rule with negative health or zero-cost unit creating degenerate gameplaymodSchema validation runs at load time. Health.max is validated as > 0. Buildable.cost is validated as >= 0. Validation errors include the YAML file path, line number, and field name.T1: negative health YAML → schema error with location
F2YAML inheritance creates infinite loop (A inherits B inherits A)modInheritance resolver detects cycles via visited-set tracking. RuleLoadError::CircularInheritance { chain } includes the human-readable cycle path.T1: circular inheritance → error with chain; T3: fuzz random inheritance trees
F3TOML config with unknown keys (typos that silently do nothing)callerserde’s #[serde(deny_unknown_fields)] on config structs. Unknown keys produce a deserialization error naming the unknown field and listing valid fields. ic server validate-config CLI runs the same check.T1: unknown key in config → error with suggestion
F4Config value within valid range but cross-parameter inconsistency (e.g., min > max)callerConfig struct validate() method checks cross-parameter invariants after deserialization. Example: OrderBudget::new() rejects refill > burst_cap. Validated construction applies to all config types with dependent constraints.T1: cross-parameter inconsistency → specific error
F5Server config hot-reload introduces inconsistent state mid-gamecallerHot-reload applies changes at tick boundary only (between ticks). Changes are validated against the full config before application — invalid partial updates are rejected atomically. The reload handler takes &mut Config, not individual fields.T2: hot-reload with invalid change → rejected; old config preserved

14. Asset Pipeline & Format Parsing (ic-cnc-content)

Surface: .mix, .shp, .pal, .aud, .oramap parsers, StrictPath, virtual filesystem.

#Misuse VectorOriginType-System DefenseTest Requirement
A1Zip Slip in .oramap archive (entry with ../../ path)adversaryStrictPath<PathBoundary> rejects any path that escapes its boundary directory. The .oramap extractor resolves entries through StrictPath::join(), which normalizes and boundary-checks.T3: fuzz .oramap with path traversal entries → all rejected
A2.mix archive with header declaring more files than the archive containsadversaryParser compares declared file count against available data. Returns MixParseError::FileCountMismatch { declared, actual } with full diagnostic.T1: truncated .mix → specific error; T3: fuzz random .mix bytes
A3.shp sprite file with frame dimensions causing integer overflow in buffer allocationadversaryFrame dimensions are validated against MAX_SPRITE_DIMENSION (configurable, default: 4096×4096). Buffer allocation uses checked_mul() — overflow returns error, not wrap.T3: fuzz .shp with extreme dimensions → clean error
A4.aud audio file with decompression ratio indicating a bombadversaryAudio decoder uses bounded output buffer. If decompressed size exceeds MAX_AUDIO_OUTPUT_BYTES, decoding aborts. The declared sample count is cross-checked against the compressed data size.T3: fuzz .aud with decompression bomb → clean abort
A5Virtual filesystem path collision (two mods providing same asset path)modMod profile system (D062) uses explicit priority ordering. When two mods conflict, the higher-priority mod wins. Conflict is detected and reported at profile activation (not silently). Fingerprint of the resolved namespace ensures all clients agree.T2: conflicting mods → conflict reported; resolution deterministic

Cross-Cutting Type-System Defense Patterns

These patterns apply across multiple API surfaces:

Pattern 1: Newtype Fortress

Every domain identifier (PlayerId, SimTick, UnitTag, MissionId, PackageName, WasmInstanceId, etc.) is a newtype with a private inner field. Construction is restricted to pub(crate) or validated factory methods. This eliminates the most common bug class in game engines: parameter swap errors.

Measured impact: In a hypothetical codebase using raw integers, apply_order(player: u32, tick: u32, unit: u32) permits 6 argument orderings. With newtypes, only 1 compiles. This is a 6× reduction in the callable-but-wrong surface.

Pattern 2: Typestate Lifecycle

State machines with restricted transitions use typestate (connection, WASM sandbox, Workshop install, campaign mission, balance patch). Invalid transitions are compile errors, not runtime checks. Every consume-self-return-new-state method documents the transition in its type signature.

Coverage: 5 typestate machines currently defined. Each eliminates 2–4 invalid transition paths that would otherwise require runtime checks and tests.

Pattern 3: Capability Tokens

APIs that grant access (filesystem, network, ECS queries from mods) require unforgeable capability tokens. Tokens use _private: () to prevent external construction. This pattern applies to: FsReadCapability, ReconcilerToken, AdminCapability, and WASM host function access.

Pattern 4: Verified Wrappers

Security-critical data passes through verification once, then carries proof of verification in the type system. Verified<T> prevents the “forgot to verify” bug class. Applies to: SCR signatures, manifest hashes, replay signatures, validated orders.

StructurallyChecked<T> is the relay-side counterpart — a weaker wrapper indicating structural validation (decode + field bounds) has passed, but NOT full sim validation (D012). The relay cannot produce Verified<T> because it does not run ic-sim. Applies to: StructurallyChecked<TimestampedOrder> in the ForeignOrderPipeline (see cross-engine/relay-security.md). Same _private: () construction guard as Verified<T> — only the structural validation path can wrap.

Pattern 5: Bounded Collections

Every collection that grows based on external input has a type-enforced bound. BoundedVec<T, N> returns Err(CapacityExceeded) on overflow. This applies to: order queues, chat buffers, ping lists, draw commands, waypoints, build queues, group assignments.

Pattern 6: Direction/Scope Branding

Messages carry their origin/destination in the type: FromClient<T> vs FromServer<T>, ChatMessage<TeamScope> vs ChatMessage<AllScope>. Routing errors become type errors. No implicit conversion between branded types.


Defense Gap Analysis

Areas where type-system defenses are defined but should be validated through priority testing:

GapCurrent StateRecommended ActionPriority
DeltaSnapshot baseline validationCovered: S10 runtime test in testing-properties-misuse-integration.md (divergent-baseline delta → Err(BaselineMismatch), sim state unchanged). Delta baselines matter for replay apply_full_delta() and autosave I/O-thread reconstruction — reconnection uses full SimSnapshot, not deltas.Resolved — S10 test spec exists. Verify implementation during M2.QA.SIM_API_DEFENSE_TESTS.M2
Composite restore/apply integrationProperty tests cover GameRunner::restore_full() / apply_full_delta() round-trips and autosave fidelity (testing-properties-misuse-integration.md) — this validates the testing contract (correct types, correct assertions, correct metric). Remaining open gap: integration tests exercising campaign graph mutation + Lua VM rehydration + script on_deserialize() correctness across save-load and replay seeking paths. The property tests prove the sim-core round-trip; the integration gap is the campaign/script layer behavior under real orchestration.Add integration test scenarios: (1) save-load with active campaign branching + Lua globals, (2) replay seek across a campaign branch point, (3) autosave round-trip with WASM persistent memoryM4
Lua timer recursion depth boundDesigned (L6) but not in any fuzz targetAdd lua_timer_chain fuzz targetM7
UnitTag generation wraparoundHandled by slot retirement (U3) but rare to triggerAdd T4 stress test for wraparoundM9
Config hot-reload atomicityDesigned (F5) but not in subsystem test specsAdd to Console Command or Server Config test tableM5
Foreign replay order rejection (R5)ForeignDivergence logging defined but no labeled corpusAdd foreign replay test corpus (10 replays minimum)M10
Cross-module WASM communicationBlocked by type system; basic cross-module probe tests spec’d at M6 (V50 — testing-coverage-release.md). Remaining gap: inter-module data leaks via shared host resources (e.g., global ECS) need expanded integration testing beyond M6’s probe coverage.Add WASM shared-host-resource integration scenarios (beyond M6 probes)M7
Ping/draw rate recovery timingBudget recovery is tested but exact tick-level recovery rate is not assertedAdd specific tick-count assertion for rate recoveryM6

Summary Matrix

API SurfaceMisuse VectorsCompile-Time DefensesRuntime DefensesTests Required
Simulation103 (borrow checker, !Sync via Bevy World, ReconcilerToken)8 (hash check, validation, bounds, dedup)8 runtime tests
Order Pipeline83 (Verified, _private, BoundedVec)5 (validation, budget, clamping)6 runtime tests
Network/Relay82 (FromClient/FromServer, SyncHashStateHash)6 (challenge, hash, typestate)6 runtime tests
WASM Sandbox94 (typestate, _private, capability tokens, type rejection)5 (fuel, memory cap, timeout)7 runtime tests
Lua Sandbox70 (Lua is dynamically typed — defenses are host-side)7 (memory limit, fuel, whitelist, isolation)7 runtime tests
Replay51 (Verified<ReplaySignature>)4 (signature, version, bounds, hash)5 runtime tests
Workshop73 (typestate, _private, validated construction)4 (immutability, cycle detection, bomb guard)5 runtime tests
Campaign52 (typestate, validated construction)3 (flag namespace, roster extraction, reference check)4 runtime tests
Console51 (BoundedCvar)4 (permission, rate limit, flag, clamping)5 runtime tests
Chat52 (scope branding, SanitizedString)3 (rate limit, BiDi strip, confusable check)5 runtime tests
Double-Buffer43 (borrow checker, &T read, &mut T write)1 (debug swap count)2 runtime tests
UnitTag51 (opaque handle for mods)4 (generation check, pool bound, ownership validation)5 runtime tests
Config51 (deny_unknown_fields)4 (validation, cycle detect, cross-param, atomicity)5 runtime tests
Asset Pipeline51 (StrictPath)4 (bounds check, bomb guard, hash-check, priority)4 runtime tests
Totals8827 compile-time61 runtime76 tests

The type system eliminates 27 misuse vectors at compile time (31%). The remaining 61 require runtime checks. Most runtime defenses have corresponding tests spec’d in testing-properties-misuse-integration.md; the gap table above tracks the remaining coverage work (composite integration, Lua timer fuzz, config atomicity, foreign replay corpus, WASM inter-module isolation, ping rate recovery).