In short, this can result in type confusion on non-debug build, due to dequeueing from empty list once or more, on certain situation. I haven't yet figured out how much exploitable this is tho, it should be sec-high or higher maybe? Here's the details: In term of requests in the queue, the behavior is the following: https://tc39.es/ecma262/#sec-asyncgenerator-prototype-return ``` %AsyncGeneratorPrototype%.return ( value ) 1. Let generator be the this value. 2. Let promiseCapability be ! NewPromiseCapability(%Promise%). ... 5. Let completion be Completion Record { [[Type]]: return, [[Value]]: value, [[Target]]: empty }. 6. Perform AsyncGeneratorEnqueue(generator, completion, promiseCapability). 7. Let state be generator.[[AsyncGeneratorState]]. 8. If state is either suspended-start or completed, then a. Set generator.[[AsyncGeneratorState]] to awaiting-return. b. Perform ! AsyncGeneratorAwaitReturn(generator). ... 11. Return promiseCapability.[[Promise]]. ``` The `%AsyncGeneratorPrototype%.return` step 6 performs `AsyncGeneratorEnqueue`, which enqueues one `AsyncGeneratorRequest`, let's call this `Request1`. https://tc39.es/ecma262/#sec-asyncgeneratorenqueue ``` AsyncGeneratorEnqueue ( generator, completion, promiseCapability ) 1. Let request be AsyncGeneratorRequest { [[Completion]]: completion, [[Capability]]: promiseCapability }. 2. Append request to generator.[[AsyncGeneratorQueue]]. 3. Return unused. ``` And `%AsyncGeneratorPrototype%.return` step 8.b performs `AsyncGeneratorAwaitReturn`. At this point, there's `Request1` in the queue, and the assertion at step 2 passes. The `completion` is the `Completion Record { [[Type]]: return, [[Value]]: value, [[Target]]: empty }`, created at the `%AsyncGeneratorPrototype%.return` step 5, where `[[Value]]` is the `return` method's parameter. In the reduced testcase, it's undefined, but it can be arbitrary value, let's say `Value1`. The step 6 resolves a newly created promise, let's call `Promise1` with `Value1`, and the step 11 performs `PerformPromiseThen` on `Promise1` with the `fulfilledClosure`, which results in creating and enqueueing a promise reaction job, let's call `ReactionJob1`, for `fulfilledClosure`. https://tc39.es/ecma262/#sec-asyncgeneratorawaitreturn ``` AsyncGeneratorAwaitReturn ( generator ) 1. Let queue be generator.[[AsyncGeneratorQueue]]. 2. Assert: queue is not empty. 3. Let next be the first element of queue. 4. Let completion be Completion(next.[[Completion]]). 5. Assert: completion is a return completion. 6. Let promise be ? PromiseResolve(%Promise%, completion.[[Value]]). 7. Let fulfilledClosure be a new Abstract Closure with parameters (value) that captures generator and performs the following steps when called: ... 8. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 1, "", « »). ... 11. Perform PerformPromiseThen(promise, onFulfilled, onRejected). 12. Return unused. ``` In `ReactionJob1`, the `fulfilledClosure` is called, and the step 7.c performs `AsyncGeneratorCompleteStep` https://tc39.es/ecma262/#sec-asyncgeneratorawaitreturn ``` AsyncGeneratorAwaitReturn ( generator ) ... 7. Let fulfilledClosure be a new Abstract Closure with parameters (value) that captures generator and performs the following steps when called: a. Set generator.[[AsyncGeneratorState]] to completed. b. Let result be NormalCompletion(value). c. Perform AsyncGeneratorCompleteStep(generator, result, true). d. Perform AsyncGeneratorDrainQueue(generator). e. Return undefined. ``` At this point, `generator.[[AsyncGeneratorQueue]]` still contains only the `Request1`, and the assertion at the step 1 passes, and `next` becomes the `Request1`, and `Request1` is removed from the `generator.[[AsyncGeneratorQueue]]` at the step 3, which results in `generator.[[AsyncGeneratorQueue]]` being empty in the testcase (but it's not necessarily be empty in general). Steps 5-7 resolve the `Reqeust1.[[Capability]]` with a newly created iterator result object. https://tc39.es/ecma262/#sec-asyncgeneratorcompletestep ``` AsyncGeneratorCompleteStep ( generator, completion, done [ , realm ] ) 1. Assert: generator.[[AsyncGeneratorQueue]] is not empty. 2. Let next be the first element of generator.[[AsyncGeneratorQueue]]. 3. Remove the first element from generator.[[AsyncGeneratorQueue]]. 4. Let promiseCapability be next.[[Capability]]. 5. Let value be completion.[[Value]]. ... 7. Else, a. Assert: completion is a normal completion. b. If realm is present, then ... iii. Let iteratorResult be CreateIterResultObject(value, done). ... c. Else, i. Let iteratorResult be CreateIterResultObject(value, done). d. Perform ! Call(promiseCapability.[[Resolve]], undefined, « iteratorResult »). 8. Return unused. ``` Then, in the testcase, `%AsyncGeneratorPrototype%.return` is called at the step 7.d. * Let's call the `return` paramter `Value2` * It enqueues yet another `AsyncGeneratorRequest`, let's call this `Request2` at `%AsyncGeneratorPrototype%.return` step 6 * It created and resolved promise `Promise2` with `Value2` * It performs `PerformPromiseThen` on `Promise2`, and enqueues yet another promise reaction job `ReactionJob2` at the `AsyncGeneratorAwaitReturn` step 11, performed at the `%AsyncGeneratorPrototype%.return` step 8.b The `Request2` remains in the `generator.[[AsyncGeneratorQueue]]` at this point. https://tc39.es/ecma262/#sec-asyncgenerator-prototype-return ``` %AsyncGeneratorPrototype%.return ( value ) ... 6. Perform AsyncGeneratorEnqueue(generator, completion, promiseCapability). 7. Let state be generator.[[AsyncGeneratorState]]. 8. If state is either suspended-start or completed, then a. Set generator.[[AsyncGeneratorState]] to awaiting-return. b. Perform ! AsyncGeneratorAwaitReturn(generator). ... ``` After that, going back to `AsyncGeneratorDrainQueue`, the step d performs `AsyncGeneratorDrainQueue`. At this point, `generator.[[AsyncGeneratorQueue]]` contains only `Request2`. As mentioned above, there can be some more requests in general, and the step 3 `If queue is empty, return unused.` doesn't necessarily match. At the step 5.a, `next` becomes the `Request2`, and its `[[Completion]]` is `Completion Record { [[Type]]: return, [[Value]]: value, [[Target]]: empty }` created at the `%AsyncGeneratorPrototype%.return` step 5. Given it's `return` completion, it matches the step 5.c, and performs `AsyncGeneratorAwaitReturn` at step 5.c.ii https://tc39.es/ecma262/#sec-asyncgeneratordrainqueue ``` AsyncGeneratorDrainQueue ( generator ) 1. Assert: generator.[[AsyncGeneratorState]] is completed. 2. Let queue be generator.[[AsyncGeneratorQueue]]. 3. If queue is empty, return unused. 4. Let done be false. 5. Repeat, while done is false, a. Let next be the first element of queue. b. Let completion be Completion(next.[[Completion]]). c. If completion is a return completion, then i. Set generator.[[AsyncGeneratorState]] to awaiting-return. ii. Perform ! AsyncGeneratorAwaitReturn(generator). iii. Set done to true. ... 6. Return unused. ``` In `AsyncGeneratorAwaitReturn`, step 3 finds `Request2` again. It resolves newly created promise `Promise3` with `Value2`, and performs `PerformPromiseThen` on `Promise3`, which results in enqueueing `ReactionJob3`. https://tc39.es/ecma262/#sec-asyncgeneratorawaitreturn ``` AsyncGeneratorAwaitReturn ( generator ) 1. Let queue be generator.[[AsyncGeneratorQueue]]. 2. Assert: queue is not empty. 3. Let next be the first element of queue. 4. Let completion be Completion(next.[[Completion]]). 5. Assert: completion is a return completion. 6. Let promise be ? PromiseResolve(%Promise%, completion.[[Value]]). 7. Let fulfilledClosure be a new Abstract Closure with parameters (value) that captures generator and performs the following steps when called: ... 8. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 1, "", « »). ... 11. Perform PerformPromiseThen(promise, onFulfilled, onRejected). 12. Return unused. ``` `Promise2` and `Promise3` are both resolved with `Value2`, and `ReactionJob2` and `ReactionJob3` will be called with the same value, comes from `Value2`. If `Value2` is a promise from the same realm, or if it's not an object, those reaction jobs will receive `Value2` itself. Otherwise, `Value2`'s `then` property is accessed at the `Promise Resolve Functions` step 9, and thenable job will call the `then` function. These operations are duplicated because of the duplicate `AsyncGeneratorAwaitReturn` calls. https://tc39.es/ecma262/#sec-promise-resolve-functions ``` Promise Resolve Functions ... 8. If resolution is not an Object, then a. Perform FulfillPromise(promise, resolution). b. Return undefined. 9. Let then be Completion(Get(resolution, "then")). 10. If then is an abrupt completion, then a. Perform RejectPromise(promise, then.[[Value]]). b. Return undefined. 11. Let thenAction be then.[[Value]]. 12. If IsCallable(thenAction) is false, then a. Perform FulfillPromise(promise, resolution). b. Return undefined. 13. Let thenJobCallback be HostMakeJobCallback(thenAction). 14. Let job be NewPromiseResolveThenableJob(promise, resolution, thenJobCallback). 15. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). ... ``` https://tc39.es/ecma262/#sec-promise-resolve ``` 27.2.4.7.1 PromiseResolve ( C, x ) The abstract operation PromiseResolve takes arguments C (a constructor) and x (an ECMAScript language value) and returns either a normal completion containing an ECMAScript language value or a throw completion. It returns a new promise resolved with x. It performs the following steps when called: 1. If IsPromise(x) is true, then a. Let xConstructor be ? Get(x, "constructor"). b. If SameValue(xConstructor, C) is true, return x. 2. Let promiseCapability be ? NewPromiseCapability(C). 3. Perform ? Call(promiseCapability.[[Resolve]], undefined, « x »). 4. Return promiseCapability.[[Promise]]. ``` https://tc39.es/ecma262/#sec-asyncgeneratorawaitreturn ``` AsyncGeneratorAwaitReturn ( generator ) ... 7. Let fulfilledClosure be a new Abstract Closure with parameters (value) that captures generator and performs the following steps when called: a. Set generator.[[AsyncGeneratorState]] to completed. b. Let result be NormalCompletion(value). c. Perform AsyncGeneratorCompleteStep(generator, result, true). d. Perform AsyncGeneratorDrainQueue(generator). e. Return undefined. ``` Then, with either the same `Value2`, or some value that `Value2.then(...)` produces, `ReactionJob2` and `ReactionJob3` are called, and `AsyncGeneratorCompleteStep` is called for each. Then, if `ReactionJob2` is called first, it dequeues `Request2` from `generator.[[AsyncGeneratorQueue]]`, and resolves it with `Value2` or the value `Value2.then(...)` produces. After that, `ReactionJob3` is called, and it tries to dequeue request, but the queue is already empty. The assertion at step 1 fails, and the step 2 doesn't work. https://tc39.es/ecma262/#sec-asyncgeneratorcompletestep ``` AsyncGeneratorCompleteStep ( generator, completion, done [ , realm ] ) 1. Assert: generator.[[AsyncGeneratorQueue]] is not empty. 2. Let next be the first element of generator.[[AsyncGeneratorQueue]]. 3. Remove the first element from generator.[[AsyncGeneratorQueue]]. 4. Let promiseCapability be next.[[Capability]]. 5. Let value be completion.[[Value]]. ... 7. Else, a. Assert: completion is a normal completion. b. If realm is present, then ... iii. Let iteratorResult be CreateIterResultObject(value, done). ... c. Else, i. Let iteratorResult be CreateIterResultObject(value, done). d. Perform ! Call(promiseCapability.[[Resolve]], undefined, « iteratorResult »). 8. Return unused. ``` In SpiderMonkey's case, the dequeue is performed by `AsyncGeneratorObject::dequeueRequest`. https://searchfox.org/mozilla-central/rev/46d0387f0b582f00a5722c20d4e6b8693793631b/js/src/vm/AsyncIteration.cpp#444-457 ```cpp [[nodiscard]] static bool AsyncGeneratorCompleteStepNormal( JSContext* cx, Handle<AsyncGeneratorObject*> generator, HandleValue value, bool done) { // Step 1. Let queue be generator.[[AsyncGeneratorQueue]]. // Step 2. Assert: queue is not empty. MOZ_ASSERT(!generator->isQueueEmpty()); // Step 3. Let next be the first element of queue. // Step 4. Remove the first element from queue. AsyncGeneratorRequest* next = AsyncGeneratorObject::dequeueRequest(cx, generator); if (!next) { return false; } ``` If there was at most one request in the queue, it means the queue is now empty, and the `Slot_QueueOrRequest` value is `NullValue`, it matches `generator->isSingleQueue()` case, and it returns `generator->singleQueueRequest()`. https://searchfox.org/mozilla-central/rev/46d0387f0b582f00a5722c20d4e6b8693793631b/js/src/vm/AsyncIteration.cpp#158-167 ```cpp AsyncGeneratorRequest* AsyncGeneratorObject::dequeueRequest( JSContext* cx, Handle<AsyncGeneratorObject*> generator) { if (generator->isSingleQueue()) { AsyncGeneratorRequest* request = generator->singleQueueRequest(); generator->clearSingleQueueRequest(); return request; } Rooted<ListObject*> queue(cx, generator->queue()); return &queue->popFirstAs<AsyncGeneratorRequest>(cx); ``` Here, it tries to treat it as `AsyncGeneratorRequest`. This also hits assertion failure, but on non-debug build, it results in returning `nullptr` on 32-bit, and `0x0004000000000000` on 64-bit, which is object pointer extracted from `NullValue`. This results in simple crash. https://searchfox.org/mozilla-central/rev/46d0387f0b582f00a5722c20d4e6b8693793631b/js/src/vm/AsyncIteration.h#430-434 ```cpp AsyncGeneratorRequest* singleQueueRequest() const { return &getFixedSlot(Slot_QueueOrRequest) .toObject() .as<AsyncGeneratorRequest>(); } ``` On the other hand, if there were 2 or more requests, `Slot_QueueOrRequest` contains a ListObject, and `popFirstAs` is called, and it can read random value (for empty case, or -1 items case), and results in type confusion. https://searchfox.org/mozilla-central/rev/46d0387f0b582f00a5722c20d4e6b8693793631b/js/src/vm/AsyncIteration.cpp#158-159,166-167 ```cpp AsyncGeneratorRequest* AsyncGeneratorObject::dequeueRequest( JSContext* cx, Handle<AsyncGeneratorObject*> generator) { ... Rooted<ListObject*> queue(cx, generator->queue()); return &queue->popFirstAs<AsyncGeneratorRequest>(cx); ``` https://searchfox.org/mozilla-central/rev/46d0387f0b582f00a5722c20d4e6b8693793631b/js/src/vm/List-inl.h#86-89 ```cpp template <class T> inline T& js::ListObject::popFirstAs(JSContext* cx) { return popFirst(cx).toObject().as<T>(); } ``` https://searchfox.org/mozilla-central/rev/46d0387f0b582f00a5722c20d4e6b8693793631b/js/src/vm/List-inl.h#57-70 ```cpp inline JS::Value js::ListObject::popFirst(JSContext* cx) { uint32_t len = length(); MOZ_ASSERT(len > 0); JS::Value entry = get(0); if (!tryShiftDenseElements(1)) { moveDenseElements(0, 1, len - 1); setDenseInitializedLength(len - 1); shrinkElements(cx, len - 1); } MOZ_ASSERT(length() == len - 1); return entry; } ``` Then, this is specific to ``%AsyncGeneratorPrototype%.return`, given `next` and `throw` doesn't change the `generator.[[AsyncGeneratorState]]` and doesn't enqueue, if it's completed. https://tc39.es/ecma262/#sec-asyncgenerator-prototype-next ``` %AsyncGeneratorPrototype%.next ( value ) ... 5. Let state be generator.[[AsyncGeneratorState]]. 6. If state is completed, then a. Let iteratorResult be CreateIterResultObject(undefined, true). b. Perform ! Call(promiseCapability.[[Resolve]], undefined, « iteratorResult »). c. Return promiseCapability.[[Promise]]. ... ``` https://tc39.es/ecma262/#sec-asyncgenerator-prototype-throw ``` %AsyncGeneratorPrototype%.throw ( exception ) ... 5. Let state be generator.[[AsyncGeneratorState]]. ... 7. If state is completed, then a. Perform ! Call(promiseCapability.[[Reject]], undefined, « exception »). b. Return promiseCapability.[[Promise]]. ... ```
Bug 1901411 Comment 4 Edit History
Note: The actual edited comment in the bug view page will always show the original commenter’s name and original timestamp.
In short, this can result in type confusion on non-debug build, due to dequeueing from empty list once or more, on certain situation. I haven't yet figured out how much exploitable this is tho, it should be sec-high or higher maybe? Here's the details: In term of requests in the queue, the behavior is the following: https://tc39.es/ecma262/#sec-asyncgenerator-prototype-return ``` %AsyncGeneratorPrototype%.return ( value ) 1. Let generator be the this value. 2. Let promiseCapability be ! NewPromiseCapability(%Promise%). ... 5. Let completion be Completion Record { [[Type]]: return, [[Value]]: value, [[Target]]: empty }. 6. Perform AsyncGeneratorEnqueue(generator, completion, promiseCapability). 7. Let state be generator.[[AsyncGeneratorState]]. 8. If state is either suspended-start or completed, then a. Set generator.[[AsyncGeneratorState]] to awaiting-return. b. Perform ! AsyncGeneratorAwaitReturn(generator). ... 11. Return promiseCapability.[[Promise]]. ``` The `%AsyncGeneratorPrototype%.return` step 6 performs `AsyncGeneratorEnqueue`, which enqueues one `AsyncGeneratorRequest`, let's call this `Request1`. https://tc39.es/ecma262/#sec-asyncgeneratorenqueue ``` AsyncGeneratorEnqueue ( generator, completion, promiseCapability ) 1. Let request be AsyncGeneratorRequest { [[Completion]]: completion, [[Capability]]: promiseCapability }. 2. Append request to generator.[[AsyncGeneratorQueue]]. 3. Return unused. ``` And `%AsyncGeneratorPrototype%.return` step 8.b performs `AsyncGeneratorAwaitReturn`. At this point, there's `Request1` in the queue, and the assertion at step 2 passes. The `completion` is the `Completion Record { [[Type]]: return, [[Value]]: value, [[Target]]: empty }`, created at the `%AsyncGeneratorPrototype%.return` step 5, where `[[Value]]` is the `return` method's parameter. In the reduced testcase, it's undefined, but it can be arbitrary value, let's say `Value1`. The step 6 resolves a newly created promise, let's call `Promise1` with `Value1`, and the step 11 performs `PerformPromiseThen` on `Promise1` with the `fulfilledClosure`, which results in creating and enqueueing a promise reaction job, let's call `ReactionJob1`, for `fulfilledClosure`. https://tc39.es/ecma262/#sec-asyncgeneratorawaitreturn ``` AsyncGeneratorAwaitReturn ( generator ) 1. Let queue be generator.[[AsyncGeneratorQueue]]. 2. Assert: queue is not empty. 3. Let next be the first element of queue. 4. Let completion be Completion(next.[[Completion]]). 5. Assert: completion is a return completion. 6. Let promise be ? PromiseResolve(%Promise%, completion.[[Value]]). 7. Let fulfilledClosure be a new Abstract Closure with parameters (value) that captures generator and performs the following steps when called: ... 8. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 1, "", « »). ... 11. Perform PerformPromiseThen(promise, onFulfilled, onRejected). 12. Return unused. ``` In `ReactionJob1`, the `fulfilledClosure` is called, and the step 7.c performs `AsyncGeneratorCompleteStep` https://tc39.es/ecma262/#sec-asyncgeneratorawaitreturn ``` AsyncGeneratorAwaitReturn ( generator ) ... 7. Let fulfilledClosure be a new Abstract Closure with parameters (value) that captures generator and performs the following steps when called: a. Set generator.[[AsyncGeneratorState]] to completed. b. Let result be NormalCompletion(value). c. Perform AsyncGeneratorCompleteStep(generator, result, true). d. Perform AsyncGeneratorDrainQueue(generator). e. Return undefined. ``` At this point, `generator.[[AsyncGeneratorQueue]]` still contains only the `Request1`, and the assertion at the step 1 passes, and `next` becomes the `Request1`, and `Request1` is removed from the `generator.[[AsyncGeneratorQueue]]` at the step 3, which results in `generator.[[AsyncGeneratorQueue]]` being empty in the testcase (but it's not necessarily be empty in general). Steps 5-7 resolve the `Reqeust1.[[Capability]]` with a newly created iterator result object. https://tc39.es/ecma262/#sec-asyncgeneratorcompletestep ``` AsyncGeneratorCompleteStep ( generator, completion, done [ , realm ] ) 1. Assert: generator.[[AsyncGeneratorQueue]] is not empty. 2. Let next be the first element of generator.[[AsyncGeneratorQueue]]. 3. Remove the first element from generator.[[AsyncGeneratorQueue]]. 4. Let promiseCapability be next.[[Capability]]. 5. Let value be completion.[[Value]]. ... 7. Else, a. Assert: completion is a normal completion. b. If realm is present, then ... iii. Let iteratorResult be CreateIterResultObject(value, done). ... c. Else, i. Let iteratorResult be CreateIterResultObject(value, done). d. Perform ! Call(promiseCapability.[[Resolve]], undefined, « iteratorResult »). 8. Return unused. ``` Then, in the testcase, `%AsyncGeneratorPrototype%.return` is called at the step 7.d. * Let's call the `return` paramter `Value2` * It enqueues yet another `AsyncGeneratorRequest`, let's call this `Request2` at `%AsyncGeneratorPrototype%.return` step 6 * It created and resolved promise `Promise2` with `Value2` * It performs `PerformPromiseThen` on `Promise2`, and enqueues yet another promise reaction job `ReactionJob2` at the `AsyncGeneratorAwaitReturn` step 11, performed at the `%AsyncGeneratorPrototype%.return` step 8.b The `Request2` remains in the `generator.[[AsyncGeneratorQueue]]` at this point. https://tc39.es/ecma262/#sec-asyncgenerator-prototype-return ``` %AsyncGeneratorPrototype%.return ( value ) ... 6. Perform AsyncGeneratorEnqueue(generator, completion, promiseCapability). 7. Let state be generator.[[AsyncGeneratorState]]. 8. If state is either suspended-start or completed, then a. Set generator.[[AsyncGeneratorState]] to awaiting-return. b. Perform ! AsyncGeneratorAwaitReturn(generator). ... ``` After that, going back to `AsyncGeneratorAwaitReturn`, the step 7.d performs `AsyncGeneratorDrainQueue`. At this point, `generator.[[AsyncGeneratorQueue]]` contains only `Request2`. As mentioned above, there can be some more requests in general, and the step 3 `If queue is empty, return unused.` doesn't necessarily match. At the step 5.a, `next` becomes the `Request2`, and its `[[Completion]]` is `Completion Record { [[Type]]: return, [[Value]]: value, [[Target]]: empty }` created at the `%AsyncGeneratorPrototype%.return` step 5. Given it's `return` completion, it matches the step 5.c, and performs `AsyncGeneratorAwaitReturn` at step 5.c.ii https://tc39.es/ecma262/#sec-asyncgeneratordrainqueue ``` AsyncGeneratorDrainQueue ( generator ) 1. Assert: generator.[[AsyncGeneratorState]] is completed. 2. Let queue be generator.[[AsyncGeneratorQueue]]. 3. If queue is empty, return unused. 4. Let done be false. 5. Repeat, while done is false, a. Let next be the first element of queue. b. Let completion be Completion(next.[[Completion]]). c. If completion is a return completion, then i. Set generator.[[AsyncGeneratorState]] to awaiting-return. ii. Perform ! AsyncGeneratorAwaitReturn(generator). iii. Set done to true. ... 6. Return unused. ``` In `AsyncGeneratorAwaitReturn`, step 3 finds `Request2` again. It resolves newly created promise `Promise3` with `Value2`, and performs `PerformPromiseThen` on `Promise3`, which results in enqueueing `ReactionJob3`. https://tc39.es/ecma262/#sec-asyncgeneratorawaitreturn ``` AsyncGeneratorAwaitReturn ( generator ) 1. Let queue be generator.[[AsyncGeneratorQueue]]. 2. Assert: queue is not empty. 3. Let next be the first element of queue. 4. Let completion be Completion(next.[[Completion]]). 5. Assert: completion is a return completion. 6. Let promise be ? PromiseResolve(%Promise%, completion.[[Value]]). 7. Let fulfilledClosure be a new Abstract Closure with parameters (value) that captures generator and performs the following steps when called: ... 8. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 1, "", « »). ... 11. Perform PerformPromiseThen(promise, onFulfilled, onRejected). 12. Return unused. ``` `Promise2` and `Promise3` are both resolved with `Value2`, and `ReactionJob2` and `ReactionJob3` will be called with the same value, comes from `Value2`. If `Value2` is a promise from the same realm, or if it's not an object, those reaction jobs will receive `Value2` itself. Otherwise, `Value2`'s `then` property is accessed at the `Promise Resolve Functions` step 9, and thenable job will call the `then` function. These operations are duplicated because of the duplicate `AsyncGeneratorAwaitReturn` calls. https://tc39.es/ecma262/#sec-promise-resolve-functions ``` Promise Resolve Functions ... 8. If resolution is not an Object, then a. Perform FulfillPromise(promise, resolution). b. Return undefined. 9. Let then be Completion(Get(resolution, "then")). 10. If then is an abrupt completion, then a. Perform RejectPromise(promise, then.[[Value]]). b. Return undefined. 11. Let thenAction be then.[[Value]]. 12. If IsCallable(thenAction) is false, then a. Perform FulfillPromise(promise, resolution). b. Return undefined. 13. Let thenJobCallback be HostMakeJobCallback(thenAction). 14. Let job be NewPromiseResolveThenableJob(promise, resolution, thenJobCallback). 15. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). ... ``` https://tc39.es/ecma262/#sec-promise-resolve ``` 27.2.4.7.1 PromiseResolve ( C, x ) The abstract operation PromiseResolve takes arguments C (a constructor) and x (an ECMAScript language value) and returns either a normal completion containing an ECMAScript language value or a throw completion. It returns a new promise resolved with x. It performs the following steps when called: 1. If IsPromise(x) is true, then a. Let xConstructor be ? Get(x, "constructor"). b. If SameValue(xConstructor, C) is true, return x. 2. Let promiseCapability be ? NewPromiseCapability(C). 3. Perform ? Call(promiseCapability.[[Resolve]], undefined, « x »). 4. Return promiseCapability.[[Promise]]. ``` https://tc39.es/ecma262/#sec-asyncgeneratorawaitreturn ``` AsyncGeneratorAwaitReturn ( generator ) ... 7. Let fulfilledClosure be a new Abstract Closure with parameters (value) that captures generator and performs the following steps when called: a. Set generator.[[AsyncGeneratorState]] to completed. b. Let result be NormalCompletion(value). c. Perform AsyncGeneratorCompleteStep(generator, result, true). d. Perform AsyncGeneratorDrainQueue(generator). e. Return undefined. ``` Then, with either the same `Value2`, or some value that `Value2.then(...)` produces, `ReactionJob2` and `ReactionJob3` are called, and `AsyncGeneratorCompleteStep` is called for each. Then, if `ReactionJob2` is called first, it dequeues `Request2` from `generator.[[AsyncGeneratorQueue]]`, and resolves it with `Value2` or the value `Value2.then(...)` produces. After that, `ReactionJob3` is called, and it tries to dequeue request, but the queue is already empty. The assertion at step 1 fails, and the step 2 doesn't work. https://tc39.es/ecma262/#sec-asyncgeneratorcompletestep ``` AsyncGeneratorCompleteStep ( generator, completion, done [ , realm ] ) 1. Assert: generator.[[AsyncGeneratorQueue]] is not empty. 2. Let next be the first element of generator.[[AsyncGeneratorQueue]]. 3. Remove the first element from generator.[[AsyncGeneratorQueue]]. 4. Let promiseCapability be next.[[Capability]]. 5. Let value be completion.[[Value]]. ... 7. Else, a. Assert: completion is a normal completion. b. If realm is present, then ... iii. Let iteratorResult be CreateIterResultObject(value, done). ... c. Else, i. Let iteratorResult be CreateIterResultObject(value, done). d. Perform ! Call(promiseCapability.[[Resolve]], undefined, « iteratorResult »). 8. Return unused. ``` In SpiderMonkey's case, the dequeue is performed by `AsyncGeneratorObject::dequeueRequest`. https://searchfox.org/mozilla-central/rev/46d0387f0b582f00a5722c20d4e6b8693793631b/js/src/vm/AsyncIteration.cpp#444-457 ```cpp [[nodiscard]] static bool AsyncGeneratorCompleteStepNormal( JSContext* cx, Handle<AsyncGeneratorObject*> generator, HandleValue value, bool done) { // Step 1. Let queue be generator.[[AsyncGeneratorQueue]]. // Step 2. Assert: queue is not empty. MOZ_ASSERT(!generator->isQueueEmpty()); // Step 3. Let next be the first element of queue. // Step 4. Remove the first element from queue. AsyncGeneratorRequest* next = AsyncGeneratorObject::dequeueRequest(cx, generator); if (!next) { return false; } ``` If there was at most one request in the queue, it means the queue is now empty, and the `Slot_QueueOrRequest` value is `NullValue`, it matches `generator->isSingleQueue()` case, and it returns `generator->singleQueueRequest()`. https://searchfox.org/mozilla-central/rev/46d0387f0b582f00a5722c20d4e6b8693793631b/js/src/vm/AsyncIteration.cpp#158-167 ```cpp AsyncGeneratorRequest* AsyncGeneratorObject::dequeueRequest( JSContext* cx, Handle<AsyncGeneratorObject*> generator) { if (generator->isSingleQueue()) { AsyncGeneratorRequest* request = generator->singleQueueRequest(); generator->clearSingleQueueRequest(); return request; } Rooted<ListObject*> queue(cx, generator->queue()); return &queue->popFirstAs<AsyncGeneratorRequest>(cx); ``` Here, it tries to treat it as `AsyncGeneratorRequest`. This also hits assertion failure, but on non-debug build, it results in returning `nullptr` on 32-bit, and `0x0004000000000000` on 64-bit, which is object pointer extracted from `NullValue`. This results in simple crash. https://searchfox.org/mozilla-central/rev/46d0387f0b582f00a5722c20d4e6b8693793631b/js/src/vm/AsyncIteration.h#430-434 ```cpp AsyncGeneratorRequest* singleQueueRequest() const { return &getFixedSlot(Slot_QueueOrRequest) .toObject() .as<AsyncGeneratorRequest>(); } ``` On the other hand, if there were 2 or more requests, `Slot_QueueOrRequest` contains a ListObject, and `popFirstAs` is called, and it can read random value (for empty case, or -1 items case), and results in type confusion. https://searchfox.org/mozilla-central/rev/46d0387f0b582f00a5722c20d4e6b8693793631b/js/src/vm/AsyncIteration.cpp#158-159,166-167 ```cpp AsyncGeneratorRequest* AsyncGeneratorObject::dequeueRequest( JSContext* cx, Handle<AsyncGeneratorObject*> generator) { ... Rooted<ListObject*> queue(cx, generator->queue()); return &queue->popFirstAs<AsyncGeneratorRequest>(cx); ``` https://searchfox.org/mozilla-central/rev/46d0387f0b582f00a5722c20d4e6b8693793631b/js/src/vm/List-inl.h#86-89 ```cpp template <class T> inline T& js::ListObject::popFirstAs(JSContext* cx) { return popFirst(cx).toObject().as<T>(); } ``` https://searchfox.org/mozilla-central/rev/46d0387f0b582f00a5722c20d4e6b8693793631b/js/src/vm/List-inl.h#57-70 ```cpp inline JS::Value js::ListObject::popFirst(JSContext* cx) { uint32_t len = length(); MOZ_ASSERT(len > 0); JS::Value entry = get(0); if (!tryShiftDenseElements(1)) { moveDenseElements(0, 1, len - 1); setDenseInitializedLength(len - 1); shrinkElements(cx, len - 1); } MOZ_ASSERT(length() == len - 1); return entry; } ``` Then, this is specific to ``%AsyncGeneratorPrototype%.return`, given `next` and `throw` doesn't change the `generator.[[AsyncGeneratorState]]` and doesn't enqueue, if it's completed. https://tc39.es/ecma262/#sec-asyncgenerator-prototype-next ``` %AsyncGeneratorPrototype%.next ( value ) ... 5. Let state be generator.[[AsyncGeneratorState]]. 6. If state is completed, then a. Let iteratorResult be CreateIterResultObject(undefined, true). b. Perform ! Call(promiseCapability.[[Resolve]], undefined, « iteratorResult »). c. Return promiseCapability.[[Promise]]. ... ``` https://tc39.es/ecma262/#sec-asyncgenerator-prototype-throw ``` %AsyncGeneratorPrototype%.throw ( exception ) ... 5. Let state be generator.[[AsyncGeneratorState]]. ... 7. If state is completed, then a. Perform ! Call(promiseCapability.[[Reject]], undefined, « exception »). b. Return promiseCapability.[[Promise]]. ... ```