Skip to content

Commit eb708d6

Browse files
apaz-cligiordanovtjnashstaticfloatfingolfin
authored
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 <vtjnash@gmail.com> Co-authored-by: Elliot Saba <staticfloat@gmail.com> * Addressed review comments. * Change error handling in wrapper functions Co-authored-by: Jameson Nash <vtjnash@gmail.com> * Call write_wrapper three times instead of snprintf Co-authored-by: Jameson Nash <vtjnash@gmail.com> * Apply suggestions from code review Co-authored-by: Jameson Nash <vtjnash@gmail.com> * Update cli/loader_lib.c Co-authored-by: Jameson Nash <vtjnash@gmail.com> * 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 <vtjnash@gmail.com> * Update cli/loader_lib.c Co-authored-by: Jameson Nash <vtjnash@gmail.com> * Specified path to bundled libstdc++ on the command line. * Removed whitespace. * Update cli/Makefile Co-authored-by: Jameson Nash <vtjnash@gmail.com> * 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 <max@quendi.de> Co-authored-by: Mosè Giordano <mose@gnu.org> Co-authored-by: Jameson Nash <vtjnash@gmail.com> Co-authored-by: Elliot Saba <staticfloat@gmail.com> Co-authored-by: Max Horn <max@quendi.de>
1 parent f7d4edc commit eb708d6

File tree

6 files changed

+254
-21
lines changed

6 files changed

+254
-21
lines changed

