Repository navigation
Describe a value thrown out of a ShadowRealm without running any of its code - #4183
Merged
lahma merged 1 commit intoSep 25, 2026
Merged
Conversation
…ts 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 object with [[ErrorData]] its name and message where each is a data property holding a string (ValueSlotReader.ErrorText, the console's call-free reader). 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, as a chain of wrapped functions already is. importValue's rejection handler was %ThrowTypeError%, which ran nothing but reported a failed import as a 'caller'/'callee'/'arguments' access. It is now the proposal's ImportValueError function, performing the same copy. 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.
Follow-up to #4176.
The defect
ShadowRealm.prototype.evaluatecopies a failure into aTypeErrorof the calling realm, and it built that copy's message withToStringof the thrown value (JsValue.ToString()→TypeConverter.ToString/Error.prototype.toString). That callstoString,@@toPrimitive, anameormessagegetter, and every proxy trap along the way. CreateTypeErrorCopy says of itself that it "must not cause any ECMAScript code execution", and the host-provided message is covered by that too.These were measured on
mainat7d9cdcc8f:throw { toString() { ran++; return 'custom'; } }TypeError: Cross-Realm Error: customthrow { toString() { ran++; throw new RangeError('from toString'); } }RangeError, not aTypeErrorErrorwith amessagegetterTypeError: Cross-Realm Error: Error: gnew Proxy(new Error('pe'), { get … })TypeError: Cross-Realm Error: Error: penew ShadowRealm().evaluate("throw new Error('boom')")in a shadow realmCross-Realm Error: TypeError: Cross-Realm Error: Error: boomThe second row also broke isolation: an object from the shadow realm crossed the callable boundary. The last row is the mark #4176 stopped repeating for
WrappedFunction. It was still added once per level here, because this path formatted the error value (TypeError: Cross-Realm Error: …) rather than reusing a message.test262 at the pinned SHA (
7ab7fafa) doesn't cover any of this.built-ins/ShadowRealm/prototype/evaluate/errors-from-the-other-realm-is-wrapped-into-a-typeerror.jsand its siblings only assert the constructor of what is thrown, and none of them throws a value with an observabletoString, getter or trap.The fix
CopiedErrorMessagebuilds the message without calling anything:ToStringrefuses symbols.[[ErrorData]](JsError or IErrorData, the same test asError.isError) is rendered as the lineError.prototype.toStringwould produce.nameandmessagecome fromValueSlotReader.ErrorText, the console's call-free reader. It uses a value only when it is a data property holding a string, and it stops at an accessor or a proxy anywhere on the prototype walk.an object that is not an Error was thrown.ThrowCrossRealmErrormarks a failure leaving nested realms once.For an ordinary error or a primitive the message is the same as before (
Cross-Realm Error: ReferenceError: aaa,Cross-Realm Error: 42), and tests pin that.importValue: its rejection handler was%ThrowTypeError%. That runs nothing, but it reported a module that failed to evaluate with the strict-mode'caller', 'callee', and 'arguments' properties may not be accessed…message. It is now the proposal's ImportValueError function (length 1, name"", in the caller's realm), which performs the same copy. The new anchor is registered inSpecAnchors.txt.Tests
Jint.Tests/Runtime/ShadowRealmErrorCopyTests.cs, 26 cases:toStringis not called, and a throwingtoStringstill yields the caller'sTypeError@@toPrimitiveandvalueOfmessageandnamegetters, including a throwing getter and a getter on a subclass prototypemessage, and a proxy on the error's prototype chainimportValueof a module that throws an error, and of one that throws an object with atoStringFails before: on
7d9cdcc8fwithout the fix, 18 of the 26 fail. The 8 that pass are the rows that pin the unchanged message. For example:a throwing toStringgotnot a TypeError of this realm|from toString|ran=1, andtwo realms deepgot…Error: TypeError: Cross-Realm Error: Error: boom.Passes after:
main: 102,651 / 0 / 89)Base:
7d9cdcc8fa1179b8ea92db17f617f6154dc56b7e(upstreammain). Immediately before opening this, upstream was fetched again and the branch rebased onto it. The rebase was a no-op, so the full runs above are on the pushed commit. The ShadowRealm, spec-citation and error-message fixtures were run once more after the rebase, on both frameworks, and all passed.Not changed here
WrappedFunction's[[Call]]andWrappedFunctionCreatestill copyJavaScriptException.Message. That message is built when the throw leaves a function body, through[[Get]]ofmessage. So the getter has already run by the time the copy is made, and it runs outside ShadowRealm too. Measured onmainwith plain script:This runs the getter twice (and
throw new Proxy({}, { get })fires the get trap twice). That's an engine-wide defect with its own fix. Once it's fixed, the two wrapped-function sites can derive their message fromex.ErrorthroughCopiedErrorMessage, and then the copy will read the same whichever path it crossed.🤖 Generated with Claude Code