Keep a spread tuple alive until all its fields are packed into the rest

A spread tuple's first field carries the statements that evaluate the
tuple into a hidden local, and its later fields read that local. When
lowerArraySpreadItems stored the first field as one item's expression,
the local's frame ended with that store, so a tuple returned by a call
was released before its later fields were read (a use-after-free under
SCRIPTC_SAN=1). The field's statements now join the packing sequence at
the item's position, whose frame lasts until every item is stored.
This commit is contained in:
Chris Tate
2026-10-25 05:02:40 -05:00
parent 534c08dd2f
commit f8b1f9ffc3
2 changed files with 17 additions and 1 deletions
@@ -174,7 +174,15 @@ export function lowerArraySpreadItems(
];
for (const node of nodes) {
if (!(node instanceof AstNode)) {
const value = undefinedForNullish(lowerer, node, arrType);
// A spread tuple's first field declares the tuple's storage, which
// the later fields read: its statements join this sequence, whose
// frame lasts until every item is stored.
let item = node;
if (item.kind === "seqExpr") {
body.push(...item.stmts);
item = item.result;
}
const value = undefinedForNullish(lowerer, item, arrType);
const stored =
lowerer.runtimeOptionalWidening(value.type, elem) !== null ||
optionalElementValue(lowerer, value, elem)
@@ -58,6 +58,14 @@ const mixedChanging: [boolean, boolean | undefined] = [true, undefined];
console.log(flags(...mixedChanging, ...mixedMore, flip(mixedChanging, true), ...mixedChanging));
const mixedWords: [string, string | undefined] = ["a", undefined];
console.log(label("z", ...["b", undefined, "c"], ...mixedWords), label("y", ...mixedWords, ...new Set(["s", undefined])));
// A tuple returned by a call keeps its string fields alive until every
// field is packed beside the other spreads.
function makeTuple(n: number): [string, string | undefined, string] {
return ["p" + n.toString(16).repeat(4), n % 2 ? undefined : "q" + n, "r".repeat((n % 7) + 3) + n];
}
for (let i = 0; i < 3; i++) {
console.log(label("z", ...["m", undefined], ...makeTuple(i), ...mixedWords), label("y", ...makeTuple(i + 10), ...new Set(["s" + i])));
}
function pick(start: (number | undefined)[] | string[], other: (number | undefined)[] | string[]): string {
let a = start;