Using glibc trickery to run nodejs
- SUSE Linux Enterprise
- How to reproduce
- Libc versions of SUSE Linux Enterprise
- glibc and the loader
- Updating glibc
- The wrapper script
- Summary
- References
Let’s assume, you need to run a recent nodejs binary on a very old linux system. More specifically, your old linux system is a SUSE Linux Enterprise. The old version 12 is still supported, there is a service pack 5 available, even still this year. Version 12 was released more than 10 years ago…
What happens, if you try to run a recent, more or less statically linked, binary on such an old system? You might experience the following error:
node-v24.6.0-linux-x64/bin/node: /lib64/libm.so.6: version `GLIBC_2.27' not found (required by node-v24.6.0-linux-x64/bin/node)
node-v24.6.0-linux-x64/bin/node: /lib64/libc.so.6: version `GLIBC_2.27' not found (required by node-v24.6.0-linux-x64/bin/node)
node-v24.6.0-linux-x64/bin/node: /lib64/libc.so.6: version `GLIBC_2.28' not found (required by node-v24.6.0-linux-x64/bin/node)
node-v24.6.0-linux-x64/bin/node: /lib64/libc.so.6: version `GLIBC_2.25' not found (required by node-v24.6.0-linux-x64/bin/node)
SUSE Linux Enterprise
This distribution is a commercial one, but it is based on a free distribution by SUSE: openSUSE leap. So, you can use openSUSE leap in the matching version to test and reproduce this yourself.
According to the German wikipedia page SUSE Linux Enterprise Server SLES 12 is based on openSUSE Leap 42. More specifically, SLES 12 SP3 is based on openSUSE Leap 42.3. For the most recent service pack SLES 12 SP5, there doesn’t seem to be an openSUSE Leap equivalent version, but for our tests, 42.3 will do fine. Of course, you shouldn’t use 42.3 in production, as this won’t have any security fixes.
The next generation of SLES (SUSE Linux Enterprise Server) is SLES 15, which is based on openSUSE Leap 15. It’s convenient, that the version numbers now match. The latest version is SLES 15 SP 6 or openSUSE Leap 15.6.
The openSUSE Leap distribution is available on docker.io: https://hub.docker.com/r/opensuse/leap. We can simply start a docker image with the specific version:
podman run -it --rm --pull newer -v $(pwd):/root/node-test docker.io/opensuse/leap:42 bash
Or the newer version:
podman run -it --rm --pull newer -v $(pwd):/root/node-test docker.io/opensuse/leap:15 bash
Note: It bind-mounts the current directory into /root/node-test
, so that we can download nodejs
distributions there and run it later inside the container. These images are very basic and e.g.
don’t provide many packages, e.g. no “curl” or “wget”.
How to reproduce
Let’s download the current version of nodejs from https://nodejs.org/en/download/current as a standalone binary and extract it:
$ mkdir node-test
$ cd node-test
$ curl -O https://nodejs.org/dist/v24.6.0/node-v24.6.0-linux-x64.tar.xz
$ tar xfJv node-v24.6.0-linux-x64.tar.xz
$ node-v24.6.0-linux-x64/bin/node --version
v24.6.0
The last command runs nodejs and prints the version, you should see something like “v24.6.0”.
Now, let’s run this inside Leap 42:
$ podman run -it --rm --pull newer -v $(pwd):/root/node-test docker.io/opensuse/leap:42 /root/node-test/node-v24.6.0-linux-x64/bin/node --version
/root/node-test/node-v24.6.0-linux-x64/bin/node: /lib64/libm.so.6: version `GLIBC_2.27' not found (required by /root/node-test/node-v24.6.0-linux-x64/bin/node)
/root/node-test/node-v24.6.0-linux-x64/bin/node: /lib64/libc.so.6: version `GLIBC_2.27' not found (required by /root/node-test/node-v24.6.0-linux-x64/bin/node)
/root/node-test/node-v24.6.0-linux-x64/bin/node: /lib64/libc.so.6: version `GLIBC_2.28' not found (required by /root/node-test/node-v24.6.0-linux-x64/bin/node)
/root/node-test/node-v24.6.0-linux-x64/bin/node: /lib64/libc.so.6: version `GLIBC_2.25' not found (required by /root/node-test/node-v24.6.0-linux-x64/bin/node)
And voila, you get the errors about “version GLIBC… not found” instead of the version number printed.
However, it works inside Leap 15 (and therefore should work in SLES 15):
$ podman run -it --rm --pull newer -v $(pwd):/root/node-test docker.io/opensuse/leap:15 /root/node-test/node-v24.6.0-linux-x64/bin/node --version
v24.6.0
Libc versions of SUSE Linux Enterprise
So, whats the difference? The “libc” version seems to be too old. Which version the system is running, you can
see by executing ldd --version
:
$ podman run -it --rm --pull newer -v $(pwd):/root/node-test docker.io/opensuse/leap:42 ldd --version
ldd (GNU libc) 2.22
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.
$ podman run -it --rm --pull newer -v $(pwd):/root/node-test docker.io/opensuse/leap:15 ldd --version
ldd (GNU libc) 2.38
Copyright (C) 2023 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.
So, openSUSE Leap 42 (SLES 12) is built with and running with GNU libc 2.22. The newer openSUSE Leap 15 (SLES 15) is instead built with and running with GNU libc 2.38. And nodejs seems to require at least version 2.28.
glibc and the loader
The GNU C library provides the basic functionality for all programs running on Linux. This includes the kernel interface via syscalls. Even a simple hello world program uses it:
#include <stdio.h>
int main() {
printf("Hello world!\n");
return 0;
}
If you compile it, it is linked automatically against libc:
$ gcc -o hello hello.c
$ ./hello
Hello world!
$ ldd hello
linux-vdso.so.1 (0x00007f53b7d57000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f53b7b2d000)
/lib64/ld-linux-x86-64.so.2 (0x00007f53b7d59000)
If you statically link it, the you won’t see it anymore:
$ gcc -static -o hello hello.c
$ ./hello
Hello world!
$ ldd hello
not a dynamic executable
All the used methods are now embedded into the binary instead of dynamically linked in from libc.so.6. This makes the binary bigger, but you don’t need to have the correct libc version available.
When running a program under Linux, there are some environment variables, with which you can control from
where dynamic libraries are loaded. These are e.g. LD_LIBRARY_PATH
. With that, you can kind of “override”
which dynamic/shared libraries are actually used. Usually, the shared libraries from /lib
or /lib64
are
used, but you might have a special program with special needs.
This is documented in the man page for ld.so.
And there are other interesting options: E.g. LD_PRELOAD
, which allows you to basically override specific
methods without changing the original program. So you can inject or wrap specific system calls to e.g.
figure out where some resource leaks coming from.
The man page ld.so documents the loader, which is the part,
that loads a binary executable at the first place and the resolves the symbols finding and loading the shared objects.
And you can call it explicitly via /lib64/ld-linux-x86-64.so.2 ...
:
$ podman run -it --rm --pull newer -v $(pwd):/root/node-test docker.io/opensuse/leap:15 /lib64/ld-linux-x86-64.so.2 /root/node-test/node-v24.6.0-linux-x64/bin/node --version
v24.6.0
We’ll use this technique later on to create a wrapper script for nodejs…
Updating glibc
When a distribution updates to a new glibc version, this usually means that every package needs to be rebuilt, which is a huge effort and is risky. That’s why such updates are usually done in major versions (e.g. SLES 12 vs. SLES 15).
But maybe we can use the glibc version from SLES 15 (Leap 15) back in SLES 12 (Leap 42) and force nodejs to use
it via the environment variable LD_LIBRARY_PATH
. Let’s try.
First step is, to find the package repository of Leap 15, so that we can download the glibc rpm package from there: The repositories urls are listed on the page Package repositories and the URL for Leap 15.6 is: https://download.opensuse.org/distribution/leap/15.6/repo/oss/.
We actually need glibc-2.38-150600.12.1.x86_64.rpm. This saves us from compiling it ourselves. Hopefully, this new version will somehow run on the old Leap 42 environment.
$ curl -LO https://download.opensuse.org/distribution/leap/15.6/repo/oss/x86_64/glibc-2.38-150600.12.1.x86_64.rpm
$ rpm2cpio glibc-2.38-150600.12.1.x86_64.rpm
$ cpio -div -D glibc-2.38 < glibc-2.38-150600.12.1.x86_64.rpm.tar
Now we have it extracted to the local directory ./glibc-2.38/lib64/libc.so.6
(and many more files).
Using LD_LIBRARY_PATH
like so, doesn’t work:
$ podman run -it --rm --pull newer -v $(pwd):/root/node-test \
-e LD_LIBRARY_PATH=/root/node-test/glibc-2.38/lib64 \
docker.io/opensuse/leap:42 /root/node-test/node-v24.6.0-linux-x64/bin/node --version
/root/node-test/node-v24.6.0-linux-x64/bin/node: relocation error: /root/node-test/glibc-2.38/lib64/libc.so.6: symbol _dl_signal_error, version GLIBC_PRIVATE not defined in file ld-linux-x86-64.so.2 with link time reference
Let’s try with LD_PRELOAD
, as it is described on A Solution to Version GLIBC_2.XX Not Found:
$ podman run -it --rm --pull newer -v $(pwd):/root/node-test \
-e LD_PRELOAD=/root/node-test/glibc-2.38/lib64/libc.so.6 \
docker.io/opensuse/leap:42 /root/node-test/node-v24.6.0-linux-x64/bin/node --version
/root/node-test/node-v24.6.0-linux-x64/bin/node: /lib64/libm.so.6: version `GLIBC_2.27' not found (required by /root/node-test/node-v24.6.0-linux-x64/bin/node)
Almost, let’s try the combination:
$ podman run -it --rm --pull newer -v $(pwd):/root/node-test \
-e LD_PRELOAD=/root/node-test/glibc-2.38/lib64/libc.so.6 \
-e LD_LIBRARY_PATH=/root/node-test/glibc-2.38/lib64 \
docker.io/opensuse/leap:42 /root/node-test/node-v24.6.0-linux-x64/bin/node --version
/root/node-test/node-v24.6.0-linux-x64/bin/node: relocation error: /root/node-test/glibc-2.38/lib64/libc.so.6: symbol _dl_signal_error, version GLIBC_PRIVATE not defined in file ld-linux-x86-64.so.2 with link time reference
No. But maybe if we preload both libc and libm?
$ podman run -it --rm --pull newer -v $(pwd):/root/node-test \
-e LD_PRELOAD=/root/node-test/glibc-2.38/lib64/libc.so.6:/root/node-test/glibc-2.38/lib64/libm.so.6 \
docker.io/opensuse/leap:42 /root/node-test/node-v24.6.0-linux-x64/bin/node --version
/root/node-test/node-v24.6.0-linux-x64/bin/node: relocation error: /root/node-test/glibc-2.38/lib64/libc.so.6: symbol _dl_signal_error, version GLIBC_PRIVATE not defined in file ld-linux-x86-64.so.2 with link time reference
Maybe libc and libm and additionally LD_LIBRARY_PATH?
$ podman run -it --rm --pull newer -v $(pwd):/root/node-test \
-e LD_PRELOAD=/root/node-test/glibc-2.38/lib64/libc.so.6:/root/node-test/glibc-2.38/lib64/libm.so.6 \
-e LD_LIBRARY_PATH=/root/node-test/glibc-2.38/lib64 \
docker.io/opensuse/leap:42 /root/node-test/node-v24.6.0-linux-x64/bin/node --version
/root/node-test/node-v24.6.0-linux-x64/bin/node: relocation error: /root/node-test/glibc-2.38/lib64/libc.so.6: symbol _dl_signal_error, version GLIBC_PRIVATE not defined in file ld-linux-x86-64.so.2 with link time reference
No luck. It complains about a missing symbol in the loader ld-linux-x86-64.so.2
itself.
Now it’s time to also replace the loader and calling the loader explicitly:
$ podman run -it --rm --pull newer -v $(pwd):/root/node-test \
-e LD_LIBRARY_PATH=/root/node-test/glibc-2.38/lib64 \
docker.io/opensuse/leap:42 \
/root/node-test/glibc-2.38/lib64/ld-linux-x86-64.so.2 \
/root/node-test/node-v24.6.0-linux-x64/bin/node --version
v24.6.0
There we go! It is important to have both the loader and LD_LIBRARY_PATH
pointing to the new libc version.
Otherwise it won’t work.
The wrapper script
In order to make it simpler to call nodejs without always going through the loader explicitly, we’ll create a small bash script to run it:
#!/bin/bash
NODE_HOME=/root/node-test/node-v24.6.0-linux-x64
GLIBC_HOME=/root/node-test/glibc-2.38
exec ${GLIBC_HOME}/lib64/ld-linux-x86-64.so.2 \
--library-path ${GLIBC_HOME}/lib64 \
${NODE_HOME}/bin/node "$@"
Save this as /root/node-test/node
and make it executable.
Then, you can run nodejs as follows:
$ podman run -it --rm --pull newer -v $(pwd):/root/node-test \
docker.io/opensuse/leap:42 \
/root/node-test/node --version
v24.6.0
Instead of calling /root/node-test/node-v24.6.0-linux-x64/bin/node
directly, you are simply
executing /root/node-test/node
, which takes care of the details. If you put that onto your PATH
, it’s
even simpler:
$ podman run -it --rm --pull newer -v $(pwd):/root/node-test \
-e PATH=/root/node-test:/root/node-test/node-v24.6.0-linux-x64/bin:/bin:/usr/bin \
docker.io/opensuse/leap:42 \
node --version
v24.6.0
And you can even run npm:
$ podman run -it --rm --pull newer -v $(pwd):/root/node-test \
-e PATH=/root/node-test:/root/node-test/node-v24.6.0-linux-x64/bin:/bin:/usr/bin \
docker.io/opensuse/leap:42 \
npm --version
11.5.1
The npm
executable is found in the PATH
under /root/node-test/node-v24.6.0-linux-x64/bin/npm
. This
is actually a shebang script to run “node” from the PATH. Since the wrapper script /root/node-test/node
exists,
this will be used to run node, which in turn runs the npm js script.
Summary
You can replace the dynamically loaded shared objects with LD_LIBRARY_PATH
and LD_PRELOAD
. But sometimes,
this is not enough - you need to replace the dynamic loader as well.
With this trick (executing the loader /lib64/ld-linux-x86-64.so.2
explicitly), it was possible to
run a recent nodejs version on an old linux distribution.
There are some caveats: As the libc is a wrapper around syscalls and the syscalls are provided by the running kernel version, this could be a problem: If nodejs uses a new libc function that is implemented using a new syscall, that is not available in the old kernel, this won’t work. However, this seems not to be the case.
Using this trick is therefore not recommended to use in production environments. Having said that, it is still fascinating, that this even is possible and works.
References
- https://en.wikipedia.org/wiki/Glibc
- https://cylab.be/blog/388/a-solution-to-version-glibc-2xx-not-found
- https://hub.docker.com/r/opensuse/leap/tags
- https://de.wikipedia.org/wiki/SUSE_Linux_Enterprise_Server
- https://en.opensuse.org/openSUSE:Roadmap
- https://www.man7.org/linux/man-pages/man8/ld.so.8.html
- https://samanbarghi.com/post/2014-09-05-how-to-wrap-a-system-call-libc-function-in-linux/
- https://www.goldsborough.me/c/low-level/kernel/2016/08/29/16-48-53-the_-ld_preload-_trick/
Comments
No comments yet.Leave a comment
Your email address will not be published. Required fields are marked *. All comments are held for moderation to avoid spam and abuse.