【漏洞分析】CVE-2025-9132 "Await Using" Can't Wait
既然这是 2025-8-22 11:0:0 Author: keenlab.tencent.com(查看原文) 阅读量:19 收藏



既然这是Project Zero的BigSleep发现的第一个V8漏洞,Buff这么多,那就很难不来一窥究竟了。

背景

8月19日的 Google Chrome 更新修复了一个由 Google Big Sleep 发现的漏洞。

Chrome Releases: Stable Channel Update for Desktop

[436181695] High CVE-2025-9132: Out of bounds write in V8. Reported by Google Big Sleep on 2025-08-04

通过分析补丁,我们成功实现了 CVE-2025-9132 的利用。以下所有分析和利用都基于 v8 13.9.205.19,commit 505ec917b67c535519bebec58c62a34f145dd49f,即 v8 13.9 分支中漏洞修复前的 commit。

CVE-2025-9132 的补丁和补丁中附带的 PoC 如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16




@@ -2408,7 +2408,10 @@ Statement* Parser::DesugarLexicalBindingsInForStatement(
// make statement: let/const x = temp_x.
for (int i = 0; i < for_info.bound_names.length(); i++) {
VariableProxy* proxy = DeclareBoundVariable(
- for_info.bound_names[i], for_info.parsing_result.descriptor.mode,
+ for_info.bound_names[i],
+ for_info.parsing_result.descriptor.mode == VariableMode::kAwaitUsing
+ ? VariableMode::kConst
+ : for_info.parsing_result.descriptor.mode,
kNoSourcePosition);
inner_vars.Add(proxy->var());
VariableProxy* temp_proxy = factory()->NewVariableProxy(temps.at(i));
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




var v = [];

(async () => {
for (let i = 0; i < 6; ++i) {
v.push(i);
await 0;
}
})();

async function TestCStyleForCountTicks() {
for (await using x = {
value: 42,
[Symbol.asyncDispose]() {
v.push(`asyncDispose`);
}

};
x.value < 44; x.value++) {

v.push(x.value);
}
v.push(`afterForLoop`);
}

async function RunTest() {
await TestCStyleForCountTicks();
assertArrayEquals([0, 42, 43, `asyncDispose`, 1, `afterForLoop`, 2], v);
}

RunTest();

使用 debug 版本的 d8 运行 PoC 会在 BytecodeArrayWriter::BindJumpTableEntry 中触发 DCHECK [1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void BytecodeArrayWriter::BindJumpTableEntry(BytecodeJumpTable* jump_table,
int case_value) {
DCHECK(!jump_table->is_bound(case_value));

size_t current_offset = bytecodes()->size();
size_t relative_jump = current_offset - jump_table->switch_bytecode_offset();

constant_array_builder()->SetJumpTableSmi(
jump_table->ConstantPoolEntryFor(case_value),
Smi::FromInt(static_cast<int>(relative_jump)));
jump_table->mark_bound(case_value);

StartBasicBlock();
}

分析

补丁和 PoC 都显示漏洞与 await using 语法有关。await using 是 JavaScript 中的新特性。使用 await using 声明的变量离开其作用域时,它的 [asyncDispose] 会被异步调用。

The await using declaration declares block-scoped local variables that are asynchronously disposed.

1
2
3
4
5
6
7
8
9
10
async function foo() {
{
await using x = {
[Symbol.asyncDispose]() {
console.log("asyncDispose");
}
};

}
}

在 v8 中,如果 async function 中有 await 关键字,那么函数的开头会是一个 SwitchOnGeneratorState 字节码,每个 await 会产生一对 SuspendGenerator/ResumeGenerator 字节码。SuspendGenerator 会将函数的当前状态保存到 JSGeneratorObject 对象中,然后退出。当 await 完成,函数会重新从开头的 SwitchOnGeneratorState 处开始执行,SwitchOnGeneratorState 会根据 JSGeneratorObject 从对应的 ResumeGenerator 处恢复执行,ResumeGenerator 会从 JSGeneratorObject 导入函数状态。

SwitchOnGeneratorState 有一个 JumpTable,用于选择从哪一个 ResumeGenerator 执行。v8 先从源码产生 AST,再从 AST 生成字节码。为了确定 JumpTable 的大小,v8 在 parse 源码时会记录 awaitawait usingyield 等关键字的个数。例如 ParserBase<Impl>::ParseVariableDeclarations 第一次在一个作用域中遇到 await using 时会调用 AddSuspend 来增加计数。

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

template <typename Impl>
void ParserBase<Impl>::ParseVariableDeclarations(
VariableDeclarationContext var_context,
DeclarationParsingResult* parsing_result,
ZonePtrList<const AstRawString>* names) {




DCHECK_NOT_NULL(parsing_result);
parsing_result->descriptor.kind = NORMAL_VARIABLE;
parsing_result->descriptor.declaration_pos = peek_position();
parsing_result->descriptor.initialization_pos = peek_position();

Scope* target_scope = scope();

switch (peek()) {

case Token::kAwait:


Consume(Token::kAwait);
DCHECK(v8_flags.js_explicit_resource_management);
DCHECK_NE(var_context, kStatement);
DCHECK(is_using_allowed());
DCHECK(is_await_allowed());
Consume(Token::kUsing);
DCHECK(!scanner()->HasLineTerminatorBeforeNext());
DCHECK(peek() != Token::kLeftBracket && peek() != Token::kLeftBrace);
impl()->CountUsage(v8::Isolate::kExplicitResourceManagement);
parsing_result->descriptor.mode = VariableMode::kAwaitUsing;
if (!target_scope->has_await_using_declaration()) {
function_state_->AddSuspend();
}
break;
default:
UNREACHABLE();
break;
}

生成字节码时,BytecodeGenerator 会先 constant_pool 中预留 info()->literal()->suspend_count() 个位置(constant_pool 是一个数组),作为 JumpTable。JumpTable 会在生成字节码的过程中逐个被填充成实际的跳转偏移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void BytecodeGenerator::BuildGeneratorPrologue() {
DCHECK_GT(info()->literal()->suspend_count(), 0);
generator_jump_table_ =
builder()->AllocateJumpTable(info()->literal()->suspend_count(), 0);



builder()->SwitchOnGeneratorState(generator_object(), generator_jump_table_);




}

BytecodeGenerator 有自己的数据成员 suspend_count_,初始值为 0。 在根据 AST 生成字节码时,每当需要生成一个 SuspendGenerator ,就会以 suspend_count_ 字段作为 suspend_id,然后将 suspend_count_ 自增。suspend_id 被用作 BytecodeArrayWriter::BindJumpTableEntry 函数的 case_value 参数,作为索引填充 JumpTable。正常来说,suspend_id 的范围是 [0, info()->literal()->suspend_count() - 1]

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



void BytecodeGenerator::BuildSuspendPoint(int position) {



if (builder()->RemainderOfBlockIsDead()) {
return;
}
const int suspend_id = suspend_count_++;

RegisterList registers = register_allocator()->AllLiveRegisters();



builder()->SetExpressionPosition(position);
builder()->SuspendGenerator(generator_object(), registers, suspend_id);


builder()->Bind(generator_jump_table_, suspend_id);



builder()->ResumeGenerator(generator_object(), registers);
}

CVE-2025-9132 的根本原因是 Parser::DesugarLexicalBindingsInForStatement 对 AST进行变换时引入了额外的 await using 变量。DesugarLexicalBindingsInForStatement 的注释解释了变换的方式。for (let/const x = i; cond; next) body 中的 let/const x = i 变成了 [1] [2] 两处 let/const x = ...。当这种变换应用到 PoC 中的代码时,源码中的一个 await using x = ... 变成了 AST 中的两处await using x = ... 。因此 BytecodeGenerator 在按照 AST 生成字节码并填充 JumpTable 时会出现 suspend_id >= info()->literal()->suspend_count() 的情况, 造成越界写。

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




































利用

Xion大佬说得对,漏洞容易利用,“大觉”(真是一个信达雅的翻译:D)也确实很有趣。大家也可以动动手了。

参考

[1] https://chromereleases.googleblog.com/2025/08/stable-channel-update-for-desktop_19.html

[2] https://chromium-review.googlesource.com/c/v8/v8/+/6853483

[3] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/await_using


文章来源: https://keenlab.tencent.com/zh/2025/08/22/2025-CVE-2025-9132/
如有侵权请联系:admin#unsafe.sh