Skip to content

tc39/proposal-thenable-curtailment

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Curtailing the power of "Thenables"

Champion: Matthew Gaudet (Mozilla)

Stage: 1 (As of February Plenary)

Draft Spec Text: https://tc39.es/proposal-thenable-curtailment/

Introduction & Problem:

Quoting MDN:

The JavaScript ecosystem had made multiple Promise implementations long before it became part of the language. Despite being represented differently internally, at the minimum, all Promise-like objects implement the Thenable interface. A thenable implements the .then() method, which is called with two callbacks: one for when the promise is fulfilled, one for when it's rejected. Promises are thenables as well.

To interoperate with the existing Promise implementations, the language allows using thenables in place of promises. For example, Promise.resolve will not only resolve promises, but also trace thenables.

The problem we would like to address is that then lookup follows the whole prototype chain. Including builtin prototypes and Object.prototype. This is particularly dangerous when working with types where 'thenable' dispatch was unexpected.

Why is this a problem?

The most concrete one is security vulnerabilities. We must be ever-vigilant about this compatability supporting feature in all standard work, and throughout the web-platform. Failure to do so has the consequence of possible exploitation:

The reason this particular issue is fingered for causing security vulnerabities is that it adds many paths for user code execution which otherwise don't exist, and is not always obviously a possbility.

Of particular danger is where specification authors think of newborn objects of known types as known quantities, only to call Promise.resolve on them. At this point when they are provided a JS wrapper the JS wrapper typically has Object as their prototype, making them vulnerable to thenables.

Beyond security, this also just injects complexity. There are test cases in WPT that exist purely to work out the expected behaviour for someone breaking then

How do I propose we fix this?

I'd like to propose we add a (maybe-)delaying resolve operation. As per ususal naming will be a substantial challenging, but a general principle here would be, it is functionally identical to PromiseResolve except there is a pre-step where we check for the conditions under which we could run user-code. If we cannot run any user code, we simply tail-call into PromiseResolve. If we could run user-code, we instead enqueue a new job whose responsibility is to call into PromiseResolve, while also putting the promise into a 'parked' state such that any future resolutions are ignored (Thank you very much to Mark Miller for catching this requirement in TG3 review discussion).

The next step is to decide how to consume this. There is interest from the Mozilla DOM to explore using this to replace the steps for resolving a promise in WebIDL and more generally powering all the promise resolution code in Mozilla's DOM. This would help make C++ code safer by making promise resolution into an operation that never runs script, which simplifies the reasoning required when implementing code.

Another topic of discussion would be: If we builds this capability, do we expose it to userland code and if so, under what name.

Is this a bulletproof fix?

No. The impact of this will of course depend entirely on the scope of adoption.s

Of the previously described security bugs this mitigation would fix

It would not however fix

  • CVE-2024-43357 on the specification, as it's very unlikely we would adopt this new operation on that path.

Compatibility

This could change the order in which microtasks get resolved when 'thenables' are involved. The hope is that the majority of code dealing with promises is already relatively robust to execution order. However, it is certainly plausible this could cause a web compatibility problem.

Experiment: WebIDL

Q: Can we use a MaybeDeferredPromiseResolve to replace the promise resolution steps in WebIDL?

Experiment: Run WPT with the Firefox DOM Promise resolve steps replaced with MaybeDeferredPromiseResolve1.

Results:

The vast majority of tests (as expected) pass.

Timeout Failures:

  1. https://searchfox.org/firefox-main/source/testing/web-platform/tests/css/css-overflow/scroll-marker-in-display-none-column-crash.html -- I didn't quite figure this one out.
  2. /custom-elements/when-defined-reentry-crash.html -- this one is using a then on Object.prototype for nefarious aims. In a sense this is exactly the kind of issue we’re trying to address. https://issues.chromium.org/issues/40061097

Unexpected Pass:

  1. /fetch/api/response/response-body-read-task-handling.html - This test is using then to get insight into execution order. The test no longer tests what it thinks it is testing anymore; however the test -also- was created to address this kind of thennable issue.

Test Failures

  1. /streams/readable-byte-streams/patched-global.any.js -- Explicitly using then to peek into execution state we’d probably prefer to not be observable.
  2. /document-picture-in-picture/returns-window-with-document.https.html | requestWindow timing - assert\_equals: Got the expected order of actions expected "requestWindow,microtask,enter" but got "microtask,requestWindow,enter" -- The job timing changes because it’s resolving a promise with a window (WindowProxy) object, which causes an extra tick.
  3. /web-animations/interfaces/Animation/cancel.html; observing event timing with thenable.

Prior Art & Related Work

  • Symbol.thenable "Withdrawn; changing thenability on Module Namespace objects is not web compatible, and allowing non-Promise use of "then" is not worth slowing down all Promise operations"
  • Proposal Stabilize is trying to provide generalizable machineries for invariants -- this could be more of an invariant we could provide to user code as well.

Proposal History

Footnotes

  1. This is slightly more broad than strictly doing WebIDL because I think there’s non IDL use of dom::Promise.

About

A proposal to curtail the power of "thenable" objects.

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks