From 894496e3d57aed135b156f77a5e22809a296a323 Mon Sep 17 00:00:00 2001 From: Pepijn de Vos Date: Fri, 6 Mar 2026 08:37:22 +0100 Subject: [PATCH 1/3] [dylink] Normalize library paths to prevent duplicate loading When a shared library in a subdirectory references a dependency via $ORIGIN/.. rpath, findLibraryFS resolves it to a non-canonical path containing ".." (e.g. "sub/../lib.so"). Since loadDynamicLibrary uses the raw path as the LDSO key, this causes the same library to be loaded twice under different names, running constructors twice. Fix by normalizing libName with PATH.normalize() at the top of loadDynamicLibrary, matching what dlopenInternal already does. Co-Authored-By: Claude Opus 4.6 --- src/lib/libdylink.js | 2 ++ test/test_other.py | 54 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/lib/libdylink.js b/src/lib/libdylink.js index 5cb86ee25dd64..99ae83ff49f21 100644 --- a/src/lib/libdylink.js +++ b/src/lib/libdylink.js @@ -1103,6 +1103,8 @@ var LibraryDylink = { * @param {Object=} localScope */`, $loadDynamicLibrary: function(libName, flags = {global: true, nodelete: true}, localScope, handle) { + // Avoid duplicate LDSO entries from non-canonical paths (e.g. "sub/../lib.so") + libName = PATH.normalize(libName); #if DYLINK_DEBUG dbg(`loadDynamicLibrary: ${libName} handle: ${handle}`); dbg('existing:', Object.keys(LDSO.loadedLibsByName)); diff --git a/test/test_other.py b/test/test_other.py index 037eca335cf0f..633ea9f3f9265 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -6884,6 +6884,60 @@ def _build(rpath_flag, expected, **kwds): # case 2) with rpath: success _build(['-Wl,-rpath,$ORIGIN/subdir,-rpath,$ORIGIN'], "Hello\nHello_dep\nHello_nested_dep\nOk\n") + @also_with_wasmfs + def test_dlopen_rpath_no_duplicate_load(self): + # Test that a library loaded both directly and as a transitive dependency + # via $ORIGIN/.. rpath is not loaded twice. Without path normalization in + # loadDynamicLibrary, the LDSO would contain both "/usr/lib/libbase.so" + # and "/usr/lib/subdir/../libbase.so" as separate entries. + create_file('libbase.cpp', r''' + #include + static int init_count = 0; + struct Init { Init() { init_count++; } }; + static Init init; + extern "C" { + int base_func() { return 42; } + int get_init_count() { return init_count; } + } + ''') + create_file('libplugin.c', r''' + extern int base_func(); + int plugin_func() { return base_func() + 1; } + ''') + create_file('main.c', r''' + #include + #include + #include + + int main() { + void *hbase = dlopen("/usr/lib/libbase.so", RTLD_NOW | RTLD_GLOBAL); + assert(hbase); + + void *hplugin = dlopen("/usr/lib/subdir/libplugin.so", RTLD_NOW); + assert(hplugin); + + int (*base_func)() = dlsym(hbase, "base_func"); + int (*plugin_func)() = dlsym(hplugin, "plugin_func"); + int (*get_init_count)() = dlsym(hbase, "get_init_count"); + + printf("base: %d\n", base_func()); + printf("plugin: %d\n", plugin_func()); + printf("init_count: %d\n", get_init_count()); + + dlclose(hplugin); + dlclose(hbase); + return 0; + } + ''') + os.mkdir('subdir') + self.run_process([EMCC, '-o', 'libbase.so', 'libbase.cpp', '-sSIDE_MODULE']) + self.run_process([EMCC, '-o', 'subdir/libplugin.so', 'libplugin.c', '-sSIDE_MODULE', './libbase.so', '-Wl,-rpath,$ORIGIN/..']) + self.do_runf('main.c', 'base: 42\nplugin: 43\ninit_count: 1\n', + cflags=['--profiling-funcs', '-sMAIN_MODULE=2', '-sINITIAL_MEMORY=32Mb', + '--embed-file', 'libbase.so@/usr/lib/libbase.so', + '--embed-file', 'subdir/libplugin.so@/usr/lib/subdir/libplugin.so', + '-L.', '-lbase', '-L./subdir', '-lplugin', '-sNO_AUTOLOAD_DYLIBS']) + def test_dlopen_bad_flags(self): create_file('main.c', r''' #include From c4fb71cc38d46ef3bf585de80d8c538fa5a5adc0 Mon Sep 17 00:00:00 2001 From: Pepijn de Vos Date: Fri, 6 Mar 2026 20:30:32 +0100 Subject: [PATCH 2/3] Address PR review feedback - Convert test library from C++ to C using __attribute__((constructor)) - Use assert() checks instead of printf output matching - Remove unnecessary -sINITIAL_MEMORY=32Mb flag - Remove redundant PATH.normalize() from dlopenInternal Co-Authored-By: Claude Opus 4.6 --- src/lib/libdylink.js | 1 - test/test_other.py | 26 ++++++++++++-------------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/lib/libdylink.js b/src/lib/libdylink.js index 99ae83ff49f21..230a17111e2a2 100644 --- a/src/lib/libdylink.js +++ b/src/lib/libdylink.js @@ -1282,7 +1282,6 @@ var LibraryDylink = { #if DYLINK_DEBUG dbg('dlopenInternal:', filename); #endif - filename = PATH.normalize(filename); var searchpaths = []; var global = Boolean(flags & {{{ cDefs.RTLD_GLOBAL }}}); diff --git a/test/test_other.py b/test/test_other.py index 633ea9f3f9265..df5ac628ef32a 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -6890,15 +6890,12 @@ def test_dlopen_rpath_no_duplicate_load(self): # via $ORIGIN/.. rpath is not loaded twice. Without path normalization in # loadDynamicLibrary, the LDSO would contain both "/usr/lib/libbase.so" # and "/usr/lib/subdir/../libbase.so" as separate entries. - create_file('libbase.cpp', r''' - #include + create_file('libbase.c', r''' static int init_count = 0; - struct Init { Init() { init_count++; } }; - static Init init; - extern "C" { - int base_func() { return 42; } - int get_init_count() { return init_count; } - } + __attribute__((constructor)) + void init() { init_count++; } + int base_func() { return 42; } + int get_init_count() { return init_count; } ''') create_file('libplugin.c', r''' extern int base_func(); @@ -6920,20 +6917,21 @@ def test_dlopen_rpath_no_duplicate_load(self): int (*plugin_func)() = dlsym(hplugin, "plugin_func"); int (*get_init_count)() = dlsym(hbase, "get_init_count"); - printf("base: %d\n", base_func()); - printf("plugin: %d\n", plugin_func()); - printf("init_count: %d\n", get_init_count()); + assert(base_func() == 42); + assert(plugin_func() == 43); + assert(get_init_count() == 1); dlclose(hplugin); dlclose(hbase); + printf("success\n"); return 0; } ''') os.mkdir('subdir') - self.run_process([EMCC, '-o', 'libbase.so', 'libbase.cpp', '-sSIDE_MODULE']) + self.run_process([EMCC, '-o', 'libbase.so', 'libbase.c', '-sSIDE_MODULE']) self.run_process([EMCC, '-o', 'subdir/libplugin.so', 'libplugin.c', '-sSIDE_MODULE', './libbase.so', '-Wl,-rpath,$ORIGIN/..']) - self.do_runf('main.c', 'base: 42\nplugin: 43\ninit_count: 1\n', - cflags=['--profiling-funcs', '-sMAIN_MODULE=2', '-sINITIAL_MEMORY=32Mb', + self.do_runf('main.c', 'success\n', + cflags=['--profiling-funcs', '-sMAIN_MODULE=2', '--embed-file', 'libbase.so@/usr/lib/libbase.so', '--embed-file', 'subdir/libplugin.so@/usr/lib/subdir/libplugin.so', '-L.', '-lbase', '-L./subdir', '-lplugin', '-sNO_AUTOLOAD_DYLIBS']) From f3a60e427b297e71669d49f3e3a1989ca19684ee Mon Sep 17 00:00:00 2001 From: Pepijn de Vos Date: Tue, 10 Mar 2026 15:55:25 +0100 Subject: [PATCH 3/3] Automatic rebaseline of codesize expectations. NFC This is an automatic change generated by tools/maint/rebaseline_tests.py. The following (9) test expectation files were updated by running the tests with `--rebaseline`: ``` codesize/test_codesize_cxx_ctors1.json: 152368 => 152420 [+52 bytes / +0.03%] codesize/test_codesize_cxx_ctors2.json: 151765 => 151817 [+52 bytes / +0.03%] codesize/test_codesize_cxx_lto.json: 120968 => 121028 [+60 bytes / +0.05%] codesize/test_codesize_cxx_noexcept.json: 154375 => 154427 [+52 bytes / +0.03%] codesize/test_codesize_cxx_wasmfs.json: 179944 => 179996 [+52 bytes / +0.03%] codesize/test_codesize_hello_dylink.json: 44326 => 44334 [+8 bytes / +0.02%] codesize/test_codesize_hello_dylink_all.json: 822383 => 822379 [-4 bytes / -0.00%] codesize/test_minimal_runtime_code_size_hello_embind.json: 14891 => 14908 [+17 bytes / +0.11%] codesize/test_minimal_runtime_code_size_hello_embind_val.json: 11687 => 11703 [+16 bytes / +0.14%] Average change: +0.05% (-0.00% - +0.14%) ``` --- test/codesize/test_codesize_cxx_ctors1.json | 8 ++++---- test/codesize/test_codesize_cxx_ctors2.json | 8 ++++---- test/codesize/test_codesize_cxx_lto.json | 8 ++++---- test/codesize/test_codesize_cxx_noexcept.json | 8 ++++---- test/codesize/test_codesize_cxx_wasmfs.json | 8 ++++---- test/codesize/test_codesize_hello_dylink.json | 8 ++++---- test/codesize/test_codesize_hello_dylink_all.json | 4 ++-- .../test_minimal_runtime_code_size_hello_embind.json | 8 ++++---- .../test_minimal_runtime_code_size_hello_embind_val.json | 8 ++++---- 9 files changed, 34 insertions(+), 34 deletions(-) diff --git a/test/codesize/test_codesize_cxx_ctors1.json b/test/codesize/test_codesize_cxx_ctors1.json index c41946e53f075..e448beaef3b75 100644 --- a/test/codesize/test_codesize_cxx_ctors1.json +++ b/test/codesize/test_codesize_cxx_ctors1.json @@ -1,10 +1,10 @@ { "a.out.js": 19555, "a.out.js.gz": 8102, - "a.out.nodebug.wasm": 132813, - "a.out.nodebug.wasm.gz": 49862, - "total": 152368, - "total_gz": 57964, + "a.out.nodebug.wasm": 132865, + "a.out.nodebug.wasm.gz": 49889, + "total": 152420, + "total_gz": 57991, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_cxx_ctors2.json b/test/codesize/test_codesize_cxx_ctors2.json index 18efe0a1dae85..1ae310d7bf07d 100644 --- a/test/codesize/test_codesize_cxx_ctors2.json +++ b/test/codesize/test_codesize_cxx_ctors2.json @@ -1,10 +1,10 @@ { "a.out.js": 19532, "a.out.js.gz": 8087, - "a.out.nodebug.wasm": 132233, - "a.out.nodebug.wasm.gz": 49515, - "total": 151765, - "total_gz": 57602, + "a.out.nodebug.wasm": 132285, + "a.out.nodebug.wasm.gz": 49542, + "total": 151817, + "total_gz": 57629, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_cxx_lto.json b/test/codesize/test_codesize_cxx_lto.json index 68625e72f5a15..76bba51afeab1 100644 --- a/test/codesize/test_codesize_cxx_lto.json +++ b/test/codesize/test_codesize_cxx_lto.json @@ -1,10 +1,10 @@ { "a.out.js": 18902, "a.out.js.gz": 7782, - "a.out.nodebug.wasm": 102066, - "a.out.nodebug.wasm.gz": 39421, - "total": 120968, - "total_gz": 47203, + "a.out.nodebug.wasm": 102126, + "a.out.nodebug.wasm.gz": 39426, + "total": 121028, + "total_gz": 47208, "sent": [ "a (emscripten_resize_heap)", "b (_setitimer_js)", diff --git a/test/codesize/test_codesize_cxx_noexcept.json b/test/codesize/test_codesize_cxx_noexcept.json index 3a3adc6e6e5c9..a4019f2e47fd5 100644 --- a/test/codesize/test_codesize_cxx_noexcept.json +++ b/test/codesize/test_codesize_cxx_noexcept.json @@ -1,10 +1,10 @@ { "a.out.js": 19555, "a.out.js.gz": 8102, - "a.out.nodebug.wasm": 134820, - "a.out.nodebug.wasm.gz": 50699, - "total": 154375, - "total_gz": 58801, + "a.out.nodebug.wasm": 134872, + "a.out.nodebug.wasm.gz": 50724, + "total": 154427, + "total_gz": 58826, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_cxx_wasmfs.json b/test/codesize/test_codesize_cxx_wasmfs.json index 6ab72f046d4bd..f4faf7bc4bcb3 100644 --- a/test/codesize/test_codesize_cxx_wasmfs.json +++ b/test/codesize/test_codesize_cxx_wasmfs.json @@ -1,10 +1,10 @@ { "a.out.js": 7053, "a.out.js.gz": 3325, - "a.out.nodebug.wasm": 172891, - "a.out.nodebug.wasm.gz": 63277, - "total": 179944, - "total_gz": 66602, + "a.out.nodebug.wasm": 172943, + "a.out.nodebug.wasm.gz": 63297, + "total": 179996, + "total_gz": 66622, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_hello_dylink.json b/test/codesize/test_codesize_hello_dylink.json index cbd43f6d97db7..9dbea1c58fbd8 100644 --- a/test/codesize/test_codesize_hello_dylink.json +++ b/test/codesize/test_codesize_hello_dylink.json @@ -1,10 +1,10 @@ { - "a.out.js": 26596, - "a.out.js.gz": 11356, + "a.out.js": 26604, + "a.out.js.gz": 11359, "a.out.nodebug.wasm": 17730, "a.out.nodebug.wasm.gz": 8957, - "total": 44326, - "total_gz": 20313, + "total": 44334, + "total_gz": 20316, "sent": [ "__syscall_stat64", "emscripten_resize_heap", diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 492d4a36d3de5..f8a597c2b6728 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 244691, + "a.out.js": 244687, "a.out.nodebug.wasm": 577692, - "total": 822383, + "total": 822379, "sent": [ "IMG_Init", "IMG_Load", diff --git a/test/codesize/test_minimal_runtime_code_size_hello_embind.json b/test/codesize/test_minimal_runtime_code_size_hello_embind.json index e47b52ea73673..054a709a8231f 100644 --- a/test/codesize/test_minimal_runtime_code_size_hello_embind.json +++ b/test/codesize/test_minimal_runtime_code_size_hello_embind.json @@ -3,8 +3,8 @@ "a.html.gz": 371, "a.js": 7262, "a.js.gz": 3323, - "a.wasm": 7081, - "a.wasm.gz": 3250, - "total": 14891, - "total_gz": 6944 + "a.wasm": 7098, + "a.wasm.gz": 3255, + "total": 14908, + "total_gz": 6949 } diff --git a/test/codesize/test_minimal_runtime_code_size_hello_embind_val.json b/test/codesize/test_minimal_runtime_code_size_hello_embind_val.json index 01adce09a02a4..074910e28e3e6 100644 --- a/test/codesize/test_minimal_runtime_code_size_hello_embind_val.json +++ b/test/codesize/test_minimal_runtime_code_size_hello_embind_val.json @@ -3,8 +3,8 @@ "a.html.gz": 371, "a.js": 5353, "a.js.gz": 2522, - "a.wasm": 5786, - "a.wasm.gz": 2726, - "total": 11687, - "total_gz": 5619 + "a.wasm": 5802, + "a.wasm.gz": 2732, + "total": 11703, + "total_gz": 5625 }