High patch-now ✔︎ verified CVSS 8.8 chrome Type Confusion 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType confusion in V8
DescriptionType confusion in V8
ComponentV8
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: One-line language-mode fix; commit confirms the sloppy/strict discrepancy. But the receiver-coercion to type-confusion to memory-corruption chain and which builtins are affected are inferred, not shown in the diff.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker545767601
Fix commit6f81b563fa9e (v8/v8) +1/-0
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
CreditedZhenpeng (Leo) Lin at depthfirst
Reported2026-08-12 (per vendor advisory)
Fix landed2026-08-21 (public git)
Disclosed2026-08-25
Patch gap4 days — fix public in source before the advisory

Background

Native builtin
a C++-implemented SharedFunctionInfo exposed to JavaScript, whose backing code assumes its arguments and receiver have specific, well-known types.
`LanguageMode`
a per-function flag that is either LanguageMode::kSloppy or LanguageMode::kStrict, controlling how the receiver (this) is treated on entry.
Receiver coercion
in kSloppy mode V8 rewrites the incoming receiver before the body runs: undefined/null become the global object and primitives (number, string, boolean) are boxed into wrapper objects; in kStrict mode the receiver is passed through unchanged.
Bootstrapper vs `setup-heap-internal`
two code paths that both create the same native builtin SharedFunctionInfo objects during isolate startup, which must agree on the resulting LanguageMode.

Root Cause Analysis

Native builtins are meant to be strict so their C++ implementations always see the exact receiver the caller passed. When these builtins are registered through the bootstrapper they are correctly marked LanguageMode::kStrict, but the parallel creation helper in src/heap/setup-heap-internal.cc constructed the SharedFunctionInfo via NewSharedFunctionInfoForBuiltin without setting the language mode, leaving it at the default LanguageMode::kSloppy.

As a result, a builtin created on this path performs sloppy-mode receiver coercion: an undefined/null this is silently replaced with the global object and a primitive this is boxed into a wrapper. The C++ code behind the builtin, written on the assumption that it receives the raw receiver a strict builtin would get, then operates on a receiver of an unexpected type. This mismatch between the assumed and actual receiver type is a classic type confusion on this. The two registration paths therefore produced semantically different builtins for the same function, and the sloppy variant is the exploitable one.

Key insight
Two independent code paths built the same native builtins but disagreed on a single flag; because sloppy mode coerces and boxes the receiver while the builtin’s C++ still assumes the strict, un-coerced receiver type, the discrepancy turns directly into a type confusion on this.

Attack Path

  1. Reach the sloppy builtin Identify a native builtin that is created through the setup-heap-internal.cc path and is therefore left in LanguageMode::kSloppy.
  2. Call with a coercible receiver Invoke the builtin with a this value of undefined, null, or a primitive so that sloppy-mode entry coercion changes the receiver.
  3. Trigger coercion/boxing V8 replaces the receiver with the global object or a boxed wrapper before dispatching to the builtin’s C++ code.
  4. Confuse the type The C++ implementation treats the coerced/boxed receiver as the specific type it expected, reading or writing fields of the wrong object layout.
  5. Exploit the confusion The type-confused access is used to corrupt memory and escalate toward controlled read/write inside the V8 heap.

Impact Assessment

This is a type confusion in V8 reachable from ordinary JavaScript by calling an affected native builtin with a coercible receiver. Because the confusion arises from a wrong-typed this passed into trusted C++ builtin code, it can lead to memory corruption and, when chained, to remote code execution in the renderer. The bug is present whenever the isolate is initialized through the setup-heap-internal path, so it affects the standard startup configuration.

Files Changed

  • src/heap/setup-heap-internal.cc

Audit Directions

  • Duplicated object construction
    Wherever two paths (bootstrapper and setup-heap-internal) build the same runtime metadata, diff every flag set on one against the other; a missing set_language_mode, flags, or attribute is a latent divergence.
  • Receiver assumptions in builtins
    Audit native builtin C++ implementations that dereference this as a fixed type; confirm the corresponding SharedFunctionInfo is LanguageMode::kStrict so no sloppy coercion or boxing can substitute a different receiver type.
  • Default-value reliance
    Flag calls to NewSharedFunctionInfoForBuiltin and similar factory helpers that rely on default LanguageMode/attributes instead of setting them explicitly, since the safe default for native builtins is strict.
