mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
fix(library): reject callback re-entry (#270)
Co-authored-by: mmamedel <23098414+mmamedel@users.noreply.github.com>
This commit is contained in:
@@ -4,6 +4,10 @@ All notable changes to scriptc will be documented in this file.
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Fixes
|
||||
|
||||
- **Library host-callback re-entry now traps deterministically.** Entering an export or library control/registration ABI symbol while a synchronous host callback is active delivers the structured `SC4026` diagnostic to the existing panic sink, attributes the attempted inner symbol, and poisons only that library instance.
|
||||
|
||||
### Features
|
||||
|
||||
- **macOS arm64 executables use release-built runtime packs.** LLVM-tier builds now emit the program object through the bundled helper and link feature-selected, hashed runtime/vendor artifacts without compiling C on the user's machine. Explicit C, LLVM fallback, and sanitizer builds retain the external C-toolchain path.
|
||||
|
||||
@@ -1135,6 +1135,7 @@ export class CEmitter {
|
||||
`}`,
|
||||
``,
|
||||
`void ${lib.sinkRegisterSymbol}(void (*fn)(void *ctx, const uint8_t *msg, size_t msg_len, uint64_t address), void *ctx) {`,
|
||||
` scr_library_callback_entry_guard("${lib.sinkRegisterSymbol}");`,
|
||||
` scr_library_set_sink(fn, ctx);`,
|
||||
`}`,
|
||||
``,
|
||||
@@ -1142,11 +1143,13 @@ export class CEmitter {
|
||||
if (lib.callbacks !== undefined && lib.callbacks.length > 0) {
|
||||
// Host-callback registration: a pure store dispatch (the sink
|
||||
// registration's rule — no entry prologue, no poison guard, legal
|
||||
// before init). The channel name selects the slot; an unknown or
|
||||
// before init) except the first operation rejects callback-time
|
||||
// re-entry (SC4026). The channel name selects the slot; an unknown or
|
||||
// NULL name is a defined -1, never a store. Latest registration
|
||||
// wins; a NULL fn clears the channel.
|
||||
out.push(
|
||||
`int32_t ${lib.callbackRegisterSymbol}(const char *name, void (*fn)(void), void *ctx) {`,
|
||||
` scr_library_callback_entry_guard("${lib.callbackRegisterSymbol}");`,
|
||||
` if (name == NULL) return -1;`,
|
||||
);
|
||||
for (const cb of lib.callbacks) {
|
||||
|
||||
@@ -2095,7 +2095,9 @@ function emitCallExpr(
|
||||
// channel (the library lane loads no native-FFI manifest). The
|
||||
// dispatch fetches the slot's registered pointer — or delivers the
|
||||
// channel's unregistered-call trap through the funnel (SC4025) —
|
||||
// then makes the typed indirect call, opaque context first.
|
||||
// then brackets only the typed indirect call, opaque context first.
|
||||
// The bracket makes callback-time ABI re-entry a deterministic
|
||||
// SC4026 trap before an inner entry can mutate runtime state.
|
||||
// Marshalling matches the native ffiCall's value classes exactly:
|
||||
// buffers are borrowed (ptr, len) for the call's duration, the
|
||||
// u8/u32/i32 plumbing classes ride JS's ToUint32/ToInt32, and a
|
||||
@@ -2105,35 +2107,56 @@ function emitCallExpr(
|
||||
if (libCb !== undefined) {
|
||||
const cbArgs = e.args.map((arg) => emitter.emitExpr(arg));
|
||||
const natTypes: string[] = ["void *"];
|
||||
const natArgs: string[] = [`scr_library_cb_ctx(${libCb.slot})`];
|
||||
const natArgs: string[] = [];
|
||||
libCb.params.forEach((cls, i) => {
|
||||
const arg = cbArgs[i]!;
|
||||
const native = (): string => `sc_t${emitter.tempCounter++}`;
|
||||
switch (cls) {
|
||||
case "f64":
|
||||
case "f64": {
|
||||
const value = native();
|
||||
emitter.line(`double ${value} = ${arg.name};`);
|
||||
natTypes.push("double");
|
||||
natArgs.push(arg.name);
|
||||
natArgs.push(value);
|
||||
break;
|
||||
case "bool":
|
||||
}
|
||||
case "bool": {
|
||||
const value = native();
|
||||
emitter.line(`uint8_t ${value} = (uint8_t)(${arg.name} ? 1 : 0);`);
|
||||
natTypes.push("uint8_t");
|
||||
natArgs.push(`(uint8_t)(${arg.name} ? 1 : 0)`);
|
||||
natArgs.push(value);
|
||||
break;
|
||||
case "u8":
|
||||
}
|
||||
case "u8": {
|
||||
const value = native();
|
||||
emitter.line(`uint8_t ${value} = (uint8_t)(uint32_t)scr_bit_ushr(${arg.name}, 0.0);`);
|
||||
natTypes.push("uint8_t");
|
||||
natArgs.push(`(uint8_t)(uint32_t)scr_bit_ushr(${arg.name}, 0.0)`);
|
||||
natArgs.push(value);
|
||||
break;
|
||||
case "u32":
|
||||
}
|
||||
case "u32": {
|
||||
const value = native();
|
||||
emitter.line(`uint32_t ${value} = (uint32_t)scr_bit_ushr(${arg.name}, 0.0);`);
|
||||
natTypes.push("uint32_t");
|
||||
natArgs.push(`(uint32_t)scr_bit_ushr(${arg.name}, 0.0)`);
|
||||
natArgs.push(value);
|
||||
break;
|
||||
case "i32":
|
||||
}
|
||||
case "i32": {
|
||||
const value = native();
|
||||
emitter.line(`int32_t ${value} = (int32_t)scr_bit_or(${arg.name}, 0.0);`);
|
||||
natTypes.push("int32_t");
|
||||
natArgs.push(`(int32_t)scr_bit_or(${arg.name}, 0.0)`);
|
||||
natArgs.push(value);
|
||||
break;
|
||||
}
|
||||
case "string":
|
||||
case "bytes":
|
||||
case "bytes": {
|
||||
const ptr = native();
|
||||
const len = native();
|
||||
emitter.line(`const uint8_t *${ptr} = (const uint8_t *)${arg.name}->data;`);
|
||||
emitter.line(`size_t ${len} = ${arg.name}->len;`);
|
||||
natTypes.push("const uint8_t *", "size_t");
|
||||
natArgs.push(`(const uint8_t *)${arg.name}->data`, `${arg.name}->len`);
|
||||
natArgs.push(ptr, len);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
const retC =
|
||||
@@ -2143,18 +2166,42 @@ function emitCallExpr(
|
||||
: libCb.returns === "u32" ? "uint32_t"
|
||||
: "int32_t";
|
||||
const trapLit = cStringLiteral(Buffer.from(libCb.unregisteredTrap, "utf8"));
|
||||
const target = `((${retC} (*)(${natTypes.join(", ")}))scr_library_cb_require(${libCb.slot}, ${trapLit}))`;
|
||||
const call = `${target}(${natArgs.join(", ")})`;
|
||||
// Materialize the pointer and context before the callback-active
|
||||
// bracket. This keeps the existing SC4025 fetch path outside the
|
||||
// bracket and avoids C argument evaluation-order ambiguity.
|
||||
const fn = `sc_t${emitter.tempCounter++}`;
|
||||
emitter.line(`${retC} (*${fn})(${natTypes.join(", ")}) = (${retC} (*)(${natTypes.join(", ")}))scr_library_cb_require(${libCb.slot}, ${trapLit});`);
|
||||
const ctx = `sc_t${emitter.tempCounter++}`;
|
||||
emitter.line(`void *${ctx} = scr_library_cb_ctx(${libCb.slot});`);
|
||||
const call = `${fn}(${[ctx, ...natArgs].join(", ")})`;
|
||||
switch (libCb.returns) {
|
||||
case "void":
|
||||
emitter.line(`scr_library_callback_begin();`);
|
||||
emitter.line(`${call};${emitter.srcComment(e.loc)}`);
|
||||
emitter.line(`scr_library_callback_end();`);
|
||||
return { name: "", type: e.type };
|
||||
case "f64":
|
||||
return emitter.newTemp(e.type, call);
|
||||
case "f64": {
|
||||
const raw = `sc_t${emitter.tempCounter++}`;
|
||||
emitter.line(`scr_library_callback_begin();`);
|
||||
emitter.line(`${retC} ${raw} = ${call};${emitter.srcComment(e.loc)}`);
|
||||
emitter.line(`scr_library_callback_end();`);
|
||||
return emitter.newTemp(e.type, raw);
|
||||
}
|
||||
case "bool":
|
||||
return emitter.newTemp(e.type, `(${call} != 0)`);
|
||||
default: // u8/u32/i32 — exact widenings back to f64
|
||||
return emitter.newTemp(e.type, `(double)${call}`);
|
||||
{
|
||||
const raw = `sc_t${emitter.tempCounter++}`;
|
||||
emitter.line(`scr_library_callback_begin();`);
|
||||
emitter.line(`${retC} ${raw} = ${call};${emitter.srcComment(e.loc)}`);
|
||||
emitter.line(`scr_library_callback_end();`);
|
||||
return emitter.newTemp(e.type, `(${raw} != 0)`);
|
||||
}
|
||||
default: { // u8/u32/i32 — exact widenings back to f64
|
||||
const raw = `sc_t${emitter.tempCounter++}`;
|
||||
emitter.line(`scr_library_callback_begin();`);
|
||||
emitter.line(`${retC} ${raw} = ${call};${emitter.srcComment(e.loc)}`);
|
||||
emitter.line(`scr_library_callback_end();`);
|
||||
return emitter.newTemp(e.type, `(double)${raw}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
const entry = emitter.ffiByName.get(e.import);
|
||||
|
||||
@@ -998,6 +998,7 @@ class LlEmitter {
|
||||
this.declare(`declare void @scr_library_reset()`);
|
||||
this.declare(`declare void @scr_library_check_exc()`);
|
||||
this.declare(`declare void @scr_library_set_sink(ptr, ptr)`);
|
||||
this.declare(`declare void @scr_library_callback_entry_guard(ptr)`);
|
||||
this.declare(`declare void @scr_library_arena_reset()`);
|
||||
this.declare(`declare void @scr_library_collect()`);
|
||||
if ((this.mod.lib.callbacks?.length ?? 0) > 0) {
|
||||
@@ -1429,6 +1430,8 @@ class LlEmitter {
|
||||
out.push(`${symConst(sym)} = internal constant [${Buffer.byteLength(sym, "utf8") + 1} x i8] c"${llStrBytes(sym)}"`);
|
||||
};
|
||||
emitSymConst(lib.initSymbol);
|
||||
emitSymConst(lib.sinkRegisterSymbol);
|
||||
if (lib.callbackRegisterSymbol !== null && lib.callbackRegisterSymbol !== undefined) emitSymConst(lib.callbackRegisterSymbol);
|
||||
if (lib.resultResetSymbol !== null) emitSymConst(lib.resultResetSymbol);
|
||||
if (lib.collectSymbol !== null) emitSymConst(lib.collectSymbol);
|
||||
for (const e of lib.exports) emitSymConst(e.symbol);
|
||||
@@ -1485,6 +1488,7 @@ class LlEmitter {
|
||||
``,
|
||||
`define void @${lib.sinkRegisterSymbol}(ptr %fn, ptr %ctx) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
` call void @scr_library_callback_entry_guard(ptr ${symConst(lib.sinkRegisterSymbol)})`,
|
||||
` call void @scr_library_set_sink(ptr %fn, ptr %ctx)`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
@@ -1497,7 +1501,8 @@ class LlEmitter {
|
||||
// scr_library_cb_require operands — same bytes as the C emission by
|
||||
// construction), and the registration define: a pure store dispatch
|
||||
// (the sink registration's rule — no entry prologue, no poison
|
||||
// guard). An unknown or NULL name is a defined -1, never a store.
|
||||
// guard) whose first operation rejects callback-time re-entry
|
||||
// (SC4026). An unknown or NULL name is a defined -1, never a store.
|
||||
for (const cb of lib.callbacks) {
|
||||
out.push(
|
||||
`@sc_lib_cb_name_${cb.slot} = internal constant [${Buffer.byteLength(cb.name, "utf8") + 1} x i8] c"${llStrBytes(cb.name)}"`,
|
||||
@@ -1508,6 +1513,7 @@ class LlEmitter {
|
||||
``,
|
||||
`define i32 @${lib.callbackRegisterSymbol}(ptr %name, ptr %fn, ptr %ctx) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
` call void @scr_library_callback_entry_guard(ptr ${symConst(lib.callbackRegisterSymbol!)})`,
|
||||
` %isnull = icmp eq ptr %name, null`,
|
||||
` br i1 %isnull, label %miss, label %try0`,
|
||||
);
|
||||
|
||||
@@ -42,8 +42,9 @@ export function emitCallExpr(host: LlvmEmitterContext, e: ExprOf<"call" | "ffiCa
|
||||
// channel (the library lane loads no native-FFI manifest). Fetch
|
||||
// the slot's registered pointer — scr_library_cb_require delivers
|
||||
// the channel's trap constant through the funnel (SC4025) when the
|
||||
// host never registered — then the typed indirect call, opaque
|
||||
// context first. Marshalling matches the native ffiCall's value
|
||||
// host never registered — then brackets the typed indirect call,
|
||||
// opaque context first. The bracket makes callback-time ABI re-entry
|
||||
// a deterministic SC4026 trap. Marshalling matches the native ffiCall's value
|
||||
// classes exactly; the host cannot raise a scriptc exception, so
|
||||
// no pending check follows.
|
||||
const libCb = host.mod.lib?.callbacks?.find((c) => c.name === e.import);
|
||||
@@ -121,6 +122,8 @@ export function emitCallExpr(host: LlvmEmitterContext, e: ExprOf<"call" | "ffiCa
|
||||
});
|
||||
host.declare(`declare ptr @scr_library_cb_require(${host.sizeType}, ptr)`);
|
||||
host.declare(`declare ptr @scr_library_cb_ctx(${host.sizeType})`);
|
||||
host.declare(`declare void @scr_library_callback_begin()`);
|
||||
host.declare(`declare void @scr_library_callback_end()`);
|
||||
const fn = B.tmp();
|
||||
B.line(`${fn} = call ptr @scr_library_cb_require(${host.sizeType} ${libCb.slot}, ptr @sc_lib_cb_trap_${libCb.slot})`);
|
||||
const ctx = B.tmp();
|
||||
@@ -128,11 +131,15 @@ export function emitCallExpr(host: LlvmEmitterContext, e: ExprOf<"call" | "ffiCa
|
||||
const retTy = ffiNativeTypeLl(libCb.returns);
|
||||
const call = `call ${retTy} ${fn}(${[`ptr ${ctx}`, ...natArgs].join(", ")})`;
|
||||
if (libCb.returns === "void") {
|
||||
B.line(`call void @scr_library_callback_begin()`);
|
||||
B.line(call);
|
||||
B.line(`call void @scr_library_callback_end()`);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
const raw = B.tmp();
|
||||
B.line(`call void @scr_library_callback_begin()`);
|
||||
B.line(`${raw} = ${call}`);
|
||||
B.line(`call void @scr_library_callback_end()`);
|
||||
if (libCb.returns === "f64") return { name: raw, type: e.type };
|
||||
if (libCb.returns === "bool") {
|
||||
const value = B.tmp();
|
||||
|
||||
@@ -42,9 +42,11 @@
|
||||
* not fit ±(2^53 − 1), or is negative at a u64 slot), the
|
||||
* host-callback surface (SC4024 — a signature-only ambient
|
||||
* function reference the profile's callbacks section cannot
|
||||
* serve), and the unregistered-callback runtime trap (SC4025 —
|
||||
* a structured trap-teaching code in the funnel-classified
|
||||
* family, not a refusal)
|
||||
* serve), the unregistered-callback runtime trap (SC4025 — a
|
||||
* structured trap-teaching code in the funnel-classified family,
|
||||
* not a refusal), and callback-time library re-entry (SC4026 —
|
||||
* the distinct detected trap for an attempted ABI entry while a
|
||||
* host callback handler is active)
|
||||
* SC5xxx native FFI: malformed manifests (SC5001), a configured
|
||||
* binding that is not an ambient function declaration
|
||||
* (SC5002), and a TypeScript signature that does not match its
|
||||
@@ -1002,6 +1004,10 @@ export const LIB_INBOUND_BYTES_TRAP_CODE = "SC4012";
|
||||
* like SC4012, but the trap site is inside compiled code, so
|
||||
* the funnel assembles it and field 2 names the entry the
|
||||
* host called)
|
||||
* SC4026 library ABI entry invoked from a host callback ("scriptc:
|
||||
* library entry ..."): the callback-time guard poisons the
|
||||
* affected instance and the funnel assembles the attempted inner
|
||||
* ABI symbol in field 2
|
||||
*
|
||||
* There is no arithmetic/div-by-zero kind: JS division never traps, so the
|
||||
* runtime has no such site. The list here is the compile-time face of the
|
||||
@@ -1016,6 +1022,7 @@ export const LIB_RUNTIME_TRAP_CODES = [
|
||||
"SC4018",
|
||||
"SC4019",
|
||||
"SC4025",
|
||||
"SC4026",
|
||||
] as const;
|
||||
|
||||
/** SC4024 — a host-callback reference the profile cannot serve. Library
|
||||
|
||||
@@ -206,6 +206,9 @@
|
||||
* refusal, never a store). Latest registration wins; a NULL fn clears the
|
||||
* channel; registration is a pure store — no entry prologue, no poison
|
||||
* guard, legal before init — and registrations persist across init/reset.
|
||||
* Its first operation still rejects an active host callback, before even a
|
||||
* NULL-name check or dispatch, so callback-time registration cannot mutate
|
||||
* a slot or hide a re-entry error.
|
||||
* Calling a channel the host never registered is the SC4025 runtime trap
|
||||
* through the panic sink (structured, naming the channel in the text and
|
||||
* the entry the host called in the symbol field): register every channel
|
||||
@@ -220,11 +223,17 @@
|
||||
*
|
||||
* Reentrancy is pinned like the sink's rule: a callback runs on the
|
||||
* calling thread, inside the entry's dynamic extent, and must NOT call
|
||||
* back into any library entry (the registration symbols included) or
|
||||
* unwind/longjmp across library frames — read the borrowed buffers, hand
|
||||
* the bytes to the embedder's own structures, return. The async_free
|
||||
* posture is unchanged: a channel adds no event loop, no threads, and no
|
||||
* reentry into the archive.
|
||||
* back into any library entry (exports, init, result reset, collect, panic
|
||||
* sink registration, or callback registration) or unwind/longjmp across
|
||||
* library frames — read the borrowed buffers, hand the bytes to the
|
||||
* embedder's own structures, return. An attempted entry is SC4026: it
|
||||
* poisons only the affected instance, delivers once through the registered
|
||||
* sink, and places the attempted inner ABI symbol in structured field 2;
|
||||
* a sink that returns aborts. A later host-loop turn may enter after the
|
||||
* callback returns. The sidecar identity getters are the explicit pure-data
|
||||
* exception: they touch no mutable runtime state and remain callable before
|
||||
* init and after poison. The async_free posture is unchanged: a channel
|
||||
* adds no event loop, no threads, and no reentry into the archive.
|
||||
*
|
||||
* Marshalling classes (design §4.2 + session ruling 3 + ask 4): f64, bool,
|
||||
* string, bytes for params and returns; u8/u32/i32 are PARAM-ONLY plumbing
|
||||
|
||||
@@ -64,6 +64,10 @@ void scr_library_set_sink(ScrLibSinkFn fn, void *ctx) {
|
||||
|
||||
static SCR_TL ScrLibCbFn scr_library_cb_fns[SCR_LIB_MAX_CALLBACKS];
|
||||
static SCR_TL void *scr_library_cb_ctxs[SCR_LIB_MAX_CALLBACKS];
|
||||
/* A typed host callback runs synchronously inside one ABI entry. Like the
|
||||
* sink, slots, poison, arena, and current-entry symbol, this belongs to the
|
||||
* localized / thread-instanced library copy rather than the process. */
|
||||
static SCR_TL size_t scr_library_callback_depth = 0;
|
||||
|
||||
void scr_library_cb_set(size_t slot, ScrLibCbFn fn, void *ctx) {
|
||||
/* A pure store, the sink registration's rule: latest wins, NULL clears,
|
||||
@@ -82,6 +86,15 @@ ScrLibCbFn scr_library_cb_require(size_t slot, const char *trap_msg) {
|
||||
|
||||
void *scr_library_cb_ctx(size_t slot) { return scr_library_cb_ctxs[slot]; }
|
||||
|
||||
void scr_library_callback_begin(void) { scr_library_callback_depth++; }
|
||||
|
||||
void scr_library_callback_end(void) {
|
||||
/* Generated calls pair this only with a normally returned host callback.
|
||||
* A callback that illegally unwinds leaves the depth set, so a later ABI
|
||||
* entry fails deterministically instead of silently reusing the instance. */
|
||||
if (scr_library_callback_depth != 0) scr_library_callback_depth--;
|
||||
}
|
||||
|
||||
/* ── the trap funnel, library expansion ───────────────────────────────────
|
||||
* Poison first (the sink may longjmp to a host frame below the entry — the
|
||||
* conforming survival pattern), then deliver exactly once, then abort:
|
||||
@@ -111,8 +124,9 @@ void *scr_library_cb_ctx(size_t slot) { return scr_library_cb_ctxs[slot]; }
|
||||
* external symbol before dispatching into core code. A single static slot
|
||||
* is sound — exactly one core is live per copy of this state (the sole
|
||||
* core in a classic process; each archive's own instance under
|
||||
* abi.localize_runtime, where this slot is a per-instance local), entries
|
||||
* never nest, and a trap can only fire while an entry is on the stack.
|
||||
* abi.localize_runtime, where this slot is a per-instance local), re-entry
|
||||
* from a host callback is rejected before core work begins, and a trap can
|
||||
* only fire while an entry is on the stack.
|
||||
* NULL (never entered) renders as the empty symbol field. */
|
||||
static SCR_TL const char *scr_library_entry_symbol = NULL;
|
||||
|
||||
@@ -134,6 +148,7 @@ static const struct {
|
||||
{"scriptc: out of memory", "SC4017"}, /* allocation failure */
|
||||
{"scriptc: internal error: ", "SC4018"}, /* internal invariant failure */
|
||||
{"scriptc: library callback ", "SC4025"}, /* unregistered host callback */
|
||||
{"scriptc: library entry ", "SC4026"}, /* host callback re-entry */
|
||||
};
|
||||
|
||||
static const char *scr_library_trap_code(const char *msg, size_t len) {
|
||||
@@ -229,11 +244,28 @@ __attribute__((noinline)) _Noreturn void scr_trap_fmt(const char *fmt, ...) {
|
||||
|
||||
/* ── entry prologues ──────────────────────────────────────────────────── */
|
||||
|
||||
static _Noreturn void scr_library_callback_reentry(const char *entry_symbol) {
|
||||
/* Attribute the detected trap to the attempted INNER ABI symbol. Clear
|
||||
* depth before delivery: a conforming sink may longjmp to a host recovery
|
||||
* frame, and its poisoned instance must not retain stale callback-active
|
||||
* state that would change the established pure-registration behavior. */
|
||||
scr_library_entry_symbol = entry_symbol;
|
||||
scr_library_callback_depth = 0;
|
||||
scr_trap_fmt("scriptc: library entry '%s' invoked from a host callback\n", entry_symbol);
|
||||
}
|
||||
|
||||
void scr_library_callback_entry_guard(const char *entry_symbol) {
|
||||
if (scr_library_callback_depth != 0) scr_library_callback_reentry(entry_symbol);
|
||||
}
|
||||
|
||||
void scr_library_entry(bool reset_arena, const char *entry_symbol) {
|
||||
/* Record the entry symbol FIRST so even a poisoned-abort's core dump
|
||||
* names the entry; a trap anywhere below (the arena reset's OOM
|
||||
* included) then reports the right symbol. */
|
||||
* names the entry; a trap anywhere below (the arena reset's OOM
|
||||
* included) then reports the right symbol. */
|
||||
scr_library_entry_symbol = entry_symbol;
|
||||
/* Re-entry must win over poison and reset: no inner ABI entry may mutate
|
||||
* globals, release arena data, collect, or replace state from a callback. */
|
||||
if (scr_library_callback_depth != 0) scr_library_callback_reentry(entry_symbol);
|
||||
/* A poisoned library's entries abort deterministically — never through the
|
||||
* sink again (it received its exactly-once message when the trap fired),
|
||||
* never into a heap whose invariants already failed. */
|
||||
|
||||
@@ -108,7 +108,8 @@ typedef struct ScrBytes ScrBytes;
|
||||
* Every trap the runtime DETECTS arrives structured: the funnel assembles
|
||||
* the baseline human line into field 0 unchanged, a stable code for the
|
||||
* trap kind (the compiler registry's runtime family — SC4013–SC4019 plus
|
||||
* the SC4025 unregistered-callback trap, classified in scr_library.c), the
|
||||
* the SC4025 unregistered-callback and SC4026 callback-re-entry traps,
|
||||
* classified in scr_library.c), the
|
||||
* entry symbol recorded by the trapping
|
||||
* entry's prologue, and the profile's remediation for that code when the
|
||||
* program TU's overlay table declares one (the whole fourth field is
|
||||
@@ -133,15 +134,22 @@ void scr_library_set_sink(ScrLibSinkFn fn, void *ctx); /* latest wins */
|
||||
*
|
||||
* Registration is a pure store like the sink's (no entry prologue, no
|
||||
* poison guard, legal before init); latest wins, NULL clears, and
|
||||
* registrations persist across init/reset. Slots are per-copy of this
|
||||
* registrations persist across init/reset. While a host callback is active,
|
||||
* its registration entry is rejected before name dispatch or a store, just
|
||||
* like every runtime-touching ABI entry. Slots are per-copy of this
|
||||
* state, exactly the sink's story: per-archive under abi.localize_runtime,
|
||||
* per-thread instance under abi.instance_per_thread (SCR_TL) — a callback
|
||||
* registered on thread T fires only for T's instance. The host's callback
|
||||
* runs on the calling thread inside the entry's dynamic extent and must
|
||||
* NOT call back into any library entry (registration symbols included) or
|
||||
* unwind/longjmp across library frames: read the borrowed buffers, copy
|
||||
* what outlives the call, return. Buffer parameters are borrowed for the
|
||||
* duration of the call only. */
|
||||
* NOT call back into any library entry (exports, init, reset, collect, sink
|
||||
* registration, or callback registration) or unwind/longjmp across library
|
||||
* frames: read the borrowed buffers, copy what outlives the call, return.
|
||||
* A re-entry is a detected SC4026 trap: it poisons only this library
|
||||
* instance, delivers exactly once to the already-registered sink, names the
|
||||
* attempted inner ABI symbol in structured field 2, then aborts if the sink
|
||||
* returns. A later host-loop turn may enter normally after the callback has
|
||||
* returned. Buffer parameters are borrowed for the duration of the call
|
||||
* only. */
|
||||
#define SCR_LIB_MAX_CALLBACKS 32 /* keep in step with LIB_MAX_CALLBACKS (library/library-profile.ts) */
|
||||
/* The stored shape: generated call sites cast a slot's pointer to the
|
||||
* channel's typed shape before calling. */
|
||||
@@ -151,6 +159,15 @@ void scr_library_cb_set(size_t slot, ScrLibCbFn fn, void *ctx);
|
||||
* trap_msg (never returns NULL). */
|
||||
ScrLibCbFn scr_library_cb_require(size_t slot, const char *trap_msg);
|
||||
void *scr_library_cb_ctx(size_t slot);
|
||||
/* Generated typed call sites bracket only the actual host-function call.
|
||||
* End is reached only after a normal return; an illegal unwind deliberately
|
||||
* leaves the depth active so the next ABI entry is rejected. */
|
||||
void scr_library_callback_begin(void);
|
||||
void scr_library_callback_end(void);
|
||||
/* Registration wrappers bypass scr_library_entry because their normal path
|
||||
* is a pure store. They call this first so callback-time registration is
|
||||
* rejected before dispatch, NULL handling, or mutation. */
|
||||
void scr_library_callback_entry_guard(const char *entry_symbol);
|
||||
|
||||
/* Entry prologue: aborts deterministically when the library is poisoned (a
|
||||
* trap already fired — no profile entry may run again; recovery is process
|
||||
@@ -159,10 +176,11 @@ void *scr_library_cb_ctx(size_t slot);
|
||||
* entry_symbol is the generated entry's external symbol exactly as the
|
||||
* host linked it (a static string in the program TU): the prologue records
|
||||
* it in the funnel's current-entry slot so a detected trap's structured
|
||||
* message can name the trapping entry — sound as a single static slot
|
||||
* because exactly one core is ever live and entries never nest. Init and
|
||||
* the mode entries (reset, collect) record theirs too; the identity
|
||||
* getters and sink registration touch no runtime and never trap. */
|
||||
* message can name the trapping entry. A host callback's attempted nested
|
||||
* entry is rejected first and replaces this slot with that inner symbol.
|
||||
* Init and the mode entries (reset, collect) record theirs too. The two
|
||||
* profile identity getters are the explicit pure-data exception: they touch
|
||||
* no mutable runtime state and remain callable before init and after poison. */
|
||||
void scr_library_entry(bool reset_arena, const char *entry_symbol);
|
||||
void scr_library_arena_reset(void);
|
||||
/* The mode-provided collect entry's body: arena reset + a full cycle
|
||||
@@ -200,7 +218,8 @@ _Noreturn void scr_trap_len(const char *msg, size_t len);
|
||||
* (both emissions emit identical data) and consumed by the funnel when it
|
||||
* assembles a detected trap's structured message: flat triples of
|
||||
* (code, teaching-or-NULL, remediation-or-NULL), one per runtime trap code
|
||||
* (the SC4013–SC4019 family plus SC4025) the profile declares text for;
|
||||
* (the SC4013–SC4019 family plus SC4025 and SC4026) the profile declares
|
||||
* text for;
|
||||
* _len counts triples. A declared teaching replaces the baseline human line as field 0;
|
||||
* a declared remediation becomes the optional fourth field. */
|
||||
extern const char *const scr_library_trap_overlays[];
|
||||
|
||||
@@ -56,7 +56,12 @@
|
||||
* once while B streams through and after the
|
||||
* window; the localized external surface is
|
||||
* exactly the declared set
|
||||
* CB8 sanitized lane CB1 and CB2 re-run under ASan
|
||||
* CB8 callback re-entry every ABI entry is rejected as SC4026 while a
|
||||
* host callback is active; the attempted INNER
|
||||
* symbol wins, the original sink sees it, pure
|
||||
* registration retains its post-poison behavior,
|
||||
* and an independent thread instance survives
|
||||
* CB9 sanitized lane CB1, CB2, and callback re-entry re-run under ASan
|
||||
*/
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
@@ -88,6 +93,8 @@ interface BuildOpts {
|
||||
entry?: string;
|
||||
/** Drop the callbacks surface entirely (CB6's callback-free posture). */
|
||||
stripCallbacks?: boolean;
|
||||
/** Keep only exports the alternate callback fixture implements. */
|
||||
stripBufferedExport?: boolean;
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
@@ -112,6 +119,9 @@ async function buildLibrary(
|
||||
delete profile.callbacks;
|
||||
delete profile.abi["callback_register_symbol"];
|
||||
}
|
||||
if (opts.stripBufferedExport === true) {
|
||||
profile.exports = (profile.exports as { export: string }[]).filter((e) => e.export !== "buffered");
|
||||
}
|
||||
const profilePath = join(outDir, "profile.json");
|
||||
writeFileSync(profilePath, JSON.stringify(profile, null, 2));
|
||||
const result = await compileLibrary({ profilePath, outDir, sanitize: opts.sanitize ?? false });
|
||||
@@ -163,7 +173,16 @@ function buildProbe(
|
||||
}
|
||||
|
||||
function runProbe(bin: string, args: string[] = []): { stdout: string; status: number | null; signal: string | null } {
|
||||
const r = spawnSync(bin, args, { encoding: "utf8", timeout: 60_000 });
|
||||
// Library poison survival intentionally uses sink longjmp, which abandons
|
||||
// the active outer operation by contract. LeakSanitizer cannot model that
|
||||
// non-local recovery, while ASan still checks the memory-safety paths.
|
||||
const r = spawnSync(bin, args, {
|
||||
encoding: "utf8",
|
||||
timeout: 60_000,
|
||||
env: process.env["SCRIPTC_SAN"] === "1" || bin.includes("-san/")
|
||||
? { ...process.env, ASAN_OPTIONS: "detect_leaks=0" }
|
||||
: undefined,
|
||||
});
|
||||
return { stdout: r.stdout ?? "", status: r.status, signal: r.signal };
|
||||
}
|
||||
|
||||
@@ -230,10 +249,35 @@ survived, sink_calls=1
|
||||
`;
|
||||
|
||||
const CALLBACK_SYMBOLS = [
|
||||
"cb_init", "cb_set_panic_sink", "cb_collect", "cb_set_callback",
|
||||
"cb_stream", "cb_ask_host", "cb_poke_orphan",
|
||||
"cb_init", "cb_set_panic_sink", "cb_collect", "cb_reset_results", "cb_set_callback",
|
||||
"cb_stream", "cb_buffered", "cb_ask_host", "cb_poke_orphan",
|
||||
];
|
||||
|
||||
const REENTRY_SYMBOLS: Record<string, string> = {
|
||||
"reenter-export": "cb_stream",
|
||||
"reenter-init": "cb_init",
|
||||
"reenter-reset": "cb_reset_results",
|
||||
"reenter-collect": "cb_collect",
|
||||
"reenter-sink": "cb_set_panic_sink",
|
||||
"reenter-callback-unknown": "cb_set_callback",
|
||||
};
|
||||
|
||||
function reentryExpected(symbol: string, overlay = false): string {
|
||||
const text = overlay ? "return from the callback before calling the library" : `scriptc: library entry '${symbol}' invoked from a host callback\n`;
|
||||
const remediation = overlay ? "schedule the operation for a later host-loop turn" : undefined;
|
||||
return `callbacks ready
|
||||
sink[1]:
|
||||
text=[${text}]
|
||||
code=[SC4026]
|
||||
symbol=[${symbol}]
|
||||
${remediation === undefined ? "" : `remediation=[${remediation}]\n`}fields=${overlay ? 4 : 3} text_printable=1
|
||||
addr: nonzero
|
||||
post-poison register: 0
|
||||
replacement-sink-calls=0
|
||||
saved-result=[buffer 7]
|
||||
`;
|
||||
}
|
||||
|
||||
describe.each(EMISSIONS)("library host callbacks, %s emission", (emission) => {
|
||||
platformTest("CB1/CB4: the acceptance run, symbol exactness, ambient audit", async () => {
|
||||
const { archive, outDir } = await buildLibrary(emission);
|
||||
@@ -296,6 +340,31 @@ survived, sink_calls=1
|
||||
`);
|
||||
});
|
||||
|
||||
platformTest("CB8: callback-time entries trap SC4026 before mutation and poison the instance", async () => {
|
||||
const { archive, outDir } = await buildLibrary(emission, { tag: "reentry" });
|
||||
const probe = buildProbe("probe.c", archive, outDir, { pthread: true });
|
||||
for (const [mode, symbol] of Object.entries(REENTRY_SYMBOLS)) {
|
||||
const run = runProbe(probe, [mode]);
|
||||
expect(run.signal, mode).toBe("SIGABRT");
|
||||
expect(run.stdout, mode).toBe(reentryExpected(symbol));
|
||||
expect(run.stdout.includes("UNREACHABLE"), mode).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
platformTest("CB8: SC4026 rides the teaching overlay table with its inner symbol", async () => {
|
||||
const { archive, outDir } = await buildLibrary(emission, {
|
||||
tag: "reentry-teach",
|
||||
determinism: {
|
||||
teachings: { SC4026: "return from the callback before calling the library" },
|
||||
remediations: { SC4026: "schedule the operation for a later host-loop turn" },
|
||||
},
|
||||
});
|
||||
const probe = buildProbe("probe.c", archive, outDir, { pthread: true });
|
||||
const run = runProbe(probe, ["reenter-reset"]);
|
||||
expect(run.signal).toBe("SIGABRT");
|
||||
expect(run.stdout).toBe(reentryExpected("cb_reset_results", true));
|
||||
});
|
||||
|
||||
test("CB6: an undeclared host-callback reference refuses SC4024 with the profile teaching", async () => {
|
||||
const diags = await buildRefusal(emission, {
|
||||
tag: "undeclared",
|
||||
@@ -357,6 +426,7 @@ survived, sink_calls=1
|
||||
tag: "no-callbacks",
|
||||
entry: "lib_undeclared.ts",
|
||||
stripCallbacks: true,
|
||||
stripBufferedExport: true,
|
||||
});
|
||||
const probe = buildProbe("probe_referror.c", archive, outDir);
|
||||
const run = runProbe(probe);
|
||||
@@ -374,7 +444,7 @@ survived, sink_calls=1
|
||||
// lib_unused.ts never mentions 'orphan' (or the other channels); the
|
||||
// build succeeds and the registration symbol still answers for every
|
||||
// declared name.
|
||||
const { archive, outDir } = await buildLibrary(emission, { tag: "unused", entry: "lib_unused.ts" });
|
||||
const { archive, outDir } = await buildLibrary(emission, { tag: "unused", entry: "lib_unused.ts", stripBufferedExport: true });
|
||||
const probe = buildProbe("probe_unused.c", archive, outDir);
|
||||
const run = runProbe(probe);
|
||||
expect(run.signal).toBeNull();
|
||||
@@ -405,6 +475,7 @@ keyword: 7.5
|
||||
const { archive, outDir } = await buildLibrary(emission, {
|
||||
tag: "project-dts",
|
||||
entry: "lib_project_dts.ts",
|
||||
stripBufferedExport: true,
|
||||
});
|
||||
const probe = buildProbe("probe_project_dts.c", archive, outDir);
|
||||
const run = runProbe(probe);
|
||||
@@ -445,9 +516,22 @@ B: r1=31 r2=6 chunks=4 thread_ok=1 sink_calls=0
|
||||
expect([...undef].filter((s) => s.startsWith("cbt_"))).toEqual([]);
|
||||
});
|
||||
|
||||
/* ── CB8: the sanitized lane ─────────────────────────────────────────── */
|
||||
localizationTest("CB8: callback re-entry poisons only the active thread instance", async () => {
|
||||
const { archive, outDir } = await buildLibrary(emission, { tag: "threads-reentry", profileFile: "profile_t.json" });
|
||||
const probe = buildProbe("probe_threads_reentry.c", archive, outDir, { pthread: true });
|
||||
const run = runProbe(probe);
|
||||
expect(run.signal).toBeNull();
|
||||
expect(run.status).toBe(0);
|
||||
expect(run.stdout).toBe(`callbacks ready
|
||||
callbacks ready
|
||||
A: result=0 chunks=0 thread_ok=1 sink_calls=1 code=SC4026 symbol=cbt_stream ctx_ok=1
|
||||
B: result=5 chunks=1 thread_ok=1 sink_calls=0
|
||||
`);
|
||||
});
|
||||
|
||||
platformTest("CB8: CB1/CB2 under ASan", async () => {
|
||||
/* ── CB9: the sanitized lane ─────────────────────────────────────────── */
|
||||
|
||||
platformTest("CB9: CB1/CB2/CB8 under ASan", async () => {
|
||||
const { archive, outDir } = await buildLibrary(emission, { sanitize: true });
|
||||
const probe = buildProbe("probe.c", archive, outDir, { sanitize: true, pthread: true });
|
||||
const run = runProbe(probe, ["run"]);
|
||||
@@ -457,5 +541,8 @@ B: r1=31 r2=6 chunks=4 thread_ok=1 sink_calls=0
|
||||
const orphan = runProbe(probe, ["orphan"]);
|
||||
expect(orphan.signal).toBe("SIGABRT");
|
||||
expect(orphan.stdout).toBe(ORPHAN_EXPECTED);
|
||||
const reentry = runProbe(probe, ["reenter-reset"]);
|
||||
expect(reentry.signal).toBe("SIGABRT");
|
||||
expect(reentry.stdout).toBe(reentryExpected("cb_reset_results"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,10 +17,7 @@ export function stream(n: number, base: number): number {
|
||||
sessions++;
|
||||
let acc = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const chunk = new Uint8Array(3);
|
||||
chunk[0] = 65 + i; // 'A' + i
|
||||
chunk[1] = 48 + ((base + i) % 10); // a digit tied to the arguments
|
||||
chunk[2] = 33; // '!'
|
||||
const chunk = chunkFor(i, base);
|
||||
emitChunk(chunk, i);
|
||||
acc += (i + 1) * base; // computation between emits
|
||||
note(`chunk ${i} away`, i === n - 1);
|
||||
@@ -28,10 +25,28 @@ export function stream(n: number, base: number): number {
|
||||
return acc + sessions;
|
||||
}
|
||||
|
||||
function chunkFor(i: number, base: number): Uint8Array {
|
||||
const chunk = new Uint8Array(3);
|
||||
chunk[0] = 65 + i; // 'A' + i
|
||||
chunk[1] = 48 + ((base + i) % 10); // a digit tied to the arguments
|
||||
chunk[2] = 33; // '!'
|
||||
return chunk;
|
||||
}
|
||||
|
||||
export function askHost(x: number): number {
|
||||
return progress(x, 10) * 2 + mix(x + 300, 0 - x);
|
||||
}
|
||||
|
||||
// The re-entry probe keeps one result borrowed, then enters here with a
|
||||
// callback that tries result reset/collect. Its longjmp means this outer
|
||||
// buffer can never be returned, while the earlier result proves the inner
|
||||
// control entry did not touch the arena before SC4026.
|
||||
export function buffered(n: number): string {
|
||||
const chunk = chunkFor(n, n);
|
||||
emitChunk(chunk, n);
|
||||
return `buffer ${n}`;
|
||||
}
|
||||
|
||||
export function pokeOrphan(): number {
|
||||
orphan(7);
|
||||
return -1;
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
* library aborts the next entry deterministically
|
||||
* preregister — an unregistered-channel call BEFORE sink registration
|
||||
* aborts (the funnel's last resort)
|
||||
* reenter-* — a handler attempts the named ABI entry. SC4026 reaches
|
||||
* the original sink exactly once, names the INNER symbol,
|
||||
* leaves post-poison pure registration available, then the
|
||||
* next runtime-touching entry aborts.
|
||||
*/
|
||||
#include <pthread.h>
|
||||
#include <setjmp.h>
|
||||
@@ -26,8 +30,10 @@
|
||||
extern void cb_init(void);
|
||||
extern void cb_set_panic_sink(void (*fn)(void *, const uint8_t *, size_t, uint64_t), void *ctx);
|
||||
extern void cb_collect(void);
|
||||
extern void cb_reset_results(void);
|
||||
extern int32_t cb_set_callback(const char *name, void (*fn)(void), void *ctx);
|
||||
extern double cb_stream(double n, double base);
|
||||
extern void cb_buffered(double n, const uint8_t **out, size_t *out_len);
|
||||
extern double cb_ask_host(double x);
|
||||
extern double cb_poke_orphan(void);
|
||||
|
||||
@@ -94,6 +100,42 @@ static uint32_t on_mix(void *ctx, uint8_t a, int32_t b) {
|
||||
return (uint32_t)a + (uint32_t)(-b);
|
||||
}
|
||||
|
||||
/* ── callback-time re-entry probes ───────────────────────────────────── */
|
||||
|
||||
enum ReentryAction {
|
||||
REENTER_EXPORT,
|
||||
REENTER_INIT,
|
||||
REENTER_RESET,
|
||||
REENTER_COLLECT,
|
||||
REENTER_SINK,
|
||||
REENTER_CALLBACK_UNKNOWN,
|
||||
};
|
||||
|
||||
static enum ReentryAction reentry_action;
|
||||
static int replacement_sink_calls = 0;
|
||||
|
||||
static void replacement_sink(void *ctx, const uint8_t *msg, size_t len, uint64_t addr) {
|
||||
(void)ctx; (void)msg; (void)len; (void)addr;
|
||||
replacement_sink_calls++;
|
||||
printf("REPLACEMENT SINK\n");
|
||||
}
|
||||
|
||||
static void on_reenter(void *ctx, const uint8_t *p, size_t len, uint32_t seq) {
|
||||
(void)ctx; (void)p; (void)len; (void)seq;
|
||||
switch (reentry_action) {
|
||||
case REENTER_EXPORT: cb_stream(1, 1); break;
|
||||
case REENTER_INIT: cb_init(); break;
|
||||
case REENTER_RESET: cb_reset_results(); break;
|
||||
case REENTER_COLLECT: cb_collect(); break;
|
||||
case REENTER_SINK: cb_set_panic_sink(replacement_sink, NULL); break;
|
||||
case REENTER_CALLBACK_UNKNOWN:
|
||||
/* The guard must precede even this unknown-name dispatch. */
|
||||
cb_set_callback("not-a-channel", (cb_fn)on_chunk, NULL);
|
||||
break;
|
||||
}
|
||||
printf("UNREACHABLE callback return\n");
|
||||
}
|
||||
|
||||
/* ── the panic sink (the traps probe's parse rule) ───────────────────── */
|
||||
|
||||
static jmp_buf trap_jmp;
|
||||
@@ -164,6 +206,38 @@ int main(int argc, char **argv) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strncmp(mode, "reenter-", 8) == 0) {
|
||||
const char *which = mode + 8;
|
||||
if (strcmp(which, "export") == 0) reentry_action = REENTER_EXPORT;
|
||||
else if (strcmp(which, "init") == 0) reentry_action = REENTER_INIT;
|
||||
else if (strcmp(which, "reset") == 0) reentry_action = REENTER_RESET;
|
||||
else if (strcmp(which, "collect") == 0) reentry_action = REENTER_COLLECT;
|
||||
else if (strcmp(which, "sink") == 0) reentry_action = REENTER_SINK;
|
||||
else if (strcmp(which, "callback-unknown") == 0) reentry_action = REENTER_CALLBACK_UNKNOWN;
|
||||
else return 64;
|
||||
cb_set_panic_sink(sink, NULL);
|
||||
cb_set_callback("emitChunk", (cb_fn)on_chunk, &log_a);
|
||||
cb_init();
|
||||
const uint8_t *saved = NULL;
|
||||
size_t saved_len = 0;
|
||||
cb_buffered(7, &saved, &saved_len); /* held by the explicit-reset arena */
|
||||
cb_set_callback("emitChunk", (cb_fn)on_reenter, NULL);
|
||||
if (setjmp(trap_jmp) == 0) {
|
||||
cb_buffered(8, NULL, NULL); /* callback attempts an ABI entry; no result returns */
|
||||
printf("UNREACHABLE outer return\n");
|
||||
} else {
|
||||
/* The re-entry path cleared callback-active before delivery. Pure
|
||||
* registration therefore retains its established post-poison rule. */
|
||||
printf("post-poison register: %d\n", (int)cb_set_callback("emitChunk", (cb_fn)on_chunk, &log_a));
|
||||
printf("replacement-sink-calls=%d\n", replacement_sink_calls);
|
||||
printf("saved-result=[%.*s]\n", (int)saved_len, (const char *)saved);
|
||||
fflush(stdout);
|
||||
cb_stream(1, 1); /* poisoned runtime-touching entry must abort */
|
||||
printf("UNREACHABLE poisoned entry\n");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* mode "run" */
|
||||
|
||||
/* Registration is a pure store, legal before init; return codes are the
|
||||
|
||||
@@ -137,6 +137,7 @@ static void *worker_a(void *arg) {
|
||||
cbt_set_callback("note", (cb_fn)on_note, NULL);
|
||||
cbt_set_callback("mix", (cb_fn)on_mix, NULL);
|
||||
cbt_init();
|
||||
fflush(stdout); /* keep the two independent init logs line-separated */
|
||||
arrive(&inited, 2, 1);
|
||||
stage_wait(1);
|
||||
w->r1 = cbt_stream(3, 2); /* concurrent with B's stream */
|
||||
@@ -160,6 +161,7 @@ static void *worker_b(void *arg) {
|
||||
cbt_set_callback("note", (cb_fn)on_note, NULL);
|
||||
cbt_set_callback("mix", (cb_fn)on_mix, NULL);
|
||||
cbt_init();
|
||||
fflush(stdout); /* keep the two independent init logs line-separated */
|
||||
arrive(&inited, 2, 1);
|
||||
stage_wait(1);
|
||||
w->r1 = cbt_stream(3, 5); /* concurrent with A's stream */
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/* Localized + thread-instanced SC4026 probe. Thread A re-enters from its
|
||||
* callback and is poisoned; thread B's independent instance still calls a
|
||||
* callback and completes an entry. The ordinary SC4025 isolation fixture
|
||||
* remains in probe_threads.c. */
|
||||
#include <pthread.h>
|
||||
#include <setjmp.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
extern void cbt_init(void);
|
||||
extern void cbt_set_panic_sink(void (*fn)(void *, const uint8_t *, size_t, uint64_t), void *ctx);
|
||||
extern int32_t cbt_set_callback(const char *name, void (*fn)(void), void *ctx);
|
||||
extern double cbt_stream(double n, double base);
|
||||
|
||||
typedef void (*cb_fn)(void);
|
||||
|
||||
static pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
|
||||
static int inited = 0, a_trapped = 0;
|
||||
|
||||
typedef struct {
|
||||
pthread_t self;
|
||||
int reenter;
|
||||
int chunks;
|
||||
int thread_ok;
|
||||
int sink_calls;
|
||||
int sink_ctx_ok;
|
||||
char code[16];
|
||||
char symbol[64];
|
||||
double result;
|
||||
jmp_buf trap_jmp;
|
||||
} Worker;
|
||||
|
||||
static Worker workers[2];
|
||||
|
||||
static void parse_field(char *out, size_t cap, const uint8_t *msg, size_t len, int wanted) {
|
||||
if (len == 0 || msg[0] != 0x01) return;
|
||||
const uint8_t *p = msg + 1, *end = msg + len;
|
||||
int field = 0;
|
||||
for (;;) {
|
||||
const uint8_t *sep = memchr(p, 0x1f, (size_t)(end - p));
|
||||
const uint8_t *stop = sep != NULL ? sep : end;
|
||||
if (field == wanted) {
|
||||
snprintf(out, cap, "%.*s", (int)(stop - p), (const char *)p);
|
||||
return;
|
||||
}
|
||||
field++;
|
||||
if (sep == NULL) return;
|
||||
p = sep + 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void sink(void *ctx, const uint8_t *msg, size_t len, uint64_t addr) {
|
||||
(void)addr;
|
||||
Worker *w = (Worker *)ctx;
|
||||
w->sink_calls++;
|
||||
w->sink_ctx_ok = pthread_equal(pthread_self(), w->self) ? 1 : 0;
|
||||
parse_field(w->code, sizeof w->code, msg, len, 1);
|
||||
parse_field(w->symbol, sizeof w->symbol, msg, len, 2);
|
||||
longjmp(w->trap_jmp, 1);
|
||||
}
|
||||
|
||||
static void on_chunk(void *ctx, const uint8_t *p, size_t len, uint32_t seq) {
|
||||
(void)p; (void)len; (void)seq;
|
||||
Worker *w = (Worker *)ctx;
|
||||
if (!pthread_equal(pthread_self(), w->self)) w->thread_ok = 0;
|
||||
if (w->reenter) {
|
||||
cbt_stream(1, 1); /* rejected SC4026; never returns */
|
||||
printf("UNREACHABLE callback return\n");
|
||||
}
|
||||
w->chunks++;
|
||||
}
|
||||
|
||||
static void on_note(void *ctx, const uint8_t *p, size_t len, uint8_t last) {
|
||||
(void)ctx; (void)p; (void)len; (void)last;
|
||||
}
|
||||
|
||||
static void wait_for(int *value, int target) {
|
||||
pthread_mutex_lock(&mu);
|
||||
while (*value < target) pthread_cond_wait(&cv, &mu);
|
||||
pthread_mutex_unlock(&mu);
|
||||
}
|
||||
|
||||
static void signal_value(int *value) {
|
||||
pthread_mutex_lock(&mu);
|
||||
(*value)++;
|
||||
pthread_cond_broadcast(&cv);
|
||||
pthread_mutex_unlock(&mu);
|
||||
}
|
||||
|
||||
static void *worker_a(void *arg) {
|
||||
Worker *w = (Worker *)arg;
|
||||
w->self = pthread_self();
|
||||
w->thread_ok = 1;
|
||||
w->reenter = 1;
|
||||
cbt_set_panic_sink(sink, w);
|
||||
cbt_set_callback("emitChunk", (cb_fn)on_chunk, w);
|
||||
cbt_set_callback("note", (cb_fn)on_note, NULL);
|
||||
cbt_init();
|
||||
fflush(stdout); /* keep the two independent init logs line-separated */
|
||||
signal_value(&inited);
|
||||
wait_for(&inited, 2);
|
||||
if (setjmp(w->trap_jmp) == 0) {
|
||||
cbt_stream(1, 2);
|
||||
printf("UNREACHABLE outer return\n");
|
||||
}
|
||||
signal_value(&a_trapped);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void *worker_b(void *arg) {
|
||||
Worker *w = (Worker *)arg;
|
||||
w->self = pthread_self();
|
||||
w->thread_ok = 1;
|
||||
cbt_set_panic_sink(sink, w);
|
||||
cbt_set_callback("emitChunk", (cb_fn)on_chunk, w);
|
||||
cbt_set_callback("note", (cb_fn)on_note, NULL);
|
||||
cbt_init();
|
||||
fflush(stdout); /* keep the two independent init logs line-separated */
|
||||
signal_value(&inited);
|
||||
wait_for(&inited, 2);
|
||||
wait_for(&a_trapped, 1);
|
||||
w->result = cbt_stream(1, 4);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
pthread_t a, b;
|
||||
pthread_create(&a, NULL, worker_a, &workers[0]);
|
||||
pthread_create(&b, NULL, worker_b, &workers[1]);
|
||||
pthread_join(a, NULL);
|
||||
pthread_join(b, NULL);
|
||||
printf("A: result=%g chunks=%d thread_ok=%d sink_calls=%d code=%s symbol=%s ctx_ok=%d\n",
|
||||
workers[0].result, workers[0].chunks, workers[0].thread_ok, workers[0].sink_calls,
|
||||
workers[0].code, workers[0].symbol, workers[0].sink_ctx_ok);
|
||||
printf("B: result=%g chunks=%d thread_ok=%d sink_calls=%d\n",
|
||||
workers[1].result, workers[1].chunks, workers[1].thread_ok, workers[1].sink_calls);
|
||||
return 0;
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
"init_symbol": "cb_init",
|
||||
"sink_register_symbol": "cb_set_panic_sink",
|
||||
"collect_symbol": "cb_collect",
|
||||
"result_reset_symbol": null,
|
||||
"result_reset_symbol": "cb_reset_results",
|
||||
"callback_register_symbol": "cb_set_callback"
|
||||
},
|
||||
"callbacks": [
|
||||
@@ -20,6 +20,7 @@
|
||||
],
|
||||
"exports": [
|
||||
{ "export": "stream", "symbol": "cb_stream", "params": ["f64", "f64"], "returns": "f64" },
|
||||
{ "export": "buffered", "symbol": "cb_buffered", "params": ["f64"], "returns": "string" },
|
||||
{ "export": "askHost", "symbol": "cb_ask_host", "params": ["f64"], "returns": "f64" },
|
||||
{ "export": "pokeOrphan", "symbol": "cb_poke_orphan", "params": [], "returns": "f64" }
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user