diff --git a/Jint.Tests/Runtime/ExecutionConstraintTests.cs b/Jint.Tests/Runtime/ExecutionConstraintTests.cs index d6fd04a8b..b22910dfe 100644 --- a/Jint.Tests/Runtime/ExecutionConstraintTests.cs +++ b/Jint.Tests/Runtime/ExecutionConstraintTests.cs @@ -1,3 +1,4 @@ +using Jint.Constraints; using Jint.Native.Function; using Jint.Runtime; @@ -356,6 +357,151 @@ public void ShouldLimitTypedArraySizeForWith() Assert.Throws(() => 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(() => 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(() => 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(() => engine.Evaluate("Array.from({ length: 50000000 });")); + } + + [Fact] + public void ShouldLimitStringSizeForStringRaw() + { + var engine = new Engine(o => o.LimitMemory(4_000_000)); + Assert.Throws(() => 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(() => 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(() => 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()!; + maxStatements.MaxStatements = 1; + engine.Constraints.Reset(); + Assert.Throws(() => 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()!; + maxStatements.MaxStatements = 1; + engine.Constraints.Reset(); + Assert.Throws(() => 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()!; + maxStatements.MaxStatements = 1; + engine.Constraints.Reset(); + Assert.Throws(() => 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()!; + maxStatements.MaxStatements = 1; + engine.Constraints.Reset(); + Assert.Throws(() => engine.Evaluate("m.forEach(Math.max);")); + } + [Fact] public void ShouldConsiderConstraintsWhenCallingInvoke() { diff --git a/Jint/Engine.cs b/Jint/Engine.cs index 1ffdc75c7..03c203382 100644 --- a/Jint/Engine.cs +++ b/Jint/Engine.cs @@ -28,6 +28,13 @@ namespace Jint; [DebuggerTypeProxy(typeof(EngineDebugView))] public sealed partial class Engine : IDisposable { + /// + /// How often (in loop iterations) bulk built-in operations should call + /// 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. + /// + internal const int ConstraintCheckInterval = 10_000; + private static readonly Options _defaultEngineOptions = new(); private readonly Parser _defaultParser; diff --git a/Jint/Native/Array/ArrayConstructor.cs b/Jint/Native/Array/ArrayConstructor.cs index 7410618cc..87ee26d60 100644 --- a/Jint/Native/Array/ArrayConstructor.cs +++ b/Jint/Native/Array/ArrayConstructor.cs @@ -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( @@ -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) { @@ -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; } diff --git a/Jint/Native/Array/ArrayInstance.cs b/Jint/Native/Array/ArrayInstance.cs index 56d030ddf..9d3cdbf44 100644 --- a/Jint/Native/Array/ArrayInstance.cs +++ b/Jint/Native/Array/ArrayInstance.cs @@ -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; diff --git a/Jint/Native/Array/ArrayOperations.cs b/Jint/Native/Array/ArrayOperations.cs index ccf3ca349..80cb2aa3e 100644 --- a/Jint/Native/Array/ArrayOperations.cs +++ b/Jint/Native/Array/ArrayOperations.cs @@ -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) { diff --git a/Jint/Native/Array/ArrayPrototype.cs b/Jint/Native/Array/ArrayPrototype.cs index a9e214e60..42eca973b 100644 --- a/Jint/Native/Array/ArrayPrototype.cs +++ b/Jint/Native/Array/ArrayPrototype.cs @@ -20,7 +20,7 @@ namespace Jint.Native.Array; [JsObject] public sealed partial class ArrayPrototype : ArrayInstance { - private const int ConstraintCheckInterval = 10_000; + private const int ConstraintCheckInterval = Engine.ConstraintCheckInterval; private readonly Realm _realm; @@ -347,6 +347,12 @@ private JsValue LastIndexOf(JsValue thisObject, JsCallArguments arguments) var i = (ulong) k; for (; ; i--) { + // Slow scan over a (possibly huge) length; check constraints periodically. + if (i % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var kPresent = o.HasProperty(i); if (kPresent) { @@ -396,6 +402,11 @@ private JsValue Reduce(JsValue thisObject, JsCallArguments arguments) var kPresent = false; while (kPresent == false && k < len) { + if (k > 0 && k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + if (kPresent = o.TryGetValue((uint) k, out var temp)) { accumulator = temp; @@ -414,6 +425,11 @@ private JsValue Reduce(JsValue thisObject, JsCallArguments arguments) args[3] = o.Target; while (k < len) { + if (k > 0 && k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var i = (uint) k; if (o.TryGetValue(i, out var kvalue)) { @@ -451,6 +467,11 @@ private JsValue Filter(JsValue thisObject, JsCallArguments arguments) args[2] = o.Target; for (uint k = 0; k < len; k++) { + if (k > 0 && k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + if (o.TryGetValue(k, out var kvalue)) { args[0] = kvalue; @@ -499,6 +520,11 @@ private JsValue Map(JsValue thisObject, JsCallArguments arguments) args[2] = o.Target; for (uint k = 0; k < len; k++) { + if (k > 0 && k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + if (o.TryGetValue(k, out var kvalue)) { args[0] = kvalue; @@ -582,6 +608,11 @@ private ulong FlattenIntoArray( while (sourceIndex < sourceLen) { + if (sourceIndex > 0 && sourceIndex % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var exists = source.HasProperty(sourceIndex); if (exists) { @@ -647,6 +678,11 @@ private JsValue ForEach(JsValue thisObject, JsCallArguments arguments) args[2] = o.Target; for (uint k = 0; k < len; k++) { + if (k > 0 && k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + if (o.TryGetValue(k, out var kvalue)) { args[0] = kvalue; @@ -723,6 +759,11 @@ private JsValue Includes(JsValue thisObject, JsCallArguments arguments) while (k < len) { + if (k > 0 && k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var value = o.Get((ulong) k); if (SameValueZeroComparer.Equals(value, searchElement)) { @@ -762,6 +803,11 @@ private JsValue Every(JsValue thisObject, JsCallArguments arguments) args[2] = o.Target; for (uint k = 0; k < len; k++) { + if (k > 0 && k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + if (o.TryGetValue(k, out var kvalue)) { args[0] = kvalue; @@ -847,6 +893,11 @@ private JsValue IndexOf(JsValue thisObject, JsCallArguments arguments) for (; k < len; k++) { + if (k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var kPresent = o.HasProperty(k); if (kPresent) { @@ -1051,6 +1102,11 @@ private JsValue Splice(JsValue thisObject, JsCallArguments arguments) for (uint k = 0; k < actualDeleteCount; k++) { + if (k > 0 && k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var index = actualStart + k; if (o.HasProperty(index)) { @@ -1066,6 +1122,11 @@ private JsValue Splice(JsValue thisObject, JsCallArguments arguments) { for (ulong k = actualStart; k < len - actualDeleteCount; k++) { + if (k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var from = k + actualDeleteCount; var to = k + (ulong) items.Length; if (o.HasProperty(from)) @@ -1081,6 +1142,11 @@ private JsValue Splice(JsValue thisObject, JsCallArguments arguments) for (var k = len; k > len - actualDeleteCount + (ulong) items.Length; k--) { + if (k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + o.DeletePropertyOrThrow(k - 1); } } @@ -1088,6 +1154,11 @@ private JsValue Splice(JsValue thisObject, JsCallArguments arguments) { for (var k = len - actualDeleteCount; k > actualStart; k--) { + if (k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var from = k + actualDeleteCount - 1; var to = k + (ulong) items.Length - 1; if (o.HasProperty(from)) @@ -1159,6 +1230,11 @@ private JsValue Unshift(JsValue thisObject, JsCallArguments arguments) var minIndex = o.GetSmallestIndex(len); for (var k = len; k > minIndex; k--) { + if (k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var from = k - 1; var to = k + argCount - 1; if (o.TryGetValue(from, out var fromValue)) @@ -1199,6 +1275,11 @@ private JsValue Sort(JsValue thisObject, JsCallArguments arguments) var items = new List((int) System.Math.Min(1024, len)); for (ulong k = 0; k < len; ++k) { + if (k > 0 && k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + if (obj.TryGetValue(k, out var kValue)) { items.Add(kValue); @@ -1231,12 +1312,22 @@ private JsValue Sort(JsValue thisObject, JsCallArguments arguments) for (uint j = 0; j < itemCount; j++) { + if (j > 0 && j % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + obj.Set(j, items[(int) j], updateLength: false, throwOnError: true); } // Holes (TryGetValue returned false) only need clearing when the input had holes. for (uint j = (uint) itemCount; j < len; ++j) { + if (j % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + obj.DeletePropertyOrThrow(j); } } @@ -1253,7 +1344,7 @@ private JsValue Sort(JsValue thisObject, JsCallArguments arguments) /// per comparison, which is O(n log n) ToString allocations. Cache the coerced key once per /// element (Schwartzian transform), then sort indices stably (tiebreak by original index). /// - private static void SortByCachedStringKeys(List items) + private void SortByCachedStringKeys(List items) { var itemCount = items.Count; if (itemCount <= 1) @@ -1264,6 +1355,12 @@ private static void SortByCachedStringKeys(List items) var keys = new string?[itemCount]; for (var i = 0; i < itemCount; i++) { + // ToString coercion per element can be costly; check constraints periodically. + if (i > 0 && i % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var v = items[i]; // Spec: undefined elements sort to the end and are not coerced via ToString. keys[i] = v.IsUndefined() ? null : TypeConverter.ToString(v); @@ -1373,6 +1470,11 @@ private JsValue Slice(JsValue thisObject, JsCallArguments arguments) var operations = ArrayOperations.For(a, forWrite: true); for (uint n = 0; k < final; k++, n++) { + if (n > 0 && n % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + if (o.TryGetValue(k, out var kValue)) { operations.CreateDataPropertyOrThrow(n, kValue); @@ -1407,6 +1509,11 @@ private JsValue Shift(JsValue thisObject) var firstSlow = o.Get(0); for (uint k = 1; k < len; k++) { + if (k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var to = k - 1; if (o.TryGetValue(k, out var fromVal)) { @@ -1504,26 +1611,38 @@ static string StringFromJsValue(JsValue value) : TypeConverter.ToString(value); } - var s = StringFromJsValue(o.Get(0)); - if (len == 1) + // try/finally so a Constraints.Check() throw inside the loop cannot leak the entry on the + // long-lived join stack (which would corrupt cyclic-join detection for later calls). + try { - _joinStack.Exit(); - return s; - } + var s = StringFromJsValue(o.Get(0)); + if (len == 1) + { + return s; + } - using var sb = new ValueStringBuilder(); - sb.Append(s); - for (uint k = 1; k < len; k++) - { - if (sep != "") + using var sb = new ValueStringBuilder(); + sb.Append(s); + for (uint k = 1; k < len; k++) { - sb.Append(sep); + if (k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + + if (sep != "") + { + sb.Append(sep); + } + sb.Append(StringFromJsValue(o.Get(k))); } - sb.Append(StringFromJsValue(o.Get(k))); - } - _joinStack.Exit(); - return sb.ToString(); + return sb.ToString(); + } + finally + { + _joinStack.Exit(); + } } /// @@ -1551,22 +1670,35 @@ private JsValue ToLocaleString(JsValue thisObject, JsCallArguments arguments) var options = arguments.At(1); var invokeArgs = new[] { locales, options }; - using var r = new ValueStringBuilder(); - for (uint k = 0; k < len; k++) + // try/finally so a Constraints.Check() throw inside the loop cannot leak the entry on the + // long-lived join stack (which would corrupt cyclic-join detection for later calls). + try { - if (k > 0) - { - r.Append(Separator); - } - if (array.TryGetValue(k, out var nextElement) && !nextElement.IsNullOrUndefined()) + using var r = new ValueStringBuilder(); + for (uint k = 0; k < len; k++) { - var s = TypeConverter.ToString(Invoke(nextElement, "toLocaleString", invokeArgs)); - r.Append(s); + if (k > 0) + { + if (k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + + r.Append(Separator); + } + if (array.TryGetValue(k, out var nextElement) && !nextElement.IsNullOrUndefined()) + { + var s = TypeConverter.ToString(Invoke(nextElement, "toLocaleString", invokeArgs)); + r.Append(s); + } } - } - _joinStack.Exit(); - return r.ToString(); + return r.ToString(); + } + finally + { + _joinStack.Exit(); + } } /// @@ -1614,6 +1746,11 @@ private JsValue Concat(JsValue thisObject, JsCallArguments arguments) for (uint k = 0; k < len; k++) { + if (k > 0 && k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + operations.TryGetValue(k, out var subElement); aOperations.CreateDataPropertyOrThrow(n, subElement); n++; @@ -1856,6 +1993,11 @@ private JsValue ToSpliced(JsValue thisObject, JsCallArguments arguments) while (i < actualStart) { + if (i > 0 && i % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + a.SetIndexValue(i, o.Get(i), updateLength: false); i++; } @@ -1868,6 +2010,11 @@ private JsValue ToSpliced(JsValue thisObject, JsCallArguments arguments) while (i < newLen) { + if (i % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var fromValue = o.Get(r); a.SetIndexValue(i, fromValue, updateLength: false); @@ -1939,6 +2086,11 @@ private JsValue ReduceRight(JsValue thisObject, JsCallArguments arguments) var kPresent = false; while (kPresent == false && k >= 0) { + if (k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + if ((kPresent = o.TryGetValue((ulong) k, out var temp))) { accumulator = temp; @@ -1957,6 +2109,11 @@ private JsValue ReduceRight(JsValue thisObject, JsCallArguments arguments) jsValues[3] = o.Target; for (; k >= 0; k--) { + if (k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + if (o.TryGetValue((ulong) k, out var kvalue)) { jsValues[0] = accumulator; diff --git a/Jint/Native/Iterator/IteratorProtocol.cs b/Jint/Native/Iterator/IteratorProtocol.cs index f7798526d..2364882f9 100644 --- a/Jint/Native/Iterator/IteratorProtocol.cs +++ b/Jint/Native/Iterator/IteratorProtocol.cs @@ -26,10 +26,18 @@ internal bool Execute() { var args = _engine._jsValueArrayPool.RentArray(_argCount); var done = false; + var iterations = 0; try { while (ShouldContinue) { + // Check constraints periodically so a huge (or native-backed) iterator cannot run + // uninterrupted; the surrounding try closes the iterator and returns the pool array. + if (++iterations % Engine.ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + if (!_iterator.TryIteratorStep(out var item)) { done = true; @@ -79,10 +87,18 @@ internal static void AddEntriesFromIterable(ObjectInstance target, IteratorInsta var args = target.Engine._jsValueArrayPool.RentArray(2); var skipClose = true; + var iterations = 0; try { do { + // Check constraints periodically so a huge (or native-backed) iterable cannot run + // uninterrupted (covers new Map/WeakMap and Object.fromEntries). + if (++iterations % Engine.ConstraintCheckInterval == 0) + { + target.Engine.Constraints.Check(); + } + if (!iterable.TryIteratorStep(out var nextItem)) { return; diff --git a/Jint/Native/JsMap.cs b/Jint/Native/JsMap.cs index 9b9926e22..c9aa94dac 100644 --- a/Jint/Native/JsMap.cs +++ b/Jint/Native/JsMap.cs @@ -95,8 +95,15 @@ internal void ForEach(ICallable callable, JsValue thisArg) args[2] = this; var i = 0; + var iterations = 0; while (i < _map.Count) { + // A native (CLR) callback does not self-throttle via statement checks; check periodically. + if (++iterations % Engine.ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var key = _map.GetKey(i); args[0] = _map[key]; args[1] = key; diff --git a/Jint/Native/JsSet.cs b/Jint/Native/JsSet.cs index 4ccd60981..e6d2d1f98 100644 --- a/Jint/Native/JsSet.cs +++ b/Jint/Native/JsSet.cs @@ -62,8 +62,15 @@ internal void ForEach(ICallable callable, JsValue thisArg) args[2] = this; var i = 0; + var iterations = 0; while (i < _set._list.Count) { + // A native (CLR) callback does not self-throttle via statement checks; check periodically. + if (++iterations % Engine.ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var value = _set._list[i]; args[0] = value; args[1] = value; diff --git a/Jint/Native/Json/JsonParser.cs b/Jint/Native/Json/JsonParser.cs index 7091021d5..99a0749e2 100644 --- a/Jint/Native/Json/JsonParser.cs +++ b/Jint/Native/Json/JsonParser.cs @@ -39,6 +39,8 @@ public JsonParseResult(JsValue value, JsonParseNode? node) public sealed class JsonParser { + private const int ConstraintCheckInterval = Engine.ConstraintCheckInterval; + private readonly Engine _engine; private readonly int _maxDepth; @@ -338,8 +340,14 @@ private Token ScanStringLiteral(ref State state) ++_index; using var sb = new ValueStringBuilder(stackalloc char[64]); + var scanned = 0; while (_index < _length) { + if (++scanned % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + char ch = _source[_index++]; if (ch == quote) @@ -563,8 +571,14 @@ when its full. JsValue[] buffer = ArrayPool.Shared.Rent(16); try { + var elementCount = 0; while (!Match(']')) { + if (++elementCount % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + buffer[bufferIndex++] = ParseJsonValue(ref state); if (!Match(']')) @@ -638,8 +652,14 @@ private JsObject ParseJsonObject(ref State state) var obj = new JsObject(_engine); + var memberCount = 0; while (!Match('}')) { + if (++memberCount % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + Tokens type = _lookahead.Type; if (type != Tokens.String) { @@ -816,8 +836,14 @@ private JsonParseResult ParseJsonArrayWithSourceInfo(ref State state) JsValue[] buffer = ArrayPool.Shared.Rent(16); try { + var elementCount = 0; while (!Match(']')) { + if (++elementCount % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var elementResult = ParseJsonValueWithSourceInfo(ref state); buffer[bufferIndex++] = elementResult.Value; if (elementResult.Node != null) @@ -900,8 +926,14 @@ private JsonParseResult ParseJsonObjectWithSourceInfo(ref State state) var obj = new JsObject(_engine); + var memberCount = 0; while (!Match('}')) { + if (++memberCount % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + Tokens type = _lookahead.Type; if (type != Tokens.String) { diff --git a/Jint/Native/Json/JsonSerializer.cs b/Jint/Native/Json/JsonSerializer.cs index 5bc668330..98aecbc06 100644 --- a/Jint/Native/Json/JsonSerializer.cs +++ b/Jint/Native/Json/JsonSerializer.cs @@ -15,6 +15,8 @@ namespace Jint.Native.Json; public sealed class JsonSerializer { + private const int ConstraintCheckInterval = Engine.ConstraintCheckInterval; + private readonly Engine _engine; private ObjectTraverseStack _stack = null!; private string? _indent; @@ -87,6 +89,11 @@ private void SetupReplacer(JsValue replacer) var k = 0; while (k < len) { + if (k > 0 && k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var prop = JsString.Create(k); var v = replacer.Get(prop); var item = JsValue.Undefined; @@ -441,6 +448,11 @@ private void SerializeJSONArray(ObjectInstance value, ref ValueStringBuilder jso for (int i = 0; i < len; i++) { + if (i > 0 && i % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + if (hasPrevious) { json.Append(separator); @@ -508,6 +520,11 @@ private void SerializeJSONObject(ObjectInstance value, ref ValueStringBuilder js var hasPrevious = false; for (var i = 0; i < enumeration.Keys.Count; i++) { + if (i > 0 && i % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var p = enumeration.Keys[i]; int position = json.Length; diff --git a/Jint/Native/Object/ObjectConstructor.cs b/Jint/Native/Object/ObjectConstructor.cs index 92beaf1fa..3bf0ae9b1 100644 --- a/Jint/Native/Object/ObjectConstructor.cs +++ b/Jint/Native/Object/ObjectConstructor.cs @@ -44,8 +44,16 @@ private ObjectInstance Assign(JsValue thisObject, JsValue target, [Rest] ReadOnl var from = TypeConverter.ToObject(_realm, nextSource); var keys = from.GetOwnPropertyKeys(); + var processed = 0; foreach (var nextKey in keys) { + // Pure native key copy over a JS-controlled key count; check constraints periodically. + if (processed > 0 && processed % Engine.ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + processed++; + var desc = from.GetOwnProperty(nextKey); if (desc != PropertyDescriptor.Undefined && desc.Enumerable) { diff --git a/Jint/Native/Object/ObjectInstance.cs b/Jint/Native/Object/ObjectInstance.cs index 39f0acaf6..dcaa7679f 100644 --- a/Jint/Native/Object/ObjectInstance.cs +++ b/Jint/Native/Object/ObjectInstance.cs @@ -1196,45 +1196,62 @@ bool TryGetValue(ulong idx, out JsValue jsValue) 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 (ulong k = 0; k < length; k++) + if (!fromEnd) { - if (TryGetValue(k, out var kvalue) || visitUnassigned) + for (ulong k = 0; k < length; k++) { - 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) + { + args[0] = kvalue; + args[1] = k; + var testResult = callable.Call(thisArg, args); + if (TypeConverter.ToBoolean(testResult)) + { + index = k; + value = kvalue; + return true; + } } } } - } - else - { - for (var k = (long) (length - 1); k >= 0; k--) + else { - if (TryGetValue((ulong) k, out var kvalue) || visitUnassigned) + for (var k = (long) (length - 1); k >= 0; k--) { - kvalue ??= Undefined; - args[0] = kvalue; - args[1] = k; - var testResult = callable.Call(thisArg, args); - if (TypeConverter.ToBoolean(testResult)) + if (k % Engine.ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + + if (TryGetValue((ulong) k, out var kvalue) || visitUnassigned) { - index = (ulong) k; - value = kvalue; - return true; + kvalue ??= Undefined; + args[0] = kvalue; + args[1] = k; + var testResult = callable.Call(thisArg, args); + if (TypeConverter.ToBoolean(testResult)) + { + index = (ulong) k; + value = kvalue; + return true; + } } } } } - - _engine._jsValueArrayPool.ReturnArray(args); + finally + { + _engine._jsValueArrayPool.ReturnArray(args); + } index = 0; value = Undefined; @@ -1477,6 +1494,12 @@ internal JsArray EnumerableOwnProperties(EnumerableOwnPropertyNamesKind kind) for (var i = 0; i < ownKeys.Count; i++) { + // Pure native enumeration over a JS-controlled key count; check constraints periodically. + if (i > 0 && i % Engine.ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var property = ownKeys[i]; if (!property.IsString()) diff --git a/Jint/Native/RegExp/RegExpPrototype.cs b/Jint/Native/RegExp/RegExpPrototype.cs index 3b7c1e220..962e75fea 100644 --- a/Jint/Native/RegExp/RegExpPrototype.cs +++ b/Jint/Native/RegExp/RegExpPrototype.cs @@ -17,6 +17,8 @@ namespace Jint.Native.RegExp; [JsObject(ExtraCapacity = 1)] internal sealed partial class RegExpPrototype : Prototype { + private const int ConstraintCheckInterval = Engine.ConstraintCheckInterval; + private static readonly JsString PropertyExec = new("exec"); private static readonly JsString PropertyIndex = new("index"); private static readonly JsString PropertyInput = new("input"); @@ -214,6 +216,11 @@ private JsValue Replace(JsValue thisObject, JsValue stringArg, JsValue replaceVa while (count < maxCount && searchStart <= s.Length) { + if (count > 0 && count % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var result = ExecuteWithTimeout(customRei, customEngine, s, searchStart); if (!result.Success) { @@ -303,8 +310,14 @@ string Evaluator(Match match) var results = new List(); + var matchCount = 0; while (true) { + if (++matchCount % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var result = RegExpExec(rx, s); if (result.IsNull()) { @@ -332,6 +345,11 @@ string Evaluator(Match match) var captures = new List(); for (var i = 0; i < results.Count; i++) { + if (i > 0 && i % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var result = results[i]; var nCaptures = (int) result.GetLength(); nCaptures = System.Math.Max(nCaptures - 1, 0); @@ -568,15 +586,21 @@ private JsValue Split(JsValue thisObject, JsValue stringArg, JsValue limit) if (string.Equals(R.Source, JsRegExp.regExpForMatchingAllCharacters, StringComparison.Ordinal)) { // if empty string, just a string split - return StringPrototype.SplitWithStringSeparator(_realm, "", s, (uint) s.Length); + return StringPrototype.SplitWithStringSeparator(_engine, _realm, "", s, (uint) s.Length); } var a = _realm.Intrinsics.Array.Construct(Arguments.Empty); int lastIndex = 0; uint index = 0; + var matchCount = 0; for (var match = R.Value.Match(s, 0); match.Success; match = match.NextMatch()) { + if (++matchCount % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + if (match.Length == 0 && (match.Index == 0 || match.Index == s.Length || match.Index == lastIndex)) { continue; @@ -624,8 +648,14 @@ private JsArray SplitSlow(string s, ObjectInstance splitter, bool unicodeMatchin var a = _realm.Intrinsics.Array.ArrayCreate(0); ulong previousStringIndex = 0; ulong currentIndex = 0; + var iterations = 0; while (currentIndex < (ulong) s.Length) { + if (++iterations % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + splitter.Set(JsRegExp.PropertyLastIndex, currentIndex, true); var z = RegExpExec(splitter, s); if (z.IsNull()) @@ -842,6 +872,11 @@ private JsValue Match(JsValue thisObject, JsValue stringArg) int lastIndex = 0; while (lastIndex <= s.Length) { + if (n > 0 && n % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var result = ExecuteWithTimeout(rei, customEngine, s, lastIndex); if (!result.Success) { @@ -884,6 +919,11 @@ private JsValue Match(JsValue thisObject, JsValue stringArg) uint li = 0; while (true) { + if (li > 0 && li % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + match = match.NextMatch(); if (!match.Success || match.Index != ++li) break; @@ -904,6 +944,11 @@ private JsValue Match(JsValue thisObject, JsValue stringArg) a.SetLength((uint) matches.Count); for (var i = 0; i < matches.Count; i++) { + if (i > 0 && i % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + a.SetIndexValue((uint) i, matches[i].Value, updateLength: false); } return a; @@ -919,6 +964,11 @@ private JsValue MatchSlow(ObjectInstance rx, string s, bool fullUnicode) uint n = 0; while (true) { + if (n > 0 && n % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var result = RegExpExec(rx, s); if (result.IsNull()) { @@ -1074,8 +1124,14 @@ private static JsValue RegExpBuiltinExec(JsRegExp R, string s) } var startAt = (int) lastIndex; + var iterations = 0; while (true) { + if (++iterations % ConstraintCheckInterval == 0) + { + R.Engine.Constraints.Check(); + } + match = R.Value.Match(s, startAt); // The conversion of Unicode regex patterns to .NET Regex has some flaws: diff --git a/Jint/Native/Set/SetConstructor.cs b/Jint/Native/Set/SetConstructor.cs index 789a24958..c60a988be 100644 --- a/Jint/Native/Set/SetConstructor.cs +++ b/Jint/Native/Set/SetConstructor.cs @@ -50,8 +50,15 @@ public override ObjectInstance Construct(JsCallArguments arguments, JsValue newT try { var args = new JsValue[1]; + var iterations = 0; do { + // Check constraints periodically so a huge (or native-backed) iterable cannot run uninterrupted. + if (++iterations % Engine.ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + if (!iterable.TryIteratorStep(out var next)) { return set; diff --git a/Jint/Native/String/StringConstructor.cs b/Jint/Native/String/StringConstructor.cs index eff2b359b..667ee0b6b 100644 --- a/Jint/Native/String/StringConstructor.cs +++ b/Jint/Native/String/StringConstructor.cs @@ -16,6 +16,8 @@ namespace Jint.Native.String; [JsObject] internal sealed partial class StringConstructor : Constructor { + private const int ConstraintCheckInterval = Engine.ConstraintCheckInterval; + private static readonly JsString _functionName = new JsString("String"); public StringConstructor( @@ -39,7 +41,7 @@ public StringConstructor( /// https://tc39.es/ecma262/#sec-string.fromcharcode /// [JsFunction(Length = 1)] - private static JsValue FromCharCode(JsValue thisObject, JsCallArguments arguments) + private JsValue FromCharCode(JsValue thisObject, JsCallArguments arguments) { var length = arguments.Length; @@ -53,6 +55,8 @@ private static JsValue FromCharCode(JsValue thisObject, JsCallArguments argument return JsString.Create((char) TypeConverter.ToUint16(arguments[0])); } + // length is arguments.Length, already bounded by the materialized arguments array, so no + // size cap is needed here (unlike padStart/repeat whose length comes from a JS number). #if SUPPORTS_SPAN_PARSE var elements = length < 512 ? stackalloc char[length] : new char[length]; #else @@ -60,6 +64,12 @@ private static JsValue FromCharCode(JsValue thisObject, JsCallArguments argument #endif for (var i = 0; i < elements.Length; i++) { + // Spread can inflate the argument count; check constraints periodically. + if (i > 0 && i % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var nextCu = TypeConverter.ToUint16(arguments[i]); elements[i] = (char) nextCu; } @@ -75,8 +85,16 @@ private JsValue FromCodePoint(JsValue thisObject, JsCallArguments arguments) { JsNumber codePoint; using var result = new ValueStringBuilder(stackalloc char[128]); + var processed = 0; foreach (var a in arguments) { + // Spread can inflate the argument count; check constraints periodically. + if (processed > 0 && processed % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + processed++; + int point; codePoint = TypeConverter.ToJsNumber(a); if (codePoint.IsInteger()) @@ -139,8 +157,14 @@ private JsValue Raw(JsValue thisObject, JsCallArguments arguments) using var result = new ValueStringBuilder(); for (var i = 0; i < length; i++) { + // Check constraints periodically so a huge raw.length cannot run uninterrupted. if (i > 0) { + if (i % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + if (i < arguments.Length && !arguments[i].IsUndefined()) { result.Append(TypeConverter.ToString(arguments[i])); diff --git a/Jint/Native/String/StringPrototype.cs b/Jint/Native/String/StringPrototype.cs index 910c8015a..60941a646 100644 --- a/Jint/Native/String/StringPrototype.cs +++ b/Jint/Native/String/StringPrototype.cs @@ -22,6 +22,8 @@ namespace Jint.Native.String; [JsObject(ExtraCapacity = 2)] internal sealed partial class StringPrototype : StringInstance { + private const int ConstraintCheckInterval = Engine.ConstraintCheckInterval; + private readonly Realm _realm; [JsProperty(Name = "constructor", Flags = PropertyFlag.NonEnumerable)] @@ -240,7 +242,7 @@ private JsValue ToLocaleUpperCase(JsValue thisObject, JsCallArguments arguments) [JsFunction] [RequireObjectCoercible] - private static JsValue ToUpperCase(JsValue thisObject) + private JsValue ToUpperCase(JsValue thisObject) { var s = TypeConverter.ToString(thisObject); return new JsString(ToUpperCaseWithSpecialCasing(s, CultureInfo.InvariantCulture)); @@ -251,7 +253,7 @@ private static JsValue ToUpperCase(JsValue thisObject) /// expansions (e.g. ß → SS, ff → FF, Greek titlecase → upper + Ι). /// https://www.unicode.org/Public/UCD/latest/ucd/SpecialCasing.txt /// - private static string ToUpperCaseWithSpecialCasing(string s, CultureInfo culture) + private string ToUpperCaseWithSpecialCasing(string s, CultureInfo culture) { // Fast path: no codepoint in the string has a SpecialCasing expansion. if (!NeedsUpperSpecialCasing(s)) @@ -264,6 +266,11 @@ private static string ToUpperCaseWithSpecialCasing(string s, CultureInfo culture var sb = new ValueStringBuilder(stackBuffer); for (var i = 0; i < s.Length; i++) { + if (i > 0 && i % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var c = s[i]; var mapped = GetSpecialUpperCasing(c); if (mapped is not null) @@ -438,7 +445,7 @@ private JsValue ToLocaleLowerCase(JsValue thisObject, JsCallArguments arguments) [JsFunction] [RequireObjectCoercible] - private static JsValue ToLowerCase(JsValue thisObject) + private JsValue ToLowerCase(JsValue thisObject) { var s = TypeConverter.ToString(thisObject); return ToLowerCaseWithSpecialCasing(s, CultureInfo.InvariantCulture); @@ -449,7 +456,7 @@ private static JsValue ToLowerCase(JsValue thisObject) /// Handles Final_Sigma, Turkish/Azeri I-dot, Lithuanian soft-dotted, and İ decomposition. /// https://unicode.org/reports/tr21/tr21-5.html#SpecialCasing /// - private static string ToLowerCaseWithSpecialCasing(string s, CultureInfo culture) + private string ToLowerCaseWithSpecialCasing(string s, CultureInfo culture) { var langName = culture.TwoLetterISOLanguageName; var isTurkishOrAzeri = string.Equals(langName, "tr", StringComparison.Ordinal) || @@ -468,6 +475,11 @@ private static string ToLowerCaseWithSpecialCasing(string s, CultureInfo culture for (var i = 0; i < s.Length; i++) { + if (i > 0 && i % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var c = s[i]; // Greek Final_Sigma (all locales) @@ -1099,10 +1111,10 @@ private JsValue Split(JsValue thisObject, JsCallArguments arguments) return arrayInstance; } - return SplitWithStringSeparator(_realm, separator, s, lim); + return SplitWithStringSeparator(_engine, _realm, separator, s, lim); } - internal static JsValue SplitWithStringSeparator(Realm realm, JsValue separator, string s, uint lim) + internal static JsValue SplitWithStringSeparator(Engine engine, Realm realm, JsValue separator, string s, uint lim) { var segments = StringExecutionContext.Current.SplitSegmentList; segments.Clear(); @@ -1117,6 +1129,11 @@ internal static JsValue SplitWithStringSeparator(Realm realm, JsValue separator, for (var i = 0; i < s.Length; i++) { + if (i > 0 && i % ConstraintCheckInterval == 0) + { + engine.Constraints.Check(); + } + segments.Add(TypeConverter.ToString(s[i])); } } @@ -1131,6 +1148,11 @@ internal static JsValue SplitWithStringSeparator(Realm realm, JsValue separator, var a = realm.Intrinsics.Array.ArrayCreate(length); for (int i = 0; i < length; i++) { + if (i > 0 && i % ConstraintCheckInterval == 0) + { + engine.Constraints.Check(); + } + a.SetIndexValue((uint) i, segments[i], updateLength: false); } @@ -1353,9 +1375,15 @@ static int StringIndexOf(string s, string search, int fromIndex) var endOfLastMatch = 0; using var result = new ValueStringBuilder(); + var matchCount = 0; var position = StringIndexOf(thisString, searchString, 0); while (position != -1) { + if (++matchCount % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + string replacement; var preserved = thisString.Substring(endOfLastMatch, position - endOfLastMatch); if (functionalReplace) @@ -1657,9 +1685,9 @@ private JsValue ValueOf(JsValue thisObject) /// [JsFunction(Length = 1)] [RequireObjectCoercible] - private static JsValue PadStart(JsValue thisObject, JsCallArguments arguments) + private JsValue PadStart(JsValue thisObject, JsCallArguments arguments) { - return StringPad(thisObject, arguments, true); + return StringPad(thisObject, arguments, padStart: true); } /// @@ -1667,39 +1695,79 @@ private static JsValue PadStart(JsValue thisObject, JsCallArguments arguments) /// [JsFunction(Length = 1)] [RequireObjectCoercible] - private static JsValue PadEnd(JsValue thisObject, JsCallArguments arguments) + private JsValue PadEnd(JsValue thisObject, JsCallArguments arguments) { - return StringPad(thisObject, arguments, false); + return StringPad(thisObject, arguments, padStart: false); } /// /// https://tc39.es/ecma262/#sec-stringpad /// - private static JsValue StringPad(JsValue thisObject, JsCallArguments arguments, bool padStart) + private JsValue StringPad(JsValue thisObject, JsCallArguments arguments, bool padStart) { var s = TypeConverter.ToJsString(thisObject); - var targetLength = TypeConverter.ToInt32(arguments.At(0)); - var padStringValue = arguments.At(1); + // ToLength (not ToInt32) so large values don't wrap and bypass the size cap below. + var intMaxLength = TypeConverter.ToLength(arguments.At(0)); + if (intMaxLength <= (ulong) s.Length) + { + return s; + } + // Per spec the fill string is resolved (and its ToString side effect runs) only after the + // length check above; the previous implementation evaluated it too early. + var padStringValue = arguments.At(1); var padString = padStringValue.IsUndefined() ? " " : TypeConverter.ToString(padStringValue); - if (s.Length > targetLength || padString.Length == 0) + if (padString.Length == 0) { return s; } - targetLength -= s.Length; - if (targetLength > padString.Length) + // Convert a would-be OutOfMemoryException into a catchable RangeError, mirroring repeat. + if (intMaxLength > ClrLimits.MaxArrayLength) { - padString = string.Join("", System.Linq.Enumerable.Repeat(padString, (targetLength / padString.Length) + 1)); + Throw.RangeError(_realm, "Invalid string length"); + } + + var targetLength = (int) intMaxLength; + var sourceString = s.ToString(); + var fillLength = targetLength - sourceString.Length; + + // Build incrementally: do not pre-rent the full (possibly huge) target up front, and check + // the engine constraints periodically so timeout/memory/statements limits can interrupt a + // large fill instead of allocating until the CLR throws OutOfMemoryException. + // 'using' so the pooled buffer is returned even when a periodic Check() throws mid-fill. + using var sb = new ValueStringBuilder(System.Math.Min(targetLength, ConstraintCheckInterval)); + if (!padStart) + { + sb.Append(sourceString); } - return padStart - ? $"{padString.Substring(0, targetLength)}{s}" - : $"{s}{padString.Substring(0, targetLength)}"; + var remaining = fillLength; + var sinceCheck = 0; + var padSpan = padString.AsSpan(); + while (remaining > 0) + { + var take = System.Math.Min(padSpan.Length, remaining); + sb.Append(take == padSpan.Length ? padSpan : padSpan.Slice(0, take)); + remaining -= take; + sinceCheck += take; + if (sinceCheck >= ConstraintCheckInterval && remaining > 0) + { + sinceCheck = 0; + _engine.Constraints.Check(); + } + } + + if (padStart) + { + sb.Append(sourceString); + } + + return sb.ToString(); } /// @@ -1872,9 +1940,15 @@ private JsValue Repeat(JsValue thisObject, JsCallArguments arguments) return new string(s[0], (int) n); } - var sb = new ValueStringBuilder((int) resultLength); + // 'using' so the pooled buffer is returned even when a periodic Check() throws mid-build. + using var sb = new ValueStringBuilder((int) resultLength); for (var i = 0; i < n; ++i) { + if (i > 0 && i % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + sb.Append(s); } @@ -1892,16 +1966,22 @@ private static JsValue IsWellFormed(JsValue thisObject) [JsFunction] [RequireObjectCoercible] - private static JsValue ToWellFormed(JsValue thisObject) + private JsValue ToWellFormed(JsValue thisObject) { var s = TypeConverter.ToString(thisObject); var strLen = s.Length; var k = 0; - var result = new ValueStringBuilder(); + // 'using' so the pooled buffer is returned even when a periodic Check() throws mid-build. + using var result = new ValueStringBuilder(); while (k < strLen) { + if (k > 0 && k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var cp = CodePointAt(s, k); if (cp.IsUnpairedSurrogate) { diff --git a/Jint/Native/TypedArray/IntrinsicTypedArrayPrototype.cs b/Jint/Native/TypedArray/IntrinsicTypedArrayPrototype.cs index 3c16ce0fb..a871b5387 100644 --- a/Jint/Native/TypedArray/IntrinsicTypedArrayPrototype.cs +++ b/Jint/Native/TypedArray/IntrinsicTypedArrayPrototype.cs @@ -21,7 +21,7 @@ namespace Jint.Native.TypedArray; [JsObject(ExtraCapacity = 1)] internal sealed partial class IntrinsicTypedArrayPrototype : Prototype { - private const int ConstraintCheckInterval = 10_000; + private const int ConstraintCheckInterval = Engine.ConstraintCheckInterval; [JsProperty(Name = "constructor", Flags = PropertyFlag.NonEnumerable)] private readonly IntrinsicTypedArrayConstructor _constructor; @@ -394,6 +394,11 @@ private JsValue Every(JsValue thisObject, JsValue callbackFn, JsValue thisArg) args[2] = o; for (var k = 0; k < len; k++) { + if (k > 0 && k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + args[0] = o[k]; args[1] = k; if (!TypeConverter.ToBoolean(predicate.Call(thisArg, args))) @@ -501,6 +506,11 @@ private JsValue Filter(JsValue thisObject, JsValue callbackFn, JsValue thisArg) args[2] = o; for (var k = 0; k < len; k++) { + if (k > 0 && k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var kValue = o[k]; args[0] = kValue; args[1] = k; @@ -572,6 +582,11 @@ private KeyValuePair DoFind(JsValue thisObject, JsValue predic { for (var k = 0; k < len; k++) { + if (k > 0 && k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var kNumber = JsNumber.Create(k); var kValue = o[k]; args[0] = kValue; @@ -586,6 +601,11 @@ private KeyValuePair DoFind(JsValue thisObject, JsValue predic { for (var k = (int) (len - 1); k >= 0; k--) { + if (k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var kNumber = JsNumber.Create(k); var kValue = o[k]; args[0] = kValue; @@ -616,6 +636,11 @@ private JsValue ForEach(JsValue thisObject, JsValue callbackFn, JsValue thisArg) args[2] = o; for (var k = 0; k < len; k++) { + if (k > 0 && k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var kValue = o[k]; args[0] = kValue; args[1] = k; @@ -669,6 +694,11 @@ private JsValue Includes(JsValue thisObject, JsValue searchElement, JsValue from while (k < len) { + if (k > 0 && k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var value = o[(int) k]; if (SameValueZeroComparer.Equals(value, searchElement)) { @@ -722,6 +752,11 @@ private JsValue IndexOf(JsValue thisObject, JsValue searchElement, JsValue fromI for (; k < len; k++) { + if (k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var kPresent = o.HasProperty(k); if (kPresent) { @@ -770,6 +805,11 @@ static string StringFromJsValue(JsValue value) result.Append(s); for (var k = 1; k < len; k++) { + if (k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + result.Append(sep); result.Append(StringFromJsValue(o[k])); } @@ -828,6 +868,11 @@ private JsValue LastIndexOf(JsValue thisObject, JsCallArguments arguments) for (; k >= 0; k--) { + if (k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var kPresent = o.HasProperty(k); if (kPresent) { @@ -859,6 +904,11 @@ private ObjectInstance Map(JsValue thisObject, JsValue callbackFn, JsValue thisA args[2] = o; for (var k = 0; k < len; k++) { + if (k > 0 && k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + args[0] = o[k]; args[1] = k; var mappedValue = callable.Call(thisArg, args); @@ -905,6 +955,11 @@ private JsValue Reduce(JsValue thisObject, JsCallArguments arguments) args[3] = o; while (k < len) { + if (k > 0 && k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var kValue = o[k]; args[0] = accumulator; args[1] = kValue; @@ -953,6 +1008,11 @@ private JsValue ReduceRight(JsValue thisObject, JsCallArguments arguments) jsValues[3] = o; for (; k >= 0; k--) { + if (k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + jsValues[0] = accumulator; jsValues[1] = o[(int) k]; jsValues[2] = k; @@ -1089,6 +1149,7 @@ private void SetTypedArrayFromTypedArray(JsTypedArray target, double targetOffse var targetByteIndex = (int) (targetOffset * targetElementSize + targetByteOffset); var limit = targetByteIndex + targetElementSize * srcLength; + var processed = 0; if (srcType == targetType) { // NOTE: If srcType and targetType are the same, the transfer must be performed in a manner that preserves the bit-level encoding of the source data. @@ -1098,6 +1159,11 @@ private void SetTypedArrayFromTypedArray(JsTypedArray target, double targetOffse targetBuffer.SetValueInBuffer(targetByteIndex, TypedArrayElementType.Uint8, value, isTypedArray: true, ArrayBufferOrder.Unordered); srcByteIndex += 1; targetByteIndex += 1; + + if (++processed % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } } } else @@ -1108,6 +1174,11 @@ private void SetTypedArrayFromTypedArray(JsTypedArray target, double targetOffse targetBuffer.SetValueInBuffer(targetByteIndex, targetType, value, isTypedArray: true, ArrayBufferOrder.Unordered); srcByteIndex += srcElementSize; targetByteIndex += targetElementSize; + + if (++processed % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } } } } @@ -1143,6 +1214,11 @@ private void SetTypedArrayFromArrayLike(JsTypedArray target, int targetOffset, J var k = 0; while (k < srcLength) { + if (k > 0 && k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var jsValue = src.Get((ulong) k); target.IntegerIndexedElementSet(targetOffset + k, jsValue); k++; @@ -1242,6 +1318,11 @@ private JsValue Slice(JsValue thisObject, JsValue start, JsValue end) var n = 0; while (startIndex < endIndex) { + if (n > 0 && n % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + var kValue = o[startIndex]; a[n] = kValue; startIndex++; @@ -1257,12 +1338,18 @@ private JsValue Slice(JsValue thisObject, JsValue start, JsValue end) var targetByteIndex = a._byteOffset; var srcByteIndex = (int) startIndex * elementSize + srcByteOffset; var limit = targetByteIndex + countBytes * elementSize; + var copied = 0; while (targetByteIndex < limit) { var value = srcBuffer.GetValueFromBuffer(srcByteIndex, TypedArrayElementType.Uint8, true, ArrayBufferOrder.Unordered); targetBuffer.SetValueInBuffer(targetByteIndex, TypedArrayElementType.Uint8, value, true, ArrayBufferOrder.Unordered); srcByteIndex++; targetByteIndex++; + + if (++copied % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } } } } @@ -1286,6 +1373,11 @@ private JsValue Some(JsValue thisObject, JsValue callbackFn, JsValue thisArg) args[2] = o; for (var k = 0; k < len; k++) { + if (k > 0 && k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + args[0] = o[k]; args[1] = k; if (TypeConverter.ToBoolean(callbackfn.Call(thisArg, args))) @@ -1328,6 +1420,11 @@ private JsValue Sort(JsValue thisObject, JsValue compareFnArg) for (var i = 0; i < (uint) array.Length; ++i) { + if (i > 0 && i % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + o[i] = array[i]; } @@ -1448,6 +1545,11 @@ private JsValue ToLocaleString(JsValue thisObject, JsValue locales, JsValue opti { if (k > 0) { + if (k % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + r.Append(Separator); } if (array.TryGetValue(k, out var nextElement) && !nextElement.IsNullOrUndefined()) @@ -1525,6 +1627,11 @@ private JsValue ToSorted(JsValue thisObject, JsValue compareFnArg) var array = SortArray(buffer, compareFn, o); for (var i = 0; (uint) i < (uint) array.Length; ++i) { + if (i > 0 && i % ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + a[i] = array[i]; } diff --git a/Jint/Native/WeakSet/WeakSetConstructor.cs b/Jint/Native/WeakSet/WeakSetConstructor.cs index 0e9a3eb90..f9801d839 100644 --- a/Jint/Native/WeakSet/WeakSetConstructor.cs +++ b/Jint/Native/WeakSet/WeakSetConstructor.cs @@ -44,8 +44,16 @@ public override ObjectInstance Construct(JsCallArguments arguments, JsValue newT // check fast path if (arg1 is JsArray array && ReferenceEquals(adder, _engine.Realm.Intrinsics.WeakSet.PrototypeObject.OriginalAddFunction)) { + var index = 0; foreach (var value in array) { + // Check constraints periodically so a huge source array cannot run uninterrupted. + if (index > 0 && index % Engine.ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + index++; + set.WeakSetAdd(value); } @@ -62,8 +70,15 @@ public override ObjectInstance Construct(JsCallArguments arguments, JsValue newT try { var args = new JsValue[1]; + var iterations = 0; do { + // Check constraints periodically so a huge (or native-backed) iterable cannot run uninterrupted. + if (++iterations % Engine.ConstraintCheckInterval == 0) + { + _engine.Constraints.Check(); + } + if (!iterable.TryIteratorStep(out var next)) { return set;