High patch-now ~ plausible CVSS 8.8 chrome Type Confusion 🔧 Commit mapped 🧬 Broken deopt/OSR bookkeeping and unsound escape analysis

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType confusion in V8
DescriptionType confusion in V8
ComponentV8
Sub-componentWASM
Bug ClassType Confusion
Action tierpatch-now — corruption primitive, web-reachable in default config (plausible) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIPlausible
Reviewer notes: Overstates cloning as 'corrupting deopt correspondence'; commit says cloning causes metadata overhead, likely hitting kTooManyDeoptimizationBailouts whose failure was ignored. Kind CHECKs are consumer hardening, not root cause. Wasm-deopt default-on assumed, not shown.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker565328105
Fix commitc4a176650083 (v8/v8) +30/-5
CWECWE-843 (Type confusion)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedTaisic Yun (@taisic_) of Theori, with Xint
Reported2026-09-23 (per vendor advisory)
Fix landed2026-09-25 (public git)
Disclosed2026-09-29
Patch gap4 days — fix public in source before the advisory

Background

Wasm deoptimization (deopt)
the mechanism that transfers execution from optimized (Turboshaft) Wasm code back to the Liftoff baseline frame, reconstructing register/stack values from TranslatedValue entries and per-slot Liftoff kind metadata.
`DeoptimizeIfOp` / `BranchEliminationReducer::ReduceGoto`
a Turboshaft conditional deopt operation, and the reducer optimization that may clone merge blocks when eliminating branches.
`WasmDeoptData` / `WasmDeoptView::GetDeoptEntry`
the serialized deopt metadata blob and the accessor that indexes into it by deopt_index.
`CodeGenerator::result_` (`kSuccess` / `kTooManyDeoptimizationBailouts`)
the code-generation outcome enum; a non-kSuccess result means the emitted code / deopt tables are incomplete or inconsistent.

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
What the commit does not establish: Overstates cloning as 'corrupting deopt correspondence'; commit says cloning causes metadata overhead, likely hitting kTooManyDeoptimizationBailouts whose failure was ignored. Kind CHECKs are consumer hardening, not root cause. Wasm-deopt default-on assumed, not shown.

Two related defects allowed inconsistent Wasm deopt metadata to be produced and then consumed without validation. First, BranchEliminationReducer::ReduceGoto could clone a merge block that contained a DeoptimizeIfOp, duplicating deopt points and corrupting the correspondence between deopt exits and their metadata. Second, Pipeline::GenerateWasmCode and CodeGenerator::GenerateWasmDeoptimizationData proceeded to serialize deopt data even when code generation did not finish with kSuccess (e.g. kTooManyDeoptimizationBailouts), so the deopt entry count and per-slot value kinds could disagree with what Deoptimizer::DoComputeWasmLiftoffFrame later assumed. When deopt then ran, the deoptimizer wrote each TranslatedValue into a Liftoff slot without confirming the slot’s declared wasm::ValueKind matched the value’s TranslatedValue::Kind, permitting a type-confused write (e.g. a tagged pointer into an i32 slot or vice versa).

The fix prevents cloning blocks containing DeoptimizeIfOp, promotes the code-generation-success and deopt-count/index invariants from DCHECK to release CHECK/CHECK_EQ/CHECK_LT/SBXCHECK, and adds per-slot CHECK_EQ(liftoff_iter->kind(), ...) assertions so any kind mismatch aborts instead of performing a confused write.

Key insight
The core mistake was consuming Wasm deopt metadata whose integrity was only enforced in debug builds (DCHECK) while the optimizer could actually produce inconsistent metadata (cloned deopt points, entries generated after a non-success codegen), so a mismatched value/slot kind became an exploitable type confusion; the fix both stops producing the inconsistency and hardens the consumer with release-mode checks that verify each Liftoff slot’s ValueKind against the TranslatedValue::Kind before writing.

