High watch CVSS 8.8 chrome Type Confusion 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType confusion in V8
DescriptionType confusion in V8
ComponentV8
Sub-componentWASM
Bug ClassType Confusion
Action tierwatch — dos-class only — no demonstrated corruption path
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.
Tracker560536735
Fix commit65acd1f04328 (v8/v8) +48/-1
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
CreditedOpenAI Codex Security (amyb)
Reported2026-09-12 (per vendor advisory)
Fix landed2026-09-17 (public git)
Disclosed2026-09-22
Patch gap5 days — fix public in source before the advisory

Background

`WASM_LIFTOFF_SETUP` frame
a transient V8 stack frame built by Generate_WasmLiftoffFrameSetup to run the Liftoff feedback-vector setup builtin before a wasm function body executes.
PAC (Pointer Authentication)
an arm64 control-flow-integrity feature where return addresses / code pointers are signed and must be authenticated before use; authenticating an unsigned value aborts the process.
`ComputeCallerState()`
a Frame method that reconstructs the caller’s State, including callee_pc, while the stack profiler walks frames.
`HasTaggedOutgoingParams()`
the sole consumer of this frame’s callee_pc, which uses it only as a lookup key to decide GC-tagging of outgoing parameters, never as a jump/return target.

Root Cause Analysis

Generate_WasmLiftoffFrameSetup writes the WASM_LIFTOFF_SETUP type marker into the frame several instructions before it signs and spills the calling PC into kCallingPCOffset. This opens a window in which the frame is already correctly typed but its calling-PC slot still holds an unsigned value. If a sampling-profiler tick interrupts execution inside that window, the profiler walks the stack, matches the frame type, and calls WasmLiftoffSetupFrame::ComputeCallerState(). The old code set state->callee_pc = CallingPC(), which authenticates the slot as a signed pointer. Because the slot is not yet signed, PAC authentication fails and aborts the process on arm64. The tracker records this as a type confusion in the frame’s PC-slot state: a frame whose type says “signed PC present” while the slot actually holds a raw, unsigned value.
Key insight
The calling-PC slot is only ever used as a lookup key in HasTaggedOutgoingParams(), never dereferenced or branched to, so it can be read unauthenticated with unauthenticated_pc() and the missing/invalid signature is harmless.

Attack Path

  1. Run Liftoff-tiered wasm Execute many wasm functions so each enters Generate_WasmLiftoffFrameSetup and repeatedly hits the feedback-vector setup builtin.
  2. Hit the unsigned window For each call, the frame is marked WASM_LIFTOFF_SETUP before the calling PC is signed and spilled to kCallingPCOffset.
  3. Land a profiler tick A stack-sampling profiler fires inside that window and walks the interrupted stack.
  4. Authenticate an unsigned slot ComputeCallerState() calls CallingPC(), which PAC-authenticates the still-unsigned slot and fails.
  5. Abort on arm64 The failed authentication triggers a CFI abort, crashing the process.

Impact Assessment

On arm64 with PAC enabled, the observable effect is a control-flow-integrity abort (process crash) triggered when a profiler samples during the narrow setup window, so the practical impact is denial of service / reliability rather than a demonstrated exploitable primitive. The tracker classifies it as a type confusion because the frame’s declared type implies a signed PC while the slot still holds an unsigned value. The condition requires both profiling to be active and precise timing against the setup builtin, which constrains reachability.

Changed Functions

FunctionChangeNotes
for
test/mjsunit/regress/wasm/regress-562455563.js
modified

Files Changed

  • src/execution/frames.cc
  • test/mjsunit/regress/wasm/regress-562455563.js
  • test/mjsunit/mjsunit.status

Audit Directions

  • Type-marker vs. field-init ordering
    Audit builtins/frame constructors that set a frame type marker before fully populating the frame’s fields; a profiler or GC walking that window may consume half-initialized state.
  • Authenticated reads of signable slots
    Look for other ComputeCallerState() / frame accessors that call authenticating helpers (e.g. CallingPC(), signed-PC reads) on slots that may be transiently unsigned, and switch to unauthenticated_pc() where the value is only a lookup key.
  • Profiler-reentrancy on transient frames
    Review asynchronously sampled frame types for windows where the stack-walk assumptions (signed PCs, populated slots) do not yet hold.
