From 5574ffa101f31b1a47867ea3ccbee1d0ed5dfbc8 Mon Sep 17 00:00:00 2001 From: Aaron Paterson Date: Thu, 22 Oct 2026 14:03:22 -0500 Subject: [PATCH 1/3] Free the names registered in vec_static_blobs Inserting a row into vec_static_blobs copies its name with sqlite3_mprintf() into the connection's registry, and the module freed the registry with sqlite3_free() alone when the connection closed, so every registered name leaked. LeakSanitizer reports it from vec_static_blobsUpdate() for any connection that registers a static blob, as test_vec_static_blob_entries does. The module's destructor now frees each registered name before the registry. --- sqlite-vec.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/sqlite-vec.c b/sqlite-vec.c index ab77083c..cb53f906 100644 --- a/sqlite-vec.c +++ b/sqlite-vec.c @@ -10871,6 +10871,15 @@ struct vec_static_blob_data { static_blob static_blobs[MAX_STATIC_BLOBS]; }; +/** Frees the registry and the names vec_static_blobsUpdate() copied in. */ +static void vec_static_blob_data_free(void *p) { + vec_static_blob_data *data = p; + for (int i = 0; i < MAX_STATIC_BLOBS; i++) { + sqlite3_free(data->static_blobs[i].name); + } + sqlite3_free(data); +} + typedef struct vec_static_blobs_vtab vec_static_blobs_vtab; struct vec_static_blobs_vtab { sqlite3_vtab base; @@ -11657,7 +11666,7 @@ sqlite3_vec_static_blobs_init(sqlite3 *db, char **pzErrMsg, return rc; rc = sqlite3_create_module_v2(db, "vec_static_blobs", &vec_static_blobsModule, - static_blob_data, sqlite3_free); + static_blob_data, vec_static_blob_data_free); if (rc != SQLITE_OK) return rc; rc = sqlite3_create_module_v2(db, "vec_static_blob_entries", From 02e019831bb9c95228ed3ac32a56c20196136665 Mon Sep 17 00:00:00 2001 From: Aaron Paterson Date: Thu, 22 Oct 2026 14:07:46 -0500 Subject: [PATCH 2/3] Keep a static blob KNN inside its candidate bitmap vec_static_blob_entriesFilter() rounds the blob's vector count up to a multiple of 8 (bsize) for its candidate bitmap, then clears the bits past the last vector with a loop that starts at bsize itself, one bit past the bitmap's end. When the vector count fills the bitmap's bytes exactly, as 64 vectors do, SQLite's allocator has no padding to absorb the write, and AddressSanitizer reports a heap-buffer-overflow in bitmap_set() from the filter. The loop now clears the bits from the vector count up to bsize. test_vec_static_blob_entries_knn_with_whole_candidate_bytes runs a KNN query over a 64-vector blob and checks its rows; against an AddressSanitizer build it reports the overflow without the fix and passes with it. --- sqlite-vec.c | 2 +- tests/test-loadable.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/sqlite-vec.c b/sqlite-vec.c index cb53f906..590c73f3 100644 --- a/sqlite-vec.c +++ b/sqlite-vec.c @@ -11367,7 +11367,7 @@ static int vec_static_blob_entriesFilter(sqlite3_vtab_cursor *pVtabCursor, } bitmap_fill(candidates, bsize); - for (size_t i = bsize; i >= p->blob->nvectors; i--) { + for (size_t i = p->blob->nvectors; i < bsize; i++) { bitmap_set(candidates, i, 0); } i32 k_used = 0; diff --git a/tests/test-loadable.py b/tests/test-loadable.py index 0ef935c5..9f64406a 100644 --- a/tests/test-loadable.py +++ b/tests/test-loadable.py @@ -264,6 +264,19 @@ def test_vec_static_blob_entries(): ] +def test_vec_static_blob_entries_knn_with_whole_candidate_bytes(): + # 64 vectors fill the KNN's candidate bitmap to its last byte. The blob + # reads the array's memory, so the array stays referenced while it is queried. + db = connect(EXT_PATH, extra_entrypoint="sqlite3_vec_static_blobs_init") + vectors = np.array([[i / 64] * 4 for i in range(64)], np.float32) + register_numpy(db, "w", vectors) + rows = db.execute( + "select rowid from w where vector match ? and k = 3 order by distance", + [np.array([0.5] * 4, dtype=np.float32)], + ).fetchall() + assert [r[0] for r in rows] == [32, 33, 31] + + def test_limits(): db = connect(EXT_PATH) with _raises( From 24349b36424f75538fdc4071ee2e657b061c400c Mon Sep 17 00:00:00 2001 From: Aaron Paterson Date: Thu, 22 Oct 2026 14:08:42 -0500 Subject: [PATCH 3/3] Free a static blob KNN's results when its cursor closes vec_static_blob_entriesFilter() allocates a KNN query's rowids and distances into the cursor's knn_data, and vec_static_blob_entriesClose() freed knn_data alone, so every KNN query on a vec_static_blob_entries table leaked both arrays. LeakSanitizer reports them from the filter. The cursor's close now calls sbe_query_knn_data_clear() first, which frees them, as photostructure/sqlite-vec f56fdeb does. --- sqlite-vec.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sqlite-vec.c b/sqlite-vec.c index 590c73f3..7a52250a 100644 --- a/sqlite-vec.c +++ b/sqlite-vec.c @@ -11186,7 +11186,10 @@ static int vec_static_blob_entriesOpen(sqlite3_vtab *p, static int vec_static_blob_entriesClose(sqlite3_vtab_cursor *cur) { vec_static_blob_entries_cursor *pCur = (vec_static_blob_entries_cursor *)cur; - sqlite3_free(pCur->knn_data); + if (pCur->knn_data) { + sbe_query_knn_data_clear(pCur->knn_data); + sqlite3_free(pCur->knn_data); + } sqlite3_free(pCur); return SQLITE_OK; }