guides / glibc-version-not-found troubleshootingupdated 2026-10-25 readelf -h ./app

version `GLIBC_2.34' not found — why copying libc never works

2026-10-25 By luckyray Debian 12 · Ubuntu 24.04 · Alpine 3.20
$ ./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

DistributionglibcNotes
CentOS 7 / RHEL 72.17the long-standing compatibility floor
Ubuntu 18.042.27
Debian 102.28
Ubuntu 20.042.31
Ubuntu 22.042.35
Debian 122.36
Ubuntu 24.042.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.6 is not standalone. It needs a matching ld-linux-x86-64.so.2. Mixing a new libc with the old dynamic loader produces Assertion failed or a segfault before main — 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, sudo and the package manager. This is how machines stop booting.
  • The loader path is baked into the binary. PT_INTERP says /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.

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

  1. readelf -V ./app | grep GLIBC — which version is being demanded?
  2. ldd --version on the target — what is provided?
  3. docker image inspect — is the runtime image actually the one you think it is? A silently pulled ubuntu:24.04 in place of ubuntu:22.04 explains this entire class of failure.
  4. If the answer is “the base image moved”, pin the tag by digest. ubuntu:22.04 is 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.

Related

guides
  1. cannot execute binary file: Exec format error — causes and fixes Why Linux refuses to run a binary with Exec format error, how to confirm the cause in three commands, and the five fixes that actually apply.
  2. wrong ELF class: ELFCLASS32 or ELFCLASS64 — what it means The 32-bit versus 64-bit mismatch that shows up at link time and at dlopen time, why it is not the same as Exec format error, and how to resolve it.
  3. error while loading shared libraries: cannot open shared object file How the dynamic loader searches for libraries, why a library can be installed and still not found, and the five fixes that work in order of preference.