From 6eb11bf7f4affc14bd0d4c86268ba078aab6d871 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:29:24 -0700 Subject: [PATCH] Pin frame records that are arms of record unions in the corpus A record shape that is an arm of a union of records carries a hidden shape word at struct index 1, so its fields start at index 2. The new program keeps such records in frame storage while they own string and array fields, reads and writes those fields, passes the shapes through the union, compares identities and serializes them. With the frame storage's reference-field slots addressed at index + 1 instead of the shape's field base, the compiled program segfaults in the plain lane and ASan reports a heap-buffer-overflow in the sanitized one. Co-Authored-By: Claude Opus 5.5 --- ...997-frame-records-in-record-unions.ts.json | 6 ++ .../5997-frame-records-in-record-unions.ts | 88 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5997-frame-records-in-record-unions.ts.json create mode 100644 tests/corpus/5997-frame-records-in-record-unions.ts diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5997-frame-records-in-record-unions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5997-frame-records-in-record-unions.ts.json new file mode 100644 index 000000000..7e3fb1c50 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5997-frame-records-in-record-unions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5997-frame-records-in-record-unions.ts" + ], + "diags": [] +} diff --git a/tests/corpus/5997-frame-records-in-record-unions.ts b/tests/corpus/5997-frame-records-in-record-unions.ts new file mode 100644 index 000000000..c066c3edb --- /dev/null +++ b/tests/corpus/5997-frame-records-in-record-unions.ts @@ -0,0 +1,88 @@ +// Records that are arms of a union of records and also live in frame +// storage. An arm of such a union carries a hidden shape word before its +// fields, so every field address has to skip it: the reference fields a +// frame record owns are read, overwritten and released through the same +// slots as heap records of the shape. The same shapes also flow through +// the union, are compared by identity and are serialized. +interface Ident { + kind: "ident"; + name: string; + tags: string[]; +} +interface Call { + kind: "call"; + callee: string; + args: Ident[]; +} +type Node = Ident | Call; + +const words = ["alpha", "beta", "gamma", "delta", "epsilon"]; +function word(i: number): string { + return words[i % words.length]! + i; +} + +function describe(node: Node): string { + switch (node.kind) { + case "ident": + return `${node.name}[${node.tags.join(",")}]`; + case "call": + return `${node.callee}(${node.args.map((a) => a.name).join(", ")})`; + } +} +function weight(node: Node): number { + return node.kind === "ident" ? node.name.length + node.tags.length : node.args.length * 10; +} + +// Heap values of the union: the arms are tagged shapes. +const nodes: Node[] = []; +for (let i = 0; i < 6; i++) { + const id: Ident = { kind: "ident", name: word(i), tags: [word(i + 1)] }; + nodes.push(i % 2 === 0 ? id : { kind: "call", callee: word(i + 2), args: [id, id] }); +} +console.log(nodes.map(describe).join(" | ")); + +// Frame records of the arms that only flow into field reads. +let total = 0; +let log = ""; +for (let i = 0; i < 10; i++) { + const w = word(i); + const id: Ident = { kind: "ident", name: w + "!", tags: [w, word(i + 3)] }; + total += id.name.length + id.tags.length + id.tags[1]!.length; + const call: Call = { kind: "call", callee: w.toUpperCase(), args: [] }; + total += call.callee.length + call.args.length; + log += id.kind[0]! + call.kind[0]! + id.name[0]!; +} +console.log(total, log); + +// A reassigned frame binding: each new value is built apart and copied over +// the slot, and the replaced reference fields are released. +let cur: Ident = { kind: "ident", name: word(0), tags: [] }; +for (let i = 1; i < 8; i++) { + cur = { kind: "ident", name: cur.name.slice(0, 3) + word(i), tags: [...cur.tags, cur.name] }; + if (i % 3 === 0) cur = { kind: "ident", name: cur.tags[0] ?? "none", tags: cur.tags.slice(1) }; +} +console.log(cur.name, cur.tags.length, cur.tags.join("+")); + +// Field writes into a frame record's reference slots. +for (let i = 0; i < 4; i++) { + const id: Ident = { kind: "ident", name: word(i), tags: [word(i)] }; + id.name = id.name + "/" + i; + id.tags = [id.name, ...id.tags]; + id.tags.push(word(i + 7)); + console.log(id.name, id.tags.join(" ")); +} + +// Through the union: describe and weigh by union, keep identity, and +// serialize the arms and the union values. +const a: Ident = { kind: "ident", name: "x", tags: ["t1", "t2"] }; +const b: Call = { kind: "call", callee: "f", args: [a] }; +const asNode: Node = a; +const picked: Node = weight(b) > weight(a) ? b : a; +console.log(describe(asNode), describe(b), weight(asNode), weight(picked)); +console.log(asNode === a, picked === b, b.args[0] === a, (picked as Call).args[0] === asNode); +a.tags.push("t3"); +console.log(describe(asNode), describe(picked)); +console.log(JSON.stringify(a), JSON.stringify(picked)); +console.log(JSON.stringify(nodes.slice(0, 2))); +const mixed: Node[] = [a, b, nodes[1]!]; +console.log(mixed.filter((n) => n.kind === "ident").length, mixed.indexOf(asNode), mixed.indexOf(b));