version `GLIBC_2.34' not found — why copying libc never works
$ ./app
./app: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found (required by ./app)
This is the most misdiagnosed error in Linux packaging, because the library was found. The loader
located libc.so.6, opened it, and then discovered that the specific versioned symbols the binary
needs are not in it. Every “I copied libc.so.6 over and now nothing works” story starts here.
What symbol versioning is for
glibc exports symbols with version tags. Look at any binary on a modern distribution:
$ objdump -T /bin/ls | grep GLIBC_2 | head -5
0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 __libc_start_main
0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 __errno_location
0000000000000000 DF *UND* 0000000000000000 GLIBC_2.3.4 __xstat
0000000000000000 DF *UND* 0000000000000000 GLIBC_2.34 __libc_start_main
GLIBC_2.2.5 means “the ABI as it existed in glibc 2.2.5”. GLIBC_2.34 means the ABI as of 2.34.
The mechanism exists so a single libc.so.6 can serve binaries built against a decade of different
ABIs. When a function’s signature or semantics change, the new implementation is added under a new
version node and the old one is kept.
The compatibility contract runs in exactly one direction: newer glibc runs older binaries; older glibc cannot run newer binaries. There is no forward compatibility, and there never will be — providing it would mean shipping the future ABI in a past release.
Confirming which version you need
$ readelf -V ./app | grep -A1 'Name: GLIBC' | tail -6
0x0010: Name: GLIBC_2.34 Flags: none Version: 7
0x0020: Name: GLIBC_2.2.5 Flags: none Version: 3
0x0030: Name: GLIBC_2.28 Flags: none Version: 5
The .gnu.version_r section (Version needs) is the authoritative list. A blunt but effective
alternative:
$ strings ./app | grep -o 'GLIBC_2\.[0-9]*' | sort -uV | tail -1
GLIBC_2.38
The highest number is the constraint. Now compare it with what the host provides:
$ ldd --version | head -1
ldd (Ubuntu GLIBC 2.31-0ubuntu9.16) 2.31
2.38 > 2.31, so this binary will never run here. No library copy, no ldconfig, no
LD_LIBRARY_PATH changes the outcome.
Distribution reference
| Distribution | glibc | Notes |
|---|---|---|
| CentOS 7 / RHEL 7 | 2.17 | the long-standing compatibility floor |
| Ubuntu 18.04 | 2.27 | |
| Debian 10 | 2.28 | |
| Ubuntu 20.04 | 2.31 | |
| Ubuntu 22.04 | 2.35 | |
| Debian 12 | 2.36 | |
| Ubuntu 24.04 | 2.39 |
A binary built on Ubuntu 24.04 requires at least 2.39. That is not a bug in either system; it is the contract working as designed.
Why copying libc.so.6 is not a fix
The tempting move is to copy the newer libc.so.6 from the build machine onto the target. It fails
for structural reasons:
libc.so.6is not standalone. It needs a matchingld-linux-x86-64.so.2. Mixing a new libc with the old dynamic loader producesAssertion failedor a segfault beforemain— sometimes before any symbol resolution at all.- Everything else links against it. Replacing the system libc changes the behaviour of every
binary on the host, including
sshd,sudoand the package manager. This is how machines stop booting. - The loader path is baked into the binary.
PT_INTERPsays/lib64/ld-linux-x86-64.so.2. You cannot redirect it without editing the binary anyway.
The four legitimate fixes
1. Build on the oldest distribution you intend to support
The standard approach, and the reason manylinux images exist. Build inside the old base and the
result runs everywhere newer:
docker run --rm -v "$PWD":/src -w /src quay.io/pypa/manylinux2014_x86_64 ./build.sh
manylinux2014 pins glibc 2.17; manylinux_2_28 pins 2.28. For non-Python artifacts the same idea
applies with any old base image — debian:10, centos:7, ubuntu:20.04.
2. Link statically
gcc -static -O2 -o app app.c
# or, better for portability:
musl-gcc -static -O2 -o app app.c
Static linking removes the glibc dependency completely: the required code is in the binary, so there
is no version negotiation at all. Two caveats: glibc’s static libc warns about getpwnam() and
dlopen() because they need the NSS and dynamic-loading machinery at runtime, and static glibc
binaries are not smaller than a container base image by much. For real portability, musl-gcc -static avoids the NSS problem entirely.
3. Ship the runtime with the application
A container solves this by construction: the image contains the glibc the binary was built against.
FROM ubuntu:24.04
COPY --from=build /src/app /usr/local/bin/app
For non-container delivery, bundle the loader and the libraries in a directory and invoke the loader
explicitly, which sidesteps PT_INTERP:
./bundle/lib/ld-linux-x86-64.so.2 --library-path ./bundle/lib ./app
4. Build with a newer-glibc-capable cross toolchain
zig cc targets old glibc versions directly and is a pragmatic option for small C/C++ tools:
zig cc -target x86_64-linux-gnu.2.17 -O2 -o app app.c
What to check first when you hit this in production
readelf -V ./app | grep GLIBC— which version is being demanded?ldd --versionon the target — what is provided?docker image inspect— is the runtime image actually the one you think it is? A silently pulledubuntu:24.04in place ofubuntu:22.04explains this entire class of failure.- If the answer is “the base image moved”, pin the tag by digest.
ubuntu:22.04is a moving target;ubuntu:22.04@sha256:…is not.
The Compatibility Checker reports the highest required GLIBC version
of a binary against a chosen target, without needing a shell on either machine. If you want to see
the raw structures, ELF Inspector shows both .gnu.version_r entries and the
interpreter path.
Related: missing shared libraries — the failure that happens when the library is not found at all, rather than found and rejected.
Every command in this guide was run against the distribution named in the output. If your output differs, the mechanism section explains why — and the ELF Inspector shows the same fields without needing a shell.