Fix LLVM-tier link failure on net autosel timeouts; audit every emittable runtime symbol name

- LIB_FN_SYMS mapped net.getAutoSelTimeout/setAutoSelTimeout to scr_net_get_auto_sel_timeout/scr_net_set_auto_sel_timeout, but the runtime defines scr_net_get_autosel_timeout/scr_net_set_autosel_timeout (the C backend already used the correct spelling), so any program touching net.getDefaultAutoSelectFamilyAttemptTimeout on the LLVM tier died at link with undefined symbols.
- The ABI guard could not see this: the literal-declare source scan only checks fully-literal declare strings, and the emitted scan's corpus slice never exercises the net autosel surface — a table entry whose fixture doesn't exist was invisible from both directions.
- llvm-runtime-abi.test.ts grows a third, table-driven direction: every scr_* name the backend sources can ever emit — the static string tables plus every literal @scr_* template reference (interpolated per-type families excluded) — must exist in scr_runtime.h as a prototype or extern data symbol, so a two-string name skew now fails at test time instead of at the user's link step.
- parseHeader additionally collects extern data symbols (scr_emitter_vt, scr_error_vts) so vtable-global references audit cleanly.
- New corpus fixture 2500-net-autosel-timeout.ts round-trips the default attempt timeout (set then get, integers >= 10 to stay clear of Node's clamp) and is byte-identical vs Node through both backends.
This commit is contained in:
Chris Tate
2026-07-23 15:47:44 -05:00
parent 8ce27dfcb4
commit 586683ccae
3 changed files with 72 additions and 8 deletions
@@ -551,8 +551,8 @@ const LIB_FN_SYMS: Record<string, string> = {
"net.sockUnshift": "scr_net_sock_unshift_bytes",
"net.sockPipeRes": "scr_http_sock_pipe_res",
"net.serverEmitConnection": "scr_net_server_emit_connection",
"net.getAutoSelTimeout": "scr_net_get_auto_sel_timeout",
"net.setAutoSelTimeout": "scr_net_set_auto_sel_timeout",
"net.getAutoSelTimeout": "scr_net_get_autosel_timeout",
"net.setAutoSelTimeout": "scr_net_set_autosel_timeout",
"http.reqUrl": "scr_http_req_url",
"http.reqMethod": "scr_http_req_method",
"http.reqSocket": "scr_http_req_socket",
@@ -5,7 +5,7 @@
* type-checks its calls against the header), but a .ll `declare` is taken
* on faith by the linker, so a disagreement is silent UB that can run
* clean under one toolchain and misbehave under another. This test kills
* the class mechanically, from two directions:
* the class mechanically, from three directions:
*
* 1. Source scan: every fully-literal `declare ... @scr_*` template
* string in the backend/llvm sources is checked against the header.
@@ -13,6 +13,14 @@
* through the LLVM backend and every `declare @scr_*` in the emitted
* .ll — including the generic LIB_FN_SYMS path, whose signatures are
* derived from IR arg types per call site — is checked the same way.
* 3. Name-existence scan: every scr_* symbol name the backend sources
* can EVER put in a .ll — the static string tables (LIB_FN_SYMS and
* friends, whose signatures only exist per call site so neither scan
* above sees an unexercised entry) plus every literal @scr_* template
* reference — must exist in the header as a prototype or an extern
* data symbol. This is what catches a two-string name skew (table
* says scr_foo_bar, runtime defines scr_foobar) at test time instead
* of at the user's link step.
*
* The header parser fails LOUDLY on any C type it cannot map so it can
* never silently fall behind the header. */
@@ -112,10 +120,12 @@ function splitParams(argsText: string): string[] {
return out;
}
/** Parse scr_runtime.h into name → prototype. Only `;`-terminated
* prototypes count: a `static inline` definition has no linkage symbol the
* emitter could declare, so a declare against one must report as missing. */
async function parseHeader(): Promise<{ protos: Map<string, CProto> }> {
/** Parse scr_runtime.h into name → prototype, plus the extern data
* symbols (vtable globals and the like) the emitter references by name.
* Only `;`-terminated prototypes count: a `static inline` definition has
* no linkage symbol the emitter could declare, so a declare against one
* must report as missing. */
async function parseHeader(): Promise<{ protos: Map<string, CProto>; dataSyms: Set<string> }> {
const raw = await readFile(headerPath, "utf8");
const src = raw
.replace(/\/\*[\s\S]*?\*\//g, " ")
@@ -160,7 +170,11 @@ async function parseHeader(): Promise<{ protos: Map<string, CProto> }> {
const params = (variadic ? parts.slice(0, -1) : parts).map((p) => cParamToLl(p, types));
protos.set(name, { ret: cTypeToLl(retText, types), params, variadic });
}
return { protos };
const dataSyms = new Set<string>();
for (const m of src.matchAll(/\bextern\s+[^;(){}]*?\b(scr_[a-z0-9_]+)\s*(?:\[[^\]]*\])?\s*;/g)) {
dataSyms.add(m[1]!);
}
return { protos, dataSyms };
}
interface LlDeclare {
@@ -225,6 +239,37 @@ describe("LLVM backend declares match scr_runtime.h prototypes", () => {
expect(failures).toEqual([]);
});
test("every scr_* name the backend sources can emit exists in the header", async () => {
const { protos, dataSyms } = await parseHeader();
const names = new Map<string, string>(); // name → first file seen in
for (const file of await readdir(llvmSrcDir)) {
if (!file.endsWith(".ts")) continue;
const src = await readFile(join(llvmSrcDir, file), "utf8");
// The static string tables: every double-quoted scr_* literal is a
// symbol name some path can hand to the .ll (LIB_FN_SYMS et al.).
for (const m of src.matchAll(/"(scr_[a-z0-9_]+)"/g)) {
if (!names.has(m[1]!)) names.set(m[1]!, file);
}
// Literal @scr_* references inside template strings (declares AND
// call sites). A reference whose name continues with `${...}` is an
// interpolated per-type family (scr_arr_get_${suffix}) — the full
// name only exists per suffix, so those ride the emitted scan.
for (const m of src.matchAll(/@(scr_[a-z0-9_]+)/g)) {
if (src.startsWith("${", m.index + m[0].length)) continue;
if (!names.has(m[1]!)) names.set(m[1]!, file);
}
}
const failures: string[] = [];
for (const [name, file] of names) {
if (protos.has(name) || dataSyms.has(name)) continue;
failures.push(`${file}: ${name} — the backend can emit this symbol but scr_runtime.h declares no such prototype or extern data symbol`);
}
// Extractor guard: the tables alone carry hundreds of names — finding
// few means the scan rotted, not that the backend went quiet.
expect(names.size).toBeGreaterThan(400);
expect(failures).toEqual([]);
});
test("every declare emitted for the fs/path corpus slice", async () => {
const { protos } = await parseHeader();
const failures: string[] = [];
+19
View File
@@ -0,0 +1,19 @@
// node:net's process-wide happy-eyeballs attempt budget: the
// getDefaultAutoSelectFamilyAttemptTimeout/setDefaultAutoSelectFamilyAttemptTimeout
// pair round-trips one runtime double. Values stay integers >= 10 (Node
// clamps smaller settings up to 10ms; the fixture stays inside the
// clamp-free region so both implementations answer the stored value).
import {
getDefaultAutoSelectFamilyAttemptTimeout,
setDefaultAutoSelectFamilyAttemptTimeout,
} from "node:net";
setDefaultAutoSelectFamilyAttemptTimeout(300);
const a = getDefaultAutoSelectFamilyAttemptTimeout();
console.log(typeof a, a);
setDefaultAutoSelectFamilyAttemptTimeout(1234);
console.log(getDefaultAutoSelectFamilyAttemptTimeout());
setDefaultAutoSelectFamilyAttemptTimeout(10);
console.log(getDefaultAutoSelectFamilyAttemptTimeout());