Skip to content

bugfix(worldbuilder): Handle null boundary lookup output - #3383

Draft
OmarAglan wants to merge 2 commits into
TheSuperHackers:mainfrom
OmarAglan:bugfix/worldbuilder-null-boundary-handle
Draft

OmarAglan wants to merge 2 commits into
TheSuperHackers:mainfrom
OmarAglan:bugfix/worldbuilder-null-boundary-handle

Conversation

@OmarAglan

@OmarAglan OmarAglan commented Sep 28, 2026 •

Copy link
Copy Markdown

Applies the same fix to the Generals and Zero Hour copies of WHeightMapEdit.cpp in their existing directories.

findBoundaryNear() permits a null outHandle, but its no-match path dereferences it unconditionally. This can crash a lookup when no nearby boundary exists.

Guard the final handle assignment, matching the existing checks on successful lookups. The function still sets outNdx to -1 when no boundary matches.

Validation:

  • Reproduced the original null-pointer crash using the extracted production function.
  • Passed 18 regression assertions for each game copy, covering empty and nonmatching boundaries, all four corners, distance thresholds and null inputs.
  • Built both Generals and Zero Hour WorldBuilder targets with VC6.

This branch contains one fix commit above #3368 and should be rebased onto main after that PR merges.

Codex generated the fix and local regression fixtures

@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 260b183a-dc2e-4a60-9ed4-df68a662ebac

📥 Commits

Reviewing files that changed from the base of the PR and between 41e4e15 and 16fe4d2.

📒 Files selected for processing (1)
  • Generals/Code/Tools/WorldBuilder/include/WHeightMapEdit.h

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 0 remain after this review.


Walkthrough

WorldBuilder changes flip-state sizing and blend-tile serialization, adjusts image and blend handling, removes the raw tile data API, and updates boundary editing and lookup.

Changes

Height-Map Data Handling

Layer / File(s) Summary
Size and serialize flip state
Generals/Code/Tools/WorldBuilder/src/WHeightMapEdit.cpp, GeneralsMD/Code/Tools/WorldBuilder/src/WHeightMapEdit.cpp
Flip-state row allocation uses rounded-up byte widths. Blend-tile output selects version 7 for the specified Generals retail build and version 8 otherwise, with corresponding cliff-state serialization.
Update blend, image, and raw-tile handling
Generals/Code/Tools/WorldBuilder/include/WHeightMapEdit.h, Generals/Code/Tools/WorldBuilder/src/WHeightMapEdit.cpp
Image loading copies the filename into the file buffer. Blend flipping uses a copied blend description. The getRawTileData declaration and implementation are removed. Comments and include spacing are also edited.

Boundary Editing and Lookup

Layer / File(s) Summary
Handle boundary selection and insertion
Generals/Code/Tools/WorldBuilder/src/BorderTool.cpp, Generals/Code/Tools/WorldBuilder/src/WHeightMapEdit.cpp, GeneralsMD/Code/Tools/WorldBuilder/src/WHeightMapEdit.cpp
BorderTool adds a boundary when no nearby boundary is found. findBoundaryNear sets the no-match handle only when the output pointer is non-null.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Merge Risk: 🟡 Moderate · up to 16fe4

Terrain blending can leave an invalid index when the blend table fills, risking unsafe memory access during later editing. Guard the failed remap before merging; the API-removal concern is resolved.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 16fe4

The terrain-blending change can store a failed allocation result as a valid blend index, exposing later editor operations to out-of-bounds memory access. The demonstrated scope is local WorldBuilder editing. The shared map loader repairs invalid indices, which limits propagation through saved maps.

Retained concerns

  • Medium · security · observed: The new copied-primary blend transition commits the secondary blend before checking whether replacement of the primary succeeds. Blend-table exhaustion can therefore store -1 as the primary identity, which later editor consumers use as an array index. This violates the shared per-cell index invariant and defeats failure containment within the editing lifecycle.
