From 152ce07955a41dc809804b2d0c5db5109d3fb408 Mon Sep 17 00:00:00 2001 From: map588 Date: Sat, 4 Apr 2026 19:08:09 -0400 Subject: [PATCH 01/18] fix(cypher): prevent stack buffer overflow in string literal lexer --- src/cypher/cypher.c | 2 ++ tests/test_cypher.c | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/cypher/cypher.c b/src/cypher/cypher.c index 6aedeb92..89ff779e 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]) { diff --git a/tests/test_cypher.c b/tests/test_cypher.c index 13527d53..a1694322 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); From 1d39640032592efeeeed0aeda2c6cd47acceded3 Mon Sep 17 00:00:00 2001 From: map588 Date: Sat, 4 Apr 2026 20:17:47 -0400 Subject: [PATCH 02/18] fix(store): add sqlite3_prepare_v2 error checks to prevent NULL stmt crashes Three locations in store.c called sqlite3_prepare_v2 without checking the return code. If the statement fails to prepare (DB corruption, malformed SQL), subsequent bind_text and sqlite3_step calls dereference NULL, crashing the server. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/agents/c-test-writer.md | 23 +++++++++++++++ .claude/agents/security-reviewer.md | 20 +++++++++++++ .claude/settings.json | 26 +++++++++++++++++ .claude/skills/add-language/SKILL.md | 39 ++++++++++++++++++++++++++ .claude/skills/security-audit/SKILL.md | 23 +++++++++++++++ .claude/worktrees/improvements | 1 + .gitignore | 4 +++ src/cypher/cypher.c | 19 +++++++++++++ src/foundation/compat_thread.c | 12 ++++++++ src/foundation/compat_thread.h | 3 ++ src/main.c | 3 ++ src/store/store.c | 12 ++++++-- src/ui/http_server.c | 14 +++++---- src/ui/http_server.h | 3 ++ 14 files changed, 194 insertions(+), 8 deletions(-) create mode 100644 .claude/agents/c-test-writer.md create mode 100644 .claude/agents/security-reviewer.md create mode 100644 .claude/settings.json create mode 100644 .claude/skills/add-language/SKILL.md create mode 100644 .claude/skills/security-audit/SKILL.md create mode 160000 .claude/worktrees/improvements diff --git a/.claude/agents/c-test-writer.md b/.claude/agents/c-test-writer.md new file mode 100644 index 00000000..425f91c5 --- /dev/null +++ b/.claude/agents/c-test-writer.md @@ -0,0 +1,23 @@ +You write C tests for a pure C11 codebase using the custom test framework in `tests/test_framework.h`. + +## Conventions + +- Use the `TEST(name)` macro to define test functions. +- Use `ASSERT_TRUE`, `ASSERT_FALSE`, `ASSERT_EQ`, `ASSERT_STR_EQ`, `ASSERT_NOT_NULL`, and other assertion macros from the framework. +- Each test must be self-contained with proper setup and teardown (especially freeing arenas and closing store handles). +- Tests compile with ASan + UBSan — no memory leaks, no undefined behavior. + +## Patterns to follow + +- **Store tests**: See `tests/test_store_nodes.c`, `tests/test_store_edges.c` — open a temporary in-memory store, perform operations, assert results, close store. +- **Pipeline tests**: See `tests/test_pipeline.c` — write source to a temp file, run pipeline passes, query the resulting graph. +- **Extraction tests**: See `tests/test_extraction.c` — parse source with tree-sitter, verify extracted functions/classes/calls. +- **MCP tests**: See `tests/test_mcp.c` — construct JSON-RPC requests, call handlers, verify JSON responses. +- **Foundation tests**: See `tests/test_arena.c`, `tests/test_hash_table.c` — unit test data structures directly. + +## Build and run + +```bash +scripts/test.sh # Full suite with sanitizers +make -f Makefile.cbm test-foundation # Foundation tests only (fast) +``` diff --git a/.claude/agents/security-reviewer.md b/.claude/agents/security-reviewer.md new file mode 100644 index 00000000..4dcd93f9 --- /dev/null +++ b/.claude/agents/security-reviewer.md @@ -0,0 +1,20 @@ +You are a security reviewer for a pure C11 codebase that implements an MCP server. + +## What to check + +1. **Dangerous calls** — Any new `system()`, `popen()`, `fork()`, `exec*()`, or network calls must be listed in `scripts/security-allowlist.txt`. Flag any that are missing. +2. **Buffer safety** — Look for unbounded `strcpy`, `sprintf`, `strcat`, `gets`. All should use bounded variants (`strncpy`, `snprintf`, arena-allocated buffers). +3. **SQL injection** — All queries in `src/store/store.c` must use parameterized statements (`sqlite3_bind_*`). Flag any string-concatenated SQL. +4. **Prompt injection** — MCP tool handlers in `src/mcp/mcp.c` must validate and sanitize all user-provided input before including it in responses or graph queries. +5. **Memory safety** — Check for use-after-free, double-free, null dereference, and uninitialized reads. The project uses arena allocators (`src/foundation/arena.c`) — verify allocations go through arenas where appropriate. +6. **NOLINT usage** — Any `// NOLINT` suppression must be whitelisted in `src/foundation/recursion_whitelist.h`. Flag unwhitelisted suppressions. +7. **Integer overflow** — Check size calculations, especially in allocation paths and buffer length computations. + +## How to verify + +Run the 8-layer security audit: +```bash +make -f Makefile.cbm security +``` + +Review `scripts/security-allowlist.txt` for the current allow-list of dangerous calls. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..a897c34e --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,26 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "if echo \"$CLAUDE_FILE_PATH\" | grep -qE '\\.([ch])$'; then clang-format -i \"$CLAUDE_FILE_PATH\"; fi" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "if echo \"$CLAUDE_FILE_PATH\" | grep -qE '(vendored/|internal/cbm/grammar_)'; then echo 'BLOCKED: Do not edit vendored or generated grammar files' >&2; exit 1; fi" + } + ] + } + ] + } +} diff --git a/.claude/skills/add-language/SKILL.md b/.claude/skills/add-language/SKILL.md new file mode 100644 index 00000000..02e9061f --- /dev/null +++ b/.claude/skills/add-language/SKILL.md @@ -0,0 +1,39 @@ +--- +name: add-language +description: Guide through adding or fixing language support (tree-sitter extraction + pipeline passes) +disable-model-invocation: true +--- + +# Adding Language Support + +Language support has two layers. Determine which type of language you're adding: + +## Standard Languages (need tree-sitter grammar) + +1. **Add grammar** — Vendor the tree-sitter grammar into `internal/cbm/grammar_.c` using `scripts/vendor-grammar.sh` +2. **Configure node types** — Add language entry in `internal/cbm/lang_specs.c` with AST node types for functions, classes, calls, imports +3. **Write extractor** — Create `internal/cbm/extract_.c` for language-specific extraction logic +4. **Add enum** — Add `CBM_LANG_` to `internal/cbm/cbm.h` +5. **Hook into pipeline** — Update `src/pipeline/pipeline.c` for call resolution, usage tracking +6. **Add tests**: + - `tests/test_extraction.c` — AST extraction regression tests + - `tests/test_pipeline.c` — Integration-level pipeline tests + +## Infrastructure Languages (Dockerfile, K8s, etc. — no new grammar needed) + +Follow the **infra-pass pattern**: + +1. **Detection helper** — Add `cbm_is__file()` in `src/pipeline/pass_infrascan.c` +2. **Enum value** — Add `CBM_LANG_` in `internal/cbm/cbm.h` and row in `lang_specs.c` +3. **Custom extractor** — Write extractor returning `CBMFileResult*` (reuse YAML grammar if applicable) +4. **Pipeline pass** — Register in `pipeline.c` +5. **Tests** — Follow `TEST(infra_is_dockerfile)` and `TEST(k8s_extract_manifest)` patterns in `tests/test_pipeline.c` + +## Verification + +```bash +scripts/test.sh # Full test suite +scripts/lint.sh # Must pass all linters +``` + +Test against a real open-source repo that uses the language. diff --git a/.claude/skills/security-audit/SKILL.md b/.claude/skills/security-audit/SKILL.md new file mode 100644 index 00000000..c23e7bf6 --- /dev/null +++ b/.claude/skills/security-audit/SKILL.md @@ -0,0 +1,23 @@ +--- +name: security-audit +description: Run the full 8-layer security audit and analyze results +--- + +Run the 8-layer security audit: + +```bash +make -f Makefile.cbm security +``` + +Analyze the output. The 8 layers are: + +1. **Static allow-list audit** — Checks for dangerous calls (`system`, `popen`, `fork`, network) not in `scripts/security-allowlist.txt` +2. **Binary string scan** — Searches compiled binary for suspicious strings +3. **UI audit** — Validates embedded frontend assets +4. **Install audit** — Checks install scripts for unsafe operations +5. **Network egress test** — Verifies no unauthorized network access +6. **MCP robustness (fuzz)** — Sends malformed JSON-RPC to test input handling +7. **Vendored dependency integrity** — Verifies vendored source checksums +8. **Frontend integrity** — Checks graph-ui build artifacts + +For each failure, explain what the layer checks, why it failed, and how to fix it. If a new dangerous call is intentional, guide adding it to `scripts/security-allowlist.txt`. diff --git a/.claude/worktrees/improvements b/.claude/worktrees/improvements new file mode 160000 index 00000000..1d30971f --- /dev/null +++ b/.claude/worktrees/improvements @@ -0,0 +1 @@ +Subproject commit 1d30971ff0f7a817e2e60f8c16f604e893a73166 diff --git a/.gitignore b/.gitignore index 7a7666e0..ca040e13 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 89ff779e..24c445ab 100644 --- a/src/cypher/cypher.c +++ b/src/cypher/cypher.c @@ -471,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); @@ -571,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) { @@ -764,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); @@ -1063,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 e87afb12..163aaa2b 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 145b68bf..7d561093 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/main.c b/src/main.c index 9a79d05e..9f8f187f 100644 --- a/src/main.c +++ b/src/main.c @@ -307,6 +307,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/store/store.c b/src/store/store.c index 4920732e..4d5db451 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -2552,7 +2552,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; @@ -2577,7 +2579,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; @@ -3283,7 +3287,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 053f317b..f5af47fa 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 4858a049..4a63a0f5 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); From fd360458381457db883931f825653547f63e7f83 Mon Sep 17 00:00:00 2001 From: map588 Date: Sat, 4 Apr 2026 20:20:16 -0400 Subject: [PATCH 03/18] fix(watcher): add mutex to protect projects hash table from concurrent access The watcher's projects hash table was written by the main thread (watch/unwatch) and iterated by the watcher thread (poll_once) with no synchronization. Added cbm_mutex_t to the watcher struct and wrapped all hash table operations. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/watcher/watcher.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/watcher/watcher.c b/src/watcher/watcher.c index 8bef36e9..5f3ec760 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; } From ac8fde696c4f58cf1beda0829f200283eaad041d Mon Sep 17 00:00:00 2001 From: map588 Date: Sat, 4 Apr 2026 20:20:24 -0400 Subject: [PATCH 04/18] feat(pipeline): emit CALLS edges for decorator applications Decorators previously only created DECORATES edges. A @login_required decorator was invisible to "find all references" queries because those look for CALLS and USAGE edges. Now resolve_decorator emits both DECORATES and CALLS edges. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/pipeline/pass_semantic.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pipeline/pass_semantic.c b/src/pipeline/pass_semantic.c index ef327801..253070d9 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)++; } } From bac1867e6c92f8284dd3d4e98d9d8b92d4c3fb78 Mon Sep 17 00:00:00 2001 From: map588 Date: Sat, 4 Apr 2026 20:54:40 -0400 Subject: [PATCH 05/18] feat(pipeline): emit CALLS edges for decorator applications Decorators previously only created DECORATES edges. References like @login_required were invisible to "find all references" queries which look for CALLS and USAGE edges. Now both sequential (resolve_decorator) and parallel (resolve_def_decorators) paths emit a CALLS edge alongside DECORATES. Uses "{}" properties to avoid clobbering richer metadata from pass_calls when a function both has @decorator and calls decorator() directly. --- src/pipeline/pass_parallel.c | 3 +++ src/pipeline/pass_semantic.c | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 68843df2..28125597 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -1261,6 +1261,9 @@ static void resolve_def_decorators(resolve_ctx_t *rc, resolve_worker_state_t *ws char dp[CBM_SZ_256]; snprintf(dp, sizeof(dp), "{\"decorator\":\"%s\"}", def->decorators[dc]); cbm_gbuf_insert_edge(ws->local_edge_buf, node->id, dn->id, "DECORATES", dp); + /* Ensure a CALLS edge exists so decorator appears in reference queries. + * Use "{}" to avoid clobbering richer metadata from pass_calls. */ + cbm_gbuf_insert_edge(ws->local_edge_buf, node->id, dn->id, "CALLS", "{}"); ws->semantic_resolved++; } } diff --git a/src/pipeline/pass_semantic.c b/src/pipeline/pass_semantic.c index ef327801..30c6081f 100644 --- a/src/pipeline/pass_semantic.c +++ b/src/pipeline/pass_semantic.c @@ -321,6 +321,10 @@ 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); + /* Ensure a CALLS edge exists so decorator appears in reference queries. + * Use "{}" to avoid clobbering richer metadata from pass_calls + * (dedup skips replacement when new props are "{}"). */ + cbm_gbuf_insert_edge(ctx->gbuf, node->id, dec->id, "CALLS", "{}"); (*count)++; } } From c8661b9633a86252c0f214347a000019c098fd82 Mon Sep 17 00:00:00 2001 From: map588 Date: Sat, 4 Apr 2026 20:42:48 -0400 Subject: [PATCH 06/18] fix(cypher,store): prevent crashes from buffer overflow, OOM, and NULL stmts - cypher: Add bounds check in lex_string_literal to prevent stack buffer overflow on string literals >4096 bytes. Escape sequences are always parsed correctly even past the truncation boundary. - cypher: Add malloc/calloc NULL checks in parse_props, parse_rel_types, parse_in_condition, and parse_case_expr. Growth paths use non-destructive realloc (temp pointer) so accumulated elements can be freed on OOM instead of leaking through safe_realloc's free-on-failure semantics. - store: Add sqlite3_prepare_v2 return code checks at 3 sites in cbm_store_schema_info and collect_pkg_names. Partially prepared statements are finalized before returning. Schema function cleans up partially populated output on failure. collect_pkg_names returns CBM_NOT_FOUND (not 0) to distinguish errors from empty results. --- src/cypher/cypher.c | 110 +++++++++++++++++++++++++++++++++++--------- src/store/store.c | 22 +++++++-- tests/test_cypher.c | 27 +++++++++++ 3 files changed, 134 insertions(+), 25 deletions(-) diff --git a/src/cypher/cypher.c b/src/cypher/cypher.c index 6aedeb92..b4b049b6 100644 --- a/src/cypher/cypher.c +++ b/src/cypher/cypher.c @@ -92,25 +92,30 @@ 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 (input[*pos] == '\\' && *pos + SKIP_ONE < len) { (*pos)++; - switch (input[*pos]) { - case 'n': - buf[blen++] = '\n'; - break; - case 't': - buf[blen++] = '\t'; - break; - case '\\': - buf[blen++] = '\\'; - break; - default: - buf[blen++] = input[*pos]; - break; + if (blen < max_blen) { + switch (input[*pos]) { + case 'n': + buf[blen++] = '\n'; + break; + case 't': + buf[blen++] = '\t'; + break; + case '\\': + buf[blen++] = '\\'; + break; + default: + buf[blen++] = input[*pos]; + break; + } } } else { - buf[blen++] = input[*pos]; + if (blen < max_blen) { + buf[blen++] = input[*pos]; + } } (*pos)++; } @@ -469,6 +474,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); @@ -487,8 +495,18 @@ static int parse_props(parser_t *p, cbm_prop_filter_t **out, int *count) { } if (n >= cap) { - cap *= PAIR_LEN; - arr = safe_realloc(arr, cap * sizeof(cbm_prop_filter_t)); + int new_cap = cap * PAIR_LEN; + void *tmp = realloc(arr, new_cap * sizeof(cbm_prop_filter_t)); + if (!tmp) { + for (int i = 0; i < n; i++) { + free((void *)arr[i].key); + free((void *)arr[i].value); + } + free(arr); + return CBM_NOT_FOUND; + } + arr = tmp; + cap = new_cap; } arr[n].key = heap_strdup(key->text); arr[n].value = heap_strdup(val->text); @@ -569,6 +587,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) { @@ -587,8 +608,17 @@ static int parse_rel_types(parser_t *p, cbm_rel_pattern_t *out) { return CBM_NOT_FOUND; } if (n >= cap) { - cap *= PAIR_LEN; - types = safe_realloc(types, cap * sizeof(const char *)); + int new_cap = cap * PAIR_LEN; + void *tmp = realloc(types, new_cap * sizeof(const char *)); + if (!tmp) { + for (int i = 0; i < n; i++) { + free((void *)types[i]); + } + free(types); + return CBM_NOT_FOUND; + } + types = (const char **)tmp; + cap = new_cap; } types[n++] = heap_strdup(t->text); } @@ -762,14 +792,32 @@ 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); } if (check(p, TOK_STRING) || check(p, TOK_NUMBER)) { if (vn >= vcap) { - vcap *= PAIR_LEN; - vals = safe_realloc(vals, vcap * sizeof(const char *)); + int new_vcap = vcap * PAIR_LEN; + void *tmp = realloc((void *)vals, new_vcap * sizeof(const char *)); + if (!tmp) { + for (int i = 0; i < vn; i++) { + free((void *)vals[i]); + } + free((void *)vals); + free((void *)c->variable); + free((void *)c->property); + free((void *)c->op); + return NULL; + } + vals = (const char **)tmp; + vcap = new_vcap; } vals[vn++] = heap_strdup(advance(p)->text); } else { @@ -1061,8 +1109,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); @@ -1073,8 +1128,19 @@ static cbm_case_expr_t *parse_case_expr(parser_t *p) { } const char *then_val = parse_value_literal(p); if (kase->branch_count >= bcap) { - bcap *= PAIR_LEN; - kase->branches = safe_realloc(kase->branches, bcap * sizeof(cbm_case_branch_t)); + int new_bcap = bcap * PAIR_LEN; + void *tmp = realloc(kase->branches, new_bcap * sizeof(cbm_case_branch_t)); + if (!tmp) { + expr_free(when); + for (int i = 0; i < kase->branch_count; i++) { + expr_free(kase->branches[i].when_expr); + } + free(kase->branches); + free(kase); + return NULL; + } + kase->branches = tmp; + bcap = new_bcap; } kase->branches[kase->branch_count++] = (cbm_case_branch_t){.when_expr = when, .then_val = then_val}; diff --git a/src/store/store.c b/src/store/store.c index 4920732e..876a828d 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -2552,7 +2552,12 @@ 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) { + if (stmt) { + sqlite3_finalize(stmt); + } + return CBM_NOT_FOUND; + } bind_text(stmt, SKIP_ONE, project); int cap = ST_INIT_CAP_8; @@ -2577,7 +2582,13 @@ 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) { + if (stmt) { + sqlite3_finalize(stmt); + } + cbm_store_schema_free(out); + return CBM_NOT_FOUND; + } bind_text(stmt, SKIP_ONE, project); int cap = ST_INIT_CAP_8; @@ -3283,7 +3294,12 @@ 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) { + if (stmt) { + sqlite3_finalize(stmt); + } + return CBM_NOT_FOUND; + } bind_text(stmt, SKIP_ONE, project); int count = 0; while (sqlite3_step(stmt) == SQLITE_ROW && count < max_pkgs) { diff --git a/tests/test_cypher.c b/tests/test_cypher.c index 13527d53..a1694322 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); From d6966f984902c90cc22e0147563f5fbfc1581cc3 Mon Sep 17 00:00:00 2001 From: map588 Date: Sat, 4 Apr 2026 20:49:56 -0400 Subject: [PATCH 07/18] fix(thread-safety): eliminate races in log mutex, watcher, and index threads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - http_server: Replace lazy log mutex init with 3-state init (UNINIT → INITING → INITED) using atomic CAS. Concurrent callers spin until init completes, preventing use of uninitialized mutex. cbm_ui_log_append calls cbm_ui_log_init on first use so early startup logs are not dropped. - watcher: Add cbm_mutex_t to protect projects hash table. All accessors (watch, unwatch, touch, watch_count, poll_once) are guarded. poll_once snapshots project pointers under lock then polls without holding it, keeping the critical section small during git I/O and indexing. state_new OOM is handled with early return. - compat_thread: Add cbm_thread_detach() for POSIX and Windows. Both join() and detach() clear the handle on success across both platforms for consistent lifecycle tracking. - http_server: Detach index job threads to prevent handle leaks. --- src/foundation/compat_thread.c | 23 +++++++++++- src/foundation/compat_thread.h | 3 ++ src/main.c | 3 ++ src/ui/http_server.c | 36 ++++++++++++++++--- src/ui/http_server.h | 3 ++ src/watcher/watcher.c | 64 ++++++++++++++++++++++++++++++++-- 6 files changed, 124 insertions(+), 8 deletions(-) diff --git a/src/foundation/compat_thread.c b/src/foundation/compat_thread.c index e87afb12..19610cef 100644 --- a/src/foundation/compat_thread.c +++ b/src/foundation/compat_thread.c @@ -56,6 +56,15 @@ int cbm_thread_join(cbm_thread_t *t) { return CBM_NOT_FOUND; } CloseHandle(t->handle); + t->handle = NULL; + return 0; +} + +int cbm_thread_detach(cbm_thread_t *t) { + if (t->handle) { + CloseHandle(t->handle); + t->handle = NULL; + } return 0; } @@ -74,7 +83,19 @@ int cbm_thread_create(cbm_thread_t *t, size_t stack_size, void *(*fn)(void *), v } int cbm_thread_join(cbm_thread_t *t) { - return pthread_join(t->handle, NULL); + int rc = pthread_join(t->handle, NULL); + if (rc == 0) { + memset(&t->handle, 0, sizeof(t->handle)); + } + return rc; +} + +int cbm_thread_detach(cbm_thread_t *t) { + int rc = pthread_detach(t->handle); + if (rc == 0) { + memset(&t->handle, 0, sizeof(t->handle)); + } + return rc; } #endif diff --git a/src/foundation/compat_thread.h b/src/foundation/compat_thread.h index 145b68bf..7d561093 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/main.c b/src/main.c index 9a79d05e..9f8f187f 100644 --- a/src/main.c +++ b/src/main.c @@ -307,6 +307,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/ui/http_server.c b/src/ui/http_server.c index 053f317b..d115905c 100644 --- a/src/ui/http_server.c +++ b/src/ui/http_server.c @@ -140,16 +140,41 @@ static char g_log_ring[LOG_RING_SIZE][LOG_LINE_MAX]; static int g_log_head = 0; static int g_log_count = 0; static cbm_mutex_t g_log_mutex; -static atomic_int g_log_mutex_init = 0; + +enum { + CBM_LOG_MUTEX_UNINIT = 0, + CBM_LOG_MUTEX_INITING = 1, + CBM_LOG_MUTEX_INITED = 2 +}; +static atomic_int g_log_mutex_init = CBM_LOG_MUTEX_UNINIT; + +/* Safe for concurrent callers: only publishes INITED after cbm_mutex_init() + * has completed. Callers that lose the CAS race spin until init finishes. */ +void cbm_ui_log_init(void) { + int state = atomic_load(&g_log_mutex_init); + if (state == CBM_LOG_MUTEX_INITED) + return; + + state = CBM_LOG_MUTEX_UNINIT; + if (atomic_compare_exchange_strong(&g_log_mutex_init, &state, CBM_LOG_MUTEX_INITING)) { + cbm_mutex_init(&g_log_mutex); + atomic_store(&g_log_mutex_init, CBM_LOG_MUTEX_INITED); + return; + } + + /* Another thread is initializing — spin until done */ + while (atomic_load(&g_log_mutex_init) != CBM_LOG_MUTEX_INITED) { + cbm_usleep(1000); /* 1ms */ + } +} /* Called from a log hook — appends a line to the ring buffer (thread-safe) */ void cbm_ui_log_append(const char *line) { if (!line) return; - if (!atomic_load(&g_log_mutex_init)) { - cbm_mutex_init(&g_log_mutex); - atomic_store(&g_log_mutex_init, 1); - } + /* Ensure mutex is initialized (safe for early single-threaded logging + * and concurrent calls via atomic_exchange once-init pattern). */ + cbm_ui_log_init(); 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 +816,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 4858a049..4a63a0f5 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 8bef36e9..8d1f85f9 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); @@ -271,7 +278,13 @@ 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); + if (!s) { + cbm_mutex_unlock(&w->projects_lock); + cbm_log_warn("watcher.watch.oom", "project", project_name, "path", root_path); + return; + } 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 +292,16 @@ void cbm_watcher_unwatch(cbm_watcher_t *w, const char *project_name) { if (!w || !project_name) { return; } + bool removed = false; + 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); + removed = true; + } + cbm_mutex_unlock(&w->projects_lock); + if (removed) { cbm_log_info("watcher.unwatch", "project", project_name); } } @@ -291,18 +310,23 @@ void cbm_watcher_touch(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) { /* Reset backoff — poll immediately on next cycle */ s->next_poll_ns = 0; } + cbm_mutex_unlock(&w->projects_lock); } int cbm_watcher_watch_count(const cbm_watcher_t *w) { if (!w) { return 0; } - return (int)cbm_ht_count(w->projects); + cbm_mutex_lock(&((cbm_watcher_t *)w)->projects_lock); + int count = (int)cbm_ht_count(w->projects); + cbm_mutex_unlock(&((cbm_watcher_t *)w)->projects_lock); + return count; } /* ── Single poll cycle ──────────────────────────────────────────── */ @@ -411,17 +435,53 @@ static void poll_project(const char *key, void *val, void *ud) { s->next_poll_ns = ctx->now + ((int64_t)s->interval_ms * US_PER_MS); } +/* Callback to snapshot project state pointers into an array. */ +typedef struct { + project_state_t **items; + int count; + int cap; +} snapshot_ctx_t; + +static void snapshot_project(const char *key, void *val, void *ud) { + (void)key; + snapshot_ctx_t *sc = ud; + if (val && sc->count < sc->cap) { + sc->items[sc->count++] = val; + } +} + int cbm_watcher_poll_once(cbm_watcher_t *w) { if (!w) { return 0; } + /* Snapshot project pointers under lock, then poll without holding it. + * This keeps the critical section small — poll_project does git I/O + * and may invoke index_fn which runs the full pipeline. */ + cbm_mutex_lock(&w->projects_lock); + int n = cbm_ht_count(w->projects); + if (n == 0) { + cbm_mutex_unlock(&w->projects_lock); + return 0; + } + project_state_t **snap = malloc(n * sizeof(project_state_t *)); + if (!snap) { + cbm_mutex_unlock(&w->projects_lock); + return 0; + } + snapshot_ctx_t sc = {.items = snap, .count = 0, .cap = n}; + cbm_ht_foreach(w->projects, snapshot_project, &sc); + cbm_mutex_unlock(&w->projects_lock); + poll_ctx_t ctx = { .w = w, .now = now_ns(), .reindexed = 0, }; - cbm_ht_foreach(w->projects, poll_project, &ctx); + for (int i = 0; i < sc.count; i++) { + poll_project(NULL, snap[i], &ctx); + } + free(snap); return ctx.reindexed; } From b07fdd3a7a3fe92177c1e5073b3e57df193a3e18 Mon Sep 17 00:00:00 2001 From: Matthew Prock <122550757+map588@users.noreply.github.com> Date: Sun, 5 Apr 2026 15:07:29 -0400 Subject: [PATCH 08/18] Update src/pipeline/pass_semantic.c Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/pipeline/pass_semantic.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pipeline/pass_semantic.c b/src/pipeline/pass_semantic.c index 30c6081f..423375c7 100644 --- a/src/pipeline/pass_semantic.c +++ b/src/pipeline/pass_semantic.c @@ -321,10 +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); - /* Ensure a CALLS edge exists so decorator appears in reference queries. - * Use "{}" to avoid clobbering richer metadata from pass_calls - * (dedup skips replacement when new props are "{}"). */ - cbm_gbuf_insert_edge(ctx->gbuf, node->id, dec->id, "CALLS", "{}"); + /* Ensure a reference edge exists so the decorator appears in usage queries + * without being misclassified as a real call by downstream passes. */ + cbm_gbuf_insert_edge(ctx->gbuf, node->id, dec->id, "USAGE", "{}"); (*count)++; } } From c48da5e206adec0025d6f61eceabeac5b2986f90 Mon Sep 17 00:00:00 2001 From: Matthew Prock <122550757+map588@users.noreply.github.com> Date: Sun, 5 Apr 2026 15:07:37 -0400 Subject: [PATCH 09/18] Update src/pipeline/pass_parallel.c Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/pipeline/pass_parallel.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 28125597..d2b1703f 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -1261,9 +1261,9 @@ static void resolve_def_decorators(resolve_ctx_t *rc, resolve_worker_state_t *ws char dp[CBM_SZ_256]; snprintf(dp, sizeof(dp), "{\"decorator\":\"%s\"}", def->decorators[dc]); cbm_gbuf_insert_edge(ws->local_edge_buf, node->id, dn->id, "DECORATES", dp); - /* Ensure a CALLS edge exists so decorator appears in reference queries. - * Use "{}" to avoid clobbering richer metadata from pass_calls. */ - cbm_gbuf_insert_edge(ws->local_edge_buf, node->id, dn->id, "CALLS", "{}"); + /* Ensure a reference-style edge exists so the decorator appears in queries + * without being misclassified as a real call by downstream passes. */ + cbm_gbuf_insert_edge(ws->local_edge_buf, node->id, dn->id, "USAGE", "{}"); ws->semantic_resolved++; } } From 348f890d3eed5a76b3aaab51b77957c8d13a1c48 Mon Sep 17 00:00:00 2001 From: map588 Date: Sun, 5 Apr 2026 16:03:53 -0400 Subject: [PATCH 10/18] refactor(memory): add safe_free, safe_str_free, safe_buf_free, safe_grow to platform.h Centralize heap memory management into four safe wrappers alongside existing safe_realloc: - safe_free(ptr): frees and NULLs any pointer to prevent double-free - safe_str_free(&str): frees const char* with NULL-out (replaces free((void*)str)) - safe_buf_free(buf, &count): frees array and zeros its count - safe_grow(arr, n, cap, factor): one-line capacity-doubling realloc Applied across cypher.c, store.c, mcp.c, and pass_githistory.c, eliminating ~60 lines of repetitive free/grow boilerplate. --- src/cypher/cypher.c | 285 ++++++++++----------- src/foundation/platform.h | 42 ++++ src/mcp/mcp.c | 5 +- src/pipeline/pass_githistory.c | 15 +- src/store/store.c | 441 ++++++++++++++------------------- 5 files changed, 364 insertions(+), 424 deletions(-) diff --git a/src/cypher/cypher.c b/src/cypher/cypher.c index 6aedeb92..a0d652a6 100644 --- a/src/cypher/cypher.c +++ b/src/cypher/cypher.c @@ -405,10 +405,10 @@ void cbm_lex_free(cbm_lex_result_t *r) { return; } for (int i = 0; i < r->count; i++) { - free((void *)r->tokens[i].text); + safe_str_free(&r->tokens[i].text); } - free(r->tokens); - free(r->error); + safe_free(r->tokens); + safe_free(r->error); memset(r, 0, sizeof(*r)); } @@ -473,23 +473,20 @@ static int parse_props(parser_t *p, cbm_prop_filter_t **out, int *count) { while (!check(p, TOK_RBRACE) && !check(p, TOK_EOF)) { const cbm_token_t *key = expect(p, TOK_IDENT); if (!key) { - free(arr); + safe_free(arr); return CBM_NOT_FOUND; } if (!expect(p, TOK_COLON)) { - free(arr); + safe_free(arr); return CBM_NOT_FOUND; } const cbm_token_t *val = expect(p, TOK_STRING); if (!val) { - free(arr); + safe_free(arr); return CBM_NOT_FOUND; } - if (n >= cap) { - cap *= PAIR_LEN; - arr = safe_realloc(arr, cap * sizeof(cbm_prop_filter_t)); - } + safe_grow(arr, n, cap, PAIR_LEN); arr[n].key = heap_strdup(key->text); arr[n].value = heap_strdup(val->text); n++; @@ -572,7 +569,7 @@ static int parse_rel_types(parser_t *p, cbm_rel_pattern_t *out) { const cbm_token_t *t = expect(p, TOK_IDENT); if (!t) { - free(types); + safe_free(types); return CBM_NOT_FOUND; } types[n++] = heap_strdup(t->text); @@ -581,15 +578,12 @@ static int parse_rel_types(parser_t *p, cbm_rel_pattern_t *out) { t = expect(p, TOK_IDENT); if (!t) { for (int i = 0; i < n; i++) { - free((void *)types[i]); + safe_str_free(&types[i]); } - free(types); + safe_free(types); return CBM_NOT_FOUND; } - if (n >= cap) { - cap *= PAIR_LEN; - types = safe_realloc(types, cap * sizeof(const char *)); - } + safe_grow(types, n, cap, PAIR_LEN); types[n++] = heap_strdup(t->text); } @@ -670,14 +664,14 @@ static void expr_free(cbm_expr_t *e) { while (top > 0) { cbm_expr_t *cur = stack[--top]; if (cur->type == EXPR_CONDITION) { - free((void *)cur->cond.variable); - free((void *)cur->cond.property); - free((void *)cur->cond.op); - free((void *)cur->cond.value); + safe_str_free(&cur->cond.variable); + safe_str_free(&cur->cond.property); + safe_str_free(&cur->cond.op); + safe_str_free(&cur->cond.value); for (int i = 0; i < cur->cond.in_value_count; i++) { - free((void *)cur->cond.in_values[i]); + safe_str_free(&cur->cond.in_values[i]); } - free(cur->cond.in_values); + safe_free(cur->cond.in_values); } if (cur->right && top < EXPR_FREE_STACK) { stack[top++] = cur->right; @@ -685,7 +679,7 @@ static void expr_free(cbm_expr_t *e) { if (cur->left && top < EXPR_FREE_STACK) { stack[top++] = cur->left; } - free(cur); + safe_free(cur); } } @@ -754,9 +748,9 @@ static cbm_expr_t *parse_in_list(parser_t *p, cbm_condition_t *c) { advance(p); c->op = heap_strdup("IN"); if (!expect(p, TOK_LBRACKET)) { - free((void *)c->variable); - free((void *)c->property); - free((void *)c->op); + safe_str_free(&c->variable); + safe_str_free(&c->property); + safe_str_free(&c->op); return NULL; } int vcap = CYP_INIT_CAP8; @@ -767,10 +761,7 @@ static cbm_expr_t *parse_in_list(parser_t *p, cbm_condition_t *c) { match(p, TOK_COMMA); } if (check(p, TOK_STRING) || check(p, TOK_NUMBER)) { - if (vn >= vcap) { - vcap *= PAIR_LEN; - vals = safe_realloc(vals, vcap * sizeof(const char *)); - } + safe_grow(vals, vn, vcap, PAIR_LEN); vals[vn++] = heap_strdup(advance(p)->text); } else { break; @@ -870,8 +861,8 @@ static cbm_expr_t *parse_condition_expr(parser_t *p) { c.op = parse_comparison_op(p); if (!c.op) { snprintf(p->error, sizeof(p->error), "unexpected operator at pos %d", peek(p)->pos); - free((void *)c.variable); - free((void *)c.property); + safe_str_free(&c.variable); + safe_str_free(&c.property); return NULL; } @@ -886,9 +877,9 @@ static cbm_expr_t *parse_condition_expr(parser_t *p) { c.value = heap_strdup("false"); } else { snprintf(p->error, sizeof(p->error), "expected value at pos %d", peek(p)->pos); - free((void *)c.variable); - free((void *)c.property); - free((void *)c.op); + safe_str_free(&c.variable); + safe_str_free(&c.property); + safe_str_free(&c.op); return NULL; } @@ -978,7 +969,7 @@ static int parse_where(parser_t *p, cbm_where_clause_t **out) { cbm_where_clause_t *w = calloc(CBM_ALLOC_ONE, sizeof(cbm_where_clause_t)); w->root = parse_or_expr(p); if (!w->root && p->error[0]) { - free(w); + safe_free(w); return CBM_NOT_FOUND; } @@ -1072,10 +1063,7 @@ static cbm_case_expr_t *parse_case_expr(parser_t *p) { break; } const char *then_val = parse_value_literal(p); - if (kase->branch_count >= bcap) { - bcap *= PAIR_LEN; - kase->branches = safe_realloc(kase->branches, bcap * sizeof(cbm_case_branch_t)); - } + safe_grow(kase->branches, kase->branch_count, bcap, PAIR_LEN); kase->branches[kase->branch_count++] = (cbm_case_branch_t){.when_expr = when, .then_val = then_val}; } @@ -1244,15 +1232,12 @@ static int parse_return_or_with(parser_t *p, cbm_return_clause_t **out, bool is_ cbm_return_item_t item = {0}; if (parse_return_item(p, &item) < 0) { - free(r->items); - free(r); + safe_free(r->items); + safe_free(r); return CBM_NOT_FOUND; } - if (r->count >= cap) { - cap *= PAIR_LEN; - r->items = safe_realloc(r->items, cap * sizeof(cbm_return_item_t)); - } + safe_grow(r->items, r->count, cap, PAIR_LEN); r->items[r->count++] = item; } while (check(p, TOK_COMMA)); @@ -1302,19 +1287,13 @@ static int parse_match_pattern(parser_t *p, cbm_pattern_t *pat) { pat->node_count = SKIP_ONE; while (check(p, TOK_DASH) || check(p, TOK_LT)) { - if (pat->rel_count >= rel_cap) { - rel_cap *= PAIR_LEN; - pat->rels = safe_realloc(pat->rels, rel_cap * sizeof(cbm_rel_pattern_t)); - } + safe_grow(pat->rels, pat->rel_count, rel_cap, PAIR_LEN); if (parse_rel(p, &pat->rels[pat->rel_count]) < 0) { return CBM_NOT_FOUND; } pat->rel_count++; - if (pat->node_count >= node_cap) { - node_cap *= PAIR_LEN; - pat->nodes = safe_realloc(pat->nodes, node_cap * sizeof(cbm_node_pattern_t)); - } + safe_grow(pat->nodes, pat->node_count, node_cap, PAIR_LEN); if (parse_node(p, &pat->nodes[pat->node_count]) < 0) { return CBM_NOT_FOUND; } @@ -1500,7 +1479,7 @@ void cbm_parse_free(cbm_parse_result_t *r) { return; } cbm_query_free(r->query); - free(r->error); + safe_free(r->error); memset(r, 0, sizeof(*r)); } @@ -1509,25 +1488,25 @@ void cbm_parse_free(cbm_parse_result_t *r) { static void free_pattern(cbm_pattern_t *pat) { for (int i = 0; i < pat->node_count; i++) { cbm_node_pattern_t *n = &pat->nodes[i]; - free((void *)n->variable); - free((void *)n->label); + safe_str_free(&n->variable); + safe_str_free(&n->label); for (int j = 0; j < n->prop_count; j++) { - free((void *)n->props[j].key); - free((void *)n->props[j].value); + safe_str_free(&n->props[j].key); + safe_str_free(&n->props[j].value); } - free(n->props); + safe_free(n->props); } - free(pat->nodes); + safe_free(pat->nodes); for (int i = 0; i < pat->rel_count; i++) { cbm_rel_pattern_t *r = &pat->rels[i]; - free((void *)r->variable); + safe_str_free(&r->variable); for (int j = 0; j < r->type_count; j++) { - free((void *)r->types[j]); + safe_str_free(&r->types[j]); } - free(r->types); - free((void *)r->direction); + safe_free(r->types); + safe_str_free(&r->direction); } - free(pat->rels); + safe_free(pat->rels); } static void free_where(cbm_where_clause_t *w) { @@ -1536,18 +1515,18 @@ static void free_where(cbm_where_clause_t *w) { } expr_free(w->root); for (int i = 0; i < w->count; i++) { - free((void *)w->conditions[i].variable); - free((void *)w->conditions[i].property); - free((void *)w->conditions[i].op); - free((void *)w->conditions[i].value); + safe_str_free(&w->conditions[i].variable); + safe_str_free(&w->conditions[i].property); + safe_str_free(&w->conditions[i].op); + safe_str_free(&w->conditions[i].value); for (int j = 0; j < w->conditions[i].in_value_count; j++) { - free((void *)w->conditions[i].in_values[j]); + safe_str_free(&w->conditions[i].in_values[j]); } - free(w->conditions[i].in_values); + safe_free(w->conditions[i].in_values); } - free(w->conditions); - free((void *)w->op); - free(w); + safe_free(w->conditions); + safe_str_free(&w->op); + safe_free(w); } static void free_case_expr(cbm_case_expr_t *k) { @@ -1556,11 +1535,11 @@ static void free_case_expr(cbm_case_expr_t *k) { } for (int i = 0; i < k->branch_count; i++) { expr_free(k->branches[i].when_expr); - free((void *)k->branches[i].then_val); + safe_str_free(&k->branches[i].then_val); } - free(k->branches); - free((void *)k->else_val); - free(k); + safe_free(k->branches); + safe_str_free(&k->else_val); + safe_free(k); } static void free_return_clause(cbm_return_clause_t *r) { @@ -1568,16 +1547,16 @@ static void free_return_clause(cbm_return_clause_t *r) { return; } for (int i = 0; i < r->count; i++) { - free((void *)r->items[i].variable); - free((void *)r->items[i].property); - free((void *)r->items[i].alias); - free((void *)r->items[i].func); + safe_str_free(&r->items[i].variable); + safe_str_free(&r->items[i].property); + safe_str_free(&r->items[i].alias); + safe_str_free(&r->items[i].func); free_case_expr(r->items[i].kase); } - free(r->items); - free((void *)r->order_by); - free((void *)r->order_dir); - free(r); + safe_free(r->items); + safe_str_free(&r->order_by); + safe_str_free(&r->order_dir); + safe_free(r); } void cbm_query_free(cbm_query_t *q) { @@ -1586,15 +1565,15 @@ void cbm_query_free(cbm_query_t *q) { for (int i = 0; i < q->pattern_count; i++) { free_pattern(&q->patterns[i]); } - free(q->patterns); - free(q->pattern_optional); + safe_free(q->patterns); + safe_free(q->pattern_optional); free_where(q->where); free_where(q->post_with_where); free_return_clause(q->with_clause); free_return_clause(q->ret); - free((void *)q->unwind_expr); - free((void *)q->unwind_alias); - free(q); + safe_str_free(&q->unwind_expr); + safe_str_free(&q->unwind_alias); + safe_free(q); q = next; } } @@ -1765,12 +1744,12 @@ static void node_fields_free(cbm_node_t *n) { if (!n) { return; } - free((void *)n->project); - free((void *)n->label); - free((void *)n->name); - free((void *)n->qualified_name); - free((void *)n->file_path); - free((void *)n->properties_json); + safe_str_free(&n->project); + safe_str_free(&n->label); + safe_str_free(&n->name); + safe_str_free(&n->qualified_name); + safe_str_free(&n->file_path); + safe_str_free(&n->properties_json); } /* Deep copy an edge (binding owns the strings) */ @@ -1782,9 +1761,9 @@ static void edge_deep_copy(cbm_edge_t *dst, const cbm_edge_t *src) { } static void edge_fields_free(cbm_edge_t *e) { - free((void *)e->project); - free((void *)e->type); - free((void *)e->properties_json); + safe_str_free(&e->project); + safe_str_free(&e->type); + safe_str_free(&e->properties_json); } /* Set an edge variable in a binding */ @@ -2027,10 +2006,7 @@ static void rb_set_columns(result_builder_t *rb, const char **cols, int count) { } static void rb_add_row(result_builder_t *rb, const char **values) { - if (rb->row_count >= rb->row_cap) { - rb->row_cap *= PAIR_LEN; - rb->rows = safe_realloc(rb->rows, rb->row_cap * sizeof(const char **)); - } + safe_grow(rb->rows, rb->row_count, rb->row_cap, PAIR_LEN); const char **row = malloc((rb->col_count > 0 ? (size_t)rb->col_count : SKIP_ONE) * sizeof(const char *)); for (int i = 0; i < rb->col_count; i++) { @@ -2324,7 +2300,7 @@ static void expand_pattern_rels(cbm_store_t *store, cbm_pattern_t *pat, binding_ for (int bi = 0; bi < *bind_count; bi++) { binding_free(&(*bindings)[bi]); } - free(*bindings); + safe_free(*bindings); *bindings = new_bindings; *bind_count = new_count; *var_name = to_var; @@ -2403,18 +2379,18 @@ static void rb_apply_skip_limit(result_builder_t *rb, int skip_n, int limit) { if (skip_n > 0 && skip_n < rb->row_count) { for (int i = 0; i < skip_n; i++) { for (int c = 0; c < rb->col_count; c++) { - free((void *)rb->rows[i][c]); + safe_str_free(&rb->rows[i][c]); } - free(rb->rows[i]); + safe_free(rb->rows[i]); } memmove(rb->rows, rb->rows + skip_n, (rb->row_count - skip_n) * sizeof(const char **)); rb->row_count -= skip_n; } else if (skip_n >= rb->row_count) { for (int i = 0; i < rb->row_count; i++) { for (int c = 0; c < rb->col_count; c++) { - free((void *)rb->rows[i][c]); + safe_str_free(&rb->rows[i][c]); } - free(rb->rows[i]); + safe_free(rb->rows[i]); } rb->row_count = 0; } @@ -2422,9 +2398,9 @@ static void rb_apply_skip_limit(result_builder_t *rb, int skip_n, int limit) { if (limit > 0 && rb->row_count > limit) { for (int i = limit; i < rb->row_count; i++) { for (int c = 0; c < rb->col_count; c++) { - free((void *)rb->rows[i][c]); + safe_str_free(&rb->rows[i][c]); } - free(rb->rows[i]); + safe_free(rb->rows[i]); } rb->row_count = limit; } @@ -2455,9 +2431,9 @@ static void rb_apply_distinct(result_builder_t *rb) { kept++; } else { for (int c = 0; c < rb->col_count; c++) { - free((void *)rb->rows[i][c]); + safe_str_free(&rb->rows[i][c]); } - free(rb->rows[i]); + safe_free(rb->rows[i]); } } rb->row_count = kept; @@ -2466,15 +2442,15 @@ static void rb_apply_distinct(result_builder_t *rb) { static void rb_free(result_builder_t *rb) { for (int i = 0; i < rb->row_count; i++) { for (int c = 0; c < rb->col_count; c++) { - free((void *)rb->rows[i][c]); + safe_str_free(&rb->rows[i][c]); } - free(rb->rows[i]); + safe_free(rb->rows[i]); } - free(rb->rows); + safe_free(rb->rows); for (int i = 0; i < rb->col_count; i++) { - free((void *)rb->columns[i]); + safe_str_free(&rb->columns[i]); } - free(rb->columns); + safe_free(rb->columns); } /* ── Get projection value for a binding + return item ─────────── */ @@ -2673,15 +2649,15 @@ static void with_add_vbinding_var(binding_t *vb, const char *alias, const char * static void with_agg_free(with_agg_t *aggs, int agg_cnt, int item_count) { for (int a = 0; a < agg_cnt; a++) { for (int ci = 0; ci < item_count; ci++) { - free((void *)aggs[a].group_vals[ci]); + safe_str_free(&aggs[a].group_vals[ci]); } - free(aggs[a].group_vals); - free(aggs[a].sums); - free(aggs[a].counts); - free(aggs[a].mins); - free(aggs[a].maxs); + safe_free(aggs[a].group_vals); + safe_free(aggs[a].sums); + safe_free(aggs[a].counts); + safe_free(aggs[a].mins); + safe_free(aggs[a].maxs); } - free(aggs); + safe_free(aggs); } /* Execute WITH aggregation path */ @@ -2785,7 +2761,7 @@ static void execute_with_clause(cbm_query_t *q, binding_t **bindings_ptr, int *b for (int bi = 0; bi < bind_count; bi++) { binding_free(&bindings[bi]); } - free(bindings); + safe_free(bindings); if (q->post_with_where) { filter_bindings_where(q->post_with_where, vbindings, &vcount); @@ -2833,7 +2809,7 @@ static void build_star_columns(result_builder_t *rb, const char **vars, int vc) } rb_set_columns(rb, col_names, col_n); for (int i = 0; i < col_n; i++) { - free((void *)col_names[i]); + safe_str_free(&col_names[i]); } } @@ -2970,21 +2946,21 @@ static void ret_agg_accumulate(ret_agg_entry_t *entry, cbm_return_clause_t *ret, static void ret_agg_free(ret_agg_entry_t *aggs, int agg_count, int item_count) { for (int a = 0; a < agg_count; a++) { for (int ci = 0; ci < item_count; ci++) { - free((void *)aggs[a].group_vals[ci]); + safe_str_free(&aggs[a].group_vals[ci]); for (int j = 0; j < aggs[a].collect_counts[ci]; j++) { - free(aggs[a].collect_lists[ci][j]); + safe_free(aggs[a].collect_lists[ci][j]); } - free(aggs[a].collect_lists[ci]); + safe_free(aggs[a].collect_lists[ci]); } - free(aggs[a].group_vals); - free(aggs[a].sums); - free(aggs[a].counts); - free(aggs[a].mins); - free(aggs[a].maxs); - free(aggs[a].collect_lists); - free(aggs[a].collect_counts); + safe_free(aggs[a].group_vals); + safe_free(aggs[a].sums); + safe_free(aggs[a].counts); + safe_free(aggs[a].mins); + safe_free(aggs[a].maxs); + safe_free(aggs[a].collect_lists); + safe_free(aggs[a].collect_counts); } - free(aggs); + safe_free(aggs); } /* Execute RETURN with aggregation */ @@ -3042,10 +3018,7 @@ static void execute_return_agg(cbm_return_clause_t *ret, binding_t *bindings, in } } if (found < 0) { - if (agg_count >= agg_cap) { - agg_cap *= PAIR_LEN; - aggs = safe_realloc(aggs, agg_cap * sizeof(ret_agg_entry_t)); - } + safe_grow(aggs, agg_count, agg_cap, PAIR_LEN); found = agg_count++; ret_agg_init_group(&aggs[found], key, ret->count, vals); } @@ -3083,7 +3056,7 @@ static void build_return_columns(result_builder_t *rb, cbm_return_clause_t *ret) for (int i = 0; i < ret->count && i < CBM_SZ_32; i++) { cbm_return_item_t *item = &ret->items[i]; if (!item->alias && (item->func || (!item->kase && item->property))) { - free((void *)col_names[i]); + safe_str_free(&col_names[i]); } } } @@ -3121,7 +3094,7 @@ static void build_default_columns(result_builder_t *rb, const char **vars, int v } rb_set_columns(rb, col_names, col_n); for (int i = 0; i < col_n; i++) { - free((void *)col_names[i]); + safe_str_free(&col_names[i]); } } @@ -3170,7 +3143,7 @@ static void cross_join_nodes(binding_t **bindings, int *bind_count, cbm_node_t * for (int bi = 0; bi < *bind_count; bi++) { binding_free(&(*bindings)[bi]); } - free(*bindings); + safe_free(*bindings); *bindings = new_bindings; *bind_count = new_count; } @@ -3196,7 +3169,7 @@ static void cross_join_with_rels(cbm_store_t *store, cbm_pattern_t *patn, bindin for (int ti = 0; ti < tc; ti++) { new_bindings[new_count++] = tmp[ti]; } - free(tmp); + safe_free(tmp); } if (opt && extra_count == 0) { binding_t nb = {0}; @@ -3207,7 +3180,7 @@ static void cross_join_with_rels(cbm_store_t *store, cbm_pattern_t *patn, bindin for (int bi = 0; bi < *bind_count; bi++) { binding_free(&(*bindings)[bi]); } - free(*bindings); + safe_free(*bindings); *bindings = new_bindings; *bind_count = new_count; } @@ -3322,7 +3295,7 @@ static int execute_single(cbm_store_t *store, cbm_query_t *q, const char *projec for (int bi = 0; bi < bind_count; bi++) { binding_free(&bindings[bi]); } - free(bindings); + safe_free(bindings); cbm_store_free_nodes(scanned, scan_count); return 0; } @@ -3397,16 +3370,16 @@ void cbm_cypher_result_free(cbm_cypher_result_t *r) { return; } for (int i = 0; i < r->col_count; i++) { - free((void *)r->columns[i]); + safe_str_free(&r->columns[i]); } - free(r->columns); + safe_free(r->columns); for (int i = 0; i < r->row_count; i++) { for (int j = 0; j < r->col_count; j++) { - free((void *)r->rows[i][j]); + safe_str_free(&r->rows[i][j]); } - free(r->rows[i]); + safe_free(r->rows[i]); } - free(r->rows); - free(r->error); + safe_free(r->rows); + safe_free(r->error); memset(r, 0, sizeof(*r)); } diff --git a/src/foundation/platform.h b/src/foundation/platform.h index 5624810c..f0665d5c 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/mcp/mcp.c b/src/mcp/mcp.c index c4073e59..fb89251a 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -2391,10 +2391,7 @@ static grep_match_t *collect_grep_matches(FILE *fp, const char *root_path, size_ continue; } - if (gm_count >= gm_cap) { - gm_cap *= PAIR_LEN; - gm = safe_realloc(gm, gm_cap * sizeof(grep_match_t)); - } + safe_grow(gm, gm_count, gm_cap, PAIR_LEN); snprintf(gm[gm_count].file, sizeof(gm[0].file), "%s", file); gm[gm_count].line = (int)strtol(colon1 + SKIP_ONE, NULL, CBM_DECIMAL_BASE); snprintf(gm[gm_count].content, sizeof(gm[0].content), "%s", colon2 + SKIP_ONE); diff --git a/src/pipeline/pass_githistory.c b/src/pipeline/pass_githistory.c index 36b491f5..6be5138a 100644 --- a/src/pipeline/pass_githistory.c +++ b/src/pipeline/pass_githistory.c @@ -185,10 +185,7 @@ static int parse_git_log(const char *repo_path, commit_t **out, int *out_count) } if (current.count > 0) { - if (count >= cap) { - cap *= PAIR_LEN; - commits = safe_realloc(commits, cap * sizeof(commit_t)); - } + safe_grow(commits, count, cap, PAIR_LEN); commits[count++] = current; } else { commit_free(¤t); @@ -250,10 +247,7 @@ static int parse_git_log(const char *repo_path, commit_t **out, int *out_count) if (strncmp(line, "COMMIT:", SLEN("COMMIT:")) == 0) { if (current.count > 0) { - if (count >= cap) { - cap *= PAIR_LEN; - commits = safe_realloc(commits, cap * sizeof(commit_t)); - } + safe_grow(commits, count, cap, PAIR_LEN); commits[count++] = current; memset(¤t, 0, sizeof(current)); } @@ -265,10 +259,7 @@ static int parse_git_log(const char *repo_path, commit_t **out, int *out_count) } } if (current.count > 0) { - if (count >= cap) { - cap *= PAIR_LEN; - commits = safe_realloc(commits, cap * sizeof(commit_t)); - } + safe_grow(commits, count, cap, PAIR_LEN); commits[count++] = current; } else { commit_free(¤t); diff --git a/src/store/store.c b/src/store/store.c index 4920732e..061c4134 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -373,7 +373,7 @@ static cbm_store_t *store_open_internal(const char *path, bool in_memory) { int rc = sqlite3_open_v2(path, &s->db, flags, NULL); if (rc != SQLITE_OK) { - free(s); + safe_free(s); return NULL; } @@ -395,8 +395,8 @@ static cbm_store_t *store_open_internal(const char *path, bool in_memory) { if (configure_pragmas(s, in_memory) != CBM_STORE_OK || init_schema(s) != CBM_STORE_OK || create_user_indexes(s) != CBM_STORE_OK) { sqlite3_close(s->db); - free((void *)s->db_path); - free(s); + safe_str_free(&s->db_path); + safe_free(s); return NULL; } @@ -429,7 +429,7 @@ cbm_store_t *cbm_store_open_path_query(const char *db_path) { if (rc != SQLITE_OK) { /* sqlite3_open_v2 allocates a handle even on failure — must close it. */ sqlite3_close(s->db); - free(s); + safe_free(s); return NULL; } @@ -446,8 +446,8 @@ cbm_store_t *cbm_store_open_path_query(const char *db_path) { if (configure_pragmas(s, false) != CBM_STORE_OK) { sqlite3_close(s->db); - free((void *)s->db_path); - free(s); + safe_str_free(&s->db_path); + safe_free(s); return NULL; } @@ -767,10 +767,7 @@ int cbm_store_list_projects(cbm_store_t *s, cbm_project_t **out, int *count) { cbm_project_t *arr = malloc(cap * sizeof(cbm_project_t)); while (sqlite3_step(stmt) == SQLITE_ROW) { - if (n >= cap) { - cap *= ST_GROWTH; - arr = safe_realloc(arr, cap * sizeof(cbm_project_t)); - } + safe_grow(arr, n, cap, ST_GROWTH); arr[n].name = heap_strdup((const char *)sqlite3_column_text(stmt, 0)); arr[n].indexed_at = heap_strdup((const char *)sqlite3_column_text(stmt, SKIP_ONE)); arr[n].root_path = heap_strdup((const char *)sqlite3_column_text(stmt, CBM_SZ_2)); @@ -934,10 +931,7 @@ int cbm_store_find_nodes_by_name_any(cbm_store_t *s, const char *name, cbm_node_ int n = 0; cbm_node_t *arr = malloc(cap * sizeof(cbm_node_t)); while (sqlite3_step(stmt) == SQLITE_ROW) { - if (n >= cap) { - cap *= ST_GROWTH; - arr = safe_realloc(arr, cap * sizeof(cbm_node_t)); - } + safe_grow(arr, n, cap, ST_GROWTH); scan_node(stmt, &arr[n]); n++; } @@ -993,10 +987,7 @@ static int find_nodes_generic(cbm_store_t *s, sqlite3_stmt **slot, const char *s cbm_node_t *arr = malloc(cap * sizeof(cbm_node_t)); while (sqlite3_step(stmt) == SQLITE_ROW) { - if (n >= cap) { - cap *= ST_GROWTH; - arr = safe_realloc(arr, cap * sizeof(cbm_node_t)); - } + safe_grow(arr, n, cap, ST_GROWTH); scan_node(stmt, &arr[n]); n++; } @@ -1182,10 +1173,7 @@ static int find_edges_generic(cbm_store_t *s, sqlite3_stmt **slot, const char *s cbm_edge_t *arr = malloc(cap * sizeof(cbm_edge_t)); while (sqlite3_step(stmt) == SQLITE_ROW) { - if (n >= cap) { - cap *= ST_GROWTH; - arr = safe_realloc(arr, cap * sizeof(cbm_edge_t)); - } + safe_grow(arr, n, cap, ST_GROWTH); scan_edge(stmt, &arr[n]); n++; } @@ -1396,10 +1384,7 @@ int cbm_store_get_file_hashes(cbm_store_t *s, const char *project, cbm_file_hash cbm_file_hash_t *arr = malloc(cap * sizeof(cbm_file_hash_t)); while (sqlite3_step(stmt) == SQLITE_ROW) { - if (n >= cap) { - cap *= ST_GROWTH; - arr = safe_realloc(arr, cap * sizeof(cbm_file_hash_t)); - } + safe_grow(arr, n, cap, ST_GROWTH); arr[n].project = heap_strdup((const char *)sqlite3_column_text(stmt, 0)); arr[n].rel_path = heap_strdup((const char *)sqlite3_column_text(stmt, SKIP_ONE)); arr[n].sha256 = heap_strdup((const char *)sqlite3_column_text(stmt, CBM_SZ_2)); @@ -1475,10 +1460,7 @@ int cbm_store_find_nodes_by_file_overlap(cbm_store_t *s, const char *project, co int n = 0; cbm_node_t *nodes = malloc(cap * sizeof(cbm_node_t)); while (sqlite3_step(stmt) == SQLITE_ROW) { - if (n >= cap) { - cap *= ST_GROWTH; - nodes = safe_realloc(nodes, cap * sizeof(cbm_node_t)); - } + safe_grow(nodes, n, cap, ST_GROWTH); memset(&nodes[n], 0, sizeof(cbm_node_t)); scan_node(stmt, &nodes[n]); n++; @@ -1531,10 +1513,7 @@ int cbm_store_find_nodes_by_qn_suffix(cbm_store_t *s, const char *project, const int n = 0; cbm_node_t *nodes = malloc(cap * sizeof(cbm_node_t)); while (sqlite3_step(stmt) == SQLITE_ROW) { - if (n >= cap) { - cap *= ST_GROWTH; - nodes = safe_realloc(nodes, cap * sizeof(cbm_node_t)); - } + safe_grow(nodes, n, cap, ST_GROWTH); memset(&nodes[n], 0, sizeof(cbm_node_t)); scan_node(stmt, &nodes[n]); n++; @@ -1596,10 +1575,7 @@ int cbm_store_list_files(cbm_store_t *s, const char *project, char ***out, int * if (!fp) { continue; } - if (n >= cap) { - cap *= ST_GROWTH; - files = safe_realloc(files, cap * sizeof(char *)); - } + safe_grow(files, n, cap, ST_GROWTH); files[n++] = heap_strdup(fp); } sqlite3_finalize(stmt); @@ -1629,10 +1605,7 @@ static int query_neighbor_names(sqlite3 *db, const char *sql, int64_t node_id, i if (!name) { continue; } - if (count >= cap) { - cap *= ST_GROWTH; - names = safe_realloc(names, (size_t)cap * sizeof(char *)); - } + safe_grow(names, count, cap, ST_GROWTH); names[count++] = strdup(name); } sqlite3_finalize(stmt); @@ -1795,10 +1768,7 @@ int cbm_store_find_edges_by_url_path(cbm_store_t *s, const char *project, const int n = 0; cbm_edge_t *edges = malloc(cap * sizeof(cbm_edge_t)); while (sqlite3_step(stmt) == SQLITE_ROW) { - if (n >= cap) { - cap *= ST_GROWTH; - edges = safe_realloc(edges, cap * sizeof(cbm_edge_t)); - } + safe_grow(edges, n, cap, ST_GROWTH); memset(&edges[n], 0, sizeof(cbm_edge_t)); scan_edge(stmt, &edges[n]); n++; @@ -2175,7 +2145,7 @@ int cbm_store_search(cbm_store_t *s, const cbm_search_params_t *params, cbm_sear rc = sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &main_stmt, NULL); if (rc != SQLITE_OK) { store_set_error_sqlite(s, "search prepare"); - free(like_pattern); + safe_free(like_pattern); return CBM_STORE_ERR; } @@ -2188,10 +2158,7 @@ int cbm_store_search(cbm_store_t *s, const cbm_search_params_t *params, cbm_sear cbm_search_result_t *results = malloc(cap * sizeof(cbm_search_result_t)); while (sqlite3_step(main_stmt) == SQLITE_ROW) { - if (n >= cap) { - cap *= ST_GROWTH; - results = safe_realloc(results, cap * sizeof(cbm_search_result_t)); - } + safe_grow(results, n, cap, ST_GROWTH); memset(&results[n], 0, sizeof(cbm_search_result_t)); scan_node(main_stmt, &results[n].node); results[n].in_degree = sqlite3_column_int(main_stmt, ST_COL_9); @@ -2200,7 +2167,7 @@ int cbm_store_search(cbm_store_t *s, const cbm_search_params_t *params, cbm_sear } sqlite3_finalize(main_stmt); - free(like_pattern); + safe_free(like_pattern); out->results = results; out->count = n; @@ -2213,18 +2180,18 @@ void cbm_store_search_free(cbm_search_output_t *out) { } for (int i = 0; i < out->count; i++) { cbm_search_result_t *r = &out->results[i]; - free((void *)r->node.project); - free((void *)r->node.label); - free((void *)r->node.name); - free((void *)r->node.qualified_name); - free((void *)r->node.file_path); - free((void *)r->node.properties_json); + safe_str_free(&r->node.project); + safe_str_free(&r->node.label); + safe_str_free(&r->node.name); + safe_str_free(&r->node.qualified_name); + safe_str_free(&r->node.file_path); + safe_str_free(&r->node.properties_json); for (int j = 0; j < r->connected_count; j++) { - free((void *)r->connected_names[j]); + safe_str_free(&r->connected_names[j]); } - free(r->connected_names); + safe_free(r->connected_names); } - free(out->results); + safe_free(out->results); memset(out, 0, sizeof(*out)); } @@ -2279,10 +2246,7 @@ static int bfs_collect_edges(cbm_store_t *s, int64_t start_id, const cbm_node_ho cbm_edge_info_t *edges = malloc(ecap * sizeof(cbm_edge_info_t)); while (sqlite3_step(estmt) == SQLITE_ROW) { - if (en >= ecap) { - ecap *= ST_GROWTH; - edges = safe_realloc(edges, ecap * sizeof(cbm_edge_info_t)); - } + safe_grow(edges, en, ecap, ST_GROWTH); edges[en].from_name = heap_strdup((const char *)sqlite3_column_text(estmt, 0)); edges[en].to_name = heap_strdup((const char *)sqlite3_column_text(estmt, SKIP_ONE)); edges[en].type = heap_strdup((const char *)sqlite3_column_text(estmt, CBM_SZ_2)); @@ -2384,10 +2348,7 @@ int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const cbm_node_hop_t *visited = malloc(cap * sizeof(cbm_node_hop_t)); while (sqlite3_step(stmt) == SQLITE_ROW) { - if (n >= cap) { - cap *= ST_GROWTH; - visited = safe_realloc(visited, cap * sizeof(cbm_node_hop_t)); - } + safe_grow(visited, n, cap, ST_GROWTH); scan_node(stmt, &visited[n].node); visited[n].hop = sqlite3_column_int(stmt, ST_COL_9); n++; @@ -2415,32 +2376,32 @@ void cbm_store_traverse_free(cbm_traverse_result_t *out) { return; } /* Free root */ - free((void *)out->root.project); - free((void *)out->root.label); - free((void *)out->root.name); - free((void *)out->root.qualified_name); - free((void *)out->root.file_path); - free((void *)out->root.properties_json); + safe_str_free(&out->root.project); + safe_str_free(&out->root.label); + safe_str_free(&out->root.name); + safe_str_free(&out->root.qualified_name); + safe_str_free(&out->root.file_path); + safe_str_free(&out->root.properties_json); /* Free visited */ for (int i = 0; i < out->visited_count; i++) { cbm_node_hop_t *h = &out->visited[i]; - free((void *)h->node.project); - free((void *)h->node.label); - free((void *)h->node.name); - free((void *)h->node.qualified_name); - free((void *)h->node.file_path); - free((void *)h->node.properties_json); + safe_str_free(&h->node.project); + safe_str_free(&h->node.label); + safe_str_free(&h->node.name); + safe_str_free(&h->node.qualified_name); + safe_str_free(&h->node.file_path); + safe_str_free(&h->node.properties_json); } - free(out->visited); + safe_free(out->visited); /* Free edges */ for (int i = 0; i < out->edge_count; i++) { - free((void *)out->edges[i].from_name); - free((void *)out->edges[i].to_name); - free((void *)out->edges[i].type); + safe_str_free(&out->edges[i].from_name); + safe_str_free(&out->edges[i].to_name); + safe_str_free(&out->edges[i].type); } - free(out->edges); + safe_free(out->edges); memset(out, 0, sizeof(*out)); } @@ -2559,10 +2520,7 @@ int cbm_store_get_schema(cbm_store_t *s, const char *project, cbm_schema_info_t int n = 0; cbm_label_count_t *arr = malloc(cap * sizeof(cbm_label_count_t)); while (sqlite3_step(stmt) == SQLITE_ROW) { - if (n >= cap) { - cap *= ST_GROWTH; - arr = safe_realloc(arr, cap * sizeof(cbm_label_count_t)); - } + safe_grow(arr, n, cap, ST_GROWTH); arr[n].label = heap_strdup((const char *)sqlite3_column_text(stmt, 0)); arr[n].count = sqlite3_column_int(stmt, SKIP_ONE); n++; @@ -2584,10 +2542,7 @@ int cbm_store_get_schema(cbm_store_t *s, const char *project, cbm_schema_info_t int n = 0; cbm_type_count_t *arr = malloc(cap * sizeof(cbm_type_count_t)); while (sqlite3_step(stmt) == SQLITE_ROW) { - if (n >= cap) { - cap *= ST_GROWTH; - arr = safe_realloc(arr, cap * sizeof(cbm_type_count_t)); - } + safe_grow(arr, n, cap, ST_GROWTH); arr[n].type = heap_strdup((const char *)sqlite3_column_text(stmt, 0)); arr[n].count = sqlite3_column_int(stmt, SKIP_ONE); n++; @@ -2605,34 +2560,34 @@ void cbm_store_schema_free(cbm_schema_info_t *out) { return; } for (int i = 0; i < out->node_label_count; i++) { - free((void *)out->node_labels[i].label); + safe_str_free(&out->node_labels[i].label); } - free(out->node_labels); + safe_free(out->node_labels); for (int i = 0; i < out->edge_type_count; i++) { - free((void *)out->edge_types[i].type); + safe_str_free(&out->edge_types[i].type); } - free(out->edge_types); + safe_free(out->edge_types); for (int i = 0; i < out->rel_pattern_count; i++) { - free((void *)out->rel_patterns[i]); + safe_str_free(&out->rel_patterns[i]); } - free(out->rel_patterns); + safe_free(out->rel_patterns); for (int i = 0; i < out->sample_func_count; i++) { - free((void *)out->sample_func_names[i]); + safe_str_free(&out->sample_func_names[i]); } - free(out->sample_func_names); + safe_free(out->sample_func_names); for (int i = 0; i < out->sample_class_count; i++) { - free((void *)out->sample_class_names[i]); + safe_str_free(&out->sample_class_names[i]); } - free(out->sample_class_names); + safe_free(out->sample_class_names); for (int i = 0; i < out->sample_qn_count; i++) { - free((void *)out->sample_qns[i]); + safe_str_free(&out->sample_qns[i]); } - free(out->sample_qns); + safe_free(out->sample_qns); memset(out, 0, sizeof(*out)); } @@ -2843,10 +2798,7 @@ static int arch_entry_points(cbm_store_t *s, const char *project, cbm_architectu int n = 0; cbm_entry_point_t *arr = calloc(cap, sizeof(cbm_entry_point_t)); while (sqlite3_step(stmt) == SQLITE_ROW) { - if (n >= cap) { - cap *= ST_GROWTH; - arr = safe_realloc(arr, cap * sizeof(cbm_entry_point_t)); - } + safe_grow(arr, n, cap, ST_GROWTH); arr[n].name = heap_strdup((const char *)sqlite3_column_text(stmt, 0)); arr[n].qualified_name = heap_strdup((const char *)sqlite3_column_text(stmt, SKIP_ONE)); arr[n].file = heap_strdup((const char *)sqlite3_column_text(stmt, CBM_SZ_2)); @@ -2905,10 +2857,7 @@ static int arch_routes(cbm_store_t *s, const char *project, cbm_architecture_inf if (cbm_is_test_file_path(fp)) { continue; } - if (n >= cap) { - cap *= ST_GROWTH; - arr = safe_realloc(arr, cap * sizeof(cbm_route_info_t)); - } + safe_grow(arr, n, cap, ST_GROWTH); arr[n].method = heap_strdup(""); arr[n].path = heap_strdup(name); @@ -2917,17 +2866,17 @@ static int arch_routes(cbm_store_t *s, const char *project, cbm_architecture_inf char *val; val = extract_json_string_prop(props, "\"method\"", ST_METHOD_PROP_LEN); if (val) { - free((void *)arr[n].method); + safe_str_free(&arr[n].method); arr[n].method = val; } val = extract_json_string_prop(props, "\"path\"", ST_PATH_PROP_LEN); if (val) { - free((void *)arr[n].path); + safe_str_free(&arr[n].path); arr[n].path = val; } val = extract_json_string_prop(props, "\"handler\"", ST_HANDLER_PROP_LEN); if (val) { - free((void *)arr[n].handler); + safe_str_free(&arr[n].handler); arr[n].handler = val; } n++; @@ -2957,10 +2906,7 @@ static int arch_hotspots(cbm_store_t *s, const char *project, cbm_architecture_i int n = 0; cbm_hotspot_t *arr = calloc(cap, sizeof(cbm_hotspot_t)); while (sqlite3_step(stmt) == SQLITE_ROW) { - if (n >= cap) { - cap *= ST_GROWTH; - arr = safe_realloc(arr, cap * sizeof(cbm_hotspot_t)); - } + safe_grow(arr, n, cap, ST_GROWTH); arr[n].name = heap_strdup((const char *)sqlite3_column_text(stmt, 0)); arr[n].qualified_name = heap_strdup((const char *)sqlite3_column_text(stmt, SKIP_ONE)); arr[n].fan_in = sqlite3_column_int(stmt, CBM_SZ_2); @@ -3020,10 +2966,10 @@ static int arch_boundaries(cbm_store_t *s, const char *project, cbm_cross_pkg_bo char **npkgs = malloc(ncap * sizeof(char *)); while (sqlite3_step(nstmt) == SQLITE_ROW) { - if (nn >= ncap) { + if ((size_t)nn >= (size_t)ncap) { ncap *= ST_GROWTH; - nids = safe_realloc(nids, ncap * sizeof(int64_t)); - npkgs = safe_realloc(npkgs, ncap * sizeof(char *)); + nids = safe_realloc(nids, (size_t)ncap * sizeof(int64_t)); + npkgs = safe_realloc(npkgs, (size_t)ncap * sizeof(char *)); } nids[nn] = sqlite3_column_int64(nstmt, 0); const char *qn = (const char *)sqlite3_column_text(nstmt, SKIP_ONE); @@ -3037,10 +2983,10 @@ static int arch_boundaries(cbm_store_t *s, const char *project, cbm_cross_pkg_bo sqlite3_stmt *estmt = NULL; if (sqlite3_prepare_v2(s->db, esql, CBM_NOT_FOUND, &estmt, NULL) != SQLITE_OK) { for (int i = 0; i < nn; i++) { - free(npkgs[i]); + safe_free(npkgs[i]); } - free(nids); - free(npkgs); + safe_free(nids); + safe_free(npkgs); store_set_error_sqlite(s, "arch_boundaries_edges"); return CBM_STORE_ERR; } @@ -3064,10 +3010,10 @@ static int arch_boundaries(cbm_store_t *s, const char *project, cbm_cross_pkg_bo } sqlite3_finalize(estmt); for (int i = 0; i < nn; i++) { - free(npkgs[i]); + safe_free(npkgs[i]); } - free(nids); - free(npkgs); + safe_free(nids); + safe_free(npkgs); /* Sort by count descending */ for (int i = SKIP_ONE; i < bn; i++) { @@ -3087,8 +3033,8 @@ static int arch_boundaries(cbm_store_t *s, const char *project, cbm_cross_pkg_bo } if (bn > CBM_DECIMAL_BASE) { for (int i = ST_MAX_ITERATIONS; i < bn; i++) { - free(bfroms[i]); - free(btos[i]); + safe_free(bfroms[i]); + safe_free(btos[i]); } bn = ST_MAX_ITERATIONS; } @@ -3100,9 +3046,9 @@ static int arch_boundaries(cbm_store_t *s, const char *project, cbm_cross_pkg_bo result[i].to = btos[i]; result[i].call_count = bcounts[i]; } - free(bfroms); - free(btos); - free(bcounts); + safe_free(bfroms); + safe_free(btos); + safe_free(bcounts); *out_arr = result; *out_count = bn; return CBM_STORE_OK; @@ -3163,7 +3109,7 @@ static int arch_packages_from_qn(cbm_store_t *s, const char *project, } if (np > MAX_PREVIEW_NAMES) { for (int i = MAX_PREVIEW_NAMES; i < np; i++) { - free(pnames[i]); + safe_free(pnames[i]); } np = MAX_PREVIEW_NAMES; } @@ -3194,10 +3140,7 @@ static int arch_packages(cbm_store_t *s, const char *project, cbm_architecture_i int n = 0; cbm_package_summary_t *arr = calloc(cap, sizeof(cbm_package_summary_t)); while (sqlite3_step(stmt) == SQLITE_ROW) { - if (n >= cap) { - cap *= ST_GROWTH; - arr = safe_realloc(arr, cap * sizeof(cbm_package_summary_t)); - } + safe_grow(arr, n, cap, ST_GROWTH); arr[n].name = heap_strdup((const char *)sqlite3_column_text(stmt, 0)); arr[n].node_count = sqlite3_column_int(stmt, SKIP_ONE); n++; @@ -3206,7 +3149,7 @@ static int arch_packages(cbm_store_t *s, const char *project, cbm_architecture_i /* Fallback: group by QN segment if no Package nodes */ if (n == 0) { - free(arr); + safe_free(arr); int rc = arch_packages_from_qn(s, project, &arr, &n); if (rc != CBM_STORE_OK) { return rc; @@ -3369,15 +3312,15 @@ static int arch_layers(cbm_store_t *s, const char *project, cbm_architecture_inf /* Cleanup */ for (int i = 0; i < bcount; i++) { - free((void *)boundaries[i].from); - free((void *)boundaries[i].to); + safe_str_free(&boundaries[i].from); + safe_str_free(&boundaries[i].to); } - free(boundaries); + safe_free(boundaries); for (int i = 0; i < nrpkgs; i++) { - free(route_pkgs[i]); + safe_free(route_pkgs[i]); } for (int i = 0; i < nepkgs; i++) { - free(entry_pkgs[i]); + safe_free(entry_pkgs[i]); } return CBM_STORE_OK; @@ -3564,20 +3507,20 @@ static void arch_collect_entries(char **dir_paths, int *dir_child_counts, char * static void arch_free_dirs(char **dir_paths, int *dir_child_counts, char ***dir_children, int *dir_children_caps, int dn, char **files, int fn) { for (int i = 0; i < dn; i++) { - free(dir_paths[i]); + safe_free(dir_paths[i]); for (int k = 0; k < dir_child_counts[i]; k++) { - free(dir_children[i][k]); + safe_free(dir_children[i][k]); } - free(dir_children[i]); + safe_free(dir_children[i]); } - free(dir_paths); - free(dir_child_counts); - free(dir_children); - free(dir_children_caps); + safe_free(dir_paths); + safe_free(dir_child_counts); + safe_free(dir_children); + safe_free(dir_children_caps); for (int i = 0; i < fn; i++) { - free(files[i]); + safe_free(files[i]); } - free(files); + safe_free(files); } static int arch_file_tree(cbm_store_t *s, const char *project, cbm_architecture_info_t *out) { @@ -3605,10 +3548,7 @@ static int arch_file_tree(cbm_store_t *s, const char *project, cbm_architecture_ if (!fp) { continue; } - if (fn >= fcap) { - fcap *= ST_GROWTH; - files = safe_realloc(files, fcap * sizeof(char *)); - } + safe_grow(files, fn, fcap, ST_GROWTH); files[fn++] = heap_strdup(fp); arch_register_file_dirs(fp, dir_paths, dir_child_counts, dir_children, dir_children_caps, &dn, dcap); @@ -3782,23 +3722,23 @@ static bool louvain_iteration(int n, int **adj, double **adj_w, const int *adj_n improved = true; } - free(nc_weight); - free(nc_seen); + safe_free(nc_weight); + safe_free(nc_seen); } - free(order); - free(comm_degree); + safe_free(order); + safe_free(comm_degree); return improved; } static void louvain_free_adj(int **adj, double **adj_w, int *adj_n, int *adj_cap, int n) { for (int i = 0; i < n; i++) { - free(adj[i]); - free(adj_w[i]); + safe_free(adj[i]); + safe_free(adj_w[i]); } - free(adj); - free(adj_w); - free(adj_n); - free(adj_cap); + safe_free(adj); + safe_free(adj_w); + safe_free(adj_n); + safe_free(adj_cap); } int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *edges, @@ -3824,13 +3764,13 @@ int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t * int *adj_n = calloc(n, sizeof(int)); int *adj_cap = calloc(n, sizeof(int)); if (!adj || !adj_w || !adj_n || !adj_cap) { - free(adj); - free(adj_w); - free(adj_n); - free(adj_cap); - free(wsi); - free(wdi); - free(ww); + safe_free(adj); + safe_free(adj_w); + safe_free(adj_n); + safe_free(adj_cap); + safe_free(wsi); + safe_free(wdi); + safe_free(ww); return CBM_NOT_FOUND; } @@ -3839,9 +3779,9 @@ int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t * total_weight += ww[i]; adj_add_edge(adj, adj_w, adj_n, adj_cap, wsi[i], wdi[i], ww[i]); } - free(wsi); - free(wdi); - free(ww); + safe_free(wsi); + safe_free(wdi); + safe_free(ww); /* Initialize communities */ int *community = malloc(n * sizeof(int)); @@ -3857,7 +3797,7 @@ int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t * } *out = result; *out_count = n; - free(community); + safe_free(community); louvain_free_adj(adj, adj_w, adj_n, adj_cap, n); return CBM_STORE_OK; } @@ -3865,7 +3805,7 @@ int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t * /* Compute node degrees */ double *degree = calloc(n, sizeof(double)); if (!degree) { - free(community); + safe_free(community); louvain_free_adj(adj, adj_w, adj_n, adj_cap, n); return CBM_NOT_FOUND; } @@ -3893,8 +3833,8 @@ int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t * *out = result; *out_count = n; - free(community); - free(degree); + safe_free(community); + safe_free(degree); louvain_free_adj(adj, adj_w, adj_n, adj_cap, n); return CBM_STORE_OK; } @@ -3982,68 +3922,68 @@ void cbm_store_architecture_free(cbm_architecture_info_t *out) { return; } for (int i = 0; i < out->language_count; i++) { - free((void *)out->languages[i].language); + safe_str_free(&out->languages[i].language); } - free(out->languages); + safe_free(out->languages); for (int i = 0; i < out->package_count; i++) { - free((void *)out->packages[i].name); + safe_str_free(&out->packages[i].name); } - free(out->packages); + safe_free(out->packages); for (int i = 0; i < out->entry_point_count; i++) { - free((void *)out->entry_points[i].name); - free((void *)out->entry_points[i].qualified_name); - free((void *)out->entry_points[i].file); + safe_str_free(&out->entry_points[i].name); + safe_str_free(&out->entry_points[i].qualified_name); + safe_str_free(&out->entry_points[i].file); } - free(out->entry_points); + safe_free(out->entry_points); for (int i = 0; i < out->route_count; i++) { - free((void *)out->routes[i].method); - free((void *)out->routes[i].path); - free((void *)out->routes[i].handler); + safe_str_free(&out->routes[i].method); + safe_str_free(&out->routes[i].path); + safe_str_free(&out->routes[i].handler); } - free(out->routes); + safe_free(out->routes); for (int i = 0; i < out->hotspot_count; i++) { - free((void *)out->hotspots[i].name); - free((void *)out->hotspots[i].qualified_name); + safe_str_free(&out->hotspots[i].name); + safe_str_free(&out->hotspots[i].qualified_name); } - free(out->hotspots); + safe_free(out->hotspots); for (int i = 0; i < out->boundary_count; i++) { - free((void *)out->boundaries[i].from); - free((void *)out->boundaries[i].to); + safe_str_free(&out->boundaries[i].from); + safe_str_free(&out->boundaries[i].to); } - free(out->boundaries); + safe_free(out->boundaries); for (int i = 0; i < out->service_count; i++) { - free((void *)out->services[i].from); - free((void *)out->services[i].to); - free((void *)out->services[i].type); + safe_str_free(&out->services[i].from); + safe_str_free(&out->services[i].to); + safe_str_free(&out->services[i].type); } - free(out->services); + safe_free(out->services); for (int i = 0; i < out->layer_count; i++) { - free((void *)out->layers[i].name); - free((void *)out->layers[i].layer); - free((void *)out->layers[i].reason); + safe_str_free(&out->layers[i].name); + safe_str_free(&out->layers[i].layer); + safe_str_free(&out->layers[i].reason); } - free(out->layers); + safe_free(out->layers); for (int i = 0; i < out->cluster_count; i++) { - free((void *)out->clusters[i].label); + safe_str_free(&out->clusters[i].label); for (int j = 0; j < out->clusters[i].top_node_count; j++) { - free((void *)out->clusters[i].top_nodes[j]); + safe_str_free(&out->clusters[i].top_nodes[j]); } - free(out->clusters[i].top_nodes); + safe_free(out->clusters[i].top_nodes); for (int j = 0; j < out->clusters[i].package_count; j++) { - free((void *)out->clusters[i].packages[j]); + safe_str_free(&out->clusters[i].packages[j]); } - free(out->clusters[i].packages); + safe_free(out->clusters[i].packages); for (int j = 0; j < out->clusters[i].edge_type_count; j++) { - free((void *)out->clusters[i].edge_types[j]); + safe_str_free(&out->clusters[i].edge_types[j]); } - free(out->clusters[i].edge_types); + safe_free(out->clusters[i].edge_types); } - free(out->clusters); + safe_free(out->clusters); for (int i = 0; i < out->file_tree_count; i++) { - free((void *)out->file_tree[i].path); - free((void *)out->file_tree[i].type); + safe_str_free(&out->file_tree[i].path); + safe_str_free(&out->file_tree[i].type); } - free(out->file_tree); + safe_free(out->file_tree); memset(out, 0, sizeof(*out)); } @@ -4294,8 +4234,8 @@ void cbm_adr_sections_free(cbm_adr_sections_t *s) { return; } for (int i = 0; i < s->count; i++) { - free(s->keys[i]); - free(s->values[i]); + safe_free(s->keys[i]); + safe_free(s->values[i]); } memset(s, 0, sizeof(*s)); } @@ -4386,7 +4326,7 @@ int cbm_store_adr_update_sections(cbm_store_t *s, const char *project, const cha bool found = false; for (int j = 0; j < sections.count; j++) { if (strcmp(sections.keys[j], keys[i]) == 0) { - free(sections.values[j]); + safe_free(sections.values[j]); sections.values[j] = heap_strdup(values[i]); found = true; break; @@ -4409,13 +4349,13 @@ int cbm_store_adr_update_sections(cbm_store_t *s, const char *project, const cha snprintf(msg, sizeof(msg), "merged ADR exceeds %d chars (%d chars)", CBM_ADR_MAX_LENGTH, (int)strlen(merged)); store_set_error(s, msg); - free(merged); + safe_free(merged); return CBM_STORE_ERR; } /* Store merged */ rc = cbm_store_adr_store(s, project, merged); - free(merged); + safe_free(merged); if (rc != CBM_STORE_OK) { return rc; } @@ -4427,10 +4367,10 @@ void cbm_store_adr_free(cbm_adr_t *adr) { if (!adr) { return; } - free((void *)adr->project); - free((void *)adr->content); - free((void *)adr->created_at); - free((void *)adr->updated_at); + safe_str_free(&adr->project); + safe_str_free(&adr->content); + safe_str_free(&adr->created_at); + safe_str_free(&adr->updated_at); memset(adr, 0, sizeof(*adr)); } @@ -4453,10 +4393,7 @@ int cbm_store_find_architecture_docs(cbm_store_t *s, const char *project, char * int n = 0; char **arr = malloc(cap * sizeof(char *)); while (sqlite3_step(stmt) == SQLITE_ROW) { - if (n >= cap) { - cap *= ST_GROWTH; - arr = safe_realloc(arr, cap * sizeof(char *)); - } + safe_grow(arr, n, cap, ST_GROWTH); arr[n++] = heap_strdup((const char *)sqlite3_column_text(stmt, 0)); } sqlite3_finalize(stmt); @@ -4468,12 +4405,12 @@ int cbm_store_find_architecture_docs(cbm_store_t *s, const char *project, char * /* ── Memory management ──────────────────────────────────────────── */ void cbm_node_free_fields(cbm_node_t *n) { - free((void *)n->project); - free((void *)n->label); - free((void *)n->name); - free((void *)n->qualified_name); - free((void *)n->file_path); - free((void *)n->properties_json); + safe_str_free(&n->project); + safe_str_free(&n->label); + safe_str_free(&n->name); + safe_str_free(&n->qualified_name); + safe_str_free(&n->file_path); + safe_str_free(&n->properties_json); } void cbm_store_free_nodes(cbm_node_t *nodes, int count) { @@ -4483,7 +4420,7 @@ void cbm_store_free_nodes(cbm_node_t *nodes, int count) { for (int i = 0; i < count; i++) { cbm_node_free_fields(&nodes[i]); } - free(nodes); + safe_free(nodes); } void cbm_store_free_edges(cbm_edge_t *edges, int count) { @@ -4491,17 +4428,17 @@ void cbm_store_free_edges(cbm_edge_t *edges, int count) { return; } for (int i = 0; i < count; i++) { - free((void *)edges[i].project); - free((void *)edges[i].type); - free((void *)edges[i].properties_json); + safe_str_free(&edges[i].project); + safe_str_free(&edges[i].type); + safe_str_free(&edges[i].properties_json); } - free(edges); + safe_free(edges); } void cbm_project_free_fields(cbm_project_t *p) { - free((void *)p->name); - free((void *)p->indexed_at); - free((void *)p->root_path); + safe_str_free(&p->name); + safe_str_free(&p->indexed_at); + safe_str_free(&p->root_path); } void cbm_store_free_projects(cbm_project_t *projects, int count) { @@ -4511,7 +4448,7 @@ void cbm_store_free_projects(cbm_project_t *projects, int count) { for (int i = 0; i < count; i++) { cbm_project_free_fields(&projects[i]); } - free(projects); + safe_free(projects); } void cbm_store_free_file_hashes(cbm_file_hash_t *hashes, int count) { @@ -4519,9 +4456,9 @@ void cbm_store_free_file_hashes(cbm_file_hash_t *hashes, int count) { return; } for (int i = 0; i < count; i++) { - free((void *)hashes[i].project); - free((void *)hashes[i].rel_path); - free((void *)hashes[i].sha256); + safe_str_free(&hashes[i].project); + safe_str_free(&hashes[i].rel_path); + safe_str_free(&hashes[i].sha256); } - free(hashes); + safe_free(hashes); } From 9e926c8d427df29b274697c6d48c0c9f8d3ea1b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 14:54:29 +0000 Subject: [PATCH 11/18] Bump actions/download-artifact from 4.3.0 to 8.0.1 Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4.3.0 to 8.0.1. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/d3f86a106a0bac45b974a628896c90dbdf5c8093...3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: 8.0.1 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/_smoke.yml | 6 +++--- .github/workflows/release.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/_smoke.yml b/.github/workflows/_smoke.yml index 8c2a8809..0a051e3f 100644 --- a/.github/workflows/_smoke.yml +++ b/.github/workflows/_smoke.yml @@ -31,7 +31,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: binaries-${{ matrix.goos }}-${{ matrix.goarch }} @@ -118,7 +118,7 @@ jobs: zip coreutils - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: binaries-windows-amd64 @@ -181,7 +181,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: binaries-linux-${{ matrix.arch }}-portable diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5c6d8725..e72e38b9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -81,7 +81,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: merge-multiple: true From d3a555b63223eaf53ebeeecdc1ca3808cafab4b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 14:54:35 +0000 Subject: [PATCH 12/18] Bump msys2/setup-msys2 from 2.30.0 to 2.31.0 Bumps [msys2/setup-msys2](https://github.com/msys2/setup-msys2) from 2.30.0 to 2.31.0. - [Release notes](https://github.com/msys2/setup-msys2/releases) - [Changelog](https://github.com/msys2/setup-msys2/blob/main/CHANGELOG.md) - [Commits](https://github.com/msys2/setup-msys2/compare/4f806de0a5a7294ffabaff804b38a9b435a73bda...cafece8e6baf9247cf9b1bf95097b0b983cc558d) --- updated-dependencies: - dependency-name: msys2/setup-msys2 dependency-version: 2.31.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/_build.yml | 2 +- .github/workflows/_smoke.yml | 2 +- .github/workflows/_soak.yml | 4 ++-- .github/workflows/_test.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/_build.yml b/.github/workflows/_build.yml index 8a3336a8..ea9fabe9 100644 --- a/.github/workflows/_build.yml +++ b/.github/workflows/_build.yml @@ -92,7 +92,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # v2 + - uses: msys2/setup-msys2@cafece8e6baf9247cf9b1bf95097b0b983cc558d # v2 with: msystem: CLANG64 path-type: inherit diff --git a/.github/workflows/_smoke.yml b/.github/workflows/_smoke.yml index 8c2a8809..2147b5a6 100644 --- a/.github/workflows/_smoke.yml +++ b/.github/workflows/_smoke.yml @@ -108,7 +108,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # v2 + - uses: msys2/setup-msys2@cafece8e6baf9247cf9b1bf95097b0b983cc558d # v2 with: msystem: CLANG64 path-type: inherit diff --git a/.github/workflows/_soak.yml b/.github/workflows/_soak.yml index fa92790a..31970377 100644 --- a/.github/workflows/_soak.yml +++ b/.github/workflows/_soak.yml @@ -70,7 +70,7 @@ jobs: timeout-minutes: 30 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # v2 + - uses: msys2/setup-msys2@cafece8e6baf9247cf9b1bf95097b0b983cc558d # v2 with: msystem: CLANG64 path-type: inherit @@ -153,7 +153,7 @@ jobs: timeout-minutes: 45 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # v2 + - uses: msys2/setup-msys2@cafece8e6baf9247cf9b1bf95097b0b983cc558d # v2 with: msystem: CLANG64 path-type: inherit diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index 4b8b9894..1bbdb140 100644 --- a/.github/workflows/_test.yml +++ b/.github/workflows/_test.yml @@ -50,7 +50,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # v2 + - uses: msys2/setup-msys2@cafece8e6baf9247cf9b1bf95097b0b983cc558d # v2 with: msystem: CLANG64 path-type: inherit From 3e0cc05bcc00d6490d16eef3e9ea1c6e6a46ff6e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 14:54:42 +0000 Subject: [PATCH 13/18] Bump actions/attest-sbom Bumps [actions/attest-sbom](https://github.com/actions/attest-sbom) from 10926c72720ffc3f7b666661c8e55b1344e2a365 to bd218ad0dbcb3e146bd073d1d9c6d78e08aa8a0b. - [Release notes](https://github.com/actions/attest-sbom/releases) - [Changelog](https://github.com/actions/attest-sbom/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-sbom/compare/10926c72720ffc3f7b666661c8e55b1344e2a365...bd218ad0dbcb3e146bd073d1d9c6d78e08aa8a0b) --- updated-dependencies: - dependency-name: actions/attest-sbom dependency-version: bd218ad0dbcb3e146bd073d1d9c6d78e08aa8a0b dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5c6d8725..147e0321 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -124,7 +124,7 @@ jobs: " - name: Attest SBOM - uses: actions/attest-sbom@10926c72720ffc3f7b666661c8e55b1344e2a365 # v2 + uses: actions/attest-sbom@bd218ad0dbcb3e146bd073d1d9c6d78e08aa8a0b # v2 with: subject-path: '*.tar.gz' sbom-path: 'sbom.json' From 0885ad20df4496cb26ff18329e4cc04a9286750b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 14:54:46 +0000 Subject: [PATCH 14/18] Bump sigstore/cosign-installer from 3.9.1 to 4.1.1 Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.9.1 to 4.1.1. - [Release notes](https://github.com/sigstore/cosign-installer/releases) - [Commits](https://github.com/sigstore/cosign-installer/compare/398d4b0eeef1380460a10c8013a76f728fb906ac...cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003) --- updated-dependencies: - dependency-name: sigstore/cosign-installer dependency-version: 4.1.1 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5c6d8725..746317a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -130,7 +130,7 @@ jobs: sbom-path: 'sbom.json' - name: Install cosign - uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3 + uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 - name: Sign artifacts run: | From b2d3d64fc45fe4ecc00b6e137001ad9599acc07f Mon Sep 17 00:00:00 2001 From: map588 Date: Mon, 6 Apr 2026 11:09:24 -0400 Subject: [PATCH 15/18] gitignore --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7a7666e0..57bfdaf5 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,10 @@ Thumbs.db # MCP config (user-local, generated by install command) .mcp.json - +./.claude/settings.json +./.claude/agents/* +./.claude/skills/* +./.claude/worktrees/* # MCP Registry auth tokens .mcpregistry_* From 08259bcd132b0740db3eca17757e6e4d02303bd6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:28:31 +0000 Subject: [PATCH 16/18] Bump softprops/action-gh-release from 2.6.1 to 3.0.0 Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.6.1 to 3.0.0. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/153bb8e04406b158c6c84fc1615b65b24149a1fe...b4309332981a82ec1c5618f44dd2e27cc8bfbfda) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-version: 3.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fc73c849..3ba59bf5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -152,7 +152,7 @@ jobs: git tag -f "$VERSION" git push origin "$VERSION" --force - - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2 + - uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v2 with: tag_name: ${{ inputs.version }} draft: true From 9b496d3d7fcdaeb1502dc3ec73bf686b43384eb2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:28:38 +0000 Subject: [PATCH 17/18] Bump actions/attest-sbom from 2.4.0 to 4.1.0 Bumps [actions/attest-sbom](https://github.com/actions/attest-sbom) from 2.4.0 to 4.1.0. - [Release notes](https://github.com/actions/attest-sbom/releases) - [Changelog](https://github.com/actions/attest-sbom/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-sbom/compare/bd218ad0dbcb3e146bd073d1d9c6d78e08aa8a0b...c604332985a26aa8cf1bdc465b92731239ec6b9e) --- updated-dependencies: - dependency-name: actions/attest-sbom dependency-version: 4.1.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fc73c849..8144cc1c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -124,7 +124,7 @@ jobs: " - name: Attest SBOM - uses: actions/attest-sbom@bd218ad0dbcb3e146bd073d1d9c6d78e08aa8a0b # v2 + uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 with: subject-path: '*.tar.gz' sbom-path: 'sbom.json' From 9d3a245481e5f03ba2d91dd2c07ad901969ca17c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:28:42 +0000 Subject: [PATCH 18/18] Bump actions/upload-artifact from 7.0.0 to 7.0.1 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 7.0.0 to 7.0.1. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/bbbca2ddaa5d8feaa63e36b76fdaad77386f024f...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/_build.yml | 6 +++--- .github/workflows/_soak.yml | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/_build.yml b/.github/workflows/_build.yml index ea9fabe9..2686049c 100644 --- a/.github/workflows/_build.yml +++ b/.github/workflows/_build.yml @@ -81,7 +81,7 @@ jobs: tar -czf codebase-memory-mcp-ui-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz \ -C build/c codebase-memory-mcp LICENSE install.sh - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: binaries-${{ matrix.goos }}-${{ matrix.goarch }} path: "*.tar.gz" @@ -130,7 +130,7 @@ jobs: cp "$BIN" codebase-memory-mcp.exe zip codebase-memory-mcp-ui-windows-amd64.zip codebase-memory-mcp.exe LICENSE install.ps1 - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: binaries-windows-amd64 path: "*.zip" @@ -181,7 +181,7 @@ jobs: tar -czf codebase-memory-mcp-ui-linux-${{ matrix.arch }}-portable.tar.gz \ -C build/c codebase-memory-mcp LICENSE install.sh - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: binaries-linux-${{ matrix.arch }}-portable path: "*.tar.gz" diff --git a/.github/workflows/_soak.yml b/.github/workflows/_soak.yml index 31970377..34893b26 100644 --- a/.github/workflows/_soak.yml +++ b/.github/workflows/_soak.yml @@ -59,7 +59,7 @@ jobs: run: scripts/soak-test.sh build/c/codebase-memory-mcp ${{ inputs.duration_minutes }} - name: Upload metrics if: always() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: soak-quick-${{ matrix.goos }}-${{ matrix.goarch }} path: soak-results/ @@ -92,7 +92,7 @@ jobs: scripts/soak-test.sh "$BIN" ${{ inputs.duration_minutes }} - name: Upload metrics if: always() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: soak-quick-windows-amd64 path: soak-results/ @@ -141,7 +141,7 @@ jobs: run: scripts/soak-test.sh build/c/codebase-memory-mcp 15 - name: Upload metrics if: always() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: soak-asan-${{ matrix.goos }}-${{ matrix.goarch }} path: soak-results/ @@ -179,7 +179,7 @@ jobs: scripts/soak-test.sh "$BIN" 15 - name: Upload metrics if: always() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: soak-asan-windows-amd64 path: soak-results/