From 8a2ad6b7a8437ca9a7df41ff337c06317f468277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mos=C3=A8=20Giordano?= Date: Tue, 27 Sep 2022 01:46:39 +0100 Subject: [PATCH] Probe if system libstdc++ is newer than ours If the system libstdc++ is detected to be newer, load it. Otherwise, load the one that we ship. This improves compatibility with external shared libraries that the user might have on their system. Fixes #34276 Co-authored-by: Jameson Nash Co-authored-by: Elliot Saba --- Make.inc | 2 + cli/Makefile | 6 +- cli/loader_lib.c | 220 +++++++++++++++++++++++++++++++++++++++++++++-- deps/csl.mk | 1 - 4 files changed, 221 insertions(+), 8 deletions(-) diff --git a/Make.inc b/Make.inc index dfc2594c406eb..d011290d2af9d 100644 --- a/Make.inc +++ b/Make.inc @@ -1149,6 +1149,8 @@ BB_TRIPLET := $(subst $(SPACE),-,$(filter-out cxx%,$(filter-out libgfortran%,$(s LIBGFORTRAN_VERSION := $(subst libgfortran,,$(filter libgfortran%,$(subst -,$(SPACE),$(BB_TRIPLET_LIBGFORTRAN)))) +CSL_NEXT_GLIBCXX_VERSION=GLIBCXX_3\.4\.30|GLIBCXX_3\.5\.|GLIBCXX_4\. + # This is the set of projects that BinaryBuilder dependencies are hooked up for. # Note: we explicitly _do not_ define `CSL` here, since it requires some more # advanced techniques to decide whether it should be installed from a BB source diff --git a/cli/Makefile b/cli/Makefile index 274877ecaf12a..58c1f82f48662 100644 --- a/cli/Makefile +++ b/cli/Makefile @@ -12,6 +12,8 @@ LOADER_LDFLAGS = $(JLDFLAGS) -ffreestanding -L$(build_shlibdir) -L$(build_libdir ifeq ($(OS),WINNT) LOADER_CFLAGS += -municode -mconsole -nostdlib -fno-stack-check -fno-stack-protector -mno-stack-arg-probe +else ifeq ($(OS),Linux) +LOADER_CFLAGS += -DGLIBCXX_LEAST_VERSION_SYMBOL=\"$(shell echo "$(CSL_NEXT_GLIBCXX_VERSION)" | cut -d'|' -f1 | sed 's/\\//g')\" endif ifeq ($(OS),WINNT) @@ -110,7 +112,7 @@ endif $(build_shlibdir)/libjulia.$(JL_MAJOR_MINOR_SHLIB_EXT): $(LIB_OBJS) $(SRCDIR)/list_strip_symbols.h | $(build_shlibdir) $(build_libdir) @$(call PRINT_LINK, $(CC) $(call IMPLIB_FLAGS,$@.tmp) $(LOADER_CFLAGS) -DLIBRARY_EXPORTS -shared $(SHIPFLAGS) $(LIB_OBJS) -o $@ \ - $(JLIBLDFLAGS) $(LOADER_LDFLAGS) $(RPATH_LIB) $(call SONAME_FLAGS,libjulia.$(JL_MAJOR_SHLIB_EXT))) + $(JLIBLDFLAGS) $(LOADER_LDFLAGS) $(call SONAME_FLAGS,libjulia.$(JL_MAJOR_SHLIB_EXT))) @$(INSTALL_NAME_CMD)libjulia.$(SHLIB_EXT) $@ ifeq ($(OS), WINNT) @# Note that if the objcopy command starts getting too long, we can use `@file` to read @@ -120,7 +122,7 @@ endif $(build_shlibdir)/libjulia-debug.$(JL_MAJOR_MINOR_SHLIB_EXT): $(LIB_DOBJS) $(SRCDIR)/list_strip_symbols.h | $(build_shlibdir) $(build_libdir) @$(call PRINT_LINK, $(CC) $(call IMPLIB_FLAGS,$@.tmp) $(LOADER_CFLAGS) -DLIBRARY_EXPORTS -shared $(DEBUGFLAGS) $(LIB_DOBJS) -o $@ \ - $(JLIBLDFLAGS) $(LOADER_LDFLAGS) $(RPATH_LIB) $(call SONAME_FLAGS,libjulia-debug.$(JL_MAJOR_SHLIB_EXT))) + $(JLIBLDFLAGS) $(LOADER_LDFLAGS) $(call SONAME_FLAGS,libjulia-debug.$(JL_MAJOR_SHLIB_EXT))) @$(INSTALL_NAME_CMD)libjulia-debug.$(SHLIB_EXT) $@ ifeq ($(OS), WINNT) @$(call PRINT_ANALYZE, $(OBJCOPY) $(build_libdir)/$(notdir $@).tmp.a $(STRIP_EXPORTED_FUNCS) $(build_libdir)/$(notdir $@).a && rm $(build_libdir)/$(notdir $@).tmp.a) diff --git a/cli/loader_lib.c b/cli/loader_lib.c index 74241510ffd25..c13f80796f864 100644 --- a/cli/loader_lib.c +++ b/cli/loader_lib.c @@ -33,7 +33,6 @@ void jl_loader_print_stderr3(const char * msg1, const char * msg2, const char * /* Wrapper around dlopen(), with extra relative pathing thrown in*/ static void * load_library(const char * rel_path, const char * src_dir, int err) { void * handle = NULL; - // See if a handle is already open to the basename const char *basename = rel_path + strlen(rel_path); while (basename-- > rel_path) @@ -153,6 +152,181 @@ JL_DLLEXPORT const char * jl_get_libdir() return lib_dir; } +#ifdef _OS_LINUX_ +#ifndef GLIBCXX_LEAST_VERSION_SYMBOL /* Should always be defined in the makefile. This appeases the linter. */ +#define GLIBCXX_LEAST_VERSION_SYMBOL "GLIBCXX_a.b.c" +#endif + +#include +#include + +static void write_wrapper(int fd, char *str, size_t len) +{ + size_t written_sofar = 0; + while (len) { + ssize_t bytes_written = write(fd, str + written_sofar, len); + if (bytes_written == -1 && errno == EINTR) continue; + if (bytes_written == -1 && errno != EINTR) { + perror("Error in write wrapper:\nwrite"); + _exit(1); + } + len -= bytes_written; + written_sofar += bytes_written; + } +} + +// Where the buf passed in was malloced +static void read_wrapper(int fd, char **ret, size_t *ret_len) +{ + // Allocate an initial buffer + size_t len = JL_PATH_MAX; + char *buf = (char *)malloc(len + 1); + if (!buf) { + char err_str[] = "Error in read wrapper:\nmalloc() returned NULL.\n"; + size_t err_strlen = strlen(err_str); + write_wrapper(STDERR_FILENO, err_str, err_strlen); + exit(1); + } + + // Read into it, reallocating as necessary + size_t have_read = 0; + while (1) { + ssize_t n = read(fd, buf + have_read, len - have_read); + have_read += n; + if (n == 0) break; + if (n == -1 && errno != EINTR) { + perror("Error in read wrapper:\nread"); + exit(1); + } + if (n == -1 && errno == EINTR) continue; + if (have_read == len) { + buf = (char *)realloc(buf, 1 + (len *= 2)); + if (!buf) { + char err_str[] = "Error in read wrapper:\nrealloc() returned NULL.\n"; + size_t err_strlen = strlen(err_str); + write_wrapper(STDERR_FILENO, err_str, err_strlen); + exit(1); + } + } + } + + *ret = buf; + *ret_len = have_read; +} + +// On Linux, it can happen that the system has a newer libstdc++ than the one we ship, +// which can break loading of some system libraries: . + +// Return the path to the libstdcxx to load. +// If the path is found, return it. +// Otherwise, print the error and exit. +// The path returned must be freed. +static char *libstdcxxprobe(void) +{ + // Create the pipe and child process. + int fork_pipe[2]; + int ret = pipe(fork_pipe); + if (ret == -1) { + perror("Error during libstdcxxprobe:\npipe"); + exit(1); + } + pid_t pid = fork(); + if (pid == -1) { + perror("Error during libstdcxxprobe:\nfork"); + exit(1); + } + if (pid == (pid_t) 0) { // Child process. + ret = close(fork_pipe[0]); + if (ret == -1) { + perror("Error during libstdcxxprobe in child process:\nclose"); + _exit(1); + } + + // Open the first available libstdc++.so. + // If it can't be found, report so by exiting zero. + void *handle = dlopen("libstdc++.so.6", RTLD_LAZY); + if (!handle) { + _exit(0); + } + + // See if the version is compatible + char *dlerr = dlerror(); // clear out dlerror + void *sym = dlsym(handle, GLIBCXX_LEAST_VERSION_SYMBOL); + dlerr = dlerror(); + if (dlerr) { + // We can't use the library that was found, so don't write anything. + // The main process will see that nothing was written, + // then exit the function and return null. + _exit(0); + } + + // No error means the symbol was found, we can use this library. + // Get the path to it, and write it to the parent process. + struct link_map *lm; + ret = dlinfo(handle, RTLD_DI_LINKMAP, &lm); + if (ret == -1) { + char errbuf[2048]; + int err_len = snprintf(errbuf, 2048, "Error during libstdcxxprobe in child process:\ndlinfo() - %s\n", dlerror()); + write_wrapper(STDOUT_FILENO, errbuf, (size_t)err_len); + _exit(1); + } + char *libpath = lm->l_name; + write_wrapper(fork_pipe[1], libpath, strlen(libpath)); + _exit(0); + } + else { // Parent process. + ret = close(fork_pipe[1]); + if (ret == -1) { + perror("Error during libstdcxxprobe in parent process:\nclose"); + _exit(1); + } + + // Wait for the child to complete. + while (1) { + int wstatus; + pid_t npid = waitpid(pid, &wstatus, 0); + if (npid == -1) { + if (errno == EINTR) continue; + if (errno != EINTR) { + perror("Error during libstdcxxprobe in parent process:\nwaitpid"); + exit(1); + } + } + else if (!WIFEXITED(wstatus)) { + char err_str[] = "Error during libstdcxxprobe in parent process:\n" + "The child process did not exit normally.\n"; + size_t err_strlen = strlen(err_str); + write_wrapper(STDERR_FILENO, err_str, err_strlen); + exit(1); + } + else if (WEXITSTATUS(wstatus)) { + // The child has printed an error and exited, so the parent should exit too. + exit(1); + } + break; + } + + // Read the absolute path to the lib from the child process. + char *path; + size_t pathlen; + read_wrapper(fork_pipe[0], &path, &pathlen); + + // Close the read end of the pipe + ret = close(fork_pipe[0]); + if (ret == -1) { + perror("Error during libstdcxxprobe in parent process:\nclose"); + _exit(1); + } + + if (!pathlen) { + free(path); + return NULL; + } + return path; + } +} +#endif + void * libjulia_internal = NULL; __attribute__((constructor)) void jl_load_libjulia_internal(void) { // Only initialize this once @@ -161,11 +335,46 @@ __attribute__((constructor)) void jl_load_libjulia_internal(void) { } // Introspect to find our own path - const char * lib_dir = jl_get_libdir(); + const char *lib_dir = jl_get_libdir(); // Pre-load libraries that libjulia-internal needs. int deps_len = strlen(dep_libs); - char * curr_dep = &dep_libs[0]; + char *curr_dep = &dep_libs[0]; + + void *cxx_handle; + int done_probe = 0; +#if defined(_OS_LINUX_) + int do_probe = 1; + char *probevar = getenv("JULIA_PROBE_LIBSTDCXX"); + if (probevar) { + if (strcmp(probevar, "1") == 0 || strcmp(probevar, "yes")) + do_probe = 1; + else if (strcmp(probevar, "0") == 0 || strcmp(probevar, "no")) + do_probe = 0; + } + if (do_probe) { + char *cxxpath = libstdcxxprobe(); + if (cxxpath) { + cxx_handle = dlopen(cxxpath, RTLD_LAZY); + char *dlr = dlerror(); + if (dlr) { + size_t buflen = strlen(cxxpath) + 2048; + char *errbuf = malloc(buflen); + int err_len = snprintf(errbuf, buflen, "Error, cannot load \"%s\"\n" + "dlinfo() - %s\n", cxxpath, dlr); + write_wrapper(STDOUT_FILENO, errbuf, (size_t)err_len); + free(errbuf); + exit(1); + } + free(cxxpath); + done_probe = 1; + } + } +#endif + // If not on linux, or the probe does not finish successfully, load the bundled version. + if (!done_probe) { + load_library("libstdc++.so", jl_get_libdir(), 1); + } // We keep track of "special" libraries names (ones whose name is prefixed with `@`) // which are libraries that we want to load in some special, custom way, such as @@ -189,7 +398,8 @@ __attribute__((constructor)) void jl_load_libjulia_internal(void) { } special_library_names[special_idx] = curr_dep + 1; special_idx += 1; - } else { + } + else { load_library(curr_dep, lib_dir, 1); } @@ -278,7 +488,7 @@ JL_DLLEXPORT int jl_load_repl(int argc, char * argv[]) { } #ifdef _OS_WINDOWS_ -int __stdcall DllMainCRTStartup(void* instance, unsigned reason, void* reserved) { +int __stdcall DllMainCRTStartup(void *instance, unsigned reason, void *reserved) { setup_stdio(); // Because we override DllMainCRTStartup, we have to manually call our constructor methods diff --git a/deps/csl.mk b/deps/csl.mk index e3f84aa98974d..ff500620d18fd 100644 --- a/deps/csl.mk +++ b/deps/csl.mk @@ -22,7 +22,6 @@ endef # would get from CSL (by searching for a `GLIBCXX_3.4.X` symbol that does not exist # in our CSL, but would in a newer one), and default to `USE_BINARYBUILDER_CSL=0` in # this case. -CSL_NEXT_GLIBCXX_VERSION=GLIBCXX_3\.4\.30|GLIBCXX_3\.5\.|GLIBCXX_4\. # First, check to see if BB is disabled on a global setting ifeq ($(USE_BINARYBUILDER),0)