From a2426147eca101b671765c555cc548759e35221f Mon Sep 17 00:00:00 2001 From: NikitaProgArt Date: Thu, 20 Aug 2026 18:58:46 +0500 Subject: [PATCH] Search the calling image's RUNPATH for dlopen from loaded code glibc resolves a bare dlopen name through the calling object's DT_RUNPATH (or its DT_RPATH when it carries no RUNPATH). SoLo consulted the requester's paths only while resolving its DT_NEEDED entries, so a dlopen issued by loaded code could not reach a sibling through $ORIGIN. On WSL2, dzn's D3D12 UMD fails to load its libigd12um64xel.so exactly this way and previously required DL_ELF_LIBRARY_PATH pointed at the driver directory. Map the glibc-boundary caller's return address to the issuing image, thread it into Loader::load, and let resolvePath use it for the rpath/runPath slots, in glibc's order. Register dlopen@GLIBC_2.2.5 in the shim table as well: pre-2.34 importers previously fell through to the raw stub and skipped the caller plumbing. The smoke test builds a host DSO with -rpath '$ORIGIN/runpath' - and an old-dtags DT_RPATH variant - that dlopens a sibling by bare name from a directory no other search path carries, so the load succeeds only through the caller's path. --- build.py | 4 ++- lib/dlfcn.cpp | 12 +++++-- lib/dlfcn.h | 4 +++ lib/elf_loader.cpp | 54 +++++++++++++++++++++++------ lib/elf_loader.h | 3 ++ lib/glibc_shim.cpp | 16 ++++++--- tst/glibc_runpath_host_test.c | 23 +++++++++++++ tst/glibc_runpath_sibling_test.c | 5 +++ tst/run_smoke.py | 59 ++++++++++++++++++++++++++++++++ tst/smoke.cpp | 26 ++++++++++++++ 10 files changed, 189 insertions(+), 17 deletions(-) create mode 100644 tst/glibc_runpath_host_test.c create mode 100644 tst/glibc_runpath_sibling_test.c diff --git a/build.py b/build.py index 4db5bbf9..a0195a6e 100644 --- a/build.py +++ b/build.py @@ -938,7 +938,7 @@ def downloadPackage(name, url, filename, sha256): arch_smoke = command( name="arch_smoke", - inputs=["$(S)/tst/glibc_test.c", "$(S)/tst/glibc_exception_test.cpp", "$(S)/tst/glibc_lazy_test.c", "$(S)/tst/glibc_shim_test.c", "$(S)/tst/glibc_ie_test.c", "$(S)/tst/glibc_interpose_test.c", "$(S)/tst/glibc_overridable_test.c", "$(S)/tst/glibc_caller_test.c", "$(S)/tst/glibc_versioned_test.c", "$(S)/tst/glibc_version_consumer_test.c", "$(S)/tst/glibc_ie_gd_test.c", "$(S)/tst/glibc_ie_ref_test.c", "$(S)/tst/glibc_big_tls_test.c", "$(S)/tst/run_smoke.py", *archives], + inputs=["$(S)/tst/glibc_test.c", "$(S)/tst/glibc_exception_test.cpp", "$(S)/tst/glibc_lazy_test.c", "$(S)/tst/glibc_shim_test.c", "$(S)/tst/glibc_ie_test.c", "$(S)/tst/glibc_interpose_test.c", "$(S)/tst/glibc_overridable_test.c", "$(S)/tst/glibc_caller_test.c", "$(S)/tst/glibc_versioned_test.c", "$(S)/tst/glibc_version_consumer_test.c", "$(S)/tst/glibc_ie_gd_test.c", "$(S)/tst/glibc_ie_ref_test.c", "$(S)/tst/glibc_big_tls_test.c", "$(S)/tst/glibc_runpath_host_test.c", "$(S)/tst/glibc_runpath_sibling_test.c", "$(S)/tst/run_smoke.py", *archives], outputs=["$(B)/tst/arch-smoke.log"], deps=[smoke, *downloads], cmd=[ @@ -962,6 +962,8 @@ def downloadPackage(name, url, filename, sha256): "DLFCN_GLIBC_IE_GD_TEST_SOURCE": "$(S)/tst/glibc_ie_gd_test.c", "DLFCN_GLIBC_IE_REF_TEST_SOURCE": "$(S)/tst/glibc_ie_ref_test.c", "DLFCN_GLIBC_BIG_TLS_TEST_SOURCE": "$(S)/tst/glibc_big_tls_test.c", + "DLFCN_GLIBC_RUNPATH_HOST_TEST_SOURCE": "$(S)/tst/glibc_runpath_host_test.c", + "DLFCN_GLIBC_RUNPATH_SIBLING_TEST_SOURCE": "$(S)/tst/glibc_runpath_sibling_test.c", "DLFCN_GLIBC_TEST_SOURCE": "$(S)/tst/glibc_test.c", "DLFCN_SMOKE": "$(B)/tst/smoke", "DLFCN_SYSROOT_LIB": sysroot_lib, diff --git a/lib/dlfcn.cpp b/lib/dlfcn.cpp index ba168e3c..1693e86c 100644 --- a/lib/dlfcn.cpp +++ b/lib/dlfcn.cpp @@ -266,7 +266,7 @@ extern "C" void* stub_dlsym(void* handle, const char* symbol) { return nullptr; } -extern "C" void* stub_dlopen(const char* filename, int mode) { +static void* dlopenImpl(const void* caller, const char* filename, int mode) { clearLastError(); try { @@ -290,7 +290,7 @@ extern "C" void* stub_dlopen(const char* filename, int mode) { // The loader keeps one wrapper per image, so repeated dlopen of the // same library returns the same handle. - return ElfImage::loadElf(filename, mode); + return caller ? ElfImage::loadElfFrom(caller, filename, mode) : ElfImage::loadElf(filename, mode); } catch (const std::exception& error) { setLastError(error.what()); return nullptr; @@ -300,6 +300,14 @@ extern "C" void* stub_dlopen(const char* filename, int mode) { } } +extern "C" void* stub_dlopen(const char* filename, int mode) { + return dlopenImpl(nullptr, filename, mode); +} + +extern "C" void* stub_dlopen_from(const void* caller, const char* filename, int mode) { + return dlopenImpl(caller, filename, mode); +} + extern "C" int stub_dlclose(void* handle) { clearLastError(); diff --git a/lib/dlfcn.h b/lib/dlfcn.h index 479db9e1..2f05a0a0 100644 --- a/lib/dlfcn.h +++ b/lib/dlfcn.h @@ -45,6 +45,10 @@ extern "C" { void* stub_dlsym(void* handle, const char* symbol); void* stub_dlopen(const char* filename, int flags); + // The dlopen issued by loaded code: caller is the return address the + // calling image will resume at, and its DT_RPATH/DT_RUNPATH join the + // library search for names without a slash. + void* stub_dlopen_from(const void* caller, const char* filename, int flags); int stub_dlclose(void* handle); char* stub_dlerror(void); // Startup wiring: the registry is unsynchronized, so finish every diff --git a/lib/elf_loader.cpp b/lib/elf_loader.cpp index e36091d4..d0671f53 100644 --- a/lib/elf_loader.cpp +++ b/lib/elf_loader.cpp @@ -350,7 +350,7 @@ namespace { static Loader& instance(); - LinkMap* load(const std::string_view& requestedPath, int flags); + LinkMap* load(const std::string_view& requestedPath, int flags, LinkMap* dlopenCaller); void runPendingInitializers(); void* lookup(LinkMap& image, std::string_view name, std::string_view version); @@ -359,6 +359,7 @@ namespace { void makeGlobal(LinkMap& image); bool findAddress(const void* address, ElfAddress* res); + LinkMap* findImageByAddress(const void* address); int iterateProgramHeaders(ElfProgramHeaderCallback& callback); LinkMap* findByName(const std::string_view& name) const noexcept; @@ -369,7 +370,7 @@ namespace { static std::optional inSearchPath(std::string_view directories, const std::string_view& name, bool emptyIsCurrentDirectory); static std::optional inCache(const std::string_view& name); - std::optional resolvePath(const std::string_view& path) const; + std::optional resolvePath(const std::string_view& path, const LinkMap* dlopenCaller) const; void rememberLibraryDirectory(const std::string& path); size_t addTlsModule(); @@ -505,7 +506,7 @@ Loader& Loader::instance() { return *loader; } -LinkMap* Loader::load(const std::string_view& requestedPath, int flags) { +LinkMap* Loader::load(const std::string_view& requestedPath, int flags, LinkMap* dlopenCaller) { std::lock_guard lock(mutex_); if (requestedPath.empty()) { @@ -523,7 +524,7 @@ LinkMap* Loader::load(const std::string_view& requestedPath, int flags) { return image; } - auto resolved = resolvePath(requestedPath); + auto resolved = resolvePath(requestedPath, dlopenCaller); if (!resolved) { throwError("cannot resolve ELF image: %.*s", static_cast(requestedPath.size()), requestedPath.data()); @@ -894,6 +895,25 @@ bool Loader::findAddress(const void* address, ElfAddress* res) { return true; } +LinkMap* Loader::findImageByAddress(const void* address) { + std::lock_guard lock(mutex_); + auto needle = reinterpret_cast(address); + auto found = imagesByAddress_.upper_bound(needle); + + if (found == imagesByAddress_.begin()) { + return nullptr; + } + --found; + + auto& image = *found->second; + + if (needle >= image.mapStart + image.mapSize) { + return nullptr; + } + + return ℑ +} + int Loader::iterateProgramHeaders(ElfProgramHeaderCallback& callback) { std::vector images; { @@ -1053,18 +1073,22 @@ std::optional Loader::inCache(const std::string_view& name) { return resolved; } -std::optional Loader::resolvePath(const std::string_view& path) const { +std::optional Loader::resolvePath(const std::string_view& path, const LinkMap* dlopenCaller) const { if (path.find('/') != std::string_view::npos) { return realPath(std::string(path)); } + // A dlopen issued by loaded code searches the calling image's paths; + // while resolving DT_NEEDED, the image being loaded is the requester. + const LinkMap* requester = dlopenCaller ? dlopenCaller : requester_; + if (const auto* configured = getenv("DL_ELF_LIBRARY_PATH"); configured) { if (auto resolved = inSearchPath(configured, path, false); resolved) { return resolved; } } - if (requester_ && !requester_->rpath.empty()) { - if (auto resolved = inSearchPath(requester_->rpath, path, false); resolved) { + if (requester && !requester->rpath.empty()) { + if (auto resolved = inSearchPath(requester->rpath, path, false); resolved) { return resolved; } } @@ -1073,8 +1097,8 @@ std::optional Loader::resolvePath(const std::string_view& path) con return resolved; } } - if (requester_ && !requester_->runPath.empty()) { - if (auto resolved = inSearchPath(requester_->runPath, path, false); resolved) { + if (requester && !requester->runPath.empty()) { + if (auto resolved = inSearchPath(requester->runPath, path, false); resolved) { return resolved; } } @@ -2091,7 +2115,17 @@ IfaceHandle::Kind ElfImage::handleKind() const { ElfImage* ElfImage::loadElf(std::string_view path, int flags) { auto& loader = Loader::instance(); - auto* image = loader.load(path, flags); + auto* image = loader.load(path, flags, nullptr); + + loader.runPendingInitializers(); + + return image ? image->wrapper.get() : nullptr; +} + +ElfImage* ElfImage::loadElfFrom(const void* callerAddress, std::string_view path, int flags) { + auto& loader = Loader::instance(); + auto* caller = callerAddress ? loader.findImageByAddress(callerAddress) : nullptr; + auto* image = loader.load(path, flags, caller); loader.runPendingInitializers(); diff --git a/lib/elf_loader.h b/lib/elf_loader.h index 6b6071b5..dcceaf6b 100644 --- a/lib/elf_loader.h +++ b/lib/elf_loader.h @@ -57,6 +57,9 @@ namespace dyn { virtual void* lookupVersion(std::string_view symbol, std::string_view version) const = 0; static ElfImage* loadElf(std::string_view path, int flags); + // The dlopen entry from loaded code: callerAddress names the image + // that issued the call, whose DT_RPATH/DT_RUNPATH join the search. + static ElfImage* loadElfFrom(const void* callerAddress, std::string_view path, int flags); static bool findAddress(const void* address, ElfAddress* res); static int iterateProgramHeaders(ElfProgramHeaderCallback& callback); diff --git a/lib/glibc_shim.cpp b/lib/glibc_shim.cpp index 3185415b..b5af25df 100644 --- a/lib/glibc_shim.cpp +++ b/lib/glibc_shim.cpp @@ -3370,7 +3370,7 @@ namespace { return translated; } - static void* sh_glibc_dlopen(const char* path, int flags) { + static void* sh_glibc_dlopenFrom(const void* caller, const char* path, int flags) { ThreadTls::current()->clearDlError(); try { @@ -3379,7 +3379,7 @@ namespace { } auto* provider = runtimeProvider(path); - auto* handle = stub_dlopen(provider ? provider : path, sh_translate_dlopen_flags(flags)); + auto* handle = stub_dlopen_from(caller, provider ? provider : path, sh_translate_dlopen_flags(flags)); if (!handle) { copyStubError("library not found"); @@ -3396,6 +3396,13 @@ namespace { return nullptr; } + // The glibc-boundary entry: its caller's return address names the image + // that issued the dlopen, whose DT_RPATH/DT_RUNPATH join the search for + // names without a slash. + __attribute__((noinline)) static void* sh_glibc_dlopen(const char* path, int flags) { + return sh_glibc_dlopenFrom(__builtin_return_address(0), path, flags); + } + static int sh_dladdr1(const void* address, Dl_info* information, void** extra, int flags) { if (!stub_dladdr(address, information)) { return 0; @@ -3416,14 +3423,14 @@ namespace { // The base namespace is plain dlopen; new link-map namespaces stay an // explicit non-goal, declined through dlerror rather than an abort — // libcuda imports the symbol. - static void* sh_glibc_dlmopen(long namespace_id, const char* path, int flags) { + __attribute__((noinline)) static void* sh_glibc_dlmopen(long namespace_id, const char* path, int flags) { if (namespace_id != 0) { ThreadTls::current()->setDlError("dlmopen: link-map namespaces are not supported"); return nullptr; } - return sh_glibc_dlopen(path, flags); + return sh_glibc_dlopenFrom(__builtin_return_address(0), path, flags); } static void* sh_glibc_dlsym(void* handle, const char* name) { @@ -3905,6 +3912,7 @@ namespace { SH_FUNCTION("posix_memalign", "GLIBC_2.2.5", posix_memalign), SH_FUNCTION("strcmp", "GLIBC_2.2.5", strcmp), SH_FUNCTION("dlopen", "GLIBC_2.34", sh_glibc_dlopen), + SH_FUNCTION("dlopen", "GLIBC_2.2.5", sh_glibc_dlopen), SH_FUNCTION("dlmopen", "GLIBC_2.3.4", sh_glibc_dlmopen), SH_FUNCTION("__memcpy_chk", "GLIBC_2.3.4", sh_memcpy_chk), SH_FUNCTION("realpath", "GLIBC_2.3", realpath), diff --git a/tst/glibc_runpath_host_test.c b/tst/glibc_runpath_host_test.c new file mode 100644 index 00000000..c201ca5a --- /dev/null +++ b/tst/glibc_runpath_host_test.c @@ -0,0 +1,23 @@ +/* Caller-RUNPATH conformance: built with -Wl,-rpath,'$ORIGIN/runpath', the + * host loads its sibling by a bare name that no other search path carries, + * so the load succeeds only when the caller's DT_RUNPATH joins the search. */ + +extern void* dlopen(const char* filename, int flags); +extern void* dlsym(void* handle, const char* symbol); + +#define RTLD_NOW 2 +#define RTLD_LOCAL 0 + +typedef int (*GlibcRunpathValue)(void); + +int glibc_runpath_host_value(void) { + void* handle = dlopen("libdlfcn-test-runpath-sibling.so", RTLD_NOW | RTLD_LOCAL); + + if (!handle) { + return 0; + } + + GlibcRunpathValue value = (GlibcRunpathValue)dlsym(handle, "glibc_runpath_sibling_value"); + + return value ? value() : 0; +} diff --git a/tst/glibc_runpath_sibling_test.c b/tst/glibc_runpath_sibling_test.c new file mode 100644 index 00000000..963f1e5b --- /dev/null +++ b/tst/glibc_runpath_sibling_test.c @@ -0,0 +1,5 @@ +/* The sibling reachable only through the host's DT_RUNPATH. */ + +int glibc_runpath_sibling_value(void) { + return 77; +} diff --git a/tst/run_smoke.py b/tst/run_smoke.py index a37a5f79..f0b462c5 100644 --- a/tst/run_smoke.py +++ b/tst/run_smoke.py @@ -298,6 +298,65 @@ def main(): ], check=True, ) + # The caller-RUNPATH family: a host carrying -rpath '$ORIGIN/runpath' + # loads a sibling by bare name that sits outside every other search + # path, so the load succeeds only when the caller's DT_RUNPATH joins + # the search. + runpath_directory = library_path / "runpath" + runpath_directory.mkdir() + subprocess.run( + [ + *shlex.split(os.environ["DLFCN_CC"]), + "-fPIC", + "-fno-stack-protector", + "-shared", + "-nostdlib", + "-Wl,--no-as-needed", + "-Wl,--enable-new-dtags", + "-Wl,-rpath,$ORIGIN/runpath", + str(root / sysroot_lib / "libc.so.6"), + "-Wl,-soname,libdlfcn-test-runpath-host.so", + os.environ["DLFCN_GLIBC_RUNPATH_HOST_TEST_SOURCE"], + "-o", + str(library_path / "libdlfcn-test-runpath-host.so"), + ], + check=True, + ) + subprocess.run( + [ + *shlex.split(os.environ["DLFCN_CC"]), + "-fPIC", + "-fno-stack-protector", + "-shared", + "-nostdlib", + "-Wl,--no-as-needed", + str(root / sysroot_lib / "libc.so.6"), + "-Wl,-soname,libdlfcn-test-runpath-sibling.so", + os.environ["DLFCN_GLIBC_RUNPATH_SIBLING_TEST_SOURCE"], + "-o", + str(runpath_directory / "libdlfcn-test-runpath-sibling.so"), + ], + check=True, + ) + # The same host with old-dtags: a caller carrying DT_RPATH only. + subprocess.run( + [ + *shlex.split(os.environ["DLFCN_CC"]), + "-fPIC", + "-fno-stack-protector", + "-shared", + "-nostdlib", + "-Wl,--no-as-needed", + "-Wl,--disable-new-dtags", + "-Wl,-rpath,$ORIGIN/runpath", + str(root / sysroot_lib / "libc.so.6"), + "-Wl,-soname,libdlfcn-test-rpath-host.so", + os.environ["DLFCN_GLIBC_RUNPATH_HOST_TEST_SOURCE"], + "-o", + str(library_path / "libdlfcn-test-rpath-host.so"), + ], + check=True, + ) (library_path / "libdlfcn-test-pci.so").symlink_to( root / sysroot_lib / "libpciaccess.so.0" ) diff --git a/tst/smoke.cpp b/tst/smoke.cpp index 878b0591..294f2be4 100644 --- a/tst/smoke.cpp +++ b/tst/smoke.cpp @@ -318,6 +318,32 @@ int main() { return 1; } + // A dlopen issued by loaded code searches the caller's DT_RUNPATH: the + // host carries -rpath '$ORIGIN/runpath' and loads a sibling that sits + // outside every other search path. + auto* runpathHost = stub_dlopen("libdlfcn-test-runpath-host.so", RTLD_NOW | RTLD_LOCAL); + if (!runpathHost) { + fprintf(stderr, "runpath host load failed: %s\n", stub_dlerror()); + return 1; + } + auto runpathHostValue = reinterpret_cast(requiredSymbol(runpathHost, "glibc_runpath_host_value")); + if (!runpathHostValue || runpathHostValue() != 77) { + fprintf(stderr, "caller DT_RUNPATH lookup failed\n"); + return 1; + } + + // The old-dtags counterpart: a caller carrying DT_RPATH only. + auto* rpathHost = stub_dlopen("libdlfcn-test-rpath-host.so", RTLD_NOW | RTLD_LOCAL); + if (!rpathHost) { + fprintf(stderr, "rpath host load failed: %s\n", stub_dlerror()); + return 1; + } + auto rpathHostValue = reinterpret_cast(requiredSymbol(rpathHost, "glibc_runpath_host_value")); + if (!rpathHostValue || rpathHostValue() != 77) { + fprintf(stderr, "caller DT_RPATH lookup failed\n"); + return 1; + } + // Initial-exec TLS: one process-wide GOT offset must be valid in every // thread through the surplus static arena. The parked thread exists // before the module loads and must see zeroed TLS for it.