Skip to content

Commit b96ccff

Browse files
malone-at-workdbentley-vsql
authored andcommitted
VEF extensions shouldn't share symbols (#435)
If two VEBs are installed with similar methods this could cause: 1. The wrong function called 2. After uninstalling one extension, calling the other could crash. 3. After uninstalling one extension, unregistering the other could crash. Changes included: * RTLD_LOCAL is not the default on mac, so we should set it in dlopen(). * Mark materialize_func_desc as hidden so the dynamic linker doesn't coallesce it across .so files.. * Add a test that reproduces this behavior (at least on mac). GitOrigin-RevId: 09375fc42938f751d0f22c471bcc9b1019c0e944
1 parent a3bcaf5 commit b96ccff

5 files changed

Lines changed: 133 additions & 2 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Test: VDF crash when two extensions share func_desc pointers
2+
call mtr.add_suppression("Orphaned expansion directory found but not removed");
3+
# restart: --veb-dir=MYSQLTEST_VARDIR/veb
4+
Creating extension test_vdf_ext using SDK...
5+
Created test_vdf_ext.veb
6+
Creating extension test_vdf_alt using SDK...
7+
Created test_vdf_alt.veb
8+
# Install both extensions
9+
INSTALL EXTENSION test_vdf_ext;
10+
INSTALL EXTENSION test_vdf_alt;
11+
# Qualified calls work with both extensions loaded
12+
SELECT test_vdf_ext.simple_int_func() AS result;
13+
result
14+
42
15+
SELECT test_vdf_alt.simple_int_func() AS result;
16+
result
17+
99
18+
# Uninstall second extension only
19+
UNINSTALL EXTENSION test_vdf_alt;
20+
# Qualified call to first extension should still work
21+
SELECT test_vdf_ext.simple_int_func() AS result;
22+
result
23+
42
24+
# Cleanup
25+
UNINSTALL EXTENSION test_vdf_ext;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Reproduction test: loading two extensions with similar VDF signatures
2+
# causes func_desc pointer sharing due to COMDAT/ICF merging of static
3+
# variables in materialize_func_desc<>. Uninstalling one extension
4+
# invalidates the other extension's func_desc pointers.
5+
#
6+
# This test uses only qualified VDF calls (extension.function syntax).
7+
8+
--echo # Test: VDF crash when two extensions share func_desc pointers
9+
10+
# Setup VEB directory for testing
11+
--let $veb_dir = $MYSQLTEST_VARDIR/veb
12+
--exec mkdir -p $veb_dir
13+
14+
# Restart server with --veb-dir option
15+
call mtr.add_suppression("Orphaned expansion directory found but not removed");
16+
--replace_result $MYSQLTEST_VARDIR MYSQLTEST_VARDIR
17+
--let $restart_parameters = restart: --veb-dir=$veb_dir
18+
--source include/restart_mysqld.inc
19+
20+
# Create first extension
21+
--let $extension_name = test_vdf_ext
22+
--let $extension_version = 1.0.0
23+
--let $extension_source = $MYSQL_TEST_DIR/suite/villagesql/std_data/simple_vdf.cc
24+
--source include/villagesql/create_extension_sdk.inc
25+
26+
# Create second extension (has overlapping function name: simple_int_func)
27+
--let $extension_name = test_vdf_alt
28+
--let $extension_version = 1.0.0
29+
--let $extension_source = $MYSQL_TEST_DIR/suite/villagesql/std_data/simple_vdf_alt.cc
30+
--source include/villagesql/create_extension_sdk.inc
31+
32+
--echo # Install both extensions
33+
INSTALL EXTENSION test_vdf_ext;
34+
INSTALL EXTENSION test_vdf_alt;
35+
36+
--echo # Qualified calls work with both extensions loaded
37+
SELECT test_vdf_ext.simple_int_func() AS result;
38+
SELECT test_vdf_alt.simple_int_func() AS result;
39+
40+
--echo # Uninstall second extension only
41+
UNINSTALL EXTENSION test_vdf_alt;
42+
43+
--echo # Qualified call to first extension should still work
44+
SELECT test_vdf_ext.simple_int_func() AS result;
45+
46+
--echo # Cleanup
47+
UNINSTALL EXTENSION test_vdf_ext;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/* Copyright (c) 2026 VillageSQL Contributors
2+
*
3+
* This program is free software; you can redistribute it and/or
4+
* modify it under the terms of the GNU General Public License
5+
* as published by the Free Software Foundation; either version 2
6+
* of the License, or (at your option) any later version.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License
14+
* along with this program; if not, see <https://www.gnu.org/licenses/>.
15+
*/
16+
17+
// Alternative simple test UDFs for extension testing.
18+
// Used to test ambiguous function name resolution when multiple extensions
19+
// provide the same function name.
20+
21+
#include <villagesql/extension.h>
22+
23+
#include <cstring>
24+
25+
// Returns a different constant integer value (99)
26+
void simple_int_func_impl(vef_context_t *ctx, vef_vdf_result_t *out) {
27+
out->int_value = 99;
28+
out->type = VEF_RESULT_VALUE;
29+
}
30+
31+
// Returns a different constant string value
32+
void alt_string_func_impl(vef_context_t *ctx, vef_vdf_result_t *out) {
33+
const char *msg = "Hello from alt extension";
34+
size_t len = strlen(msg);
35+
memcpy(out->str_buf, msg, len);
36+
out->actual_len = len;
37+
out->type = VEF_RESULT_VALUE;
38+
}
39+
40+
VEF_GENERATE_ENTRY_POINTS(
41+
make_extension("simple_udf_alt", "0.0.1-devtest")
42+
.func(make_func<&simple_int_func_impl>("simple_int_func")
43+
.returns(INT)
44+
.build())
45+
.func(make_func<&alt_string_func_impl>("alt_string_func")
46+
.returns(STRING)
47+
.buffer_size(100)
48+
.build()))

