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

API Misuse Analysis & Type-System Defenses

This document systematically analyzes every public API boundary in Iron Curtain, identifies how each can be misused (by callers, mods, network peers, or contributors making honest mistakes), and maps each misuse vector to a concrete type-system defense. It complements type-safety.md, which defines the policies — this document applies them to every API surface.

Methodology: For each API boundary, we identify: (1) what the API exposes, (2) every way a caller can misuse it, (3) which Rust type-system mechanisms prevent or detect the misuse, and (4) what automated tests verify the defense. Misuse vectors are tagged by origin: caller (honest code error), mod (sandboxed external code), network (untrusted peer), adversary (deliberate attack).


1. Simulation API (ic-sim::Simulation)

Surface: Simulation::new(), apply_tick(), snapshot(), restore(), delta_snapshot(), apply_delta(), state_hash(), apply_correction(), query_state().

#Misuse VectorOriginType-System DefenseTest Requirement
S1Call apply_tick() with orders from a future tick (tick N+2 when sim is at N)callerTickOrders carries a tick: SimTick field. apply_tick() returns Result<(), SimError>debug_assert! panics in debug builds; returns Err(SimError::TickMismatch { expected, got }) in release. SimTick newtype prevents confusion with raw u64. See 02-ARCHITECTURE.md § Simulation API for the canonical signature.T1: unit test — future-tick orders panic in debug, return Err in release
S2Call apply_tick() with duplicate orders (same order replayed twice in one tick)caller/networkTickOrders::chronological() does not deduplicate — the relay is responsible for dedup. In-sim, order validation rejects duplicate actions (e.g., two build commands on the same cell). ValidatedOrder wrapper (post-validation) is consumed once.T2: replay with duplicate injected orders → second copy rejected
S3Call restore() with a snapshot from a different game (wrong seed, wrong map)callerSimCoreSnapshot includes game_seed: u64 and map_hash: StateHash. restore() returns Result<(), SimError> — checks both match the current Simulation config. Mismatch returns Err(SimError::ConfigMismatch). See 02-ARCHITECTURE.md § Simulation API.T2: cross-game snapshot restore → Err, sim state_hash() unchanged
S4Load a truncated or corrupted save/snapshot filenetwork/adversarySaveHeader.payload_hash stores a SHA-256 (StateHash) over the compressed payload bytes (Fossilize pattern). The file-loading layer (in ic-game, not Simulation::restore()) reads the header first, SHA-256-hashes the compressed payload bytes, and verifies the hash matches before decompression or deserialization. Simulation::restore() itself is a pure sim-core operation that accepts an already-verified &SimCoreSnapshot (see 02-ARCHITECTURE.md § Simulation API). The full save-load path goes through GameRunner::restore_full() (see 02-ARCHITECTURE.md § ic-game Integration), which orchestrates file verification → decompression → deserialization → sim restore → campaign/script rehydration. Verified<SimSnapshot> is required by the reconnection codepath. The hash lives in the outer file envelope (header), not inside the serialized struct — so verification requires no parsing of untrusted data.T3: fuzz snapshot bytes → parser never panics; corrupted hash → rejection
S5Call apply_correction() outside the reconciler contextcallerapply_correction() takes &ReconcilerToken — an unforgeable capability token with _private: (). Only the SimReconciler system can construct it. Calling without a token is a compile error.T1: compile-time — no test needed; verify in code review
S6Pass f32/f64 values into sim state via snapshot deserializationadversaryThree layers of defense: (1) clippy::disallowed_types bans f32/f64 in ic-sim — no float field can exist in any snapshot struct (compile-time). (2) The save payload hash (SaveHeader.payload_hash, see S4) prevents any bit-level tampering before deserialization begins. (3) Post-deserialization, FixedPoint values are range-validated against domain-specific bounds (e.g., map coordinate limits, stat ranges). Note: the snapshot format is binary (bincode) — there is no type-level parse-time distinction between f32 bits and i32 bits in a binary codec. The compile-time float ban is the primary defense; the payload hash prevents injection; range validation catches semantically invalid values.T3: fuzz snapshot bytes → never panics; verify clippy::disallowed_types lint active in CI; post-deser range validation rejects out-of-bounds FixedPoint values
S7Inject orders for a player ID that doesn’t exist in the gamenetworkvalidate_order() checks PlayerId exists in sim.players. Unknown PlayerId triggers immediate rejection under OrderRejectionCategory::Ownership (D012). PlayerId newtype prevents confusion with other IDs.T1: exhaustive rejection matrix includes unknown-player case
S8Call snapshot() and apply_tick() concurrently from different threadscallerSimulation contains a Bevy World, which internally uses UnsafeCell and is !Sync. Since Simulation contains a !Sync field, Rust’s auto-trait rules make Simulation itself !Sync — a &Simulation cannot be shared across threads. All mutation methods additionally require &mut self, so concurrent mutable access is also prevented by the borrow checker.Compile-time enforcement — no runtime test needed
S9Construct WorldPos with out-of-range coordinatescaller/modWorldPos fields are SimCoord (newtype over i32). Map bounds are checked at order validation time, not at coordinate construction — WorldPos is a value type. OrderValidator::validate() checks position against MapBounds.T1: order with coordinates outside map bounds → OrderRejectionCategory::Placement (D012)
S10Call delta_snapshot() with a baseline from a different game branch (wrong tick sequence)callerSimCoreDelta includes baseline_tick: SimTick and baseline_hash: StateHash (full SHA-256, not SyncHash). apply_delta() returns Result<(), SimError> — verifies both match the current sim state before applying, returning Err(SimError::BaselineMismatch) on failure. Using StateHash (not SyncHash) ensures the replay seeking and autosave paths meet the same integrity bar as Verified<SimSnapshot> (see N3). See 02-ARCHITECTURE.md § Simulation API.T2: delta from divergent branch → Err, sim state unchanged

