High chrome Type Confusion 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in V8
DescriptionType Confusion in V8
ComponentV8
Bug ClassType Confusion
Tracker446124892
Fix commit48c355117929 (v8/v8) +79/-63
CISA KEVNot listed
CreditedSeunghyun Lee (@0x10n)
Disclosed2025-10-28

Files Changed

  • src/compiler/turboshaft/wasm-gc-typed-optimization-reducer.cc
  • src/compiler/turboshaft/wasm-gc-typed-optimization-reducer.h
  • src/wasm/canonical-types.cc
  • src/wasm/canonical-types.h
  • src/wasm/constant-expression-interface.cc
  • src/wasm/function-body-decoder-impl.h
From 48c3551179299151b044b2c161566465497c0407 Mon Sep 17 00:00:00 2001
From: Jakob Kummerow <[email protected]>
Date: Tue, 23 Sep 2025 15:39:41 +0200
Subject: [PATCH] [wasm-custom-desc] Fix subtyping

This updates the restrictions on subtyping of descriptors, in
anticipation of upcoming spec changes:
- descriptors and described types must have matching subtyping
- ref.func for imported functions returns inexact types (for
  now; long-term solution TBD)

And it fixes our implementation:
- `ProcessBranchOnTarget` erroneously still thought it could
  compute reachability based on static types when Custom
  Descriptors are in play
- `JSToWasmObject` was missing support for exact types
- `ref.get_desc` must not return an exact type when the actual
  type on the stack is exact, but a non-trivial subtype of the
  instruction's type immediate.

