Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/store/hnsw.c
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,13 @@ ray_hnsw_t* ray_hnsw_build(const float* vectors, int64_t n_nodes, int32_t dim,
ray_hnsw_metric_t metric,
int32_t M, int32_t ef_construction) {
if (!vectors || n_nodes <= 0 || dim <= 0) return NULL;
/* Overflow guard: n_nodes * dim * sizeof(float) sizes the copied vector
* block and also indexes every distance computation (vectors + id*dim).
* If it wraps size_t the copy under-allocates and the memcpy below — and
* later reads during construction — run past the buffer. Reject before
* any allocation. Mirrors the per-layer neighbor guard in the loader.
* n_nodes and dim are > 0 here, so the divide is safe. */
if ((uint64_t)n_nodes > SIZE_MAX / sizeof(float) / (uint64_t)dim) return NULL;
if (M <= 0) M = HNSW_DEFAULT_M;
if (ef_construction <= 0) ef_construction = HNSW_DEFAULT_EF_C;
if (metric < RAY_HNSW_COSINE || metric > RAY_HNSW_IP) metric = RAY_HNSW_COSINE;
Expand Down
15 changes: 15 additions & 0 deletions test/test_embedding.c
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,20 @@ static test_result_t test_hnsw_mmap_load(void) {
PASS();
}

/* Regression: ray_hnsw_build must reject dimensions whose
* n_nodes * dim * sizeof(float) overflows size_t, before it copies the source
* vectors. Values are chosen so the byte count wraps to 16: without the guard
* the copy under-allocates and memcpy over-reads the 1-element source (ASan
* traps it); with the guard the call returns NULL and the source is never
* touched. (2^62 + 2) * 2 * sizeof(float) == 2^65 + 16 == 16 (mod 2^64). */
static test_result_t test_hnsw_build_overflow_rejected(void) {
float dummy[1] = { 0.0f };
ray_hnsw_t* idx = ray_hnsw_build(dummy, ((int64_t)1 << 62) + 2, 2,
RAY_HNSW_L2, 4, 50);
TEST_ASSERT_NULL(idx);
PASS();
}

/* Trigger the maxheap_sift_down / results-replacement path in hnsw_search_layer.
*
* The replacement branch (lines 342-344) fires when:
Expand Down Expand Up @@ -1964,6 +1978,7 @@ const test_entry_t embedding_entries[] = {
{ "embedding/hnsw_dim_accessor", test_hnsw_dim_accessor, emb_setup, emb_teardown },
{ "embedding/hnsw_search_filter_null_accept", test_hnsw_search_filter_null_accept, emb_setup, emb_teardown },
{ "embedding/hnsw_mmap_load", test_hnsw_mmap_load, emb_setup, emb_teardown },
{ "embedding/hnsw_build_overflow_rejected", test_hnsw_build_overflow_rejected, emb_setup, emb_teardown },
{ "embedding/hnsw_search_sift_down", test_hnsw_search_sift_down, emb_setup, emb_teardown },

/* rerank coverage (S7) */
Expand Down
Loading