Skip to content

fix: prevent StackOverflow, IndexOutOfRange, and InvalidCast crashes on malformed JavaScript#2340

Merged
lahma merged 4 commits into
mainfrom
copilot/fix-engine-execute-crashes
Mar 20, 2026
Merged

fix: prevent StackOverflow, IndexOutOfRange, and InvalidCast crashes on malformed JavaScript#2340
lahma merged 4 commits into
mainfrom
copilot/fix-engine-execute-crashes

Conversation

Copilot AI commented Mar 20, 2026

Copy link
Copy Markdown
Contributor

Fuzzing with AFL++/SharpFuzz found three distinct crash vectors in Engine.Execute — all denial-of-service via uncaught CLR exceptions. Two terminate the process entirely.

Crash 1 — StackOverflowException in HoistingScope.ScriptWalker.Visit

Unbounded recursion over deeply-nested ASTs during hoisting analysis — before execution begins, so all engine constraints (timeout, recursion limit, memory) are bypassed. StackOverflowException is uncatchable and terminates the process.

Fix: Split Visit into a guarded entry point and VisitCore. Added _depth counter with MaxDepth = 2_000; exceeding it throws ScriptPreparationException — the existing public JintException subclass for pre-execution failures.

// Now throws catchable ScriptPreparationException instead of killing the process
engine.Execute(new string('x', 0).PadLeft(0) + string.Concat(Enumerable.Repeat("if(true){", 1100)) + "1" + new string('}', 1100));

Crash 2 — IndexOutOfRangeException in JsonParser.ScanHexEscape

