Using a New Libc on an Old Linux Distro with Patchelf

Recently, I’ve been wanting to install the Helix editor onto a shared linux server, running Debian stretch. Since I don’t have root access, it seems easiest to download a binary release.

[~/opt] wget https://github.com/helix-editor/helix/releases/download/24.07/helix-24.07-x86_64-linux.tar.xz
[~/opt] tar xf helix-24.07-x86_64-linux.tar.xz
[~/opt] mv helix-24.07-x86_64-linux helix

Easy enough. Now let’s run it.

[~/opt] ./helix/hx
./helix/hx: /lib/x86_64-linux-gnu/libm.so.6: version `GLIBC_2.29' not found (required by ./helix/hx)
./helix/hx: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.32' not found (required by ./helix/hx)
./helix/hx: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.25' not found (required by ./helix/hx)
./helix/hx: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.29' not found (required by ./helix/hx)
./helix/hx: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.33' not found (required by ./helix/hx)
./helix/hx: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.28' not found (required by ./helix/hx)
./helix/hx: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found (required by ./helix/hx)

Oh. Searching around, it looks like Debian Stretch was released in 2017, and reached End of Life in 2022. It’s using glibc version 2.24, from 2016.

Since updating the system glibc is “not recommended”, and we don’t have root access anyway, we have a few options:

  • Getting the server administrator to update the operating system.
  • See if an older release supports glibc 2.24
  • Compile the program on this machine, linking it to our older glibc
  • Get a current copy of libc, and try to run Helix with it

An OS update would be nice, but will not be happening. I’d like to use the latest version of Helix, and there’s no guarantee that an older release will accept my glibc. That leaves re-compiling, and running the existing binary with a new libc.

We could definitely compile, but it will take a while, and we’d have to install the rust toolchain, and I just don’t want to. And if we want to run a closed-source program, we won’t be able to re-compile it for our libc version.

So, let’s get a new libc, and convince the Helix binary to use it.

Getting a new libc

We could definitely get the glibc source and compile it, but once again, I don’t want to. Luckily, the Debian maintainers have provided us with a pre-compiled version.

That page has a link to a .deb package with a recent libc. We definitely don’t want to try and install the package system-wide, but if we unpack it, we can copy out its binaries.

[~] mkdir ~/libc_2.39
[~] cd ~/libc_2.39
[~/libc_2.39] wget http://ftp.us.debian.org/debian/pool/main/g/glibc/libc6_2.39-6_amd64.deb
[~/libc_2.39] dpkg-deb --raw-extract libc6_2.39-6_amd64.deb tmp
[~/libc_2.39] cp tmp/usr/lib/x86_64-linux-gnu ./ -r
[~/libc_2.39] cp tmp/usr/lib64/ld-linux-x86-64.so.2 ./
[~/libc_2.39] rm -r tmp libc6_2.39-6_amd64.deb
[~/libc_2.39] ls
ld-linux-x86-64.so.2  x86_64-linux-gnu

Done.

Running Helix with our new libc

To get Helix to stop complaining, we need to do two separate things:

  • Tell it to use our new shared libraries – libc.so.6, libm.so.6, etc.
  • Tell it to use our new loader – ld-linux-x86-64.so.2

We can set the shared library locations with the LD_PRELOAD environment variable. Unfortunately, that won’t work for the loader, but we can call the loader directly, and pass the Helix program to it.

[~/opt/helix] LD_PRELOAD="$HOME/libc/x86_64-linux-gnu/libc.so.6 $HOME/libc/x86_64-linux-gnu/libm.so.6" ~/libc_2.39/ld-linux-x86-64.so.2 ./hx

This works, but it would be nice to be able to run ./hx normally.

Patching the Helix binary to use our new libc

Instead of calling the loader and setting LD_PRELOAD, we can use the patchelf program patch the Helix binary, and point it at the new libc.

Let’s get a copy.

[~/opt/patchelf] wget https://github.com/NixOS/patchelf/releases/download/0.18.0/patchelf-0.18.0-x86_64.tar.gz
[~/opt/patchelf] tar xf patchelf-0.18.0-x86_64.tar.gz
[~/opt/patchelf] cd bin/

Now, we can use --set-interpreter to choose the loader location, and --set-rpath, to tell Helix where to look for the shared libc libraries.

Note that currently, due to a patchelf bug, --set-interpreter should be run in a separate call, before --set-rpath.

[~/opt/patchelf/bin] ./patchelf ~/opt/helix/hx --set-interpreter ~/libc_2.39/ld-linux-x86-64.so.2
[~/opt/patchelf/bin] ./patchelf ~/opt/helix/hx --set-rpath ~/libc_2.39

Ok, now let’s go ahead and run Helix.

[~/opt/patchelf/bin] ~/opt/helix/hx
~/opt/helix/hx: /lib/x86_64-linux-gnu/libm.so.6: version `GLIBC_2.29' not found (required by ~/opt/helix/hx)
~/opt/helix/hx: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.32' not found (required by ~/opt/helix/hx)
~/opt/helix/hx: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.25' not found (required by ~/opt/helix/hx)
~/opt/helix/hx: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.29' not found (required by ~/opt/helix/hx)
~/opt/helix/hx: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.33' not found (required by ~/opt/helix/hx)
~/opt/helix/hx: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.28' not found (required by ~/opt/helix/hx)
~/opt/helix/hx: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found (required by ~/opt/helix/hx)

