
既然这是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 源码时会记录 await、await using、yield 等关键字的个数。例如 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