Cross-cutting defense: Simulation holds all state in a Bevy World. Systems access components via Query<> which enforces borrow rules at the ECS level — a system cannot write to a component another system is reading in the same phase.


2. Order Pipeline (ic-protocol)

Surface: PlayerOrder enum, TimestampedOrder, TickOrders, OrderCodec trait, OrderBudget.

#Misuse VectorOriginType-System DefenseTest Requirement
O1Send order referencing a UnitTag the player doesn’t ownnetwork/adversaryvalidate_order() checks ownership. UnitTag includes generation — stale references to recycled slots are caught by generation mismatch.T1: exhaustive rejection matrix — ownership column
O2Flood the relay with orders exceeding the budgetadversaryOrderBudget is a validated-construction type (private fields, _private: ()). try_spend() returns Err(BudgetExhausted) when tokens are depleted. Budget cannot be constructed in a broken state (refill=0 or cap=0 rejected). The relay calls try_spend() per order.T2: 1000 orders/tick → excess dropped; budget recovers next tick
O3Craft a TimestampedOrder with sub_tick_time in the far future to gain unfair ordering advantageadversaryRelay clamps sub_tick_time to the feasible envelope based on RTT measurement. SubTickTimestamp newtype prevents confusion with SimTick. Clamping is a relay-side operation, not trusting client values.T2: timestamp envelope clamping test; anti-abuse telemetry fires
O4Deserialize a PlayerOrder variant with an unknown discriminant (protocol version mismatch)networkserde deserialization into PlayerOrder enum returns Err for unknown variants — no silent truncation. Wire format includes protocol version header; version mismatch terminates handshake before orders flow.T3: fuzz random bytes as PlayerOrder → never panics; unknown variants → clean error
O5Construct TickOrders with orders from multiple different tickscallerTickOrders has a single tick: SimTick field. The chronological() method sorts by sub-tick time only. If orders from a wrong tick are included, that’s a relay bug — the relay constructs TickOrders per tick and only includes orders assigned to that tick. Debug builds can assert tick consistency at the relay level.T2: orders injected into wrong TickOrders batch → relay-level assertion in debug; sim processes whatever the relay emits
O6Bypass OrderBudget by constructing it via struct literal with inflated burst_capcaller/adversaryOrderBudget has _private: () field — construction outside the defining module is a compile error. Only OrderBudget::new() can create instances, which validates refill_per_tick > 0 && burst_cap > 0.Compile-time enforcement — verify in code review
O7Create Verified<PlayerOrder> without actually validating the ordercallerVerified::new_verified() is pub(crate) — only code within the same crate (the order validation module) can wrap a PlayerOrder in Verified. External code receives ValidatedOrder (type alias) from the validation function.Compile-time enforcement + code review checklist item
O8Vec<UnitTag> in Move order with 65,535 entries (oversized selection)adversaryWire protocol enforces maximum payload size per message. Inside sim, validate_order() checks selection size against MAX_SELECTION_SIZE (configurable per game module, RA1 default: 40). BoundedVec<UnitTag, MAX_SELECTION_SIZE> enforces at the type level.T1: oversized selection → OrderRejectionCategory::Custom (D012, game-module-defined cap)