Make.inc

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,6 +1149,29 @@ BB_TRIPLET := $(subst $(SPACE),-,$(filter-out cxx%,$(filter-out libgfortran%,$(s
11491149

11501150
LIBGFORTRAN_VERSION := $(subst libgfortran,,$(filter libgfortran%,$(subst -,$(SPACE),$(BB_TRIPLET_LIBGFORTRAN))))
11511151

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

12211242
# --whole-archive
12221243
ifeq ($(OS), Darwin)

Makefile

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ endif
232232
# Note that we disable MSYS2's path munging here, as otherwise
233233
# it replaces our `:`-separated list as a `;`-separated one.
234234
define stringreplace
235-
MSYS2_ARG_CONV_EXCL='*' $(build_depsbindir)/stringreplace $$(strings -t x - $1 | grep $2 | awk '{print $$1;}') $3 255 "$(call cygpath_w,$1)"
235+
MSYS2_ARG_CONV_EXCL='*' $(build_depsbindir)/stringreplace $$(strings -t x - '$1' | grep "$2" | awk '{print $$1;}') "$3" 255 "$(call cygpath_w,$1)"
236236
endef
237237

238238

@@ -382,6 +382,16 @@ else ifeq ($(JULIA_BUILD_MODE),debug)
382382
endif
383383
endif
384384

385+
# Fix rpaths for dependencies. This should be fixed in BinaryBuilder later.
386+
ifeq ($(OS), Linux)
387+
-$(PATCHELF) --set-rpath '$$ORIGIN' $(DESTDIR)$(private_shlibdir)/libLLVM.$(SHLIB_EXT)
388+
endif
389+
390+
# Replace libstdc++ path, which is also moving from `lib` to `../lib/julia`.
391+
ifeq ($(OS),Linux)
392+
$(call stringreplace,$(DESTDIR)$(shlibdir)/libjulia.$(JL_MAJOR_MINOR_SHLIB_EXT),\*libstdc++\.so\.6$$,*$(call dep_lib_path,$(shlibdir),$(private_shlibdir)/libstdc++.so.6))
393+
endif
394+
385395

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

NEWS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,8 @@ Deprecated or removed
187187

188188
External dependencies
189189
---------------------
190-
190+
* 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`.
191+
* Removed `RPATH` from the julia binary. On Linux this may break libraries that have failed to set `RUNPATH`.
191192

192193
Tooling Improvements
193194
---------------------

cli/Makefile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ LOADER_LDFLAGS = $(JLDFLAGS) -ffreestanding -L$(build_shlibdir) -L$(build_libdir
1212

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

1719
ifeq ($(OS),WINNT)
@@ -110,7 +112,7 @@ endif
110112

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

121123
$(build_shlibdir)/libjulia-debug.$(JL_MAJOR_MINOR_SHLIB_EXT): $(LIB_DOBJS) $(SRCDIR)/list_strip_symbols.h | $(build_shlibdir) $(build_libdir)
122124
@$(call PRINT_LINK, $(CC) $(call IMPLIB_FLAGS,$@.tmp) $(LOADER_CFLAGS) -DLIBRARY_EXPORTS -shared $(DEBUGFLAGS) $(LIB_DOBJS) -o $@ \
123-
$(JLIBLDFLAGS) $(LOADER_LDFLAGS) $(RPATH_LIB) $(call SONAME_FLAGS,libjulia-debug.$(JL_MAJOR_SHLIB_EXT)))
125+
$(JLIBLDFLAGS) $(LOADER_LDFLAGS) $(call SONAME_FLAGS,libjulia-debug.$(JL_MAJOR_SHLIB_EXT)))
124126
@$(INSTALL_NAME_CMD)libjulia-debug.$(SHLIB_EXT) $@
125127
ifeq ($(OS), WINNT)
126128
@$(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

Lines changed: 207 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ static int win_file_exists(wchar_t* wpath) {
2121
#endif
2222

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

2626
JL_DLLEXPORT void jl_loader_print_stderr(const char * msg)
2727
{
@@ -45,7 +45,6 @@ void jl_loader_print_stderr3(const char * msg1, const char * msg2, const char *
4545
* and abort the process. */
4646
static void * load_library(const char * rel_path, const char * src_dir, int err) {
4747
void * handle = NULL;
48-
4948
// See if a handle is already open to the basename
5049
const char *basename = rel_path + strlen(rel_path);
5150
while (basename-- > rel_path)
@@ -167,6 +166,174 @@ JL_DLLEXPORT const char * jl_get_libdir()
167166
return lib_dir;
168167
}
169168

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

177344
// Introspect to find our own path
178-
const char * lib_dir = jl_get_libdir();
345+
const char *lib_dir = jl_get_libdir();
179346

180347
// Pre-load libraries that libjulia-internal needs.
181-
int deps_len = strlen(dep_libs);
182-
char * curr_dep = &dep_libs[0];
348+
int deps_len = strlen(&dep_libs[1]);
349+
char *curr_dep = &dep_libs[1];
350+
351+
void *cxx_handle;
352+
353+
#if defined(_OS_LINUX_)
354+
int do_probe = 1;
355+
int done_probe = 0;
356+
char *probevar = getenv("JULIA_PROBE_LIBSTDCXX");
357+
if (probevar) {
358+
if (strcmp(probevar, "1") == 0 || strcmp(probevar, "yes") == 0)
359+
do_probe = 1;
360+
else if (strcmp(probevar, "0") == 0 || strcmp(probevar, "no") == 0)
361+
do_probe = 0;
362+
}
363+
if (do_probe) {
364+
char *cxxpath = libstdcxxprobe();
365+
if (cxxpath) {
366+
cxx_handle = dlopen(cxxpath, RTLD_LAZY);
367+
char *dlr = dlerror();
368+
if (dlr) {
369+
jl_loader_print_stderr("ERROR: Unable to dlopen(cxxpath) in parent!\n");
370+
jl_loader_print_stderr3("Message: ", dlr, "\n");
371+
exit(1);
372+
}
373+
free(cxxpath);
374+
done_probe = 1;
375+
}
376+
}
377+
if (!done_probe) {
378+
const static char bundled_path[256] = "\0*libstdc++.so.6";
379+
load_library(&bundled_path[2], lib_dir, 1);
380+
}
381+
#endif
183382

184383
// We keep track of "special" libraries names (ones whose name is prefixed with `@`)
185384
// which are libraries that we want to load in some special, custom way, such as
@@ -203,7 +402,8 @@ __attribute__((constructor)) void jl_load_libjulia_internal(void) {
203402
}
204403
special_library_names[special_idx] = curr_dep + 1;
205404
special_idx += 1;
206-
} else {
405+
}
406+
else {
207407
load_library(curr_dep, lib_dir, 1);
208408
}
209409

@@ -292,7 +492,7 @@ JL_DLLEXPORT int jl_load_repl(int argc, char * argv[]) {
292492
}
293493

294494
#ifdef _OS_WINDOWS_
295-
int __stdcall DllMainCRTStartup(void* instance, unsigned reason, void* reserved) {
495+
int __stdcall DllMainCRTStartup(void *instance, unsigned reason, void *reserved) {
296496
setup_stdio();
297497

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

deps/csl.mk

Lines changed: 3 additions & 4 deletions
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)