Skip to content

fix(gamefont): Ceil font glyph buffer size to the actual glyph size to prevent a buffer write overflow - #3268

Merged
xezon merged 2 commits into
TheSuperHackers:mainfrom
tintinhamans:arctic/fix/font-glyph-buffer-overflow
Sep 12, 2026
Merged

xezon merged 2 commits into
TheSuperHackers:mainfrom
tintinhamans:arctic/fix/font-glyph-buffer-overflow

Conversation

@tintinhamans

@tintinhamans tintinhamans commented Sep 7, 2026

Copy link
Copy Markdown

The glyph buffer was a fixed uint16[32768]. Store_GDI_Char writes
width * height pixels with no bound check, so a big glyph writes past the end.

A pointSize > 100 cap used to hide this, but it was removed in #3051 so 4K UI scaling
can use bigger fonts. A scaled font can now hit the overflow.

  • FontCharsBuffer allocates its pixels and grows to fit the glyph.
  • getFont clamps requests to 512 instead of rejecting them, so an oversized
    request still returns a usable font rather than nullptr (callers like
    W3DDisplayString::setFont ignore null and would show no text).

A font size of about 460 is the most any real screen needs (a 48pt heading blown up 9.6x on an 8K display) so I think 512 is a safe value for now.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 7, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review Completed 2026-09-07T21:42:55.605071Z dbebfc4 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Prevent large glyphs from overflowing font buffers

🐞 Bug fix 🕐 10-20 Minutes

Grey Divider

AI Description

• Prevents large GDI glyphs from overflowing fixed pixel buffers.
• Allocates each backing buffer to fit its triggering glyph.
• Rejects font requests outside the supported 1–512 point range.
Diagram

graph TD
    A["Font request"] --> B{"Size valid?"} -->|No| C["Reject request"]
    B -->|Yes| D["GDI glyph"] --> E{"Buffer fits?"} -->|Yes| F["Store pixels"]
    E -->|No| G["Allocate buffer"] --> F
Loading
High-Level Assessment

The proposed defense-in-depth approach is appropriate: validate externally supplied font sizes and independently size internal storage from measured glyph dimensions. Retaining pooled default buffers for ordinary glyphs avoids unnecessary allocations, while oversized glyphs receive dedicated capacity using the engine's existing allocation conventions.

Files changed (3) +28 / -7

Bug fix (3) +28 / -7
GameFont.cppCap accepted font sizes at 512 points +2/-2

Cap accepted font sizes at 512 points

• Extends font request validation to reject sizes above 512 points while continuing to reject zero and negative sizes. This limits content-driven memory usage without restoring the previous 100-point restriction that blocked high-DPI scaling.

Core/GameEngine/Source/GameClient/GUI/GameFont.cpp

render2dsentence.cppAllocate glyph buffers according to required capacity +19/-3

Allocate glyph buffers according to required capacity

• Adds allocation and cleanup for dynamically sized pixel arrays. Buffer selection now checks each buffer's actual capacity and allocates at least the glyph's width-by-height pixel count, preventing large glyph writes from exceeding the former fixed allocation.

Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp

render2dsentence.hTrack dynamic font buffer storage and length +7/-2

Track dynamic font buffer storage and length

• Replaces the fixed 32,768-element pixel array with an owned pointer and explicit capacity. Declares lifecycle methods and retains the original constant as the default allocation size for ordinary glyphs.

Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 7, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Oversized interface text disappears 🐞 Bug ≡ Correctness
Description
FontLibrary::getFont returns nullptr rather than clamping requests above 512, while
display-string consumers silently ignore a null font. When resolution scaling pushes configured
credits, tooltip, or header fonts over the threshold, new strings retain no font and compute zero
extents, while existing strings can retain a stale font.
Code

Core/GameEngine/Source/GameClient/GUI/GameFont.cpp[R182-184]

