diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index f3a444ea98..149c348d04 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -21,6 +21,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation | | `json-unions` | cpu | `JSON.parse` into discriminated unions: an event log with eight kinds and nested expression trees with nine, evaluated after the cast | | `keyed-option-writes` | cpu | class methods whose names are also keys of option records written through element accesses: `enabled[name] = true` over a union of literal names and `totals["count"] = n` | +| `layered-errors` | cpu | validation errors thrown below a stack of small functions that hold strings and arrays, and caught per entry in a loop: about half of 640,000 entries fail | | `line-reader` | cpu | `readline` over a file stream: 'line' events for a 67 MB access log (every 50th line not ASCII), with a status count, a path tally and byte totals | | `log-lines` | cpu | 200k immediate `console.log`/`process.stdout.write` lines into the runner's pipe | | `log-summary`, `inventory-report` | cpu | the multi-module applications from `benchmarks/builds`, on 300k-line inputs | diff --git a/benchmarks/runtime/workloads/layered-errors.json b/benchmarks/runtime/workloads/layered-errors.json new file mode 100644 index 0000000000..70971552c4 --- /dev/null +++ b/benchmarks/runtime/workloads/layered-errors.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/layered-errors.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/layered-errors.ts b/benchmarks/runtime/workloads/layered-errors.ts new file mode 100644 index 0000000000..bf2ff581cb --- /dev/null +++ b/benchmarks/runtime/workloads/layered-errors.ts @@ -0,0 +1,95 @@ +// A configuration loader whose validation errors are thrown at the bottom +// of a stack of small layers and caught per entry at the top: each layer +// holds strings and arrays while the error passes through it. +class ConfigError extends Error { + readonly key: string; + constructor(key: string, message: string) { + super(message); + this.key = key; + } +} + +let seed = 4242; +function random(): number { + seed = (Math.imul(seed, 1103515245) + 12345) >>> 0; + return seed / 4294967296; +} + +function fail(key: string, text: string): never { + throw new ConfigError(key, `invalid value "${text}" for ${key}`); +} +function digits(key: string, text: string): number { + let value = 0; + for (let i = 0; i < text.length; i++) { + const c = text.charCodeAt(i); + if (c < 48 || c > 57) fail(key, text); + value = value * 10 + (c - 48); + } + return value; +} +function parseNumber(key: string, text: string): number { + return text.length === 0 ? fail(key, text) : digits(key, text); +} +function checkRange(key: string, text: string, max: number): number { + const value = parseNumber(key, text); + return value > max ? fail(key, text) : value; +} +function coerce(key: string, raw: string, max: number): number { + const trimmed = raw.trim(); + const parts = [key, trimmed]; + const value = checkRange(key, trimmed, max); + return value + parts.length; +} +function resolveValue(key: string, raw: string, defaults: Map): number { + const text = raw === "default" ? (defaults.get(key) ?? "") : raw; + const label = "value:" + key; + return coerce(key, text, 100000) + label.length; +} +function loadEntry(section: string, entry: string, defaults: Map): number { + const eq = entry.indexOf("="); + const key = section + "." + entry.slice(0, eq); + const path = [section, key]; + return resolveValue(key, entry.slice(eq + 1), defaults) + path.length; +} +function loadEntryWithContext(section: string, entry: string, defaults: Map): number { + const context = `${section}/${entry.length}`; + return loadEntry(section, entry, defaults) + context.length; +} + +function makeEntry(i: number): string { + const r = random(); + const name = "k" + (i % 97); + if (r < 0.3) return `${name}=x${i}`; + if (r < 0.4) return `${name}=`; + if (r < 0.5) return `${name}=${200000 + i}`; + if (r < 0.55) return `${name}=default`; + return `${name}= ${Math.floor(random() * 90000)} `; +} + +const rounds = Number(process.argv[2] ?? "1"); +const defaults = new Map(); +for (let i = 0; i < 97; i += 2) defaults.set(`net.k${i}`, String(i * 10)); +const sections = ["net", "db", "cache", "auth"]; +const entries: string[] = []; +for (let i = 0; i < 4000; i++) entries.push(makeEntry(i)); + +let loaded = 0; +let sum = 0; +let failed = 0; +const failedKeys = new Map(); +for (let round = 0; round < rounds * 40; round++) { + for (const section of sections) { + for (const entry of entries) { + try { + sum += loadEntryWithContext(section, entry, defaults); + loaded++; + } catch (error) { + if (!(error instanceof ConfigError)) throw error; + failed++; + failedKeys.set(error.key, (failedKeys.get(error.key) ?? 0) + 1); + } + } + } +} +console.log(`loaded ${loaded} failed ${failed} sum ${sum} distinct failing keys ${failedKeys.size}`); +console.log(`net.k0 failures ${failedKeys.get("net.k0") ?? 0}`); diff --git a/packages/compiler/src/backend/llvm/blocks.ts b/packages/compiler/src/backend/llvm/blocks.ts index 3ae65cb0f0..2aef040a19 100644 --- a/packages/compiler/src/backend/llvm/blocks.ts +++ b/packages/compiler/src/backend/llvm/blocks.ts @@ -97,12 +97,22 @@ export class BlockBuilder { if (s === UNWIND_RESUME) { const exception = this.tmp(); this.line(`${exception} = load { ptr, i32 }, ptr ${UNWIND_SLOT}`); - s = `resume { ptr, i32 } ${exception}`; + this.resumeUnwind(exception); + return; } this.cur.term = ` ${this.withDebugLocation(s)}`; } } + /** Ends the block by continuing an unwind with `exception`, the value + * of a landing pad (UNWIND_RESUME_DECLARATION). */ + resumeUnwind(exception: string): void { + const token = this.tmp(); + this.line(`${token} = extractvalue { ptr, i32 } ${exception}, 0`); + this.line(`call void @scr_unwind_resume(ptr ${token})`); + this.terminate("unreachable"); + } + br(label: string, attachment = ""): void { this.terminate(`br label %${label}${attachment}`); } diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index 02c12305c5..aab4459e0c 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -62,7 +62,7 @@ import { emitIntegerClone, emitIntegerDispatcher } from "./integer-clones.js"; import { beginFunctionLoops, LOOPS } from "./loop-state.js"; import { runningExtremumUpdates } from "./running-extrema.js"; import { findIntegerOrNaNLocals } from "./wrapping-sums.js"; -import { UNWIND_PERSONALITY, UNWIND_SLOT } from "./unwind-abi.js"; +import { UNWIND_PERSONALITY, UNWIND_RESUME_DECLARATION, UNWIND_SLOT } from "./unwind-abi.js"; export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { const B = new BlockBuilder(); @@ -681,7 +681,10 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { emitter.privateGlobalSlots.clear(); // Landing pads (emitDirectCall) need the personality. const personality = emitter.usesLandingPads ? ` ${UNWIND_PERSONALITY}` : ""; - if (emitter.usesLandingPads) emitter.declare(`declare i32 @__gcc_personality_v0(...)`); + if (emitter.usesLandingPads) { + emitter.declare(`declare i32 @__gcc_personality_v0(...)`); + emitter.declare(UNWIND_RESUME_DECLARATION); + } let body = `define internal ${ret} @${symbol}${int32Abi ? INT32_ABI_SUFFIX : ""}(${params.join(", ")}) ${attrs}${personality}${debug} { ; ${fn.name}\n${B.render()}\n}`; if (int32Abi) body += "\n" + emitter.emitInt32AbiWrapper(fn, symbol, int32Abi); if (emitter.integerClones.members.has(fn.name)) { diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index e25d78e53d..9c2d3bfe64 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -160,6 +160,7 @@ import { UNWIND_PERSONALITY, UNWIND_RAISE, UNWIND_RAISE_DECLARATION, + UNWIND_RESUME_DECLARATION, UNWIND_RESUME, UNWIND_SLOT, UnwindAbiViolation, @@ -6110,7 +6111,8 @@ export class LlEmitter { const exception = B.tmp(); B.line(`${exception} = landingpad { ptr, i32 } cleanup`); for (const index of borrowed) this.releaseFrame(`%p${index}`, fn.params[index]!.type); - B.terminate(`resume { ptr, i32 } ${exception}`); + this.declare(UNWIND_RESUME_DECLARATION); + B.resumeUnwind(exception); B.startBlock(ok); } else B.line(ret === "void" ? call : `%result = ${call}`); // A borrowed result is reachable from the parameters: own it first. A diff --git a/packages/compiler/src/backend/llvm/unwind-abi.test.ts b/packages/compiler/src/backend/llvm/unwind-abi.test.ts index bcef1c3796..da490324b7 100644 --- a/packages/compiler/src/backend/llvm/unwind-abi.test.ts +++ b/packages/compiler/src/backend/llvm/unwind-abi.test.ts @@ -93,7 +93,9 @@ test("a method of a class outside any hierarchy is on the unwinding ABI; closure // closure over mid is a pointer target and returns with the flag. const add = body(ir, "Counter_add"); expect(add).toMatch(/invoke double @sc_b?f_[^(]*mid/); - expect(add).toMatch(/resume \{ ptr, i32 \} %[\w.]+/); + expect(add).toMatch(/call void @scr_unwind_resume\(ptr %[\w.]+\)\n\s+unreachable/); + expect(add).not.toMatch(/\bresume \{/); + expect(ir).toContain("declare void @scr_unwind_resume(ptr) cold"); expect(add).not.toContain("@scr_unwind_raise"); const closures = ir .split(/\n(?=define )/) @@ -212,7 +214,10 @@ test("an invoked wrapper of a raising function resumes the unwind; its address m expect(out).toContain( "%r = invoke double @sc_bf_body.i32(i32 %i) to label %ub.ok.0 unwind label %ub.lp.0", ); - expect(out).toContain("resume { ptr, i32 } %ub.v.0"); + expect(out).toContain( + "%ub.e.0 = extractvalue { ptr, i32 } %ub.v.0, 0\n call void @scr_unwind_resume(ptr %ub.e.0)\n unreachable", + ); + expect(out).toContain("declare void @scr_unwind_resume(ptr) cold"); expect(() => bridgePlainUnwindCalls([ `${raising}\n@table = internal constant [1 x ptr] [ptr @sc_bf_body.i32]`, @@ -277,11 +282,27 @@ test("calls that cannot unwind are marked nounwind", () => { " unreachable", "}", ].join("\n"); - const [out] = bridgePlainUnwindCalls([raising]); + const resuming = [ + "define internal void @sc_bf_resumes(ptr %b) #0 personality ptr @__gcc_personality_v0 {", + "entry:", + " invoke void @sc_bf_raises(ptr %b) to label %ok unwind label %lp", + "ok:", + " ret void", + "lp:", + " %e = landingpad { ptr, i32 } cleanup", + " %t = extractvalue { ptr, i32 } %e, 0", + " call void @scr_unwind_resume(ptr %t)", + " unreachable", + "}", + ].join("\n"); + const [out] = bridgePlainUnwindCalls([[raising, resuming].join("\n")]); expect(out).toContain("%n = call i64 @scr_bytes_length(ptr %b) nounwind ; length"); expect(out).toContain("call void @scr_bytes_release(ptr %b) nounwind, !dbg !4"); expect(out).toContain("%r = call ptr %b(ptr %b) nounwind"); expect(out).toContain(" call void @scr_unwind_raise()\n"); + // The resume unwinds: where LLVM inlines the function at an invoke, the + // call becomes an invoke of the caller's landing pad. + expect(out).toContain(" call void @scr_unwind_resume(ptr %t)\n"); }); test("functions without a plain call of a raising function change only their call lines", () => { diff --git a/packages/compiler/src/backend/llvm/unwind-abi.ts b/packages/compiler/src/backend/llvm/unwind-abi.ts index fdd0a2f18b..2c0721a133 100644 --- a/packages/compiler/src/backend/llvm/unwind-abi.ts +++ b/packages/compiler/src/backend/llvm/unwind-abi.ts @@ -76,9 +76,11 @@ function recursiveFunctions(edges: ReadonlyMap): Set< * than a `call`'s callee, and not called from an async or generator body * (coroutine bodies keep plain calls). * - * A native unwind costs about 2 µs a raise and 0.1 µs a frame, against a - * few nanoseconds for a flag test, so the functions an exception is likely to - * cross many of keep the flag: those with a `throw` or rethrow of their own + * A native unwind costs about 0.05 µs a raise and 10 to 25 ns a frame on + * x86-64 (scr_unwind.c's cached plans) and about 2 µs a raise and 0.1 µs a + * frame through libgcc's unwinder, against a few nanoseconds for a flag + * test, so the functions an exception is likely to cross many of keep the + * flag: those with a `throw` or rethrow of their own * and those up to THROW_LEVELS calls above one (a handler near the throw * takes it without a native unwind), and the recursive ones (deep stacks * are recursions). */ @@ -254,8 +256,8 @@ export function unwindAbiFunctions( * its own takes the exception. */ export const UNWIND_RAISE = "call void @scr_unwind_raise()\n unreachable"; /** What the unwind of a landing pad ends with when no handler of the - * function takes the exception: the unwind goes on (`resume`) with the - * exception the landing pad stored in UNWIND_SLOT. The block builder + * function takes the exception: the unwind goes on (scr_unwind_resume) with + * the exception the landing pad stored in UNWIND_SLOT. The block builder * expands it (BlockBuilder.terminate). */ export const UNWIND_RESUME = "resume { ptr, i32 } "; /** The function's slot for the exception its landing pads take. */ @@ -264,13 +266,17 @@ export const UNWIND_SLOT = "%unw.exc"; * would call the sanitizer's no-return hook before it, which fails on fiber * stacks; scr_unwind_raise clears the unwound frames' shadow itself. */ export const UNWIND_RAISE_DECLARATION = "declare void @scr_unwind_raise() cold"; +/** scr_unwind_resume's declaration. Landing pads call it with the + * exception they took to continue the unwind (BlockBuilder.resumeUnwind); + * like scr_unwind_raise, it is not `noreturn`. */ +export const UNWIND_RESUME_DECLARATION = "declare void @scr_unwind_resume(ptr) cold"; /** The personality of functions with landing pads: the landing pads are - * cleanups that scr_unwind_raise's forced unwind runs. */ + * cleanups that scr_unwind_raise's unwind runs. */ export const UNWIND_PERSONALITY = "personality ptr @__gcc_personality_v0"; -/** An unwind leaving the function: a raise, or a landing pad's `resume`. */ -const UNWINDS = /call void @scr_unwind_raise\(\)|\n\s+resume /; +/** An unwind leaving the function: a raise, or a landing pad's resume. */ +const UNWINDS = /call void @scr_unwind_raise\(\)|call void @scr_unwind_resume\(/; /** A pending-flag test: the thread's exception word, the active cell, or the runtime query. */ const PENDING_TEST = /@scr_exc_flag\b|%exc\.cell\b|@scr_exc_active\b|@scr_exc_pending\(/; @@ -307,15 +313,16 @@ function argumentsEnd(line: string, open: number): number { return close; } -/** A call that cannot unwind, marked so: only scr_unwind_raise and the - * raising functions propagate an unwind (runtime code and every function - * reached through a pointer return normally), so every other call is - * `nounwind`, and the inliner keeps it a call where it inlines a raising - * function at an invoke. */ +/** A call that cannot unwind, marked so: only scr_unwind_raise, + * scr_unwind_resume and the raising functions propagate an unwind (runtime + * code and every function reached through a pointer return normally), so + * every other call is `nounwind`, and the inliner keeps it a call where it + * inlines a raising function at an invoke. */ function markNounwind(line: string): string { const call = line.indexOf("call "); const callee = CALLEE.exec(line.slice(call)); - if (callee === null || callee[0] === "@scr_unwind_raise(") return line; + if (callee === null || callee[0] === "@scr_unwind_raise(" || callee[0] === "@scr_unwind_resume(") + return line; const close = argumentsEnd(line, call + callee.index + callee[0].length - 1); return close >= line.length ? line @@ -446,6 +453,7 @@ export function bridgePlainUnwindCalls(parts: string[]): string[] { } if (violations.size > 0) throw new UnwindAbiViolation(violations); let personality = false; + let resumes = false; let counter = 0; const out = split.map((chunks) => chunks @@ -525,8 +533,11 @@ export function bridgePlainUnwindCalls(parts: string[]): string[] { result.push( `${pad}:`, `${indent}%ub.v.${n} = landingpad { ptr, i32 } cleanup`, - `${indent}resume { ptr, i32 } %ub.v.${n}`, + `${indent}%ub.e.${n} = extractvalue { ptr, i32 } %ub.v.${n}, 0`, + `${indent}call void @scr_unwind_resume(ptr %ub.e.${n})`, + `${indent}unreachable`, ); + resumes = true; result.push(`${ok}:`); } else if (m[2] !== undefined) { const type = (m[3] ?? "").replace(RETURN_ATTRIBUTES, "").trim(); @@ -585,5 +596,7 @@ export function bridgePlainUnwindCalls(parts: string[]): string[] { ); if (personality && !out.some((part) => part.includes("@__gcc_personality_v0(...)"))) out[0] = `declare i32 @__gcc_personality_v0(...)\n${out[0]}`; + if (resumes && !out.some((part) => part.includes(UNWIND_RESUME_DECLARATION))) + out[0] = `${UNWIND_RESUME_DECLARATION}\n${out[0]}`; return out; } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/unwind-landing-pad-chains.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/unwind-landing-pad-chains.ts.json new file mode 100644 index 0000000000..84c925f1ed --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/unwind-landing-pad-chains.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/unwind-landing-pad-chains.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_exception.h b/packages/runtime/src/scr_exception.h index 168b2fe2d7..6c5f83358c 100644 --- a/packages/runtime/src/scr_exception.h +++ b/packages/runtime/src/scr_exception.h @@ -129,6 +129,8 @@ _Static_assert(offsetof(ScrExcCell, kind) == 0 && sizeof(ScrExcKind) == 4 && SCR * functions, unwind-abi.ts). Linux (glibc and musl) only; it never * returns. */ SCR_COLD _Noreturn void scr_unwind_raise(void); +/* Continues the unwind from a landing pad with the exception it took. */ +SCR_COLD void scr_unwind_resume(void *exc); SCR_COLD void scr_throw_f64(double v); SCR_COLD void scr_throw_bool(bool v); SCR_COLD void scr_throw_str(ScrStr *v); /* takes ownership */ diff --git a/packages/runtime/src/scr_unwind.c b/packages/runtime/src/scr_unwind.c index 50474e82e4..339f85088b 100644 --- a/packages/runtime/src/scr_unwind.c +++ b/packages/runtime/src/scr_unwind.c @@ -1,6 +1,24 @@ /* scr_unwind_raise: the native unwind of the unwinding exception ABI of - * emitted functions (the compiler's unwind-abi.ts). Linux (glibc and musl) - * only; elsewhere this unit is empty. */ + * emitted functions (the compiler's unwind-abi.ts), and scr_unwind_resume, + * which emitted landing pads call to continue it. Linux (glibc and musl) + * only; elsewhere this unit is empty. + * + * Emitted landing pads are cleanups: they release the frame's references + * and then either take the exception (a handler of theirs, or a frame on + * the pending-flag ABI that returns with it pending) or resume the unwind. + * So the unwind is a single pass through every landing pad; the exception + * itself stays in the active cell. + * + * On x86-64 the unwind walks with plans cached per return address. The + * first raise through a return address finds the function's FDE through + * the .eh_frame_hdr of the object that contains it, runs the CFI up to the + * call, and finds the call site's landing pad in the LSDA. The plan records + * how to compute the frame's CFA, where each callee-saved register is + * saved, and the landing pad; later raises only look plans up. A landing + * pad runs with rax holding the raise's token and rdx 0 (a cleanup). When a + * frame's FDE uses CFI or encodings the plans do not express, or a return + * address has no FDE, the unwind goes on from the frame the walk started at + * with libgcc's forced unwind, as on other Linux targets. */ #include "scr_runtime.h" #if defined(__linux__) && !defined(__ANDROID__) && !defined(__wasi__) @@ -16,6 +34,7 @@ * which link it in. */ #pragma weak _Unwind_ForcedUnwind #pragma weak _Unwind_GetCFA +#pragma weak _Unwind_Resume #if defined(__SANITIZE_ADDRESS__) #define SCR_UNWIND_ASAN 1 @@ -34,11 +53,6 @@ static SCR_TL uintptr_t scr_unwind_low; #endif -/* Emitted landing pads are cleanups: they release the frame's references - * and then either take the exception (a handler of theirs, or a frame on - * the pending-flag ABI that returns with it pending) or resume the unwind. - * So the unwind is forced, a single pass through every landing pad; the - * exception itself stays in the active cell. */ static _Unwind_Reason_Code scr_unwind_stop(int version, _Unwind_Action actions, _Unwind_Exception_Class cls, struct _Unwind_Exception *exc, @@ -69,7 +83,10 @@ static _Unwind_Reason_Code scr_unwind_stop(int version, _Unwind_Action actions, static SCR_TL struct _Unwind_Exception scr_unwind_exception; -void scr_unwind_raise(void) { +/* libgcc's forced unwind, from the caller of the function that calls this. + * It does not return; it is not declared noreturn, so sanitized builds call + * no sanitizer hook before it (see scr_unwind_stop). */ +static __attribute__((noinline)) void scr_unwind_forced(void) { struct _Unwind_Exception *exc = &scr_unwind_exception; memset(exc, 0, sizeof *exc); exc->exception_class = 0x5343524950540000ull; /* "SCRIPT\0\0" */ @@ -80,4 +97,686 @@ void scr_unwind_raise(void) { fputs("scriptc: the native unwinder could not raise an exception\n", stderr); abort(); } + +#if defined(__x86_64__) +#include + +/* DWARF register numbers. */ +enum { + SCR_UNW_RBX = 3, + SCR_UNW_RBP = 6, + SCR_UNW_RSP = 7, + SCR_UNW_R12 = 12, + SCR_UNW_R13 = 13, + SCR_UNW_R14 = 14, + SCR_UNW_R15 = 15, + SCR_UNW_RA = 16, + SCR_UNW_REGS = 17 +}; + +/* The callee-saved registers a plan restores, in the order of its bits. */ +static const uint8_t scr_unwind_callee[6] = {SCR_UNW_RBX, SCR_UNW_RBP, SCR_UNW_R12, + SCR_UNW_R13, SCR_UNW_R14, SCR_UNW_R15}; + +typedef struct { + uint64_t pc; /* the return address the plan is for (0: an empty slot) */ + uint64_t pad; /* the call site's landing pad, or 0 */ + int32_t cfa_off; /* the CFA is cfa_reg's value plus cfa_off */ + uint8_t cfa_reg; /* SCR_UNW_RSP or SCR_UNW_RBP */ + uint8_t saved; /* bit i: scr_unwind_callee[i] is saved at CFA + off[i] * 8 */ + int8_t off[6]; + uint8_t fallback; /* the frame is left to libgcc's unwinder */ +} ScrUnwindPlan; + +_Static_assert(sizeof(ScrUnwindPlan) == 32, "a plan is half a cache line"); + +#define SCR_UNWIND_PLAN_BITS 14 +#define SCR_UNWIND_PLAN_SLOTS (1u << SCR_UNWIND_PLAN_BITS) +static ScrUnwindPlan scr_unwind_plans[SCR_UNWIND_PLAN_SLOTS]; +static const ScrUnwindPlan scr_unwind_fallback_plan = {.fallback = 1}; +static size_t scr_unwind_plan_count; +/* Inserts take this lock; lookups take none. A plan is published by the + * release store of its pc. */ +static int scr_unwind_lock; + +/* Reads a value in DWARF pointer encoding enc. datarel is the base of + * datarel values (0 where they do not occur). Sets *bad for an encoding the + * plans do not read, which includes indirect values. Like libgcc, a zero + * value stays zero. */ +static uint64_t scr_unwind_read(const uint8_t **p, uint8_t enc, uint64_t datarel, int *bad) { + const uint8_t *start = *p; + uint64_t v = 0; + switch (enc & 0x0f) { + case 0x00: /* absptr */ + case 0x04: /* udata8 */ + case 0x0c: /* sdata8 */ + memcpy(&v, *p, 8); + *p += 8; + break; + case 0x01: { /* uleb128 */ + unsigned shift = 0; + uint8_t b; + do { + b = *(*p)++; + if (shift < 64) v |= (uint64_t)(b & 0x7f) << shift; + shift += 7; + } while (b & 0x80); + break; + } + case 0x02: { + uint16_t x; + memcpy(&x, *p, 2); + *p += 2; + v = x; + break; + } + case 0x03: { + uint32_t x; + memcpy(&x, *p, 4); + *p += 4; + v = x; + break; + } + case 0x0a: { + int16_t x; + memcpy(&x, *p, 2); + *p += 2; + v = (uint64_t)(int64_t)x; + break; + } + case 0x0b: { + int32_t x; + memcpy(&x, *p, 4); + *p += 4; + v = (uint64_t)(int64_t)x; + break; + } + default: /* sleb128 and unknown formats */ + *bad = 1; + return 0; + } + if (v == 0) return 0; + switch (enc & 0x70) { + case 0x00: + break; + case 0x10: /* pcrel */ + v += (uint64_t)(uintptr_t)start; + break; + case 0x30: /* datarel */ + if (datarel == 0) *bad = 1; + v += datarel; + break; + default: + *bad = 1; + return 0; + } + if (enc & 0x80) *bad = 1; /* indirect: read only through scr_unwind_skip */ + return v; +} + +static uint64_t scr_unwind_uleb(const uint8_t **p) { + int bad = 0; + return scr_unwind_read(p, 0x01, 0, &bad); +} + +static int64_t scr_unwind_sleb(const uint8_t **p) { + int64_t v = 0; + unsigned shift = 0; + uint8_t b; + do { + b = *(*p)++; + if (shift < 64) v |= (int64_t)((uint64_t)(b & 0x7f) << shift); + shift += 7; + } while (b & 0x80); + if (shift < 64 && (b & 0x40)) v |= (int64_t)(~(uint64_t)0 << shift); + return v; +} + +/* A search for the loaded object that contains pc, and its .eh_frame_hdr + * search table. */ +typedef struct { + uint64_t pc; + const uint8_t *hdr; + const uint8_t *table; + size_t count; +} ScrUnwindObject; + +static int scr_unwind_find_object(struct dl_phdr_info *info, size_t size, void *data) { + (void)size; + ScrUnwindObject *object = data; + int contains = 0; + const uint8_t *h = NULL; + for (int i = 0; i < info->dlpi_phnum; i++) { + const ElfW(Phdr) *phdr = &info->dlpi_phdr[i]; + uint64_t start = (uint64_t)info->dlpi_addr + phdr->p_vaddr; + if (phdr->p_type == PT_LOAD && object->pc >= start && object->pc - start < phdr->p_memsz) + contains = 1; + else if (phdr->p_type == PT_GNU_EH_FRAME) + h = (const uint8_t *)(uintptr_t)start; + } + if (!contains) return 0; + /* Version 1 with the table LLVM's and GNU's linkers write: datarel + * sdata4 pairs, sorted by initial location. */ + if (h != NULL && h[0] == 1 && h[1] != 0xff && h[2] != 0xff && h[3] == 0x3b) { + int bad = 0; + const uint8_t *p = h + 4; + (void)scr_unwind_read(&p, h[1], (uint64_t)(uintptr_t)h, &bad); + uint64_t count = scr_unwind_read(&p, h[2], (uint64_t)(uintptr_t)h, &bad); + if (!bad) { + object->hdr = h; + object->table = p; + object->count = (size_t)count; + } + } + return 1; +} + +/* The FDE whose initial location is the greatest one at or below pc in + * the table of the object that contains pc. */ +static const uint8_t *scr_unwind_find_fde(uint64_t pc) { + ScrUnwindObject object = {.pc = pc}; + dl_iterate_phdr(scr_unwind_find_object, &object); + if (object.table == NULL) return NULL; + uint64_t base = (uint64_t)(uintptr_t)object.hdr; + size_t lo = 0, hi = object.count; + while (lo < hi) { + size_t mid = lo + (hi - lo) / 2; + int32_t start; + memcpy(&start, object.table + mid * 8, 4); + if (base + (uint64_t)(int64_t)start <= pc) lo = mid + 1; + else hi = mid; + } + if (lo == 0) return NULL; + int32_t fde; + memcpy(&fde, object.table + (lo - 1) * 8 + 4, 4); + return (const uint8_t *)(uintptr_t)(base + (uint64_t)(int64_t)fde); +} + +#define SCR_UNWIND_UNSAVED INT32_MIN + +typedef struct { + uint64_t cfa_reg; + int64_t cfa_off; + int32_t saved[SCR_UNW_REGS]; /* CFA-relative offsets, or SCR_UNWIND_UNSAVED */ + uint64_t args_size; +} ScrUnwindRules; + +/* Whether the plans track register reg: the callee-saved registers, the + * stack pointer and the return address. */ +static int scr_unwind_tracked(uint64_t reg) { + return reg == SCR_UNW_RBX || reg == SCR_UNW_RBP || reg == SCR_UNW_RSP || + (reg >= SCR_UNW_R12 && reg <= SCR_UNW_RA); +} + +static void scr_unwind_save(ScrUnwindRules *r, uint64_t reg, int64_t off, int *bad) { + if (reg >= SCR_UNW_REGS) return; /* vector and other registers stay as they are */ + if (off < INT32_MIN + 1 || off > INT32_MAX) *bad = 1; + else r->saved[reg] = (int32_t)off; +} + +/* Runs CFI instructions up to the location pc. Sets *bad for an + * instruction the plans do not express. */ +static void scr_unwind_run_cfi(const uint8_t *p, const uint8_t *end, uint64_t code_align, + int64_t data_align, uint64_t loc, uint64_t pc, ScrUnwindRules *r, + const ScrUnwindRules *initial, int *bad) { + ScrUnwindRules stack[8]; + int depth = 0; + while (p < end && loc <= pc && !*bad) { + uint8_t op = *p++; + uint8_t low = op & 0x3f; + switch (op & 0xc0) { + case 0x40: /* advance_loc */ + loc += low * code_align; + continue; + case 0x80: /* offset */ + scr_unwind_save(r, low, (int64_t)scr_unwind_uleb(&p) * data_align, bad); + continue; + case 0xc0: /* restore */ + if (low < SCR_UNW_REGS) r->saved[low] = initial->saved[low]; + continue; + default: + break; + } + switch (op) { + case 0x00: /* nop */ + break; + case 0x02: /* advance_loc1 */ + loc += *p++ * code_align; + break; + case 0x03: { /* advance_loc2 */ + uint16_t d; + memcpy(&d, p, 2); + p += 2; + loc += d * code_align; + break; + } + case 0x04: { /* advance_loc4 */ + uint32_t d; + memcpy(&d, p, 4); + p += 4; + loc += d * code_align; + break; + } + case 0x05: { /* offset_extended */ + uint64_t reg = scr_unwind_uleb(&p); + scr_unwind_save(r, reg, (int64_t)scr_unwind_uleb(&p) * data_align, bad); + break; + } + case 0x06: { /* restore_extended */ + uint64_t reg = scr_unwind_uleb(&p); + if (reg < SCR_UNW_REGS) r->saved[reg] = initial->saved[reg]; + break; + } + case 0x07: /* undefined */ + case 0x08: { /* same_value */ + uint64_t reg = scr_unwind_uleb(&p); + if (op == 0x07 && scr_unwind_tracked(reg)) *bad = 1; + else if (reg < SCR_UNW_REGS) r->saved[reg] = SCR_UNWIND_UNSAVED; + break; + } + case 0x09: { /* register */ + uint64_t reg = scr_unwind_uleb(&p); + (void)scr_unwind_uleb(&p); + if (scr_unwind_tracked(reg)) *bad = 1; + break; + } + case 0x0a: /* remember_state */ + if (depth == 8) *bad = 1; + else stack[depth++] = *r; + break; + case 0x0b: { /* restore_state: the CFA rule and args size stay */ + if (depth == 0) { + *bad = 1; + break; + } + uint64_t args_size = r->args_size; + *r = stack[--depth]; + r->args_size = args_size; + break; + } + case 0x0c: /* def_cfa */ + r->cfa_reg = scr_unwind_uleb(&p); + r->cfa_off = (int64_t)scr_unwind_uleb(&p); + break; + case 0x0d: /* def_cfa_register */ + r->cfa_reg = scr_unwind_uleb(&p); + break; + case 0x0e: /* def_cfa_offset */ + r->cfa_off = (int64_t)scr_unwind_uleb(&p); + break; + case 0x11: { /* offset_extended_sf */ + uint64_t reg = scr_unwind_uleb(&p); + scr_unwind_save(r, reg, scr_unwind_sleb(&p) * data_align, bad); + break; + } + case 0x12: /* def_cfa_sf */ + r->cfa_reg = scr_unwind_uleb(&p); + r->cfa_off = scr_unwind_sleb(&p) * data_align; + break; + case 0x13: /* def_cfa_offset_sf */ + r->cfa_off = scr_unwind_sleb(&p) * data_align; + break; + case 0x2e: /* GNU_args_size */ + r->args_size = scr_unwind_uleb(&p); + break; + default: /* expressions, val_offset, set_loc and vendor instructions */ + *bad = 1; + break; + } + } +} + +/* The landing pad of the call site that contains pc in an LSDA, or 0. */ +static uint64_t scr_unwind_lsda_pad(const uint8_t *p, uint64_t func_start, uint64_t pc, int *bad) { + uint8_t lpstart_enc = *p++; + uint64_t lpstart = func_start; + if (lpstart_enc != 0xff) lpstart = scr_unwind_read(&p, lpstart_enc, 0, bad); + uint8_t ttype_enc = *p++; + if (ttype_enc != 0xff) (void)scr_unwind_uleb(&p); + uint8_t cs_enc = *p++; + uint64_t cs_len = scr_unwind_uleb(&p); + const uint8_t *cs_end = p + cs_len; + while (p < cs_end && !*bad) { + uint64_t start = scr_unwind_read(&p, cs_enc, 0, bad); + uint64_t len = scr_unwind_read(&p, cs_enc, 0, bad); + uint64_t pad = scr_unwind_read(&p, cs_enc, 0, bad); + (void)scr_unwind_uleb(&p); /* action */ + if (pc < func_start + start) return 0; + if (pc < func_start + start + len) return pad ? lpstart + pad : 0; + } + return 0; +} + +/* The personality routine of emitted functions; the walk reads their + * LSDAs itself. Frames with any other personality are left to libgcc. */ +extern _Unwind_Reason_Code __gcc_personality_v0(int, _Unwind_Action, _Unwind_Exception_Class, + struct _Unwind_Exception *, + struct _Unwind_Context *); +#pragma weak __gcc_personality_v0 + +/* Builds the plan of return address ra. */ +static void scr_unwind_build(ScrUnwindPlan *plan, uint64_t ra) { + memset(plan, 0, sizeof *plan); + plan->fallback = 1; + uint64_t pc = ra - 1; /* within the call instruction */ + const uint8_t *fde = scr_unwind_find_fde(pc); + if (fde == NULL) return; + uint32_t fde_len; + memcpy(&fde_len, fde, 4); + if (fde_len == 0 || fde_len == 0xffffffffu) return; + const uint8_t *fde_end = fde + 4 + fde_len; + int32_t cie_off; + memcpy(&cie_off, fde + 4, 4); + if (cie_off == 0) return; + const uint8_t *cie = fde + 4 - cie_off; + uint32_t cie_len; + memcpy(&cie_len, cie, 4); + if (cie_len == 0 || cie_len == 0xffffffffu) return; + const uint8_t *cie_end = cie + 4 + cie_len; + const uint8_t *q = cie + 8; /* past the length and the CIE id */ + uint8_t version = *q++; + if (version != 1 && version != 3) return; + const char *aug = (const char *)q; + q += strlen(aug) + 1; + uint64_t code_align = scr_unwind_uleb(&q); + int64_t data_align = scr_unwind_sleb(&q); + uint64_t ra_reg = version == 1 ? *q++ : scr_unwind_uleb(&q); + if (ra_reg != SCR_UNW_RA) return; + int bad = 0; + uint8_t fde_enc = 0x00, lsda_enc = 0xff; + if (aug[0] == 'z') { + uint64_t aug_len = scr_unwind_uleb(&q); + const uint8_t *aug_end = q + aug_len; + for (const char *a = aug + 1; *a != '\0'; a++) { + if (*a == 'R') { + fde_enc = *q++; + } else if (*a == 'L') { + lsda_enc = *q++; + } else if (*a == 'P') { + uint8_t enc = *q++; + uint64_t slot = scr_unwind_read(&q, enc & 0x7f, 0, &bad); + uint64_t personality = slot; + if ((enc & 0x80) && slot != 0) memcpy(&personality, (const void *)(uintptr_t)slot, 8); + if (personality != (uint64_t)(uintptr_t)&__gcc_personality_v0) bad = 1; + } else { + return; /* signal frames and unknown augmentations */ + } + } + q = aug_end; + } else if (aug[0] != '\0') { + return; + } + if (bad) return; + ScrUnwindRules initial = {.cfa_reg = SCR_UNW_RSP, .cfa_off = 8}; + for (int i = 0; i < SCR_UNW_REGS; i++) initial.saved[i] = SCR_UNWIND_UNSAVED; + scr_unwind_run_cfi(q, cie_end, code_align, data_align, 0, UINT64_MAX, &initial, &initial, &bad); + const uint8_t *f = fde + 8; + uint64_t func_start = scr_unwind_read(&f, fde_enc, 0, &bad); + uint64_t func_len = scr_unwind_read(&f, fde_enc & 0x0f, 0, &bad); + if (bad || pc < func_start || pc - func_start >= func_len) return; + const uint8_t *lsda = NULL; + if (aug[0] == 'z') { + uint64_t aug_len = scr_unwind_uleb(&f); + const uint8_t *aug_end = f + aug_len; + if (lsda_enc != 0xff && aug_len != 0) + lsda = (const uint8_t *)(uintptr_t)scr_unwind_read(&f, lsda_enc, 0, &bad); + f = aug_end; + } + ScrUnwindRules r = initial; + scr_unwind_run_cfi(f, fde_end, code_align, data_align, func_start, pc, &r, &initial, &bad); + if (bad || r.args_size != 0) return; + if (r.cfa_reg != SCR_UNW_RSP && r.cfa_reg != SCR_UNW_RBP) return; + if (r.cfa_off < INT32_MIN || r.cfa_off > INT32_MAX) return; + if (r.saved[SCR_UNW_RA] != -8) return; + uint8_t saved = 0; + for (int i = 0; i < 6; i++) { + int32_t o = r.saved[scr_unwind_callee[i]]; + if (o == SCR_UNWIND_UNSAVED) continue; + if (o % 8 != 0 || o / 8 < INT8_MIN || o / 8 > INT8_MAX) return; + saved |= (uint8_t)(1u << i); + plan->off[i] = (int8_t)(o / 8); + } + uint64_t pad = lsda ? scr_unwind_lsda_pad(lsda, func_start, pc, &bad) : 0; + if (bad) return; + plan->cfa_reg = (uint8_t)r.cfa_reg; + plan->cfa_off = (int32_t)r.cfa_off; + plan->saved = saved; + plan->pad = pad; + plan->fallback = 0; +} + +static uint64_t scr_unwind_hash(uint64_t ra) { + return (ra * 0x9E3779B97F4A7C15ull) >> (64 - SCR_UNWIND_PLAN_BITS); +} + +static __attribute__((noinline)) const ScrUnwindPlan *scr_unwind_insert(uint64_t ra) { + while (__atomic_exchange_n(&scr_unwind_lock, 1, __ATOMIC_ACQUIRE)) + while (__atomic_load_n(&scr_unwind_lock, __ATOMIC_RELAXED)) __builtin_ia32_pause(); + const ScrUnwindPlan *result = &scr_unwind_fallback_plan; + uint64_t h = scr_unwind_hash(ra); + ScrUnwindPlan *slot = &scr_unwind_plans[h]; + while (slot->pc != 0 && slot->pc != ra) { + h = (h + 1) & (SCR_UNWIND_PLAN_SLOTS - 1); + slot = &scr_unwind_plans[h]; + } + if (slot->pc == ra) { + result = slot; + } else if (scr_unwind_plan_count < SCR_UNWIND_PLAN_SLOTS / 4 * 3) { + /* A full table leaves further return addresses to libgcc. */ + scr_unwind_plan_count++; + ScrUnwindPlan built; + scr_unwind_build(&built, ra); + /* Lookups read pc atomically, so the plan's other fields are written + * first and pc only by the release store that publishes them. */ + memcpy((char *)slot + sizeof slot->pc, (const char *)&built + sizeof built.pc, + sizeof *slot - sizeof slot->pc); + __atomic_store_n(&slot->pc, ra, __ATOMIC_RELEASE); + result = slot; + } + __atomic_store_n(&scr_unwind_lock, 0, __ATOMIC_RELEASE); + return result; +} + +static const ScrUnwindPlan *scr_unwind_plan(uint64_t ra) { + uint64_t h = scr_unwind_hash(ra); + for (;;) { + const ScrUnwindPlan *plan = &scr_unwind_plans[h]; + uint64_t pc = __atomic_load_n(&plan->pc, __ATOMIC_ACQUIRE); + if (pc == ra) return plan; + if (pc == 0) return scr_unwind_insert(ra); + h = (h + 1) & (SCR_UNWIND_PLAN_SLOTS - 1); + } +} + +/* Restores the callee-saved registers and the stack pointer of regs + * (indexed by DWARF number) and jumps to the landing pad with rax holding + * the token and rdx 0. */ +static _Noreturn __attribute__((naked)) void scr_unwind_install(const uint64_t *regs, uint64_t pad, + const void *token) { + __asm__ volatile("movq %rdx, %rax\n\t" + "movq %rsi, %r11\n\t" + "movq 24(%rdi), %rbx\n\t" + "movq 48(%rdi), %rbp\n\t" + "movq 96(%rdi), %r12\n\t" + "movq 104(%rdi), %r13\n\t" + "movq 112(%rdi), %r14\n\t" + "movq 120(%rdi), %r15\n\t" + "movq 56(%rdi), %rsp\n\t" + "xorl %edx, %edx\n\t" + "jmpq *%r11\n\t"); +} + +/* Identifies a walk's raise to the landing pads it runs: they pass it to + * scr_unwind_resume. The thrown value is in the active cell. */ +static const char scr_unwind_token; + +#ifdef SCR_UNWIND_TESTING +/* The walk's raises and resumes that went on with libgcc's unwinder, and + * the frames whose stack pointer or callee-saved registers differ from + * libgcc's for the same return address. */ +size_t scr_unwind_test_fallbacks; +size_t scr_unwind_test_mismatches; + +typedef struct { + uint64_t ip, cfa, regs[6]; +} ScrUnwindTestFrame; + +typedef struct { + ScrUnwindTestFrame frames[512]; + int count, next; +} ScrUnwindTestTrace; + +static _Thread_local ScrUnwindTestTrace scr_unwind_test_trace; + +static _Unwind_Reason_Code scr_unwind_test_record(struct _Unwind_Context *context, void *arg) { + ScrUnwindTestTrace *trace = arg; + if (trace->count == 512) return _URC_END_OF_STACK; + ScrUnwindTestFrame *frame = &trace->frames[trace->count++]; + frame->ip = _Unwind_GetIP(context); + frame->cfa = _Unwind_GetCFA(context); + for (int i = 0; i < 6; i++) frame->regs[i] = _Unwind_GetGR(context, scr_unwind_callee[i]); + return _URC_NO_REASON; +} + +/* Records libgcc's frames from here, before the walk changes anything. */ +static void scr_unwind_test_start(void) { + scr_unwind_test_trace.count = 0; + scr_unwind_test_trace.next = 0; + _Unwind_Backtrace(scr_unwind_test_record, &scr_unwind_test_trace); +} + +/* Compares the walk's frame, regs at its call, with libgcc's: libgcc's + * CFA there is the callee's, the frame's stack pointer at the call. */ +static void scr_unwind_test_check(const uint64_t *regs) { + ScrUnwindTestTrace *trace = &scr_unwind_test_trace; + while (trace->next < trace->count && trace->frames[trace->next].ip != regs[SCR_UNW_RA]) + trace->next++; + if (trace->next == trace->count) { + if (trace->count < 512) __atomic_add_fetch(&scr_unwind_test_mismatches, 1, __ATOMIC_RELAXED); + return; + } + const ScrUnwindTestFrame *frame = &trace->frames[trace->next++]; + int same = frame->cfa == regs[SCR_UNW_RSP]; + for (int i = 0; i < 6; i++) same &= frame->regs[i] == regs[scr_unwind_callee[i]]; + if (!same) __atomic_add_fetch(&scr_unwind_test_mismatches, 1, __ATOMIC_RELAXED); +} + +#define SCR_UNWIND_FALLBACK() __atomic_add_fetch(&scr_unwind_test_fallbacks, 1, __ATOMIC_RELAXED) +#define SCR_UNWIND_TEST_START() scr_unwind_test_start() +#define SCR_UNWIND_TEST_CHECK(regs) scr_unwind_test_check(regs) +#else +#define SCR_UNWIND_FALLBACK() ((void)0) +#define SCR_UNWIND_TEST_START() ((void)0) +#define SCR_UNWIND_TEST_CHECK(regs) ((void)0) +#endif + +/* Walks from the frame regs describes, a caller's registers at its call, + * and installs the first frame with a landing pad. Returns when a frame is + * left to libgcc, before anything has been installed. */ +static __attribute__((no_sanitize("address"))) void scr_unwind_walk(uint64_t *regs) { + SCR_UNWIND_TEST_START(); + for (;;) { + const ScrUnwindPlan *plan = scr_unwind_plan(regs[SCR_UNW_RA]); + if (plan->fallback) return; + SCR_UNWIND_TEST_CHECK(regs); + if (plan->pad != 0) { +#ifdef SCR_UNWIND_ASAN + /* The frames below the landing pad's never return: clear their + * redzones' shadow, up to the pad frame's stack pointer. */ + uintptr_t sp; + __asm__ volatile("movq %%rsp, %0" : "=r"(sp)); + if (regs[SCR_UNW_RSP] > sp) + __asan_unpoison_memory_region((void *)sp, regs[SCR_UNW_RSP] - sp); +#endif + scr_unwind_install(regs, plan->pad, &scr_unwind_token); + } + uint64_t cfa = regs[plan->cfa_reg] + (uint64_t)(int64_t)plan->cfa_off; + const uint64_t *frame = (const uint64_t *)(uintptr_t)cfa; + for (unsigned m = plan->saved; m != 0; m &= m - 1) { + int i = __builtin_ctz(m); + regs[scr_unwind_callee[i]] = frame[plan->off[i]]; + } + regs[SCR_UNW_RA] = frame[-1]; + regs[SCR_UNW_RSP] = cfa; + } +} + +/* Called by the entry stubs below with their caller's registers. */ +__attribute__((visibility("hidden"), used)) void scr_unwind_walk_raise(uint64_t *regs) { + scr_unwind_walk(regs); +} + +__attribute__((visibility("hidden"), used)) void scr_unwind_walk_resume(uint64_t *regs, + const void *exc) { + if (exc == &scr_unwind_token) scr_unwind_walk(regs); +} + +/* Where the entry stubs go when the walk returns, with the stack as it was + * at their entry: the caller is the emitted frame. */ +__attribute__((visibility("hidden"), used, no_sanitize("address"))) void +scr_unwind_raise_slow(void) { + SCR_UNWIND_FALLBACK(); + scr_unwind_forced(); +} + +__attribute__((visibility("hidden"), used, no_sanitize("address"))) void +scr_unwind_resume_slow(void *exc) { + /* A landing pad that libgcc's unwind ran passes libgcc's exception. */ + if (exc != &scr_unwind_token && _Unwind_Resume != NULL) _Unwind_Resume(exc); + SCR_UNWIND_FALLBACK(); + scr_unwind_forced(); +} + +/* The entry stubs store the caller's callee-saved registers, stack pointer + * after the return and return address into a register array on the stack, + * indexed by DWARF number (17 slots: 136 bytes, which keeps the stack + * 16-byte aligned at the call). */ +#define SCR_UNWIND_CAPTURE \ + "subq $136, %rsp\n\t" \ + ".cfi_adjust_cfa_offset 136\n\t" \ + "movq %rbx, 24(%rsp)\n\t" \ + "movq %rbp, 48(%rsp)\n\t" \ + "movq %r12, 96(%rsp)\n\t" \ + "movq %r13, 104(%rsp)\n\t" \ + "movq %r14, 112(%rsp)\n\t" \ + "movq %r15, 120(%rsp)\n\t" \ + "movq 136(%rsp), %rax\n\t" \ + "movq %rax, 128(%rsp)\n\t" \ + "leaq 144(%rsp), %rax\n\t" \ + "movq %rax, 56(%rsp)\n\t" + +__attribute__((naked)) void scr_unwind_raise(void) { + __asm__ volatile(SCR_UNWIND_CAPTURE "movq %rsp, %rdi\n\t" + "call scr_unwind_walk_raise\n\t" + "addq $136, %rsp\n\t" + ".cfi_adjust_cfa_offset -136\n\t" + "jmp scr_unwind_raise_slow\n\t"); +} + +/* Landing pads continue the unwind here with the exception they took. */ +__attribute__((naked)) void scr_unwind_resume(void *exc) { + __asm__ volatile(SCR_UNWIND_CAPTURE "movq %rdi, 40(%rsp)\n\t" + "movq %rdi, %rsi\n\t" + "movq %rsp, %rdi\n\t" + "call scr_unwind_walk_resume\n\t" + "movq 40(%rsp), %rdi\n\t" + "addq $136, %rsp\n\t" + ".cfi_adjust_cfa_offset -136\n\t" + "jmp scr_unwind_resume_slow\n\t"); +} + +#else + +void scr_unwind_raise(void) { + scr_unwind_forced(); + __builtin_unreachable(); +} + +/* Landing pads continue the unwind here with the exception they took. */ +void scr_unwind_resume(void *exc) { + if (_Unwind_Resume != NULL) _Unwind_Resume(exc); + scr_unwind_forced(); +} + +#endif #endif diff --git a/packages/runtime/test/test_unwind.c b/packages/runtime/test/test_unwind.c new file mode 100644 index 0000000000..5edabcde4f --- /dev/null +++ b/packages/runtime/test/test_unwind.c @@ -0,0 +1,165 @@ +/* Raises through the synthetic frames of test_unwind_frames.ll and + * test_unwind_frames.S with scr_unwind.c's walk: landing pads run inner + * first, the catching frame sees its callee-saved registers restored, every + * frame the walk steps through has the stack pointer and callee-saved + * registers libgcc's unwinder computes for it, and only frames with CFI the + * plans do not express go to libgcc's unwinder. + * Each case runs three times (the first builds plans, the others reuse + * them), and again from several threads at once. */ +#include +#include +#include +#include +#include + +typedef void Frame(void **chain); +extern Frame ir_raise, ir_plain, ir_plain_fp, ir_alloca, ir_pad, ir_pad_fp; +extern Frame asm_remember, asm_rbp, asm_expr; +extern int64_t ir_catch(void **chain, int64_t a, int64_t b, int64_t c, int64_t d, int64_t e, + int64_t f); +extern size_t scr_unwind_test_fallbacks; +extern size_t scr_unwind_test_mismatches; + +static _Thread_local void **cleanup_base; +static _Thread_local int cleanups[512]; +static _Thread_local int cleanup_count; + +/* A landing pad of the frame at chain[-1] ran. */ +void test_cleanup(void **chain) { + if (cleanup_count < 512) cleanups[cleanup_count] = (int)(chain - cleanup_base) - 1; + cleanup_count++; +} + +void test_touch(void *p) { memset(p, 0x5a, 16); } + +void test_return(void **chain) { (void)chain; } + +static int64_t checksum(int64_t a, int64_t b, int64_t c, int64_t d, int64_t e, int64_t f) { + uint64_t s = (uint64_t)a * 3 ^ (uint64_t)b * 5; + s += (uint64_t)c * 7; + s ^= (uint64_t)d * 11; + s += (uint64_t)e * 13; + s ^= (uint64_t)f * 17; + return (int64_t)(s & INT64_MAX); +} + +/* Writes over the stack the unwound frames used: in sanitized builds, a + * redzone the unwind left poisoned would be reported here. */ +static __attribute__((noinline)) void scrub(void) { + volatile char big[32768]; + memset((char *)big, 0, sizeof big); +} + +static void fail(const char *name, const char *what) { + fprintf(stderr, "%s: %s\n", name, what); + exit(1); +} + +/* Runs frames[0..n) below ir_catch. A case that ends in test_return + * raises nothing. Returns the case's fallbacks to libgcc. */ +static size_t run(const char *name, Frame *const *frames, int n, int64_t seed) { + void *chain[512]; + int expected[512]; + int pads = 0; + if (n >= 511) fail(name, "too many frames"); + for (int i = 0; i < n; i++) chain[i] = (void *)frames[i]; + chain[n] = NULL; + int raises = frames[n - 1] == ir_raise; + if (raises) + for (int i = n - 1; i >= 0; i--) + if (frames[i] == ir_pad || frames[i] == ir_pad_fp) expected[pads++] = i; + size_t before = __atomic_load_n(&scr_unwind_test_fallbacks, __ATOMIC_RELAXED); + cleanup_base = chain; + cleanup_count = 0; + int64_t a = seed, b = seed ^ 0x1234, c = seed + 77, d = seed * 31, e = ~seed, f = seed << 3; + int64_t got = ir_catch(chain, a, b, c, d, e, f); + int64_t want = checksum(a, b, c, d, e, f) | (raises ? INT64_MIN : 0); + if (got != want) fail(name, "the catching frame's values or raise flag differ"); + if (cleanup_count != pads) fail(name, "a different number of landing pads ran"); + for (int i = 0; i < pads; i++) + if (cleanups[i] != expected[i]) fail(name, "landing pads ran out of order"); + scrub(); + if (__atomic_load_n(&scr_unwind_test_mismatches, __ATOMIC_RELAXED) != 0) + fail(name, "a frame's stack pointer or callee-saved registers differ from libgcc's"); + return __atomic_load_n(&scr_unwind_test_fallbacks, __ATOMIC_RELAXED) - before; +} + +typedef struct { + const char *name; + Frame *frames[16]; + int count; + size_t fallbacks; /* raises or resumes that go to libgcc */ +} Case; + +/* Sanitized builds realign a frame with a dynamic alloca through r10 and + * describe it with DWARF expressions, which go to libgcc. */ +#if defined(__SANITIZE_ADDRESS__) +#define ALLOCA_FALLBACKS 1 +#elif defined(__has_feature) +#if __has_feature(address_sanitizer) +#define ALLOCA_FALLBACKS 1 +#endif +#endif +#ifndef ALLOCA_FALLBACKS +#define ALLOCA_FALLBACKS 0 +#endif + +static const Case cases[] = { + {"returns", {ir_pad, ir_plain, test_return}, 3, 0}, + {"raise", {ir_raise}, 1, 0}, + {"plain frames", {ir_plain, ir_plain_fp, ir_plain, ir_raise}, 4, 0}, + {"dynamic alloca", {ir_pad, ir_alloca, ir_pad, ir_raise}, 4, ALLOCA_FALLBACKS}, + {"landing pads", {ir_pad, ir_plain, ir_pad_fp, ir_pad, ir_raise}, 5, 0}, + {"remember and rbp", {asm_remember, ir_pad, asm_rbp, asm_remember, ir_raise}, 5, 0}, + {"libgcc above a pad", {asm_expr, ir_pad, ir_raise}, 3, 1}, + {"libgcc between pads", {ir_pad, asm_expr, ir_pad_fp, ir_raise}, 4, 1}, + {"libgcc at the raise", {ir_pad, asm_expr, ir_raise}, 3, 1}, +}; +#define CASES (sizeof cases / sizeof cases[0]) + +static void run_cases(int64_t seed) { + for (size_t i = 0; i < CASES; i++) { + size_t fallbacks = run(cases[i].name, cases[i].frames, cases[i].count, seed + (int64_t)i); + if (fallbacks != cases[i].fallbacks) fail(cases[i].name, "a different number of fallbacks"); + } +} + +/* A deep recursion of every frame kind. */ +static void run_deep(int64_t seed) { + static Frame *const kinds[] = {ir_pad, ir_plain, asm_remember, asm_rbp, ir_pad_fp, ir_plain_fp}; + Frame *frames[301]; + for (int i = 0; i < 300; i++) frames[i] = kinds[i % 6]; + frames[300] = ir_raise; + if (run("deep", frames, 301, seed) != 0) fail("deep", "a fallback to libgcc"); +} + +static void *thread_main(void *arg) { + int64_t seed = (int64_t)(intptr_t)arg; + for (int round = 0; round < 200; round++) { + for (size_t i = 0; i < CASES; i++) { + if (cases[i].fallbacks != 0) continue; /* the shared counter is checked alone */ + if (run(cases[i].name, cases[i].frames, cases[i].count, seed + round) != 0) + fail(cases[i].name, "a fallback to libgcc in a thread"); + } + if (round % 20 == 0) run_deep(seed); + } + return NULL; +} + +/* With an argument, the threads run first, so they build the plans + * concurrently. */ +int main(int argc, char **argv) { + (void)argv; + if (argc == 1) + for (int round = 0; round < 3; round++) { + run_cases(round * 1000); + run_deep(round); + } + pthread_t threads[8]; + for (int i = 0; i < 8; i++) + if (pthread_create(&threads[i], NULL, thread_main, (void *)(intptr_t)(i * 7919)) != 0) + fail("threads", "pthread_create failed"); + for (int i = 0; i < 8; i++) pthread_join(threads[i], NULL); + puts("unwind checks passed"); + return 0; +} diff --git a/packages/runtime/test/test_unwind_frames.S b/packages/runtime/test/test_unwind_frames.S new file mode 100644 index 0000000000..af46685927 --- /dev/null +++ b/packages/runtime/test/test_unwind_frames.S @@ -0,0 +1,101 @@ +/* Frames for test_unwind.c with CFI that compilers emit rarely. Like the + * frames in test_unwind_frames.ll, each takes a chain and calls its first + * function with the rest. */ + .text + +/* Saves rbx and r12, and leaves through an early epilogue under + * remember_state before its call, so the call's rules come from + * restore_state. */ + .globl asm_remember + .type asm_remember,@function +asm_remember: + .cfi_startproc + pushq %rbx + .cfi_def_cfa_offset 16 + .cfi_offset %rbx, -16 + pushq %r12 + .cfi_def_cfa_offset 24 + .cfi_offset %r12, -24 + subq $8, %rsp + .cfi_def_cfa_offset 32 + movq (%rdi), %rax + testq %rax, %rax + jne 1f + .cfi_remember_state + addq $8, %rsp + .cfi_def_cfa_offset 24 + popq %r12 + .cfi_def_cfa_offset 16 + .cfi_restore %r12 + popq %rbx + .cfi_def_cfa_offset 8 + .cfi_restore %rbx + retq + .cfi_restore_state +1: + movabsq $0x5151515151515151, %rbx + movabsq $0x5252525252525252, %r12 + addq $8, %rdi + callq *%rax + addq $8, %rsp + .cfi_def_cfa_offset 24 + popq %r12 + .cfi_def_cfa_offset 16 + popq %rbx + .cfi_def_cfa_offset 8 + retq + .cfi_endproc + .size asm_remember, .-asm_remember + +/* An rbp frame that saves r13 and r14 below rbp and moves the stack + * pointer by an amount that depends on the chain's address. */ + .globl asm_rbp + .type asm_rbp,@function +asm_rbp: + .cfi_startproc + pushq %rbp + .cfi_def_cfa_offset 16 + .cfi_offset %rbp, -16 + movq %rsp, %rbp + .cfi_def_cfa_register %rbp + pushq %r13 + pushq %r14 + .cfi_offset %r13, -24 + .cfi_offset %r14, -32 + movq %rdi, %rax + andq $0x70, %rax + subq %rax, %rsp + movabsq $0x5353535353535353, %r13 + movabsq $0x5454545454545454, %r14 + movq (%rdi), %rax + addq $8, %rdi + callq *%rax + leaq -16(%rbp), %rsp + popq %r14 + popq %r13 + popq %rbp + .cfi_def_cfa %rsp, 8 + retq + .cfi_endproc + .size asm_rbp, .-asm_rbp + +/* Defines its CFA with a DWARF expression (rsp + 16), which the plans do + * not express: raises through it go to libgcc's unwinder. */ + .globl asm_expr + .type asm_expr,@function +asm_expr: + .cfi_startproc + pushq %rbx + .cfi_escape 0x0f, 0x02, 0x77, 0x10 + .cfi_offset %rbx, -16 + movabsq $0x5555555555555555, %rbx + movq (%rdi), %rax + addq $8, %rdi + callq *%rax + popq %rbx + .cfi_def_cfa %rsp, 8 + retq + .cfi_endproc + .size asm_expr, .-asm_expr + + .section .note.GNU-stack,"",@progbits diff --git a/packages/runtime/test/test_unwind_frames.ll b/packages/runtime/test/test_unwind_frames.ll new file mode 100644 index 0000000000..08415e7253 --- /dev/null +++ b/packages/runtime/test/test_unwind_frames.ll @@ -0,0 +1,131 @@ +; Frames for test_unwind.c, shaped like emitted code on the unwinding +; exception ABI. Every frame takes a chain: a null-terminated array of frame +; functions, of which it calls the first with the rest. Each clobbers the +; callee-saved registers, so its prologue saves them and its CFI says where. +target triple = "x86_64-unknown-linux-gnu" + +declare void @scr_unwind_raise() cold +declare void @scr_unwind_resume(ptr) cold +declare i32 @__gcc_personality_v0(...) +declare void @test_cleanup(ptr) +declare void @test_touch(ptr) + +; Clobbers every callee-saved register the frame pointer leaves free. +define private void @clobber() alwaysinline { + call void asm sideeffect "movq $$0x6161616161616161, %rbx\0Amovq $$0x6262626262626262, %r12\0Amovq $$0x6363636363636363, %r13\0Amovq $$0x6464646464646464, %r14\0Amovq $$0x6565656565656565, %r15", "~{rbx},~{r12},~{r13},~{r14},~{r15}"() + ret void +} + +; A local that escapes, so sanitized builds give the frame redzones. +define private void @local() alwaysinline sanitize_address { + %buf = alloca [48 x i8], align 16 + call void @test_touch(ptr %buf) + ret void +} + +define void @ir_raise(ptr %chain) sanitize_address { + call void @local() + call void @clobber() + call void @scr_unwind_raise() + unreachable +} + +define void @ir_plain(ptr %chain) sanitize_address { + call void @local() + %fn = load ptr, ptr %chain + %rest = getelementptr ptr, ptr %chain, i64 1 + call void @clobber() + call void %fn(ptr %rest) + ret void +} + +define void @ir_plain_fp(ptr %chain) sanitize_address "frame-pointer"="all" { + call void @local() + %fn = load ptr, ptr %chain + %rest = getelementptr ptr, ptr %chain, i64 1 + call void @clobber() + call void %fn(ptr %rest) + ret void +} + +; A frame whose stack pointer moves by a runtime amount: its CFA is rbp's. +define void @ir_alloca(ptr %chain) sanitize_address { + %bits = ptrtoint ptr %chain to i64 + %n = and i64 %bits, 112 + %size = add i64 %n, 16 + %dyn = alloca i8, i64 %size, align 16 + call void @test_touch(ptr %dyn) + %fn = load ptr, ptr %chain + %rest = getelementptr ptr, ptr %chain, i64 1 + call void @clobber() + call void %fn(ptr %rest) + ret void +} + +; A frame with a landing pad: it records its chain position and continues +; the unwind. The chain position is live across the call. +define void @ir_pad(ptr %chain) sanitize_address personality ptr @__gcc_personality_v0 { +entry: + call void @local() + %fn = load ptr, ptr %chain + %rest = getelementptr ptr, ptr %chain, i64 1 + call void @clobber() + invoke void %fn(ptr %rest) to label %ok unwind label %lp +ok: + ret void +lp: + %e = landingpad { ptr, i32 } cleanup + call void @test_cleanup(ptr %chain) + %token = extractvalue { ptr, i32 } %e, 0 + call void @scr_unwind_resume(ptr %token) + unreachable +} + +define void @ir_pad_fp(ptr %chain) sanitize_address "frame-pointer"="all" personality ptr @__gcc_personality_v0 { +entry: + call void @local() + %fn = load ptr, ptr %chain + %rest = getelementptr ptr, ptr %chain, i64 1 + call void @clobber() + invoke void %fn(ptr %rest) to label %ok unwind label %lp +ok: + ret void +lp: + %e = landingpad { ptr, i32 } cleanup + call void @test_cleanup(ptr %chain) + %token = extractvalue { ptr, i32 } %e, 0 + call void @scr_unwind_resume(ptr %token) + unreachable +} + +; Takes the exception, as a pending-flag frame does: returns a checksum of +; six values live across the call, which hold the callee-saved registers, +; and whether the call raised in bit 63. +define i64 @ir_catch(ptr %chain, i64 %a, i64 %b, i64 %c, i64 %d, i64 %e, i64 %f) sanitize_address personality ptr @__gcc_personality_v0 { +entry: + call void @local() + %x1 = mul i64 %a, 3 + %x2 = mul i64 %b, 5 + %x3 = mul i64 %c, 7 + %x4 = mul i64 %d, 11 + %x5 = mul i64 %e, 13 + %x6 = mul i64 %f, 17 + %fn = load ptr, ptr %chain + %rest = getelementptr ptr, ptr %chain, i64 1 + invoke void %fn(ptr %rest) to label %ok unwind label %lp +ok: + br label %done +lp: + %l = landingpad { ptr, i32 } cleanup + br label %done +done: + %raised = phi i64 [ 0, %ok ], [ -9223372036854775808, %lp ] + %s1 = xor i64 %x1, %x2 + %s2 = add i64 %s1, %x3 + %s3 = xor i64 %s2, %x4 + %s4 = add i64 %s3, %x5 + %s5 = xor i64 %s4, %x6 + %s6 = and i64 %s5, 9223372036854775807 + %r = or i64 %s6, %raised + ret i64 %r +} diff --git a/packages/runtime/test/unwind.test.ts b/packages/runtime/test/unwind.test.ts new file mode 100644 index 0000000000..95fc5a0fd5 --- /dev/null +++ b/packages/runtime/test/unwind.test.ts @@ -0,0 +1,55 @@ +import { execFile } from "node:child_process"; +import { mkdir } from "node:fs/promises"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; + +const execFileAsync = promisify(execFile); +const testDir = import.meta.dirname; + +// scr_unwind.c walks with cached plans on x86-64 Linux only. +test.runIf(process.platform === "linux" && process.arch === "x64")( + "the unwinder runs landing pads in order, restores callee-saved registers and falls back to libgcc", + async () => { + const buildDir = join(testDir, "build"); + await mkdir(buildDir, { recursive: true }); + const frames = [join(testDir, "test_unwind_frames.ll"), join(testDir, "test_unwind_frames.S")]; + // The frames also run from a shared library, whose FDEs are in its own + // .eh_frame_hdr. + const library = join(buildDir, "libtest_unwind_frames.so"); + await execFileAsync("clang", ["-O1", "-shared", "-fPIC", "-o", library, ...frames]); + for (const build of ["plain", "asan", "tsan", "shared"] as const) { + const bin = join(buildDir, `test_unwind_${build}`); + await execFileAsync("clang", [ + "-std=c11", + "-O1", + "-Wall", + "-Wextra", + "-D_GNU_SOURCE", + "-DSCR_UNWIND_TESTING", + ...(build === "asan" ? ["-fsanitize=address"] : []), + ...(build === "tsan" ? ["-fsanitize=thread"] : []), + "-pthread", + "-o", + bin, + join(testDir, "test_unwind.c"), + ...(build === "shared" ? [library, `-Wl,-rpath,${buildDir}`] : frames), + join(testDir, "../src/scr_unwind.c"), + // The library's landing pads and the program share libgcc_s's unwinder. + ...(build === "shared" ? ["-shared-libgcc"] : ["-lgcc_eh"]), + ]); + const env = { + ...process.env, + ASAN_OPTIONS: "detect_leaks=0:halt_on_error=1", + TSAN_OPTIONS: "halt_on_error=1", + }; + // ThreadSanitizer checks the threads' concurrent plan inserts and + // lookups; the libgcc fallback cases run in the other builds. + for (const args of build === "tsan" ? [["threads"]] : [[], ["threads"]]) { + const run = await execFileAsync(bin, args, { env }); + expect(run.stdout).toBe("unwind checks passed\n"); + } + } + }, + 120_000, +); diff --git a/scripts/zero-cost-compare.mjs b/scripts/zero-cost-compare.mjs index 8d612db243..40f7287fbb 100644 --- a/scripts/zero-cost-compare.mjs +++ b/scripts/zero-cost-compare.mjs @@ -415,7 +415,9 @@ export function nodeIslandSymbols(names) { * whose parameters the lib types `any`: captures a match can leave unset * and results the checker types `any`. bench/record-untyped-writes, also * added later, writes typed records through `unknown` and `Object.assign` - * views. Every other bench workload is fully typed and must link none. The gate and + * views. bench/layered-errors, also added later, constructs and throws + * Error subclass instances, as bench/validate-errors does. Every other bench + * workload is fully typed and must link none. The gate and * tests/harness/bench-dynamic-tier.test.ts enforce that, and the test also * fails when a listed workload stops linking them, so remove a workload here * when a leak fix makes its binary clean. */ @@ -440,6 +442,7 @@ export const DYNAMIC_WORKLOADS = new Set([ "bench/throw-chains", "bench/regex-replacers", "bench/record-untyped-writes", + "bench/layered-errors", ]); /** Names of the functions (text and weak symbols) in `nm` output. Mach-O diff --git a/tests/corpus/unwind-landing-pad-chains.ts b/tests/corpus/unwind-landing-pad-chains.ts new file mode 100644 index 0000000000..ba605586dc --- /dev/null +++ b/tests/corpus/unwind-landing-pad-chains.ts @@ -0,0 +1,104 @@ +// Exceptions thrown below functions that hold references and propagate by +// unwinding: landing pads release a frame's strings and arrays and continue +// the unwind, also where the frame is inlined into a caller whose own handler +// takes the exception, and through methods, closures and deep chains. +function leaf(n: number): number { + if (n % 3 === 0) throw new RangeError("leaf " + n); + return n * 2; +} +function first(n: number): number { + return leaf(n) + 1; +} +function second(n: number): number { + return first(n) * 3; +} +function third(n: number): number { + return second(n) - 2; +} +function holder(n: number): number { + const parts: string[] = ["a" + n]; + const r = third(n); + parts.push("b"); + return r + parts.length; +} +function labelled(n: number): number { + const label = "u" + n; + return holder(n) + label.length; +} +function deeper(n: number, depth: number): number { + const name = "d" + depth; + if (depth === 0) return labelled(n) + name.length; + return deeper(n, depth - 1) + name.length; +} +class Counter { + total = 0; + add(n: number): void { + this.total += labelled(n); + } +} + +function caught(n: number): string { + try { + return "ok " + labelled(n); + } catch (e) { + return "caught " + (e as Error).message; + } +} + +const out: string[] = []; +for (let i = 0; i < 7; i++) out.push(caught(i)); +console.log(out.join(", ")); + +const counter = new Counter(); +let failures = 0; +for (let round = 0; round < 2000; round++) { + for (let i = 0; i < 6; i++) { + try { + counter.add(i); + } catch { + failures++; + } + } +} +console.log("counter", counter.total, "failures", failures); + +let deep = 0; +let deepFailures = 0; +for (let i = 0; i < 300; i++) { + try { + deep += deeper(i, i % 40); + } catch (e) { + deepFailures++; + if (i < 10) console.log((e as Error).name, (e as Error).message); + } +} +console.log("deep", deep, "failures", deepFailures); + +const viaClosure = (n: number): number => labelled(n) + 1; +let closureFailures = 0; +for (let i = 0; i < 30; i++) { + try { + viaClosure(i); + } catch { + closureFailures++; + } +} +console.log("closure failures", closureFailures); + +function finallyOrder(n: number): string { + const log: string[] = []; + try { + try { + log.push("try"); + labelled(n); + log.push("after"); + } finally { + log.push("finally"); + } + } catch (e) { + log.push("catch " + (e as Error).message); + } + return log.join(" > "); +} +console.log(finallyOrder(3)); +console.log(finallyOrder(4)); diff --git a/tests/harness/corpus-costs/unwind-landing-pad-chains.ts.json b/tests/harness/corpus-costs/unwind-landing-pad-chains.ts.json new file mode 100644 index 0000000000..97b189b08f --- /dev/null +++ b/tests/harness/corpus-costs/unwind-landing-pad-chains.ts.json @@ -0,0 +1 @@ +{ "plain": 4100, "san": 69700 }