That’s confusing. Why is it using /lib/x86_64-linux-gn/libc.so.6, when we just told it to use the new one? Did the rpath not get set?

[~/opt/patchelf/bin] ./patchelf --print-interpreter ~/opt/helix/hx
~/libc_2.39/ld-linux-x86-64.so.2
[~/opt/patchelf/bin] ./patchelf --print-rpath ~/opt/helix/hx
~/libc_2.39/x86_64-linux-gnu/

No, they were both set correctly. Maybe the manpages can help?

man patchelf

...
--set-rpath RUNPATH
     Change the DT_RUNPATH of the executable or library to RUNPATH.
...
man ld.so

...
If a shared object dependency does not contain a slash, then it is searched for in the following order:

o  Using the directories specified in the DT_RPATH dynamic section attribute of the binary if  present  and  DT_RUNPATH  attribute
   does not exist.  Use of DT_RPATH is deprecated.

o  Using  the  environment  variable LD_LIBRARY_PATH (unless the executable is being run in secure-execution mode; see below).  in
   which case it is ignored.

o  Using the directories specified in the DT_RUNPATH dynamic section attribute of the binary if present.

o  From the cache file /etc/ld.so.cache, which contains a compiled list of candidate shared objects previously found in  the  aug‐
   mented  library  path.   If,  however,  the binary was linked with the -z nodeflib linker option, shared objects in the default
   paths are skipped.  Shared objects installed in hardware capability directories (see  below)  are  preferred  to  other  shared
   objects.

o  In  the  default  path /lib, and then /usr/lib.  (On some 64-bit architectures, the default paths for 64-bit shared objects are
   /lib64, and then /usr/lib64.)  If the binary was linked with the -z nodeflib linker option, this step is skipped.
...

So if libc.so.6 isn’t found in DT_RUNPATH (the rpath), the loader will default to the libc in /lib. Which is exactly what we don’t want. But why isn’t it finding the libc in ~/libc_2.39?

Looking in the ~/libc_2.39 we created, libc.so.6 is in a sub-directory. Maybe the loader doesn’t check sub-directories. Let’s try setting the rpath to that sub-folder.

[~/opt/patchelf/bin] ./patchelf ~/opt/helix/hx --set-rpath ~/libc_2.39/x86_64-linux-gnu
[~] ~/opt/helix/hx

It works! According to the helix docs, we just need to copy the runtime folder from ~/opt/helix into ~/.config/helix, and we’re good to go.