Skip to content

Commit 96592e2

Browse files
apaz-cliKristofferC
authored andcommitted
Probe and dlopen() the correct libstdc++ (#46976)
* 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 <[email protected]> Co-authored-by: Elliot Saba <[email protected]> * Addressed review comments. * Change error handling in wrapper functions Co-authored-by: Jameson Nash <[email protected]> * Call write_wrapper three times instead of snprintf Co-authored-by: Jameson Nash <[email protected]> * Apply suggestions from code review Co-authored-by: Jameson Nash <[email protected]> * Update cli/loader_lib.c Co-authored-by: Jameson Nash <[email protected]> * Reordered reading and waiting to avoid a deadlock. * Fixed obvious issues. * Only load libstdc++ preemptively on linux. * Update cli/loader_lib.c Co-authored-by: Jameson Nash <[email protected]> * Update cli/loader_lib.c Co-authored-by: Jameson Nash <[email protected]> * Specified path to bundled libstdc++ on the command line. * Removed whitespace. * Update cli/Makefile Co-authored-by: Jameson Nash <[email protected]> * Handled make install stringreplace. * Correctly quoted stringreplace. * Added -Wl,--enable-new-dtags to prevent DT_RPATH for transitive dependencies * Updated news entry. * Added comment about environment variable. * patched rpath for libgfortran and libLLVM. * Added explaination to Make.inc * Removed trailing space * Removed patchelf for libgfortran, now that BB has been fixed. * Fixed typos and comments Co-authored-by: Max Horn <[email protected]> Co-authored-by: Mosè Giordano <[email protected]> Co-authored-by: Jameson Nash <[email protected]> Co-authored-by: Elliot Saba <[email protected]> Co-authored-by: Max Horn <[email protected]> (cherry picked from commit eb708d6)
1 parent c8b72e2 commit 96592e2

File tree

6 files changed

+254
-21
lines changed

6 files changed

+254
-21
lines changed

Make.inc

+27-6
Original file line numberDiff line numberDiff line change
@@ -1147,6 +1147,29 @@ BB_TRIPLET := $(subst $(SPACE),-,$(filter-out cxx%,$(filter-out libgfortran%,$(s
11471147

11481148
LIBGFORTRAN_VERSION := $(subst libgfortran,,$(filter libgfortran%,$(subst -,$(SPACE),$(BB_TRIPLET_LIBGFORTRAN))))
11491149

1150+
# CSL_NEXT_GLIBCXX_VERSION is a triple of the symbols representing support for whatever
1151+
# the next libstdc++ version would be. This is used for two things.
1152+
# 1. Whether the system libraries are new enough, if we need to use the libs bundled with CSL
1153+
# 2. To know which libstdc++ to load at runtime
1154+
# We want whichever libstdc++ library is newer, because if we don't it can cause problems.
1155+
# While what CSL bundles is quite bleeding-edge compared to what most distros ship, if someone
1156+
# tries to build an older branch of Julia, the version of CSL that ships with it may be
1157+
# relatively old. This is not a problem for code that is built in BB, but when we build Julia
1158+
# with the system compiler, that compiler uses the version of `libstdc++` that it is bundled
1159+
# with, and we can get linker errors when trying to run that `julia` executable with the
1160+
# `libstdc++` that comes from the (now old) BB-built CSL.
1161+
# To fix this, we take note when the system `libstdc++.so` is newer than whatever we
1162+
# would get from CSL (by searching for a `GLIBCXX_X.Y.Z` symbol that does not exist
1163+
# in our CSL, but would in a newer one), and default to `USE_BINARYBUILDER_CSL=0` in
1164+
# this case. This ensures that we link against a version with the symbols required.
1165+
# We also check the system libstdc++ at runtime in the cli loader library, and
1166+
# load it if it contains the version symbol that indicates that it is newer than the one
1167+
# shipped with CSL. Although we do not depend on any of the symbols, it is entirely
1168+
# possible that a user might choose to install a library which depends on symbols provided
1169+
# by a newer libstdc++. Without runtime detection, those libraries would break.
1170+
CSL_NEXT_GLIBCXX_VERSION=GLIBCXX_3\.4\.31|GLIBCXX_3\.5\.|GLIBCXX_4\.
1171+
1172+
11501173
# This is the set of projects that BinaryBuilder dependencies are hooked up for.
11511174
# Note: we explicitly _do not_ define `CSL` here, since it requires some more
11521175
# advanced techniques to decide whether it should be installed from a BB source
@@ -1203,18 +1226,16 @@ ifneq (,$(filter $(OS),WINNT emscripten))
12031226
RPATH :=
12041227
RPATH_ORIGIN :=
12051228
RPATH_ESCAPED_ORIGIN :=
1206-
RPATH_LIB :=
12071229
else ifeq ($(OS), Darwin)
12081230
RPATH := -Wl,-rpath,'@executable_path/$(build_libdir_rel)'
12091231
RPATH_ORIGIN := -Wl,-rpath,'@loader_path/'
12101232
RPATH_ESCAPED_ORIGIN := $(RPATH_ORIGIN)
1211-
RPATH_LIB := -Wl,-rpath,'@loader_path/'
12121233
else
1213-
RPATH := -Wl,-rpath,'$$ORIGIN/$(build_libdir_rel)' -Wl,-rpath,'$$ORIGIN/$(build_private_libdir_rel)' -Wl,-rpath-link,$(build_shlibdir) -Wl,-z,origin
1214-
RPATH_ORIGIN := -Wl,-rpath,'$$ORIGIN' -Wl,-z,origin
1215-
RPATH_ESCAPED_ORIGIN := -Wl,-rpath,'\$$\$$ORIGIN' -Wl,-z,origin -Wl,-rpath-link,$(build_shlibdir)
1216-
RPATH_LIB := -Wl,-rpath,'$$ORIGIN/' -Wl,-z,origin
1234+
RPATH := -Wl,-rpath,'$$ORIGIN/$(build_libdir_rel)' -Wl,-rpath,'$$ORIGIN/$(build_private_libdir_rel)' -Wl,-rpath-link,$(build_shlibdir) -Wl,-z,origin -Wl,--enable-new-dtags
1235+
RPATH_ORIGIN := -Wl,-rpath,'$$ORIGIN' -Wl,-z,origin -Wl,--enable-new-dtags
1236+
RPATH_ESCAPED_ORIGIN := -Wl,-rpath,'\$$\$$ORIGIN' -Wl,-z,origin -Wl,-rpath-link,$(build_shlibdir) -Wl,--enable-new-dtags
12171237
endif
1238+
RPATH_LIB := $(RPATH_ORIGIN)
12181239

12191240
# --whole-archive
12201241
ifeq ($(OS), Darwin)

Makefile

+11-1
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ endif
225225
# Note that we disable MSYS2's path munging here, as otherwise
226226
# it replaces our `:`-separated list as a `;`-separated one.
227227
define stringreplace
228-
MSYS2_ARG_CONV_EXCL='*' $(build_depsbindir)/stringreplace $$(strings -t x - $1 | grep $2 | awk '{print $$1;}') $3 255 "$(call cygpath_w,$1)"
228+
MSYS2_ARG_CONV_EXCL='*' $(build_depsbindir)/stringreplace $$(strings -t x - '$1' | grep "$2" | awk '{print $$1;}') "$3" 255 "$(call cygpath_w,$1)"
229229
endef
230230

231231

@@ -374,6 +374,16 @@ ifeq ($(BUNDLE_DEBUG_LIBS),1)
374374
endif
375375
endif
376376

377+
# Fix rpaths for dependencies. This should be fixed in BinaryBuilder later.
378+
ifeq ($(OS), Linux)
379+
-$(PATCHELF) --set-rpath '$$ORIGIN' $(DESTDIR)$(private_shlibdir)/libLLVM.$(SHLIB_EXT)
380+
endif
381+
382+
# Replace libstdc++ path, which is also moving from `lib` to `../lib/julia`.
383+
ifeq ($(OS),Linux)
384+
$(call stringreplace,$(DESTDIR)$(shlibdir)/libjulia.$(JL_MAJOR_MINOR_SHLIB_EXT),\*libstdc++\.so\.6$$,*$(call dep_lib_path,$(shlibdir),$(private_shlibdir)/libstdc++.so.6))
385+
endif
386+
377387

378388
ifneq ($(LOADER_BUILD_DEP_LIBS),$(LOADER_INSTALL_DEP_LIBS))
379389
# Next, overwrite relative path to libjulia-internal in our loader if $$(LOADER_BUILD_DEP_LIBS) != $$(LOADER_INSTALL_DEP_LIBS)

NEWS.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,8 @@ Deprecated or removed
244244

245245
External dependencies
246246
---------------------
247-
247+
* On Linux, now autodetects the system libstdc++ version, and automatically loads the system library if it is newer. The old behavior of loading the bundled libstdc++ regardless of the system version obtained by setting the environment variable `JULIA_PROBE_LIBSTDCXX=0`.
248+
* Removed `RPATH` from the julia binary. On Linux this may break libraries that have failed to set `RUNPATH`.
248249

249250
Tooling Improvements
250251
---------------------

cli/Makefile

+4-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ LOADER_LDFLAGS = $(JLDFLAGS) -ffreestanding -L$(build_shlibdir) -L$(build_libdir
1313

1414
ifeq ($(OS),WINNT)
1515
LOADER_CFLAGS += -municode -mconsole -nostdlib -fno-stack-check -fno-stack-protector -mno-stack-arg-probe
16+
else ifeq ($(OS),Linux)
17+
LOADER_CFLAGS += -DGLIBCXX_LEAST_VERSION_SYMBOL=\"$(shell echo "$(CSL_NEXT_GLIBCXX_VERSION)" | cut -d'|' -f1 | sed 's/\\//g')\"
1618
endif
1719

1820
ifeq ($(OS),WINNT)
@@ -111,7 +113,7 @@ endif
111113

112114
$(build_shlibdir)/libjulia.$(JL_MAJOR_MINOR_SHLIB_EXT): $(LIB_OBJS) $(SRCDIR)/list_strip_symbols.h | $(build_shlibdir) $(build_libdir)
113115
@$(call PRINT_LINK, $(CC) $(call IMPLIB_FLAGS,$@.tmp) $(LOADER_CFLAGS) -DLIBRARY_EXPORTS -shared $(SHIPFLAGS) $(LIB_OBJS) -o $@ \
114-
$(JLIBLDFLAGS) $(LOADER_LDFLAGS) $(RPATH_LIB) $(call SONAME_FLAGS,libjulia.$(JL_MAJOR_SHLIB_EXT)))
116+
$(JLIBLDFLAGS) $(LOADER_LDFLAGS) $(call SONAME_FLAGS,libjulia.$(JL_MAJOR_SHLIB_EXT)))
115117
@$(INSTALL_NAME_CMD)libjulia.$(SHLIB_EXT) $@
116118
ifeq ($(OS), WINNT)
117119
@# Note that if the objcopy command starts getting too long, we can use `@file` to read
@@ -121,7 +123,7 @@ endif
121123

122124
$(build_shlibdir)/libjulia-debug.$(JL_MAJOR_MINOR_SHLIB_EXT): $(LIB_DOBJS) $(SRCDIR)/list_strip_symbols.h | $(build_shlibdir) $(build_libdir)
123125
@$(call PRINT_LINK, $(CC) $(call IMPLIB_FLAGS,$@.tmp) $(LOADER_CFLAGS) -DLIBRARY_EXPORTS -shared $(DEBUGFLAGS) $(LIB_DOBJS) -o $@ \
124-
$(JLIBLDFLAGS) $(LOADER_LDFLAGS) $(RPATH_LIB) $(call SONAME_FLAGS,libjulia-debug.$(JL_MAJOR_SHLIB_EXT)))
126+
$(JLIBLDFLAGS) $(LOADER_LDFLAGS) $(call SONAME_FLAGS,libjulia-debug.$(JL_MAJOR_SHLIB_EXT)))
125127
@$(INSTALL_NAME_CMD)libjulia-debug.$(SHLIB_EXT) $@
126128
ifeq ($(OS), WINNT)
127129
@$(call PRINT_ANALYZE, $(OBJCOPY) $(build_libdir)/$(notdir $@).tmp.a $(STRIP_EXPORTED_FUNCS) $(build_libdir)/$(notdir $@).a && rm $(build_libdir)/$(notdir $@).tmp.a)

cli/loader_lib.c

+207-7
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ extern "C" {
1616
#endif
1717

1818
// Save DEP_LIBS to a variable that is explicitly sized for expansion
19-
static char dep_libs[1024] = DEP_LIBS;
19+
static char dep_libs[1024] = "\0" DEP_LIBS;
2020

2121
JL_DLLEXPORT void jl_loader_print_stderr(const char * msg)
2222
{
@@ -33,7 +33,6 @@ void jl_loader_print_stderr3(const char * msg1, const char * msg2, const char *
3333
/* Wrapper around dlopen(), with extra relative pathing thrown in*/
3434
static void * load_library(const char * rel_path, const char * src_dir, int err) {
3535
void * handle = NULL;
36-
3736
// See if a handle is already open to the basename
3837
const char *basename = rel_path + strlen(rel_path);
3938
while (basename-- > rel_path)
@@ -147,6 +146,174 @@ JL_DLLEXPORT const char * jl_get_libdir()
147146
return lib_dir;
148147
}
149148

149+
// On Linux, it can happen that the system has a newer libstdc++ than the one we ship,
150+
// which can break loading of some system libraries: <https://github.com/JuliaLang/julia/issues/34276>.
151+
// As a fix, on linux we probe the system libstdc++ to see if it is newer, and then load it if it is.
152+
// Otherwise, we load the bundled one. This improves compatibility with third party dynamic libs that
153+
// may depend on symbols exported by the system libstdxc++.
154+
#ifdef _OS_LINUX_
155+
#ifndef GLIBCXX_LEAST_VERSION_SYMBOL
156+
#warning GLIBCXX_LEAST_VERSION_SYMBOL should always be defined in the makefile.
157+
#define GLIBCXX_LEAST_VERSION_SYMBOL "GLIBCXX_a.b.c" /* Appease the linter */
158+
#endif
159+
160+
#include <link.h>
161+
#include <sys/wait.h>
162+
163+
// write(), but handle errors and avoid EINTR
164+
static void write_wrapper(int fd, const char *str, size_t len)
165+
{
166+
size_t written_sofar = 0;
167+
while (len) {
168+
ssize_t bytes_written = write(fd, str + written_sofar, len);
169+
if (bytes_written == -1 && errno == EINTR) continue;
170+
if (bytes_written == -1 && errno != EINTR) {
171+
perror("(julia) child libstdcxxprobe write");
172+
_exit(1);
173+
}
174+
len -= bytes_written;
175+
written_sofar += bytes_written;
176+
}
177+
}
178+
179+
// read(), but handle errors and avoid EINTR
180+
static void read_wrapper(int fd, char **ret, size_t *ret_len)
181+
{
182+
// Allocate an initial buffer
183+
size_t len = JL_PATH_MAX;
184+
char *buf = (char *)malloc(len + 1);
185+
if (!buf) {
186+
perror("(julia) malloc");
187+
exit(1);
188+
}
189+
190+
// Read into it, reallocating as necessary
191+
size_t have_read = 0;
192+
while (1) {
193+
ssize_t n = read(fd, buf + have_read, len - have_read);
194+
have_read += n;
195+
if (n == 0) break;
196+
if (n == -1 && errno != EINTR) {
197+
perror("(julia) libstdcxxprobe read");
198+
exit(1);
199+
}
200+
if (n == -1 && errno == EINTR) continue;
201+
if (have_read == len) {
202+
buf = (char *)realloc(buf, 1 + (len *= 2));
203+
if (!buf) {
204+
perror("(julia) realloc");
205+
exit(1);
206+
}
207+
}
208+
}
209+
210+
*ret = buf;
211+
*ret_len = have_read;
212+
}
213+
214+
// Return the path to the libstdcxx to load.
215+
// If the path is found, return it.
216+
// Otherwise, print the error and exit.
217+
// The path returned must be freed.
218+
static char *libstdcxxprobe(void)
219+
{
220+
// Create the pipe and child process.
221+
int fork_pipe[2];
222+
int ret = pipe(fork_pipe);
223+
if (ret == -1) {
224+
perror("(julia) Error during libstdcxxprobe: pipe");
225+
exit(1);
226+
}
227+
pid_t pid = fork();
228+
if (pid == -1) {
229+
perror("Error during libstdcxxprobe:\nfork");
230+
exit(1);
231+
}
232+
if (pid == (pid_t) 0) { // Child process.
233+
close(fork_pipe[0]);
234+
235+
// Open the first available libstdc++.so.
236+
// If it can't be found, report so by exiting zero.
237+
// The star is there to prevent the compiler from merging constants
238+
// with "\0*libstdc++.so.6", which we string replace inside the .so during
239+
// make install.
240+
void *handle = dlopen("libstdc++.so.6\0*", RTLD_LAZY);
241+
if (!handle) {
242+
_exit(0);
243+
}
244+
245+
// See if the version is compatible
246+
char *dlerr = dlerror(); // clear out dlerror
247+
void *sym = dlsym(handle, GLIBCXX_LEAST_VERSION_SYMBOL);
248+
dlerr = dlerror();
249+
if (dlerr) {
250+
// We can't use the library that was found, so don't write anything.
251+
// The main process will see that nothing was written,
252+
// then exit the function and return null.
253+
_exit(0);
254+
}
255+
256+
// No error means the symbol was found, we can use this library.
257+
// Get the path to it, and write it to the parent process.
258+
struct link_map *lm;
259+
ret = dlinfo(handle, RTLD_DI_LINKMAP, &lm);
260+
if (ret == -1) {
261+
char *errbuf = dlerror();
262+
char *errdesc = (char*)"Error during libstdcxxprobe in child process:\ndlinfo: ";
263+
write_wrapper(STDERR_FILENO, errdesc, strlen(errdesc));
264+
write_wrapper(STDERR_FILENO, errbuf, strlen(errbuf));
265+
write_wrapper(STDERR_FILENO, "\n", 1);
266+
_exit(1);
267+
}
268+
char *libpath = lm->l_name;
269+
write_wrapper(fork_pipe[1], libpath, strlen(libpath));
270+
_exit(0);
271+
}
272+
else { // Parent process.
273+
close(fork_pipe[1]);
274+
275+
// Read the absolute path to the lib from the child process.
276+
char *path;
277+
size_t pathlen;
278+
read_wrapper(fork_pipe[0], &path, &pathlen);
279+
280+
// Close the read end of the pipe
281+
close(fork_pipe[0]);
282+
283+
// Wait for the child to complete.
284+
while (1) {
285+
int wstatus;
286+
pid_t npid = waitpid(pid, &wstatus, 0);
287+
if (npid == -1) {
288+
if (errno == EINTR) continue;
289+
if (errno != EINTR) {
290+
perror("Error during libstdcxxprobe in parent process:\nwaitpid");
291+
exit(1);
292+
}
293+
}
294+
else if (!WIFEXITED(wstatus)) {
295+
const char *err_str = "Error during libstdcxxprobe in parent process:\n"
296+
"The child process did not exit normally.\n";
297+
size_t err_strlen = strlen(err_str);
298+
write_wrapper(STDERR_FILENO, err_str, err_strlen);
299+
exit(1);
300+
}
301+
else if (WEXITSTATUS(wstatus)) {
302+
// The child has printed an error and exited, so the parent should exit too.
303+
exit(1);
304+
}
305+
break;
306+
}
307+
308+
if (!pathlen) {
309+
free(path);
310+
return NULL;
311+
}
312+
return path;
313+
}
314+
}
315+
#endif
316+
150317
void * libjulia_internal = NULL;
151318
__attribute__((constructor)) void jl_load_libjulia_internal(void) {
152319
// Only initialize this once
@@ -155,11 +322,43 @@ __attribute__((constructor)) void jl_load_libjulia_internal(void) {
155322
}
156323

157324
// Introspect to find our own path
158-
const char * lib_dir = jl_get_libdir();
325+
const char *lib_dir = jl_get_libdir();
159326

160327
// Pre-load libraries that libjulia-internal needs.
161-
int deps_len = strlen(dep_libs);
162-
char * curr_dep = &dep_libs[0];
328+
int deps_len = strlen(&dep_libs[1]);
329+
char *curr_dep = &dep_libs[1];
330+
331+
void *cxx_handle;
332+
333+
#if defined(_OS_LINUX_)
334+
int do_probe = 1;
335+
int done_probe = 0;
336+
char *probevar = getenv("JULIA_PROBE_LIBSTDCXX");
337+
if (probevar) {
338+
if (strcmp(probevar, "1") == 0 || strcmp(probevar, "yes") == 0)
339+
do_probe = 1;
340+
else if (strcmp(probevar, "0") == 0 || strcmp(probevar, "no") == 0)
341+
do_probe = 0;
342+
}
343+
if (do_probe) {
344+
char *cxxpath = libstdcxxprobe();
345+
if (cxxpath) {
346+
cxx_handle = dlopen(cxxpath, RTLD_LAZY);
347+
char *dlr = dlerror();
348+
if (dlr) {
349+
jl_loader_print_stderr("ERROR: Unable to dlopen(cxxpath) in parent!\n");
350+
jl_loader_print_stderr3("Message: ", dlr, "\n");
351+
exit(1);
352+
}
353+
free(cxxpath);
354+
done_probe = 1;
355+
}
356+
}
357+
if (!done_probe) {
358+
const static char bundled_path[256] = "\0*libstdc++.so.6";
359+
load_library(&bundled_path[2], lib_dir, 1);
360+
}
361+
#endif
163362

164363
// We keep track of "special" libraries names (ones whose name is prefixed with `@`)
165364
// which are libraries that we want to load in some special, custom way, such as
@@ -183,7 +382,8 @@ __attribute__((constructor)) void jl_load_libjulia_internal(void) {
183382
}
184383
special_library_names[special_idx] = curr_dep + 1;
185384
special_idx += 1;
186-
} else {
385+
}
386+
else {
187387
load_library(curr_dep, lib_dir, 1);
188388
}
189389

@@ -272,7 +472,7 @@ JL_DLLEXPORT int jl_load_repl(int argc, char * argv[]) {
272472
}
273473

274474
#ifdef _OS_WINDOWS_
275-
int __stdcall DllMainCRTStartup(void* instance, unsigned reason, void* reserved) {
475+
int __stdcall DllMainCRTStartup(void *instance, unsigned reason, void *reserved) {
276476
setup_stdio();
277477

278478
// Because we override DllMainCRTStartup, we have to manually call our constructor methods

deps/csl.mk

+3-4
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,16 @@ endef
1212

1313
# CSL bundles lots of system compiler libraries, and while it is quite bleeding-edge
1414
# as compared to what most distros ship, if someone tries to build an older branch,
15-
# the version of CSL that ships with that branch may become relatively old. This is
16-
# not a problem for code that is built in BB, but when we build Julia with the system
15+
# the version of CSL that ships with that branch may be relatively old. This is not
16+
# a problem for code that is built in BB, but when we build Julia with the system
1717
# compiler, that compiler uses the version of `libstdc++` that it is bundled with,
18-
# and we can get linker errors when trying to run that `julia` executable with the
18+
# and we can get linker errors when trying to run that `julia` executable with the
1919
# `libstdc++` that comes from the (now old) BB-built CSL.
2020
#
2121
# To fix this, we take note when the system `libstdc++.so` is newer than whatever we
2222
# would get from CSL (by searching for a `GLIBCXX_3.4.X` symbol that does not exist
2323
# in our CSL, but would in a newer one), and default to `USE_BINARYBUILDER_CSL=0` in
2424
# this case.
25-
CSL_NEXT_GLIBCXX_VERSION=GLIBCXX_3\.4\.31|GLIBCXX_3\.5\.|GLIBCXX_4\.
2625

2726
# First, check to see if BB is disabled on a global setting
2827
ifeq ($(USE_BINARYBUILDER),0)

0 commit comments

Comments
 (0)