Skip to content

preprocessor: fix unbounded MacroExpand/PrescanMacroArg mutual recursion (stack overflow)#4338

Merged
arcady-lunarg merged 1 commit into
KhronosGroup:mainfrom
ashketchumwashere:fix/preprocessor-macro-busy-recursion
Jul 23, 2026
Merged

preprocessor: fix unbounded MacroExpand/PrescanMacroArg mutual recursion (stack overflow)#4338
arcady-lunarg merged 1 commit into
KhronosGroup:mainfrom
ashketchumwashere:fix/preprocessor-macro-busy-recursion

Conversation

@ashketchumwashere

@ashketchumwashere ashketchumwashere commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes a stack-overflow vulnerability in the GLSL preprocessor where
MacroExpand and PrescanMacroArg could recurse into each other without
bound, exhausting the process stack.

Root Cause

macro->busy was set to 1 after the PrescanMacroArg loop
(Pp.cpp, previously after pushInput). But PrescanMacroArg itself calls
MacroExpand to expand tokens it finds in macro arguments. Because
busy == 0 during that pre-scan, a macro whose argument contains an
invocation of itself (directly or through a chain) causes:

MacroExpand → PrescanMacroArg → MacroExpand → PrescanMacroArg → ...

until the process stack is exhausted. Confirmed via OSS-Fuzz:

==ERROR: AddressSanitizer: stack-overflow
#7  MacroExpand  Pp.cpp:1295
#8  PrescanMacroArg  Pp.cpp:1109
#9  MacroExpand  Pp.cpp:1392
#10 PrescanMacroArg  Pp.cpp:1109
... [248 alternating frames]

Fix

Fix 1 (root cause) — PpContext.h + Pp.cpp:
Move macro->busy = 1 to before the PrescanMacroArg loop. This
matches standard C preprocessor semantics (C17 §6.10.3.4): a macro
currently being expanded must not be re-expanded, including during argument
pre-scanning. GCC and Clang both enforce this.

Fix 2 (defense-in-depth) — PpContext.h + Pp.cpp:
Add a macroExpandDepth counter (bounded at 200) to TPpContext. A RAII
DepthGuard increments/decrements it on each MacroExpand entry. This
independently prevents any future unbounded recursion path not caught by
the busy flag, and makes the limit explicit and auditable.

Reproduction

// triggers the bug before this fix
#define A(x) A(x)
void main() { A(1); }

@CLAassistant

CLAassistant commented Jul 16, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@ashketchumwashere ashketchumwashere changed the title preprocessor: fix unbounded MacroExpand/PrescanMacroArg mutual recursion (stack overflow)Fix/preprocessor macro busy recursion preprocessor: fix unbounded MacroExpand/PrescanMacroArg mutual recursion (stack overflow) Jul 16, 2026

@arcady-lunarg arcady-lunarg left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Before anything else, you're going to need to a) get rid of extraneous whitespace diffs, and b) add a test that exercises your new check.

//
/****************************************************************************\
Copyright (c) 2002, NVIDIA Corporation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Get rid of these extraneous whitespace changes

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. I've removed all extraneous whitespace changes and added Test/preprocessor.macro.recursion.vert — a 210-level nested F(F(...F(1)...)) call that exercises the macroExpandDepth guard. With the fix it produces 10 "macro expansion depth limit exceeded" errors cleanly; without it the same input causes a stack overflow (SIGSEGV with 4 MB stack / timeout).

@ashketchumwashere
ashketchumwashere force-pushed the fix/preprocessor-macro-busy-recursion branch from ca5d815 to 0d49458 Compare July 17, 2026 05:02
@ashketchumwashere

Copy link
Copy Markdown
Contributor Author

Hi @arcady-lunarg — just wanted to flag that I've addressed your feedback: all extraneous whitespace changes have been removed and I've added Test/preprocessor.macro.recursion.vert to exercise the depth guard (210-level nested F(F(...F(1)...)) — produces 10 clean "macro expansion depth limit exceeded" errors with the fix, stack overflow without it). Would appreciate a re-review when you get a chance. Thanks!

@dneto0 dneto0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This looks good to me. Neither GLSL nor C++98 specify a minimum number of macro expansion nesting levels. 200 seems fine.

MacroExpand can call PrescanMacroArg (to pre-expand function-like macro
arguments), which in turn calls MacroExpand for any macros found in the
argument token stream.  With deeply nested macro invocations in the
source, this creates a call chain of depth proportional to the nesting
level.  At 200+ levels the native C++ stack overflows.

Add macroExpandDepth to TPpContext (initialized to 0) and a RAII
DepthGuard in MacroExpand that increments it on entry and decrements on
exit.  If the depth reaches maxMacroExpandDepth (200) MacroExpand emits
a preprocessor error and returns MacroExpandNotStarted, unwinding the
recursion cleanly instead of crashing.

Add test preprocessor.macro.recursion.vert: F(x)=x nested 210 times
triggers the guard 10 times with 'macro expansion depth limit exceeded'
errors; without the guard the same input causes a stack overflow.
@ashketchumwashere
ashketchumwashere force-pushed the fix/preprocessor-macro-busy-recursion branch from 0d49458 to 8c6c961 Compare July 23, 2026 09:19
@arcady-lunarg arcady-lunarg added the kokoro:run Trigger Google bot runs label Jul 23, 2026
@kokoro-team kokoro-team removed the kokoro:run Trigger Google bot runs label Jul 23, 2026
@arcady-lunarg
arcady-lunarg merged commit 6cbfdd6 into KhronosGroup:main Jul 23, 2026
54 of 55 checks passed
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.

5 participants