Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in Turbofan
DescriptionType Confusion in Turbofan
ComponentTurbofan
Sub-componentTurbofan
Bug ClassType Confusion
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — 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 AIConfirmed
Reviewer notes: Truncation of depth to uint16_t under a release-stripped DCHECK is shown by the diff. The wrong-context walk and slot type confusion are inferred, not shown; the analysis does not show how script reaches a context depth above 65535.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker495273999
Fix commit036e5e8f69be (v8/v8) +11/-9
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
Creditedqymag1c
Reported2026-03-23 (per vendor advisory)
Fix landed2026-03-25 (public git)
Disclosed2026-04-15
Patch gap21 days — fix public in source before the advisory

Background

`ContextAccess`
a TurboFan operator parameter (src/compiler/js-operator.h) describing a context-slot access by its depth, index, and immutable flag.
Context depth
the number of enclosing lexical-scope contexts the optimizer must walk up to reach the target context holding a variable slot.
`base::BitField`
a V8 helper that packs several small fields into one integer word, exposing per-field encode/decode and a Next<T, bits> chaining alias.
Type confusion
treating a value or slot as a type it is not, which in TurboFan follows from reading a wrong context slot and misinterpreting its contents.

Root Cause Analysis

ContextAccess stored depth_ as a uint16_t, and its constructor set it via depth_(static_cast<uint16_t>(depth)) while only asserting the bound under DCHECK. Because DCHECK is compiled out in release builds, a context access with depth > 65535 silently truncated modulo 65536, so depth() returned a value different from the one the front end requested. TurboFan then emitted context-walk code for the truncated depth, reaching the wrong context object and reading the wrong slot, whose contents were assumed to have the type of the intended slot — a type confusion.

The fix packs immutable and depth into a single uint32_t immutable_and_depth_ bitfield, giving depth 31 bits (DepthField::Next<uint32_t, 31>), and replaces the compiled-out DCHECK with a CHECK_EQ(depth, DepthField::decode(immutable_and_depth_)) round-trip that aborts in every build if the value cannot be stored faithfully.

Key insight
The single mistake was storing a size_t depth in a uint16_t field guarded only by a release-stripped DCHECK, letting a large depth truncate unnoticed; the fix widens the field to 31 bits and enforces round-trip fidelity with an always-on CHECK_EQ.

Attack Path

  1. Construct deep nesting Craft JavaScript whose lexical scoping produces a variable access at a context depth exceeding 65535 (e.g. via extreme nesting or generated code).
  2. Trigger optimization Get the containing function tiered up so TurboFan builds a ContextAccess operator for that access, truncating the stored depth to 16 bits.
  3. Emit wrong context walk The optimizer computes the context chain for the truncated depth, so generated code walks to the wrong context object.
  4. Read wrong slot The access reads a slot in the wrong context whose stored value has a type differing from what the optimizer assumed for the intended slot.
  5. Exploit the confusion Downstream optimized operations act on the misinterpreted value, yielding a type confusion the attacker can shape into memory corruption.

Impact Assessment

An attacker running crafted JavaScript in the renderer gains a TurboFan type confusion, a strong primitive typically leveraged toward arbitrary read/write and remote code execution in the sandboxed renderer process. The only precondition is the ability to run script and drive a function to optimization with a context access whose depth exceeds the old 16-bit limit.

Files Changed

  • src/compiler/js-operator.cc
  • src/compiler/js-operator.h

Audit Directions

  • Narrowing casts on sizes
    Search for static_cast<uint16_t>/static_cast<uint32_t> applied to size_t operator parameters, where a large value can truncate.
  • `DCHECK`-only bounds
    Flag range or fit invariants enforced solely by DCHECK, which vanish in release builds; prefer CHECK or round-trip verification for security-relevant fields.
  • Bitfield capacity
    Review base::BitField/Next<> field widths against the full range of values they may store, ensuring encode/decode round-trips cannot lose bits.
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