Security review details

Security Blast Radius

  • inferred — The demonstrated attackable scope requires control over local terrain-editing operations with three-way blending enabled. The affected assets are the live Generals WorldBuilder heightmap and potentially its saved blend data. Remote execution, privilege gain, and cross-service exposure are not established; the inspected shared loader repairs invalid indices.

Security Findings and Attack Paths

  • inferred — If a secondary blend succeeds but the required flipped-primary descriptor cannot be allocated, the changed transition stores -1 as the primary index. A subsequent blend accepts that nonzero value and reads outside m_blendedTiles. Blend-table exhaustion is an explicit failure condition; an executable trigger sequence and exploit consequences were not dynamically demonstrated.

Trust Boundaries and Controls

  • observed — The inspected persistence boundary has two distinct controls: version-aware row decoding preserves format compatibility, and post-parse index normalization protects loaded maps. Neither control protects the live editor immediately after the failed remapping transition.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: preventing a null-pointer dereference in the WorldBuilder boundary lookup.
Description check ✅ Passed The description directly explains the null-pointer fix, affected game copies, validation results, dependency, and rebase requirement. It is fully related to the changeset.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: b7d8f3e0-b520-425e-9af7-399518604f30

📥 Commits

Reviewing files that changed from the base of the PR and between b687c07 and 16019dd.

📒 Files selected for processing (13)
  • Core/Tools/CMakeLists.txt
  • Core/Tools/WorldBuilder/CMakeLists.txt
  • Core/Tools/WorldBuilder/include/BorderTool.h
  • Core/Tools/WorldBuilder/include/WHeightMapEdit.h
  • Core/Tools/WorldBuilder/src/BorderTool.cpp
  • Core/Tools/WorldBuilder/src/WHeightMapEdit.cpp
  • Generals/Code/Tools/WorldBuilder/CMakeLists.txt
  • Generals/Code/Tools/WorldBuilder/include/BorderTool.h
  • Generals/Code/Tools/WorldBuilder/include/WHeightMapEdit.h
  • Generals/Code/Tools/WorldBuilder/src/BorderTool.cpp
  • Generals/Code/Tools/WorldBuilder/src/WHeightMapEdit.cpp
  • GeneralsMD/Code/Tools/WorldBuilder/CMakeLists.txt
  • scripts/cpp/unify_move_files.py
