From 3f04f13b0734c6c6bf64e782ef996c0bcd23f258 Mon Sep 17 00:00:00 2001 From: Aaron Paterson Date: Thu, 22 Oct 2026 20:14:21 -0500 Subject: [PATCH] Hide the loadable extension's internal symbols The loadable extension exported every non-static function and sqlite3_api, 111 symbols in all. SQLite opens an extension with RTLD_GLOBAL, so with two builds of sqlite-vec loaded into one process, the second build's vec0 calls bound to the first build's functions: loading a build with the rescore index and then one built with SQLITE_VEC_ENABLE_RESCORE=0 crashed the process when the second connection created a table. Compile with -fvisibility=hidden, as vlasky/sqlite-vec@e8f156b does, and give SQLITE_VEC_API default visibility so sqlite3_vec_init stays exported. The static branch uses the attribute only with GCC and Clang outside Windows, so MSVC still compiles SQLITE_VEC_STATIC builds. --- Makefile | 6 ++++-- site/compiling.md | 6 ++++-- sqlite-vec.h.tmpl | 10 ++++++++-- tests/test-loadable.py | 14 ++++++++++++++ 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 175ab169..60b984c1 100644 --- a/Makefile +++ b/Makefile @@ -95,6 +95,7 @@ $(BUILD_DIR): $(prefix) $(TARGET_LOADABLE): sqlite-vec.c sqlite-vec.h $(prefix) $(CC) \ -fPIC -shared \ + -fvisibility=hidden \ -Wall -Wextra \ -Ivendor/ \ -O3 \ @@ -102,7 +103,7 @@ $(TARGET_LOADABLE): sqlite-vec.c sqlite-vec.h $(prefix) $< -o $@ $(TARGET_STATIC): sqlite-vec.c sqlite-vec.h $(prefix) $(OBJS_DIR) - $(CC) -Ivendor/ $(CFLAGS) -DSQLITE_CORE -DSQLITE_VEC_STATIC \ + $(CC) -Ivendor/ -fvisibility=hidden $(CFLAGS) -DSQLITE_CORE -DSQLITE_VEC_STATIC \ -O3 -c $< -o $(OBJS_DIR)/vec.o $(AR) rcs $@ $(OBJS_DIR)/vec.o @@ -130,7 +131,7 @@ $(LIBS_DIR)/shell.a: $(OBJS_DIR)/shell.o $(LIBS_DIR) $(AR) rcs $@ $< $(OBJS_DIR)/sqlite-vec.o: sqlite-vec.c $(OBJS_DIR) - $(CC) -c -g3 -Ivendor/ -I./ $(CFLAGS) $< -o $@ + $(CC) -c -g3 -fvisibility=hidden -Ivendor/ -I./ $(CFLAGS) $< -o $@ $(LIBS_DIR)/sqlite-vec.a: $(OBJS_DIR)/sqlite-vec.o $(LIBS_DIR) $(AR) rcs $@ $< @@ -138,6 +139,7 @@ $(LIBS_DIR)/sqlite-vec.a: $(OBJS_DIR)/sqlite-vec.o $(LIBS_DIR) $(TARGET_CLI): sqlite-vec.h $(LIBS_DIR)/sqlite-vec.a $(LIBS_DIR)/shell.a $(LIBS_DIR)/sqlite3.a examples/sqlite3-cli/core_init.c $(prefix) $(CC) -g3 \ + -fvisibility=hidden \ -Ivendor/ -I./ \ -DSQLITE_CORE \ -DSQLITE_VEC_STATIC \ diff --git a/site/compiling.md b/site/compiling.md index b3b2e330..5c01bb16 100644 --- a/site/compiling.md +++ b/site/compiling.md @@ -37,10 +37,10 @@ There will now be `sqlite-vec.c` and `sqlite-vec.h` available in your current di ```bash # Linux -gcc -g -fPIC -shared sqlite-vec.c -o vec0.so +gcc -g -fPIC -fvisibility=hidden -shared sqlite-vec.c -o vec0.so # MacOS -gcc -g -fPIC -dynamiclib sqlite-vec.c -o vec0.dylib +gcc -g -fPIC -fvisibility=hidden -dynamiclib sqlite-vec.c -o vec0.dylib # Windows, MSVC compiler cl sqlite-vec.c -link -dll -out:sqlite-vec.dll @@ -49,6 +49,8 @@ cl sqlite-vec.c -link -dll -out:sqlite-vec.dll gcc -g -shared sqlite-vec.c -o vec0.dll ``` +`-fvisibility=hidden` keeps the library from exporting any symbol but its entry point, `sqlite3_vec_init`. SQLite loads extensions with `RTLD_GLOBAL`, so an exported internal function can otherwise be called in place of a same-named function of another library in the process, another build of `sqlite-vec` included. + Different platforms, compiler, or architectures may require different compilation flags. ## Compile-time options diff --git a/sqlite-vec.h.tmpl b/sqlite-vec.h.tmpl index f49f62f6..1566f731 100644 --- a/sqlite-vec.h.tmpl +++ b/sqlite-vec.h.tmpl @@ -7,13 +7,19 @@ #include "sqlite3.h" #endif +// The Makefile compiles with -fvisibility=hidden, so the entry point is the +// one symbol a shared library built from sqlite-vec exports. #ifdef SQLITE_VEC_STATIC - #define SQLITE_VEC_API + #if defined(__GNUC__) && !defined(_WIN32) + #define SQLITE_VEC_API __attribute__((visibility("default"))) + #else + #define SQLITE_VEC_API + #endif #else #ifdef _WIN32 #define SQLITE_VEC_API __declspec(dllexport) #else - #define SQLITE_VEC_API + #define SQLITE_VEC_API __attribute__((visibility("default"))) #endif #endif diff --git a/tests/test-loadable.py b/tests/test-loadable.py index 0044144b..8da933c9 100644 --- a/tests/test-loadable.py +++ b/tests/test-loadable.py @@ -1,6 +1,9 @@ # ruff: noqa: E731 +import ctypes +import os import re +import sys from typing import List import sqlite3 import unittest @@ -153,6 +156,17 @@ def test_modules(): assert modules == MODULES +def test_exports_only_entry_point(): + # SQLite opens an extension with RTLD_GLOBAL, so calls inside one library + # can bind to another library's exported function of the same name, as they + # do between two builds of sqlite-vec loaded into one process. + suffix = {"darwin": ".dylib", "win32": ".dll"}.get(sys.platform, ".so") + lib = ctypes.CDLL(os.path.abspath(EXT_PATH + suffix)) + assert hasattr(lib, "sqlite3_vec_init") + for name in ["sqlite3_api", "bitmap_new", "vec0Filter_fullscan"]: + assert not hasattr(lib, name), name + + def test_vec_version(): vec_version = lambda *args: db.execute("select vec_version()", args).fetchone()[0] assert vec_version()[0] == "v"