Files
scriptc/tests/library-mode/multi/probe.c
Chris Tate da2ad7e002 feat: support multi-instance library mode via runtime symbol localization (#136)
* feat: support multi-instance library mode via runtime symbol localization

- Add the abi.localize_runtime profile field: the archive build combines the
  program object with exactly the runtime/vendor members it reaches and
  demotes every external definition except the profile-declared symbols to a
  local symbol (darwin: one ld -r pass with -exported_symbols_list; linux:
  ld -r then objcopy --keep-global-symbols)
- N archives built under pairwise-distinct prefixes now link into one process
  with no symbol collisions and no shared mutable runtime state: each
  instance owns a private copy of the allocator, collector, result arena, and
  panic sink, so sinks register per instance and a trap poisons only the
  instance it fired in
- Document the embedder contract beside the profile spec: one thread per
  instance (an instance is never entered from two threads), and values cross
  instances only through the embedder's own byte/record marshalling
- Refuse cross-target localized builds with SC3002 (the step runs the host
  toolchain's ld/objcopy over host-format objects); absent or false keeps the
  classic single-archive artifact byte-for-byte
- Add the two-instance acceptance probe (two archives, two embedder threads,
  independent collects, a trap delivered to its own sink exactly once while
  the other instance keeps answering) plus symbol-exactness, profile-shape,
  and target-posture suites, and schedule the artifact contracts on the gate
  host

* fix: tighten runtime localization validation

* fix: harden localized library publication
2026-08-12 02:19:37 -05:00

199 lines
6.4 KiB
C

/* Multi-instance acceptance probe: TWO runtime-localized library archives
* (prefixes ma_ / mb_) linked into ONE process, each instance driven from
* its own dedicated embedder thread — the documented contract: one thread
* per instance, an instance never entered from two threads. Checks:
* - both instances init, run their export families, and run their
* collect entries independently;
* - a deliberate range trap in instance A reaches ONLY A's registered
* sink, exactly once, as the structured message (SC4014, symbol
* ma_boom) with A's registration ctx and a nonzero fault address;
* - instance B keeps answering: its worker loops through allocation-
* heavy calls across A's trap window and still answers afterwards,
* with exact values throughout;
* - B's sink never fires.
* Workers record results into per-thread state; main prints everything
* after both joins, so stdout is deterministic under any interleaving. */
#include <pthread.h>
#include <setjmp.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
extern void ma_init(void);
extern void ma_set_panic_sink(void (*fn)(void *, const uint8_t *, size_t, uint64_t), void *ctx);
extern void ma_collect(void);
extern double ma_bump(double x);
extern double ma_calls_seen(void);
extern double ma_boom(double i);
extern void mb_init(void);
extern void mb_set_panic_sink(void (*fn)(void *, const uint8_t *, size_t, uint64_t), void *ctx);
extern void mb_collect(void);
extern double mb_sum_to(double n);
extern double mb_add(double x);
/* Probe stages: 0 start; 1 A initialized; 2 B initialized (both workers'
* concurrent loops run); 3 both fixed loops finished (A traps, B keeps
* looping); 4 A's trap delivered (B answers once more and exits). */
static pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
static int stage = 0;
static int fixed_loops_done = 0;
static void stage_set(int s) {
pthread_mutex_lock(&mu);
if (stage < s) stage = s;
pthread_cond_broadcast(&cv);
pthread_mutex_unlock(&mu);
}
static int stage_get(void) {
pthread_mutex_lock(&mu);
int s = stage;
pthread_mutex_unlock(&mu);
return s;
}
static void stage_wait(int s) {
pthread_mutex_lock(&mu);
while (stage < s) pthread_cond_wait(&cv, &mu);
pthread_mutex_unlock(&mu);
}
static void fixed_loop_done(void) {
pthread_mutex_lock(&mu);
fixed_loops_done++;
if (fixed_loops_done == 2 && stage < 3) {
stage = 3;
pthread_cond_broadcast(&cv);
}
pthread_mutex_unlock(&mu);
}
/* Per-instance sink records. Both instances register the SAME sink
* function with instance-specific ctx: delivery routing is observable as
* which record advances. */
typedef struct {
int calls;
int ctx_ok;
int fields;
int text_printable;
int addr_nonzero;
char code[32];
char symbol[64];
} SinkRec;
static SinkRec rec_a, rec_b;
static jmp_buf trap_jmp_a;
static void copy_field(char *dst, size_t cap, const uint8_t *p, size_t len) {
if (len >= cap) len = cap - 1;
memcpy(dst, p, len);
dst[len] = 0;
}
static void record_structured(SinkRec *r, const uint8_t *msg, size_t len) {
if (len == 0 || msg[0] != 0x01) return; /* fields stays 0: not structured */
r->text_printable = len > 1 && msg[1] >= 0x20;
const uint8_t *p = msg + 1, *end = msg + len;
int i = 0;
for (;;) {
const uint8_t *sep = memchr(p, 0x1f, (size_t)(end - p));
const uint8_t *stop = sep != NULL ? sep : end;
if (i == 1) copy_field(r->code, sizeof r->code, p, (size_t)(stop - p));
if (i == 2) copy_field(r->symbol, sizeof r->symbol, p, (size_t)(stop - p));
i++;
if (sep == NULL) break;
p = sep + 1;
}
r->fields = i;
}
static void sink(void *ctx, const uint8_t *msg, size_t len, uint64_t addr) {
SinkRec *r = ctx;
r->calls++;
r->addr_nonzero = addr != 0;
record_structured(r, msg, len);
if (r == &rec_a) {
r->ctx_ok = 1;
longjmp(trap_jmp_a, 1); /* the conforming survival pattern */
}
/* B's sink must never fire; returning would abort, and main would never
* print (the probe's failure shows as missing output). */
}
/* Instance A's dedicated thread: state-advancing calls, collects, then the
* deliberate trap. */
static double a_last_bump, a_calls;
static int a_trap_fell_through = 0;
static void *worker_a(void *arg) {
(void)arg;
ma_set_panic_sink(sink, &rec_a);
ma_init(); /* prints "multi-a ready" */
stage_set(1);
stage_wait(2);
double last = 0;
for (int i = 0; i < 200; i++) {
last = ma_bump(1);
if ((i + 1) % 50 == 0) ma_collect();
}
a_last_bump = last;
a_calls = ma_calls_seen();
fixed_loop_done();
stage_wait(3);
if (setjmp(trap_jmp_a) == 0) {
ma_boom(9); /* xs[9] of a length-3 array: the runtime's range trap */
a_trap_fell_through = 1;
}
stage_set(4);
return NULL;
}
/* Instance B's dedicated thread: allocation-heavy calls that keep running
* across A's trap window, then post-trap answers. */
static int b_sums_ok = 1, b_adds_ok = 1, b_reached_200 = 0, b_post_ok = 0;
static void *worker_b(void *arg) {
(void)arg;
mb_set_panic_sink(sink, &rec_b);
stage_wait(1);
mb_init(); /* prints "multi-b ready" */
stage_set(2);
double total = 0;
long iters = 0;
for (;;) {
if (mb_sum_to(100) != 5050.0) b_sums_ok = 0;
total = mb_add(1);
iters++;
if (total != (double)iters) b_adds_ok = 0;
if (iters % 25 == 0) mb_collect();
if (iters == 200) fixed_loop_done();
if (iters >= 200 && stage_get() >= 4) break;
}
b_reached_200 = iters >= 200;
/* A's trap has been delivered; B answers again. */
double post_sum = mb_sum_to(10);
double post_add = mb_add(5);
mb_collect();
b_post_ok = post_sum == 55.0 && post_add == total + 5.0;
return NULL;
}
int main(void) {
pthread_t ta, tb;
pthread_create(&ta, NULL, worker_a, NULL);
pthread_create(&tb, NULL, worker_b, NULL);
pthread_join(ta, NULL);
pthread_join(tb, NULL);
printf("a: bump(1) x200 -> %.0f, calls_seen %.0f, trap fell through %d\n",
a_last_bump, a_calls, a_trap_fell_through);
printf("a sink: calls=%d ctx_ok=%d fields=%d code=[%s] symbol=[%s] text_printable=%d addr_nonzero=%d\n",
rec_a.calls, rec_a.ctx_ok, rec_a.fields, rec_a.code, rec_a.symbol,
rec_a.text_printable, rec_a.addr_nonzero);
printf("b: concurrent sums_ok=%d adds_ok=%d reached_200=%d\n", b_sums_ok, b_adds_ok, b_reached_200);
printf("b: post-trap answers ok=%d\n", b_post_ok);
printf("b sink: calls=%d\n", rec_b.calls);
return 0;
}