High chrome Type Confusion 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in Turbofan
DescriptionType Confusion in Turbofan
ComponentTurbofan
Bug ClassType Confusion
Tracker495273999
Fix commit036e5e8f69be (v8/v8) +11/-9
CISA KEVNot listed
Creditedqymag1c
Disclosed2026-04-15

Files Changed

  • src/compiler/js-operator.cc
  • src/compiler/js-operator.h
From 036e5e8f69be9fddc80bdbac10406186be2fa5b5 Mon Sep 17 00:00:00 2001
From: Nico Hartmann <[email protected]>
Date: Wed, 25 Mar 2026 12:11:21 +0100
Subject: [PATCH] [turbofan] Grow ContextAccess' depth field to 31 bits

Fixed: 495273999
Change-Id: I1ce294051aad3f413744386223976cd9c8b24bca
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7698216
Commit-Queue: Nico Hartmann <[email protected]>
Reviewed-by: Darius Mercadier <[email protected]>
Auto-Submit: Nico Hartmann <[email protected]>
Cr-Commit-Position: refs/heads/main@{#106031}
---

diff --git a/src/compiler/js-operator.cc b/src/compiler/js-operator.cc
index a598cbc..46a4346 100644
--- a/src/compiler/js-operator.cc
+++ b/src/compiler/js-operator.cc
@@ -152,16 +152,14 @@
   return OpParameter<CallRuntimeParameters>(op);
 }
 
-
 ContextAccess::ContextAccess(size_t depth, size_t index, bool immutable)
-    : immutable_(immutable),
-      depth_(static_cast<uint16_t>(depth)),
+    : immutable_and_depth_(ImmutableField::encode(immutable) |
+                           DepthField::encode(static_cast<uint32_t>(depth))),
       index_(static_cast<uint32_t>(index)) {
-  DCHECK(depth <= std::numeric_limits<uint16_t>::max());
+  CHECK_EQ(depth, DepthField::decode(immutable_and_depth_));
   DCHECK(index <= std::numeric_limits<uint32_t>::max());
 }
 
-
 bool operator==(ContextAccess const& lhs, ContextAccess const& rhs) {
   return lhs.depth() == rhs.depth() && lhs.index() == rhs.index() &&
          lhs.immutable() == rhs.immutable();
diff --git a/src/compiler/js-operator.h b/src/compiler/js-operator.h
index ac3b4c0..a2c4fa4 100644
--- a/src/compiler/js-operator.h
+++ b/src/compiler/js-operator.h
@@ -354,15 +354,19 @@
  public:
   ContextAccess(size_t depth, size_t index, bool immutable);
 
-  size_t depth() const { return depth_; }
+  size_t depth() const { return DepthField::decode(immutable_and_depth_); }
   size_t index() const { return index_; }
-  bool immutable() const { return immutable_; }
+  bool immutable() const {
+    return ImmutableField::decode(immutable_and_depth_);
+  }
 
  private:
+  using ImmutableField = base::BitField<bool, 0, 1>;
+  using DepthField = ImmutableField::Next<uint32_t, 31>;
+
   // For space reasons, we keep this tightly packed, otherwise we could just use
   // a simple int/int/bool POD.
-  const bool immutable_;
-  const uint16_t depth_;
+  uint32_t immutable_and_depth_;
   const uint32_t index_;
 };
 
Loading diff…

Original Bug Report

reported by [email protected]

V8 TurboFan contextAccess depth truncation cause type confusion in Module Variable Access

Steps to reproduce the problem

Use CF’s default run command: ASAN_OPTIONS="alloc_dealloc_mismatch=0:allocator_may_return_null=1:allow_user_segv_handler=1:check_malloc_usable_size=0:detect_leaks=1:detect_odr_violation=0:detect_stack_use_after_return=1:external_symbolizer_path=/mnt/scratch0/clusterfuzz/bot/builds/v8-asan_linux-release_dd2f90e18dce5d8550461e387b6dcf5a476ceb72/symbolized/debug/llvm-symbolizer:fast_unwind_on_fatal=1:handle_abort=1:handle_segv=1:handle_sigbus=1:handle_sigfpe=1:handle_sigill=1:handle_sigtrap=1:malloc_context_size=128:print_scariness=1:print_summary=1:print_suppressions=0:redzone=128:strict_memcmp=0:symbolize=1:symbolize_inline_frames=true:use_sigaltstack=1" out/x64.asan/d8 --fuzzing --disable-abortjs --disable-in-process-stack-traces --verify-heap poc.js

