Race Condition / Use-After-Free in HNSW cursor handling

MEDIUM
redis/redis
Commit: 8a5cf17cb2cd
Affected: <= 8.6.2 (prior to this fix)
2026-04-04 11:38 UTC

Description

The commit adds mutex/lock handling around the HNSW cursor and index data structures to make access thread-safe. Prior to this change, hnsw_cursor_init, hnsw_cursor_free, and hnsw_cursor_next manipulated the shared index cursors list without proper synchronization. This creates race conditions when multiple threads concurrently create, traverse, and free cursors, potentially leading to use-after-free, memory corruption, or crashes under concurrent access. The fix introduces a global lock (index->global_lock) to guard cursor insertion/removal and provides lock/unlock helpers for safe iteration. This is a genuine security-related improvement because race conditions in shared data structures can be exploited for crashes or information leakage under concurrency.

Proof of Concept

// Proof-of-concept: demonstrates a race between cursor iteration and freeing in the pre-fix API // NOTE: This PoC targets the pre-fix behavior where hnsw_cursor_next(index, cursor) and // hnsw_cursor_free(index, cursor) operate without proper synchronization. // The code is a conceptual demonstration and may require adaptation to a real test harness. #include <pthread.h> #include <unistd.h> #include "hnsw.h" static HNSW *g_index = NULL; static hnswCursor *g_cursor = NULL; void* iter_thread(void* arg) { // Initialize a cursor and iterate through the index. g_cursor = hnsw_cursor_init(g_index); if (!g_cursor) return NULL; hnswNode *node; while ((node = hnsw_cursor_next(g_index, g_cursor)) != NULL) { // Simulate work on node (void)node; usleep(100); // small delay to widen the race window } // Free the cursor (without synchronization in pre-fix code) hnsw_cursor_free(g_index, g_cursor); g_cursor = NULL; return NULL; } void* free_thread(void* arg) { // Sleep a bit to ensure the iterator has started usleep(50); if (g_cursor) { // Potential race: freeing the cursor while iter_thread is using it hnsw_cursor_free(g_index, g_cursor); g_cursor = NULL; } return NULL; } int main() { // Initialize index (details omitted). In a real test, populate with some nodes. g_index = hnsw_create_index(/* params */); pthread_t t1, t2; pthread_create(&t1, NULL, iter_thread, NULL); pthread_create(&t2, NULL, free_thread, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); // Cleanup index hnsw_free_index(g_index); return 0; } // Expected outcome prior to the fix: a race between hnsw_cursor_next() and hnsw_cursor_free() // may cause use-after-free or memory corruption when the cursor is freed while being iterated.

Commit Details

Author: antirez

Date: 2025-03-15 22:31 UTC

Message:

HNSW: cursor fixes and thread safety.

Triage Assessment

Vulnerability Type: Race Condition

Confidence: MEDIUM

Reasoning:

The changes add mutex/lock handling around the HNSW cursor, making access to the index and cursors thread-safe. This mitigates potential race conditions that could lead to data corruption, crashes, or information leakage under concurrent access, which are security-relevant issues.

Verification Assessment

Vulnerability Type: Race Condition / Use-After-Free in HNSW cursor handling

Confidence: MEDIUM

Affected Versions: <= 8.6.2 (prior to this fix)

Code Diff

diff --git a/hnsw.c b/hnsw.c index f09e0497a8b..1c29b75c136 100644 --- a/hnsw.c +++ b/hnsw.c @@ -2237,36 +2237,58 @@ int hnsw_deserialize_index(HNSW *index) { * * The function returns NULL on out of memory. */ hnswCursor *hnsw_cursor_init(HNSW *index) { + if (pthread_rwlock_wrlock(&index->global_lock) != 0) return NULL; hnswCursor *cursor = hmalloc(sizeof(*cursor)); - if (cursor == NULL) return NULL; + if (cursor == NULL) { + pthread_rwlock_unlock(&index->global_lock); + return NULL; + } + cursor->index = index; cursor->next = index->cursors; cursor->current = index->head; index->cursors = cursor; + pthread_rwlock_unlock(&index->global_lock); return cursor; } /* Free the cursor. Can be called both at the end of the iteration, when * hnsw_cursor_next() returned NULL, or before. */ -void hnsw_cursor_free(HNSW *index, hnswCursor *cursor) { - hnswCursor *x = index->cursors; +void hnsw_cursor_free(hnswCursor *cursor) { + pthread_rwlock_wrlock(&cursor->index->global_lock); // Best effort. + hnswCursor *x = cursor->index->cursors; hnswCursor *prev = NULL; while(x) { if (x == cursor) { if (prev) prev->next = cursor->next; else - index->cursors = cursor->next; + cursor->index->cursors = cursor->next; hfree(cursor); - return; + break; } x = x->next; } + pthread_rwlock_unlock(&cursor->index->global_lock); +} + +/* Acquire a lock to use the cursor. Returns 1 if the lock was acquired + * with success, otherwise zero is returned. The returned element is + * protected after calling hnsw_cursor_next() for all the time required to + * access it, then hnsw_cursor_release_lock() should be called in order + * to unlock the HNSW index. */ +int hnsw_cursor_acquire_lock(hnswCursor *cursor) { + return pthread_rwlock_rdlock(&cursor->index->global_lock) == 0; +} + +/* Release the cursor lock, see hnsw_cursor_acquire_lock() top comment + * for more information. */ +void hnsw_cursor_release_lock(hnswCursor *cursor) { + pthread_rwlock_unlock(&cursor->index->global_lock); } /* Return the next element of the HNSW. See hnsw_cursor_init() for * the guarantees of the function. */ -hnswNode *hnsw_cursor_next(HNSW *index, hnswCursor *cursor) { - (void)index; // Unused but future proof to have. +hnswNode *hnsw_cursor_next(hnswCursor *cursor) { hnswNode *ret = cursor->current; if (ret) cursor->current = ret->next; return ret; diff --git a/hnsw.h b/hnsw.h index 69c3153b365..48af67d788e 100644 --- a/hnsw.h +++ b/hnsw.h @@ -65,12 +65,15 @@ typedef struct hnswNode { hnswNodeLayer layers[]; } hnswNode; +struct HNSW; + /* It is possible to navigate an HNSW with a cursor that guarantees * visiting all the elements that remain in the HNSW from the start to the * end of the process (but not the new ones, so that the process will * eventually finish). Check hnsw_cursor_init(), hnsw_cursor_next() and * hnsw_cursor_free(). */ typedef struct hnswCursor { + struct HNSW *index; // Reference to the index of this cursor. hnswNode *current; // Element to report when hnsw_cursor_next() is called. struct hnswCursor *next; // Next cursor active. } hnswCursor; @@ -156,8 +159,10 @@ uint32_t hnsw_quants_bytes(HNSW *index); /* Cursors. */ hnswCursor *hnsw_cursor_init(HNSW *index); -void hnsw_cursor_free(HNSW *index, hnswCursor *cursor); -hnswNode *hnsw_cursor_next(HNSW *index, hnswCursor *cursor); +void hnsw_cursor_free(hnswCursor *cursor); +hnswNode *hnsw_cursor_next(hnswCursor *cursor); +int hnsw_cursor_acquire_lock(hnswCursor *cursor); +void hnsw_cursor_release_lock(hnswCursor *cursor); /* Allocator selection. */ void hnsw_set_allocator(void (*free_ptr)(void*), void *(*malloc_ptr)(size_t),
← Back to Alerts View on GitHub →