Skip to content

Host-controlled RNG seed enables deterministic faction prediction/abuse #2359

@tintinhamans

Description

@tintinhamans

Prerequisites

  • I have searched for similar issues and confirmed this is not a duplicate

Game Version

  • Command & Conquer Generals
  • Command & Conquer Generals: Zero Hour
  • Other (please specify below)

Bug Description

Summary

The game's random number generator (RNG) seed for multiplayer matches is fully deterministic. Once the seed is known, all "random" outcomes — most notably faction assignment when players select "Random" — can be predicted. Depending on the multiplayer backend, the seed is either directly controlled by the host (LAN) or visible to all clients before the game starts (GeneralsOnline). This undermines fair play in competitive and casual multiplayer.

How the seed flows

LAN

  1. Host sets the seed — When a LAN game lobby is created, the host calls setSeed(GetTickCount()) in LANAPI::RequestGameCreate.
  2. Seed is broadcast in plaintext — The seed is serialized into the game options string as SD=<value> via GameInfoToAsciiString() and sent over unencrypted UDP to all clients. The host periodically re-broadcasts this via RequestGameOptions / RequestGameAnnounce.
  3. All clients apply the seed — At game start, every client calls InitGameLogicRandom(m_currentGame->getSeed()).

On LAN the host has full unilateral control over the seed.

GeneralsOnline

  1. Server generates the seed — The host client's CreateLobby request does not include a seed. The server assigns RNGSeed and returns it in the lobby JSON response (OnlineServices_LobbyInterface.cpp).
  2. Seed is distributed to every client before game start — All lobby members receive lobby.rng_seed as part of the LobbyEntry struct when they sync with the lobby. NGMPGame::SyncWithLobby calls setSeed(lobby.rng_seed).

On GeneralsOnline the host cannot choose the seed, but every client can observe it before the match begins and predict the outcome.

Faction resolution (both backends)

populateRandomSideAndColor() in GameLogic.cpp resolves every PLAYERTEMPLATE_RANDOM slot using GameLogicRandomValue(), which is seeded by the host-/server-provided seed. The existing MORE_RANDOM mitigation attempts to discard early values, but the discard count itself is derived from the seed (GetGameLogicRandomSeed() % 7), so it is equally predictable.

How the seed can be obtained or abused

Vector Applies to Description
Host manipulation LAN The host can restart lobbies until GetTickCount() yields a favourable seed, or a modified client can set an arbitrary seed directly.
Memory scraping LAN, GeneralsOnline Any player can read the seed from the game process memory (theGameLogicBaseSeed / m_seed in GameInfo) before the match begins and compute the faction table offline.
LAN traffic sniffing LAN Game options are broadcast as unencrypted UDP containing SD=<seed>. Any device on the network can capture and decode it.
Lobby API observation GeneralsOnline The RNGSeed field is part of the lobby response JSON visible to every client. Even though the server generates it, any client (or third-party tool querying the API) can read the seed and predict assignments before the game launches.

Impact on fair play

  • On LAN, a host who wants a specific faction matchup can trivially achieve it.
  • On any backend, any player with basic tooling can predict what "Random" will resolve to for every slot, removing the strategic uncertainty that "Random" is supposed to provide.
  • In ranked/competitive play this directly compromises match integrity.

Proposed solution

Short term (server-side, GeneralsOnline):

  • Do not distribute the RNG seed to clients until the moment of game launch (i.e. after all players have locked in and the countdown completes). This closes the window for pre-game prediction.
  • Ensure the seed is generated with a cryptographically secure RNG on the server.

Long term (all backends including LAN):
Implement a commit-reveal multi-party entropy protocol:

  1. Each player generates a local secret random value and broadcasts a hash (commitment) during the pre-game phase.
  2. Once all commitments are collected, each player reveals their secret.
  3. The final game seed is computed as a deterministic combination (e.g. XOR or cryptographic hash) of all revealed values.
  4. Any player who fails to reveal, or whose reveal doesn't match the commitment, triggers a lobby abort or re-roll.

This ensures no single client — including the host — can determine or predict the seed, and passive observation of network traffic or memory is insufficient to derive the final seed before all parties have committed.

Relevant code

Reproduction Steps

  1. Host a multiplayer game (LAN or GeneralsOnline).
  2. Have one or more players select "Random" faction.
  3. Start the game and note the assigned factions.
  4. Re-host with the same seed (on LAN: set m_seed in memory or replay the same GetTickCount() value; on GeneralsOnline: read RNGSeed from the lobby API response) and the same lobby configuration.
  5. Observe that the "random" faction assignments are identical — they are fully determined by the seed.
  6. On LAN, alternatively capture the UDP broadcast to obtain the SD= value and compute the expected assignments offline before the game begins.

Additional Context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    MajorSeverity: Minor < Major < Critical < BlockerTaskA task for someone to work on

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions