CVE-2026-102321
Overview
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
TranslatedValueentries 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-
kSuccessresult means the emitted code / deopt tables are incomplete or inconsistent.
Root Cause Analysis
Verify before building a PoC
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.
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
- 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)
- Produce inconsistent deopt metadata
BranchEliminationReducer::ReduceGotoclones a block containing aDeoptimizeIfOp, or code generation ends non-kSuccessyetGenerateWasmDeoptimizationDatastill serializes entries, so the deopt table’s counts/kinds no longer match the frame layout. - Trigger deoptimization
Execution hits the deopt point, invoking
Deoptimizer::DoComputeWasmLiftoffFrame, which iterates Liftoff slots and writes eachTranslatedValueby itsTranslatedValue::Kind. - Type-confused frame write
Because the pre-patch consumer only
DCHECKed slot/value-kind agreement, akTaggedvalue can be written where the slot iskI32(or ani32/i64/f64/s128mismatch), 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
CHECK/SBXCHECK crash. No exploited-in-the-wild status is stated in the advisory.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/compiler/backend/code-generator.cc |
modified | |
forsrc/compiler/backend/code-generator.cc |
modified |
Files Changed
src/wasm/wasm-deopt-data.ccsrc/compiler/pipeline.ccsrc/wasm/wasm-deopt-data.hsrc/compiler/turboshaft/branch-elimination-reducer.hsrc/compiler/backend/code-generator.ccsrc/deoptimizer/deoptimizer.ccsrc/compiler/backend/code-generator.h
Audit Directions
- `DCHECK` guarding attacker-influenced invariantsSearch deopt, code-generation, and serialization paths for
DCHECK/DCHECK_EQthat enforce count/index/kind agreement on data derived from optimized compilation, since these vanish in release builds; promote toCHECK/SBXCHECKwhere the input can be adversarial. - Value-kind vs. slot-kind agreementReview every place that writes a
TranslatedValueinto 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 blocksAudit 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.
Patch
From c4a176650083c701534cd6daa3ac0411351db83d Mon Sep 17 00:00:00 2001
From: Matthias Liedtke <mliedtke@chromium.org>
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 <mliedtke@chromium.org>
Reviewed-by: Jakob Kummerow <jkummerow@chromium.org>
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>(