3. Network Protocol & Relay (ic-net)

Surface: Relay handshake, session creation, NetworkModel trait, message lanes, reconnection, transport encryption.

#Misuse VectorOriginType-System DefenseTest Requirement
N1Send a FromServer<T> message from a clientadversaryFromClient<T> and FromServer<T> are distinct types. Relay handler functions accept only FromClient<T>. A FromServer<T> message arriving on a client connection is a type mismatch that the handler signature rejects.T2: protocol test — client packet with server message type → connection terminated
N2Replay a captured handshake challenge responseadversarySHA-256 challenge-response with server-generated nonce. Challenge includes session_id and monotonic nonce_counter. Replayed responses fail because the nonce has been consumed. Verified<HandshakeResponse> ensures the verification step cannot be skipped.T2: replay captured handshake → rejection
N3Reconnect and receive a snapshot that’s been tampered with by a malicious relayadversaryReconnection snapshot is Verified<SimSnapshot> — the hash is checked against the local trusted hash chain. Requires StateHash match, not just SyncHash. The client verifies before restoring.T4: corrupted reconnection snapshot → client rejects; stale tick → client rejects
N4Exploit NetworkModel trait to inject orders that bypass validationcallerNetworkModel::poll_tick() returns TickOrders — which still pass through Simulation::apply_tick()validate_order(). The trait has no way to inject pre-validated orders. Even LocalNetwork (test impl) goes through the full validation path.T2: LocalNetwork integration test — invalid orders still rejected
N5Cause a desync by sending different orders to different clients (relay compromise)adversaryAll clients compute SyncHash per tick. Relay distributes canonical TickOrders with hashes. Divergence detected within the sync frame window. Merkle tree localization identifies the divergent archetype.T2: desync injection → detection within N ticks
N6Denial-of-service via connection flood (half-open connections)adversaryConnection state machine uses typestate: Connection<Disconnected>Connection<Handshaking>Connection<Authenticated>Connection<InGame>Connection<PostGame>. Half-open timeout is enforced — Connection<Handshaking> that doesn’t advance within timeout is dropped. BoundedVec for pending connections with configurable cap.T3: 10,000 half-open connections → all timeout; relay remains responsive
N7SyncHash and StateHash confused in desync comparison logiccallerDistinct newtypes with no implicit conversion. Functions that compare sync data accept SyncHash; functions that verify snapshots accept StateHash. Using the wrong type is a compile error.Compile-time enforcement
N8Message lane priority manipulation (sending low-priority data on high-priority lane)adversaryMessage lanes are an enum MessageLane { Orders, Control, Chat, Voice, Bulk } (see wire-format.md). Lane selection is relay-side — clients submit to the relay’s ingress, and the relay routes to appropriate lanes. Client cannot select output lane.T2: client message with wrong lane header → relay re-routes or rejects

4. WASM Sandbox API (ic-script)

Surface: Host functions exposed to WASM mods, WasmSandbox<S> lifecycle, WasmInstanceId, capability tokens.

