Skip to content

Commit

Permalink
Add workaround for musl lack of SONAME support
Browse files Browse the repository at this point in the history
Because `musl` doesn't pay attention to SONAMEs, we cannot load JLLs
with transient dependencies because `musl` always tries to do an
environment search for dependencies, even if they're already loaded.  We
work around this by manually altering the `dso` structures that `musl`
maintains for each loaded library, making it look like the library was
loaded by a previous environment search instead of by loading directly
via full path.
  • Loading branch information
staticfloat committed Aug 17, 2021
1 parent dd045e0 commit 8ddec8b
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/JLLWrappers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ end

if VERSION >= v"1.6.0-DEV"
using Preferences
using Base.BinaryPlatforms
end

# We need to glue expressions together a lot
Expand Down
5 changes: 4 additions & 1 deletion src/products/library_generators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ macro init_library_product(product_name, product_path, dlopen_flags)
# of `ccall` with its path/SONAME will find this path immediately.
# dlopen_flags === nothing means to not dlopen the library.
if $(dlopen_flags) !== nothing
global $(handle_name) = dlopen($(path_name), $(dlopen_flags))
global $(handle_name) = Base.invokelatest(
JLLWrappers.musl_soname_workaround,
dlopen($(path_name), $(dlopen_flags))
)
push!(LIBPATH_list, dirname($(path_name)))
end
end,
Expand Down
21 changes: 21 additions & 0 deletions src/runtime.jl
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,24 @@ function get_julia_libpaths()
end
return JULIA_LIBDIRS
end

@static if VERSION >= v"1.6.0" && libc(HostPlatform()) == "musl"
include("runtime_musl_workaround.jl")

"""
musl_soname_workaround(lib_handle::Ptr{Cvoid})
Applies a workaround for musl's lack of SONAME loading by twiddling
some internal datastructures. See `src/runtime_musl_workaround.jl`
for more details. Returns the altered library handle.
"""
musl_soname_workaround(lib_handle::Ptr{Cvoid}) = replace_musl_shortname(lib_handle)
else
"""
musl_soname_workaround(lib_handle::Ptr{Cvoid})
Not applicable on this platform. Returns the altered library handle.
"""
musl_soname_workaround(lib_handle::Ptr{Cvoid}) = lib_handle
end

147 changes: 147 additions & 0 deletions src/runtime_musl_workaround.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
## JLLWrappers musl SONAME workaround
#
# The problem is detailed in this thread [0], but in short:
#
# JLLs rely on a specific behavior of most `dlopen()` implementations; that if
# a library with the same SONAME will not be loaded twice; e.g. if you first
# load `/a/libfoo.so`, loading `/b/libbar.so` which declares a dependency on
# `libfoo.so` will find the previously-loaded `libfoo.so` without needing to
# search because the SONAME `libbar.so` looks for matches the SONAME of the
# previously-loaded `libfoo.so`. This allows JLLs to store libraries all over
# the place, and directly `dlopen()` all dependencies before any dependents
# would trigger a system-wide search.
#
# Musl does not do this. They do have a mechanism for skipping the directory
# search, but it is only invoked when loading a library without specifying
# the full path [1]. This means that when checking for dependencies, musl
# skips all libraries that were loaded by full path [2]. All that needs to
# happen is that musl needs to record the `shortname` (e.g. SONAME) of all
# libraries, but sadly there's no way to do that if we also want to specify
# the library unambiguously [3,2]. Manipulating the environment to allow for
# non-fully-specified searches to work (e.g. changing `LD_LIBRARY_PATH` then
# invoking `dlopen("libfoo.so")`) won't work, as the environment is only read
# at process initialization. We are therefore backed into a corner and must
# resort to heroic measures: manually inserting an appropriate `shortname`.
#
# [0] https://github.com/JuliaLang/julia/issues/40556
# [1] https://github.com/ifduyue/musl/blob/aad50fcd791e009961621ddfbe3d4c245fd689a3/ldso/dynlink.c#L1163-L1164
# [2] https://github.com/ifduyue/musl/blob/aad50fcd791e009961621ddfbe3d4c245fd689a3/ldso/dynlink.c#L1047-L1052
# [3] https://github.com/ifduyue/musl/blob/aad50fcd791e009961621ddfbe3d4c245fd689a3/ldso/dynlink.c#L1043-L1044


