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 <[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>(