ldd is not safe to run on an untrusted binary — use these instead
ldd is the fastest way to see whether a binary’s libraries resolve, and it is in every “how do I
debug a missing .so” answer on the internet. It is also, by design, able to run the binary you point
it at. On a file you did not build, that is an unacceptable trade.
What ldd actually is
$ file $(command -v ldd)
/usr/bin/ldd: Bourne-Again shell script, ASCII text executable
It is a shell script. Historically it set LD_TRACE_LOADED_OBJECTS=1 and executed the target, which
made the loader print the dependency list instead of running the program. On glibc systems the script
now tries ld.so --list first, which does not execute the program — but the script still contains
fallback paths, and on other libcs and older systems the exec happens.
Two concrete consequences:
- A malicious binary can run code at
lddtime on systems where the fallback path is taken. It can also simply be a shell script, whichlddwill happily execute. lddon a static binary printsnot a dynamic executable, which people misread as an error.
The man page states it directly: “ldd should never be employed on an untrusted executable, since this may result in the execution of arbitrary code.”
Read-only alternative 1: objdump -p
$ objdump -p ./app | grep NEEDED
NEEDED libssl.so.1.1
NEEDED libc.so.6
objdump parses the dynamic section. It never loads, maps or executes anything. This is the
recommended replacement in the glibc documentation and it works on any ELF, including ones whose
architecture does not match the host.
Read-only alternative 2: readelf -d
$ readelf -d ./app | grep -E 'NEEDED|RPATH|RUNPATH'
0x0000000000000001 (NEEDED) Shared library: [libssl.so.1.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN/../lib]
Strictly better than objdump -p for this purpose, because it also prints the search paths. Both
read only.
Read-only alternative 3: resolve it yourself
ldd really answers two questions at once: what does the binary need, and what would resolve on
this host. Split them:
# what it needs (read-only)
readelf -d ./app | awk '/NEEDED/ {print $NF}' | tr -d '[]'
# what the host can provide (index lookup, no execution)
ldconfig -p | grep -E 'libssl\.so\.1\.1|libc\.so\.6'
Comparing those two lists gives you the same answer as ldd, using only index reads.
When you do want loader behaviour
If you need the loader’s actual search decisions — including RUNPATH ordering, which is where most
surprises live — invoke the loader explicitly rather than through the wrapper:
/lib64/ld-linux-x86-64.so.2 --list ./app
--list is a loader option, not an execution mode, and the loader is the component that would have
been started anyway. Understand the caveat: on a foreign-architecture binary this fails, because the
loader itself is architecture-specific.
For the search trace, LD_DEBUG=libs is the tool that ends arguments:
$ LD_DEBUG=libs ./app 2>&1 | grep -A2 'libssl'
21490: find library=libssl.so.1.1 [0]; searching
21490: trying file=/usr/lib/x86_64-linux-gnu/libssl.so.1.1
21490: search path=/usr/lib/x86_64-linux-gnu (system search path)
This executes the program, so it is for binaries you trust and environments you control.
What to do in practice
| Situation | Use |
|---|---|
| Binary from a vendor or the internet | readelf -d / objdump -p only |
| Binary you built, on your machine | ldd is fine, LD_DEBUG=libs for ordering |
| Different architecture from the host | readelf -d — nothing else can run |
| Need to know if this host resolves it | ldconfig -p + the NEEDED list |
The Dependency Doctor does the first two columns in a browser: it parses
DT_NEEDED, RPATH and RUNPATH from the file without executing it, and if you paste your ldd
output it maps resolved and missing entries onto that list — so you get the diagnosis without
running the loader on an unknown file.
Related: missing shared libraries for the search order in
detail, and musl vs glibc for the interpreter mismatch that ldd itself
cannot report.
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.