fix: cap optimize()/i_generate_node() recursion depth

A left-nested chain of binary/unary/ternary operators (e.g. tens of
thousands of '+' terms seeded by a non-constant operand, so constant
folding can't collapse it) builds a parse tree as deep as the input --
LALR parsing itself stays shallow since each operator reduces
immediately, but the post-parse optimizer (optimize(), generate.cc)
and code generator (i_generate_node(), icode.cc) then walk that tree
with plain recursion and no depth cap, overflowing the C stack at
compile time from ordinary mudlib source (e.g. via write_file() +
load_object()).

Add a depth counter to each, mirroring MAX_INCLUDE_DEPTH's role for
#include nesting: i_generate_node() reports a clean compile error and
stops recursing past the cap (letting num_parse_error abort the
compile normally); optimize() just stops optimizing the excess depth
and returns the subtree as-is, which is behavior-preserving since
skipping that optimization doesn't change program semantics.

The depth threshold needed to be well under a "plausible" value: under
this build's ASan instrumentation each stack frame is large enough
that a depth cap of 4000 still overflowed the stack before being
reached, confirmed by testing (500 is comfortably safe here; verified
with a 3x full-suite re-run for regressions in legitimately deep
existing code).
This commit is contained in:
Claude 2026-07-14 03:30:01 +00:00 committed by Yucong Sun
parent c4dbb17c96
commit 117cbc1875
3 changed files with 61 additions and 0 deletions

View file

@ -50,10 +50,25 @@ static void optimize_lvalue_list(parse_node_t* expr) {
#define OPTIMIZER_IN_COND 2 /* switch or if or ?: */
static int optimizer_state = 0;
namespace {
// See the matching guard in icode.cc's i_generate_node(): a left-nested
// operator chain builds a parse tree as deep as the input, with no bound
// from the grammar/parser. Skipping optimization of a pathologically deep
// subtree (rather than erroring) is behavior-preserving -- it only means
// that subtree keeps its un-optimized (still correct) bytecode.
int g_optimize_depth = 0;
constexpr int kMaxOptimizeDepth = 500;
} // namespace
static parse_node_t* optimize(parse_node_t* expr) {
if (!expr) {
return nullptr;
}
if (++g_optimize_depth > kMaxOptimizeDepth) {
--g_optimize_depth;
return expr;
}
DEFER { --g_optimize_depth; };
switch (expr->kind) {
case NODE_TERNARY_OP:

View file

@ -357,11 +357,28 @@ static int try_to_push(int kind, int value) {
return 0;
}
namespace {
// A left-nested chain of binary/unary/ternary operators (e.g. tens of
// thousands of '+' terms) builds a parse tree of matching depth with no
// bound from the grammar/parser -- walking it here recurses just as deep.
// Cap it so pathological input errors cleanly instead of overflowing the
// C stack (mirrors MAX_INCLUDE_DEPTH's role for #include nesting).
int g_generate_node_depth = 0;
constexpr int kMaxGenerateNodeDepth = 500;
} // namespace
void i_generate_node(parse_node_t* expr) {
if (!expr) {
return;
}
if (++g_generate_node_depth > kMaxGenerateNodeDepth) {
--g_generate_node_depth;
yyerror("Expression nested too deeply.");
return;
}
DEFER { --g_generate_node_depth; };
if (expr->line && expr->line != line_being_generated) {
switch_to_line(expr->line);
}

View file

@ -0,0 +1,29 @@
// A left-nested chain of binary operators (e.g. tens of thousands of
// '+' terms) builds a parse tree as deep as the input -- LALR parsing
// itself stays shallow (each '+' reduces immediately), but the
// optimizer (optimize(), generate.cc) and code generator
// (i_generate_node(), icode.cc) then walk that tree with plain
// recursion and no depth cap, overflowing the C stack at compile time.
// A single seed variable (not a literal) prevents constant-folding
// from collapsing the chain before it reaches the walkers.
void do_tests() {
string src, expr;
int i;
expr = "x";
for (i = 0; i < 50000; i++) {
expr += "+x";
}
src = "int x; int f() { x = 1; return " + expr + "; }\n";
rm("/gen_deep_expr.c");
write_file("/gen_deep_expr.c", src);
// Surviving this compile is the regression signal -- whether the
// compile is accepted or cleanly rejected past the new depth cap,
// it must not crash the driver.
catch(load_object("/gen_deep_expr"));
ASSERT(1);
rm("/gen_deep_expr.c");
}