Attack Path

  1. Craft Wasm that stresses deopt generation An attacker supplies a Wasm module whose optimized compilation creates conditional deopt points in mergeable blocks and/or exceeds the deopt bailout budget. (inferred)
  2. Produce inconsistent deopt metadata BranchEliminationReducer::ReduceGoto clones a block containing a DeoptimizeIfOp, or code generation ends non-kSuccess yet GenerateWasmDeoptimizationData still serializes entries, so the deopt table’s counts/kinds no longer match the frame layout.
  3. Trigger deoptimization Execution hits the deopt point, invoking Deoptimizer::DoComputeWasmLiftoffFrame, which iterates Liftoff slots and writes each TranslatedValue by its TranslatedValue::Kind.
  4. Type-confused frame write Because the pre-patch consumer only DCHECKed slot/value-kind agreement, a kTagged value can be written where the slot is kI32 (or an i32/i64/f64/s128 mismatch), confusing a pointer with a scalar in the reconstructed frame. (inferred as the exploitation primitive; the patch demonstrates the missing kind checks but not a concrete read/write.)

Impact Assessment

An attacker running attacker-controlled WebAssembly in the renderer can drive Wasm deoptimization with mismatched value/slot kinds to achieve a type confusion between tagged pointers and scalars in a reconstructed Liftoff frame, the class the advisory reports as type confusion in V8. Successful exploitation would require winning the codegen/optimizer conditions above and is confined to the renderer process; escalation to a controlled read/write or RCE is (inferred) and not demonstrated by the patch, which converts the failure into a safe release-mode CHECK/SBXCHECK crash. No exploited-in-the-wild status is stated in the advisory.

Changed Functions

FunctionChangeNotes
if
src/compiler/backend/code-generator.cc
modified
for
src/compiler/backend/code-generator.cc
modified

Files Changed

  • src/wasm/wasm-deopt-data.cc
  • src/compiler/pipeline.cc
  • src/wasm/wasm-deopt-data.h
  • src/compiler/turboshaft/branch-elimination-reducer.h
  • src/compiler/backend/code-generator.cc
  • src/deoptimizer/deoptimizer.cc
  • src/compiler/backend/code-generator.h

Audit Directions

  • `DCHECK` guarding attacker-influenced invariants
    Search deopt, code-generation, and serialization paths for DCHECK/DCHECK_EQ that enforce count/index/kind agreement on data derived from optimized compilation, since these vanish in release builds; promote to CHECK/SBXCHECK where the input can be adversarial.
  • Value-kind vs. slot-kind agreement
    Review every place that writes a TranslatedValue into a typed frame slot (Liftoff and other frame reconstructors) to confirm the value’s kind is validated against the slot’s declared kind before the write.
  • Optimizer transforms over deopt-bearing blocks
    Audit Turboshaft/other reducers that clone, merge, or duplicate blocks to ensure they refuse blocks containing deopt operations whose metadata must stay 1:1 with exits.
From c4a176650083c701534cd6daa3ac0411351db83d Mon Sep 17 00:00:00 2001
From: Matthias Liedtke <[email protected]>
Date: Fri, 25 Sep 2026 11:55:44 +0200
Subject: [PATCH] [wasm][deopt] Fix Wasm deopt data checks & prevent DeoptimizeIf cloning

- Avoid cloning merge blocks containing DeoptimizeIfOp in Turboshaft's
  BranchEliminationReducer::ReduceGoto.
- Check CodeGenerator::kSuccess in Pipeline::GenerateWasmCode and
  CodeGenerator::GenerateWasmDeoptimizationData, and harden
  WasmDeoptDataProcessor::Serialize, WasmDeoptView::GetDeoptEntry, and
  Deoptimizer::DoComputeWasmLiftoffFrame with release CHECKs.