From 6f81b563fa9e3fde15412b0e3ab2b1e2b7e6d0d7 Mon Sep 17 00:00:00 2001
From: pthier <[email protected]>
Date: Fri, 21 Aug 2026 11:58:35 +0200
Subject: [PATCH] Native builtins are strict

There was a discrepancy between native builtins registered in
bootstrapper vs setup-heap-internal.
The former marked the builtins as strict, whereas the latter didn't.

Mark builtins as strict also in setup-heap-internal.

Fixed: 545767601
Change-Id: Ida4da4bdcfe7dcfdc16f1da0ec022a65cf853ce8
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8274474
Reviewed-by: Leszek Swirski <[email protected]>
Commit-Queue: Patrick Thier <[email protected]>
Cr-Commit-Position: refs/heads/main@{#109425}
---
diff --git a/src/heap/setup-heap-internal.cc b/src/heap/setup-heap-internal.cc
index b6d23f6..e7b27b1 100644
--- a/src/heap/setup-heap-internal.cc
+++ b/src/heap/setup-heap-internal.cc
@@ -75,6 +75,7 @@
   DirectHandle<SharedFunctionInfo> shared =
       isolate->factory()->NewSharedFunctionInfoForBuiltin(
           isolate->factory()->empty_string(), builtin, len, kAdapt, kind);
+  shared->set_language_mode(LanguageMode::kStrict);
   return shared;
 }
 
Loading diff…

Original Bug Report

reported by [email protected]

v8 memory corruption leading to in sandbox arbitrary r/w


Report description

v8 memory corruption leading to in sandbox arbitrary r/w


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

https://chromium.googlesource.com/v8/v8.git


The problem

Please describe the technical details of the vulnerability

Summary

TurboFan’s optimized Promise.prototype.finally path creates the internal PromiseThenFinally and PromiseCatchFinally closures with the SharedFunctionInfo’s default constructable normal-function map. The generic builtin path creates the same closures with STRICT_FUNCTION_WITHOUT_PROTOTYPE_MAP_INDEX, so only the optimized helpers are constructable.

A sloppy onFinally callback can obtain the optimized internal helper through legacy Function.caller. After the helper escapes, JavaScript can replace Promise.prototype.then and invoke the helper with new. JSBuiltinsConstructStub returns the PromiseThenFinally builtin’s result directly, including a primitive Smi selected by the attacker.

TurboFan types every JSConstruct result as Receiver. Optimized code therefore omits checks that would reject the Smi, causing type confusion, invalid native memory access, and exploitable optimized property/element accesses.

Version

Source revision: V8 15.3.55 at commit 1ed8fce48374f15f382bb0a708eca9ce5463aa90

Chromium source revision: d712c14d603b9a96757629c6c0d61012a9665c0d

Browser validation: Chromium 153.0.7993.0 custom Linux ASAN build, SHA-256 b6e9ac14f260d01233b15e137d992ec39ff7985096786f93ee5e624838051406, ELF Build ID 18cbaf21969c3b3c.

Root cause analysis

  1. The generic and optimized paths allocate different function types

The generic Promise.prototype.finally path uses AllocateRootFunctionWithContext, which hardcodes STRICT_FUNCTION_WITHOUT_PROTOTYPE_MAP_INDEX and creates nonconstructable helpers. TurboFan’s ReducePromisePrototypeFinally instead uses JSCreateClosure, whose lowering selects the map from each helper’s SharedFunctionInfo.

  1. The optimized helpers retain a constructable map

src/v8/src/heap/setup-heap-internal.cc creates the PromiseThenFinally and PromiseCatchFinally SharedFunctionInfos as default normal functions and never assigns STRICT_FUNCTION_WITHOUT_PROTOTYPE_MAP_INDEX. Optimized closures therefore receive the default constructable, sloppy-function map.

  1. The internal helper can escape src/v8/src/builtins/accessors.cc allows sloppy Function.caller to return the next native or user JavaScript caller. Because the optimized helper remains sloppy, an onFinally callback can obtain its PromiseThenFinally caller.

  2. Construction can return a primitive