+	if (pointSize < 1 || pointSize > 512)
  {
  	return nullptr;
Evidence
Resolution scaling can produce a size above 512, and credits pass that value directly to getFont.
A null result is ignored by W3DDisplayString::setFont; after text changes, computeExtents
explicitly assigns zero dimensions when no font is installed, demonstrating the missing-text
outcome.

Core/GameEngine/Source/GameClient/GlobalLanguage.cpp[278-286]
Core/GameEngine/Source/GameClient/Credits.cpp[243-247]
Core/GameEngine/Source/GameClient/GUI/HeaderTemplate.cpp[217-227]
Core/GameEngine/Source/GameClient/Input/Mouse.cpp[621-635]
Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp[292-314]
Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp[356-375]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Font requests above 512 are rejected with `nullptr`, but callers generally expect a usable font and may consequently display no text. Preserve the allocation limit by clamping positive oversized requests to 512 while continuing to reject invalid non-positive sizes.
## Issue Context
DPI-adjusted font sizes flow directly into `getFont`, and display-string font assignment ignores null values. Clamping enforces the intended maximum glyph allocation without requiring every font consumer to implement fallback behavior.
## Fix Focus Areas
- Core/GameEngine/Source/GameClient/GUI/GameFont.cpp[179-185]
- Core/GameEngine/Source/GameClient/GlobalLanguage.cpp[278-286]
- Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp[292-314]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can hide the parts of a finding you never read, like the evidence or the agent prompt

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread Core/GameEngine/Source/GameClient/GUI/GameFont.cpp Outdated
@greptile-apps

greptile-apps Bot commented Sep 7, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces the fixed-size glyph pixel storage with dynamically allocated buffers sized to accommodate each glyph and caps font requests at 512 points.

  • Tracks each glyph buffer’s allocated length and uses it when deciding whether another buffer is required.
  • Releases dynamically allocated pixel arrays with matching array deletion.
  • Clamps oversized requests at the font-library boundary so callers still receive a usable font.

Confidence Score: 5/5

The PR appears safe to merge; the dynamic allocation and matching cleanup address the fixed-buffer overflow without leaving a new actionable failure.

The font library consistently enforces the 512-point limit, while glyph storage allocates at least width × height pixels and checks actual buffer capacity before writing. Pixel arrays are released exactly once through the owning FontCharsClass lifecycle.

Important Files Changed

Filename Overview
Core/GameEngine/Source/GameClient/GUI/GameFont.cpp Clamps positive font requests to 512 points at the common font-creation boundary.
Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp Allocates glyph buffers according to required capacity, checks their recorded lengths, and releases their pixel arrays correctly.
Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h Replaces the fixed pixel array with a value descriptor containing allocated length and pixel pointer.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    Request[Font request] --> Clamp[Clamp point size to 512]
    Clamp --> Glyph[Rasterize glyph]
    Glyph --> Size[Compute width × height]
    Size --> Capacity{Current buffer has capacity?}
    Capacity -->|Yes| Store[Store glyph pixels]
    Capacity -->|No| Allocate[Allocate max of default capacity and glyph size]
    Allocate --> Store
    Store --> Destroy[Font destruction]
    Destroy --> Free[Delete each pixel array]
Loading

Reviews (6): Last reviewed commit: "refactor(gamefont): Store glyph buffer d..." | Re-trigger Greptile

@tintinhamans
tintinhamans force-pushed the arctic/fix/font-glyph-buffer-overflow branch from dbebfc4 to 33b6309 Compare September 7, 2026 21:49
@stephanmeesters

Copy link
Copy Markdown

Can't we use std::vector<uint16> Buffer?

At what font size did it overflow the original buffer? Was this leading to crashes?

@tintinhamans

tintinhamans commented Sep 7, 2026

Copy link
Copy Markdown
Author

At what font size did it overflow the original buffer? Was this leading to crashes?

Theoretically somewhere around 130 to 150 point, can also be triggered by something like a custom map with DISPLAY_CINEMATIC_TEXT so might have some security implications.

Can't we use std::vector Buffer?

Each glyph caches a raw pointer into its slab (char_data->Buffer = slab->Buffer + CurrPixelOffset), idk how I'd handle that in a vector.

@xezon xezon 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.

Needs fixing the slop.

Comment thread Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h Outdated
Comment thread Core/GameEngine/Source/GameClient/GUI/GameFont.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h Outdated
@xezon xezon added Minor Severity: Minor < Major < Critical < Blocker Gen Relates to Generals ZH Relates to Zero Hour Fix Is fixing something, but is not user facing labels Sep 8, 2026
@OmarAglan

Copy link
Copy Markdown

Do this effect PR #3231 ?

@tintinhamans
tintinhamans force-pushed the arctic/fix/font-glyph-buffer-overflow branch 2 times, most recently from eae556c to cb3ed94 Compare September 8, 2026 14:58
@tintinhamans
tintinhamans requested a review from xezon September 9, 2026 00:56
Comment thread Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h Outdated
Comment thread Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h Outdated
Comment thread Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h Outdated
Comment thread Core/GameEngine/Source/GameClient/GlobalLanguage.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp Outdated
@tintinhamans
tintinhamans force-pushed the arctic/fix/font-glyph-buffer-overflow branch from 1ce2f06 to 099cb78 Compare September 10, 2026 20:01
@tintinhamans
tintinhamans requested a review from xezon September 10, 2026 20:04

@xezon xezon 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.

Looks safe.

@xezon xezon changed the title fix(gamefont): Size glyph buffers to the glyph to prevent an overflow fix(gamefont): Size glyph buffers to the actual maximum glyph size to prevent buffer write overflows Sep 12, 2026
@xezon xezon changed the title fix(gamefont): Size glyph buffers to the actual maximum glyph size to prevent buffer write overflows fix(gamefont): Ceil font glyph buffer size to the actual glyph size to prevent a buffer write overflow Sep 12, 2026
@xezon
xezon merged commit b57adac into TheSuperHackers:main Sep 12, 2026
23 checks passed
@tintinhamans
tintinhamans deleted the arctic/fix/font-glyph-buffer-overflow branch September 12, 2026 12:07
fbraz3 added a commit to fbraz3/GeneralsX that referenced this pull request Sep 16, 2026
* bugfix(gamewindow): Remove destroyed windows from the modal stack and prevent duplicate modals for the same window (TheSuperHackers#3224)

* feat(commandline): Add working directory command line options (TheSuperHackers#3149)

Append -useCwd to apply the startup working directory, -setCwd "path" to apply a custom working directory, otherwise it falls back to the default executable working directory

* bugfix(neutronmissile): Fix and improve Nuke Missile damage for large objects inside the outer blast radius (TheSuperHackers#3161)

* bugfix(dozeraiupdate): Fix issue where builders could resume completed tasks after being disabled (TheSuperHackers#2793)

* refactor(milesaudiomanager): Use consistent variable names for PlayingAudio in MilesAudioManager (TheSuperHackers#3254)

* refactor(milesaudiomanager): Simplify MilesAudioManager::notifyOfAudioCompletion() (TheSuperHackers#3254)

* refactor(milesaudiomanager): Simplify MilesAudioManager::findLowestPrioritySound() (TheSuperHackers#3254)

* bugfix(milesaudiomanager): Fix premature 2d and 3d sound cancellations from MilesAudioManager::stopAudioEvent() (TheSuperHackers#3254)

* bugfix(milesaudiomanager): No longer use stopped audio in queries and updates (TheSuperHackers#3254)

* refactor(bink): Replace the Bink SDK stub with a Bink runtime loader (TheSuperHackers#3272)

The Bink SDK stub was linked as an import library, so binkw32.dll had to be
resolvable while the process image was still loading, long before WinMain and
therefore long before the command line was parsed. That is why -setCwd could not
point a build at a retail installation: the working directory it selects is set
far too late to influence how the library is found.

BinkLoader loads binkw32.dll explicitly once BinkVideoPlayer is initialized, at
which point the working directory is final. The Bink functions declared in bink.h
are now ordinary functions that forward to the matching export of the loaded
module, so no call site changes. An unresolved function returns the same neutral
value the stub library returned, which means a missing binkw32.dll disables video
playback instead of preventing the game from starting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(miles): Replace the Miles SDK stub with a Miles runtime loader (TheSuperHackers#3272)

The Miles SDK stub was linked as an import library, so mss32.dll had to be
resolvable while the process image was still loading, long before WinMain and
therefore long before the command line was parsed. That is why -setCwd could not
point a build at a retail installation: the working directory it selects is set
far too late to influence how the library is found.

MilesLoader loads mss32.dll explicitly once the audio device is opened, at which
point the working directory is final. The Miles functions declared in mss/mss.h
are now ordinary functions that forward to the matching export of the loaded
module, so no call site changes. An unresolved function returns the same neutral
value the stub library returned, which means a missing mss32.dll turns audio off
instead of preventing the game from starting.

Nine declarations were dropped along the way, because the retail mss32.dll does
not export them and nothing has called them since they were replaced by their
volume_pan counterparts: AIL_sample_volume, AIL_set_sample_volume, AIL_sample_pan,
AIL_set_sample_pan and the four stream equivalents, plus AIL_open_stream_by_sample.
The MSS_auto_cleanup hook was dropped as well, because its atexit handler would
have called AIL_shutdown after the module was already freed. All 92 remaining
exports were verified to resolve against the retail mss32.dll.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(miles): Fix the written primitive types in mss.h and all its call sites; no ABI changes (TheSuperHackers#3272)

* perf(productionupdate): Simplify and correct implementations of cancel functions in ProductionUpdate (TheSuperHackers#3270)

* chore(gamememory): Compile out the memory link tester in Release (TheSuperHackers#3266)

* perf(gamememory): Early exit delete and free functions on null pointer (TheSuperHackers#3266)

* perf(gamememory): Inline preMainInitMemoryManager (TheSuperHackers#3266)

* perf(gamememory): Add overloads for the deletes with size_t argument (TheSuperHackers#3266)

* chore(gamememory): Remove superfluous extern keywords from operator overloads (TheSuperHackers#3266)

* perf(gamememory): Remove unnecessary calls to preMainInitMemoryManager from delete and free functions and make freeBytes noexcept to get rid of EH frame (TheSuperHackers#3266)

* fix(gamefont): Ceil font glyph buffer size to the actual glyph size to prevent a buffer write overflow (TheSuperHackers#3268)

* refactor(particlesys): Parse IsGroundAligned as an enum instead of a boolean (TheSuperHackers#3265)

* ci(release): Stop requesting permissions from the reusable workflow (TheSuperHackers#3276)

* refactor(basetype): Add utility functions to Region and Coord types (TheSuperHackers#3271)

New functions are:
intersectWith, uniteWith for IRegion3D, IRegion2D, Region3D, Region2D
updateMin, updateMax for ICoord3D, ICoord2D, Coord3D, Coord2D
asICoord2D, asCoord2D for ICoord3D, Coord3D

* build(cmake): Add retail compatibility option in CMake config (TheSuperHackers#2379)

RTS_BUILD_OPTION_RETAIL_COMPATIBLE_GAME=DEFAULT/ON/OFF

* bugfix(meshmatdesc): Fix mesh material color processing (TheSuperHackers#3246)

* ci: Restore CI workflow permission compatibility (TheSuperHackers#3286)

* chore: Remove trailing commas in braced initializers that break clang-format's compact layout (TheSuperHackers#3274)

Scoped to comment-free array/struct literals (BorderColors, TeamGeneric,
BezierSegment, GameMemoryInitPools, BFISH, Properties, Scripts) where
clang-format explodes each element onto its own line without this.

* chore(license): Add SPDX-License-Identifier to LICENSE.md (TheSuperHackers#3290)

Helps github detect the license version

* bugfix(pathfinder): Restore Generals retail compatibility after crash fix changes to Pathfinder::findAttackPath (TheSuperHackers#3289)

* feat(recorder): Play a replay file from the command line (TheSuperHackers#3227)

Use -loadreplay <file> as a command line argument to load the replay with full game context

* fix(audio): Copy SoundSceneObjClass state safely (TheSuperHackers#3247)

* fix(hash): Fix initialization of HashTableIteratorClass and make it work with an empty HashTableClass (TheSuperHackers#3284)

* chore(pathfinder): Remove superfluous CPOP_STARTS_FROM_PREV_SEG macro (TheSuperHackers#3295)

* fix(milesaudiomanager): Prevent heap-buffer-overflow read in MilesAudioManager::selectProvider() (TheSuperHackers#3281)

* bugfix(filesystem): Preserve write paths with missing directories (TheSuperHackers#3104)

* perf(pathfinder): Optimize appending node to end of the path (TheSuperHackers#3198)

PathNode::appendToList() walks the entire list from the head to find the tail on every call, making repeated appendNode() calls O(n^2) in path length. Path already tracks m_pathTail, so append directly onto it in O(1) instead. Removed PathNode::appendToList() as it is not used anywhere else.

* perf(pathfinder): Take parents cell's position outside of for-loop for optimization (TheSuperHackers#3198)

The parent cell's world position fromPos never changes across the neighbour loop, so compute it once instead.

* perf(pathfinder): Remove redundant isCrusher recomputation for optimization (TheSuperHackers#3198)

* ci(windows): make bink and miles runtime stubs optional in build artifacts

* fix(platform): preserve POSIX startup working directory and set Flatpak asset paths

* docs(worklog): document CI fixes and verification for upstream sync PR 304

---------

Co-authored-by: ArcticDolphin <5984296+tintinhamans@users.noreply.github.com>
Co-authored-by: Jacob Lane Ledbetter <23038070+CryoTheRenegade@users.noreply.github.com>
Co-authored-by: xezon <4720891+xezon@users.noreply.github.com>
Co-authored-by: Stubbjax <11547761+Stubbjax@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: stm <14291421+stephanmeesters@users.noreply.github.com>
Co-authored-by: mirelle7 <115191165+mirelle7@users.noreply.github.com>
Co-authored-by: Caball009 <82909616+Caball009@users.noreply.github.com>
Co-authored-by: Bobby Battista <bobtista@gmail.com>
Co-authored-by: SkyAero <21192585+Skyaero42@users.noreply.github.com>
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 Minor Severity: Minor < Major < Critical < Blocker ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants