CVE-2026-15770
Overview
Background
- TurboFan instruction selector
- the V8 backend pass that lowers scheduled IR nodes into machine instructions, with per-architecture files
instruction-selector-arm.ccandinstruction-selector-arm64.cc. - Flag-setting binop
- an arithmetic operation (e.g.
kExprI32Add,kExprI32Sub) whose result also sets the condition flags, letting a following compare be folded away viaMapForFlagSettingBinop. - `FlagsContinuation` (`cont`)
- the object describing how flag results are consumed, which may be a branch (
cont->IsBranch()), a set (cont->IsSet()), a deoptimize, or a trap. - `IsOnlyUserOfNodeInSameBlock`
- a selector query returning whether the comparison is the sole in-block user of the binop
*node, used to justify folding the binop into the continuation.
Root Cause Analysis
When lowering a comparison against zero, the selector tried to fold a flag-setting binop directly into the FlagsContinuation whenever IsOnlyUserOfNodeInSameBlock(*node, binop) held, without checking what kind of continuation was in play. For continuations that are not a branch or a set (for example a trap or deoptimize), the selector may have already emitted instructions after exploring through the original conditional-setting instruction, so tying the binop to the continuation forces the flag-setting instruction to be pulled past an existing use. This breaks the emission schedule: the register that should hold the binop result is read before the flag-setting instruction that defines it has run, so a garbage/uninitialized register value is consumed.
The fix gates the fold with (cont->IsBranch() || cont->IsSet()), so the optimization only applies to continuation kinds that cannot reorder the flag-setting instruction past its users, preserving the invariant that a value is defined before it is used.
IsBranch()/IsSet() continuations, the only ones where moving the flag-setting instruction cannot cross a prior use and corrupt the schedule.Attack Path
- Craft a WebAssembly module
Build a function whose body computes
v2 = param + 1andv27 = 0 - v2, both flag-setting binops feeding later uses, as inregress-524792614.js. - Force block splitting
Insert an
kExprIfblock so aReturnuse of the binop is pushed into a following basic block, creating cross-block users while the compare stays in-block. - Trigger optimizing compilation
Instantiate and call
main, then invoke%WasmTierUpFunctionto run TurboFan and reach the vulnerable selector path on arm/arm64. - Mis-fold the binop The selector folds the flag-setting binop into a non-branch/non-set continuation, scheduling the flag-setting instruction after a use of its result.
- Consume uninitialized register The generated code reads the target register before it is defined, yielding uninitialized memory used as a program value.
Impact Assessment
Files Changed
src/compiler/backend/arm/instruction-selector-arm.ccsrc/compiler/backend/arm64/instruction-selector-arm64.cctest/mjsunit/regress/wasm/regress-524792614.js
Audit Directions
- Continuation-kind assumptionsReview other
FlagsContinuationfolds that assume any continuation is safe; each should verifyIsBranch()/IsSet()before moving a flag-setting instruction. - In-block-user rewritesAudit uses of
IsOnlyUserOfNodeInSameBlock(and similar single-user checks) that reorder defining instructions, ensuring the move cannot cross an existing use. - Per-architecture parityWhen one instruction selector is patched, check the sibling backends (arm, arm64, x64, ia32, etc.) for the same unguarded pattern, as this fix spanned both arm files.
Patch
From 78afe7b553f2befacffd4fe882e0947484b093d2 Mon Sep 17 00:00:00 2001
From: Sam Parker <sam.parker@arm.com>
Date: Tue, 07 Jul 2026 10:11:14 +0100
Subject: [PATCH] [compiler][arm64][arm] IsOnlyUserOfNodeInSameBlock
Don't try to use IsOnlyUserOfNodeInSameBlock when the continuation is
anything but a branch or set as it's possible that we've already
explored through the original conditional setting instruction and are
trying to optimise a binop that would then have to be pulled past a
use to be tied to the continuation.
Bug: 524792614
Change-Id: I94a3c46799197c152873ea9f09e99ff4db4efccd
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8024753
Reviewed-by: Nico Hartmann <nicohartmann@chromium.org>
Commit-Queue: Sam Parker-Haynes <sam.parker@arm.com>
Reviewed-by: Daniel Lehmann <dlehmann@chromium.org>
Cr-Commit-Position: refs/heads/main@{#108482}
---
diff --git a/src/compiler/backend/arm/instruction-selector-arm.cc b/src/compiler/backend/arm/instruction-selector-arm.cc
index 5053420..3e56d63 100644
--- a/src/compiler/backend/arm/instruction-selector-arm.cc
+++ b/src/compiler/backend/arm/instruction-selector-arm.cc
@@ -2061,10 +2061,13 @@
cont->Overwrite(MapForFlagSettingBinop(cond));
*opcode = no_output_opcode;
*node = binop;
- } else if (selector->IsOnlyUserOfNodeInSameBlock(*node, binop)) {
- // We can also handle the case where the {node} and the comparison are in
- // the same basic block, and the comparison is the only user of {node} in
- // this basic block ({node} has users in other basic blocks).
+ } else if ((cont->IsBranch() || cont->IsSet()) &&
+ selector->IsOnlyUserOfNodeInSameBlock(*node, binop)) {
+ // We can also handle the case where the add and the compare are in the
+ // same basic block, and the compare is the only use of add in this basic
+ // block (the add has users in other basic blocks). We only do this for
+ // branches and sets as they can't end up breaking the schedule by pulling
+ // the flag-setting instruction past its users.
cont->Overwrite(MapForFlagSettingBinop(cond));
*opcode = binop_opcode;
*node = binop;
diff --git a/src/compiler/backend/arm64/instruction-selector-arm64.cc b/src/compiler/backend/arm64/instruction-selector-arm64.cc
index 7fae7f1..9cf8cbe 100644
--- a/src/compiler/backend/arm64/instruction-selector-arm64.cc
+++ b/src/compiler/backend/arm64/instruction-selector-arm64.cc
@@ -3553,10 +3553,13 @@
*opcode = no_output_opcode;
*node = binop;
*immediate_mode = binop_immediate_mode;
- } else if (selector->IsOnlyUserOfNodeInSameBlock(*node, binop)) {
+ } else if ((cont->IsBranch() || cont->IsSet()) &&
+ selector->IsOnlyUserOfNodeInSameBlock(*node, binop)) {
// We can also handle the case where the add and the compare are in the
// same basic block, and the compare is the only use of add in this basic
- // block (the add has users in other basic blocks).
+ // block (the add has users in other basic blocks). We only do this for
+ // branches and sets as they can't end up breaking the schedule by pulling
+ // the flag-setting instruction past its users.
cont->Overwrite(MapForFlagSettingBinop(cond));
*opcode = binop_opcode;
*node = binop;
diff --git a/test/mjsunit/regress/wasm/regress-524792614.js b/test/mjsunit/regress/wasm/regress-524792614.js
new file mode 100644
index 0000000..9a90f23
--- /dev/null
+++ b/test/mjsunit/regress/wasm/regress-524792614.js
@@ -0,0 +1,61 @@
+// 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: --allow-natives-syntax
+
+d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');
+
+const builder = new WasmModuleBuilder();
+const g = builder.addGlobal(kWasmI32, true);
+
+builder.addFunction('main', makeSig([kWasmI32], [kWasmI32, kWasmI32]))
+ .exportAs('main')
+ .addLocals(kWasmI32, 2)
+ .addBody([
+ // v2 = param 0 + 1
+ kExprLocalGet,
+ 0,
+ kExprI32Const,
+ 1,
+ kExprI32Add,
+ kExprLocalSet,
+ 1,
+
+ // v27 = 0 - v2
+ kExprI32Const,
+ 0,
+ kExprLocalGet,
+ 1,
+ kExprI32Sub,
+ kExprLocalSet,
+ 2,
+
+ // select(v27, 0, v27)
+ kExprLocalGet,
+ 2,
+ kExprI32Const,
+ 0,
+ kExprLocalGet,
+ 2,
+ kExprSelect,
+
+ // If block to split the block (pushes Return use to next block)
+ kExprLocalGet,
+ 0,
+ kExprIf,
+ kWasmVoid,
+ kExprI32Const,
+ 0,
+ kExprGlobalSet,
+ g.index,
+ kExprEnd,
+
+ // Return v6, v2
+ kExprLocalGet,
+ 1,
+ ]);
+
+const instance = builder.instantiate()
+instance.exports.main();
+%WasmTierUpFunction(instance.exports.main);
Regression Test / PoC
diff --git a/test/mjsunit/regress/wasm/regress-524792614.js b/test/mjsunit/regress/wasm/regress-524792614.js
new file mode 100644
index 0000000..9a90f23
--- /dev/null
+++ b/test/mjsunit/regress/wasm/regress-524792614.js
@@ -0,0 +1,61 @@
+// 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: --allow-natives-syntax
+
+d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');
+
+const builder = new WasmModuleBuilder();
+const g = builder.addGlobal(kWasmI32, true);
+
+builder.addFunction('main', makeSig([kWasmI32], [kWasmI32, kWasmI32]))
+ .exportAs('main')
+ .addLocals(kWasmI32, 2)
+ .addBody([
+ // v2 = param 0 + 1
+ kExprLocalGet,
+ 0,
+ kExprI32Const,
+ 1,
+ kExprI32Add,
+ kExprLocalSet,
+ 1,
+
+ // v27 = 0 - v2
+ kExprI32Const,
+ 0,
+ kExprLocalGet,
+ 1,
+ kExprI32Sub,
+ kExprLocalSet,
+ 2,
+
+ // select(v27, 0, v27)
+ kExprLocalGet,
+ 2,
+ kExprI32Const,
+ 0,
+ kExprLocalGet,
+ 2,
+ kExprSelect,
+
+ // If block to split the block (pushes Return use to next block)
+ kExprLocalGet,
+ 0,
+ kExprIf,
+ kWasmVoid,
+ kExprI32Const,
+ 0,
+ kExprGlobalSet,
+ g.index,
+ kExprEnd,
+
+ // Return v6, v2
+ kExprLocalGet,
+ 1,
+ ]);
+
+const instance = builder.instantiate()
+instance.exports.main();
+%WasmTierUpFunction(instance.exports.main);
Original Bug Report
DCHECK failure in NextIntervalStartsInDifferentBlocks(*interval, *next_interval) in register-alloc
Detailed Report: https://clusterfuzz.com/testcase?key=4840004635361280
Fuzzer: None Job Type: linux_asan_d8_v8_arm64_dbg Platform Id: linux
Crash Type: DCHECK failure Crash Address: Crash State: NextIntervalStartsInDifferentBlocks(*interval, *next_interval) in register-alloc v8::internal::compiler::LiveRangeBuilder::Verify auto v8::internal::compiler::turboshaft::Pipeline::Run<v8::internal::compiler::t
Sanitizer: address (ASAN)
Regressed: https://clusterfuzz.com/revisions?job=linux_asan_d8_v8_arm64_dbg&range=99794:99795
Reproducer Testcase: https://clusterfuzz.com/download?testcase_id=4840004635361280
Issue filed automatically.
To reproduce this, please build the target in this report and run it against the reproducer testcase. Please use the GN arguments provided at bottom of this report when building the binary.
If you have trouble reproducing, please also export the environment variables listed under “[Environment]” in the crash stacktrace.
If you have any feedback on reproducing test cases, let us know at https://forms.gle/Yh3qCYFveHj6E5jz5 so we can improve.