Fixed: 565328105
Change-Id: I49f5eb34609fc23bf47a79ec6e7a831f544ea8aa
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8460985
Commit-Queue: Matthias Liedtke <[email protected]>
Reviewed-by: Jakob Kummerow <[email protected]>
Cr-Commit-Position: refs/heads/main@{#110094}
---
diff --git a/src/compiler/backend/code-generator.cc b/src/compiler/backend/code-generator.cc
index ed3c705..28faaf1 100644
--- a/src/compiler/backend/code-generator.cc
+++ b/src/compiler/backend/code-generator.cc
@@ -1102,6 +1102,7 @@
 
 #if V8_ENABLE_WEBASSEMBLY
 base::OwnedVector<uint8_t> CodeGenerator::GenerateWasmDeoptimizationData() {
+  CHECK_EQ(result_, kSuccess);
   int deopt_count = static_cast<int>(deoptimization_exits_.size());
   if (deopt_count == 0) {
     return {};
@@ -1117,7 +1118,7 @@
   for (int i = 0; i < deopt_count; i++) {
     const DeoptimizationExit* deoptimization_exit = deoptimization_exits_[i];
     CHECK_NOT_NULL(deoptimization_exit);
-    DCHECK_EQ(i, deoptimization_exit->deoptimization_id());
+    CHECK_EQ(i, deoptimization_exit->deoptimization_id());
     deopt_entries[i] = {deoptimization_exit->bailout_id(),
                         deoptimization_exit->translation_id()};
   }
diff --git a/src/compiler/backend/code-generator.h b/src/compiler/backend/code-generator.h
index 41006f5..1b5c316 100644
--- a/src/compiler/backend/code-generator.h
+++ b/src/compiler/backend/code-generator.h
@@ -159,6 +159,9 @@
 
   CodeKind code_kind() const { return info_->code_kind(); }
 
+  enum CodeGenResult { kSuccess, kTooManyDeoptimizationBailouts };
+  CodeGenResult result() const { return result_; }
+
  private:
   GapResolver* resolver() { return &resolver_; }
   SafepointTableBuilder* safepoints() { return &safepoints_; }
@@ -181,8 +184,6 @@
   bool IsMaterializableFromRoot(Handle<HeapObject> object,
                                 RootIndex* index_return);
 
-  enum CodeGenResult { kSuccess, kTooManyDeoptimizationBailouts };
-
   // Assemble instructions for the specified block.
   CodeGenResult AssembleBlock(const InstructionBlock* block);
 
diff --git a/src/compiler/pipeline.cc b/src/compiler/pipeline.cc
index 38406b8..27f9007 100644
--- a/src/compiler/pipeline.cc
+++ b/src/compiler/pipeline.cc
@@ -3106,6 +3106,7 @@
                                         &pipeline, data.osr_helper_ptr()));
 
   CodeGenerator* code_generator = turboshaft_data.code_generator();
+  CHECK_EQ(code_generator->result(), CodeGenerator::kSuccess);
 
   wasm::WasmCompilationResult result;
   code_generator->masm()->GetCode(
diff --git a/src/compiler/turboshaft/branch-elimination-reducer.h b/src/compiler/turboshaft/branch-elimination-reducer.h
index d5fbf34..70ae78a 100644
--- a/src/compiler/turboshaft/branch-elimination-reducer.h
+++ b/src/compiler/turboshaft/branch-elimination-reducer.h
@@ -376,6 +376,15 @@
       goto no_change;
     }
 
+    // Do not clone blocks containing deoptimization points. These may have a
+    // significant metadata overhead.
+    for (const Operation& op :
+         __ input_graph().operations(*destination_origin)) {
+      if (op.Is<DeoptimizeIfOp>()) {
+        goto no_change;
+      }
+    }
+
     if (const BranchOp* branch = last_op.template TryCast<BranchOp>()) {
       V<Word32> condition =
           __ template MapToNewGraph<true>(branch->condition());
diff --git a/src/deoptimizer/deoptimizer.cc b/src/deoptimizer/deoptimizer.cc
index 685ba1f..95bde97 100644
--- a/src/deoptimizer/deoptimizer.cc
+++ b/src/deoptimizer/deoptimizer.cc
@@ -1180,13 +1180,17 @@
           intptr_t reg_value = kZapValue;
           switch (value.kind()) {
             case TranslatedValue::Kind::kInt32:
+              CHECK_EQ(liftoff_iter->kind(), wasm::ValueKind::kI32);
               // Ensure that the upper half is zeroed out.
               reg_value = static_cast<uint32_t>(value.int32_value());
               break;
             case TranslatedValue::Kind::kTagged:
+              CHECK(liftoff_iter->kind() == wasm::ValueKind::kRef ||
+                    liftoff_iter->kind() == wasm::ValueKind::kRefNull);
               reg_value = value.raw_literal().ptr();
               break;
             case TranslatedValue::Kind::kInt64:
+              CHECK_EQ(liftoff_iter->kind(), wasm::ValueKind::kI64);
               reg_value = value.int64_value();
               break;
             default:
@@ -1196,10 +1200,12 @@
         } else if (liftoff_iter->is_fp_reg()) {
           switch (value.kind()) {
             case TranslatedValue::Kind::kDouble:
+              CHECK_EQ(liftoff_iter->kind(), wasm::ValueKind::kF64);
               output_frame->SetDoubleRegister(liftoff_iter->reg().fp().code(),
                                               value.double_value());
               break;
             case TranslatedValue::Kind::kFloat:
+              CHECK_EQ(liftoff_iter->kind(), wasm::ValueKind::kF32);
               // Liftoff doesn't have a concept of floating point registers.
               // This is an important distinction as e.g. on arm s1 and d1 are
               // two completely distinct registers.
@@ -1210,6 +1216,7 @@
                   Float64::FromBits(value.float_value().get_bits()));
               break;
             case TranslatedValue::Kind::kSimd128:
+              CHECK_EQ(liftoff_iter->kind(), wasm::ValueKind::kS128);
               output_frame->SetSimd128Register(liftoff_iter->reg().fp().code(),
                                                value.simd_value());
               break;
@@ -1217,13 +1224,15 @@
               UNIMPLEMENTED();
           }
         } else if (liftoff_iter->is_simd128_reg()) {
-          DCHECK_EQ(TranslatedValue::Kind::kSimd128, value.kind());
+          CHECK_EQ(liftoff_iter->kind(), wasm::ValueKind::kS128);
+          CHECK_EQ(TranslatedValue::Kind::kSimd128, value.kind());
           output_frame->SetSimd128Register(liftoff_iter->reg().simd128().code(),
                                            value.simd_value());
         } else if (!Is64() && liftoff_iter->is_gp_reg_pair()) {
           intptr_t reg_value = kZapValue;
           switch (value.kind()) {
             case TranslatedValue::Kind::kInt32:
+              CHECK_EQ(liftoff_iter->kind(), wasm::ValueKind::kI64);
               // Ensure that the upper half is zeroed out.
               reg_value = static_cast<uint32_t>(value.int32_value());
               break;
@@ -1240,6 +1249,7 @@
           if (int64_lowering_is_low) skip_increase_liftoff_iter = true;
           int64_lowering_is_low = !int64_lowering_is_low;
         } else if (!Is64() && liftoff_iter->is_fp_reg_pair()) {
+          CHECK_EQ(liftoff_iter->kind(), wasm::ValueKind::kS128);
           CHECK_EQ(value.kind(), TranslatedValue::Kind::kSimd128);
           Simd128 simd_value = value.simd_value();
           Address val_ptr = reinterpret_cast<Address>(&simd_value);
@@ -1298,6 +1308,7 @@
             }
             break;
           case wasm::ValueKind::kS128: {
+            CHECK_EQ(value.kind(), TranslatedValue::Kind::kSimd128);
             Simd128::int64x2 values = value.simd_value().to_i64x2();
             const int offset = base_offset - liftoff_iter->offset();
             output_frame->SetLiftoffFrameSlot64(offset, values[0]);
diff --git a/src/wasm/wasm-deopt-data.cc b/src/wasm/wasm-deopt-data.cc
index 506c96f..87ab2a0 100644
--- a/src/wasm/wasm-deopt-data.cc
+++ b/src/wasm/wasm-deopt-data.cc
@@ -29,6 +29,8 @@
     base::Vector<const uint8_t> translation_array,
     base::Vector<wasm::WasmDeoptEntry> deopt_entries,
     const ZoneDeque<DeoptimizationLiteral>& deopt_literals) {
+  CHECK_GE(eager_deopt_count, 0);
+  CHECK_EQ(deopt_entries.size(), static_cast<size_t>(eager_deopt_count));
   wasm::WasmDeoptData data;
   data.entry_count = eager_deopt_count;
   data.deopt_exit_start_offset = deopt_exit_start_offset;
diff --git a/src/wasm/wasm-deopt-data.h b/src/wasm/wasm-deopt-data.h
index eaad582..d0f2909 100644
--- a/src/wasm/wasm-deopt-data.h
+++ b/src/wasm/wasm-deopt-data.h
@@ -69,7 +69,7 @@
 
   WasmDeoptEntry GetDeoptEntry(uint32_t deopt_index) const {
     DCHECK(HasDeoptData());
-    DCHECK(deopt_index < base_data_.entry_count);
+    CHECK_LT(deopt_index, base_data_.entry_count);
     const uint8_t* begin = deopt_data_.begin() + sizeof(base_data_) +
                            base_data_.translation_array_size;
     return base::ReadUnalignedValue<WasmDeoptEntry>(reinterpret_cast<Address>(
Loading diff…