Bug: 403372470
Fixed: 446113731, 446113732, 446122633, 446124892, 446124893
Change-Id: Ic79ab08d906a2e21e66b76e9d96eebb4ebb7a8e5
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6973586
Commit-Queue: Jakob Kummerow <[email protected]>
Reviewed-by: Matthias Liedtke <[email protected]>
Cr-Commit-Position: refs/heads/main@{#102697}
---

diff --git a/src/compiler/turboshaft/wasm-gc-typed-optimization-reducer.cc b/src/compiler/turboshaft/wasm-gc-typed-optimization-reducer.cc
index c8d6693..f6905e6 100644
--- a/src/compiler/turboshaft/wasm-gc-typed-optimization-reducer.cc
+++ b/src/compiler/turboshaft/wasm-gc-typed-optimization-reducer.cc
@@ -445,7 +445,10 @@
       } else {
         DCHECK_EQ(branch.if_false, &target);
         if (wasm::IsSubtypeOf(GetResolvedType(check.object()), check.config.to,
-                              module_)) {
+                              module_) &&
+            // When checking for a particular custom descriptor, static types
+            // cannot predict the outcome.
+            !(IsCastToCustomDescriptor(module_, check.config))) {
           // The type check always succeeds, the target is impossible to be
           // reached.
           DCHECK_EQ(target.PredecessorCount(), 1);
diff --git a/src/compiler/turboshaft/wasm-gc-typed-optimization-reducer.h b/src/compiler/turboshaft/wasm-gc-typed-optimization-reducer.h
index a571fe4..2ba8fbe 100644
--- a/src/compiler/turboshaft/wasm-gc-typed-optimization-reducer.h
+++ b/src/compiler/turboshaft/wasm-gc-typed-optimization-reducer.h
@@ -18,6 +18,13 @@
 
 namespace v8::internal::compiler::turboshaft {
 
+inline bool IsCastToCustomDescriptor(const wasm::WasmModule* module,
+                                     WasmTypeCheckConfig config) {
+  return config.to.has_index() &&
+         module->type(config.to.ref_index()).has_descriptor() &&
+         config.exactness == compiler::kExactMatchOnly;
+}
+
 // The WasmGCTypedOptimizationReducer infers type information based on the input
 // graph and reduces type checks and casts based on that information.
 //
@@ -179,7 +186,7 @@
       bool to_nullable = cast_op.config.to.is_nullable();
       if (wasm::IsHeapSubtypeOf(type.heap_type(), cast_op.config.to.heap_type(),
                                 module_) &&
-          !IsCastToCustomDescriptor(cast_op.config)) {
+          !IsCastToCustomDescriptor(module_, cast_op.config)) {
         if (to_nullable || type.is_non_nullable()) {
           // The inferred type is already as specific as the cast target, the
           // cast is guaranteed to always succeed and can therefore be removed.
@@ -247,7 +254,7 @@
                                 type_check.config.to.heap_type(), module_) &&
           // When checking for a particular custom descriptor, static types
           // cannot guarantee success.
-          !(IsCastToCustomDescriptor(type_check.config))) {
+          !(IsCastToCustomDescriptor(module_, type_check.config))) {
         if (to_nullable || type.is_non_nullable()) {
           // The inferred type is guaranteed to be a subtype of the checked
           // type.
@@ -435,12 +442,6 @@
   }
 
  private:
-  bool IsCastToCustomDescriptor(WasmTypeCheckConfig config) {
-    return config.to.has_index() &&
-           module_->type(config.to.ref_index()).has_descriptor() &&
-           config.exactness == compiler::kExactMatchOnly;
-  }
-
   Graph& graph_ = __ modifiable_input_graph();
   const wasm::WasmModule* module_ = __ data() -> wasm_module();
   WasmGCTypeAnalyzer analyzer_{__ data(), graph_, __ phase_zone()};
diff --git a/src/wasm/canonical-types.cc b/src/wasm/canonical-types.cc
index 359ca5e..50120e0 100644
--- a/src/wasm/canonical-types.cc
+++ b/src/wasm/canonical-types.cc
@@ -243,15 +243,19 @@
 }
 
 bool TypeCanonicalizer::IsCanonicalSubtype(CanonicalTypeIndex sub_index,
-                                           CanonicalTypeIndex super_index) {
+                                           CanonicalValueType super_type) {
+  DCHECK(super_type.has_index());
   // Fast path without synchronization:
-  if (sub_index == super_index) return true;
+  if (sub_index == super_type.ref_index()) return true;
+  // If the supertype is exact, then only the equality case above is
+  // successful.
+  if (super_type.is_exact()) return false;
 
   // Multiple threads could try to register and access recursive groups
   // concurrently.
   // TODO(manoskouk): Investigate if we can improve this synchronization.
   base::MutexGuard mutex_guard(&mutex_);
-  return IsCanonicalSubtype_Locked(sub_index, super_index);
+  return IsCanonicalSubtype_Locked(sub_index, super_type.ref_index());
 }
 bool TypeCanonicalizer::IsCanonicalSubtype_Locked(
     CanonicalTypeIndex sub_index, CanonicalTypeIndex super_index) const {
@@ -266,16 +270,6 @@
   return false;
 }
 
-bool TypeCanonicalizer::IsCanonicalSubtype(ModuleTypeIndex sub_index,
-                                           ModuleTypeIndex super_index,
-                                           const WasmModule* sub_module,
-                                           const WasmModule* super_module) {
-  CanonicalTypeIndex canonical_super =
-      super_module->canonical_type_id(super_index);
-  CanonicalTypeIndex canonical_sub = sub_module->canonical_type_id(sub_index);
-  return IsCanonicalSubtype(canonical_sub, canonical_super);
-}
-
 bool TypeCanonicalizer::IsHeapSubtype(CanonicalTypeIndex sub,
                                       CanonicalTypeIndex super) const {
   DCHECK_NE(sub, super);
diff --git a/src/wasm/canonical-types.h b/src/wasm/canonical-types.h
index b992652..5301224 100644
--- a/src/wasm/canonical-types.h
+++ b/src/wasm/canonical-types.h
@@ -85,17 +85,12 @@
   V8_EXPORT_PRIVATE const CanonicalArrayType* LookupArray(
       CanonicalTypeIndex index) const;
 
-  // Returns if {canonical_sub_index} is a canonical subtype of
-  // {canonical_super_index}.
+  // Returns if {sub_index} is a canonical subtype of {super_type}, which must
+  // be an indexed type. Interprets {sub_index} as (exact sub_index), which is
+  // appropriate for checking the actual type of a thing against a required
+  // type.
   V8_EXPORT_PRIVATE bool IsCanonicalSubtype(CanonicalTypeIndex sub_index,
-                                            CanonicalTypeIndex super_index);
-
-  // Returns if the type at {sub_index} in {sub_module} is a subtype of the
-  // type at {super_index} in {super_module} after canonicalization.
-  V8_EXPORT_PRIVATE bool IsCanonicalSubtype(ModuleTypeIndex sub_index,
-                                            ModuleTypeIndex super_index,
-                                            const WasmModule* sub_module,
-                                            const WasmModule* super_module);
+                                            CanonicalValueType super_type);
 
   // Deletes recursive groups. Used by fuzzers to avoid accumulating memory, and
   // used by specific tests e.g. for serialization / deserialization.
diff --git a/src/wasm/constant-expression-interface.cc b/src/wasm/constant-expression-interface.cc
index 0bd4c138..757b4a1 100644
--- a/src/wasm/constant-expression-interface.cc
+++ b/src/wasm/constant-expression-interface.cc
@@ -124,8 +124,13 @@
   bool function_is_shared = module_->type(sig_index).is_shared;
   CanonicalValueType type =
       CanonicalValueType::Ref(module_->canonical_type_id(sig_index),
-                              function_is_shared, RefTypeKind::kFunction)
-          .AsExactIfEnabled(decoder->enabled_);
+                              function_is_shared, RefTypeKind::kFunction);
+  // Imported functions can be subtypes of their static import type,
+  // for non-imported functions we can return an exact type.
+  if (decoder->enabled_.has_custom_descriptors() &&
+      function_index >= module_->num_imported_functions) {
+    type = type.AsExact();
+  }
   DirectHandle<WasmFuncRef> func_ref =
       WasmTrustedInstanceData::GetOrCreateFuncRef(
           isolate_,
diff --git a/src/wasm/function-body-decoder-impl.h b/src/wasm/function-body-decoder-impl.h
index 6aa071e..5a6a2ec 100644
--- a/src/wasm/function-body-decoder-impl.h
+++ b/src/wasm/function-body-decoder-impl.h
@@ -4243,9 +4243,17 @@
     if (!this->ValidateFunction(this->pc_ + 1, imm)) return 0;
     ModuleTypeIndex index = this->module_->functions[imm.index].sig_index;
     const TypeDefinition& type_def = this->module_->type(index);
-    Value* value =
-        Push(ValueType::Ref(index, type_def.is_shared, RefTypeKind::kFunction)
-                 .AsExactIfEnabled(this->enabled_));
+    ValueType result_type =
+        ValueType::Ref(index, type_def.is_shared, RefTypeKind::kFunction);
+    // For imported functions, we must return an inexact type, because
+    // importing checks subtyping, i.e. for function types f1 <: f2, it is
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/mjsunit/wasm/custom-descriptors-validity.js b/test/mjsunit/wasm/custom-descriptors-validity.js
index 9b3118f2..204d0a5 100644
--- a/test/mjsunit/wasm/custom-descriptors-validity.js
+++ b/test/mjsunit/wasm/custom-descriptors-validity.js
@@ -119,7 +119,7 @@
   builder.addStruct({describes: 2, supertype: 1});   // 3
 });
 
