mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
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:
6 files changed
+182
-16
No files matched your search
+36
-8
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user