From befeae45918590089227d4a329c734c44c5914e2 Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Fri, 31 Oct 2025 15:29:33 -0700 Subject: [PATCH] Fix nested dynamic library loading via RPATH There is a bug with dynamic library loading. Suppose liba.so is on LD_LIBRARY_PATH and it has an RPATH of `$ORIGIN/other_dir` and loads `libb.so` from other_dir. Then suppose `libb.so` has an RPATH of `$ORIGIN` and wants to load `libc.so` also from `other_dir`. Before this PR this doesn't work. The problem is that `flags.rpath.parentLibPath` is set to `libb.so` and we replace $ORIGIN with `PATH.dirname(parentLibName)` which is `.`. So unless `other_dir` is on the `LD_LIBRARY_PATH` or is the current working directory, loading will fail. The fix: if `findLibraryFS()` returns a value that is not `undefined`, replace `libName` with the returned value. --- src/lib/libdylink.js | 4 ++- test/test_other.py | 61 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/lib/libdylink.js b/src/lib/libdylink.js index 1315fa3f2a6bd..0b100b64d866a 100644 --- a/src/lib/libdylink.js +++ b/src/lib/libdylink.js @@ -1051,7 +1051,7 @@ var LibraryDylink = { #endif } var rpathResolved = (rpath?.paths || []).map((p) => replaceORIGIN(rpath?.parentLibPath, p)); - return withStackSave(() => { + var result = withStackSave(() => { // In dylink.c we use: `char buf[2*NAME_MAX+2];` and NAME_MAX is 255. // So we use the same size here. var bufSize = 2*255 + 2; @@ -1061,6 +1061,7 @@ var LibraryDylink = { var resLibNameC = __emscripten_find_dylib(buf, rpathC, libNameC, bufSize); return resLibNameC ? UTF8ToString(resLibNameC) : undefined; }); + return FS.lookupPath(result).path; }, #endif // FILESYSTEM @@ -1168,6 +1169,7 @@ var LibraryDylink = { dbg(`checking filesystem: ${libName}: ${f ? 'found' : 'not found'}`); #endif if (f) { + libName = f; var libData = FS.readFile(f, {encoding: 'binary'}); return flags.loadAsync ? Promise.resolve(libData) : libData; } diff --git a/test/test_other.py b/test/test_other.py index 8806d7ff3ed33..9ead460e94583 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -2437,6 +2437,67 @@ def get_runtime_paths(path): self.assertEqual(get_runtime_paths('libside1.so'), ['$ORIGIN']) self.assertEqual(get_runtime_paths('a.out.wasm'), ['$ORIGIN']) + def test_dylink_dependencies_rpath_nested(self): + create_file('pre.js', r''' + Module.preRun.push(() => { + Module.ENV.LD_LIBRARY_PATH = "/lib1"; + }); + ''') + create_file('side1.c', r''' + #include + + void side2(); + + void side1() { + printf("side1\n"); + side2(); + } + ''') + create_file('side2.c', r''' + #include + void side3(); + + void side2() { + printf("side2\n"); + side3(); + } + ''') + create_file('side3.c', r''' + #include + + void side3() { + printf("side3\n"); + } + ''') + create_file('main.c', r''' + #include + #include + + typedef void (*F)(void); + + int main() { + void* handle = dlopen("libside1.so", RTLD_NOW); + F side1 = (F)dlsym(handle, "side1"); + + printf("main\n"); + side1(); + return 0; + } + ''') + os.mkdir('libs') + os.mkdir('lib1') + self.emcc('side3.c', ['-fPIC', '-sSIDE_MODULE', '-olibs/libside3.so']) + self.emcc('side2.c', ['-fPIC', '-sSIDE_MODULE', '-olibs/libside2.so', '-Wl,-rpath,$ORIGIN', 'libs/libside3.so']) + self.emcc('side1.c', ['-fPIC', '-sSIDE_MODULE', '-Wl,-rpath,$ORIGIN/../libs', '-olib1/libside1.so', 'libs/libside2.so']) + settings = ['-sMAIN_MODULE=2', '-sDYLINK_DEBUG', "-sEXPORTED_FUNCTIONS=[_printf,_main]", "-sEXPORTED_RUNTIME_METHODS=ENV"] + preloads = [] + for file in ['lib1/libside1.so', 'libs/libside2.so', 'libs/libside3.so']: + preloads += ['--preload-file', file] + cmd = [EMCC, 'main.c', '-fPIC', '--pre-js', 'pre.js'] + settings + preloads + self.run_process(cmd) + + self.run_js('a.out.js') + def test_dylink_LEGACY_GL_EMULATION(self): # LEGACY_GL_EMULATION wraps JS library functions. This test ensure that when it does # so it preserves the `.sig` attributes needed by dynamic linking.