Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions Jint.Tests/Runtime/ExecutionConstraintTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Jint.Constraints;
using Jint.Native.Function;
using Jint.Runtime;

Expand Down Expand Up @@ -356,6 +357,151 @@ public void ShouldLimitTypedArraySizeForWith()
Assert.Throws<MemoryLimitExceededException>(() => engine.Evaluate("var arr = new Uint8Array(100000000); arr.with(0, 1);"));
}

// https://github.com/sebastienros/jint/issues/2486
[Fact]
public void ShouldThrowRangeErrorWhenPadStartExceedsMaxStringLength()
{
// The result length (2147483647) exceeds ClrLimits.MaxArrayLength, so the size cap converts
// a would-be OutOfMemoryException into a catchable RangeError without any constraints set.
var engine = new Engine();
var ex = Assert.Throws<JavaScriptException>(() => engine.Evaluate("'x'.padStart(2147483647)"));
Assert.Contains("Invalid string length", ex.Message);
}

// https://github.com/sebastienros/jint/issues/2486
[Fact]
public void ShouldLimitStringSizeForPadEnd()
{
// The result (536870911) is below the size cap, so it must be built incrementally and the
// memory limit must be able to interrupt it instead of allocating ~1 GB up front.
var engine = new Engine(o => o.LimitMemory(4_000_000));
Assert.Throws<MemoryLimitExceededException>(() => engine.Evaluate("'x'.padEnd(536870911, 'ab')"));
}

// https://github.com/sebastienros/jint/issues/2486
[Fact]
public void ShouldLimitArraySizeForArrayFrom()
{
var engine = new Engine(o => o.LimitMemory(4_000_000));
Assert.Throws<MemoryLimitExceededException>(() => engine.Evaluate("Array.from({ length: 50000000 });"));
}

[Fact]
public void ShouldLimitStringSizeForStringRaw()
{
var engine = new Engine(o => o.LimitMemory(4_000_000));
Assert.Throws<MemoryLimitExceededException>(() => engine.Evaluate("String.raw({ raw: { length: 50000000 } });"));
}

[Fact]
public void ShouldLimitArraySizeForSort()
{
// The element-collection loop in sort is interruptible: a low statement budget aborts it.
var engine = new Engine(o => o.MaxStatements(1_000));
Assert.Throws<StatementsCountOverflowException>(() => engine.Evaluate("new Array(50000000).sort();"));
}

[Fact]
public void ShouldLimitArraySizeForForEach()
{
// forEach over a huge (sparse) array must be interruptible even though the callback never
// runs for holes. A sparse array is used so the guard exercised is forEach's own loop, not
// fill's (fill on a dense 50M array would trip the limit before forEach was ever reached).
var engine = new Engine(o => o.MaxStatements(1_000));
Assert.Throws<StatementsCountOverflowException>(() => engine.Evaluate("new Array(50000000).forEach(function () {});"));
}

[Fact]
public void PadStartAndPadEndProduceCorrectResults()
{
var engine = new Engine();
Assert.Equal("005", engine.Evaluate("'5'.padStart(3, '0')").AsString());
Assert.Equal("500", engine.Evaluate("'5'.padEnd(3, '0')").AsString());
Assert.Equal("ab", engine.Evaluate("'ab'.padStart(1)").AsString());
Assert.Equal(" x", engine.Evaluate("'x'.padStart(5)").AsString());
Assert.Equal("x ", engine.Evaluate("'x'.padEnd(5)").AsString());
// Empty fill string returns the input unchanged.
Assert.Equal("x", engine.Evaluate("'x'.padEnd(5, '')").AsString());
Assert.Equal("1231231abc", engine.Evaluate("'abc'.padStart(10, '123')").AsString());
Assert.Equal("abc1231231", engine.Evaluate("'abc'.padEnd(10, '123')").AsString());
}

[Fact]
public void PadStartEvaluatesFillStringAfterLengthCheck()
{
// Per spec (https://tc39.es/ecma262/#sec-stringpad) the fillString is resolved only after the
// maxLength <= stringLength early return, so its ToString side effect must not run here.
var engine = new Engine();
var result = engine.Evaluate(
"var sideEffect = false;" +
"var fill = { toString() { sideEffect = true; return '0'; } };" +
"'abc'.padStart(2, fill);" +
"sideEffect;");
Assert.False(result.AsBoolean());
}

