patchelf: Surgically Edit ELF Binaries Without a Recompile

2026-06-27

Vendor ships you a precompiled binary that crashes with error while loading shared libraries: libssl.so.1.0.0: cannot open shared object file. Your distro moved on to OpenSSL 3 years ago. The vendor's support contract expired in 2019. You don't have the source. What do you do?

You reach for patchelf — a tiny utility from the NixOS folks that rewrites the metadata sections of ELF executables and shared libraries in place. Interpreter path, RPATH/RUNPATH, DT_NEEDED entries, SONAME — all editable without touching a compiler.

The basics:

$ patchelf --print-interpreter ./oldbinary
/lib64/ld-linux-x86-64.so.2

$ patchelf --print-rpath ./oldbinary
/opt/vendor/lib:/usr/local/vendor/lib

$ patchelf --print-needed ./oldbinary
libssl.so.1.0.0
libcrypto.so.1.0.0
libc.so.6

Now the real magic. Swap a missing dependency for one that exists:

$ patchelf --replace-needed libssl.so.1.0.0 libssl.so.3 ./oldbinary
$ patchelf --replace-needed libcrypto.so.1.0.0 libcrypto.so.3 ./oldbinary

If the ABI is close enough, it just runs. (It usually is for OpenSSL's data-plane symbols; YMMV for fancier ones.) If you need to point a binary at libraries in a non-standard prefix without polluting LD_LIBRARY_PATH, use the dynamic-loader's $ORIGIN trick:

$ patchelf --set-rpath '$ORIGIN/../lib' ./mybin

Now mybin finds its libs relative to its own location — drop the whole tree anywhere and it works. This is how AppImages and most "portable Linux app" packaging schemes get away with it.

Running an Alpine-built binary on glibc, or vice versa? The dynamic linker path differs. Repoint it:

$ patchelf --set-interpreter /lib/ld-musl-x86_64.so.1 ./glibc-binary

Stripping cruft from over-eager build systems:

$ patchelf --shrink-rpath ./bigbin          # remove unused RPATH entries
$ patchelf --remove-needed libpython3.10.so.1.0 ./plugin.so

Add a library without recompiling — useful for LD_PRELOAD-style hooks you want to make permanent:

$ patchelf --add-needed libmytrace.so ./victim

Rename a shared library's SONAME so the linker treats two copies as distinct:

$ patchelf --set-soname libfoo.so.2 ./libfoo.so.2.0.1

Why not just LD_LIBRARY_PATH or a wrapper script?

LD_LIBRARY_PATH leaks into every child process and breaks their library resolution. Wrapper scripts confuse anything that inspects argv[0] or /proc/self/exe. RPATH edits live inside the binary itself — no environment fiddling, no extra processes, no surprise behavior for child programs. The binary is genuinely portable in the way the original packager should have shipped it.

The catch

patchelf works by relocating sections inside the ELF file, which can grow the binary slightly and occasionally trip checksums or signed binaries. Don't try it on /usr/bin/sudo. And always operate on a copy — --output writes to a separate path:

$ patchelf --replace-needed libssl.so.1.0.0 libssl.so.3 \
    --output ./fixed ./oldbinary

Available everywhere: apt install patchelf, dnf install patchelf, brew install patchelf. One static binary, no dependencies, ~100KB. The Nix folks couldn't build their universe without it; once you've used it twice you'll wonder how you ever shipped binaries without it.

Key Takeaway: When a binary points at the wrong interpreter, the wrong RPATH, or a library that no longer exists, patchelf lets you rewrite those fields in place — no source, no recompile, no LD_LIBRARY_PATH contamination.

All newsletters