From 2bc4a539ac9c1236c78568e47e419fc895eab736 Mon Sep 17 00:00:00 2001 From: Gol3vka Date: Sat, 20 Jun 2026 22:45:08 +0800 Subject: [PATCH] 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 04f27f127..3d506f920 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);