# Use this to ensure the GC doesn't clean up values we insert into musl.
manual_gc_roots = String[]

## We define these structures so that Julia's internal struct padding logic
## can do some arithmetic for us, instead of us needing to do manual offset
## calculation ourselves, which is more error-prone.


# This structure taken from `libc.h`
# https://github.com/ifduyue/musl/blob/aad50fcd791e009961621ddfbe3d4c245fd689a3/src/internal/libc.h#L14-L18
struct musl_tls_module
next::Ptr{musl_tls_module}
image::Ptr{musl_tls_module}
len::Csize_t
size::Csize_t
align::Csize_t
offset::Csize_t
end

# This structure taken from `ldso/dynlink.c`
# https://github.com/ifduyue/musl/blob/aad50fcd791e009961621ddfbe3d4c245fd689a3/ldso/dynlink.c#L53-L107
struct musl_dso
# Things we find mildly interesting
base::Ptr{Cvoid}
name::Ptr{UInt8}

# The wasteland of things we don't care about
dynv::Ptr{Csize_t}
next::Ptr{musl_dso}
prev::Ptr{musl_dso}

phdr::Ptr{Cvoid}
phnum::Cint
phentsize::Csize_t

syms::Ptr{Cvoid}
hashtab::Ptr{Cvoid}
ghashtab::Ptr{Cvoid}
versym::Ptr{Int16}
strings::Ptr{UInt8}
syms_next::Ptr{musl_dso}
lazy_next::Ptr{musl_dso}
lazy::Ptr{Csize_t}
lazy_cnt::Csize_t

map::Ptr{Cuchar}
map_len::Csize_t

# We assume that dev_t and ino_t are always `uint64_t`, even on 32-bit systems.
dev::UInt64
ino::UInt64
relocated::Cchar
constructed::Cchar
kernel_mapped::Cchar
mark::Cchar
bfs_built::Cchar
runtime_loaded::Cchar
# NOTE: struct layout rules should insert two bytes of space here
deps::Ptr{Ptr{musl_dso}}
needed_by::Ptr{musl_dso}
ndeps_direct::Csize_t
next_dep::Csize_t
ctor_visitor::Cint
rpath_orig::Ptr{UInt8}
rpath::Ptr{UInt8}

tls::musl_tls_module
tls_id::Csize_t
relro_start::Csize_t
relro_end::Csize_t
new_dtv::Ptr{Ptr{Cuint}}
new_tls::Ptr{UInt8}
td_index::Ptr{Cvoid}
fini_next::Ptr{musl_dso}

# Finally! The field we're interested in!
shortname::Ptr{UInt8}

# We'll put this stuff at the end because it might be interesting to someone somewhere
loadmap::Ptr{Cvoid}
funcdesc::Ptr{Cvoid}
got::Ptr{Csize_t}
end

function replace_musl_shortname(lib_handle::Ptr{Cvoid})
# First, find the absolute path of the library we're talking about
lib_path = abspath(dlpath(lib_handle))

# Load the DSO object, which conveniently is the handle that `dlopen()`
# itself passes back to us. Check to make sure it's what we expect, by
# inspecting the `name` field. If it's not, something has gone wrong,
# and we should stop before touching anything else.
dso = unsafe_load(Ptr{musl_dso}(lib_handle))
dso_name = abspath(unsafe_string(dso.name))
if dso_name != lib_path
@debug("Unable to synchronize to DSO structure", name=dso_name, path=lib_path)
return lib_handle
end

# If the shortname is not NULL, break out.
if dso.shortname != C_NULL
@debug("shortname != NULL!", ptr=shortname_ptr, value=unsafe_string(shortname_ptr))
return lib_handle
end

# Calculate the offset of `shortname` from the base pointer of the DSO object
shortname_offset = fieldoffset(musl_dso, findfirst(fieldnames(musl_dso) .== :shortname))

# Replace the shortname with the basename of lib_path. Note that, in general, this
# should be the SONAME, but not always. If we wanted to be pedantic, we should
# actually parse out the SONAME of this object. But we don't want to be.
new_shortname = basename(lib_path)
push!(manual_gc_roots, new_shortname)
unsafe_store!(Ptr{Ptr{UInt8}}(lib_handle + shortname_offset), pointer(new_shortname))
return lib_handle
end

0 comments on commit 8ddec8b

Please sign in to comment.