ReviewOS

home-lang/home

Interpreter recursion guard (512) doesn't prevent stack-overflow crashes; reconcile with parser nesting cap (256)

#76
Closed glennmichael123 opened this 10 days ago ·
10 days ago

Follow-up from #75.

The interpreter's runtime recursion guard MAX_RECURSION_DEPTH = 512 (packages/interpreter/src/interpreter.zig, evaluateExpression) does not actually protect against native stack exhaustion, and is unreachable for the case it looks designed for:

  1. Pure expression nesting can't reach it — the parser rejects nesting deeper than 256 first (EP0001 "Expression is too deeply nested (maximum depth is 256)"). So a deeply-nested expression never reaches the runtime guard at all.

  2. Function self-recursion crashes before it — a genuinely self-recursing Home function (fn rec(n) { return rec(n+1) }) exhausts the native stack (SIGSEGV) before logical depth 512, because each Home-level call chains several large native frames (callUserFunctionWithNamed → executeStatement → evaluateExpression → evaluateCallExpression → …). Observed as a hard crash in a test thread (smaller stack); the main-thread CLI may tolerate more but the guard still doesn't bound it deterministically.

Net: the three limits (parser 256, runtime 512, actual native-stack budget) are not reconciled, so the runtime guard provides little real protection.

Possible directions

  • Lower MAX_RECURSION_DEPTH to a value that trips before native overflow for the function-call chain (measure per-level native stack cost), or
  • Add an explicit stack-depth/remaining-guard check in callUserFunctionWithNamed (the deep frame), not only in evaluateExpression, or
  • Align the parser cap and runtime cap intentionally and document the relationship.

Not blocking anything today — surfaced while fixing #75 (uninitialized recursion_depth).

10 days ago

Implemented and pushed directly to main in b191d69e1 (fix(interpreter): bound user recursion (#76)).

  • split expression nesting from active interpreted user-call depth
  • aligned the interpreter expression backstop with the parser at 256 levels
  • bounded functions, closures, pipelines, Result callbacks, impl methods, and trait defaults to 64 active user frames
  • added finite-recursion and runaway-recursion regression coverage
  • documented both limits

Verification: interpreter suite passed 23/23 in Debug (396 MB guarded peak) and ReleaseSafe (624 MB guarded peak); the runaway case now returns RuntimeError instead of exhausting the native stack.

Sign in to comment on this issue.