Skip to content
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

Incumbent seems broken for pending promises #5213

Closed
domenic opened this issue Jan 15, 2020 · 19 comments
Closed

Incumbent seems broken for pending promises #5213

domenic opened this issue Jan 15, 2020 · 19 comments

Comments

@domenic
Copy link
Member

domenic commented Jan 15, 2020

I was working on fixing entry (#1426) but @syg pointed out that incumbent is not quite right either. @bzbarsky and I spent a lot of time on this in #1189 so it's possible it works in some non-obvious way, but as of right now it seems broken to me.

The specific problem is that EnqueueJob's choice of incumbent settings is wrong when job is a PromiseReactionJob that is being queued as a result of TriggerPromiseReactions (i.e., because someone did a resolve() on a pending promise). PromiseResolveThenableJobs work fine, as do PromiseReaction jobs triggered by PerformPromiseThen immediately enqueuing jobs (because someone did a .then() on a settled promise.)

In particular, to match Web IDL semantics (and the general expectations around how incumbent behaves) we want to capture the incumbent realm at the time promise.then(handler) is called, and then, when we run the PromiseReactionJob, prepare to call a callback using that settings object. But right now we just grab the incumbent at job-enqueuing time.

Fixing this seems like it needs hooks deeper into the JS spec. (Even more deep than the arguments-introspection of #5212.) In particular we need a chance to store the incumbent in the [[Handler]] data structure in PerformPromiseThen.

@bzbarsky, can you check my analysis? Does Firefox implement this? Maybe we decided that it's OK for incumbent to be broken in these cases?

/cc @littledan as he may be able to help us with this cross-spec integration work.

@bzbarsky
Copy link
Contributor

I will not be able to get to this until Monday next week, unfortunately (have some pre-existing commitments). If you haven't heard from me by end of (US East Coast) day then, please ping me!

@domenic
Copy link
Member Author

domenic commented Jan 15, 2020

For what it's worth I wrote tests in web-platform-tests/wpt#21206, assuming Web IDL-like semantics, which seem to pass in both Chrome and Firefox.

In Chrome's case, this is surprising; based on @syg's code inspection we seem to be doing something rather different. Maybe if I just introduce even more realms (e.g. promise constructor realm is currently the same as entry realm) we'll find issues...

Edit: we found that cases depending on the backup incumbent settings object stack fail in Chrome.

@syg
Copy link
Collaborator

syg commented Jan 15, 2020

(As an aside, is realm 1:1 with "settings object"? Saying "settings object" is rather verbose.)

@domenic
Copy link
Member Author

domenic commented Jan 15, 2020

@syg
Copy link
Collaborator

syg commented Jan 16, 2020

In Chrome's case, this is surprising; based on @syg's code inspection we seem to be doing something rather different.

For Chrome, what I think is going on is that enqueuing a Promise microtask always saves the isolate's current realm. For pending Promises, the current realm at the time of enqueuing the task is the resolve function's realm, since the resolve function is at the top of the stack. In other words, it'll always be the Promise's creation realm. I believe that explains the Chrome failure in web-platform-tests/wpt#21206: for pending promises, the incorrectly saved incumbent realm is the fulfilled Promises's creation realm.

What @domenic has pointed out, and what I agree with, is that this is super edge casey. A difference is observed only when there is no user code on the JS stack, like when builtins are bound with .bind and directly passed to then. Otherwise, the incumbent will end up being the realm of the JS function at the top of the stack (barring yet another edge case of triggering callbacks directly from script...)

This also raises the point that since Chrome/V8 is buggy here and that is unlikely that the incumbent being used in this way in Promise callbacks is depended upon in the web, and that we still have existing sentiment to simplify the incumbent concept, is there possibility of change here? For instance, just saving the callback's creation realm instead of the incumbent.

@domenic
Copy link
Member Author

domenic commented Jan 16, 2020

that we still have existing sentiment to simplify the incumbent concept

Well, we'd like to remove uses of it. I don't think introducing two slightly-different incumbent concepts, one used by promises and another by all other callbacks on the platform, would be much of a simplification.

@littledan
Copy link
Contributor

My thoughts are tending in the direction of @syg's comment. Applying HTML's concept of "incumbent" to JS here would complicate the specification and implementation in certain ways that I don't understand the motivation for (even if some implementations already do it). How would a web developer run into this sort of issue?

A bit of a tangent, but in addition to Promises and WeakRefs, would the idea of threading incumbent realm through be relevant for iterator helpers (on async iterables) and Observables?

@domenic
Copy link
Member Author

domenic commented Jan 16, 2020

I don't understand the motivation for (even if some implementations already do it). How would a web developer run into this sort of issue?

Web developers run into this issue whenever they use incumbent-dependent APIs. See #1430 for a relatively-complete list.

A lot of the uses there can be considered "legacy", i.e. we don't like that they're using incumbent, and would like to switch to relevant or current if web compatible. However, there are some notable ones where incumbent makes sense, notably the security checks involved in navigating via the location object, and figuring out what the "source" is for cross-document messaging APIs like postMessage(), MessageChannel, or BroadcastChannel.

In particular, consider a case like

someOtherWindow.postMessage.call(someThirdWindow, ...args);

This will eventually fire a MessageEvent inside someThirdWindow, with a source property that indicates where the message comes from. This is used for e.g. communicating back to the sender. This source property is derived from the incumbent. And doing so makes more sense than using someOtherWindow or someThirdWindow; although those mechanisms were used to send the message, it's really the code in the incumbent window that is sending the message.

Currently, if you send postMessage()s from inside any deferred callback, the incumbent (and thus the source) will be correctly determined in Firefox, and in the specs modulo this bug, and in Safari and Chrome modulo certain edge cases involving combinations of bind() and promises. I argue we should move toward the Firefox behavior, because I don't think we should make the source of postMessages unreliable when you use promises to defer your callback, if it is reliable in all other cases.

So to be concrete about the web developer impact:

  • If we stopped tracking incumbent for promises entirely, then code like Promise.resolve().then(() => someOtherWindow.postMessage(...args)) would give a different resulting source property than someOtherWindow.postMessage(...args).
  • If we changed the definition of incumbent in a way that is simpler to implement (I think this is what @syg is suggesting), then code like Promise.resolve().then(() => someOtherWindow.postMessage(...args)) would give a different resulting source property than Promise.resolve.then(someOtherWindow.postMessage.bind(someOtherWindow, ...args)).

A bit of a tangent, but in addition to Promises and WeakRefs, would the idea of threading incumbent realm through be relevant for iterator helpers (on async iterables) and Observables?

Great find. I think it would. This argues that we either need general host hooks for all callback storage and use (I think it only matters for potentially-async callbacks?), or we should try to move the entry and incumbent concepts down into the ES layer, as something that both the ES spec and hosts maintain, but hosts take advantage of.

@bzbarsky
Copy link
Contributor

To the extent that anyone ever examines an incumbent realm, the idea is that any time a function that examines it is captured in some way the incumbent should be captured as well.

like when builtins are bound with .bind and directly passed to then

Indeed. The idea is that p.then(someWindow.postMesssage.bind(stuff)) should not allow you to spoof who the sender of the message is. In this case it should basicallly be someone who could legitimately have been the caller of the postMessage method. In practical terms, the IDL spec makes it be the caller of then (if you replace then with a function taking an IDL callback), since that caller could have just called the bound method anyway.

We could, I guess, move the incumbent handling into bind(), but it seems like that still needs ES spec changes and would slow down bind() in general; at least right now only places that need to pay the cost (i.e. IDL callbacks that can get invoked later with no script on the stack) pay it.

@syg
Copy link
Collaborator

syg commented Jan 17, 2020

If we changed the definition of incumbent in a way that is simpler to implement (I think this is what @syg is suggesting), then code like Promise.resolve().then(() => someOtherWindow.postMessage(...args)) would give a different resulting source property than Promise.resolve.then(someOtherWindow.postMessage.bind(someOtherWindow, ...args)).

Yep, that's what I was suggesting.

It still strikes me as odd that, if the ultimate motivation is to counter spoofing for a specific set of APIs, that the countermeasures are not built into those APIs but into all callbacks.

@bzbarsky
Copy link
Contributor

How would you do it in the API?

@syg
Copy link
Collaborator

syg commented Jan 17, 2020

I see your point. I can imagine something fairly intrusive, such as instead of building the logic into F.p.bind, make such APIs have something like a IncumbentSavingFunction on their prototype chain that have the special bind.

Edit: missed operative "instead of"

@littledan
Copy link
Contributor

This is a bit of a tangent, but it's hard for me to see what you could do to bind here. For example, you could get the same "calling with another receiver" behavior by doing someWindow.spoofPostMessage = otherWindow.postMessage and then just call someWindow.spoofPostMessage as a method.

I'm trying to understand the strength of this "no spoofing" goal. We're talking about similar-origin windows that could reach into each other's globals and do all kinds of manipulation, right? This feels like something nice for regularity, but is it intended to be considered a security property?

@bzbarsky
Copy link
Contributor

For example, you could get the same "calling with another receiver" behavior

The issue is not the receiver. The issue is what the callee perceives as "the caller".

I'm trying to understand the strength of this "no spoofing" goal.

That is an interesting question, yes.

@bzbarsky
Copy link
Contributor

I will not be able to get to this until Monday next week

Er, Tuesday. I forgot that Monday is a US holiday.

@bzbarsky
Copy link
Contributor

OK, so I am still trying to page all the Promise stuff back in. @jswalden or @tschneidereit might have this more at the top of their heads, maybe.

Looking at the Gecko code, I think it works like this:

  1. EnqueuePromiseReactionJob (which I hope does what it says on the label) gets an incumbent global that was initially stored in the reaction and stashes it in the job information. That is, the PromiseReactionRecord struct in Gecko stores an incumbent global, captured at the point when the PromiseReactionRecord is created, and this is used as the incumbent for the reaction job.

  2. EnqueuePromiseResolveThenableJob grabs the then-current incumbent and stashes it in the job.

That is, this basically does implement WebIDL semantics, as far as I can see.

@domenic
Copy link
Member Author

domenic commented Jan 29, 2020

Thanks Boris! That's what I suspected. I'd really like to do that at the spec level too, and in Chromium and WebKit, so that promises aren't weird here.

@syg and @littledan, does that seem like a reasonable medium-term goal? I understand it's probably not super high priority to do the kind of ES262 spec work, or V8 work, that would be involved in threading the incumbent through here. (And I don't think this kind of stuff should block work on WeakRef callbacks.) But I'd like to get a sense as to whether it's acceptable as a goal, even if not as something on the roadmap.

@littledan
Copy link
Contributor

littledan commented Feb 10, 2020

[I'd like to defer this to @syg / multiple engine support; I feel like I'm still missing some understanding here, even after @bzbarsky 's patient explanations.]

domenic added a commit to web-platform-tests/wpt that referenced this issue Feb 12, 2020
@syg
Copy link
Collaborator

syg commented Feb 14, 2020

My reading of the V8 code is that correctly threading an incumbent global through is possible with a little work. Isolates already have a GetIncumbentContext() method and there's an RAII thing embedders can use to deal with the backup incumbent context stack, but it is not clear to me that the blink side is using this correctly in all places...

Correctness aside, I have two other concerns:

  1. ISTM looking up the incumbent realm will always incur a stack walk to see if there's a scripted caller, and if so, gets its realm. I don't love the idea of walking the stack for allocating every PromiseReaction...
  2. Even though we don't interop, what should we do if Chrome lands this and finds some breakage?

I'm kind of on the fence. I'm not fully convinced yet of the usefulness of the incumbent concept, and it's only observable when .bind()ing builtins, so it seems pretty edge case-y. I do take the point that it's weird that Promises aren't like literally every other web API, though.

My current position is something like: more interop is always great, so I'm not against trying to implement incumbents in V8 Promises. But as @domenic has said, this is going to be pretty low priority. However, if there are performance regressions that come from this, I envision a hard time justifying engineering effort to optimize for incumbent realm tracking.

Edit: Missed an operative 'not'

moz-v2v-gh pushed a commit to mozilla/gecko-dev that referenced this issue Feb 17, 2020
… for promise jobs, a=testonly

Automatic update from web-platform-tests
Test entry and incumbent settings object for promise jobs

Follows whatwg/html#5212 and whatwg/html#5213.
--

wpt-commits: bc4ddbbddc566c3602d1af8d73c0225f8335855d
wpt-pr: 21206
xeonchen pushed a commit to xeonchen/gecko that referenced this issue Feb 18, 2020
… for promise jobs, a=testonly

Automatic update from web-platform-tests
Test entry and incumbent settings object for promise jobs

Follows whatwg/html#5212 and whatwg/html#5213.
--

wpt-commits: bc4ddbbddc566c3602d1af8d73c0225f8335855d
wpt-pr: 21206
gecko-dev-updater pushed a commit to marco-c/gecko-dev-wordified that referenced this issue Feb 18, 2020
… for promise jobs, a=testonly

Automatic update from web-platform-tests
Test entry and incumbent settings object for promise jobs

Follows whatwg/html#5212 and whatwg/html#5213.
--

wpt-commits: bc4ddbbddc566c3602d1af8d73c0225f8335855d
wpt-pr: 21206

UltraBlame original commit: 3017b5083bf29149178a2c1357796936a5a95065
gecko-dev-updater pushed a commit to marco-c/gecko-dev-wordified-and-comments-removed that referenced this issue Feb 18, 2020
… for promise jobs, a=testonly

Automatic update from web-platform-tests
Test entry and incumbent settings object for promise jobs

Follows whatwg/html#5212 and whatwg/html#5213.
--

wpt-commits: bc4ddbbddc566c3602d1af8d73c0225f8335855d
wpt-pr: 21206

UltraBlame original commit: 3017b5083bf29149178a2c1357796936a5a95065
gecko-dev-updater pushed a commit to marco-c/gecko-dev-comments-removed that referenced this issue Feb 18, 2020
… for promise jobs, a=testonly

Automatic update from web-platform-tests
Test entry and incumbent settings object for promise jobs

Follows whatwg/html#5212 and whatwg/html#5213.
--

wpt-commits: bc4ddbbddc566c3602d1af8d73c0225f8335855d
wpt-pr: 21206

UltraBlame original commit: 3017b5083bf29149178a2c1357796936a5a95065
@domenic domenic closed this as completed in 01574f3 Feb 3, 2021
imhele added a commit to imhele/html that referenced this issue Feb 18, 2021
* Editorial: remove redundant "the"

* Meta: default branch rename

Also correct a broken link. Not even w3.org URLs are that cool.

Helps with whatwg/meta#174.

* Editorial: clean up calls to "parse a URL"

It actually takes a string, so calls should be clear about that.

* Review Draft Publication: January 2021

* Simplify <link>s

In particular, remove their activation behavior, stop them from matching
:link and :visited, and stop suggesting that they be focusable areas.

This also includes a slight expansion and rearrangement of the link
element's section to make it clearer what hyperlinks created by <link>
are meant for, contrasting them to <a> and <area> hyperlinks.

Closes whatwg#4831. Closes whatwg#2617. Helps with whatwg#5490.

* Meta: remove demos/offline/* (whatwg#6307)

These are no longer needed as of e4330d5.

* Meta: minor references cleanup

Use more HTTPS and drop obsolete HTML Differences reference.

* Editorial: anticlockwise → counterclockwise

We use en-US these days. Spotted in https://twitter.com/iso2022jp/status/1352601086519955456.

* Use :focus-visible in the UA stylesheet

See w3c/csswg-drafts#4278.

* Editorial: align with WebIDL and Infra

* Fix "update a style block" early return

The new version matches implementation reality and CSSWG resolution.

The algorithm was also inconsistent, as it looked at whether
the element was in a shadow tree or in the document tree, but it was
only specified to be re-run if the element becomes connected or
disconnected.

The CSSWG discussed this in
w3c/csswg-drafts#3096 (comment)
and http://wpt.live/shadow-dom/ShadowRoot-interface.html tests this.

This also matches closer the definition of <link rel="stylesheet">,
which does use connectedness (though it uses "browsing-context
connected", which is a bit different):
https://html.spec.whatwg.org/#link-type-stylesheet

* Modernize and refactor simple dialogs

This contains a small bug fix, in that confirm() and prompt() said
"return" in some cases instead of "return false" or "return null" as
appropriate.

Other notable changes, all editorial, are:

* Factoring out repeated "cannot show simple dialogs" steps, which will
  likely expand over time (see e.g. whatwg#6297).
* Separating out and explaining the no-argument overload of alert().
* Passing the document through to the "printing steps", instead of just
  having them talk about "this Window object".

* Meta: add definition markup for MessageEvent

* Remove <marquee> events

They are only supported by one engine (Gecko).

Closes whatwg#2957.

* Clarify when microtasks happen

* Ignore COEP on non-secure contexts

Fixes whatwg#6328.

* Editorial: update URL Standard integration

* Editorial: only invoke response's location URL once

Complements whatwg/fetch#1149.

* Track the incumbent settings and active script in Promise callbacks

Closes whatwg#5213.

* createImageBitmap(): stop clipping sourceRect to source's dimensions

It has been found in whatwg#6306 that this was an oversight at the time of its introduction. Current behavior goes against author expectations and no implementer has opposed the change to "no-clip".

Tests: web-platform-tests/wpt#27040.

Closes whatwg#6306.

* Remove CSP plugin-types blocking

With Flash not being supported anymore, the CSP directive plugin-types has lost its main reason for being and is being removed from the Content Security Policy specification: w3c/webappsec-csp#456.

This change removes references to the relevant algorithm from the Content Security Policy spec.

* Meta: set more dfn types

A follow-up to:

* whatwg#5694
* whatwg#5916

* Editorial: occuring → occurring

* Make all plugin-related APIs no-ops

Part of whatwg#6003.

* Disallow simple dialogs from different-origin domain iframes

Closes whatwg#5407.

* Revive @@iterator for PluginArray/MimeTypeArray/Plugin

@@iterator is implicitly installed by defining an indexed property getter. Since there is no other way to define it exclusively, this restores some methods back to being indexed getters.

This fixes an inadvertent observable behavior change in d4f07b8.

* Adjust web+ scheme security considerations to account for FTP removal

Also, network scheme is now reduced to HTTP(S) scheme.

Helps with whatwg#5375, but form submission issue remains.

See whatwg/fetch#1166 for context.

* Meta: export pause

Nobody but XMLHttpRequest take a dependency on this please. You have been warned.

Context: whatwg/xhr#311.

* Fix typo: ancestor → accessor

Fixes whatwg#6374.

Co-authored-by: Dominic Farolino <domfarolino@gmail.com>
Co-authored-by: Anne van Kesteren <annevk@annevk.nl>
Co-authored-by: Domenic Denicola <d@domenic.me>
Co-authored-by: Emilio Cobos Álvarez <emilio@crisal.io>
Co-authored-by: Momdo Nakamura <xmomdo@gmail.com>
Co-authored-by: Jake Archibald <jaffathecake@gmail.com>
Co-authored-by: Yutaka Hirano <yhirano@chromium.org>
Co-authored-by: Shu-yu Guo <syg@chromium.org>
Co-authored-by: Kaiido <tristan.fraipont@gmail.com>
Co-authored-by: Antonio Sartori <anton.sartori@gmail.com>
Co-authored-by: Michael[tm] Smith <mike@w3.org>
Co-authored-by: Ikko Ashimine <eltociear@gmail.com>
Co-authored-by: Carlos IL <carlosjoan91@gmail.com>
Co-authored-by: Kagami Sascha Rosylight <saschanaz@outlook.com>
Co-authored-by: Simon Pieters <zcorpan@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

4 participants