Skip to content

[GEN][ZH] Fix heap-use-after-free in W3DRadar::drawHeroIcon() #1035

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

xezon
Copy link

@xezon xezon commented Jun 9, 2025

When a Hero unit is killed, a heap-use-after-free is hit in W3DRadar::drawHeroIcon().

This happens because W3DRadar::m_cachedHeroPosList contains pointers to object positions, which are never cleaned up when the objects are destroyed.

To solve this problem, the cache Hero list has been reworked and tied to the established addObject and removeObject functions.

This fix also revealed a problem with the subsystems reset where cross dependencies can cause issues on GameEngine::reset. This is fixed as well.

I expect that this change fixes the user facing bug #1034. Not tested.

Asan Report

=================================================================
==18020==ERROR: AddressSanitizer: heap-use-after-free on address 0x50db3338 at pc 0x00d2c932 bp 0x018ff258 sp 0x018ff24c
READ of size 4 at 0x50db3338 thread T0
==18020==WARNING: Failed to use and restart external symbolizer!
    #0 0x00d2c931 in W3DRadar::drawHeroIcon D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngineDevice\Source\W3DDevice\Common\System\W3DRadar.cpp:262
    #1 0x00d2c363 in W3DRadar::draw D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngineDevice\Source\W3DDevice\Common\System\W3DRadar.cpp:1425
    #2 0x00e12275 in W3DLeftHUDDraw D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngineDevice\Source\W3DDevice\GameClient\GUI\GUICallbacks\W3DControlBar.cpp:89
    #3 0x007f5eb6 in GameWindowManager::drawWindow D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngine\Source\GameClient\GUI\GameWindowManager.cpp:1298
    #4 0x007f5fb7 in GameWindowManager::drawWindow D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngine\Source\GameClient\GUI\GameWindowManager.cpp:1313
    #5 0x007fd31d in GameWindowManager::winRepaint D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngine\Source\GameClient\GUI\GameWindowManager.cpp:1353
    #6 0x00de6ea9 in W3DInGameUI::draw D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngineDevice\Source\W3DDevice\GameClient\W3DInGameUI.cpp:439
    #7 0x00ddb5aa in W3DDisplay::draw D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngineDevice\Source\W3DDevice\GameClient\W3DDisplay.cpp:1902
    #8 0x007e11eb in GameClient::update D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngine\Source\GameClient\GameClient.cpp:766
    #9 0x006897df in GameEngine::update D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngine\Source\Common\GameEngine.cpp:746
    #10 0x00cb253d in Win32GameEngine::update D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngineDevice\Source\Win32Device\Common\Win32GameEngine.cpp:90
    #11 0x00686b5a in GameEngine::execute D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngine\Source\Common\GameEngine.cpp:822
    #12 0x00676e55 in GameMain D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngine\Source\Common\GameMain.cpp:44
    #13 0x00673759 in WinMain D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\Main\WinMain.cpp:986
    #14 0x010665c6 in __scrt_common_main_seh D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288
    #15 0x75e3fcc8 in BaseThreadInitThunk+0x18 (C:\WINDOWS\System32\KERNEL32.DLL+0x6b81fcc8)
    #16 0x76f582ad in RtlGetAppContainerNamedObjectPath+0x11d (C:\WINDOWS\SYSTEM32\ntdll.dll+0x4b2e82ad)
    #17 0x76f5827d in RtlGetAppContainerNamedObjectPath+0xed (C:\WINDOWS\SYSTEM32\ntdll.dll+0x4b2e827d)

0x50db3338 is located 56 bytes inside of 656-byte region [0x50db3300,0x50db3590)
freed by thread T0 here:
    #0 0x53aae6ed in free D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win.cpp:125
    #1 0x53ac081c in __asan::__RuntimeFunctions<__asan::Ucrtbase>::Free D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_win_runtime_functions.cpp:273
    #2 0x01065ca3 in operator delete D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_win_delete_scalar_size_thunk.cpp:44
    #3 0x0088cccb in Object::`scalar deleting destructor'+0x1b (C:\Generals\English\Command & Conquer Generals Zero Hour\generalszh.exe+0x63cccb)
    #4 0x0077c8a2 in GameLogic::processDestroyList D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngine\Source\GameLogic\System\GameLogic.cpp:2575
    #5 0x00782533 in GameLogic::update D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngine\Source\GameLogic\System\GameLogic.cpp:3831
    #6 0x006898e8 in GameEngine::update D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngine\Source\Common\GameEngine.cpp:761
    #7 0x00cb253d in Win32GameEngine::update D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngineDevice\Source\Win32Device\Common\Win32GameEngine.cpp:90
    #8 0x00686b5a in GameEngine::execute D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngine\Source\Common\GameEngine.cpp:822
    #9 0x00676e55 in GameMain D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngine\Source\Common\GameMain.cpp:44
    #10 0x00673759 in WinMain D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\Main\WinMain.cpp:986
    #11 0x010665c6 in __scrt_common_main_seh D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288
    #12 0x75e3fcc8 in BaseThreadInitThunk+0x18 (C:\WINDOWS\System32\KERNEL32.DLL+0x6b81fcc8)
    #13 0x76f582ad in RtlGetAppContainerNamedObjectPath+0x11d (C:\WINDOWS\SYSTEM32\ntdll.dll+0x4b2e82ad)
    #14 0x76f5827d in RtlGetAppContainerNamedObjectPath+0xed (C:\WINDOWS\SYSTEM32\ntdll.dll+0x4b2e827d)