From 65acd1f04328e65e9eecd67f8119d123c35c0600 Mon Sep 17 00:00:00 2001
From: Daniel Lehmann <dlehmann@chromium.org>
Date: Thu, 17 Sep 2026 12:09:13 +0200
Subject: [PATCH] Reland "[wasm] Don't authenticate LiftoffSetupFrame calling PC"

This is a reland of commit 3640a862c8e6638fe42235df945b29003d000bd2

Skip the new regression test under `sandbox_hardware_support` in
mjsunit.status, as it uses `console.profile()` which is unsupported
with PKU on Linux kernels older than 6.12 (crbug.com/429173713).

Original change's description:
> [wasm] Don't authenticate LiftoffSetupFrame calling PC
>
> Generate_WasmLiftoffFrameSetup sets the WASM_LIFTOFF_SETUP frame marker
> a few instructions before it signs and spills the calling PC. A profiler
> tick in that window sees a correctly typed frame whose calling PC slot
> is still unsigned, and ComputeCallerState() authenticated it, which
> aborts on arm64 with control flow integrity.
>
> Read the slot unauthenticated instead. It only serves as a lookup key in
> HasTaggedOutgoingParams(), never as a branch target.
>
> Bug: 560536735
> Fixed: 562455563

TAG=agy
CONV=b2d96b5c-8e0b-4e74-8be9-54627a91f304