#Misuse VectorOriginType-System DefenseTest Requirement
W1Call execute() on a WasmTerminated instancemod/callerTypestate pattern: WasmSandbox<WasmTerminated> has no execute() method. Only WasmSandbox<WasmReady> does. Attempting to call it is a compile error.Compile-time enforcement
W2Module A reads Module B’s ECS data via crafted querymodHost API functions take WasmInstanceId (which encodes the owning mod). The host filters ECS queries to only return data the mod has permission to see. WasmInstanceId is a newtype — modules cannot forge another module’s ID (it’s assigned by the host, never exposed as raw integer to the module).T3: cross-module data probe → permission error
W3Module attempts memory.grow(65536) (4GB) to exhaust host memorymod/adversaryWASM runtime configured with memory cap (max_pages). memory.grow beyond the limit traps the module. The trap is caught by the host, which terminates the module cleanly. BoundedCvar<u32> controls the max pages setting — cannot be set to unreasonable values.T3: adversarial memory.grow → denied at limit; host stable
W4Module writes f32 value to sim state via host callbackmodHost API functions that write to sim components accept FixedPoint, not f32. The WASM→host ABI conversion layer rejects f32 arguments for sim-writing functions. No implicit float→fixed conversion.T3: float write attempt → type rejection error
W5Module enters infinite loop (CPU exhaustion)modFuel metering: WASM runtime configured with instruction fuel budget per tick. When fuel exhausted, execution traps. Host catches trap, terminates module, continues game. The fuel budget is a BoundedCvar<u64> — runtime configurable within bounds.T3: infinite loop module → terminated at fuel limit; game continues
W6Module calls FsReadCapability with ../../etc/passwd pathmod/adversaryFsReadCapability restricts to a StrictPath<PathBoundary>. Path traversal via ../ is caught by StrictPath::join(), which rejects any path that escapes the boundary. The capability’s allowed_path cannot be modified by the module (private field + _private: ()).T3: path traversal attempt → SandboxError::PathEscapeAttempt
W7Module constructs a FsReadCapability from scratchmodFsReadCapability has _private: () — unconstructible outside the host module. WASM modules receive capabilities as opaque handles passed by the host. The handle-to-capability mapping is host-internal.Compile-time enforcement
W8Module calls host functions after being terminated (use-after-terminate)callerTypestate: WasmSandbox<WasmTerminated> exposes no host callback methods. Drop implementation cleans up the WASM instance. Host function table is deregistered on termination.Compile-time enforcement + T3: access after terminate → no-op or clean error
W9Two WASM modules loaded with the same WasmInstanceIdcallerWasmInstanceId is allocated by a global monotonic counter in the host. Allocation is pub(crate) — external code cannot construct WasmInstanceId. Counter never resets within a process.T2: load 100 modules → all IDs unique; ID pool exhaustion → clean error

5. Lua Sandbox API (ic-script)

Surface: Lua globals (D024: 16+ OpenRA-compatible + IC extensions), resource limits, deterministic execution.

#Misuse VectorOriginType-System DefenseTest Requirement
L1string.rep("a", 2^30) memory bombmodLua runtime configured with memory limit. Allocation hook denies requests beyond budget. Script receives out of memory error. Memory budget is BoundedCvar<usize>.T3: string.rep bomb → allocation denied; host stable
L2Infinite loop via while true do endmodInstruction count hook fires after fuel budget expires. Script terminated with timeout error. Host recovers execution.T3: infinite loop → terminated at instruction limit
L3os.execute(), io.open(), loadfile() — system accessmodThese standard library functions are not registered in the sandbox. The Lua environment is constructed with a whitelist of allowed globals. Attempts to call them produce nil function error.T1: attempt to call each banned function → nil/error
L4Script modifies global state shared between scriptsmodEach Lua script runs in an isolated environment (_ENV). Global tables are read-only from the script’s perspective (metatable __newindex error). Scripts communicate only through host-mediated APIs.T3: script A sets global → script B cannot see it
L5Script crafts a UnitTag directly to manipulate units it shouldn’t accessmodLua receives UnitTag values from host API calls. Lua’s number type represents them as opaque integers. Every host API that accepts a UnitTag validates ownership against the calling script’s PlayerId. Lua cannot construct a valid UnitTag — it must receive one from the host.T3: script forges UnitTag for enemy unit → host rejects
L6Script calls Trigger.AfterDelay() with delay=0 to create recursive timer bombmodTimer system enforces minimum delay (MIN_TRIGGER_DELAY, e.g., 1 tick). BoundedCvar ensures the minimum cannot be configured to 0. Timer depth is bounded — recursive timer chains exceeding MAX_TRIGGER_DEPTH are terminated.T3: delay=0 timer → rejected or clamped; recursive chain → terminated
L7Non-deterministic Lua behavior (e.g., table iteration order, math.random())modmath.random() is redirected to the sim’s DeterministicRng (not removed — OpenRA compat). Table creation uses deterministic key ordering by construction. os.time(), os.clock() are removed from the environment.T2: Lua script producing random numbers → identical sequence across platforms

6. Replay System

Surface: Recording, playback, signing, ForeignReplayPlayback, format parsing.