Problem Description

A uint16_t integer truncation in TurboFan’s ContextAccess class causes a type confusion when accessing module variables from deeply nested closures. When the context chain depth from a closure to its enclosing module scope exceeds 65535, the depth is silently truncated modulo 65536 in the TurboFan optimizing compiler. This causes the context chain walk to terminate prematurely—landing on an intermediate context instead of the module context. When the walk lands on a catch context, the EXTENSION_INDEX slot aliases THROWN_OBJECT_INDEX, giving the attacker direct control over the value that TurboFan unconditionally treats as a SourceTextModule pointer. The subsequent unguarded field loads from this attacker-controlled object cause heap memory corruption.

2. Root Cause: uint16_t Truncation in ContextAccess::depth_

The Vuln (src/compiler/js-operator.h:361-366)

class ContextAccess final {
 public:
  ContextAccess(size_t depth, size_t index, bool immutable);

  size_t depth() const { return depth_; }  //returns size_t (64-bit)
  size_t index() const { return index_; }
  bool immutable() const { return immutable_; }

 private:
  // For space reasons, we keep this tightly packed, otherwise we could just use
  // a simple int/int/bool POD.
  const bool immutable_;
  const uint16_t depth_;    //truncated to 16 bits
  const uint32_t index_;
};

The Constructor (src/compiler/js-operator.cc:156-162)

ContextAccess::ContextAccess(size_t depth, size_t index, bool immutable)
    : immutable_(immutable),
      depth_(static_cast<uint16_t>(depth)),  
      index_(static_cast<uint32_t>(index)) {
  DCHECK(depth <= std::numeric_limits<uint16_t>::max());  
  DCHECK(index <= std::numeric_limits<uint32_t>::max());
}

Key observations:

  • The depth parameter arrives as size_t (64-bit).
  • It is stored into a uint16_t field via static_cast<uint16_t>(depth). Any value > 65535 is silently truncated to depth % 65536.
  • The getter depth() returns size_t, masking the internal truncation from callers.

The bytecode format uses kUImm (scalable unsigned byte) which correctly scales to 2-byte or 4-byte encoding for values > 255. The interpreter walks the full context chain correctly.

The truncation happens only when TurboFan compiles the bytecode into its IR. The BytecodeGraphBuilder reads the correct uint32_t depth from bytecode, but then passes it to ContextAccess where it gets truncated:

// src/compiler/bytecode-graph-builder.cc:2386-2393
void BytecodeGraphBuilder::VisitStaModuleVariable() {
  int32_t cell_index = bytecode_iterator().GetImmediateOperand(0);
  uint32_t depth = bytecode_iterator().GetUnsignedImmediateOperand(1);  // correct value
  Node* module = NewNode(
      javascript()->LoadContextNoCell(depth, Context::EXTENSION_INDEX, true));
      //passes to ContextAccess,truncated
  Node* value = environment()->LookupAccumulator();
  NewNode(javascript()->StoreModule(cell_index), module, value);
}

This means:

  • During interpreter execution (before JIT): the function runs correctly, exportedValue is accessed at the right module context.
  • After TurboFan compilation (triggered by warmup loop): the optimized code uses the truncated depth, context walk stops short, and the type confusion occurs.

Additional Comments

Introduced by commit

commit  a9f593ef6ba936f07c034ce74e93a1bd9507df5f
[compiler,modules] Introduce JS operators for module loads and stores.

With this CL, the bytecode graph builder no longer translates module
loads/stores as runtime calls but in terms of two new JS operators.  These are
lowered in typed-lowering to a sequence of LoadField's.

[email protected]
[email protected]
BUG=v8:1569

