Task 2.4: outcome capture moves to sched_trace_outcome.c and the position/host/summary/rollback half of the model to sched_trace_scan.c; shared helpers go behind sched_trace_internal.h (sti_ prefix). Code is moved unchanged; every target that compiles the trace sources gets all four, and the core isolation scan covers the new files. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
808 lines
30 KiB
C
808 lines
30 KiB
C
/*
|
|
* 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 <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#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 <client.h>", "#include <netio.h>",
|
|
"#include <xceapi.h>", "#include <delay.h>",
|
|
"#include <idgen.h>", "#include <xce/cluster.h>",
|
|
};
|
|
#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[] = {
|
|
"<stdio.h>", "<fcntl.h>", "<unistd.h>", "<sys/stat.h>",
|
|
"<sys/file.h>", "<tlog.h>", "\"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: "<name> 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();
|
|
}
|