[Fact]
public void JoinReleasesJoinStackWhenInterrupted()
{
// Regression: when a constraint interrupts a large join mid-loop, the array must not be left
// on the engine's long-lived join stack — otherwise a later join of the same array would
// wrongly return "" via false cyclic-reference detection.
var engine = new Engine(o => o.MaxStatements(10_000_000));
engine.Evaluate("var a = []; for (var i = 0; i < 20000; i++) a[i] = i;");

var maxStatements = engine.Constraints.Find<MaxStatementsConstraint>()!;
maxStatements.MaxStatements = 1;
engine.Constraints.Reset();
Assert.Throws<StatementsCountOverflowException>(() => engine.Evaluate("a.join(',')"));

// With a generous budget the same array must join correctly (not the empty string).
maxStatements.MaxStatements = 10_000_000;
engine.Constraints.Reset();
Assert.StartsWith("0,1,2,3,", engine.Evaluate("a.join(',')").AsString());
}

[Fact]
public void ShouldLimitArrayFromWithNativeIterator()
{
// Array.from over a native (statement-free) string iterator must be interruptible via the
// shared iterator-protocol guard; the string iterator runs no JS statements per element.
var engine = new Engine(o => o.MaxStatements(10_000_000));
engine.Evaluate("var s = 'x'; for (var i = 0; i < 17; i++) s += s;"); // 131072 chars

var maxStatements = engine.Constraints.Find<MaxStatementsConstraint>()!;
maxStatements.MaxStatements = 1;
engine.Constraints.Reset();
Assert.Throws<StatementsCountOverflowException>(() => engine.Evaluate("Array.from(s);"));
}

[Fact]
public void ShouldLimitObjectKeysForLargeArray()
{
// Object.keys is a pure native enumeration with no JS callback to self-throttle; it must be
// interruptible by the constraint check in EnumerableOwnProperties.
var engine = new Engine(o => o.MaxStatements(10_000_000));
engine.Evaluate("var a = []; for (var i = 0; i < 30000; i++) a[i] = i;");

var maxStatements = engine.Constraints.Find<MaxStatementsConstraint>()!;
maxStatements.MaxStatements = 1;
engine.Constraints.Reset();
Assert.Throws<StatementsCountOverflowException>(() => engine.Evaluate("Object.keys(a);"));
}

[Fact]
public void ShouldLimitMapForEachWithNativeCallback()
{
// A native (CLR) callback does not self-throttle via statement checks, so Map.prototype.forEach
// must be interruptible by its own constraint check.
var engine = new Engine(o => o.MaxStatements(10_000_000));
engine.Evaluate("var m = new Map(); for (var i = 0; i < 30000; i++) m.set(i, i);");

var maxStatements = engine.Constraints.Find<MaxStatementsConstraint>()!;
maxStatements.MaxStatements = 1;
engine.Constraints.Reset();
Assert.Throws<StatementsCountOverflowException>(() => engine.Evaluate("m.forEach(Math.max);"));
}

