1414#include "lsp/rust_lsp.h"
1515#include "preprocessor.h"
1616#include "foundation/compat.h"
17+ #include "foundation/compat_fs.h" // cbm_fopen — crash-supervisor per-file marker write
18+ #include "foundation/hash_table.h" // CBMHashTable — crash-supervisor quarantine set
1719#include "tree_sitter/api.h" // TSParser, TSNode, TSTree, TSInput, TSLanguage, TSPoint, TSParseOptions, TSParseState
1820#include "foundation/constants.h"
1921#include "mimalloc.h" // mi_malloc/mi_calloc/mi_realloc/mi_free/mi_usable_size — bind 3rd-party allocators (#424)
@@ -566,6 +568,146 @@ static int count_params_from_signature(const char *sig) {
566568
567569// --- Main extraction function ---
568570
571+ /* Test-only deterministic fault injection for the crash/hang supervisor tests.
572+ * Gated entirely behind env vars that are never set in production; a matching
573+ * rel_path either aborts (a fault signal the supervisor classifies as a crash)
574+ * or spins forever (an external-scanner infinite loop the quiet-timeout kills).
575+ * This gives an honest guard — green iff the supervisor actually contains a real
576+ * fault — instead of a fixture that may stop faulting once a root cause is fixed. */
577+ /* Crash-supervisor per-file marker (Stage 3c skip-and-continue). In the
578+ * supervisor's single-threaded recovery re-run, cbm_extract_file records the
579+ * file it is ABOUT to process here (CBM_INDEX_MARKER_FILE) before touching it,
580+ * so that if this file hard-crashes the worker the parent can read the marker
581+ * back and learn the EXACT crasher to quarantine. Only meaningful single-
582+ * threaded (parallel would race the marker); the env var is set solely by the
583+ * supervisor during recovery, so it is a no-op on every normal run. */
584+ static void cbm_index_write_marker (const char * rel_path ) {
585+ const char * mf = getenv ("CBM_INDEX_MARKER_FILE" );
586+ if (!mf || !mf [0 ] || !rel_path || !rel_path [0 ]) {
587+ return ;
588+ }
589+ FILE * f = cbm_fopen (mf , "wb" );
590+ if (f ) {
591+ (void )fputs (rel_path , f );
592+ (void )fclose (f );
593+ }
594+ }
595+
596+ /* ── Crash-quarantine set (Stage 3c skip-and-continue) ──────────────────────
597+ * After a crash the supervisor re-runs the worker single-threaded, passing
598+ * CBM_INDEX_QUARANTINE_FILE — a newline-delimited list of repo-relative paths
599+ * that already crashed the indexer and MUST NOT be extracted again. Owned here,
600+ * next to the other env-driven extract hooks (marker + fault injector), so the
601+ * single hard guard lives at the one choke point every pass funnels through
602+ * (cbm_extract_file): whether a pass re-extracts from disk on a cache miss
603+ * (sequential pass_calls/usages/semantic) or extracts fresh, a quarantined file
604+ * short-circuits to an empty result and never reaches the parser/crash. The
605+ * pipeline extract loops separately REPORT the skip as phase="crash" via
606+ * cbm_index_is_quarantined() so the crasher surfaces in the response skipped[].
607+ * Loaded once, lazily; read-only after load (safe for the parallel workers,
608+ * though recovery runs single-threaded). Unset env ⇒ empty set ⇒ cheap no-op. */
609+ static CBMHashTable * g_quarantine_set = NULL ;
610+ enum { CBM_QSET_UNINIT = 0 , CBM_QSET_INITING = 1 , CBM_QSET_INITED = 2 };
611+ static atomic_int g_quarantine_state = CBM_QSET_UNINIT ;
612+
613+ static void cbm_quarantine_load (void ) {
614+ const char * qf = getenv ("CBM_INDEX_QUARANTINE_FILE" );
615+ if (!qf || !qf [0 ]) {
616+ return ; /* normal path: empty set */
617+ }
618+ FILE * f = cbm_fopen (qf , "rb" );
619+ if (!f ) {
620+ return ;
621+ }
622+ CBMHashTable * set = cbm_ht_create (16 );
623+ if (!set ) {
624+ (void )fclose (f );
625+ return ;
626+ }
627+ char line [2048 ];
628+ while (fgets (line , sizeof (line ), f )) {
629+ size_t len = strlen (line );
630+ while (len > 0 && (line [len - 1 ] == '\n' || line [len - 1 ] == '\r' )) {
631+ line [-- len ] = '\0' ;
632+ }
633+ if (len == 0 ) {
634+ continue ;
635+ }
636+ /* Line format: "path\tphase" where phase is "crash" or "hang". A bare
637+ * "path" line (no tab) is tolerated and defaults to phase "crash" for
638+ * backward compatibility with older quarantine files. */
639+ char * tab = strchr (line , '\t' );
640+ const char * phase = "crash" ;
641+ if (tab ) {
642+ * tab = '\0' ;
643+ if (tab [1 ]) {
644+ phase = tab + 1 ;
645+ }
646+ }
647+ if (line [0 ] == '\0' ) {
648+ continue ; /* empty path (line began with a tab) — skip */
649+ }
650+ /* The table borrows the key + value pointers, so dup both. Intentionally
651+ * never freed: the set lives for the whole (short-lived worker) process.
652+ * The value stores the phase so cbm_index_quarantine_phase() can report
653+ * "crash" vs "hang"; membership (cbm_index_is_quarantined) is value != NULL. */
654+ char * key = cbm_strdup (line );
655+ char * pval = cbm_strdup (phase );
656+ if (key && pval ) {
657+ cbm_ht_set (set , key , (void * )pval );
658+ }
659+ }
660+ (void )fclose (f );
661+ g_quarantine_set = set ;
662+ }
663+
664+ bool cbm_index_is_quarantined (const char * rel_path ) {
665+ if (!rel_path || !rel_path [0 ]) {
666+ return false;
667+ }
668+ int state = atomic_load (& g_quarantine_state );
669+ if (state != CBM_QSET_INITED ) {
670+ /* First caller wins the CAS and loads; racers spin until INITED.
671+ * Same once-init pattern as cbm_ui_log_init (http_server.c). */
672+ state = CBM_QSET_UNINIT ;
673+ if (atomic_compare_exchange_strong (& g_quarantine_state , & state , CBM_QSET_INITING )) {
674+ cbm_quarantine_load ();
675+ atomic_store (& g_quarantine_state , CBM_QSET_INITED );
676+ } else {
677+ while (atomic_load (& g_quarantine_state ) != CBM_QSET_INITED ) {
678+ cbm_usleep (1000 ); /* 1ms */
679+ }
680+ }
681+ }
682+ return g_quarantine_set && cbm_ht_has (g_quarantine_set , rel_path );
683+ }
684+
685+ const char * cbm_index_quarantine_phase (const char * rel_path ) {
686+ /* cbm_index_is_quarantined drives the lazy once-load and returns true only
687+ * when the set is loaded and holds rel_path — so on true, g_quarantine_set is
688+ * non-NULL and the stored value is the phase string ("crash"/"hang"). */
689+ if (!cbm_index_is_quarantined (rel_path )) {
690+ return NULL ;
691+ }
692+ return (const char * )cbm_ht_get (g_quarantine_set , rel_path );
693+ }
694+
695+ static void cbm_test_fault_inject (const char * rel_path ) {
696+ if (!rel_path || !rel_path [0 ]) {
697+ return ;
698+ }
699+ const char * crash_on = getenv ("CBM_TEST_CRASH_ON" );
700+ if (crash_on && crash_on [0 ] && strstr (rel_path , crash_on )) {
701+ abort (); /* SIGABRT → WIFSIGNALED → classified as a crash */
702+ }
703+ const char * hang_on = getenv ("CBM_TEST_HANG_ON" );
704+ if (hang_on && hang_on [0 ] && strstr (rel_path , hang_on )) {
705+ for (;;) {
706+ /* Busy-spin: the supervisor's quiet-timeout kills + reports us. */
707+ }
708+ }
709+ }
710+
569711CBMFileResult * cbm_extract_file (const char * source , int source_len , CBMLanguage language ,
570712 const char * project , const char * rel_path , int64_t timeout_micros ,
571713 const char * * extra_defines , const char * * include_paths ) {
@@ -579,6 +721,20 @@ CBMFileResult *cbm_extract_file(const char *source, int source_len, CBMLanguage
579721 cbm_arena_init (& result -> arena );
580722 CBMArena * a = & result -> arena ;
581723
724+ /* Crash-quarantine hard guard (Stage 3c): a file the supervisor pinned as a
725+ * crasher must NEVER be parsed again. Return a clean empty result BEFORE the
726+ * marker write and fault injector so no pass (including sequential re-extract
727+ * passes that miss the result cache) can crash on it. The pipeline extract
728+ * loops separately record it as a phase="crash" skip. Checked before the
729+ * marker so quarantined files never overwrite it — the marker keeps pointing
730+ * at the real (non-quarantined) file being processed when a crash hits. */
731+ if (cbm_index_is_quarantined (rel_path )) {
732+ return result ;
733+ }
734+
735+ cbm_index_write_marker (rel_path );
736+ cbm_test_fault_inject (rel_path );
737+
582738 // Get language spec
583739 const CBMLangSpec * spec = cbm_lang_spec (language );
584740 if (!spec ) {
0 commit comments