previously allocated by thread T0 here:
    #0 0x53aae7ed in malloc D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win.cpp:134
    #1 0x0067434a in operator new D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngine\Source\Common\System\GameMemoryNull.cpp:158
    #2 0x007785cc in GameLogic::friend_createObject D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngine\Source\GameLogic\System\GameLogic.cpp:3998
    #3 0x007d3704 in ThingFactory::newObject D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngine\Source\Common\Thing\ThingFactory.cpp:329
    #4 0x00780a9d in GameLogic::startNewGame D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngine\Source\GameLogic\System\GameLogic.cpp:1919
    #5 0x00782085 in GameLogic::update D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngine\Source\GameLogic\System\GameLogic.cpp:3641
    #6 0x006898e8 in GameEngine::update D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngine\Source\Common\GameEngine.cpp:761
    #7 0x00cb253d in Win32GameEngine::update D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngineDevice\Source\Win32Device\Common\Win32GameEngine.cpp:90
    #8 0x00686b5a in GameEngine::execute D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngine\Source\Common\GameEngine.cpp:822
    #9 0x00676e55 in GameMain D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngine\Source\Common\GameMain.cpp:44
    #10 0x00673759 in WinMain D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\Main\WinMain.cpp:986
    #11 0x010665c6 in __scrt_common_main_seh D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288
    #12 0x75e3fcc8 in BaseThreadInitThunk+0x18 (C:\WINDOWS\System32\KERNEL32.DLL+0x6b81fcc8)
    #13 0x76f582ad in RtlGetAppContainerNamedObjectPath+0x11d (C:\WINDOWS\SYSTEM32\ntdll.dll+0x4b2e82ad)
    #14 0x76f5827d in RtlGetAppContainerNamedObjectPath+0xed (C:\WINDOWS\SYSTEM32\ntdll.dll+0x4b2e827d)

SUMMARY: AddressSanitizer: heap-use-after-free D:\Projects\TheSuperHackers\GeneralsGameCode\GeneralsMD\Code\GameEngineDevice\Source\W3DDevice\Common\System\W3DRadar.cpp:262 in W3DRadar::drawHeroIcon
Shadow bytes around the buggy address:
  0x50db3080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50db3100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50db3180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50db3200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50db3280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x50db3300: fd fd fd fd fd fd fd[fd]fd fd fd fd fd fd fd fd
  0x50db3380: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x50db3400: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x50db3480: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x50db3500: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x50db3580: fd fd fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
Address Sanitizer Error: Use of deallocated memory

TODO

  • Replicate in Generals

@xezon xezon added this to the Major bug fixes milestone Jun 9, 2025
@xezon xezon added Bug Something is not working right, typically is user facing Major Severity: Minor < Major < Critical < Blocker Gen Relates to Generals ZH Relates to Zero Hour Memory Is memory related labels Jun 9, 2025
// This avoids potentially catastrophic issues when objects and subsystems have cross dependencies.
TheGameLogic->reset();

TheSubsystemList->resetAll();
Copy link
Author

Choose a reason for hiding this comment

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

With this change, this resetAll crashed on map unload in