[Fact]
public void ShouldConsiderConstraintsWhenCallingInvoke()
{
Expand Down
7 changes: 7 additions & 0 deletions Jint/Engine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ namespace Jint;
[DebuggerTypeProxy(typeof(EngineDebugView))]
public sealed partial class Engine : IDisposable
{
/// <summary>
/// How often (in loop iterations) bulk built-in operations should call <see cref="ConstraintOperations.Check"/>
/// so that long native loops over JS-controlled sizes remain interruptible by execution constraints.
/// Single source of truth shared by every built-in that uses the periodic-check idiom.
/// </summary>
internal const int ConstraintCheckInterval = 10_000;

private static readonly Options _defaultEngineOptions = new();

private readonly Parser _defaultParser;
Expand Down
33 changes: 28 additions & 5 deletions Jint/Native/Array/ArrayConstructor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ namespace Jint.Native.Array;
[JsObject]
public sealed partial class ArrayConstructor : Constructor
{
private const int ConstraintCheckInterval = Engine.ConstraintCheckInterval;

private static readonly JsString _functionName = new JsString("Array");

internal ArrayConstructor(
Expand Down Expand Up @@ -611,6 +613,12 @@ private ObjectInstance ConstructArrayFromArrayLike(
uint n = 0;
for (uint i = 0; i < length; i++)
{
// Check constraints periodically so a huge array-like length cannot run uninterrupted.
if (i > 0 && i % ConstraintCheckInterval == 0)
{
_engine.Constraints.Check();
}

var value = source.Get(i);
if (callable is not null)
{
Expand Down Expand Up @@ -874,14 +882,29 @@ private JsArray ConstructArrayFromIEnumerable(IEnumerable enumerable)
{
var jsArray = Construct(Arguments.Empty);
var tempArray = _engine._jsValueArrayPool.RentArray(1);
foreach (var item in enumerable)
// try/finally so the rented pool array is returned even when a periodic Check() throws.
try
{
long count = 0;
foreach (var item in enumerable)
{
// Check constraints periodically so a huge enumerable cannot run uninterrupted.
if (count > 0 && count % ConstraintCheckInterval == 0)
{
_engine.Constraints.Check();
}
count++;

var jsItem = FromObject(Engine, item);
tempArray[0] = jsItem;
_realm.Intrinsics.Array.PrototypeObject.Push(jsArray, tempArray);
}
}
finally
{
var jsItem = FromObject(Engine, item);
tempArray[0] = jsItem;
_realm.Intrinsics.Array.PrototypeObject.Push(jsArray, tempArray);
_engine._jsValueArrayPool.ReturnArray(tempArray);
}

_engine._jsValueArrayPool.ReturnArray(tempArray);
return jsArray;
}

Expand Down
71 changes: 44 additions & 27 deletions Jint/Native/Array/ArrayInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1320,47 +1320,64 @@ internal sealed override bool FindWithCallback(
var args = _engine._jsValueArrayPool.RentArray(3);
args[2] = this;

if (!fromEnd)
// try/finally so the rented pool array is returned on every exit path: the early
// return-on-match below and a periodic Check() throw both previously leaked it.
try
{
for (uint k = 0; k < len; k++)
if (!fromEnd)
{
if (TryGetValue(k, out var kvalue) || visitUnassigned)
for (uint k = 0; k < len; k++)
{
kvalue ??= Undefined;
args[0] = kvalue;
args[1] = k;
var testResult = callable.Call(thisArg, args);
if (TypeConverter.ToBoolean(testResult))
if (k > 0 && k % Engine.ConstraintCheckInterval == 0)
{
index = k;
value = kvalue;
return true;
_engine.Constraints.Check();
}

if (TryGetValue(k, out var kvalue) || visitUnassigned)
{
kvalue ??= Undefined;
args[0] = kvalue;
args[1] = k;
var testResult = callable.Call(thisArg, args);
if (TypeConverter.ToBoolean(testResult))
{
index = k;
value = kvalue;
return true;
}
}
}
}
}
else
{
for (long k = len - 1; k >= 0; k--)
else
{
var idx = (uint) k;
if (TryGetValue(idx, out var kvalue) || visitUnassigned)
for (long k = len - 1; k >= 0; k--)
{
kvalue ??= Undefined;
args[0] = kvalue;
args[1] = idx;
var testResult = callable.Call(thisArg, args);
if (TypeConverter.ToBoolean(testResult))
if (k % Engine.ConstraintCheckInterval == 0)
{
index = idx;
value = kvalue;
return true;
_engine.Constraints.Check();
}

var idx = (uint) k;
if (TryGetValue(idx, out var kvalue) || visitUnassigned)
{
kvalue ??= Undefined;
args[0] = kvalue;
args[1] = idx;
var testResult = callable.Call(thisArg, args);
if (TypeConverter.ToBoolean(testResult))
{
index = idx;
value = kvalue;
return true;
}
}
}
}
}

_engine._jsValueArrayPool.ReturnArray(args);
finally
{
_engine._jsValueArrayPool.ReturnArray(args);
}

index = 0;
value = Undefined;
Expand Down
7 changes: 7 additions & 0 deletions Jint/Native/Array/ArrayOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ public virtual JsValue[] GetAll(
var jsValues = new JsValue[n];
for (uint i = 0; i < (uint) n; i++)
{
// Slow array-like expansion (e.g. Function.prototype.apply / Reflect.apply on a huge
// {length} object): per-element property Get; check constraints periodically.
if (i > 0 && i % Engine.ConstraintCheckInterval == 0)
{
Target.Engine.Constraints.Check();
}

var jsValue = skipHoles && !HasProperty(i) ? JsValue.Undefined : Get(i);
if ((jsValue.Type & elementTypes) == Types.Empty)
{
Expand Down
Loading
Loading