mirror of
https://github.com/fluffos/fluffos
synced 2026-08-12 18:26:06 -04:00
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:
parent
c4dbb17c96
commit
117cbc1875
3 changed files with 61 additions and 0 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
29
testsuite/single/tests/compiler/deep_expr_recursion.lpc
Normal file
29
testsuite/single/tests/compiler/deep_expr_recursion.lpc
Normal 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");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue