CVE-2025-9132
Overview
Background
- `await using` declaration
- an explicit-resource-management form that registers a value’s
[Symbol.asyncDispose]to be awaited and disposed when its scope exits. - C-style `for` per-iteration bindings
- for
for (let/const x = …; …; …), V8 creates fresh per-iteration copies of the loop variables (theinner_vars/tempsmachinery) so each iteration sees its own binding. - `VariableMode`
- the enum tagging how a binding behaves (
kLet,kConst,kAwaitUsing, …);kAwaitUsingmarks a binding as an async-disposable resource requiring an await on scope exit. - `DeclareBoundVariable`
- the parser helper that declares a bound name in a scope with a given
VariableMode, here used to build the per-iteration copies.
Root Cause Analysis
Verify before building a PoC
What the commit does not establish: Semantic description (per-iteration copies declared kAwaitUsing, fixed to kConst) matches the diff, but the analysis never explains how this yields the advisory's out-of-bounds write; the diff shows only parser mode and tick-count changes.
When desugaring a c-style for loop with let/const-style bindings, the loop over for_info.bound_names called DeclareBoundVariable(...) passing for_info.parsing_result.descriptor.mode verbatim for each per-iteration copy. For an await using loop head, that mode is VariableMode::kAwaitUsing, so every per-iteration copy was itself declared as an async-disposable resource. Per the spec (and the commit message), a c-style for with await using must perform exactly one await/disposal when the loop exits — not one per iteration — so tagging each per-iteration copy kAwaitUsing caused the loop to register/await disposal repeatedly, violating the intended single-disposal invariant and the count of disposable resources tracked for the loop.
The fix maps kAwaitUsing to VariableMode::kConst when creating the per-iteration copies (leaving other modes unchanged), so the copies are plain constants and only the original outer declaration remains an async-disposable resource. The regression test asserts the exact interleaving of ticks (a single asyncDispose after the loop), confirming the corrected disposal count.
kAwaitUsing mode instead of a plain binding, turning each iteration’s copy into an extra async-disposable resource; the fix substitutes VariableMode::kConst for those copies so disposal happens once, as the spec requires.Attack Path
- Author an `await using` c-style loop
Write an async function containing
for (await using x = {…, [Symbol.asyncDispose](){…}}; x.value < N; x.value++) {…}, as in the regression test. - Reach the vulnerable desugaring
Parsing the loop head calls
DeclareBoundVariablefor each bound name withdescriptor.mode == kAwaitUsing, marking every per-iteration copy as an async-disposable resource. - Mis-track disposable resources The loop then manages one disposable-resource entry per iteration rather than a single one, so the number of tracked/awaited resources diverges from what the generated code and disposable stack were sized for.
- Corrupt memory This mismatch in the disposable-resource bookkeeping produces the out-of-bounds write the advisory reports (inferred as to the exact buffer/offset, which the diff does not expose).
Impact Assessment
await using inside a c-style for loop can cause V8 to mishandle per-iteration disposable bindings, which the advisory classifies as an out-of-bounds write in V8, in the renderer process. The only precondition is script execution with the explicit-resource-management feature available; the advisory does not report exploitation in the wild, and escalation from the OOB write to a full read/write primitive or code execution is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
fortest/mjsunit/harmony/regress/regress-436181695.js |
modified |
Files Changed
src/parsing/parser.cctest/mjsunit/harmony/regress/regress-436181695.js
Audit Directions
- Mode propagation into per-iteration copiesReview every place that reuses
descriptor.modewhen cloning loop/binding variables (for-of,for-in, c-stylefor) to ensure special modes likekAwaitUsing/kUsingare not duplicated across copies. - Disposable-resource countingAudit explicit-resource-management desugaring for consistency between the number of registered disposables and the slots/stack the generated code allocates, across all loop forms and early-exit paths.
- New `VariableMode` handlingTrace all consumers of recently added modes (
kUsing,kAwaitUsing) through parser, scope, and bytecode to find spots that assume only classiclet/const/varsemantics.
Patch
From 848d7f4122826f4d26ee09404fe57ac9b9295b00 Mon Sep 17 00:00:00 2001
From: Rezvan Mahdavi Hezaveh <rezvan@chromium.org>
Date: Mon, 11 Aug 2025 19:01:45 +0000
Subject: [PATCH] [explicit-resource-management] Fix parsing in c-style for
Based on spec text, we should have only one await when exiting
c-style for loop with `await using` declaration and not one
await per iteration.
Bug: 436181695
Change-Id: I853219ff6ad5f98c0619de891bb4ce916e3eb999
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6830541
Reviewed-by: Olivier Flückiger <olivf@chromium.org>
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
Commit-Queue: Rezvan Mahdavi Hezaveh <rezvan@chromium.org>
Cr-Commit-Position: refs/heads/main@{#101859}
---
diff --git a/src/parsing/parser.cc b/src/parsing/parser.cc
index 610eeb0..2789e34 100644
--- a/src/parsing/parser.cc
+++ b/src/parsing/parser.cc
@@ -2438,7 +2438,10 @@
// 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));
diff --git a/test/mjsunit/harmony/regress/regress-436181695.js b/test/mjsunit/harmony/regress/regress-436181695.js
new file mode 100644
index 0000000..e1ff927
--- /dev/null
+++ b/test/mjsunit/harmony/regress/regress-436181695.js
@@ -0,0 +1,34 @@
+// Copyright 2025 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+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`);
+ } // One tick is expected after calling asyncDispose to allow it to be
+ // asynchronous. It will be called after exiting the for-loop.
+ };
+ x.value < 44; x.value++) {
+ // These pushes are expected to be synchronous.
+ v.push(x.value);
+ }
+ v.push(`afterForLoop`);
+}
+
+async function RunTest() {
+ await TestCStyleForCountTicks();
+ assertArrayEquals([0, 42, 43, `asyncDispose`, 1, `afterForLoop`, 2], v);
+}
+
+RunTest();
Regression Test / PoC
diff --git a/test/mjsunit/harmony/regress/regress-436181695.js b/test/mjsunit/harmony/regress/regress-436181695.js
new file mode 100644
index 0000000..e1ff927
--- /dev/null
+++ b/test/mjsunit/harmony/regress/regress-436181695.js
@@ -0,0 +1,34 @@
+// Copyright 2025 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+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`);
+ } // One tick is expected after calling asyncDispose to allow it to be
+ // asynchronous. It will be called after exiting the for-loop.
+ };
+ x.value < 44; x.value++) {
+ // These pushes are expected to be synchronous.
+ v.push(x.value);
+ }
+ v.push(`afterForLoop`);
+}
+
+async function RunTest() {
+ await TestCStyleForCountTicks();
+ assertArrayEquals([0, 42, 43, `asyncDispose`, 1, `afterForLoop`, 2], v);
+}
+
+RunTest();
Original Bug Report
V8: Bytecode corruption due to invalid parsing of ‘await using’ in c-style loops
We are tracking this issue with the public ID BIGSLEEP-436210783. Please use this identifier for reference in any future communication.
Vulnerability Details
The below sample triggers a bug in the JavaScript parser, causing invalid bytecode to be emitted.
When parsing async and/or generator functions, the resulting bytecode will make use of suspend points to indicate the different positions where execution might restart from (e.g. after a yield). These are effectively implemented as a big switch statement at the start of the function switching on the generator state and then jumping to the respective position. During parsing, the number of these suspend points is computed via FunctionState::AddSuspend and then during bytecode compilation the suspend points are emitted via BytecodeGenerator::BuildSuspendPoint. What appears to happen in the sample below is a mismatch between these two, where the parser only counts N suspend points but the bytecode compiler generates N+1 such points. Since the jump tables used for switch statements are implemented as a range of Smi values inside a BytecodeArray’s constant pool, this effectively leads to an out-of-bounds write inside the constant pool (manifesting as a DCHECK failure: Debug check failed: case_value < case_value_base_ + size() (2 vs. 2)). It appears that the switch table from which the OOB write happens is always followed by a second switch table. As such, the OOB write would corrupt the first entry of the next switch table. Since the Smi values represent relative offsets into the BytecodeArray to which execution would jump, this will often cause a jump into the middle of another bytecode. In the sample below, this will lead to the execution of an invalid bytecode opcode, resulting in a nullptr dereference. However, by modifying the code of the function it is possible to influence the bytecode instruction that will be jumped into. With that, it should be possible to execute unexpected-but-valid bytecode opcodes to cause further memory corruption.
The bug appears to have been introduced by https://crrev.com/c/6594471 which allowed using await using inside c-style for loops.
Affected Version(s)
Current HEAD (f697fbd47c40208a31984bf138587a579679c031) and current Stable.
Reproduction
Test Case
The following testcase causes a DCHECK failure in debug builds and a segmentation fault in release builds as described above.
async function* bug() {
for (await using x = { [Symbol.asyncDispose]() {} }; 1; ) {}
}
async function run() {
for await (const x of bug()) {}
}
run();
Build Instructions
Follow the instructions at https://v8.dev/docs/build. The crash was verified on a debug build:
gm.py x64.debug
Command
./out/x64.debug/d8 crash.js
Reporter Credit
Google Big Sleep
Disclosure Policy
This bug is subject to a 90-day disclosure deadline. If a fix for this issue is made available to users before the end of the 90-day deadline, this bug report will become public 30 days after the fix was made available. Otherwise, this bug report will become public at the deadline. The scheduled deadline is 2025-11-02.
For more information, visit https://goo.gle/bigsleep