Summary
V8ScriptEngine.MaxRuntimeStackUsage is the only supported way to increase V8's JS stack limit (deeper recursion), and it works — but on current V8 it aborts the process at the next GC or on dispose, and no managed-side mitigation avoids it.
Environment
- Microsoft.ClearScript.V8 7.5.1
- Microsoft.ClearScript.V8.Native.win-x64 7.5.1 (bundled V8 14.7.173.23)
- .NET 10 (net10.0), win-x64
Repro (minimal, no external JS libs)
var engine = new V8ScriptEngine();
engine.MaxRuntimeStackUsage = (UIntPtr)(2 * 1024 * 1024); // remove this line -> no crash
engine.Execute("var a=[]; for (var k=0;k<200000;k++){ a.push({x:k}); } a=null;");
engine.CollectGarbage(true); // aborts: "# Check failed: isolate_->IsOnCentralStack()."
engine.Dispose();
Exit code 0x80000003 (native int 3). Removing only the MaxRuntimeStackUsage line makes it exit cleanly — it is the sole trigger.
It genuinely increases depth (so it's a needed capability)
Measured max recursion depth with an 8 MB cap on a 16 MB thread: 104,827 frames, vs the V8 default of ~12,480. Depth is otherwise independent of the OS thread stack size (V8 uses its own --stack-size, default ~1 MB), so this cap is the only lever that raises it.
No managed workaround avoids the abort
All of these still abort at the GC/dispose with the same IsOnCentralStack CHECK:
- Setting
MaxRuntimeStackUsage = 0 before the GC/dispose
- The reset + a dummy
Execute (fresh execution scope) before the GC
V8GlobalFlags.DisableBackgroundWork | DisableJITCompilation
- Setting the limit at the
V8Runtime level instead of the engine
Root cause (analysis)
MaxRuntimeStackUsage is the only path that calls v8::Isolate::SetStackLimit() (V8IsolateImpl::EnterExecutionScope), overriding V8's own limit with an absolute address. On V8 14.7 (WebAssembly enabled) V8 asserts CHECK(isolate_->IsOnCentralStack()) during the GC conservative stack scan (and in Isolate::Throw); the override leaves the live SP outside V8's recorded central-stack window, so the CHECK fails and V8 aborts. A full-dump native stack shows: V8Context_CollectGarbage -> V8 GC stack scan -> CHECK -> DebugBreak (int 3). Note ClearScript's exit path resets the limit to a sentinel (s_pMinStackLimit, addr 0x8) rather than restoring V8's real limit, so the isolate stays poisoned even after resetting.
Requests
- Fix
MaxRuntimeStackUsage on V8 >= 13/14 (keep the stack bookkeeping consistent with V8's central-stack invariant), or document it as unsupported there.
- Expose V8's
--stack-size (e.g. V8RuntimeConstraints.MaxStackSize) as the safe equivalent — it raises the limit through V8's own init path, so overflow stays a catchable RangeError with no abort.
Happy to share the full minimal repro project and the captured crash dump / symbolized analysis if useful.
Summary
V8ScriptEngine.MaxRuntimeStackUsageis the only supported way to increase V8's JS stack limit (deeper recursion), and it works — but on current V8 it aborts the process at the next GC or on dispose, and no managed-side mitigation avoids it.Environment
Repro (minimal, no external JS libs)
Exit code
0x80000003(nativeint 3). Removing only theMaxRuntimeStackUsageline makes it exit cleanly — it is the sole trigger.It genuinely increases depth (so it's a needed capability)
Measured max recursion depth with an 8 MB cap on a 16 MB thread: 104,827 frames, vs the V8 default of ~12,480. Depth is otherwise independent of the OS thread stack size (V8 uses its own
--stack-size, default ~1 MB), so this cap is the only lever that raises it.No managed workaround avoids the abort
All of these still abort at the GC/dispose with the same
IsOnCentralStackCHECK:MaxRuntimeStackUsage = 0before the GC/disposeExecute(fresh execution scope) before the GCV8GlobalFlags.DisableBackgroundWork | DisableJITCompilationV8Runtimelevel instead of the engineRoot cause (analysis)
MaxRuntimeStackUsageis the only path that callsv8::Isolate::SetStackLimit()(V8IsolateImpl::EnterExecutionScope), overriding V8's own limit with an absolute address. On V8 14.7 (WebAssembly enabled) V8 assertsCHECK(isolate_->IsOnCentralStack())during the GC conservative stack scan (and inIsolate::Throw); the override leaves the live SP outside V8's recorded central-stack window, so the CHECK fails and V8 aborts. A full-dump native stack shows:V8Context_CollectGarbage-> V8 GC stack scan -> CHECK ->DebugBreak(int 3). Note ClearScript's exit path resets the limit to a sentinel (s_pMinStackLimit, addr 0x8) rather than restoring V8's real limit, so the isolate stays poisoned even after resetting.Requests
MaxRuntimeStackUsageon V8 >= 13/14 (keep the stack bookkeeping consistent with V8's central-stack invariant), or document it as unsupported there.--stack-size(e.g.V8RuntimeConstraints.MaxStackSize) as the safe equivalent — it raises the limit through V8's own init path, so overflow stays a catchableRangeErrorwith no abort.Happy to share the full minimal repro project and the captured crash dump / symbolized analysis if useful.