-
Notifications
You must be signed in to change notification settings - Fork 67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Inspecting errors thrown from ShadowRealm #353
Comments
I would assume the thrown exception can't contain any object references from inside the shadow realm, which means it either can't have a getter, or, the object would be cloned such that the getter was wrapped. If the getter was wrapped, then it would set the shadow realm's |
It can access the getter once and copy the name & message (but should we?). I don't think we should wrap the getter. |
it would make sense to me that all getters would be reified as own properties prior to crossing the realm boundary. Since objects aren't permitted to cross the boundary, i don't think it's a reasonable expectation that obscure things about a thrown object will survive to the outer realm. |
It sounds good to me to reify const fn = new ShadowRealm().evaluate(`
function createErrorObject() {
return {
get message() {
throw createErrorObject();
}
}
}
function foo() {
throw createErrorObject();
}
foo;
`);
try {
fn();
} catch {} |
This is interesting. where (in specs) do we connect the getter of I'm not sure if I want to have a wrapped functions for it, but if triggering a |
What happens if the error object is a proxy that claims it has an own |
I think there are 3 options:
|
why |
To build the data property, you have to eagerly read the value on the original error (option 1), unless you make this new error object exotic. |
ahhh right, then yes that's what i'd suggest. |
To clarify, what do you suggest: eager read or using an exotic error object ? |
Eager read - like getters and setters, exotics should be eliminated as much as is possible :-D |
So I suppose the question is, what should the behavior be if accessing |
i'd assume whatever it throws would then be cloned in the same way as the original error was attempted to be, and that is what makes it across. Certainly if the original thing doesn't have an [[ErrorData]] slot i wouldn't expect |
I think if reading the message throws, or the result is not a string, we ignore it. |
We discussed this in the SES meeting today, and given that ShadowRealm is an advanced API, I don't think it makes sense to try and provide some improvements to the handling of Error instances thrown across the callable boundary. Aka, if a wrapped function throws an error, the caller should not get any more information than a generic TypeError, with no name or stack information about where the original error was thrown. However it was raised that we may want to allow values that would otherwise be allowed through the callable boundary to be thrown, aka pass thrown primitives as-is, and wrap thrown functions. This may simplify the handling of objects, including error instances, for membranes working across the callable boundary. |
@mhofman the problem about leaving the wrapping to userland is that values thrown from the shadow realm are not always from user codes. Like, |
Afaik, both |
Aka either the error is thrown by the ShadowRealm API itself, in which case it can include details about the error since the error is created in the caller realm, or the wrapped function is invoked, and the target code can make sure any error thrown during its execution is handled appropriately. |
Specifically, I mean the HostImportModuleDynamically can reject the promise with host defined errors, and step 13 of https://tc39.es/proposal-shadowrealm/#sec-shadowrealmimportvalue replaces the rejection with TypeError and discard the host defined errors. In HostImportModuleDynamically, user code may not get a chance to be evaluated. |
Ok so that case doesn't deal with the callable boundary, right? I'm not sure I grok the different places that can throw an error in this case, but maybe there is a way to let errors resulting from the parsing and loading of the module through without neutering them, while making sure runtime errors while evaluating the module end up as a caller realm |
I'm not sure I get your point clearly. The host errors are created in the shadow realm (the executing context on HostImportModuleDynamically is the one shadow realm created), so the error values still need to go through the boundary.
Right. However, if I recall correctly, Hosts like Node.js can expose module loader hooks to userland so user code can still get a chance to evaluate to "load the module". In this case, the problem can still arise that inspecting the error may cause side effects. |
By callable boundary in this case I meant the wrapping that is done by the wrapped function exotics. Here it seems the issues you raise stem from the "entrypoints" into the ShadowRealm, aka As I said, I don't fully grasp the layering for these operations, but as long as the following invariants are preserved, I think it's acceptable for errors to contain useful information:
(The above only applies where at least one ShadowRealm is involved, of course there is nothing preventing 2 legacy realms from leaking to each other) |
@mhofman I don't understand SES's recommendation here to not do anything about the errors. If At the very least I'd expect reading the Edit: This opaque errors thing has bitten me already! In @legendecas's implementation in V8, we forgot to upload the imported module files in some |
Ok I probably didn't express myself correctly. Conceptually I have no problems with errors thrown in the ShadowRealm context with no user code on the stack to contain debugging information visible from the caller realm. That includes the host's module resolution mechanism, parsing or any early syntax errors. My concern is errors thrown when user code running in the context of the ShadowRealm is on the stack. That includes errors thrown while executing a module graph's top level code, including top level await. The first can only ever happen during the |
Hm, I don't have time to dig in right now on how to give an allowance in spec text for actionable error messages if they're not user-originated, but that's a pretty important thing to allow. I don't think there is a way to distinguish errors that are user-originated vs not. Perhaps we'd need to manually examine throw completions from |
I've put this issue on the agenda for the June meeting to get clarity on what should or should not be exposed on errors across ShadowRealm boundaries. It's uncontroversial that errors should not pierce the callable boundary via direct object references, but not at all clear to me:
|
Thanks @syg. We plan to also discuss this in next week's SES meeting, if you're able to join. |
So HTML does define serialization and deserialization of objects that have [[ErrorData]]. This should not end up contradicting that, right? How will that be ensured? (See also #336 which dealt with what seems like a very similar issue. My concerns are similar to those raised there, but perhaps I'm missing something.) |
@annevk I'm failing to understand what you mean. Objects with an In what case does HTML serialization come into play? FYI, I am looking at an explicit structured cloning like API as a followup proposal to handle sharing objects through the callable boundary, but that's out of scope for the current proposal. |
imo it should definitely do a brand check, and it sounds fine to only do it for data properties. |
I was proposing that there be no brand check, and that it would invoke a getter if present. The algorithm would just be a normal Get. What is the motivation for doing otherwise, or trying to limit observability? |
I was just asking for a clarification, I do not currently hold a strong opinion one way or another (well I'd probably be against an observable Get on the original error deferred to the time of Get on the new error). |
Keep it mind something that was discussed in plenary, which is that there is no evidence, or strong desire to hide error information from the incubator realm, in fact it is desirable that the incubator can get a lot more info when possible. If course, this is a two ways street, and sometimes it is the shadow realm the one calling into the incubator realm, in which case the error should be completely hidden. |
I'd be happy to see a uniform approach to transferring exceptions across the boundary. But I'm curious about how to handle the possible failure on the normal Get -- like in the example raised previously #353 (comment). |
|
Leaving it undefined would conflate it with throwing an error with an actual undefined message property, no? |
With debugging being a core motivator, would one option be to keep the originally thrown error referenced in a hostInternalSlot of the new TypeError. Which hosts are permitted to read in a non-observable (to the application) way. e.g if the error was passed to the platforms built-in I can imagine a similar thing happening for errors thrown in their own task. e.g a |
During the SES call yesterday I suggested constructing a new shadow realm error that captures the message and name of the original error, and is used as a |
Maybe the normative spec specifies what, if any, data can observably be cloned to the new error, and an editorial note suggesting additional, non-observable from the application, developer-experience improvements? |
What engines have asked us for is guidance on what they should actually do, rather than latitude to do a wide range of things as such a note would leave them. (Note that a "MAY" is normative, not a note/editorial, and that might be what we want to do here.) Let's see if we can answer the implementers' question concretely. |
We discussed this issue at length in the SES call this week. After some thought, I believe we reached the shared conclusion that it should be OK to copy a string We did not settle on whether to do an arbitrary Get of the |
I believe we also agreed during the call that the type of error is not relevant or important, and that we will continue throwing the TypeError with the proper data attached to it. |
I just discussed this issue at length again with @erights, and we arrived at the following observations:
|
Why should it not? A stack string can be freely passed across explicitly, and unlike type classes like "objects" etc, there's no way to prevent that. Developer tooling includes "sending data to remote loggers with javascript", so this isn't something that can be relegated only to the executing environment's privileged tools. It seems perfectly fine to me to require that a membrane library do the censoring. Why is that not workable? |
Passing a stack string explicitly through the callable boundary is different than an uncensored stack string being available by the mere act of constructing an error object (possibly implicitly). It sounds perfectly fine to me to require debug tooling that runs with the same privilege of the code to be limited, the same way they already are regarding async stack traces. These libraries can wrap |
Btw, I do not expect logging libraries to ever need to actually do that. ShadowRealm is not intended to be an API most developer will use directly, and I expect libraries that use ShadowRealm to handle errors in accordance with their purpose. For these libraries, recomposing stack traces in user code when an error crosses the boundary is a lot easier than taming stack traces on all created errors. |
I'm having a lot of trouble understanding @mhofman 's proposal. Wouldn't GetWrappedValue on a normal error object fail, because it's a non-callable object? |
Exactly. What I'm suggesting is that we provide guidelines that in that |
E.g. an implementation would be allowed to do: const thrower = (new ShadowRealm()).evaluate(`() => {
throw new RangeError('0 is too small');
}`);
try {
thrower();
} catch (err) {
console.log(err); // TypeError: RangeError thrown across callable boundary: 0 is too small
} |
This behavior seems reasonable, but I'd prefer that we specify exactly what the message is, rather than asking implementations to come up with their own algorithm. I was proposing that we just include |
This would be the only place in the spec where we force a specific message on the implementation. Anywhere else the implementation is free to define its own message. Regarding the unobservable access, the implementation is free to check whether the object is a proxy or has accessors. Or maybe we could make this stricter: if the object has an |
Agreed with @ljharb on #353 (comment). I see no reason to censor stacks. Ignoring the procedural question of what normative thing 262 can even say, ShadowRealms have use beyond SES's membrane model, so I'm also inclined to to think that if the SES security model requires it, then it's those libraries' job to ensure that property holds. |
Thanks @syg! It's common understanding that ShadowRealms is to be considered low-level while membrane systems are supposed to maintain any ShadowRealm output and not just use it as-is. |
Solved by #382 More details on the errors section in the explainer. |
The errors thrown from ShadowRealm, with
ShadowRealm.prototype.evaluate
,WrappedFunction.[[Call]]
, andShadowRealm.prototype.importValue
, are all wrapped with TypeError created in the caller realm. Exposing inspection of these errors to the executing realm can be very helpful in the real world. However, the spec didn't have a word if the creation of the TypeError can cause side effects in the executing realm.Like say:
In this case, can the message getter be called? should the return value of the getter be wrapped? Or the implementation should only provide a non-observable inspection -- only exposing the inner
name
andmessage
when thename
andmessage
of the error objects are data properties of primitive values?Edit: although the title says "the error thrown from ShadowRealm", this problem also applies to the
WrappedFunction
s created for passing arguments into the ShadowRealm.The text was updated successfully, but these errors were encountered: