Skip to content

Make bulk built-ins interruptible by execution constraints (#2486)#2487

Merged
lahma merged 2 commits into
sebastienros:mainfrom
lahma:execution-constraints-fixes
May 31, 2026
Merged

Make bulk built-ins interruptible by execution constraints (#2486)#2487
lahma merged 2 commits into
sebastienros:mainfrom
lahma:execution-constraints-fixes

Conversation

@lahma

@lahma lahma commented May 30, 2026

Copy link
Copy Markdown
Collaborator

Fixes #2486.

Problem

Several built-ins perform a full bulk operation in a single CLR call without per-step constraint checks, so a configured TimeoutInterval / MaxStatements / LimitMemory cannot interrupt them. For large inputs they either hang for seconds (no constraint can fire until the call returns) or the CLR throws System.OutOfMemoryException — which is not one of Jint's catchable constraint exceptions (TimeoutException, MemoryLimitExceededException, StatementsCountOverflowException).

The cases from the issue:

'x'.padStart(2147483647)        // hang, then OOM
'x'.padEnd(536870911, 'ab')     // hang, then OOM
Array.from({ length: 50000000 })// hang

This is the same fix class as #2349 (BigInt **) and #2465 (String.prototype.repeat, BigInt.asIntN/asUintN, Function.prototype.apply). The reporter and maintainer asked to also cover String.raw, Array.prototype.join, Array.prototype.sort, and any other affected sites, so this PR does the full sweep.

Approach

Two complementary mechanisms already present in the codebase are reused:

  1. Hard size capClrLimits.MaxArrayLength (0x7FFFFFC7). Converts a would-be OutOfMemoryException into a catchable RangeError, exactly as String.prototype.repeat already does.
  2. Periodic constraint check — the established ConstraintCheckInterval = 10_000 idiom (already used by Array.prototype.fill/reverse/with, etc.):
    if (k > 0 && k % ConstraintCheckInterval == 0) _engine.Constraints.Check();
    A periodic Check() in a native loop lets all three constraints fire: time (stopwatch), memory (allocations accumulate as the structure grows), and statements (the counter increments per Check()).

A single giant up-front allocation can't be interrupted before it OOMs, so it is either pre-capped (mechanism 1) or built incrementally with periodic checks (mechanism 2).

Changes

String.prototype.padStart / padEnd (the issue) — rewritten:

  • Use ToLength instead of ToInt32 so a large maxLength can't wrap and slip past the size cap (spec step 2).
  • Resolve fillString (and run its ToString) only after the maxLength <= stringLength early return — fixes an observable side-effect-ordering bug.
  • Throw a catchable RangeError("Invalid string length") when the result exceeds MaxArrayLength.
  • Build incrementally with a bounded initial capacity + periodic checks, instead of Enumerable.Repeat + string.Join + Substring + interpolation (~3× transient allocation).

Array.from (the issue) — periodic checks in ConstructArrayFromArrayLike and ConstructArrayFromIEnumerable.

String.raw / fromCharCode / fromCodePoint — periodic checks in the element/char loops; size cap added to fromCharCode.

Array.prototype & %TypedArray%.prototype bulk loopsjoin, sort (collection/write-back), map, filter, forEach, reduce/reduceRight, find* (via FindWithCallback), slice, splice, unshift, shift, concat, toSpliced, toLocaleString, set, and the slow indexOf/lastIndexOf/includes scans. Dense primitive fast-path scans are intentionally left unguarded (they're tight, bounded by MaxArraySize, and the one place a per-iteration check would actually cost).

RegExp.prototype@@replace / @@split / @@match outer match-collection loops.

JSONJSON.stringify breadth (array/object/replacer) and JSON.parse array/object/string-literal loops (depth was already capped; breadth was not).

Not changed (already covered / fast paths)

  • ArrayBuffer/TypedArray constructors — already capped at int.MaxValue.
  • String.prototype.repeat single-char path, new Array(N) — already capped/lazy.
  • Dense array fast-path scans — fast and MaxArraySize-bounded.

Performance

Reuses the % ConstraintCheckInterval idiom (RyuJIT lowers % const to a multiply+shift; the branch is predicted not-taken 9999/10000). The predicate is added only to loops whose body is already heavy (a JS call, property Get/Set, ToString, regex NextMatch), and the common small-input case runs the predicate a handful of times.

Measured with BenchmarkDotNet (--job short, net10.0, 1000 ops/benchmark), base (main) vs this branch:

Benchmark (1000 ops) Mean before Mean after Alloc before Alloc after
padStart (small) 223.6 µs 186.9 µs 188.7 KB 71.5 KB
padEnd (small) 231.2 µs 194.4 µs 204.3 KB 79.3 KB
filter (1000-elem) 138,494 µs 120,107 µs 48,095 KB 48,095 KB
forEach (1000-elem) 142,163 µs 150,465 µs 66,525 KB 66,525 KB
join (1000-elem) 7,230 µs 7,966 µs 7,688 KB 7,688 KB
  • padStart/padEnd: ~60% fewer allocations and slightly faster — the rewrite removes the old 3× transient allocation.
  • Guarded array methods: allocation counts are identical (the guard allocates nothing). The time deltas are within the noise of the ShortRun config — between two baseline runs filter alone varied 119,636 → 138,494 µs (±16%), which is larger than any of the before/after deltas.

Testing

  • Full Test262: 99,015 passed, 0 failed (no regressions). The 70 padStart/padEnd conformance tests pass, confirming the ToLength + side-effect-ordering changes are spec-correct.
  • Jint.Tests: 3024 passed, 0 failed, including new tests in ExecutionConstraintTests for the three issue repros plus String.raw, sort, an empty-callback forEach, padStart/padEnd correctness, and the fill-string side-effect ordering.

🤖 Generated with Claude Code

lahma and others added 2 commits May 30, 2026 13:36
…ros#2486)

Several built-ins performed a full bulk operation in a single CLR call
without per-step constraint checks, so TimeoutInterval / MaxStatements /
LimitMemory could not interrupt them. For large inputs they either hung
for seconds or threw an uncatchable System.OutOfMemoryException instead of
one of Jint's catchable constraint exceptions.

This continues the fix class of sebastienros#2349 and sebastienros#2465 across the remaining sites
(the reporter and maintainer asked to also cover String.raw,
Array.prototype.join/sort and any other affected methods):

- String.prototype.padStart/padEnd: use ToLength instead of ToInt32 so a
  large length can't wrap past the size cap; resolve the fill string only
  after the length check (spec-correct side-effect ordering); throw a
  catchable RangeError when the result exceeds ClrLimits.MaxArrayLength;
  build incrementally with periodic checks instead of allocating ~3x the
  result up front. Net effect: the common small-pad case allocates ~60%
  less and is slightly faster.
- Array.from, String.raw / fromCharCode / fromCodePoint: periodic checks in
  the element/char loops; size cap added to fromCharCode.
- Array.prototype and %TypedArray%.prototype bulk loops (join, sort, map,
  filter, forEach, reduce/reduceRight, find*, slice, splice, unshift, shift,
  concat, toSpliced, toLocaleString, set, and the slow indexOf/lastIndexOf/
  includes scans): periodic checks. Dense primitive fast-path scans are left
  unguarded.
- RegExp.prototype @@replace / @@split / @@match outer match-collection loops.
- JSON.stringify breadth (array/object/replacer) and JSON.parse array/object/
  string-literal loops.

Reuses the existing `% ConstraintCheckInterval (10_000)` idiom; the predicate
is only added to loops whose bodies are already heavy (a JS call, property
Get/Set, ToString, regex NextMatch), so the cost is within measurement noise
and allocation counts are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… gaps

Follow-up to the execution-constraints work, addressing code-review findings.

Correctness:
- Array.prototype.join/toLocaleString: wrap the element loop in try/finally so the
  entry is always popped from the long-lived join stack even when a periodic
  Constraints.Check() throws. Previously an interrupted join could leave the array
  on the stack, making a later join of the same array silently return "".
- Return pooled resources on the Check()-throw path: 'using' the ValueStringBuilder
  in padStart/padEnd, repeat and toWellFormed; try/finally around the rented args
  array in ArrayInstance/ObjectInstance FindWithCallback (this also fixes a
  pre-existing leak on the return-on-match path) and in ConstructArrayFromIEnumerable.

Close remaining gaps of the same class (bulk loops over JS-controlled sizes that
the first pass missed) — these matter for native/host iterators and native
callbacks that run no per-element JS statements and so cannot self-throttle:
- IteratorProtocol.Execute + AddEntriesFromIterable, and the Set/WeakSet constructor
  loops: covers Array.from(iterable), [...iterable], new Map/Set/WeakMap/WeakSet and
  Object.fromEntries.
- ObjectInstance.EnumerableOwnProperties (Object.keys/values/entries) and Object.assign.
- ArrayOperations.GetAll: the array-like expansion behind Function.prototype.apply and
  Reflect.apply/construct (the existing size cap is kept; this adds interruptibility).
- Map.prototype.forEach / Set.prototype.forEach.

Cleanup:
- Single-source the interval as Engine.ConstraintCheckInterval and alias the per-class
  consts to it, so the value is defined once.
- Remove the unreachable size cap in String.fromCharCode (length is bounded by the
  already-materialized arguments array, unlike padStart/repeat).

Tests: fixed ShouldLimitArraySizeForForEach (the previous version tripped fill()
before forEach ever ran, so it gave no coverage of the forEach guard) and added
coverage for join-stack recovery after interruption, the native-iterator Array.from
path, Object.keys, and Map.forEach with a native callback.

All green: Jint.Tests 3028 passed, Test262 99,015 passed, 0 regressions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lahma
lahma merged commit aa715b2 into sebastienros:main May 31, 2026
4 checks passed
@lahma
lahma deleted the execution-constraints-fixes branch May 31, 2026 12:30
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.

A few more built-ins bypass execution constraints

1 participant