Repository navigation
Throw a ShadowRealm cross-realm TypeError after its catch block, not inside it - #4176
Merged
Merged
Conversation
…inside it WrappedFunction's [[Call]] and WrappedFunctionCreate copied a failure into a TypeError by throwing from inside the catch handling it. A throw from a handler is dispatched on top of every frame down to the throw it handles, so a failure crossing a chain of wrapped functions nested one exception dispatch per hop, which no native stack probe sees: the StackOverflowGuard's catchable RangeError at the bottom of the chain became a process-ending stack overflow on the way back up. Five hundred round trips of a function across the boundary were enough on a 1 MB thread; a name getter that wraps its own receiver needed no depth at all. Both sites now record the failure and throw once the handler has returned. The copy's "Cross-Realm Error: " mark is also added once instead of once per hop, which kept the message, and the allocation of every copy on the way up, growing with the depth of the chain. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
lahma
force-pushed
the
fix/shadowrealm-wrapped-chain
branch
from
September 25, 2026 08:39
b9dcf58 to
e0e63a7
Compare
lahma
added a commit
to lahma/jint
that referenced
this pull request
Sep 25, 2026
…hmark/AGENTS.md sebastienros#4176 and this branch each added a gotcha to Jint/Constraints/AGENTS.md, and together they put the file over the 32 KiB budget AgentInstructionFileTests enforces. The bullet recording the measured NO-GO on the double native-stack probe repeated the mechanism and the verdict that Jint.Benchmark/AGENTS.md already holds with its table, so it keeps the trap (the route, the verdict, the issue) and points there. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
lahma
added a commit
that referenced
this pull request
Sep 25, 2026
#4173) * Pin that a deferred concatenation escapes LimitMemory (repro for #4162) Seven of the eight tests fail on main: a long `a + b` builds a RopeString whose characters are allocated only when something flattens it, so under LimitMemory(4 MB) a 16 MB doubling, a shared-operand accumulator, a value grown across entries and a host string with one character appended all leave the engine uncharged. The eighth is the guard on the other side: a fix must not charge an accumulator as if every intermediate were copied. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Pin the aggregate and deferral halves of the #4162 repro IndependentConcatenationsShareOneBudget fails on main and on a fix that only refuses a single value larger than the budget; the leftover-size assertions now demand that a failed run leaves nothing larger than its budget behind. AccumulatingWithPlusStaysDeferredUnderAMemoryLimit passes on main and guards against a fix that copies eagerly whenever a limit is configured. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Say what the #4162 repro tests do allocate Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Charge a deferred concatenation to LimitMemory when it is built A long `a + b` returns a RopeString, which is 56 bytes whatever length it stands for; its characters are allocated by whoever flattens it, and that can be a host's ToString() after every constraint is disarmed. JsString.Concat now charges the active operation on its deferring branch: the shorter operand, which keeps `s = s + x` linear, or the whole length, refused before the node exists, when the flat form alone exceeds the budget. Concat takes the EvaluationContext the interpreter already holds and reads its engine only on the deferring branch, so the short `+` path loads nothing new. A constant fold evaluates with an engine-less context, and the charge skips it: the node is then made from source literals. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Document that a deferred string is charged when it is built Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Cut the double-probe bullet to its trap; the table lives in Jint.Benchmark/AGENTS.md #4176 and this branch each added a gotcha to Jint/Constraints/AGENTS.md, and together they put the file over the 32 KiB budget AgentInstructionFileTests enforces. The bullet recording the measured NO-GO on the double native-stack probe repeated the mechanism and the verdict that Jint.Benchmark/AGENTS.md already holds with its table, so it keeps the trap (the route, the verdict, the issue) and points there. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
lahma
added a commit
that referenced
this pull request
Sep 26, 2026
…stack overflow or an endless thread hop (#4187) Evaluating eval's source re-enters the interpreter without entering a function, so no function-entry probe saw it: `var s = 'eval(s)'; eval(s)`, `(0, eval)(s)`, `eval?.(s)`, `[s].forEach(eval)` and eval installed as @@hasInstance all ended the host with a native stack overflow on net10.0, net8.0 and net472 under the default StackOverflowGuard. PerformEval now probes before it parses, gated like the ScriptFunction entries (off on the MaxExecutionStackCount lane). On that lane a direct eval and eval?.() were the one call a call expression dispatches without a call-stack frame, so a recursion of them never grew the count and hopped to a fresh thread at every exhaustion without ever throwing; they now count as the call they are, through a counter rather than a frame because a frame is observable in error.stack, the debugger and MaxRecursionDepth. Follow-up to #4076 / #4078 / #4165 / #4176. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
lahma
added a commit
that referenced
this pull request
Oct 5, 2026
…lling the host or running script (#4220) * Backport #4176 to 4.x: Throw a ShadowRealm cross-realm TypeError after its catch block, not inside it WrappedFunction's [[Call]] and WrappedFunctionCreate copied a failure into a TypeError by throwing from inside the catch handling it. A throw from a handler is dispatched on top of every frame down to the throw it handles, so a failure crossing a chain of wrapped functions nested one exception dispatch per hop, which no native stack probe sees: the StackOverflowGuard's catchable RangeError at the bottom of the chain became a process-ending stack overflow on the way back up. Both sites now record the failure and throw once the handler has returned, and the "Cross-Realm Error: " mark is added once instead of once per hop. Adapted for 4.x: StackOverflowGuard is opt-in on this branch, so the new test rows run on the file's Guarded() engine; xUnit TheoryData instead of main's TestCases; the gotcha goes in the root AGENTS.md (4.x has no Jint/Constraints/AGENTS.md) and there is no SpecAnchors.txt to register. Adapted from c1c180b Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backport #4183 to 4.x: Describe a value thrown out of a ShadowRealm without running any of its code ShadowRealm.prototype.evaluate copied a failure into the caller's TypeError by formatting the thrown value with ToString, which calls toString, @@toPrimitive, a name or message getter and every proxy trap on the way. CreateTypeErrorCopy must not cause any ECMAScript code execution, and when one of those calls threw, its exception - an object of the shadow realm - reached the caller instead of the TypeError. The copy's message is now read without calling anything: a primitive's own string form, and for an error its name and message where each is a data property holding a string. Any other object, a proxy included, is described by kind alone. An error that is itself a copy keeps its message, so a failure leaving nested shadow realms is marked "Cross-Realm Error: " once. importValue's rejection handler was %ThrowTypeError%, which reported a failed import as a 'caller'/'callee'/'arguments' access. It is now the proposal's ImportValueError function, performing the same copy. Adapted for 4.x: main reads the error through Jint.Diagnostics.ValueSlotReader (ErrorText/ErrorLine), which this branch does not have; the same call-free descriptor walk is written as private helpers in ShadowRealm.cs, without the IErrorTextSlots branch (DOMException lives in Jint/WebApi, absent here). The [[ErrorData]] test is `is JsError`, as Error.isError's is on this branch. Tests use xUnit InlineData; there is no SpecAnchors.txt to register. Adapted from 8557142 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
A function passed back and forth across a
ShadowRealmboundary is wrapped once per crossing, so script builds achain of wrapped functions as deep as it likes:
Calling it descends a few native frames per level, and with the default options (
StackOverflowGuardon) thenative stack probe raises its catchable
RangeErrorat the bottom. It never reached the script. Onthe way back up, every level's
WrappedFunction.[[Call]]catches the failure and copies it into aTypeErrorcreated in the caller's realm, as the proposal requires — and it threw that copy from inside its
catch (JavaScriptException)block. A throw from a handler is dispatched on top of the exception being handled,whose frames (everything from the handler down to where it was raised) stay on the native stack until the handler
returns. So each level nested one more exception dispatch on top of the whole chain, nothing probes an exception
dispatch, and the host process died:
Measured on a 1 MB thread, guard on, x64: 250 round trips (500 levels) fit and answer
1; 500 round trips(1 000 levels) end the process on both net10.0 (
Stack overflow., exit 127) and net472 (Process is terminated due to StackOverflowException.), and so does every depth tried above that (1 000, 2 000, 5 000).WrappedFunctionCreatehad the same shape aroundCopyNameAndLength, and needs no depth input at all. Wrappinga function whose
namegetter is itself a wrapped function wraps the getter's receiver again, which readsnameagain — a native-only recursion of three lines of script:
The probe fires, and every level's
catcharoundCopyNameAndLengththrew its copy from inside the handler: thesame
Stack overflow.inEH.DispatchExunderShadowRealm.WrappedFunctionCreate, on net10.0, net8.0 andnet472.
The fix
Both sites record the failure's message in the
catchand throw the copy once the handler has returned, soby the time a level throws, the frames it handled are gone. The cross-realm failure now unwinds one level at a
time, as any other exception does. With the fix, both scripts above raise a catchable
TypeError: Cross-Realm Error: Maximum call stack size exceededon net10.0 and net472 at every depth tried —500, 1 000, 5 000 and 20 000 round trips on a 1 MB thread, and 20 000 on a 16 MB thread.
PerformShadowRealmEvalInternal's throw sits in atry/finally, not acatch, and nests nothing, so it isuntouched.
The message: embed once, never compound
CreateTypeErrorCopy(§2.2) only requires anew
TypeErrorcreated in the caller's realm; its note says "Host environments may provide implementation-specificmessage value" and that the operation "must not cause any ECMAScript code execution". test262 asserts no message
anywhere under
built-ins/ShadowRealm(checked at the pinned7ab7fafa), so the message is Jint's to choose.ThrowCrossRealmErrorprefixedCross-Realm Error:to the inner message at every hop, so a failure crossingnlevels builtnever-longer strings: quadratic in a depth script chooses. Measured with only thethrow-after-catch half applied, on net10.0: the message came back 14 244 characters long on a 1 MB thread
(~750 levels) and 260 009 characters on a 16 MB thread (~13 700 levels), roughly 3.5 GB of string allocation
summed over the copies on the way up, to say
Cross-Realm Error:thirteen thousand times.I kept the inner message rather than dropping it: it is the only diagnostic an embedder gets for a failure inside
a realm (
Cross-Realm Error: boomagainst a bareTypeError), and the proposal explicitly leaves room for it.What changed is that a message which already carries the mark is reused as it is, so the copy's message is the
innermost failure marked once, whatever the depth, and no hop after the first builds a message string. The
check is an ordinal prefix test. A script error whose own message happens to begin with
Cross-Realm Error:nowcrosses without a second mark, which still says what happened. The message still comes from
ex.Message, which theJavaScriptExceptioncomputed when it was raised, so the copy runs no script, as the note requires.The one observable change:
BoundFunctionChainWalkTests' ShadowRealm route (a call that crosses the boundarytwice) now reads
TypeError:Cross-Realm Error: Maximum call stack size exceededinstead ofTypeError:Cross-Realm Error: Cross-Realm Error: Maximum call stack size exceeded; its expectation is updated.Hot paths
Neither site is a hot path, and the success path of both is unchanged apart from one
nulllocal; the extrabranch runs only after a caught exception. No benchmark was run; measurement belongs to the lead.
Instructions
Jint/Constraints/AGENTS.mdgains one bullet next to the probe placement rules: nothing probes an exceptiondispatch, so a recursive native path records a failure and throws after its
catch, never inside it. The filestays under its 32 KiB budget (32 514 bytes on disk with CRLF).
Jint.Tests/SpecAnchors.txtregistersproposal-shadowrealm sec-create-type-error-copy, regenerated withJINT_SPEC_ANCHORS=update(one anchor added,nothing else changed apart from the date and count).
Tests
Jint.Tests.PublicInterface/HostNativeRecursionGuardTests.AFailureCopiedAcrossAShadowRealmBoundaryAtEveryLevelIsCatchable,two rows on a 1 MB
DedicatedThread. Each asserts the host catches aJavaScriptExceptionwhose message isexactly
Cross-Realm Error: Maximum call stack size exceededand whose error is aTypeErrorof the principalrealm, then that the engine still evaluates
6 * 7:unfixed engine dies on every framework;
namegetter recursion above, which has no depth to choose.Fail before, pass after
Unfixed
ShadowRealm.cs(basemain), each row run on its own:Stack overflow.inWrappedFunction.Call/ThrowCrossRealmErrorStack overflow.StackOverflowExceptionStack overflow.inWrappedFunctionCreate/ThrowCrossRealmErrorStack overflow.StackOverflowExceptionTest run summary: Zero tests ran/total: 0with the overflow in the error output is the confirmation: anunguarded case cannot fail, it can only take the runner with it. Each row reaches only one of the two sites. The
call chain passes no function-valued argument,
thisor result, so it never reachesWrappedFunctionCreate, andthe getter recursion throws from the
thiswrapping, which sits beforeWrappedFunction.Call'stry. With thefix, both rows pass on all three frameworks.
What was run
On
b9dcf5888, rebased ontomainatb630bf424, each project per framework, whole project, Release, no--timeout:0 failures anywhere. test262: 102 740, 102 637 passed, 0 failed, 103 skipped. The
JINT_HOST_CONTRACT_VERIFICATION=1leg was not run locally; nothing here touches a host-facing type.Not in this PR, stated rather than left to be found
ShadowRealm.prototype.evaluate's copy runs script.PerformShadowRealmEvalbuilds its message fromresult.Value.ToString(), which for an object thrown by the evaluated code isTypeConverter.ToStringorError.prototype.toString. That contradictsCreateTypeErrorCopy's "must not cause any ECMAScript codeexecution", and a throwing
toStringreplaces the copy altogether. Measured on this branch:sr.evaluate("throw { toString() { ran++; return 'custom'; } }")runs the method (ranis 1 afterwards), andsr.evaluate("throw { toString() { throw new RangeError('from toString'); } }")surfacesRangeError: from toStringinstead of aTypeError. That is a conformance change with its own test262 run, soit is left for a separate PR. The message would have to come from something that runs no script, such as an
ErrorInstance's own data-propertymessage.Follow-up to #4076 / #4078 / #4165.
🤖 Generated with Claude Code