Off-by-one: _index < _length + 1 permits _index == _length, which then accesses _source[_index] out-of-bounds when a JSON string ends immediately after \u (e.g. "\u).

Fix: _index < _length + 1_index < _length. The existing ThrowError path then correctly surfaces a JavaScriptException (SyntaxError).

Crash 3 — InvalidCastException in JintObjectExpression.Initialize

p.Value (typed as Node) was unconditionally cast to Expression. Acornima emits an AssignmentPattern node for shorthand-default syntax ({a = expr}) in ambiguous destructuring contexts, which is not a subtype of Expression.

Fix: Added a runtime type check; non-Expression values now throw a JavaScriptException (SyntaxError) with a descriptive message instead of letting the cast propagate.

Original prompt

This section details on the original issue you should resolve

<issue_title>Engine.Execute crashes on malformed JavaScript (StackOverflow, IOOB, InvalidCast)</issue_title>
<issue_description>Version/platform/runtime used

  • Jint 4.6.3 (NuGet, latest stable)
  • Also verified on current main branch
  • .NET 10.0, Linux x64

Describe the bug

Fuzzing Jint with AFL++ and SharpFuzz found 3 unique crashes triggered by malformed JavaScript input.

Crash 1 is a StackOverflowException that occurs during AST analysis (HoistingScope.ScriptWalker.Visit), before the engine starts executing. This means TimeoutInterval, LimitRecursion, MaxStatements, and LimitMemory do not prevent this crash. StackOverflowException is uncatchable in .NET and terminates the process immediately.

Crash 2 is an IndexOutOfRangeException in JsonParser.ScanHexEscape when parsing a malformed JSON string with a truncated \u escape (32 bytes).

Crash 3 is an InvalidCastException in JintObjectExpression.InitializeAssignmentPattern node cast to Expression (69 bytes).

To Reproduce

using Jint;

var engine = new Engine(options =>
{
    // NOTE: these limits do NOT prevent Crash 1 (StackOverflow)
    // because it occurs during parsing/analysis, before execution begins
    options.TimeoutInterval(TimeSpan.FromSeconds(2));
    options.LimitRecursion(100);
    options.MaxStatements(10_000);
    options.LimitMemory(16_000_000);
});

// Crash 1 — StackOverflow in HoistingScope (attached as crash_003.js.zip)
// WARNING: run this LAST or in a separate process — it terminates immediately
var crash1 = File.ReadAllText("crash_003.js");
engine.Execute(crash1); // StackOverflowException — process terminates

// Crash 2 — IOOB in JsonParser.ScanHexEscape (32 bytes)
var crash2 = System.Text.Encoding.Latin1.GetString(
    Convert.FromHexString("4a534f4e2e706172736528227b5c22493f9e5c5c5c5c5c5c755c445c6522293b"));
try { engine.Execute(crash2); }
catch (Exception ex) { Console.WriteLine($"crash_2: {ex.GetType().Name}"); }

// Crash 3 — InvalidCast in JintObjectExpression (69 bytes)
try { engine.Execute("a=[1,3];aaa={}={}.aap+=[1,3];aaa={}={a=-[]<= []<a.m}.aap+=[,2,3111-1]"); }
catch (Exception ex) { Console.WriteLine($"crash_3: {ex.GetType().Name}"); }

Stack traces

Crash 1 — StackOverflow (HIGH):

Stack overflow.
Repeated 22766 times:
   at Jint.HoistingScope+ScriptWalker.Visit(Node, Node, HashSet`1)
   at Jint.HoistingScope.GetProgramLevelDeclarations(Program, Boolean, Boolean)
   at Jint.Engine.GlobalDeclarationInstantiation(Script, GlobalEnvironment)
   at Jint.Engine.ScriptEvaluation(ScriptRecord, ParserOptions)
   at Jint.Engine.Execute(String, String)

Crash 2 — IOOB:

System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at Jint.Native.Json.JsonParser.ScanHexEscape()
   at Jint.Native.Json.JsonParser.ScanStringLiteral(State& state)
   at Jint.Native.Json.JsonParser.Parse(String code)
   at Jint.Native.Json.JsonInstance.Parse(JsValue, JsValue[])

Crash 3 — InvalidCast:

System.InvalidCastException: Unable to cast object of type 'Acornima.Ast.AssignmentPattern' to type 'Acornima.Ast.Expression'.
   at Jint.Runtime.Interpreter.Expressions.JintObjectExpression.Initialize(EvaluationContext context)

Expected behavior

  • Crash 1: Engine.Execute should throw a catchable exception (e.g. RecursionDepthOverflowException) for deeply nested input, even during parsing/analysis.
  • Crash 2: JSON.parse should throw JavaScriptException for malformed JSON, not IndexOutOfRangeException.
  • Crash 3: Malformed object expressions should throw JavaScriptException or ParseErrorException, not InvalidCastException.

Additional context

  • Crash 1 severity: HIGH (denial of service). Any application calling Engine.Execute with untrusted JavaScript is vulnerable. The process terminates immediately — no try/catch can prevent it. Execution limits are bypassed because the crash occurs pre-execution. A similar unbounded recursion bug was recently assigned CVE-2026-32933 (CVSS 7.5) in AutoMapper — you may want to consider filing a security advisory for this.
  • Crash 2 & 3 severity: Medium (denial of service via unhandled exception).
  • Found via coverage-guided fuzzing with AFL++ and SharpFuzz
  • stackoverflow input attached as a zip jint_crash_003.zip

</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits March 20, 2026 05:49
…xecute

Co-authored-by: lahma <171892+lahma@users.noreply.github.com>
Co-authored-by: lahma <171892+lahma@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix crashes in Engine.Execute from malformed JavaScript fix: prevent StackOverflow, IndexOutOfRange, and InvalidCast crashes on malformed JavaScript Mar 20, 2026
Copilot AI requested a review from lahma March 20, 2026 05:54
@lahma
lahma marked this pull request as ready for review March 20, 2026 06:01
@lahma

lahma commented Mar 20, 2026

Copy link
Copy Markdown
Collaborator

@copilot check ci failures

…rm compatibility

Co-authored-by: lahma <171892+lahma@users.noreply.github.com>

Copilot AI commented Mar 20, 2026

Copy link
Copy Markdown
Contributor Author

@copilot check ci failures

The CI failures on Windows and macOS were caused by the ShouldThrowScriptPreparationExceptionForDeeplyNestedScript test using 1100 nested if(true){...} blocks. At that depth, the Acornima parser itself throws InsufficientExecutionStackException (via its own RuntimeHelpers.EnsureSufficientExecutionStack() safety check) before my ScriptWalker guard could fire — because Windows/macOS have a 1MB default thread stack vs 8MB on Linux.

Fixed in 86d886d by:

  • Lowering MaxDepth from 2000 to 256 — fires well before Acornima's own stack check on all platforms
  • Reducing test nesting from 1100 to 150 levels (walker depth ~302 > 256; Acornima parse depth ~750 frames × ~300 bytes ≈ 225KB, safely within the 1MB Windows stack)

Copilot AI requested a review from lahma March 20, 2026 06:19
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.

Engine.Execute crashes on malformed JavaScript (StackOverflow, IOOB, InvalidCast)

2 participants