Builtin-backed SharedFunctionInfos use JSBuiltinsConstructStub, which returns the builtin result directly. PromiseThenFinally returns InvokeThen(…), so replacing Promise.prototype.then lets new leakedFinally() return an attacker-controlled Smi instead of a receiver.

  1. Critical compiler assumption TurboFan assumes every JSConstruct result is a Receiver and consequently removes checks that could reject a Smi:
  return Type::Receiver();
}
                                                                                                                      if (!input_type.Maybe(Type::SignedSmall())) {
  return Replace(input);  // Removes CheckHeapObject.
}

Optimized property or element accesses then consume the Smi as a heap object. In the minimized PoC, accessing (new leakedFinally()).field after switching the construct result to Smi 1 causes a native read from address 0x1.

Bisect

The commit that starts the current uninterrupted vulnerable history is:

b158bfdc2f9b66de130b06e48f9e0cc25206c760
Reland "[runtime] Remove the construct_stub field of the SFI"
Cr-Commit-Position: refs/heads/master@{#52389}
Commit date: April 5, 2018

Reproduce

build d8 with the following config

is_debug = false
is_component_build = false
target_cpu = "x64"
v8_target_cpu = "x64"
is_asan = true
is_lsan = false
use_remoteexec = false
use_siso = false
symbol_level = 2                                                                                                       
v8_symbol_level = 2
dcheck_always_on = true
v8_enable_slow_dchecks = true
v8_enable_test_features = true
v8_enable_sandbox = true
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_verify_heap = true
v8_enable_memory_corruption_api = true
v8_enable_undefined_double = true

run

ASAN_OPTIONS='handle_segv=1:abort_on_error=1:symbolize=1:detect_leaks=0' \
  ./d8 \
  --disable-in-process-stack-traces \
  --allow-natives-syntax \
  ./poc_asan.js

You will see the crash log attached.

Reproduce with prebuilt asan chrome:

Start a webserver in the same folder with

python3 -m httpserver
ASAN_OPTIONS='handle_segv=1:abort_on_error=1:symbolize=1:detect_leaks=0:fast_unwind_on_fatal=1:external_symbolizer_path=./153.0.7993.0/llvm-symbolizer' \
  ./153.0.7993.0/chrome-wrapper \
  --headless=new \
  --no-sandbox \
  --disable-gpu \
  --disable-dev-shm-usage \
  --disable-breakpad \
  --disable-in-process-stack-traces \
  --user-data-dir=/tmp/promise-finally-profile \
  http://127.0.0.1:8000/poc_asan.html \
  2>&1 | tee /tmp/promise-finally-chrome-asan.txt

Exploitation

This bug is exploitable to get a shell.

Build a release d8 with sandbox off

is_debug = false
is_component_build = false
target_cpu = "x64"
v8_target_cpu = "x64"
is_asan = false
is_lsan = false
use_remoteexec = false
use_siso = false
symbol_level = 2
v8_symbol_level = 2
dcheck_always_on = false
v8_enable_slow_dchecks = false
v8_enable_test_features = true
v8_enable_sandbox = false
v8_enable_pointer_compression = true
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_verify_heap = false
v8_enable_memory_corruption_api = false

Then run

./d8 \
  --predictable \
  --single-threaded \
  --no-concurrent-recompilation \
  --no-concurrent-marking \
  --allow-natives-syntax \
  ./poc_getshll.js

You will get a shell

./d8 \                                                          
  --predictable \
  --single-threaded \
  --no-concurrent-recompilation \
  --no-concurrent-marking \
  --allow-natives-syntax \
  ./poc_getshll.js
building fixed-phase large-object layout
layout 1 / 1
leaked constructable Promise finally helper
warming pointer-only field path
warming optimized access paths 1 / 3
warming optimized access paths 2 / 3
warming optimized access paths 3 / 3
optimized access paths warmed
forging fake-array holders through the Promise construct confusion
packed-double map 0x100d141
arbitrary in-cage read/write self-test passed
generic addrof/fakeobj identity self-test passed
d8 base 0x5658627c6000
Shell::System 0x565864112f60
launching interactive /bin/sh through the corrupted API callback
$ echo hello
hello

Impact analysis

RCE in render process.


The cause

What version of Chrome have you found the security issue in?

commit d712c14d603b9a96757629c6c0d61012a9665c0d

Yes, it is related to a crash.

Choose the type of vulnerability

Remote Code Execution (RCE)

How would you like to be publicly acknowledged for your report?

Zhenpeng (Leo) Lin at depthfirst

View on issue tracker