-CheckValid((builder) => {
+CheckInvalid(/type 4 has invalid explicit supertype 2/, (builder) => {
   builder.addStruct({final: false});  // 0
 }, (builder) => {
   builder.addStruct({descriptor: 2});  // 1
Loading diff…

Original Bug Report

reported by [email protected]

Wasm type confusion due to missing exactness check on JS-Wasm boundary

VULNERABILITY DETAILS

Summary

Wasm type confusion due to missing exactness check on JS-to-Wasm boundary. JSToWasmObject() allows subtyping even for exact types, leading to type confusion when a descriptor is invalidly typed as an exact supertype and is used to instantiate a described struct.

Custom descriptors feature is exposed in the wild by default through Origin Trials from M141, which is currently at Beta and very soon reaches (Early) Stable. This bug is not caused by a recent code change and has existed from the very first feature implementation (approx. 6 months).

Details

WebAssembly Custom Descriptors proposal introduces exactness as part of heaptype. An object may only be typed as exact if it is exactly an instance of that type, but not for its subtypes. This is due to issues around descriptor subtyping and struct instantiation with upcasted descriptors, as traditional Wasm subtyping semantics will allow instantiating a described object with an upcasted descriptor (supertype struct), then downcasting it (subtype struct) which should be illegal. This issue is resolved by introducing exact types as part of heaptypes.

As exactness is now part of heaptype, this means that each and every type conversions must take exactness into account. However, at JS-to-Wasm boundary this is ignored and traditional subtyping rules are allowed even for exact types:

// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/wasm/wasm-objects.cc;drc=e75b1ba2a99aa352048745d44754c5de8be273d8;l=3656
MaybeDirectHandle<Object> JSToWasmObject(Isolate* isolate,
                                         DirectHandle<Object> value,
                                         CanonicalValueType expected,
                                         const char** error_message) {
  // ...
  switch (expected.heap_representation_non_shared()) {
    // ...
    default: {
      DCHECK(expected.has_index());
      CanonicalTypeIndex canonical_index = expected.ref_index();
      auto type_canonicalizer = GetWasmEngine()->type_canonicalizer();

      if (WasmExportedFunction::IsWasmExportedFunction(*value)) {
        // ...
      } else if (WasmJSFunction::IsWasmJSFunction(*value)) {
        // ...
      } else if (WasmCapiFunction::IsWasmCapiFunction(*value)) {
        // ...
      } else if (IsWasmStruct(*value) || IsWasmArray(*value)) {
        DirectHandle<WasmObject> wasm_obj = Cast<WasmObject>(value);
        Tagged<WasmTypeInfo> type_info = wasm_obj->map()->wasm_type_info();
        CanonicalTypeIndex actual_type = type_info->type_index();
        if (!type_canonicalizer->IsCanonicalSubtype(actual_type,                // [!] ignores exactness and allows any subtypes 
                                                    canonical_index)) {
          *error_message = "object is not a subtype of expected type";
          return {};
        }
        return value;
      } else {
        *error_message = "JS object does not match expected wasm type";
        return {};
      }
    }
  }
}

This pokes a hole in the type system exactly as described in the proposal docs, allowing attackers to invalidly type a descriptor A into an exact supertype B, instantiate a struct described by B, then invalidly cast it to another described struct that is described by A. This leads to type confusion between arbitrary Wasm types.

Bisect

Bug introduced by WebAssembly Custom Descriptors, on Origin Trials from M141 and onwards.

VERSION

Chrome Version: M141~
Operating System: All

REPRODUCTION CASE

Attached as poc.js which exploits this issue to alias two unrelated struct types, then uses this type confusion to trigger an arbitrary caged write within the sandbox.

Also attached is rce.html which exploits this issue, together with the wrapper-wasmcpt-uaf v8sbx bypass, to gain RCE and print out /flag/flag to stdout.

You might want to pass --experimental-wasm-custom-descriptors on d8 or --enable-blink-features=WebAssemblyCustomDescriptors on Chrome to simulate Origin Trials behavior.

FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION

Type of crash: Renderer
Crash State: Crashes on arbitrary caged write attempt from JIT-compiled Wasm function with poc.js / RCE with rce.html

CREDIT INFORMATION

Reporter credit: Seunghyun Lee (@0x10n) of CMU CSD / CyLab

View on issue tracker