CVE-2026-6301
Overview
Files Changed
src/compiler/js-operator.ccsrc/compiler/js-operator.h
Patch
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_; };
Original Bug Report
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
depthparameter arrives assize_t(64-bit). - It is stored into a
uint16_tfield viastatic_cast<uint16_t>(depth). Any value > 65535 is silently truncated todepth % 65536. - The getter
depth()returnssize_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,
exportedValueis 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 \