mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
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:
@@ -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[] = [];
|
||||
|
||||
@@ -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());
|
||||
Reference in New Issue
Block a user