From c47783c2315788189852df196468965588bcd507 Mon Sep 17 00:00:00 2001 From: Gol3vka Date: Sat, 20 Jun 2026 22:23:42 +0800 Subject: [PATCH 1/3] fix: unwatch deleted projects to prevent zombie reindex Signed-off-by: Gol3vka --- src/mcp/mcp.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 8102b1e7..80bf44c0 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -1811,6 +1811,11 @@ static char *handle_delete_project(cbm_mcp_server_t *srv, const char *args) { } cbm_pipeline_unlock(); + + if (srv->watcher) { + cbm_watcher_unwatch(srv->watcher, name); + } + cbm_mem_collect(); /* return freed pages to OS after closing database */ yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); From a48439c3f2a51c3acdf625102ed1fdd3f2c6736c Mon Sep 17 00:00:00 2001 From: Gol3vka Date: Sat, 20 Jun 2026 22:45:08 +0800 Subject: [PATCH 2/3] fix(watcher): defer state_free in unwatch to prevent UAF Signed-off-by: Gol3vka --- src/watcher/watcher.c | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/watcher/watcher.c b/src/watcher/watcher.c index 04f27f12..3d506f92 100644 --- a/src/watcher/watcher.c +++ b/src/watcher/watcher.c @@ -53,6 +53,10 @@ struct cbm_watcher { CBMHashTable *projects; /* name → project_state_t* */ cbm_mutex_t projects_lock; atomic_int stopped; + /* Deferred-free list: freed after the next poll_once. */ + project_state_t **pending_free; + int pending_free_count; + int pending_free_cap; }; /* ── Constants ─────────────────────────────────────────────────── */ @@ -275,6 +279,10 @@ void cbm_watcher_free(cbm_watcher_t *w) { cbm_mutex_lock(&w->projects_lock); cbm_ht_foreach(w->projects, free_state_entry, NULL); cbm_ht_free(w->projects); + for (int i = 0; i < w->pending_free_count; i++) { + state_free(w->pending_free[i]); + } + free(w->pending_free); cbm_mutex_unlock(&w->projects_lock); cbm_mutex_destroy(&w->projects_lock); free(w); @@ -322,7 +330,23 @@ void cbm_watcher_unwatch(cbm_watcher_t *w, const char *project_name) { project_state_t *s = cbm_ht_get(w->projects, project_name); if (s) { cbm_ht_delete(w->projects, project_name); - state_free(s); + /* Defer free: the state may still be referenced by a poll_once + * snapshot taken before we acquired the lock. poll_once will + * drain this list at the start of its next cycle. */ + if (w->pending_free_count >= w->pending_free_cap) { + int new_cap = w->pending_free_cap ? w->pending_free_cap * 2 : 8; + project_state_t **tmp = realloc(w->pending_free, + (size_t)new_cap * sizeof(project_state_t *)); + if (tmp) { + w->pending_free = tmp; + w->pending_free_cap = new_cap; + } + } + if (w->pending_free_count < w->pending_free_cap) { + w->pending_free[w->pending_free_count++] = s; + } else { + state_free(s); /* realloc failed — fall back to immediate free */ + } removed = true; } cbm_mutex_unlock(&w->projects_lock); @@ -484,6 +508,13 @@ int cbm_watcher_poll_once(cbm_watcher_t *w) { * 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); + + /* Free deferred entries from the previous cycle. */ + for (int i = 0; i < w->pending_free_count; i++) { + state_free(w->pending_free[i]); + } + w->pending_free_count = 0; + int n = cbm_ht_count(w->projects); if (n == 0) { cbm_mutex_unlock(&w->projects_lock); From 1a3535637b7509a52738663cd63be01aa16e2f51 Mon Sep 17 00:00:00 2001 From: Gol3vka Date: Sun, 21 Jun 2026 21:54:09 +0800 Subject: [PATCH 3/3] format src/watcher.c Signed-off-by: Gol3vka --- src/watcher/watcher.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/watcher/watcher.c b/src/watcher/watcher.c index 3d506f92..cad39631 100644 --- a/src/watcher/watcher.c +++ b/src/watcher/watcher.c @@ -335,8 +335,8 @@ void cbm_watcher_unwatch(cbm_watcher_t *w, const char *project_name) { * drain this list at the start of its next cycle. */ if (w->pending_free_count >= w->pending_free_cap) { int new_cap = w->pending_free_cap ? w->pending_free_cap * 2 : 8; - project_state_t **tmp = realloc(w->pending_free, - (size_t)new_cap * sizeof(project_state_t *)); + project_state_t **tmp = + realloc(w->pending_free, (size_t)new_cap * sizeof(project_state_t *)); if (tmp) { w->pending_free = tmp; w->pending_free_cap = new_cap;