CVE-2025-9132
Overview
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
Patch
From 848d7f4122826f4d26ee09404fe57ac9b9295b00 Mon Sep 17 00:00:00 2001 From: Rezvan Mahdavi Hezaveh <[email protected]> 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 <[email protected]> Reviewed-by: Leszek Swirski <[email protected]> Commit-Queue: Rezvan Mahdavi Hezaveh <[email protected]> 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