Bug: 560536735
Change-Id: I878562ab376eee8ca6c52accb4e3905bb6ffc266
Fixed: 562455563
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8420943
Auto-Submit: Daniel Lehmann <dlehmann@chromium.org>
Commit-Queue: Matthias Liedtke <mliedtke@chromium.org>
Owners-Override: Matthias Liedtke <mliedtke@chromium.org>
Reviewed-by: Jakob Kummerow <jkummerow@chromium.org>
Commit-Queue: Daniel Lehmann <dlehmann@chromium.org>
Reviewed-by: Matthias Liedtke <mliedtke@chromium.org>
Cr-Commit-Position: refs/heads/main@{#109909}
---
diff --git a/src/execution/frames.cc b/src/execution/frames.cc
index 329e0bb..51055f1 100644
--- a/src/execution/frames.cc
+++ b/src/execution/frames.cc
@@ -3976,7 +3976,13 @@
 
 void WasmLiftoffSetupFrame::ComputeCallerState(State* state) const {
   TypedFrame::ComputeCallerState(state);
-  state->callee_pc = CallingPC();
+  // Unlike {CallingPC}, which only runs at safepoints, this can run on a
+  // profiler tick: the setup builtin sets the frame marker a few instructions
+  // before it signs and spills the calling PC, so the slot can still be
+  // unsigned here. Don't authenticate it; {callee_pc} is only a lookup key,
+  // never a branch target.
+  state->callee_pc = unauthenticated_pc(reinterpret_cast<Address*>(
+      fp() + WasmLiftoffSetupFrameConstants::kCallingPCOffset));
 }
 
 void WasmLiftoffSetupFrame::Iterate(RootVisitor* v) const {
diff --git a/test/mjsunit/mjsunit.status b/test/mjsunit/mjsunit.status
index d36924d..c202b92 100644
--- a/test/mjsunit/mjsunit.status
+++ b/test/mjsunit/mjsunit.status
@@ -3305,6 +3305,7 @@
   'regress/wasm/regress-392928805': [SKIP],
   'regress/wasm/regress-406043349': [SKIP],
   'regress/wasm/regress-413963387': [SKIP],
+  'regress/wasm/regress-562455563': [SKIP],
   'wasm/interrupt-worker-with-perf': [SKIP],
   'wasm/liftoff-trap-handler': [SKIP],
   'wasm/log-code-after-post-message': [SKIP],
diff --git a/test/mjsunit/regress/wasm/regress-562455563.js b/test/mjsunit/regress/wasm/regress-562455563.js
new file mode 100644
index 0000000..28f82f9
--- /dev/null
+++ b/test/mjsunit/regress/wasm/regress-562455563.js
@@ -0,0 +1,40 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --cpu-profiler-sampling-interval=1
+
+d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');
+
+// Profile while many Wasm functions run the Liftoff feedback vector setup
+// builtin for the first time. That builtin sets the WASM_LIFTOFF_SETUP frame
+// marker a few instructions before it spills its calling PC, and a tick landing
+// in that window used to abort on arm64 with control flow integrity.
+
+const kNumFunctions = 100;
+
+const builder = new WasmModuleBuilder();
+const $sig_v_v = builder.addType(kSig_v_v);
+// Callee of the unreachable call below, which is what makes the functions
+// require a feedback vector in the first place.
+const $dummy = builder.addFunction('dummy', $sig_v_v).addBody([]);
+
+let main_body = [];
+for (let i = 0; i < kNumFunctions; i++) {
+  const callee = builder.addFunction(undefined, $sig_v_v).addBody([
+    kExprI32Const, 0,
+    kExprIf, kWasmVoid,
+      // An unreachable call nevertheless requires a feedback vector entry.
+      kExprCallFunction, $dummy.index,
+    kExprEnd,
+  ]);
+  main_body.push(kExprCallFunction, ...wasmUnsignedLeb(callee.index));
+}
+builder.addFunction('main', $sig_v_v).exportFunc().addBody(main_body);
+
+const instance = builder.instantiate();
+
+// Note: no {console.profileEnd()}, which would dump the profile into a
+// "v8.prof" file in the current working directory.
+console.profile();
+instance.exports.main();
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/mjsunit/mjsunit.status b/test/mjsunit/mjsunit.status
index d36924d..c202b92 100644
--- a/test/mjsunit/mjsunit.status
+++ b/test/mjsunit/mjsunit.status
@@ -3305,6 +3305,7 @@
   'regress/wasm/regress-392928805': [SKIP],
   'regress/wasm/regress-406043349': [SKIP],
   'regress/wasm/regress-413963387': [SKIP],
+  'regress/wasm/regress-562455563': [SKIP],
   'wasm/interrupt-worker-with-perf': [SKIP],
   'wasm/liftoff-trap-handler': [SKIP],
   'wasm/log-code-after-post-message': [SKIP],
diff --git a/test/mjsunit/regress/wasm/regress-562455563.js b/test/mjsunit/regress/wasm/regress-562455563.js
new file mode 100644
index 0000000..28f82f9
--- /dev/null
+++ b/test/mjsunit/regress/wasm/regress-562455563.js
@@ -0,0 +1,40 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --cpu-profiler-sampling-interval=1
+
+d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');
+
+// Profile while many Wasm functions run the Liftoff feedback vector setup
+// builtin for the first time. That builtin sets the WASM_LIFTOFF_SETUP frame
+// marker a few instructions before it spills its calling PC, and a tick landing
+// in that window used to abort on arm64 with control flow integrity.
+
+const kNumFunctions = 100;
+
+const builder = new WasmModuleBuilder();
+const $sig_v_v = builder.addType(kSig_v_v);
+// Callee of the unreachable call below, which is what makes the functions
+// require a feedback vector in the first place.
+const $dummy = builder.addFunction('dummy', $sig_v_v).addBody([]);
+
+let main_body = [];
+for (let i = 0; i < kNumFunctions; i++) {
+  const callee = builder.addFunction(undefined, $sig_v_v).addBody([
+    kExprI32Const, 0,
+    kExprIf, kWasmVoid,
+      // An unreachable call nevertheless requires a feedback vector entry.
+      kExprCallFunction, $dummy.index,
+    kExprEnd,
+  ]);
+  main_body.push(kExprCallFunction, ...wasmUnsignedLeb(callee.index));
+}
+builder.addFunction('main', $sig_v_v).exportFunc().addBody(main_body);
+
+const instance = builder.instantiate();
+
+// Note: no {console.profileEnd()}, which would dump the profile into a
+// "v8.prof" file in the current working directory.
+console.profile();
+instance.exports.main();
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.