Skip to content

Commit 894496e

Browse files
pepijndevosclaude
andcommitted
[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 <noreply@anthropic.com>
1 parent 68c7164 commit 894496e

2 files changed

Lines changed: 56 additions & 0 deletions

File tree

src/lib/libdylink.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,6 +1103,8 @@ var LibraryDylink = {
11031103
* @param {Object=} localScope
11041104
*/`,
11051105
$loadDynamicLibrary: function(libName, flags = {global: true, nodelete: true}, localScope, handle) {
1106+
// Avoid duplicate LDSO entries from non-canonical paths (e.g. "sub/../lib.so")
1107+
libName = PATH.normalize(libName);
11061108
#if DYLINK_DEBUG
11071109
dbg(`loadDynamicLibrary: ${libName} handle: ${handle}`);
11081110
dbg('existing:', Object.keys(LDSO.loadedLibsByName));

test/test_other.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6884,6 +6884,60 @@ def _build(rpath_flag, expected, **kwds):
68846884
# case 2) with rpath: success
68856885
_build(['-Wl,-rpath,$ORIGIN/subdir,-rpath,$ORIGIN'], "Hello\nHello_dep\nHello_nested_dep\nOk\n")
68866886

6887+
@also_with_wasmfs
6888+
def test_dlopen_rpath_no_duplicate_load(self):
6889+
# Test that a library loaded both directly and as a transitive dependency
6890+
# via $ORIGIN/.. rpath is not loaded twice. Without path normalization in
6891+
# loadDynamicLibrary, the LDSO would contain both "/usr/lib/libbase.so"
6892+
# and "/usr/lib/subdir/../libbase.so" as separate entries.
6893+
create_file('libbase.cpp', r'''
6894+
#include <stdio.h>
6895+
static int init_count = 0;
6896+
struct Init { Init() { init_count++; } };
6897+
static Init init;
6898+
extern "C" {
6899+
int base_func() { return 42; }
6900+
int get_init_count() { return init_count; }
6901+
}
6902+
''')
6903+
create_file('libplugin.c', r'''
6904+
extern int base_func();
6905+
int plugin_func() { return base_func() + 1; }
6906+
''')
6907+
create_file('main.c', r'''
6908+
#include <assert.h>
6909+
#include <stdio.h>
6910+
#include <dlfcn.h>
6911+
6912+
int main() {
6913+
void *hbase = dlopen("/usr/lib/libbase.so", RTLD_NOW | RTLD_GLOBAL);
6914+
assert(hbase);
6915+
6916+
void *hplugin = dlopen("/usr/lib/subdir/libplugin.so", RTLD_NOW);
6917+
assert(hplugin);
6918+
6919+
int (*base_func)() = dlsym(hbase, "base_func");
6920+
int (*plugin_func)() = dlsym(hplugin, "plugin_func");
6921+
int (*get_init_count)() = dlsym(hbase, "get_init_count");
6922+
6923+
printf("base: %d\n", base_func());
6924+
printf("plugin: %d\n", plugin_func());
6925+
printf("init_count: %d\n", get_init_count());
6926+
6927+
dlclose(hplugin);
6928+
dlclose(hbase);
6929+
return 0;
6930+
}
6931+
''')
6932+
os.mkdir('subdir')
6933+
self.run_process([EMCC, '-o', 'libbase.so', 'libbase.cpp', '-sSIDE_MODULE'])
6934+
self.run_process([EMCC, '-o', 'subdir/libplugin.so', 'libplugin.c', '-sSIDE_MODULE', './libbase.so', '-Wl,-rpath,$ORIGIN/..'])
6935+
self.do_runf('main.c', 'base: 42\nplugin: 43\ninit_count: 1\n',
6936+
cflags=['--profiling-funcs', '-sMAIN_MODULE=2', '-sINITIAL_MEMORY=32Mb',
6937+
'--embed-file', 'libbase.so@/usr/lib/libbase.so',
6938+
'--embed-file', 'subdir/libplugin.so@/usr/lib/subdir/libplugin.so',
6939+
'-L.', '-lbase', '-L./subdir', '-lplugin', '-sNO_AUTOLOAD_DYLIBS'])
6940+
68876941
def test_dlopen_bad_flags(self):
68886942
create_file('main.c', r'''
68896943
#include <dlfcn.h>

0 commit comments

Comments
 (0)