Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions Makefile.cbm
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ FOUNDATION_SRCS = \
src/foundation/diagnostics.c \
src/foundation/profile.c \
src/foundation/dump_verify.c \
src/foundation/limits.c
src/foundation/limits.c \
src/foundation/subprocess.c

# Existing extraction C code (compiled from current location)
EXTRACTION_SRCS = \
Expand Down Expand Up @@ -173,7 +174,7 @@ STORE_SRCS = src/store/store.c
CYPHER_SRCS = src/cypher/cypher.c

# MCP server module (new)
MCP_SRCS = src/mcp/mcp.c
MCP_SRCS = src/mcp/mcp.c src/mcp/index_supervisor.c

# Discover module (new)
DISCOVER_SRCS = \
Expand Down Expand Up @@ -310,7 +311,8 @@ TEST_FOUNDATION_SRCS = \
tests/test_log.c \
tests/test_str_util.c \
tests/test_platform.c \
tests/test_dump_verify.c
tests/test_dump_verify.c \
tests/test_subprocess.c

TEST_EXTRACTION_SRCS = \
tests/test_extraction.c \
Expand Down
156 changes: 156 additions & 0 deletions internal/cbm/cbm.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
#include "lsp/rust_lsp.h"
#include "preprocessor.h"
#include "foundation/compat.h"
#include "foundation/compat_fs.h" // cbm_fopen — crash-supervisor per-file marker write
#include "foundation/hash_table.h" // CBMHashTable — crash-supervisor quarantine set
#include "tree_sitter/api.h" // TSParser, TSNode, TSTree, TSInput, TSLanguage, TSPoint, TSParseOptions, TSParseState
#include "foundation/constants.h"
#include "mimalloc.h" // mi_malloc/mi_calloc/mi_realloc/mi_free/mi_usable_size — bind 3rd-party allocators (#424)
Expand Down Expand Up @@ -494,6 +496,146 @@ static int count_params_from_signature(const char *sig) {

// --- Main extraction function ---

/* Test-only deterministic fault injection for the crash/hang supervisor tests.
* Gated entirely behind env vars that are never set in production; a matching
* rel_path either aborts (a fault signal the supervisor classifies as a crash)
* or spins forever (an external-scanner infinite loop the quiet-timeout kills).
* This gives an honest guard — green iff the supervisor actually contains a real
* fault — instead of a fixture that may stop faulting once a root cause is fixed. */
/* Crash-supervisor per-file marker (Stage 3c skip-and-continue). In the
* supervisor's single-threaded recovery re-run, cbm_extract_file records the
* file it is ABOUT to process here (CBM_INDEX_MARKER_FILE) before touching it,
* so that if this file hard-crashes the worker the parent can read the marker
* back and learn the EXACT crasher to quarantine. Only meaningful single-
* threaded (parallel would race the marker); the env var is set solely by the
* supervisor during recovery, so it is a no-op on every normal run. */
static void cbm_index_write_marker(const char *rel_path) {
const char *mf = getenv("CBM_INDEX_MARKER_FILE");
if (!mf || !mf[0] || !rel_path || !rel_path[0]) {
return;
}
FILE *f = cbm_fopen(mf, "wb");
if (f) {
(void)fputs(rel_path, f);
(void)fclose(f);
}
}

/* ── Crash-quarantine set (Stage 3c skip-and-continue) ──────────────────────
* After a crash the supervisor re-runs the worker single-threaded, passing
* CBM_INDEX_QUARANTINE_FILE — a newline-delimited list of repo-relative paths
* that already crashed the indexer and MUST NOT be extracted again. Owned here,
* next to the other env-driven extract hooks (marker + fault injector), so the
* single hard guard lives at the one choke point every pass funnels through
* (cbm_extract_file): whether a pass re-extracts from disk on a cache miss
* (sequential pass_calls/usages/semantic) or extracts fresh, a quarantined file
* short-circuits to an empty result and never reaches the parser/crash. The
* pipeline extract loops separately REPORT the skip as phase="crash" via
* cbm_index_is_quarantined() so the crasher surfaces in the response skipped[].
* Loaded once, lazily; read-only after load (safe for the parallel workers,
* though recovery runs single-threaded). Unset env ⇒ empty set ⇒ cheap no-op. */
static CBMHashTable *g_quarantine_set = NULL;
enum { CBM_QSET_UNINIT = 0, CBM_QSET_INITING = 1, CBM_QSET_INITED = 2 };
static atomic_int g_quarantine_state = CBM_QSET_UNINIT;

static void cbm_quarantine_load(void) {
const char *qf = getenv("CBM_INDEX_QUARANTINE_FILE");
if (!qf || !qf[0]) {
return; /* normal path: empty set */
}
FILE *f = cbm_fopen(qf, "rb");
if (!f) {
return;
}
CBMHashTable *set = cbm_ht_create(16);
if (!set) {
(void)fclose(f);
return;
}
char line[2048];
while (fgets(line, sizeof(line), f)) {
size_t len = strlen(line);
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
line[--len] = '\0';
}
if (len == 0) {
continue;
}
/* Line format: "path\tphase" where phase is "crash" or "hang". A bare
* "path" line (no tab) is tolerated and defaults to phase "crash" for
* backward compatibility with older quarantine files. */
char *tab = strchr(line, '\t');
const char *phase = "crash";
if (tab) {
*tab = '\0';
if (tab[1]) {
phase = tab + 1;
}
}
if (line[0] == '\0') {
continue; /* empty path (line began with a tab) — skip */
}
/* The table borrows the key + value pointers, so dup both. Intentionally
* never freed: the set lives for the whole (short-lived worker) process.
* The value stores the phase so cbm_index_quarantine_phase() can report
* "crash" vs "hang"; membership (cbm_index_is_quarantined) is value != NULL. */
char *key = cbm_strdup(line);
char *pval = cbm_strdup(phase);
if (key && pval) {
cbm_ht_set(set, key, (void *)pval);
}
}
(void)fclose(f);
g_quarantine_set = set;
}

