- 
                Notifications
    
You must be signed in to change notification settings  - Fork 0
 
Description
During the plenary I expressed my wish to explore the more general problem of risk of reentrancy when handling promise-like objects,
I believe all security issues are in fact rooted in this problem, and handling an object that is unexpectedly a thenable is only one of the manifestations. While unexpected thenables can also cause confusion and unexpected result values (e.g. thenable modules dynamically imported), I don't believe these would cause security issue if it the implicit promise resolve mechanism guarded against reentrancy.
Reentrancy in promise resolution
Overview of spec operations
The PromiseResolve(C, x) steps:
- If
 IsPromise(x) is true, then
a. Let xConstructor be ?Get(x, "constructor").
b. IfSameValue(xConstructor, C) is true, return x.- Let promiseCapability be ?
 NewPromiseCapability(C).- Perform ?
 Call(promiseCapability.[[Resolve]], undefined, « x »).- Return promiseCapability.[[Promise]].
 
Where [[Resolve]] for the intrinsic %Promise% constructor is:
- Let F be the active function object.
 - Assert: F has a [[Promise]] internal slot whose value is an Object.
 - Let promise be F.[[Promise]].
 - Let alreadyResolved be F.[[AlreadyResolved]].
 - If alreadyResolved.[[Value]] is true, return undefined.
 - Set alreadyResolved.[[Value]] to true.
 - If
 SameValue(resolution, promise) is true, then
a. Let selfResolutionError be a newly created TypeError object.
b. PerformRejectPromise(promise, selfResolutionError).
c. Return undefined.- If resolution is not an Object, then
 
a. PerformFulfillPromise(promise, resolution).
b. Return undefined.- Let then be
 Completion(Get(resolution, "then")).- If then is an abrupt completion, then
 
a. PerformRejectPromise(promise, then.[[Value]]).
b. Return undefined.- Let thenAction be then.[[Value]].
 - If
 IsCallable(thenAction) is false, then
a. PerformFulfillPromise(promise, resolution).
b. Return undefined.- Let thenJobCallback be
 HostMakeJobCallback(thenAction).- Let job be
 NewPromiseResolveThenableJob(promise, resolution, thenJobCallback).- Perform
 HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).- Return undefined.
 
The most common reentrancy point in step 9 in the resolve steps above:
Get(resolution, "then")
The less common but still dangerous one is in step 1.a of Promise.resolve():
Get(x, "constructor")
Reentrancy cases
PromiseResolve
Any code using PromiseResolve(%Promise%, value) to transform a value into what they believe to be a safe promise value would encounter the following risks of reentrancy during PromiseResolve itself:
- A promise object which has an own 
constructorgetter property - A Promise subclass object which has a 
constructorgetter property on its prototype - A promise object when the 
%Promise.prototype%'sconstructorproperty has been modified into an accessor. 
Please note that the value returned by PromiseResolve(%Promise%, value) may not in fact be safe to handle. See below.
Resolve steps
During the Resolve Function Steps, any resolution object which has a custom then behavior may cause reentrancy. Some notable cases:
- An object with an own 
thenaccessor, including a promise object. - A proxy object with a 
gettrap (cannot be a promise object) - An object with a 
thenaccessor on its prototype chain- an object created by the spec which inherits from 
%Object.prototype%, and%Object.prototype%had a.thenaccessor property added - a promise object which inherits from 
%Promise.prototype%, which has been modified with athenaccessor property 
 - an object created by the spec which inherits from 
 
The PromiseResolve operation may have returned a promise object with custom then behavior, either as an own property or through a modified %Promise.prototype%.
Any Call(promiseCapability.[[Resolve]] in the spec may in fact be susceptible to reentrancy if the result is one of these objects. In particular, PromiseResolve would have itself triggered these then custom behavior for any non promise object that didn't match the C promise constructor.
Proposed mitigation
Exact details TBD, but the general idea is to make PromiseResolve guaranteed to return a promise without causing reentrancy itself when called with the intrinsic %Promise% constructor. Then any spec code can use PerformPromiseThen on that returned promise and be guaranteed not to have caused reentrancy (or a throw), including Await.
User code can build a SafePromiseResolve that uses Promise.resolve() and verifies that the returned promise does not have own constructor and then properties (and inherits from %Promise.prototype% if not already checked by PromiseResolve itself), which it can all do safely since the returned promise is guaranteed to not be a proxy. The user code is then guaranteed that calling .then is safe (as long as Promise.prototype.then was not replaced, but that's not something it's in position to protect itself against unless the user code has first run on realm creation).
Unfortunately I don't think there is a way to make the "promise resolve function" not trigger any then hooks without either incurring a 1 tick delay for every non promise object used as resolution value, or allowing the creation of an async predicate that can detect whether an object is a proxy. Code interested in guarding itself against reentrancy would need to use PromiseResolve explicitly.
Related discussions
tc39/proposal-faster-promise-adoption#1 discusses some similar options for fast-pathing promise adoption which would prevent some reentrancy cases.