CVE-2026-118359
Overview
Background
- V8 sandbox / trusted pointers
- with
V8_ENABLE_SANDBOX, pointers to “trusted” objects (e.g.WasmTrustedInstanceData, function data) are not stored inline but as 32-bit handles indexing a per-isolateTrustedPointerTable. Decoding a handle loads the table entry, validates its tag, and masks off tag bits to recover the real pointer. - `BuildDecodeTrustedPointer`
- the old TurboFan helper (
src/compiler/wasm-graph-assembler.cc) that emitted the handle decode as a sequence of plain machine ops: shift the handle to an index, load the table base, load the entry, tag-check, mask, andBitcastWordToTagged. - Load elimination
- a TurboFan optimization that removes loads it proves redundant (same address/effect). It reasons over the individual machine
Loadnodes emitted inside the decode sequence. - `LoadTrustedPointer` operator
- the new dedicated
IrOpcode::kLoadTrustedPointer(common-operator.*) carrying{offset, tag}, markedkEliminatable, lowered in Turboshaft (graph-builder.cc) and ultimately the instruction selector, so the whole decode is one opaque node.
Root Cause Analysis
The advisory says “Memory corruption in V8” (MemoryCorruption); the diff shows a TurboFan miscompilation of sandbox trusted-pointer loads that corrupts V8’s object graph — a type-confusion-flavored bug. Pre-patch, LoadTrustedPointerFromObject / LoadImmutableTrustedPointerFromObject / LoadTrustedPointerFromObjectTrapOnNull first loaded the 32-bit handle from the object (LoadFromObject(MachineType::Uint32(), object, offset)) and then called BuildDecodeTrustedPointer, which expanded into separate machine Loads (table base, table entry) plus the tag check. Because these were ordinary eliminable loads, load elimination could treat the handle load (or an inner table load) as redundant with an earlier, unrelated load and erroneously eliminate it, reusing a stale value. The commit states this directly: emitting the decode as a series of instructions let “load elimination … erroneously eliminat[e] the load to the pointer handle.” A wrong/stale handle decodes to a different TrustedPointerTable entry than intended, so the recovered pointer refers to the wrong trusted object — a type confusion among trusted objects that defeats the sandbox’s integrity guarantee.
The fix emits a single dedicated operator, common_.LoadTrustedPointer(field_offset, tag), as one IrOpcode::kLoadTrustedPointer node (plumbed through opcodes.h, the verifiers, the typer, and simplified-lowering-verifier), and lowers it only later in Turboshaft via __ LoadTrustedPointer(...) and the instruction selector. By representing the entire handle-fetch-and-decode as one opaque node rather than a sequence of generic loads, load elimination can no longer match and remove the inner handle load against an unrelated load, so the decode always reads the current handle from the object. The redundant BuildDecodeTrustedPointer, LoadImmutableTrustedPointerFromObject, and LoadTrustedPointerFromObjectTrapOnNull helpers are removed, and callers (LoadFunctionDataFromJSFunction, LoadTrustedDataFromInstanceObject) now route through the single LoadTrustedPointerFromObject.
TrustedPointerTable entry — a type confusion of trusted objects; the fix makes the decode one opaque LoadTrustedPointer operator that elimination cannot tear apart.Attack Path
- Web attacker
serves a page that runs WebAssembly, since the affected decodes (
LoadFunctionDataFromJSFunction,LoadTrustedDataFromInstanceObject) are on the Wasm/TurboFan path (web-reachable). - Trigger optimization crafts Wasm/JS that TurboFan optimizes with multiple trusted-pointer loads positioned so load elimination considers the handle load redundant with an earlier load (inferred: specific graph shape needed to trip elimination).
- Stale handle decode
the optimizer removes the real handle load; the decode reuses a prior/stale handle value and indexes the wrong
TrustedPointerTableentry. - Wrong trusted object the recovered “trusted” pointer refers to a different trusted object (e.g. mismatched trusted instance/function data) than the code assumes.
- Primitive type confusion among sandbox-trusted objects, enabling corruption of V8 heap/sandbox state; the full read/write-what-where and sandbox-escape chain is (inferred), consistent with the “memory corruption” severity.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
LoadTrustedPointerOperatorsrc/compiler/common-operator.cc |
modified | |
V8_EXPORT_PRIVATEsrc/compiler/common-operator.h |
modified | |
simplified_src/compiler/wasm-graph-assembler.h |
modified | |
common_src/compiler/wasm-graph-assembler.h |
modified |
Files Changed
src/compiler/common-operator.ccsrc/compiler/turbofan-typer.ccsrc/compiler/common-operator.hsrc/compiler/turboshaft/graph-builder.ccsrc/compiler/verifier.ccsrc/compiler/wasm-graph-assembler.hsrc/compiler/simplified-lowering-verifier.ccsrc/compiler/wasm-graph-assembler.ccsrc/compiler/opcodes.h
Audit Directions
- Other decode-as-sequence loadsaudit remaining sandbox pointer decoders (external-pointer, code-pointer, indirect-pointer) still emitted as sequences of generic loads that load elimination or CSE could tear apart.
- Eliminability of security-critical loadsreview loads whose result identity matters (handles, tags) for whether marking them eliminable lets the optimizer reuse stale values; consider dedicated opaque operators.
- Turboshaft parityverify the Turboshaft/instruction-selector lowering of
LoadTrustedPointerpreserves the tag check and cannot itself be hoisted/merged incorrectly. - Immutable vs mutable trusted loadsthe removed
LoadImmutable...variant implied stronger aliasing assumptions; check otherLoadImmutable*trusted/external loads for the same erroneous-elimination risk.
Patch
From d4374eaec817692d144b16ade997040e60ee6139 Mon Sep 17 00:00:00 2001 From: Manos Koukoutos <[email protected]> Date: Fri, 16 Oct 2026 10:35:05 +0000 Subject: [PATCH] [turbofan] Emit dedicated operator for trusted-pointer loads Instead of emitting a trusted-pointer load as a series of instructions in WasmGraphAssembler::BuildDecodeTrustedPointer, we emit a dedicated operator for it (LoadTrustedPointerOperator), which is translated into Turboshaft and then lowered in the instruction selector. This prevents load elimination from erroneously eliminating the load to the pointer handle. Fixed: 573378812 Change-Id: Ic651fe29cec26aa4b23039db481dee6dcabcb5c6 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8591942 Reviewed-by: Jakob Kummerow <[email protected]> Commit-Queue: Manos Koukoutos <[email protected]> Cr-Commit-Position: refs/heads/main@{#110863} --- diff --git a/src/compiler/common-operator.cc b/src/compiler/common-operator.cc index 37ab33d..fe96407 100644 --- a/src/compiler/common-operator.cc +++ b/src/compiler/common-operator.cc @@ -515,6 +515,32 @@ return OpParameter<ExitMachineGraphParameters>(op); } +#if V8_ENABLE_SANDBOX +size_t hash_value(const LoadTrustedPointerParameters& params) { + return base::hash_combine(params.offset, params.tag); +} + +V8_EXPORT_PRIVATE std::ostream& operator<<( + std::ostream& os, LoadTrustedPointerParameters params) { + return os << "offset=" << params.offset << ", tag.first=" << params.tag.first + << ", tag.last=" << params.tag.last; +} + +class LoadTrustedPointerOperator final + : public Operator1<LoadTrustedPointerParameters> { + public: + LoadTrustedPointerOperator(int offset, TrustedPointerTagRange tag) + : Operator1<LoadTrustedPointerParameters>( + IrOpcode::kLoadTrustedPointer, Operator::kEliminatable, + "LoadTrustedPointer", 1, 1, 1, 1, 1, 0, {offset, tag}) {} +}; + +const Operator* CommonOperatorBuilder::LoadTrustedPointer( + int offset, TrustedPointerTagRange tag) { + return zone_->New<LoadTrustedPointerOperator>(offset, tag); +} +#endif // V8_ENABLE_SANDBOX + #define COMMON_CACHED_OP_LIST(V) \ V(Plug, Operator::kNoProperties, 0, 0, 0, 1, 0, 0) \ V(Dead, Operator::kFoldable, 0, 0, 0, 1, 1, 1) \ diff --git a/src/compiler/common-operator.h b/src/compiler/common-operator.h index 6499e23..1a0abe5 100644 --- a/src/compiler/common-operator.h +++ b/src/compiler/common-operator.h @@ -538,6 +538,20 @@ V8_EXPORT_PRIVATE const ExitMachineGraphParameters& ExitMachineGraphParametersOf(const Operator* op) V8_WARN_UNUSED_RESULT; +#if V8_ENABLE_SANDBOX +struct LoadTrustedPointerParameters { + int offset; + TrustedPointerTagRange tag; + + bool operator==(const LoadTrustedPointerParameters&) const = default; +}; + +size_t hash_value(const LoadTrustedPointerParameters& params); + +V8_EXPORT_PRIVATE std::ostream& operator<<(std::ostream& os, + LoadTrustedPointerParameters params); +#endif // V8_ENABLE_SANDBOX + // Interface for building common operators that can be used at any level of IR, // including JavaScript, mid-level, and low-level. class V8_EXPORT_PRIVATE CommonOperatorBuilder final @@ -616,6 +630,9 @@ const Operator* HeapConstant(const Handle<HeapObject>&); const Operator* CompressedHeapConstant(const Handle<HeapObject>&); const Operator* TrustedHeapConstant(const Handle<HeapObject>&); +#if V8_ENABLE_SANDBOX + const Operator* LoadTrustedPointer(int offset, TrustedPointerTagRange tag); +#endif const Operator* ObjectId(uint32_t); const Operator* RelocatableInt32Constant(int32_t value, diff --git a/src/compiler/opcodes.h b/src/compiler/opcodes.h index de11918..d74406b 100644 --- a/src/compiler/opcodes.h +++ b/src/compiler/opcodes.h @@ -85,15 +85,16 @@ V(EnterMachineGraph) \ V(ExitMachineGraph) -#define COMMON_OP_LIST(V) \ - CONSTANT_OP_LIST(V) \ - INNER_OP_LIST(V) \ - V(Unreachable) \ - V(DeadValue) \ - V(Dead) \ - V(Plug) \ - V(SLVerifierHint) \ - V(MajorGCForCompilerTesting) \ +#define COMMON_OP_LIST(V) \ + CONSTANT_OP_LIST(V) \ + INNER_OP_LIST(V) \ + V(Unreachable) \ + V(DeadValue) \ + V(Dead) \ + V(Plug) \ + V(SLVerifierHint) \ + V(MajorGCForCompilerTesting) \ + IF_SANDBOX(V, LoadTrustedPointer) \ V(StaticAssert) // Opcodes for JavaScript operators. diff --git a/src/compiler/simplified-lowering-verifier.cc b/src/compiler/simplified-lowering-verifier.cc index d288ed1..6d41bbb 100644 --- a/src/compiler/simplified-lowering-verifier.cc +++ b/src/compiler/simplified-lowering-verifier.cc @@ -504,6 +504,7 @@ CASE(Dead) CASE(Plug) CASE(MajorGCForCompilerTesting) + IF_SANDBOX(CASE, LoadTrustedPointer) CASE(StaticAssert) // Simplified change operators CASE(ChangeTaggedSignedToInt32) diff --git a/src/compiler/turbofan-typer.cc b/src/compiler/turbofan-typer.cc index 16fa4b6..04df612 100644 --- a/src/compiler/turbofan-typer.cc +++ b/src/compiler/turbofan-typer.cc @@ -1297,6 +1297,9 @@ UNREACHABLE(); } Type Typer::Visitor::TypeStaticAssert(Node* node) { UNREACHABLE(); } +#if V8_ENABLE_SANDBOX +Type Typer::Visitor::TypeLoadTrustedPointer(Node* node) { UNREACHABLE(); } +#endif Type Typer::Visitor::TypeSLVerifierHint(Node* node) { UNREACHABLE(); } // JS comparison operators. diff --git a/src/compiler/turboshaft/graph-builder.cc b/src/compiler/turboshaft/graph-builder.cc index 4ab5422..2b824d5 100644 --- a/src/compiler/turboshaft/graph-builder.cc +++ b/src/compiler/turboshaft/graph-builder.cc @@ -1434,6 +1434,14 @@ return __ Load(Map(node->InputAt(0)), Map(node->InputAt(1)), LoadOp::Kind::Trapping(), loaded_rep); } +#if V8_ENABLE_SANDBOX + case IrOpcode::kLoadTrustedPointer: { + auto params = OpParameter<LoadTrustedPointerParameters>(op); + return __ LoadTrustedPointer(Map(node->InputAt(0)), + LoadOp::Kind::TaggedBase(), params.tag, + params.offset + kHeapObjectTag); + } +#endif case IrOpcode::kStore: case IrOpcode::kUnalignedStore: { diff --git a/src/compiler/verifier.cc b/src/compiler/verifier.cc index a4787d8..39d2dbc 100644 --- a/src/compiler/verifier.cc +++ b/src/compiler/verifier.cc @@ -653,6 +653,11 @@ case IrOpcode::kExitMachineGraph: CheckValueInputIs(node, 0, Type::Machine()); break; +#if V8_ENABLE_SANDBOX + case IrOpcode::kLoadTrustedPointer: + // TODO(manoskouk): what are the constraints here? + break; +#endif // JavaScript operators // -------------------- diff --git a/src/compiler/wasm-graph-assembler.cc b/src/compiler/wasm-graph-assembler.cc index 77869ef..4c46921 100644 --- a/src/compiler/wasm-graph-assembler.cc +++ b/src/compiler/wasm-graph-assembler.cc @@ -242,55 +242,6 @@ #endif // V8_ENABLE_SANDBOX } -Node* WasmGraphAssembler::BuildDecodeTrustedPointer( - Node* handle, TrustedPointerTagRange tag_range) { -#if V8_ENABLE_SANDBOX - Node* index = Word32Shr(handle, Int32Constant(kTrustedPointerHandleShift)); - Node* offset = ChangeUint32ToUint64( - Word32Shl(index, Int32Constant(kTrustedPointerTableEntrySizeLog2))); - Node* table = Load(MachineType::Pointer(), LoadRootRegister(), - IsolateData::trusted_pointer_table_offset() + - Internals::kExternalEntityTableBasePointerOffset); - Node* decoded_ptr = Load(MachineType::Pointer(), table, offset); - - if (IsFastTrustedPointerTagRange(tag_range)) { - uint64_t mask = ComputeUntaggingMaskForFastTrustedPointerTag(tag_range); - decoded_ptr = WordAnd(decoded_ptr, IntPtrConstant(mask)); - } else { - Node* tag = - WordShr(decoded_ptr, IntPtrConstant(kTrustedPointerTableTagShift)); - - auto ok = MakeLabel(); - if (tag_range.Size() == 1) { - // The common and simple case: we expect exactly one tag. - Node* expected_tag = IntPtrConstant(tag_range.first); - GotoIf(WordEqual(tag, expected_tag), &ok, BranchHint::kTrue); - RuntimeAbort(AbortReason::kTrustedPointerTagMismatch); - Goto(&ok); // Unreachable, but needed for graph coherency. - } else { - // Range check. - Node* diff = IntPtrSub(tag, IntPtrConstant(tag_range.first)); - GotoIf(Uint64LessThanOrEqual( - diff, IntPtrConstant(tag_range.last - tag_range.first)), - &ok, BranchHint::kTrue); - RuntimeAbort(AbortReason::kTrustedPointerTagMismatch); - Goto(&ok); // Unreachable, but needed for graph coherency. - } - - Bind(&ok); - decoded_ptr = - WordAnd(decoded_ptr, IntPtrConstant(kTrustedPointerTablePayloadMask)); - } - - // We have to change the type of the result value to Tagged, so if the value - // gets spilled on the stack, it will get processed by the GC. - decoded_ptr = BitcastWordToTagged(decoded_ptr); - return decoded_ptr; -#else - UNREACHABLE(); -#endif // V8_ENABLE_SANDBOX -} - Node* WasmGraphAssembler::BuildLoadExternalPointerFromObject( Node* object, int field_offset, ExternalPointerTagRange tag_range, Node* isolate_root) { @@ -408,41 +359,17 @@ return LoadFromObject(type, byte_array, offset); } -Node* WasmGraphAssembler::LoadImmutableTrustedPointerFromObject( - Node* object, int field_offset, TrustedPointerTagRange tag) { - Node* offset = IntPtrConstant(field_offset); -#ifdef V8_ENABLE_SANDBOX - Node* handle = LoadImmutableFromObject(MachineType::Uint32(), object, offset); - return BuildDecodeTrustedPointer(handle, tag); -#else - return LoadImmutableFromObject(MachineType::TaggedPointer(), object, offset); -#endif -} - Node* WasmGraphAssembler::LoadTrustedPointerFromObject( Node* object, int field_offset, TrustedPointerTagRange tag) { - Node* offset = IntPtrConstant(field_offset); #ifdef V8_ENABLE_SANDBOX - Node* handle = LoadFromObject(MachineType::Uint32(), object, offset); - return BuildDecodeTrustedPointer(handle, tag); + return AddNode(graph()->NewNode(common_.LoadTrustedPointer(field_offset, tag), + object, effect(), control())); #else + Node* offset = IntPtrConstant(field_offset); return LoadFromObject(MachineType::TaggedPointer(), object, offset); #endif } -std::pair<Node*, Node*> -WasmGraphAssembler::LoadTrustedPointerFromObjectTrapOnNull( - Node* object, int field_offset, TrustedPointerTagRange tag) { - Node* offset = IntPtrConstant(field_offset); -#ifdef V8_ENABLE_SANDBOX - Node* handle = LoadTrapOnNull(MachineType::Uint32(), object, offset); - return {handle, BuildDecodeTrustedPointer(handle, tag)}; -#else - Node* value = LoadTrapOnNull(MachineType::TaggedPointer(), object, offset); - return {value, value}; -#endif -} - Node* WasmGraphAssembler::StoreFixedArrayElement(Node* array, int index, Node* value, ObjectAccess access) { @@ -465,7 +392,7 @@ Node* WasmGraphAssembler::LoadFunctionDataFromJSFunction(Node* js_function) { Node* shared = LoadSharedFunctionInfo(js_function); - return LoadImmutableTrustedPointerFromObject( + return LoadTrustedPointerFromObject( shared, offsetof(SharedFunctionInfo, trusted_function_data_) - kHeapObjectTag, kWasmExportedFunctionDataTrustedPointerTag); @@ -623,7 +550,7 @@ Node* WasmGraphAssembler::LoadTrustedDataFromInstanceObject( Node* instance_object) { - return LoadImmutableTrustedPointerFromObject( + return LoadTrustedPointerFromObject( instance_object, offsetof(WasmInstanceObject, trusted_data_) - kHeapObjectTag, kWasmTrustedInstanceDataTrustedPointerTag); diff --git a/src/compiler/wasm-graph-assembler.h b/src/compiler/wasm-graph-assembler.h index 2f5b4db..00ec30a 100644 --- a/src/compiler/wasm-graph-assembler.h +++ b/src/compiler/wasm-graph-assembler.h @@ -28,7 +28,8 @@ public: WasmGraphAssembler(MachineGraph* mcgraph, Zone* zone) : GraphAssembler(mcgraph, zone, BranchSemantics::kMachine), - simplified_(zone) {} + simplified_(zone), + common_(zone) {} // While CallBuiltin() translates to a direct call to the address of the // builtin, CallBuiltinThroughJumptable instead jumps to a slot in a jump @@ -177,15 +178,8 @@ ExternalPointerTagRange tag_range, Node* isolate_root); - Node* LoadImmutableTrustedPointerFromObject(Node* object, int offset, - TrustedPointerTagRange tag); Node* LoadTrustedPointerFromObject(Node* object, int offset, TrustedPointerTagRange tag); - // Returns the load node (where the source position for the trap needs to be - // set by the caller) and the result. - std::pair<Node*, Node*> LoadTrustedPointerFromObjectTrapOnNull( - Node* object, int offset, TrustedPointerTagRange tag); - Node* BuildDecodeTrustedPointer(Node* handle, TrustedPointerTagRange tag); Node* IsSmi(Node* object); @@ -343,6 +337,7 @@ } SimplifiedOperatorBuilder simplified_; + CommonOperatorBuilder common_; }; } // namespace compiler