💤 Files with no reviewable changes (3)
  • Generals/Code/Tools/WorldBuilder/include/BorderTool.h
  • Generals/Code/Tools/WorldBuilder/include/WHeightMapEdit.h
  • Generals/Code/Tools/WorldBuilder/src/BorderTool.cpp

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Comment on lines +614 to +618
if (blendTileVersion <= K_BLEND_TILE_VERSION_7) {
// Version 7 uses the legacy row width, even when the in-memory rows are wider.
Int byteWidth = (m_width+1)/8;
for (Int j=0; j<m_height; j++) {
chunkWriter.writeArrayOfBytes((char*)m_cellCliffState + j*m_flipStateWidth, byteWidth);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

rg -n 'setWidth|setDimensions|m_width[[:space:]]*=|mapWidth|heightMapWidth|resize|newMap|createMap|RETAIL_COMPATIBLE_DATA' Core/Tools/WorldBuilder Core/GameEngineDevice/Source/W3DDevice Generals/Code/Tools/WorldBuilder
rg -n 'width.*(multiple|%|/|64|128)|m_width.*(8|16|32|64|128)' Core/Tools/WorldBuilder Core/GameEngineDevice/Source/W3DDevice/GameClient

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 26448


🏁 Script executed:

set -e
printf '%s\n' '--- WHeightMapEdit declarations and construction ---'
sed -n '80,235p' Core/Tools/WorldBuilder/src/WHeightMapEdit.cpp
sed -n '110,175p' Core/Tools/WorldBuilder/include/WHeightMapEdit.h
printf '%s\n' '--- resize implementation ---'
sed -n '1760,1910p' Core/Tools/WorldBuilder/src/WHeightMapEdit.cpp
printf '%s\n' '--- WorldBuilderDoc map creation/loading entrypoints ---'
sed -n '960,1070p' Core/Tools/WorldBuilder/src/WorldBuilderDoc.cpp
rg -n -C 5 'new WorldHeightMapEdit|WorldHeightMapEdit\(|resize\(|initialHeight|xExtent|yExtent|borderWidth' Core/Tools/WorldBuilder/src Core/Tools/WorldBuilder/include
printf '%s\n' '--- dimension validation and format readers ---'
rg -n -C 6 'xSize|ySize|xExtent|yExtent|width.*8|width.*64|MAP_SIZE|map size|MapSize|newWidth|newHeight' Core/Tools/WorldBuilder/src Core/Tools/WorldBuilder/include Core/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 11905


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- WorldBuilder document creation/loading and resize callers ---'
rg -n -C 8 'WorldHeightMapEdit|resize\(|xExtent|yExtent|initialHeight|borderWidth|NewHeightMap|load' Generals/Code/Tools/WorldBuilder/src/WorldBuilderDoc.cpp Generals/Code/Tools/WorldBuilder/src Generals/Code/Tools/WorldBuilder/include
printf '%s\n' '--- NewHeightMap declarations and implementation ---'
fd -i 'NewHeightMap' Generals/Code/Tools/WorldBuilder
printf '%s\n' '--- WorldHeightMap loading dimension handling ---'
sed -n '820,985p' Core/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp
sed -n '1025,1090p' Core/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp
printf '%s\n' '--- all dimension-related guards in relevant WorldBuilder files ---'
rg -n -C 4 'BAD_VALUE|<= *0|< *0|% *8|/ *8|multiple|extent|size' Generals/Code/Tools/WorldBuilder/src/WorldBuilderDoc.cpp Generals/Code/Tools/WorldBuilder/src Generals/Code/Tools/WorldBuilder/include Core/Tools/WorldBuilder/src/WHeightMapEdit.cpp Core/Tools/WorldBuilder/include/WHeightMapEdit.h

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 45686


🏁 Script executed:

set -e
printf '%s\n' '--- NewHeightMap files ---'
fd -i 'NewHeightMap' Generals/Code/Tools/WorldBuilder Core/Tools/WorldBuilder
printf '%s\n' '--- NewHeightMap validation ---'
rg -n -C 10 'OnInitDialog|OnOK|DDX|xExtent|yExtent|borderWidth|forResize|SetLimitText|GetDlgItem|BAD_VALUE|<=|< *0|% *8|/ *8' Generals/Code/Tools/WorldBuilder/src/NewHeightMap.cpp Generals/Code/Tools/WorldBuilder/include/NewHeightMap.h Core/Tools/WorldBuilder/src/NewHeightMap.cpp Core/Tools/WorldBuilder/include/NewHeightMap.h 2>/dev/null || true
printf '%s\n' '--- WorldHeightMap constructors and readers ---'
rg -n -C 10 'WorldHeightMap::WorldHeightMap|readInt\(\)|K_BLEND_TILE_VERSION|numBytesX|byteWidth|m_width|m_height' Core/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp Core/GameEngineDevice/Include/W3DDevice/GameClient/WorldHeightMap.h

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 41736


🏁 Script executed:

set -e
printf '%s\n' '--- save function and changed hunk ---'
sed -n '560,635p' Core/Tools/WorldBuilder/src/WHeightMapEdit.cpp
rg -n -C 10 'saveToFile\(|SaveMap|OnSaveDocument|OnFileSave|write.*height|heightMap.*save' Core/Tools/WorldBuilder/src/WHeightMapEdit.cpp Generals/Code/Tools/WorldBuilder/src/WorldBuilderDoc.cpp Generals/Code/Tools/WorldBuilder/src/SaveMap.cpp Generals/Code/Tools/WorldBuilder/include/SaveMap.h 2>/dev/null || true
printf '%s\n' '--- exact parsing and flag readers ---'
rg -n -C 12 'ParseHeightMapData|ParseSizeOnly|m_dataSize.*m_width|byteWidth|numBytesX' Core/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 41363


🏁 Script executed:

set -e
sed -n '250,320p' Generals/Code/Tools/WorldBuilder/src/WorldBuilderDoc.cpp
sed -n '780,855p' Generals/Code/Tools/WorldBuilder/src/WorldBuilderDoc.cpp
rg -n -C 6 'throw ERROR|ERROR_CORRUPT_FILE_FORMAT|height map file write failed|saveToFile' Core/Tools/WorldBuilder/src Generals/Code/Tools/WorldBuilder/src

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 12421


🏁 Script executed:

set -e
rg -n -C 12 'setCliffState|m_cellCliffState\[|setCellCliffFlagFromHeights|initCliffFlagsFromHeights' Core/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp Core/GameEngineDevice/Include/W3DDevice/GameClient/WorldHeightMap.h Core/Tools/WorldBuilder/src/WHeightMapEdit.cpp Core/Tools/WorldBuilder/include/WHeightMapEdit.h

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 28808


Reject version-7 saves when the omitted cliff-state byte is non-zero.

WorldBuilder can resize maps to widths where version 7 writes fewer cliff-state bytes than the in-memory row. Normal cliff recalculation can set bits in that omitted byte. The retail version-7 reader does not read it, so reopening the saved map clears those bits.

Version 8 preserves the bits but violates the retail compatibility contract. Check the omitted byte before writing any chunks and reject the save when it contains data.

Suggested fix
 void WorldHeightMapEdit::saveToFile(DataChunkOutput &chunkWriter)
 {
+	#if RTS_GENERALS && RETAIL_COMPATIBLE_DATA
+		Int legacyByteWidth = (m_width+1)/8;
+		if (legacyByteWidth < m_flipStateWidth) {
+			for (Int j=0; j<m_height; j++) {
+				if (m_cellCliffState[j*m_flipStateWidth + legacyByteWidth] != 0) {
+					throw(ERROR_CORRUPT_FILE_FORMAT);
+				}
+			}
+		}
+	#endif
+
 	// This is the chunk writer stuff.
 	int i;

@OmarAglan
OmarAglan force-pushed the bugfix/worldbuilder-null-boundary-handle branch from 16019dd to 41e4e15 Compare September 30, 2026 08:25

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: e8f4db5c-e63f-4bae-add6-3392b599ac21

📥 Commits

Reviewing files that changed from the base of the PR and between 16019dd and 41e4e15.

📒 Files selected for processing (4)
  • Generals/Code/Tools/WorldBuilder/include/WHeightMapEdit.h
  • Generals/Code/Tools/WorldBuilder/src/BorderTool.cpp
  • Generals/Code/Tools/WorldBuilder/src/WHeightMapEdit.cpp
  • GeneralsMD/Code/Tools/WorldBuilder/src/WHeightMapEdit.cpp

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.

Comment on lines +1194 to +1195
Short newNdx = findOrCreateBlendTile(&tempBlendTileInfo);
m_blendTileNdxes[ndx] = newNdx; //remap this tile to use a new one.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

ast-grep outline Generals/Code/Tools/WorldBuilder/src/WHeightMapEdit.cpp \
  --match findOrCreateBlendTile --view expanded
ast-grep run --lang cpp \
  --pattern 'Int WorldHeightMapEdit::findOrCreateBlendTile(TBlendTileInfo *$INFO) { $$$BODY }' \
  Generals/Code/Tools/WorldBuilder/src/WHeightMapEdit.cpp

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 306


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '--- helper ---'
sed -n '1040,1115p' Generals/Code/Tools/WorldBuilder/src/WHeightMapEdit.cpp
printf '%s\n' '--- changed blend block ---'
sed -n '1145,1215p' Generals/Code/Tools/WorldBuilder/src/WHeightMapEdit.cpp
printf '%s\n' '--- consumers ---'
rg -n -C 5 'm_blendTileNdxes|autoBlendOut|optimizeTiles' Generals/Code/Tools/WorldBuilder/src/WHeightMapEdit.cpp
printf '%s\n' '--- requested diff ---'
git diff --unified=35 e70f26db9f0dec4e23f2443921b5341e04958a5f 41e4e15b934fd0d37a60f3bb82c80a161349e1d0 -- Generals/Code/Tools/WorldBuilder/src/WHeightMapEdit.cpp

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 41942


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '--- autoBlendOut relevant range ---'
sed -n '1210,1360p' Generals/Code/Tools/WorldBuilder/src/WHeightMapEdit.cpp
printf '%s\n' '--- optimizeTiles relevant range ---'
sed -n '1660,1765p' Generals/Code/Tools/WorldBuilder/src/WHeightMapEdit.cpp

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 8401


Handle failure before replacing the primary blend index.

findOrCreateBlendTile can return -1 when the blend table is full. The second allocation stores that value in m_blendTileNdxes[ndx] without a check. optimizeTiles and later blend operations can then use -1 as an array index.

Allocate the flipped description before committing the cell indices. If it fails, preserve the previous cell state.

Suggested fix
 	if (newNdx >= 0) {
 		Int ndx = (yIndex*m_width)+xIndex;
-		m_tileNdxes[ndx] = curTileNdx;
-		if (TheGlobalData->m_use3WayTerrainBlends && m_blendTileNdxes[ndx] != 0)
+		Bool hasBaseBlend = TheGlobalData->m_use3WayTerrainBlends &&
+			m_blendTileNdxes[ndx] != 0;
+		Short primaryNdx = hasBaseBlend ? m_blendTileNdxes[ndx] : newNdx;
+		if (hasBaseBlend)
 		{
 			//this tile already has a blend applied to it.  So we put the new blend into the
 			//secondary layer.
-			m_extraBlendTileNdxes[ndx]=newNdx;
 			//force the primary layer to flip if the extra blend layer needs flip.
 			//we only do this on vertical/horizontal base blends because they work in either flip cases.
 			if (flipped && !baseIsDiagonal)
 			{
 				//Find a new tile so as not to affect other cells using the base one.
 				TBlendTileInfo tempBlendTileInfo=m_blendedTiles[m_blendTileNdxes[ndx]];
 				tempBlendTileInfo.inverted |= FLIPPED_MASK;
-				Short newNdx = findOrCreateBlendTile(&tempBlendTileInfo);
-				m_blendTileNdxes[ndx] = newNdx;	//remap this tile to use a new one.
+				Short flippedNdx = findOrCreateBlendTile(&tempBlendTileInfo);
+				if (flippedNdx < 0)
+					return;
+				primaryNdx = flippedNdx;
 			}
-		}
-		else
-			m_blendTileNdxes[ndx] = newNdx;
+		}
+		m_tileNdxes[ndx] = curTileNdx;
+		if (hasBaseBlend)
+			m_extraBlendTileNdxes[ndx] = newNdx;
+		m_blendTileNdxes[ndx] = primaryNdx;
 	}

Align WorldHeightMapEdit and BorderTool with Zero Hour. Preserve Generals BlendTileData v7 row packing under RETAIL_COMPATIBLE_DATA, adopt the wider in-memory flag rows, and inherit the shared raw-tile accessor.
@OmarAglan
OmarAglan force-pushed the bugfix/worldbuilder-null-boundary-handle branch from 41e4e15 to 16fe4d2 Compare September 30, 2026 08:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant