For want of a relative path
Distributing dynamically-linked ELF executables on Linux can be arduous. Some downstream effects of this include:
- Distributing Linux applications in source form.
- Distributing Linux applications as Docker containers.
- Relying on OS package managers to provide pre-built packages.
- The popularity of Go, with its statically-linked ELF executables.
At first, the problem doesn't look arduous: an ELF executable can contain an rpath
or runpath
attribute telling the dynamic linker where to find its shared object dependencies, and if that attribute starts with the magic placeholder $ORIGIN/
, then the dynamic linker will look in the directory containing the executable (or a directory nearby) for its shared object dependencies. For example, if my_executable
depended upon libz.so.1
, and my_executable
had an rpath
or runpath
of $ORIGIN/libs
, then the executable and the library could be distributed using the following directory structure:
my_executable
libs/
libz.so.1
This is great, but it has one limitation: an ELF executable also contains an attribute telling the kernel where to find the dynamic linker, and that attribute has to be an absolute path (or a path relative to the current working directory); it cannot be a path relative to the executable. On contemporary x86-64 systems, that absolute path tends to be /lib64/ld-linux-x86-64.so.2
. This forces ELF executables to use whatever the system provides at /lib64/ld-linux-x86-64.so.2
, which is typically version N of glibc's dynamic linker, for some N. In turn, this forces the ELF executable to use version N of the rest of glibc (libc.so.6
, libm.so.6
, libpthread.so.0
, etc).
Continuing the example, it is likely that my_executable
and libz.so.1
were built against some version M of glibc. If M ≤ N, then everything will work fine, but problems often crop up when M > N. One commonly touted solution is to set up a build environment with a very old version M of glibc, build my_executable
and libz.so.1
in that environment, and then distribute them and hope for M ≤ N.
The polyfill-glibc project presents another possible solution: build my_executable
and libz.so.1
against whatever version of glibc is convenient, and then run polyfill-glibc --target-glibc=N my_executable libz.so.1
to make them compatible with version N of glibc.
Sometimes we don't want either of these solutions, and what we want is to distribute the required version of glibc along with the executable, as in:
my_executable
libs/
ld-linux-x86-64.so.2
libc.so.6
libz.so.1
We can get close to this by adding a launcher script:
launch_my_executable
my_executable
libs/
ld-linux-x86-64.so.2
libc.so.6
libz.so.1
Where launch_my_executable
is something like:
#!/usr/bin/env bash
ORIGIN="$(dirname "$(readlink -f "$0")")"
exec "$ORIGIN/libs/ld-linux-x86-64.so.2" --library-path "$ORIGIN/libs:$LD_LIBRARY_PATH" "$ORIGIN/my_executable" "$@"
This will work most of the time, though comes with caveats:
- Execing the dynamic linker is a slightly obscure feature, which can increase the chance of hitting bugs (e.g. BZ#16381, BZ#24900).
- If
my_executable
tries toopen("/proc/self/exe")
orreadlink("/proc/self/exe")
or similar, it'll getld-linux-x86-64.so.2
rather than itself. - Having a launcher script is aesthetically displeasing.
As an alternative without these caveats, there's an experimental tool in the polyfill-glibc repository called set_relative_interp
. For our running example, the tool would be invoked as:
$ set_relative_interp my_executable libs/ld-linux-x86-64.so.2
After running the tool as above, my_executable
will use $ORIGIN/libs/ld-linux-x86-64.so.2
as its dynamic linker.