villagesql/sdk/include/villagesql/func_builder.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,15 @@ struct StaticFuncDesc {
258258
// Materializes the ABI descriptor structures at registration time.
259259
// Uses template parameters to ensure each function gets unique static storage.
260260
// FuncData is the StaticFuncDesc type, Index ensures uniqueness per function.
261+
//
262+
// Hidden visibility prevents the dynamic linker from coalescing identical
263+
// template instantiations across different extension .so files. Without this,
264+
// two extensions with functions of the same signature and index would share
265+
// the same static desc/signature objects, causing use-after-free when one
266+
// extension is unloaded.
261267
template <typename FuncData, size_t Index>
262-
vef_func_desc_t *materialize_func_desc(const FuncData &func_data) {
268+
__attribute__((visibility("hidden"))) vef_func_desc_t *materialize_func_desc(
269+
const FuncData &func_data) {
263270
static vef_signature_t signature;
264271
static vef_func_desc_t desc;
265272

villagesql/veb/veb_file.cc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -915,7 +915,11 @@ bool load_vef_extension(const std::string &so_path,
915915
registration.registration = nullptr;
916916
registration.unregister_func = nullptr;
917917

918-
void *handle = dlopen(so_path.c_str(), RTLD_NOW);
918+
// RTLD_LOCAL ensures each extension's symbols are isolated. Without it,
919+
// macOS defaults to RTLD_GLOBAL, allowing the dynamic linker to coalesce
920+
// weak symbols (e.g. C++ template instantiations) across extensions, causing
921+
// one extension to call another's function implementations.
922+
void *handle = dlopen(so_path.c_str(), RTLD_NOW | RTLD_LOCAL);
919923
if (handle == nullptr) {
920924
const char *errmsg;
921925
int error_number = dlopen_errno;

0 commit comments

Comments
 (0)