>	generalszh.exe!TunnelContain::iterateContained(void(*)(Object *, void *) func, void * userData, bool reverse) Line 191	C++
 	generalszh.exe!Object::isHero() Line 2044	C++
 	generalszh.exe!W3DRadar::addObject(Object * obj) Line 1003	C++
 	generalszh.exe!Player::becomingLocalPlayer(bool yes) Line 1138	C++
 	[Inline Frame] generalszh.exe!PlayerList::setLocalPlayer(Player *) Line 324	C++
 	generalszh.exe!PlayerList::init() Line 246	C++
 	generalszh.exe!PlayerList::reset() Line 127	C++
 	generalszh.exe!SubsystemInterfaceList::resetAll() Line 188	C++
 	[Inline Frame] generalszh.exe!GameLogic::isInMultiplayerGame() Line 418	C++
 	generalszh.exe!GameEngine::reset() Line 703	C++
 	generalszh.exe!GameLogic::clearGameData(bool showScoreScreen) Line 281	C++
 	generalszh.exe!GameLogic::logicMessageDispatcher(GameMessage * msg, void * userData) Line 464	C++
 	generalszh.exe!GameLogic::processCommandList(CommandList * list) Line 2597	C++
 	generalszh.exe!GameLogic::update() Line 3767	C++
 	[Inline Frame] generalszh.exe!SubsystemInterface::UPDATE() Line 134	C++
 	generalszh.exe!GameEngine::update() Line 766	C++
 	generalszh.exe!Win32GameEngine::update() Line 93	C++
 	generalszh.exe!GameEngine::execute() Line 823	C++
 	generalszh.exe!GameMain(int argc, char * * argv) Line 47	C++
 	generalszh.exe!WinMain(HINSTANCE__ * hInstance, HINSTANCE__ * hPrevInstance, char * lpCmdLine, int nCmdShow) Line 988	C++
 	[Inline Frame] generalszh.exe!invoke_main() Line 102	C++
 	generalszh.exe!__scrt_common_main_seh() Line 288	C++
 	kernel32.dll!75e3fcc9()	Unknown
 	[Frames below may be incorrect and/or missing, no symbols loaded for kernel32.dll]	
 	ntdll.dll!76f582ae()	Unknown
 	ntdll.dll!76f5827e()	Unknown
Exception thrown: read access violation.
**owningPlayer** was 0x150.

The problem is that the Object's TunnelContain module has a dependency on the player, but the player was already purged from the PlayerList. And so this would crash.

Resetting GameLogic first avoids this (and potentially similar issues in the future). Alternatively we could add more NULL checks in TunnelContain, but I think that is not necessary if Subsystem just have a proper reset sequence.

@@ -118,7 +121,7 @@ class W3DRadar : public Radar
Real m_viewZoom; ///< camera zoom used for the view box we have
ICoord2D m_viewBox[ 4 ]; ///< radar cell points for the 4 corners of view box

std::list<const Coord3D *> m_cachedHeroPosList; //< cache of hero positions for drawing icons in radar overlay
std::vector<const Object *> m_cachedHeroObjectList; //< cache of hero objects for drawing icons in radar overlay
Copy link
Author

Choose a reason for hiding this comment

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

I changed the container type here because list of coordinate pointers is not particularly great.

Copy link

@Caball009 Caball009 Jun 9, 2025

Choose a reason for hiding this comment

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

Wouldn't it be better to change to std::vector<Coord2D>? As far as I can tell, the z coordinate is irrelevant for the radar and the hero coordinates are updated each frame, so using a pointer is not an advantage when the size of 2D coordinates is 2 * sizeof(float).

Changes:
void W3DRadar::drawHeroIcon( Int pixelX, Int pixelY, Int width, Int height, const Coord3D *pos )
->
void W3DRadar::drawHeroIcon( Int pixelX, Int pixelY, Int width, Int height, Coord2D pos )

m_cachedHeroPosList.push_back(obj->getPosition());
->
m_cachedHeroPosList.push_back(Coord2D(obj->getPosition()->x, obj->getPosition()->y));

Copy link
Author

Choose a reason for hiding this comment

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

The position changes when the Hero moves. Putting a fixed position into the vector would paint the radar icon at a static position.

Choose a reason for hiding this comment

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

I don't think it would. I just tried it and the position is not fixed on the radar. It wouldn't be if the position is updated each frame; i.e. if W3DRadar::renderObjectList is called for each frame.

Copy link
Author

Choose a reason for hiding this comment

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

I suspect you test with calcHero code? The calcHero rebuilds the vector every now and then. This change does not.

Choose a reason for hiding this comment

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

Right, sorry.

@xezon xezon modified the milestones: Major bug fixes, Stability fixes Jun 9, 2025
@xezon xezon added Stability Concerns stability of the runtime and removed Memory Is memory related labels Jun 9, 2025
@Caball009
Copy link

The stl namespace helper functions are a nice addition. I find myself wishing for C++98 support for lambdas because that would make it very easy to write two more helper functions that erase all elements, based on any sort of predicate.

@xezon xezon requested a review from a team June 13, 2025 10:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug Something is not working right, typically is user facing Gen Relates to Generals Major Severity: Minor < Major < Critical < Blocker Stability Concerns stability of the runtime ZH Relates to Zero Hour
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Sometimes the Radar shows wrong Hero position markers
2 participants