Review-Url: https://codereview.chromium.org/2489863003
Cr-Commit-Position: refs/heads/master@{#40881}

Suggested Fix

Widen depth_ to uint32_t

// src/compiler/js-operator.h
 private:
  const bool immutable_;
- const uint16_t depth_;
+ const uint32_t depth_;
  const uint32_t index_;
// src/compiler/js-operator.cc
ContextAccess::ContextAccess(size_t depth, size_t index, bool immutable)
    : immutable_(immutable),
-     depth_(static_cast<uint16_t>(depth)),
+     depth_(static_cast<uint32_t>(depth)),
      index_(static_cast<uint32_t>(index)) {
- DCHECK(depth <= std::numeric_limits<uint16_t>::max());
+ DCHECK(depth <= std::numeric_limits<uint32_t>::max());
  DCHECK(index <= std::numeric_limits<uint32_t>::max());
}

Summary

V8 TurboFan contextAccess depth truncation cause type confusion in Module Variable Access

Custom Questions

Type of crash:

tab

Crash state:

AddressSanitizer:DEADLYSIGNAL
=================================================================
==2487637==ERROR: AddressSanitizer: SEGV on unknown address 0x000041414141 (pc 0x62e9e0000a9a bp 0x7ffed7927c88 sp 0x7ffed7927c68 T0)
==2487637==The signal is caused by a READ memory access.
    #0 0x62e9e0000a9a  (<unknown module>)
    #1 0x62e9e00011d5  (<unknown module>)
    #2 0x62e9b8a70f29 in Builtins_PromiseFulfillReactionJob setup-isolate-deserialize.cc
    #3 0x62e9b8966552 in Builtins_RunMicrotasks setup-isolate-deserialize.cc
    #4 0x62e9b89315aa in Builtins_JSRunMicrotasksEntry setup-isolate-deserialize.cc
    #5 0x62e9b3ea2a57 in v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) src/execution/simulator.h:216:12
    #6 0x62e9b3ea4b69 in v8::internal::(anonymous namespace)::InvokeWithTryCatch(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) src/execution/execution.cc:534:18
    #7 0x62e9b3ea4f4f in v8::internal::Execution::TryRunMicrotasks(v8::internal::Isolate*, v8::internal::MicrotaskQueue*) src/execution/execution.cc:638:10
    #8 0x62e9b3f867aa in v8::internal::MicrotaskQueue::RunMicrotasks(v8::internal::Isolate*) src/execution/microtask-queue.cc:185:22
    #9 0x62e9b3f86184 in v8::internal::MicrotaskQueue::PerformCheckpointInternal(v8::Isolate*) src/execution/microtask-queue.cc:129:3
    #10 0x62e9b3f06922 in v8::internal::Isolate::FireCallCompletedCallbackInternal(v8::internal::MicrotaskQueue*) src/execution/microtask-queue.h:48:5
    #11 0x62e9b3b5e123 in v8::CallDepthScope<true>::~CallDepthScope() src/execution/isolate.h:1798:5
    #12 0x62e9b3b1403c in v8::Script::Run(v8::Local<v8::Context>, v8::Local<v8::Data>) src/api/api-inl.h:259:20
    #13 0x62e9b3767137 in v8::Shell::ExecuteString(v8::Isolate*, v8::Local<v8::String>, v8::Local<v8::String>, v8::Shell::ReportExceptions, v8::Global<v8::Value>*) src/d8/d8.cc:1041:44
    #14 0x62e9b379f8e9 in v8::SourceGroup::Execute(v8::Isolate*) src/d8/d8.cc:5670:10
    #15 0x62e9b37abded in v8::Shell::RunMainIsolate(v8::Isolate*, bool) src/d8/d8.cc:6689:37
    #16 0x62e9b37ab225 in v8::Shell::RunMain(v8::Isolate*, bool) src/d8/d8.cc:6597:18
    #17 0x62e9b37ae99b in v8::Shell::Main(int, char**) src/d8/d8.cc:7514:18
    #18 0x7a56e002a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #19 0x7a56e002a28a in __libc_start_main csu/../csu/libc-start.c:360:3
    #20 0x62e9b365e029 in _start (/home/qy/0321/asanre/v8/out/x64.asan/d8+0x1346029) (BuildId: 7c522f96ec63a807)

==2487637==Register values:
rax = 0x0000000000000002  rbx = 0x0000000000000000  rcx = 0x000062e9e0000a80  rdx = 0x000000004141411e  
rdi = 0x0000750e014f8be5  rsi = 0x0000750e014f8c05  rbp = 0x00007ffed7927c88  rsp = 0x00007ffed7927c68  
 r8 = 0x0000750e0101edc5   r9 = 0xffffff0000000000  r10 = 0x000076563f2c8000  r11 = 0x4000000000000000  
r12 = 0x000078a6df1ea860  r13 = 0x00007966df1e1080  r14 = 0x0000750e00000000  r15 = 0x0000000000165200  
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV (<unknown module>) 
==2487637==ABORTING

Reporter credit:

qymag1c

Additional Data

Category: Security
Chrome Channel: Not sure
Regression: N/A \

View on issue tracker