Merge pull request #2410 from DeusData/fix/test-runner-isolate-runtime-dir

fix(test): isolate the C test runners' daemon runtime
This commit is contained in:
Martin Vogel authored and GitHub committed 2026-09-30 22:21:48 +02:00
commit f4ff01b10b
6 files changed
+182 -16

No files matched your search

+36 -8
View File
@@ -241,21 +241,49 @@ static const char *bootstrap_runtime_parent_override(char *buffer, size_t capaci
return value && value[0] != '\0' ? value : NULL;
}
/* An explicit parent keeps precedence: it carries the compile-time test seam
* and the lifecycle guards' isolated namespace. NULL means the account-wide
* default rendezvous. */
static const char *bootstrap_runtime_parent_resolve(const char *runtime_parent, char *buffer,
size_t capacity) {
return runtime_parent ? runtime_parent : bootstrap_runtime_parent_override(buffer, capacity);
}
#ifdef CBM_ENABLE_TEST_SEAMS
/* Armed once by a test runner before any suite starts its threads. */
static bool g_bootstrap_default_runtime_forbidden = false;
void cbm_daemon_bootstrap_forbid_default_runtime_for_test(bool forbid) {
g_bootstrap_default_runtime_forbidden = forbid;
}
bool cbm_daemon_bootstrap_default_runtime_refused_for_test(const char *runtime_parent) {
char override_parent[BOOTSTRAP_PATH_CAP];
return g_bootstrap_default_runtime_forbidden &&
!bootstrap_runtime_parent_resolve(runtime_parent, override_parent,
sizeof(override_parent));
}
#endif
cbm_daemon_ipc_endpoint_t *cbm_daemon_bootstrap_endpoint_new(const char *runtime_parent) {
char key[CBM_DAEMON_KEY_SIZE];
if (!cbm_daemon_rendezvous_key(key)) {
return NULL;
}
/* An explicit parent keeps precedence: it carries the compile-time test
* seam and the lifecycle guards' isolated namespace. The override is
* resolved HERE, the one function every product endpoint goes through
* (daemon, MCP client, local CLI, index worker, activation), so no call
* site can silently keep the default. */
/* The override is resolved HERE, the one function every product endpoint
* goes through (daemon, MCP client, local CLI, index worker, activation),
* so no call site can silently keep the default. */
char override_parent[BOOTSTRAP_PATH_CAP];
const char *parent =
runtime_parent
? runtime_parent
: bootstrap_runtime_parent_override(override_parent, sizeof(override_parent));
bootstrap_runtime_parent_resolve(runtime_parent, override_parent, sizeof(override_parent));
#ifdef CBM_ENABLE_TEST_SEAMS
if (!parent && g_bootstrap_default_runtime_forbidden) {
(void)fprintf(stderr, "codebase-memory-mcp: test run refused the default daemon runtime "
"directory (no explicit parent and CBM_RUNTIME_DIR unset): the "
"developer's live daemon listens there\n");
return NULL;
}
#endif
return cbm_daemon_ipc_endpoint_new(key, parent);
}
+11
View File
@@ -45,6 +45,17 @@ bool cbm_daemon_process_role_requires_client(cbm_daemon_process_role_t role);
* ABI values must never create parallel daemon namespaces. */
cbm_daemon_ipc_endpoint_t *cbm_daemon_bootstrap_endpoint_new(const char *runtime_parent);
#ifdef CBM_ENABLE_TEST_SEAMS
/* Test runners only; no product binary arms it. Once armed, an endpoint
* request that names no runtime parent while CBM_RUNTIME_DIR is unset (one
* that would resolve to the account-wide default rendezvous, where the
* developer's live daemon listens) is refused with a diagnostic on stderr. */
void cbm_daemon_bootstrap_forbid_default_runtime_for_test(bool forbid);
/* The same decision cbm_daemon_bootstrap_endpoint_new makes, without
* resolving anything: true when that call would be refused. */
bool cbm_daemon_bootstrap_default_runtime_refused_for_test(const char *runtime_parent);
#endif
/* Cross-platform launch policy for the daemon child. The child is invoked
* directly (never through a shell), with exactly argv[0] plus the one hidden
* internal argument. It is detached from the launching client's lifetime and
+8
View File
@@ -28,6 +28,7 @@ int tf_skip_count = 0;
#include "test_framework.h"
#include "repro_runner.h"
#include "foundation/compat.h" /* cbm_setenv — #845 supervisor kill switch */
#include "daemon/bootstrap.h" /* refuse the developer's default daemon rendezvous */
/* Per-suite summary + filter. RUN_SUITE prints a one-line
* "[SUITE] <name> P passed, F failed" report (greppable for which suites still
@@ -131,6 +132,13 @@ int main(void) {
* supervisor gate already ignores unmarked hosts; pin the kill switch too.
* A test that exercises the supervisor must explicitly re-enable it. */
cbm_setenv("CBM_INDEX_SUPERVISOR", "0", 1);
#ifdef CBM_ENABLE_TEST_SEAMS
/* No reproduction needs the daemon rendezvous. Its default is the
* developer's live daemon whatever HOME says, so a case that ever resolves
* an endpoint without an explicit parent or CBM_RUNTIME_DIR is refused
* loudly instead of reaching it. */
cbm_daemon_bootstrap_forbid_default_runtime_for_test(true);
#endif
/* Unbuffered: a reproduction may crash/_exit (or a sanitizer may _exit on a
* leak) before stdio flushes — keep every printed line so the summary and the
+10 -2
View File
@@ -400,6 +400,11 @@ TEST(daemon_bootstrap_runtime_dir_env_relocates_rendezvous) {
char relocated_runtime[BOOTSTRAP_TEST_PATH_CAP] = {0};
char explicit_runtime[BOOTSTRAP_TEST_PATH_CAP] = {0};
char unusable[BOOTSTRAP_TEST_PATH_CAP] = {0};
/* The runner isolates the rendezvous for the whole run through this same
* variable, so the test must hand back exactly what it found. */
char previous_value[BOOTSTRAP_TEST_PATH_CAP] = {0};
const char *previous =
cbm_safe_getenv("CBM_RUNTIME_DIR", previous_value, sizeof(previous_value), NULL);
int written = snprintf(override_parent, sizeof(override_parent),
"%s/cbm-bootstrap-runtime-env-XXXXXX", cbm_tmpdir());
if (written <= 0 || written >= (int)sizeof(override_parent) || !cbm_mkdtemp(override_parent)) {
@@ -436,8 +441,10 @@ TEST(daemon_bootstrap_runtime_dir_env_relocates_rendezvous) {
unusable_set ? cbm_daemon_bootstrap_endpoint_new(NULL) : NULL;
/* Restore before asserting: a failed assertion returns immediately, and a
* leaked CBM_RUNTIME_DIR would follow every later suite in this process. */
(void)cbm_unsetenv("CBM_RUNTIME_DIR");
* leaked override, or a dropped run value, would follow every later suite
* in this process. */
bool restored = previous ? cbm_setenv("CBM_RUNTIME_DIR", previous_value, 1) == 0
: cbm_unsetenv("CBM_RUNTIME_DIR") == 0;
cbm_daemon_ipc_endpoint_free(refused);
cbm_daemon_ipc_endpoint_free(relocated);
if (relocated_runtime[0] != '\0') {
@@ -448,6 +455,7 @@ TEST(daemon_bootstrap_runtime_dir_env_relocates_rendezvous) {
}
(void)cbm_rmdir(override_parent);
ASSERT_TRUE(restored);
ASSERT_TRUE(prepared);
ASSERT_TRUE(explicit_started);
ASSERT_TRUE(explicit_canonical);
+9 -5
View File
@@ -180,15 +180,19 @@ static inline char *th_mktempdir(const char *prefix) {
* leaf made by cbm_mkdtemp() has a private DACL. Keep these security-sensitive
* fixtures under LocalAppData on Windows; preserve the ordinary temporary-root
* behavior on POSIX. */
static inline const char *th_secure_runtime_base(void) {
#ifdef _WIN32
return cbm_app_local_dir();
#else
return cbm_tmpdir();
#endif
}
static inline bool th_secure_runtime_parent_new(char *out, size_t out_cap, const char *tag) {
if (!out || out_cap == 0 || !tag || !tag[0]) {
return false;
}
#ifdef _WIN32
const char *base = cbm_app_local_dir();
#else
const char *base = cbm_tmpdir();
#endif
const char *base = th_secure_runtime_base();
if (!base || !base[0]) {
out[0] = '\0';
return false;
+108 -1
View File
@@ -18,6 +18,7 @@ int tf_skip_count = 0;
#include "foundation/mem.h" /* cbm_mem_init — worker budget */
#include "foundation/log.h" /* worker liveness heartbeat probe */
#include "foundation/platform.h" /* cbm_file_exists — blocking-git marker */
#include "daemon/bootstrap.h" /* runner rendezvous isolation */
#include "daemon/runtime.h" /* bounded worker response probe */
#include "daemon/ipc.h" /* Windows private-lock re-exec probe */
#include "daemon/version_cohort.h" /* Windows crash-turnover re-exec probe */
@@ -187,11 +188,15 @@ static int tf_maybe_run_blocking_git_probe(int argc, char **argv) {
* this sentinel and restore it; atexit removes anything left behind by an
* assertion that returns before fixture cleanup. */
static char tf_home_sentinel[512];
static char tf_runtime_sentinel[512];
static void tf_cleanup_cache_sentinel(void) {
if (tf_home_sentinel[0]) {
th_rmtree(tf_home_sentinel);
}
if (tf_runtime_sentinel[0]) {
th_rmtree(tf_runtime_sentinel);
}
}
/* Client home overrides the CLI honours BEFORE $HOME, so redirecting HOME alone
@@ -243,9 +248,108 @@ static bool tf_setup_cache_sentinel(void) {
cbm_unsetenv(tf_client_home_overrides[i]);
}
atexit(tf_cleanup_cache_sentinel);
/* HOME and CBM_CACHE_DIR do not move the daemon rendezvous: its key is a
* constant product hash under the system temp root. Without this, an
* install/uninstall/update test or any endpoint resolved with no explicit
* parent reaches the developer's live daemon. The run always gets a fresh
* private directory; an inherited value is replaced, never reused (the
* rule scripts/test-runtime.sh applies to the shell harness).
* th_secure_runtime_parent_new anchors it where the IPC ancestry check
* accepts it (LocalAppData on Windows). */
if (!th_secure_runtime_parent_new(tf_runtime_sentinel, sizeof(tf_runtime_sentinel), "run")) {
tf_runtime_sentinel[0] = '\0';
return false;
}
if (cbm_setenv("CBM_RUNTIME_DIR", tf_runtime_sentinel, 1) != 0) {
return false;
}
#ifdef CBM_ENABLE_TEST_SEAMS
/* Belt and braces: a fixture that later drops the variable gets a loud
* refusal instead of the default rendezvous. */
cbm_daemon_bootstrap_forbid_default_runtime_for_test(true);
#endif
return true;
}
static bool tf_path_has_parent(const char *path, const char *parent) {
size_t length = parent ? strlen(parent) : 0;
return path && length > 0 && strncmp(path, parent, length) == 0 &&
(path[length] == '/' || path[length] == '\\');
}
/* The daemon rendezvous is not derived from HOME or the cache: its key is a
* constant product hash and its default parent is the system temp root. With
* CBM_RUNTIME_DIR unset, every endpoint a test resolves is therefore the
* developer's live daemon socket (<tmp>/cbm-daemon-<uid>/cbm-<key>.sock).
* Every check runs before anything is resolved, so a run without isolation
* fails here and this test never reaches that socket itself. */
TEST(runner_isolation_daemon_runtime_dir_is_private) {
char runtime_env[sizeof(tf_runtime_sentinel)] = {0};
const char *value = cbm_safe_getenv("CBM_RUNTIME_DIR", runtime_env, sizeof(runtime_env), NULL);
ASSERT_NOT_NULL(value);
ASSERT_TRUE(value[0] != '\0');
/* The fresh per-run directory, never a value inherited from the caller. */
ASSERT_STR_EQ(value, tf_runtime_sentinel);
const char *root = th_secure_runtime_base();
ASSERT_NOT_NULL(root);
char canonical_root[CBM_SZ_4K] = {0};
char canonical_run[CBM_SZ_4K] = {0};
ASSERT_TRUE(cbm_canonical_path(root, canonical_root, sizeof(canonical_root)) != 0);
ASSERT_TRUE(cbm_canonical_path(value, canonical_run, sizeof(canonical_run)) != 0);
/* Strictly below the temp root: the root itself is the default rendezvous
* parent (/tmp on POSIX, LocalAppData on Windows). */
ASSERT_TRUE(tf_path_has_parent(canonical_run, canonical_root));
/* Only now resolve through the product resolver. A NULL parent is what
* every product call site passes: daemon, MCP client, CLI, activation. */
cbm_daemon_ipc_endpoint_t *endpoint = cbm_daemon_bootstrap_endpoint_new(NULL);
const char *resolved = endpoint ? cbm_daemon_ipc_endpoint_runtime_dir(endpoint) : NULL;
char canonical_resolved[CBM_SZ_4K] = {0};
bool resolved_ok = resolved && cbm_canonical_path(resolved, canonical_resolved,
sizeof(canonical_resolved)) != 0;
cbm_daemon_ipc_endpoint_free(endpoint);
ASSERT_TRUE(resolved_ok);
ASSERT_TRUE(tf_path_has_parent(canonical_resolved, canonical_run));
#ifndef _WIN32
char live_default[CBM_SZ_4K];
(void)snprintf(live_default, sizeof(live_default), "%s/cbm-daemon-%lu", canonical_root,
(unsigned long)geteuid());
ASSERT_TRUE(strcmp(canonical_resolved, live_default) != 0);
#endif
PASS();
}
#ifdef CBM_ENABLE_TEST_SEAMS
/* The runner arms the bootstrap guard, so a fixture that drops the variable
* gets a refusal instead of the default rendezvous. Only the decision is
* exercised: no endpoint is ever built while the variable is absent. */
TEST(runner_isolation_refuses_default_daemon_runtime) {
ASSERT_TRUE(tf_runtime_sentinel[0] != '\0');
bool allowed_with_run_dir = !cbm_daemon_bootstrap_default_runtime_refused_for_test(NULL);
(void)cbm_unsetenv("CBM_RUNTIME_DIR");
bool refused_without = cbm_daemon_bootstrap_default_runtime_refused_for_test(NULL);
bool explicit_allowed =
!cbm_daemon_bootstrap_default_runtime_refused_for_test(tf_runtime_sentinel);
/* Restore before asserting: a failed assertion returns immediately. */
bool restored = cbm_setenv("CBM_RUNTIME_DIR", tf_runtime_sentinel, 1) == 0;
ASSERT_TRUE(restored);
ASSERT_TRUE(allowed_with_run_dir);
ASSERT_TRUE(refused_without);
ASSERT_TRUE(explicit_allowed);
PASS();
}
#endif
SUITE(runner_isolation) {
RUN_TEST(runner_isolation_daemon_runtime_dir_is_private);
#ifdef CBM_ENABLE_TEST_SEAMS
RUN_TEST(runner_isolation_refuses_default_daemon_runtime);
#endif
}
/* Fast real-process probes for the async index-supervisor contract. They run
* only in a child admitted by the exact build-bound worker grammar. */
static void tf_index_worker_probe(const char *args_json, const char *response_out) {
@@ -1056,7 +1160,7 @@ int main(int argc, char **argv) {
* A test that exercises the supervisor must explicitly re-enable it. */
cbm_setenv("CBM_INDEX_SUPERVISOR", "0", 1);
if (!tf_setup_cache_sentinel()) {
fprintf(stderr, "failed to create isolated test cache\n");
fprintf(stderr, "failed to create isolated test cache and daemon runtime\n");
return 2;
}
@@ -1080,6 +1184,9 @@ int main(int argc, char **argv) {
printf("\n codebase-memory-mcp C test suite\n");
}
/* Runner isolation first: nothing below may reach the developer's daemon. */
RUN_SELECTED_SUITE(runner_isolation);
/* Foundation */
RUN_SELECTED_SUITE(arena);
RUN_SELECTED_SUITE(hash_table);