Skip to content

fix(logic): Improve handling of ENABLE_RETALIATION_MODE in GameLogicDispatch#2408

Merged
xezon merged 4 commits intoTheSuperHackers:mainfrom
stephanmeesters:fix/gamelogicdispatch-retaliation
Mar 14, 2026
Merged

fix(logic): Improve handling of ENABLE_RETALIATION_MODE in GameLogicDispatch#2408
xezon merged 4 commits intoTheSuperHackers:mainfrom
stephanmeesters:fix/gamelogicdispatch-retaliation

Conversation

@stephanmeesters
Copy link

@stephanmeesters stephanmeesters commented Mar 4, 2026

Simplifies the code and prevents other players from setting each others retaliation mode. Tested with 1k replays and with own replay where I swap the retaliation mode on both sides couple times and test with some combat.

Generals (not Zero Hour) does not have a retaliation mode

Null check of thisPlayer will be handled by #2383

@greptile-apps
Copy link

greptile-apps bot commented Mar 4, 2026

Greptile Summary

This PR fixes a security issue where a player could craft a MSG_ENABLE_RETALIATION_MODE message to manipulate another player's retaliation state in non-RETAIL_COMPATIBLE_CRC builds, and simultaneously simplifies the message format by removing the now-redundant player-index argument behind the RETAIL_COMPATIBLE_CRC guard for backward compatibility.

  • Player.cpp: Correctly gates appendIntegerArgument(getPlayerIndex()) behind #if RETAIL_COMPATIBLE_CRC so both sender and receiver use a consistent message format.
  • GameLogicDispatch.cpp: The #else path cleanly uses thisPlayer (derived from msg->getPlayerIndex(), i.e. the actual sender) instead of a caller-supplied index, which correctly prevents cross-player manipulation in non-RETAIL_COMPATIBLE_CRC builds.
  • RETAIL_COMPATIBLE_CRC caveat: In the legacy path a DEBUG_ASSERTCRASH is added to flag cross-player assignments during development, but in release builds this assertion is a no-op (((void)0)), so the underlying vulnerability is not blocked in RETAIL_COMPATIBLE_CRC release builds. The PR description should clarify this limitation.
  • thisPlayer null safety: The new #else path calls thisPlayer->setLogicalRetaliationModeEnabled(...) without a null guard, consistent with the rest of the dispatcher function. The PR description acknowledges this is covered by PR fix(logic): Add nullptr check to thisPlayer in GameLogic::logicMessageDispatcher #2383.

Confidence Score: 3/5

  • Safe to merge with awareness that the security fix only applies to non-RETAIL_COMPATIBLE_CRC builds; the legacy path retains the original vulnerability in release builds.
  • The non-RETAIL_COMPATIBLE_CRC path is a clean, correct fix that achieves its stated goal. The RETAIL_COMPATIBLE_CRC path intentionally preserves old behaviour for CRC compatibility but the debug-only assertion provides no runtime protection in release builds, leaving the vulnerability open there. The null-pointer risk on thisPlayer is pre-existing and acknowledged as handled by a separate PR (fix(logic): Add nullptr check to thisPlayer in GameLogic::logicMessageDispatcher #2383).
  • GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp — specifically the RETAIL_COMPATIBLE_CRC branch and its reliance on DEBUG_ASSERTCRASH for security.

Sequence Diagram

sequenceDiagram
    participant P as Player::update()
    participant MS as MessageStream
    participant D as GameLogicDispatch

    Note over P,D: Non-RETAIL_COMPATIBLE_CRC path (new, fixed)
    P->>MS: appendMessage(MSG_ENABLE_RETALIATION_MODE)
    P->>MS: appendBooleanArgument(enabled)
    MS->>D: logicMessageDispatcher(msg)
    D->>D: thisPlayer = getNthPlayer(msg->getPlayerIndex())
    D->>D: enableRetaliation = getArgument(0)->boolean
    D->>D: thisPlayer->setLogicalRetaliationModeEnabled(enableRetaliation)

    Note over P,D: RETAIL_COMPATIBLE_CRC path (old behaviour + debug assert)
    P->>MS: appendMessage(MSG_ENABLE_RETALIATION_MODE)
    P->>MS: appendIntegerArgument(playerIndex)
    P->>MS: appendBooleanArgument(enabled)
    MS->>D: logicMessageDispatcher(msg)
    D->>D: playerIndex = getArgument(0)->integer
    D->>D: enableRetaliation = getArgument(1)->boolean
    D->>D: player = getNthPlayer(playerIndex)
    D->>D: DEBUG_ASSERTCRASH(player == thisPlayer) [debug only]
    D->>D: player->setLogicalRetaliationModeEnabled(enableRetaliation)
Loading

Last reviewed commit: d4acfd6

@stephanmeesters stephanmeesters changed the title fix(logic): Improve handling of ENABLE_RETALIATION_MODE in GameLogicDispatch fix(logic): Improve handling of ENABLE_RETALIATION_MODE in GameLogicDispatch and optimize the MSG for non-retail mode Mar 4, 2026
@stephanmeesters stephanmeesters changed the title fix(logic): Improve handling of ENABLE_RETALIATION_MODE in GameLogicDispatch and optimize the MSG for non-retail mode fix(logic): Improve handling of ENABLE_RETALIATION_MODE in GameLogicDispatch and optimize the MSG Mar 4, 2026
@Caball009
Copy link

I'm fine with the current code, but I do think either the comment could be more informative and / or there could be an assertion like DEBUG_ASSERTCRASH(ThePlayerList->getNthPlayer(msg->getArgument(0)->integer) == thisPlayer.

@stephanmeesters
Copy link
Author

I'm fine with the current code, but I do think either the comment could be more informative and / or there could be an assertion like DEBUG_ASSERTCRASH(ThePlayerList->getNthPlayer(msg->getArgument(0)->integer) == thisPlayer.

Both this and #2380 are now under RETAIL_COMPATIBLE_CRC and debug logging was added.

#else
// TheSuperHackers @fix stephanmeesters 08/03/2026 Ensure that players can only set their own retaliation mode.
const Bool enableRetaliation = msg->getArgument( 0 )->boolean;
thisPlayer->setLogicalRetaliationModeEnabled( enableRetaliation );
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect this will cause mismatch now.

Retaliation needs to be network synced otherwise the Unit behavior will mismatch between clients.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did test it with local multiplayer multi-instance without issues.

The only effective change is to use the knowledge that a player can only change its own retaliation mode, so there is no apparent need to pass another player ID as message argument, as you can use the ID of the owner of the message (which is thisPlayer). This is also consistent with how it works in other message types

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I understand now. I was confused by thisPlayer. I suggest make a follow up and call it msgPlayer, because thisPlayer makes it sounds a bit like the local player.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can do that in #2383 if you like.

@xezon xezon added Major Severity: Minor < Major < Critical < Blocker Gen Relates to Generals ZH Relates to Zero Hour Fix Is fixing something, but is not user facing labels Mar 14, 2026
Copy link

@xezon xezon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good.

@xezon xezon added this to the Major bug fixes milestone Mar 14, 2026
@xezon xezon changed the title fix(logic): Improve handling of ENABLE_RETALIATION_MODE in GameLogicDispatch and optimize the MSG fix(logic): Improve handling of ENABLE_RETALIATION_MODE in GameLogicDispatch Mar 14, 2026
@xezon xezon merged commit 1c077c5 into TheSuperHackers:main Mar 14, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Fix Is fixing something, but is not user facing Gen Relates to Generals Major Severity: Minor < Major < Critical < Blocker ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants