fix(watcher): defer state_free in unwatch to prevent UAF

Signed-off-by: Gol3vka <gol3vka@163.com>
This commit is contained in:
Gol3vka
2026-06-26 09:36:20 +08:00
parent 3b93827fe2
commit 2bc4a539ac
+32 -1
View File
@@ -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);