bool cbm_index_is_quarantined(const char *rel_path) {
if (!rel_path || !rel_path[0]) {
return false;
}
int state = atomic_load(&g_quarantine_state);
if (state != CBM_QSET_INITED) {
/* First caller wins the CAS and loads; racers spin until INITED.
* Same once-init pattern as cbm_ui_log_init (http_server.c). */
state = CBM_QSET_UNINIT;
if (atomic_compare_exchange_strong(&g_quarantine_state, &state, CBM_QSET_INITING)) {
cbm_quarantine_load();
atomic_store(&g_quarantine_state, CBM_QSET_INITED);
} else {
while (atomic_load(&g_quarantine_state) != CBM_QSET_INITED) {
cbm_usleep(1000); /* 1ms */
}
}
}
return g_quarantine_set && cbm_ht_has(g_quarantine_set, rel_path);
}

const char *cbm_index_quarantine_phase(const char *rel_path) {
/* cbm_index_is_quarantined drives the lazy once-load and returns true only
* when the set is loaded and holds rel_path — so on true, g_quarantine_set is
* non-NULL and the stored value is the phase string ("crash"/"hang"). */
if (!cbm_index_is_quarantined(rel_path)) {
return NULL;
}
return (const char *)cbm_ht_get(g_quarantine_set, rel_path);
}

static void cbm_test_fault_inject(const char *rel_path) {
if (!rel_path || !rel_path[0]) {
return;
}
const char *crash_on = getenv("CBM_TEST_CRASH_ON");
if (crash_on && crash_on[0] && strstr(rel_path, crash_on)) {
abort(); /* SIGABRT → WIFSIGNALED → classified as a crash */
}
const char *hang_on = getenv("CBM_TEST_HANG_ON");
if (hang_on && hang_on[0] && strstr(rel_path, hang_on)) {
for (;;) {
/* Busy-spin: the supervisor's quiet-timeout kills + reports us. */
}
}
}

CBMFileResult *cbm_extract_file(const char *source, int source_len, CBMLanguage language,
const char *project, const char *rel_path, int64_t timeout_micros,
const char **extra_defines, const char **include_paths) {
Expand All @@ -507,6 +649,20 @@ CBMFileResult *cbm_extract_file(const char *source, int source_len, CBMLanguage
cbm_arena_init(&result->arena);
CBMArena *a = &result->arena;

/* Crash-quarantine hard guard (Stage 3c): a file the supervisor pinned as a
* crasher must NEVER be parsed again. Return a clean empty result BEFORE the
* marker write and fault injector so no pass (including sequential re-extract
* passes that miss the result cache) can crash on it. The pipeline extract
* loops separately record it as a phase="crash" skip. Checked before the
* marker so quarantined files never overwrite it — the marker keeps pointing
* at the real (non-quarantined) file being processed when a crash hits. */
if (cbm_index_is_quarantined(rel_path)) {
return result;
}

cbm_index_write_marker(rel_path);
cbm_test_fault_inject(rel_path);

// Get language spec
const CBMLangSpec *spec = cbm_lang_spec(language);
if (!spec) {
Expand Down
14 changes: 14 additions & 0 deletions internal/cbm/cbm.h
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,20 @@ void cbm_alloc_init(void);
// Initialize the library. Call once at startup. Returns 0 on success.
int cbm_init(void);

// True when rel_path is in the crash-quarantine set — the newline-delimited list
// of files (CBM_INDEX_QUARANTINE_FILE) the crash supervisor pinned as crashers
// during its single-threaded recovery re-run. Loaded once, lazily; read-only
// after load. cbm_extract_file short-circuits such files to an empty result so no
// pass can crash on them; the pipeline extract loops call this to also REPORT the
// skip as phase="crash". Always false (cheap no-op) when the env var is unset.
bool cbm_index_is_quarantined(const char *rel_path);

// Phase a quarantined file was pinned under: "crash" (a fault signal) or "hang"
// (killed for making no progress). Returns NULL when rel_path is not quarantined.
// Drives the same lazy once-load as cbm_index_is_quarantined. Used by the pipeline
// extract loops to report the skip's phase in skipped[] (falls back to "crash").
const char *cbm_index_quarantine_phase(const char *rel_path);

// Extract all data from one file. Caller must call cbm_free_result().
// source must remain valid for the duration of the call.
// timeout_micros: per-file parse timeout in microseconds (0 = no timeout).
Expand Down
1 change: 1 addition & 0 deletions scripts/security-allowlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
src/foundation/compat_fs.c:popen:cbm_popen wrapper definition (POSIX)
src/foundation/compat_fs.c:cbm_popen:cbm_popen function definition
src/foundation/compat_fs.c:fork:cbm_exec_no_shell — fork+execvp for shell-free subprocess execution
src/foundation/subprocess.c:fork:cbm_run_posix — fork+execv for the crash/hang-isolating index worker (supervisor primitive; child execs immediately, no code runs in the forked image)
src/foundation/compat_fs.c:execvp:cbm_exec_no_shell — direct exec without shell interpretation

# ── CLI: update command (user-initiated, interactive) ──────────────────────
Expand Down
Loading
Loading