diff --git a/src/store/hnsw.c b/src/store/hnsw.c index 16a5f581..fb53b4b4 100644 --- a/src/store/hnsw.c +++ b/src/store/hnsw.c @@ -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; diff --git a/test/test_embedding.c b/test/test_embedding.c index ef8aca58..6dea9b10 100644 --- a/test/test_embedding.c +++ b/test/test_embedding.c @@ -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: @@ -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) */