/* * test_core_isolation.c - regression guard for task 4.1's Observable * (scheduler-core spec, Req 5.1/5.2): "the core structure compiles and * links with zero symbols from the networking/API libraries; a build-time * or link-time check confirms this" -- and the accompanying constraint * "no wall-clock time call appears anywhere in the extracted core's * source." * * Design review follow-up (2026-08-19, scheduler-core retrospective * review): originally this guard mechanically covered ONLY core.c/core.h, * leaving core_events.c/core_session.c/core_clock.c -- core.c's own * 800-line-cap siblings, holding scheduling logic just as boundary- * sensitive as core.c itself -- "isolation-clean by review" only (manual, * not mechanical; scheduler/CLAUDE.md's own Architecture section used to * say so explicitly). A future change to either sibling could silently * reintroduce a networking/API dependency with nothing to catch it. Both * layers below now cover core_events.c/core_session.c too. core_clock.c * joins ONLY the nm(1) layer (2), not the wall-clock text scan: it is the * one FILE-level sanctioned exception carrying the core family's single * legitimate clock_gettime() call (Req 5 AC 3, core_clock.c's own header * comment) -- scanning it for wall-clock text would trivially and * incorrectly fail. It has no legitimate reason to reference any * networking/API symbol, so it fully participates in layer 2. * * CORRECTED 2026-09-19 (bug-1827, /xce-audit-invariants scheduler): the * paragraph above was accurate about layer 2 but the claim was narrower * than what layer 1 actually enforced -- core_clock.c was excluded from * ALL of layer 1's text scans (forbidden-headers, forbidden-symbols, AND * wall-clock), not only the wall-clock one. The forbidden-headers/ * forbidden-symbols scans have no legitimate reason to exempt * core_clock.c (it has no legitimate reason to include a networking/API * header or reference a networking/API symbol, same as its siblings), and * the nm(1) layer alone cannot catch a forbidden #include that resolves to * no undefined symbol (a macro-only or inline-only header). core_clock.c * now fully participates in the forbidden-headers and forbidden-symbols * scans too -- it remains excluded ONLY from the wall-clock scan, for the * reason stated above. * * Two layers, same genre as test_picker_core_loop_boundary.c / * test_picker_pendreason_factory_audit.c (source-text scans of the real, * current on-disk files, not a frozen snapshot): * * 1. Source-text scan of core.h/core.c/core_events.c/core_session.c/ * core_clock.c: no #include of client.h, netio.h, xceapi.h, delay.h, * idgen.h, or xce/cluster.h (the "Allowed Dependencies" list in * design.md names exactly these as forbidden); no call-site substring * matching a symbol exported only by those libraries; no wall-clock * call (time()/clock_gettime()/gettimeofday()) anywhere in core.c, * core_events.c, or core_session.c (core_clock.c excepted from this * one sub-check only -- see above). * * 2. The literal build-time/link-time proof the Observable asks for, * not achievable by a text scan alone: core.c/core_events.c/ * core_session.c/core_clock.c are compiled together in isolation * into one static library (meson.build's sched_core_isolation_lib, * this test's XCE_CORE_LIB_PATH), and this test inspects that * archive's UNDEFINED SYMBOL TABLE via nm(1) -- proving what this * code's *compiled output actually calls*, independent of what any * header happens to transitively declare (xce/types.h, used by * literally every file in this directory, itself includes netio.h * for unrelated reasons -- so a "no forbidden #include" check alone * would either false-positive on that pre-existing, harmless * transitive include, or have to special-case it; the nm check * sidesteps that entirely by checking *usage*, not *visibility*). * * Decision trace sources (scheduler-sim-decision-trace spec, task 1.3, * design.md "Invariant Cross-Check" dimension 2): sched_trace.c and * sched_trace_scan.c (record model), sched_trace_capture.c and * sched_trace_outcome.c (capture helpers) and their shared * sched_trace_internal.h (split in task 2.4) run on the allocator * thread inside the core's allocation chain, so they join layer 1's * forbidden-headers and forbidden-symbols scans, plus a check that applies * to these trace files only: no stdio, no file I/O and no logging (a trace * record is pure data; only the simulator REPL renders or writes it). They * are NOT part of layer 2: sched_core_isolation_lib covers the four core * state-machine files, and the capture file references scheduler symbols * outside that archive's scope. * * Why nm on a static_library() rather than the full xce_scheduler * executable: the executable links the whole monolithic libxce_dep, so * every symbol from netio/xceapi/client/delay/idgen would appear in ITS * undefined-symbol table regardless of whether this code itself ever * calls one -- that would prove nothing. Compiling this file set alone * into its own archive (sched_core_isolation_lib, meson.build) and * inspecting THAT archive's undefined symbols isolates exactly what these * translation units reference. */ #include #include #include #include #ifndef XCE_CORE_H_PATH #error \ "XCE_CORE_H_PATH must be defined by meson.build to core.h's absolute path" #endif #ifndef XCE_CORE_C_PATH #error \ "XCE_CORE_C_PATH must be defined by meson.build to core.c's absolute path" #endif #ifndef XCE_CORE_EVENTS_C_PATH #error \ "XCE_CORE_EVENTS_C_PATH must be defined by meson.build to core_events.c's absolute path" #endif #ifndef XCE_CORE_SESSION_C_PATH #error \ "XCE_CORE_SESSION_C_PATH must be defined by meson.build to core_session.c's absolute path" #endif #ifndef XCE_CORE_CLOCK_C_PATH #error \ "XCE_CORE_CLOCK_C_PATH must be defined by meson.build to core_clock.c's absolute path" #endif #ifndef XCE_SCHED_TRACE_C_PATH #error \ "XCE_SCHED_TRACE_C_PATH must be defined by meson.build to sched_trace.c's absolute path" #endif #ifndef XCE_SCHED_TRACE_CAPTURE_C_PATH #error \ "XCE_SCHED_TRACE_CAPTURE_C_PATH must be defined by meson.build to sched_trace_capture.c's absolute path" #endif #ifndef XCE_SCHED_TRACE_SCAN_C_PATH #error \ "XCE_SCHED_TRACE_SCAN_C_PATH must be defined by meson.build to sched_trace_scan.c's absolute path" #endif #ifndef XCE_SCHED_TRACE_OUTCOME_C_PATH #error \ "XCE_SCHED_TRACE_OUTCOME_C_PATH must be defined by meson.build to sched_trace_outcome.c's absolute path" #endif #ifndef XCE_SCHED_TRACE_INTERNAL_H_PATH #error \ "XCE_SCHED_TRACE_INTERNAL_H_PATH must be defined by meson.build to sched_trace_internal.h's absolute path" #endif #ifndef XCE_CORE_LIB_PATH #error \ "XCE_CORE_LIB_PATH must be defined by meson.build to the absolute path of core.c's isolated static_library() archive" #endif /* Forbidden headers (design.md "Allowed Dependencies", Req 5 AC 3): the * core must never directly include any of these. */ static const char* const FORBIDDEN_INCLUDES[] = { "#include \"client.h\"", "#include \"netio.h\"", "#include \"xceapi.h\"", "#include \"delay.h\"", "#include \"idgen.h\"", "#include \"xce/cluster.h\"", "#include ", "#include ", "#include ", "#include ", "#include ", "#include ", }; #define NUM_FORBIDDEN_INCLUDES \ (sizeof(FORBIDDEN_INCLUDES) / sizeof(FORBIDDEN_INCLUDES[0])) /* Forbidden call-site symbols: exported only by netio.c/xceapi.c/client.c/ * delay.c/idgen.c (verified against those headers' own extern lists) -- * none of these are legitimately callable from a core with zero controller * I/O, network, or timer dependency. */ static const char* const FORBIDDEN_SYMBOLS[] = { "xcecli_", "endpoint_", "iomgr_", "idgen_", "delaymgr_", "delay_task", "xce_connect_cluster", "xce_reconnect_", "xce_parse_peers", "xce_cluster_config_validate", "socket_connect", "create_ssl_context", "initialize_openssl", "cleanup_openssl", "io_fsm_start", }; #define NUM_FORBIDDEN_SYMBOLS \ (sizeof(FORBIDDEN_SYMBOLS) / sizeof(FORBIDDEN_SYMBOLS[0])) /* Wall-clock calls (Req 5 AC "no wall-clock time call appears anywhere in * the extracted core's source"). */ static const char* const FORBIDDEN_WALLCLOCK[] = { "time(", "clock_gettime(", "gettimeofday(", }; #define NUM_FORBIDDEN_WALLCLOCK \ (sizeof(FORBIDDEN_WALLCLOCK) / sizeof(FORBIDDEN_WALLCLOCK[0])) static char* read_whole_file(const char* path) { FILE* f = fopen(path, "rb"); if (f == NULL) { return NULL; } if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return NULL; } long len = ftell(f); if (len < 0) { fclose(f); return NULL; } rewind(f); char* buf = (char*)malloc((size_t)len + 1); if (buf == NULL) { fclose(f); return NULL; } size_t nread = fread(buf, 1, (size_t)len, f); fclose(f); buf[nread] = '\0'; return buf; } void setUp(void) { return; } void tearDown(void) { return; } /* Sanity check: both source paths baked in by meson.build must actually be * readable -- without this, a stale path would make every scan below * silently pass on zero bytes of text (the exact false-confidence failure * mode this whole test exists to avoid). */ static void test_core_source_paths_are_readable(void) { char* h = read_whole_file(XCE_CORE_H_PATH); char* c = read_whole_file(XCE_CORE_C_PATH); char* ce = read_whole_file(XCE_CORE_EVENTS_C_PATH); char* cs = read_whole_file(XCE_CORE_SESSION_C_PATH); char* cc = read_whole_file(XCE_CORE_CLOCK_C_PATH); char* st = read_whole_file(XCE_SCHED_TRACE_C_PATH); char* sc = read_whole_file(XCE_SCHED_TRACE_CAPTURE_C_PATH); char* ss = read_whole_file(XCE_SCHED_TRACE_SCAN_C_PATH); char* so = read_whole_file(XCE_SCHED_TRACE_OUTCOME_C_PATH); char* si = read_whole_file(XCE_SCHED_TRACE_INTERNAL_H_PATH); TEST_ASSERT_NOT_NULL_MESSAGE(h, XCE_CORE_H_PATH); TEST_ASSERT_NOT_NULL_MESSAGE(c, XCE_CORE_C_PATH); TEST_ASSERT_NOT_NULL_MESSAGE(ce, XCE_CORE_EVENTS_C_PATH); TEST_ASSERT_NOT_NULL_MESSAGE(cs, XCE_CORE_SESSION_C_PATH); TEST_ASSERT_NOT_NULL_MESSAGE(cc, XCE_CORE_CLOCK_C_PATH); TEST_ASSERT_NOT_NULL_MESSAGE(st, XCE_SCHED_TRACE_C_PATH); TEST_ASSERT_NOT_NULL_MESSAGE(sc, XCE_SCHED_TRACE_CAPTURE_C_PATH); TEST_ASSERT_NOT_NULL_MESSAGE(ss, XCE_SCHED_TRACE_SCAN_C_PATH); TEST_ASSERT_NOT_NULL_MESSAGE(so, XCE_SCHED_TRACE_OUTCOME_C_PATH); TEST_ASSERT_NOT_NULL_MESSAGE(si, XCE_SCHED_TRACE_INTERNAL_H_PATH); free(h); free(c); free(ce); free(cs); free(cc); free(st); free(sc); free(ss); free(so); free(si); } /* Scans one file's text for every FORBIDDEN_INCLUDES entry, tagging each * failure message with `label` so a failure names the actual offending * file -- shared by test_core_never_includes_forbidden_headers() across * core.h/core.c/core_events.c/core_session.c. */ static void assert_no_forbidden_includes(const char* text, const char* label) { size_t i; for (i = 0; i < NUM_FORBIDDEN_INCLUDES; i++) { char msg[256]; snprintf(msg, sizeof(msg), "%s directly includes forbidden header '%s'", label, FORBIDDEN_INCLUDES[i]); TEST_ASSERT_NULL_MESSAGE(strstr(text, FORBIDDEN_INCLUDES[i]), msg); } } static void test_core_never_includes_forbidden_headers(void) { char* h = read_whole_file(XCE_CORE_H_PATH); char* c = read_whole_file(XCE_CORE_C_PATH); char* ce = read_whole_file(XCE_CORE_EVENTS_C_PATH); char* cs = read_whole_file(XCE_CORE_SESSION_C_PATH); char* cc = read_whole_file(XCE_CORE_CLOCK_C_PATH); char* st = read_whole_file(XCE_SCHED_TRACE_C_PATH); char* sc = read_whole_file(XCE_SCHED_TRACE_CAPTURE_C_PATH); char* ss = read_whole_file(XCE_SCHED_TRACE_SCAN_C_PATH); char* so = read_whole_file(XCE_SCHED_TRACE_OUTCOME_C_PATH); char* si = read_whole_file(XCE_SCHED_TRACE_INTERNAL_H_PATH); TEST_ASSERT_NOT_NULL(h); TEST_ASSERT_NOT_NULL(c); TEST_ASSERT_NOT_NULL(ce); TEST_ASSERT_NOT_NULL(cs); TEST_ASSERT_NOT_NULL(cc); TEST_ASSERT_NOT_NULL(st); TEST_ASSERT_NOT_NULL(sc); TEST_ASSERT_NOT_NULL(ss); TEST_ASSERT_NOT_NULL(so); TEST_ASSERT_NOT_NULL(si); assert_no_forbidden_includes(h, "core.h"); assert_no_forbidden_includes(c, "core.c"); assert_no_forbidden_includes(ce, "core_events.c"); assert_no_forbidden_includes(cs, "core_session.c"); assert_no_forbidden_includes(cc, "core_clock.c"); assert_no_forbidden_includes(st, "sched_trace.c"); assert_no_forbidden_includes(sc, "sched_trace_capture.c"); assert_no_forbidden_includes(ss, "sched_trace_scan.c"); assert_no_forbidden_includes(so, "sched_trace_outcome.c"); assert_no_forbidden_includes(si, "sched_trace_internal.h"); free(h); free(c); free(ce); free(cs); free(cc); free(st); free(sc); free(ss); free(so); free(si); } /* Shared by test_core_never_calls_forbidden_symbols() -- see * assert_no_forbidden_includes() above for the same per-file-label * rationale. */ static void assert_no_forbidden_symbols(const char* text, const char* label) { size_t i; for (i = 0; i < NUM_FORBIDDEN_SYMBOLS; i++) { char msg[256]; snprintf(msg, sizeof(msg), "%s references forbidden networking/API symbol '%s'", label, FORBIDDEN_SYMBOLS[i]); TEST_ASSERT_NULL_MESSAGE(strstr(text, FORBIDDEN_SYMBOLS[i]), msg); } } static void test_core_never_calls_forbidden_symbols(void) { char* h = read_whole_file(XCE_CORE_H_PATH); char* c = read_whole_file(XCE_CORE_C_PATH); char* ce = read_whole_file(XCE_CORE_EVENTS_C_PATH); char* cs = read_whole_file(XCE_CORE_SESSION_C_PATH); char* cc = read_whole_file(XCE_CORE_CLOCK_C_PATH); char* st = read_whole_file(XCE_SCHED_TRACE_C_PATH); char* sc = read_whole_file(XCE_SCHED_TRACE_CAPTURE_C_PATH); char* ss = read_whole_file(XCE_SCHED_TRACE_SCAN_C_PATH); char* so = read_whole_file(XCE_SCHED_TRACE_OUTCOME_C_PATH); char* si = read_whole_file(XCE_SCHED_TRACE_INTERNAL_H_PATH); TEST_ASSERT_NOT_NULL(h); TEST_ASSERT_NOT_NULL(c); TEST_ASSERT_NOT_NULL(ce); TEST_ASSERT_NOT_NULL(cs); TEST_ASSERT_NOT_NULL(cc); TEST_ASSERT_NOT_NULL(st); TEST_ASSERT_NOT_NULL(sc); TEST_ASSERT_NOT_NULL(ss); TEST_ASSERT_NOT_NULL(so); TEST_ASSERT_NOT_NULL(si); assert_no_forbidden_symbols(h, "core.h"); assert_no_forbidden_symbols(c, "core.c"); assert_no_forbidden_symbols(ce, "core_events.c"); assert_no_forbidden_symbols(cs, "core_session.c"); assert_no_forbidden_symbols(cc, "core_clock.c"); assert_no_forbidden_symbols(st, "sched_trace.c"); assert_no_forbidden_symbols(sc, "sched_trace_capture.c"); assert_no_forbidden_symbols(ss, "sched_trace_scan.c"); assert_no_forbidden_symbols(so, "sched_trace_outcome.c"); assert_no_forbidden_symbols(si, "sched_trace_internal.h"); free(h); free(c); free(ce); free(cs); free(cc); free(st); free(sc); free(ss); free(so); free(si); } /* core_clock.c is deliberately NOT scanned here -- it is the one * sanctioned exception carrying the core family's single legitimate * clock_gettime() call (Req 5 AC 3); see this file's own header comment. */ static void test_core_never_calls_wallclock_functions(void) { char* c = read_whole_file(XCE_CORE_C_PATH); char* ce = read_whole_file(XCE_CORE_EVENTS_C_PATH); char* cs = read_whole_file(XCE_CORE_SESSION_C_PATH); size_t i; TEST_ASSERT_NOT_NULL(c); TEST_ASSERT_NOT_NULL(ce); TEST_ASSERT_NOT_NULL(cs); for (i = 0; i < NUM_FORBIDDEN_WALLCLOCK; i++) { char msg[256]; snprintf(msg, sizeof(msg), "core.c calls wall-clock function '%s' -- the core must " "access time exclusively through xce_core_now() " "(scheduler-core spec, task 4.5)", FORBIDDEN_WALLCLOCK[i]); TEST_ASSERT_NULL_MESSAGE(strstr(c, FORBIDDEN_WALLCLOCK[i]), msg); snprintf(msg, sizeof(msg), "core_events.c calls wall-clock function '%s' -- the core " "must access time exclusively through xce_core_now() " "(scheduler-core spec, task 4.5)", FORBIDDEN_WALLCLOCK[i]); TEST_ASSERT_NULL_MESSAGE(strstr(ce, FORBIDDEN_WALLCLOCK[i]), msg); snprintf(msg, sizeof(msg), "core_session.c calls wall-clock function '%s' -- the core " "must access time exclusively through xce_core_now() " "(scheduler-core spec, task 4.5)", FORBIDDEN_WALLCLOCK[i]); TEST_ASSERT_NULL_MESSAGE(strstr(cs, FORBIDDEN_WALLCLOCK[i]), msg); } free(c); free(ce); free(cs); } /* * No stdio, no file I/O, no logging in the decision trace sources * (scheduler-sim-decision-trace design.md, TraceModel / TraceCapture: "No * I/O and no tlog"). Applies to the trace sources only: sched_trace.c, * sched_trace_scan.c, sched_trace_capture.c, sched_trace_outcome.c and * sched_trace_internal.h. * * Comments, string literals and character literals are blanked before the * scan, so prose such as "never calls fopen" in a comment is not a hit. * Call names match on identifier boundaries followed by '(' (spaces * allowed), and not as a member call (`x.read(` / `x->read(`), so * `sched_trace_rec_free(` or `SCHED_TRACE(` never match `free(`/`TRACE(`. */ static const char* const TRACE_FORBIDDEN_INCLUDES[] = { "", "", "", "", "", "", "\"xcelog.h\"", }; #define NUM_TRACE_FORBIDDEN_INCLUDES \ (sizeof(TRACE_FORBIDDEN_INCLUDES) / sizeof(TRACE_FORBIDDEN_INCLUDES[0])) /* Identifiers that must not appear at all (stdio stream objects). */ static const char* const TRACE_FORBIDDEN_IDENTS[] = { "FILE", "stdin", "stdout", "stderr", }; #define NUM_TRACE_FORBIDDEN_IDENTS \ (sizeof(TRACE_FORBIDDEN_IDENTS) / sizeof(TRACE_FORBIDDEN_IDENTS[0])) /* Function-like names that must not be called. */ static const char* const TRACE_FORBIDDEN_CALLS[] = { /* stdio */ "printf", "fprintf", "vprintf", "vfprintf", "dprintf", "vdprintf", "puts", "fputs", "putchar", "fputc", "putc", "getchar", "fgetc", "getc", "fgets", "scanf", "fscanf", "perror", "fopen", "fdopen", "freopen", "fmemopen", "open_memstream", "fclose", "fflush", "fwrite", "fread", "fseek", "fseeko", "ftell", "ftello", "rewind", "setvbuf", "popen", "pclose", "tmpfile", /* POSIX file I/O */ "open", "openat", "creat", "close", "read", "write", "pread", "pwrite", "readv", "writev", "lseek", "fsync", "fdatasync", "ftruncate", "truncate", "unlink", "rename", "mkdir", "stat", "fstat", "lstat", "mmap", "syslog", /* project logging (tlog macros and SCHEDULER_LOG) */ "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL", "SCHEDULER_LOG", }; #define NUM_TRACE_FORBIDDEN_CALLS \ (sizeof(TRACE_FORBIDDEN_CALLS) / sizeof(TRACE_FORBIDDEN_CALLS[0])) static int is_ident_char(char c) { return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')) || ((c >= '0') && (c <= '9')) || (c == '_'); } /* Blanks comments and string/char literals in place (newlines kept). A * line that is an #include directive keeps its header name, so the * include scan above can still see it. */ static void blank_comments_and_literals(char* t) { size_t i = 0; int in_include = 0; while (t[i] != '\0') { if ((t[i] == '#') && (strncmp(t + i, "#include", 8) == 0)) { in_include = 1; } else if (t[i] == '\n') { in_include = 0; } if ((t[i] == '/') && (t[i + 1] == '*')) { while ((t[i] != '\0') && !((t[i] == '*') && (t[i + 1] == '/'))) { if (t[i] != '\n') { t[i] = ' '; } i++; } if (t[i] != '\0') { t[i] = ' '; t[i + 1] = ' '; i += 2; } continue; } if ((t[i] == '/') && (t[i + 1] == '/')) { while ((t[i] != '\0') && (t[i] != '\n')) { t[i++] = ' '; } continue; } if (((t[i] == '"') || (t[i] == '\'')) && !in_include) { char q = t[i++]; while ((t[i] != '\0') && (t[i] != q) && (t[i] != '\n')) { if ((t[i] == '\\') && (t[i + 1] != '\0')) { t[i++] = ' '; } t[i++] = ' '; } if (t[i] == q) { i++; } continue; } i++; } } /* Offset of the first identifier-bounded occurrence of `name` in `t` * (followed by '(' when `call`), or -1. */ static long find_ident(const char* t, const char* name, int call) { size_t n = strlen(name); const char* p = t; while ((p = strstr(p, name)) != NULL) { const char* e = p + n; int ok = 1; if (p > t) { char b = p[-1]; if (is_ident_char(b) || (b == '.') || ((b == '>') && (p - t >= 2) && (p[-2] == '-'))) { ok = 0; } } if (is_ident_char(*e)) { ok = 0; } if (ok && call) { while ((*e == ' ') || (*e == '\t')) { e++; } ok = (*e == '('); } if (ok) { return (long)(p - t); } p += n; } return -1; } static void assert_trace_source_has_no_io(char* text, const char* label) { char msg[256]; size_t i; for (i = 0; i < NUM_TRACE_FORBIDDEN_INCLUDES; i++) { snprintf(msg, sizeof(msg), "%s includes stdio/file-I/O/log header %s", label, TRACE_FORBIDDEN_INCLUDES[i]); TEST_ASSERT_NULL_MESSAGE(strstr(text, TRACE_FORBIDDEN_INCLUDES[i]), msg); } blank_comments_and_literals(text); for (i = 0; i < NUM_TRACE_FORBIDDEN_IDENTS; i++) { snprintf(msg, sizeof(msg), "%s references stdio stream '%s'", label, TRACE_FORBIDDEN_IDENTS[i]); TEST_ASSERT_EQUAL_INT_MESSAGE( -1, find_ident(text, TRACE_FORBIDDEN_IDENTS[i], 0), msg); } for (i = 0; i < NUM_TRACE_FORBIDDEN_CALLS; i++) { snprintf(msg, sizeof(msg), "%s calls stdio/file-I/O/log function '%s' -- trace model " "and capture code must do no I/O", label, TRACE_FORBIDDEN_CALLS[i]); TEST_ASSERT_EQUAL_INT_MESSAGE( -1, find_ident(text, TRACE_FORBIDDEN_CALLS[i], 1), msg); } } /* The scan itself must be able to fail: a probe text with one hit of each * class is rejected, and the same names inside comments, literals and * longer identifiers are not. */ static void test_trace_io_scan_detects_and_ignores(void) { char ok_text[] = "/* never fopen( or printf( here */\n" "// stdout FILE write(\n" "static const char* s = \"fprintf(stderr\";\n" "void f(void) { sched_trace_rec_free(0); " "SCHED_TRACE(core, x); p->read(1); q.write(2); " "readonly(3); my_FILE_t z; }\n"; char hit_call[] = "void f(void) { fwrite (b, 1, 1, 0); }\n"; char hit_ident[] = "static void* g = (void*)stderr;\n"; char hit_log[] = "void f(void) { WARN(\"x\"); }\n"; char hit_posix[] = "void f(void) { (void)write(1, 0, 0); }\n"; blank_comments_and_literals(ok_text); TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "fopen", 1)); TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "printf", 1)); TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "fprintf", 1)); TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "stdout", 0)); TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "stderr", 0)); TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "FILE", 0)); TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "free", 1)); TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "TRACE", 1)); TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "read", 1)); TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "write", 1)); TEST_ASSERT_NOT_EQUAL(-1, find_ident(hit_call, "fwrite", 1)); TEST_ASSERT_NOT_EQUAL(-1, find_ident(hit_ident, "stderr", 0)); TEST_ASSERT_NOT_EQUAL(-1, find_ident(hit_log, "WARN", 1)); TEST_ASSERT_NOT_EQUAL(-1, find_ident(hit_posix, "write", 1)); } static void test_trace_sources_do_no_stdio_or_file_io(void) { char* st = read_whole_file(XCE_SCHED_TRACE_C_PATH); char* sc = read_whole_file(XCE_SCHED_TRACE_CAPTURE_C_PATH); char* ss = read_whole_file(XCE_SCHED_TRACE_SCAN_C_PATH); char* so = read_whole_file(XCE_SCHED_TRACE_OUTCOME_C_PATH); char* si = read_whole_file(XCE_SCHED_TRACE_INTERNAL_H_PATH); TEST_ASSERT_NOT_NULL(st); TEST_ASSERT_NOT_NULL(sc); TEST_ASSERT_NOT_NULL(ss); TEST_ASSERT_NOT_NULL(so); TEST_ASSERT_NOT_NULL(si); assert_trace_source_has_no_io(st, "sched_trace.c"); assert_trace_source_has_no_io(sc, "sched_trace_capture.c"); assert_trace_source_has_no_io(ss, "sched_trace_scan.c"); assert_trace_source_has_no_io(so, "sched_trace_outcome.c"); assert_trace_source_has_no_io(si, "sched_trace_internal.h"); free(st); free(sc); free(ss); free(so); free(si); } /* * test_core_object_has_no_forbidden_undefined_symbols - the literal * build-time/link-time proof: core.c/core_events.c/core_session.c/ * core_clock.c, compiled together in isolation into * sched_core_isolation_lib (meson.build), must not reference (as an * undefined external symbol in the compiled archive) anything matching the * forbidden prefixes above. This is independent of what any header * transitively declares -- it reflects only what this file set's compiled * code actually calls. */ static void test_core_object_has_no_forbidden_undefined_symbols(void) { char cmd[4096]; char line[1024]; FILE* p; int any_undefined_seen = 0; snprintf(cmd, sizeof(cmd), "nm --format=posix -u \"%s\" 2>&1", XCE_CORE_LIB_PATH); p = popen(cmd, "r"); TEST_ASSERT_NOT_NULL_MESSAGE(p, "failed to popen() nm(1)"); while (fgets(line, sizeof(line), p) != NULL) { char symbol[512]; char type; size_t i; /* posix format for an undefined symbol: " U" (no * value/size columns). Skip anything that doesn't parse as * exactly that -- e.g. a blank line or an archive member header * some nm versions print. */ if (sscanf(line, "%511s %c", symbol, &type) != 2) { continue; } if (type != 'U') { continue; } any_undefined_seen = 1; for (i = 0; i < NUM_FORBIDDEN_SYMBOLS; i++) { if (strncmp(symbol, FORBIDDEN_SYMBOLS[i], strlen(FORBIDDEN_SYMBOLS[i])) == 0) { char msg[768]; snprintf(msg, sizeof(msg), "the core isolation archive (core.c/core_events.c/" "core_session.c/core_clock.c) has an undefined " "reference to '%s', matching forbidden " "networking/API prefix '%s' -- the core must never " "link networking/API library symbols (Req 5.1/" "5.2)", symbol, FORBIDDEN_SYMBOLS[i]); pclose(p); TEST_FAIL_MESSAGE(msg); } } } pclose(p); /* If nm produced no undefined symbols at all, either the archive path * is wrong or nm itself failed silently -- either way this test would * be vacuously passing without having checked anything real. */ TEST_ASSERT_TRUE_MESSAGE( any_undefined_seen, "nm reported zero undefined symbols for XCE_CORE_LIB_PATH -- " "the archive path is likely wrong (this test would otherwise pass " "vacuously)"); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_core_source_paths_are_readable); RUN_TEST(test_core_never_includes_forbidden_headers); RUN_TEST(test_core_never_calls_forbidden_symbols); RUN_TEST(test_core_never_calls_wallclock_functions); RUN_TEST(test_trace_io_scan_detects_and_ignores); RUN_TEST(test_trace_sources_do_no_stdio_or_file_io); RUN_TEST(test_core_object_has_no_forbidden_undefined_symbols); return UNITY_END(); }