#Misuse VectorOriginType-System DefenseTest Requirement
R1Tampered replay file submitted as evidence for dispute resolutionadversaryReplay file includes Ed25519 signature chain. Verified<ReplaySignature> is required before a replay is accepted as tamper-evident evidence (tournament review, dispute resolution). The ranking system itself accepts only relay-signed CertifiedMatchResult (D055) — replays are evidence artifacts, not the primary ranked authority.T4: tampered frame → signature verification fails
R2Replay from a different game version played back on current versioncallerReplay header contains version: u16 (serialization format version); metadata JSON contains base_build, data_version, and game_module (see formats/save-replay-formats.md § Metadata). Playback refuses mismatches with specific error. SnapshotCodec version dispatch (D054) handles format evolution.T2: wrong-version replay → version mismatch error
R3Crafted replay that causes excessive memory allocation during parsingadversaryReplay parser uses bounded readers. total_ticks declared in the header is validated against file size. Section offset/length pairs are bounds-checked before reading. Individual frame sizes are bounded. Fuzz testing covers decompression-bomb patterns.T3: fuzz replay bytes → parser never allocates unboundedly; no panics
R4Replay playback diverges from recording (non-determinism bug)callerReplay round-trip test: record → playback → hash comparison. If hashes diverge, the replay flags the exact tick and archetype where divergence occurred using Merkle tree.T2: replay round-trip → hash match; T3: cross-platform replay → hash match
R5Foreign replay (.orarep) with crafted orders exploiting IC’s order validation differencesadversaryForeignReplayPlayback NetworkModel applies IC’s full order validation to foreign orders. Invalid orders are rejected (not silently applied). Divergences between foreign engine and IC behavior are logged as ForeignDivergence events.T3: OpenRA replay with IC-invalid orders → orders rejected; divergence logged

7. Workshop Package Lifecycle (ic-net / Workshop)

Surface: PackageInstall<S> typestate, dependency resolution, manifest verification, publishing.

#Misuse VectorOriginType-System DefenseTest Requirement
P1Install a package without verifying its hash (skip verification step)callerTypestate: PackageInstall<PkgDownloading>PackageInstall<PkgVerifying>PackageInstall<PkgExtracted>. Cannot call extract() on PkgDownloading — must pass through PkgVerifying first. Each transition consumes self.Compile-time enforcement
P2Publish a package with a previously-used version number (version mutability)adversaryRegistry enforces version immutability. PackageVersion is compared using semantic versioning exact match. Once published, (PublisherId, PackageName, PackageVersion) is permanently occupied. Re-publish returns PublishError::VersionExists.T2: re-publish same version → rejection; existing package unchanged
P3Dependency confusion — malicious package with same name as popular package in different registryadversaryPackageName is scoped to PublisherId. Full identifier is publisher/package_name. Without the publisher prefix, resolution fails. PackageName is a validated newtype (3-64 chars, [a-z0-9-] only).T1: unprefixed package name → validation error; name collision across publishers → both install correctly
P4Circular dependency chain: A→B→C→Aadversary/callerDependencyGraph uses validated construction — DependencyGraph::new() runs cycle detection (topological sort). Cycles produce DependencyGraphError::Cycle { path }. The _private: () field prevents construction without validation.T1: circular deps → cycle error with full path
P5Diamond dependency with incompatible version constraintscallerPubGrub resolver detects version conflicts and reports both constraint chains. Error includes required_by_a, required_by_b, and the conflicting version ranges.T1: diamond conflict → error with both chains
P6Package manifest declares small size but contains decompression bombadversaryExtraction uses bounded decompressor. Declared size in manifest is compared against actual decompressed size — if actual exceeds declared × safety margin (e.g., 2×), extraction aborts. CompressionAlgorithm enum controls decompressor selection (no arbitrary decompressor).T3: decompression bomb → extraction aborted; host stable
P7Forge a Verified<ManifestHash> to bypass integrity checkcaller/adversaryVerified::new_verified() is pub(crate) — only the verification function in the manifest-checking module can create it. Code outside that crate receives Verified<ManifestHash> from the verification API.Compile-time enforcement + code review


Sub-Pages

SectionTopicFile
Analysis 8-14 + PatternsCampaign Graph, Console, Chat, Double-Buffered State, Entity Identity, Config Loading, Asset Pipeline + cross-cutting type-system defense patterns + defense gap analysis + summary matrixapi-misuse-patterns.md