diff --git a/.gitignore b/.gitignore index 7a7666e..ca040e1 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,10 @@ Thumbs.db # Local project memory (Claude Code auto-memory) memory/ reference/ +.remember/ + +CLAUDE.md +docs/superpowers/ # Build artifacts build/ diff --git a/src/cypher/cypher.c b/src/cypher/cypher.c index 6aedeb9..24c445a 100644 --- a/src/cypher/cypher.c +++ b/src/cypher/cypher.c @@ -92,7 +92,9 @@ static void lex_string_literal(const char *input, int len, int *pos, char quote, int start = *pos; char buf[CBM_SZ_4K]; int blen = 0; + const int max_blen = CBM_SZ_4K - 1; while (*pos < len && input[*pos] != quote) { + if (blen >= max_blen) { (*pos)++; continue; } if (input[*pos] == '\\' && *pos + SKIP_ONE < len) { (*pos)++; switch (input[*pos]) { @@ -469,6 +471,9 @@ static int parse_props(parser_t *p, cbm_prop_filter_t **out, int *count) { int cap = CYP_INIT_CAP4; int n = 0; cbm_prop_filter_t *arr = malloc(cap * sizeof(cbm_prop_filter_t)); + if (!arr) { + return CBM_NOT_FOUND; + } while (!check(p, TOK_RBRACE) && !check(p, TOK_EOF)) { const cbm_token_t *key = expect(p, TOK_IDENT); @@ -569,6 +574,9 @@ static int parse_rel_types(parser_t *p, cbm_rel_pattern_t *out) { int cap = CYP_INIT_CAP4; int n = 0; const char **types = malloc(cap * sizeof(const char *)); + if (!types) { + return CBM_NOT_FOUND; + } const cbm_token_t *t = expect(p, TOK_IDENT); if (!t) { @@ -762,6 +770,12 @@ static cbm_expr_t *parse_in_list(parser_t *p, cbm_condition_t *c) { int vcap = CYP_INIT_CAP8; int vn = 0; const char **vals = malloc(vcap * sizeof(const char *)); + if (!vals) { + free((void *)c->variable); + free((void *)c->property); + free((void *)c->op); + return NULL; + } while (!check(p, TOK_RBRACKET) && !check(p, TOK_EOF)) { if (vn > 0) { match(p, TOK_COMMA); @@ -1061,8 +1075,15 @@ static const char *parse_value_literal(parser_t *p) { static cbm_case_expr_t *parse_case_expr(parser_t *p) { /* CASE already consumed */ cbm_case_expr_t *kase = calloc(CBM_ALLOC_ONE, sizeof(cbm_case_expr_t)); + if (!kase) { + return NULL; + } int bcap = CYP_INIT_CAP4; kase->branches = malloc(bcap * sizeof(cbm_case_branch_t)); + if (!kase->branches) { + free(kase); + return NULL; + } while (check(p, TOK_WHEN)) { advance(p); diff --git a/src/foundation/compat_thread.c b/src/foundation/compat_thread.c index e87afb1..163aaa2 100644 --- a/src/foundation/compat_thread.c +++ b/src/foundation/compat_thread.c @@ -59,6 +59,14 @@ int cbm_thread_join(cbm_thread_t *t) { return 0; } +int cbm_thread_detach(cbm_thread_t *t) { + if (t->handle) { + CloseHandle(t->handle); + t->handle = NULL; + } + return 0; +} + #else /* POSIX */ int cbm_thread_create(cbm_thread_t *t, size_t stack_size, void *(*fn)(void *), void *arg) { @@ -77,6 +85,10 @@ int cbm_thread_join(cbm_thread_t *t) { return pthread_join(t->handle, NULL); } +int cbm_thread_detach(cbm_thread_t *t) { + return pthread_detach(t->handle); +} + #endif /* ── Mutex ────────────────────────────────────────────────────── */ diff --git a/src/foundation/compat_thread.h b/src/foundation/compat_thread.h index 145b68b..7d56109 100644 --- a/src/foundation/compat_thread.h +++ b/src/foundation/compat_thread.h @@ -39,6 +39,9 @@ int cbm_thread_create(cbm_thread_t *t, size_t stack_size, void *(*fn)(void *), v /* Wait for thread to finish. Returns 0 on success. */ int cbm_thread_join(cbm_thread_t *t); +/* Detach thread so resources are freed on exit. Returns 0 on success. */ +int cbm_thread_detach(cbm_thread_t *t); + /* ── Mutex ────────────────────────────────────────────────────── */ #ifdef _WIN32 diff --git a/src/foundation/platform.h b/src/foundation/platform.h index 5624810..f0665d5 100644 --- a/src/foundation/platform.h +++ b/src/foundation/platform.h @@ -31,6 +31,48 @@ static inline void *safe_realloc(void *ptr, size_t size) { return tmp; } +/* Safe free: frees and NULLs a pointer to prevent double-free / use-after-free. + * Accepts void** so it works with any pointer type via the macro. */ +static inline void safe_free_impl(void **pp) { + if (pp && *pp) { + free(*pp); + *pp = NULL; + } +} +#define safe_free(ptr) safe_free_impl((void **)(void *)&(ptr)) + +/* Safe string free: frees a const char* and NULLs it. + * Casts away const so callers don't need the (void*) dance. */ +static inline void safe_str_free(const char **sp) { + if (sp && *sp) { + free((void *)*sp); + *sp = NULL; + } +} + +/* Safe buffer free: frees a heap array and zeros its element count. + * Use for dynamic arrays paired with a size_t count. */ +static inline void safe_buf_free_impl(void **buf, size_t *count) { + if (buf && *buf) { + free(*buf); + *buf = NULL; + } + if (count) { + *count = 0; + } +} +#define safe_buf_free(buf, countp) safe_buf_free_impl((void **)(void *)&(buf), (countp)) + +/* Safe grow: doubles capacity and reallocs when count reaches cap. + * Usage: safe_grow(arr, count, cap, growth_factor) + * Evaluates to the new arr (NULL on OOM — old memory freed by safe_realloc). */ +#define safe_grow(arr, n, cap, factor) do { \ + if ((size_t)(n) >= (size_t)(cap)) { \ + (cap) *= (factor); \ + (arr) = safe_realloc((arr), (size_t)(cap) * sizeof(*(arr))); \ + } \ +} while (0) + /* ── Memory mapping ────────────────────────────────────────────── */ /* Map a file read-only into memory. Returns NULL on error. diff --git a/src/main.c b/src/main.c index 0c7c710..c66305d 100644 --- a/src/main.c +++ b/src/main.c @@ -315,6 +315,9 @@ int main(int argc, char **argv) { } /* Create and start watcher in background thread */ + /* Initialize log mutex before any threads are created */ + cbm_ui_log_init(); + cbm_store_t *watch_store = cbm_store_open_memory(); g_watcher = cbm_watcher_new(watch_store, watcher_index_fn, NULL); diff --git a/src/pipeline/pass_semantic.c b/src/pipeline/pass_semantic.c index ef32780..253070d 100644 --- a/src/pipeline/pass_semantic.c +++ b/src/pipeline/pass_semantic.c @@ -321,6 +321,9 @@ static void resolve_decorator(cbm_pipeline_ctx_t *ctx, const cbm_gbuf_node_t *no char props[CBM_SZ_256]; snprintf(props, sizeof(props), "{\"decorator\":\"%s\"}", decorator); cbm_gbuf_insert_edge(ctx->gbuf, node->id, dec->id, "DECORATES", props); + /* Also emit CALLS edge so decorator appears in "find all references" queries */ + cbm_gbuf_insert_edge(ctx->gbuf, node->id, dec->id, "CALLS", + "{\"kind\":\"decorator\"}"); (*count)++; } } diff --git a/src/store/store.c b/src/store/store.c index cacf97f..f350942 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -2704,7 +2704,9 @@ int cbm_store_get_schema(cbm_store_t *s, const char *project, cbm_schema_info_t const char *sql = "SELECT label, COUNT(*) FROM nodes WHERE project = ?1 GROUP BY label " "ORDER BY COUNT(*) DESC;"; sqlite3_stmt *stmt = NULL; - sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL); + if (sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK || !stmt) { + return CBM_NOT_FOUND; + } bind_text(stmt, SKIP_ONE, project); int cap = ST_INIT_CAP_8; @@ -2729,7 +2731,9 @@ int cbm_store_get_schema(cbm_store_t *s, const char *project, cbm_schema_info_t const char *sql = "SELECT type, COUNT(*) FROM edges WHERE project = ?1 GROUP BY type ORDER " "BY COUNT(*) DESC;"; sqlite3_stmt *stmt = NULL; - sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL); + if (sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK || !stmt) { + return CBM_NOT_FOUND; + } bind_text(stmt, SKIP_ONE, project); int cap = ST_INIT_CAP_8; @@ -3435,7 +3439,9 @@ static bool pkg_in_list(const char *pkg, char **list, int count) { static int collect_pkg_names(cbm_store_t *s, const char *sql, const char *project, char **pkgs, int max_pkgs) { sqlite3_stmt *stmt = NULL; - sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL); + if (sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK || !stmt) { + return 0; + } bind_text(stmt, SKIP_ONE, project); int count = 0; while (sqlite3_step(stmt) == SQLITE_ROW && count < max_pkgs) { diff --git a/src/ui/http_server.c b/src/ui/http_server.c index 053f317..f5af47f 100644 --- a/src/ui/http_server.c +++ b/src/ui/http_server.c @@ -142,14 +142,17 @@ static int g_log_count = 0; static cbm_mutex_t g_log_mutex; static atomic_int g_log_mutex_init = 0; +/* Must be called once before any threads are created. */ +void cbm_ui_log_init(void) { + if (!atomic_exchange(&g_log_mutex_init, 1)) { + cbm_mutex_init(&g_log_mutex); + } +} + /* Called from a log hook — appends a line to the ring buffer (thread-safe) */ void cbm_ui_log_append(const char *line) { - if (!line) + if (!line || !atomic_load(&g_log_mutex_init)) return; - if (!atomic_load(&g_log_mutex_init)) { - cbm_mutex_init(&g_log_mutex); - atomic_store(&g_log_mutex_init, 1); - } cbm_mutex_lock(&g_log_mutex); snprintf(g_log_ring[g_log_head], LOG_LINE_MAX, "%s", line); g_log_head = (g_log_head + 1) % LOG_RING_SIZE; @@ -791,6 +794,7 @@ static void handle_index_start(struct mg_connection *c, struct mg_http_message * mg_http_reply(c, 500, g_cors_json, "{\"error\":\"thread creation failed\"}"); return; } + cbm_thread_detach(&tid); /* Don't leak thread handle */ mg_http_reply(c, 202, g_cors_json, "{\"status\":\"indexing\",\"slot\":%d,\"path\":\"%s\"}", slot, job->root_path); diff --git a/src/ui/http_server.h b/src/ui/http_server.h index 4858a04..4a63a0f 100644 --- a/src/ui/http_server.h +++ b/src/ui/http_server.h @@ -32,6 +32,9 @@ void cbm_http_server_run(cbm_http_server_t *srv); /* Check if the server started successfully (listener bound). */ bool cbm_http_server_is_running(const cbm_http_server_t *srv); +/* Initialize the log ring buffer mutex. Must be called once before any threads. */ +void cbm_ui_log_init(void); + /* Append a log line to the UI ring buffer (called from log hook). */ void cbm_ui_log_append(const char *line); diff --git a/src/watcher/watcher.c b/src/watcher/watcher.c index 8bef36e..5f3ec76 100644 --- a/src/watcher/watcher.c +++ b/src/watcher/watcher.c @@ -20,6 +20,7 @@ #include "foundation/log.h" #include "foundation/hash_table.h" #include "foundation/compat.h" +#include "foundation/compat_thread.h" #include "foundation/compat_fs.h" #include "foundation/str_util.h" @@ -50,6 +51,7 @@ struct cbm_watcher { cbm_index_fn index_fn; void *user_data; CBMHashTable *projects; /* name → project_state_t* */ + cbm_mutex_t projects_lock; atomic_int stopped; }; @@ -236,6 +238,7 @@ cbm_watcher_t *cbm_watcher_new(cbm_store_t *store, cbm_index_fn index_fn, void * w->index_fn = index_fn; w->user_data = user_data; w->projects = cbm_ht_create(CBM_SZ_32); + cbm_mutex_init(&w->projects_lock); atomic_init(&w->stopped, 0); return w; } @@ -244,8 +247,11 @@ void cbm_watcher_free(cbm_watcher_t *w) { if (!w) { return; } + cbm_mutex_lock(&w->projects_lock); cbm_ht_foreach(w->projects, free_state_entry, NULL); cbm_ht_free(w->projects); + cbm_mutex_unlock(&w->projects_lock); + cbm_mutex_destroy(&w->projects_lock); free(w); } @@ -264,6 +270,7 @@ void cbm_watcher_watch(cbm_watcher_t *w, const char *project_name, const char *r } /* Remove old entry first (key points to state's project_name) */ + cbm_mutex_lock(&w->projects_lock); project_state_t *old = cbm_ht_get(w->projects, project_name); if (old) { cbm_ht_delete(w->projects, project_name); @@ -272,6 +279,7 @@ void cbm_watcher_watch(cbm_watcher_t *w, const char *project_name, const char *r project_state_t *s = state_new(project_name, root_path); cbm_ht_set(w->projects, s->project_name, s); + cbm_mutex_unlock(&w->projects_lock); cbm_log_info("watcher.watch", "project", project_name, "path", root_path); } @@ -279,10 +287,14 @@ void cbm_watcher_unwatch(cbm_watcher_t *w, const char *project_name) { if (!w || !project_name) { return; } + cbm_mutex_lock(&w->projects_lock); project_state_t *s = cbm_ht_get(w->projects, project_name); if (s) { cbm_ht_delete(w->projects, project_name); state_free(s); + } + cbm_mutex_unlock(&w->projects_lock); + if (s) { cbm_log_info("watcher.unwatch", "project", project_name); } } @@ -421,7 +433,9 @@ int cbm_watcher_poll_once(cbm_watcher_t *w) { .now = now_ns(), .reindexed = 0, }; + cbm_mutex_lock(&w->projects_lock); cbm_ht_foreach(w->projects, poll_project, &ctx); + cbm_mutex_unlock(&w->projects_lock); return ctx.reindexed; } diff --git a/tests/test_cypher.c b/tests/test_cypher.c index 13527d5..a169432 100644 --- a/tests/test_cypher.c +++ b/tests/test_cypher.c @@ -78,6 +78,32 @@ TEST(cypher_lex_single_quote_string) { PASS(); } +TEST(cypher_lex_string_overflow) { + /* Build a string literal longer than 4096 bytes to verify we don't + * overflow the stack buffer in lex_string_literal. */ + const int big = 5000; + /* query: "AAAA...A" (quotes included) */ + char *query = malloc(big + 3); /* quote + big chars + quote + NUL */ + ASSERT_NOT_NULL(query); + query[0] = '"'; + memset(query + 1, 'A', big); + query[big + 1] = '"'; + query[big + 2] = '\0'; + + cbm_lex_result_t r = {0}; + int rc = cbm_lex(query, &r); + ASSERT_EQ(rc, 0); + ASSERT_NULL(r.error); + ASSERT_GTE(r.count, 1); + ASSERT_EQ(r.tokens[0].type, TOK_STRING); + /* The string should be truncated to CBM_SZ_4K - 1 (4095) characters. */ + ASSERT_EQ((int)strlen(r.tokens[0].text), 4095); + + cbm_lex_free(&r); + free(query); + PASS(); +} + TEST(cypher_lex_number) { cbm_lex_result_t r = {0}; int rc = cbm_lex("42 3.14", &r); @@ -2064,6 +2090,7 @@ SUITE(cypher) { RUN_TEST(cypher_lex_relationship); RUN_TEST(cypher_lex_string_literal); RUN_TEST(cypher_lex_single_quote_string); + RUN_TEST(cypher_lex_string_overflow); RUN_TEST(cypher_lex_number); RUN_TEST(cypher_lex_operators); RUN_TEST(cypher_lex_keywords_case_insensitive);