RPATH versus RUNPATH: Test Transitive Library Discovery on the Target Image

If an executable finds its direct library but that library cannot find a dependency, inspect which object owns the runtime search path. With the glibc loader, an executable’s DT_RUNPATH applies to its direct DT_NEEDED dependencies, not automatically to their children. Give each object the search policy needed for its own direct dependencies, then test the complete dependency graph.

This guide uses Linux man-pages 6.12 and GNU BFD Binutils 2.44 for ordinary Linux ELF loading. An executed host fixture compared explicit old and new dynamic tags in a three-object graph. Other loaders, secure execution and production deployment paths remain outside that executed scope.

Draw the graph before listing directories

Consider a hypothetical executable app that requires libdepa.so.1. Library A requires libdepb.so.1. Both libraries sit in a private lib directory next to the application tree. The important relationships are app to A and A to B, not simply “all files are in lib.”

Inspect DT_NEEDED, DT_RPATH and DT_RUNPATH on every relevant object with readelf -d. A path on app is metadata owned by app. A’s own dependency resolution can require A’s search metadata. Keep the exact loader and environment with the graph so a workstation’s global library installation does not accidentally satisfy B during testing.

The loader reference defines RUNPATH’s direct-dependency scope and contrasts it with RPATH propagation through the dependency tree. It also explains that an existing RUNPATH changes how RPATH is considered. Treat the actual dynamic tags as evidence rather than relying on the flag spelling used by a build wrapper.

The executable finds direct dependency A using its RUNPATH, while A needs its own ORIGIN RUNPATH to locate B; a separate result box contrasts the executed RPATH and RUNPATH cases.
Original dependency-edge map. Search responsibility follows the object declaring the direct dependency.

Distinguish link-time discovery from runtime discovery

The link can succeed because the build directory or an explicit -rpath-link supplies enough information to resolve indirect dependencies. That does not prove the runtime loader has an equivalent search path. Likewise, -L helps the static link command locate inputs; it is not a runtime deployment policy.

The Binutils 2.44 linker manual distinguishes these options and supports explicit –enable-new-dtags and –disable-new-dtags. Build configurations can select different defaults, so the fixture used explicit choices and inspected the resulting tags. Do not assume that every -rpath invocation produces the same tag on every toolchain.

Use the compiler driver’s supported forwarding syntax for linker options and preserve the full command in evidence. Shell expansion can alter the intended path token before the linker sees it. A copied command that worked in one build shell may not mean the same thing inside a generator or package recipe.

Use ORIGIN with the owning object in mind

The loader expands $ORIGIN to the directory containing the program or shared object whose relevant token it is processing. For app installed under bin, $ORIGIN/../lib can describe the adjacent library directory. For A already inside lib, its own $ORIGIN can locate B beside it.

Quote the token so the shell does not substitute an unrelated environment variable. For example, a trusted local link command may pass -Wl,-rpath,’$ORIGIN/../lib’. Check the resulting ELF string afterward. This is a path-expression example, not permission to embed arbitrary writable directories in a privileged service’s search path.

A dependency string containing a slash is interpreted as a pathname under the documented rules and can bypass ordinary name searching. Review such strings separately. A private deployment intended to relocate should not accidentally retain an absolute build-tree dependency merely because the link command named a file that lacked a suitable SONAME.

Compare the three-object cases

Fixture configuration Observed host result
App RPATH points to lib; A has no runtime path A and its child B were found; app passed.
App RUNPATH points to lib; A has no runtime path A was found, but loading failed for B.
App RUNPATH plus A’s own ORIGIN RUNPATH A found its direct B dependency; app passed.

The fixture used GCC 14.2.0 and BFD ld 2.44 on x86-64 Linux. It removed LD_* variables from the child execution environment, used uniquely named synthetic libraries, and verified the expected success or missing-B error. All binaries were created for this test and trusted; no foreign target executable was run.

The successful repair gives A responsibility for B’s local placement rather than turning the executable’s legacy path propagation into an undocumented global dependency. It does not imply RUNPATH is always the right product policy or that a legacy RPATH is universally incorrect. The decision depends on the intended override, isolation and relocation behavior.

Test environment precedence explicitly

The loader’s documented order considers RPATH under its specified condition, then LD_LIBRARY_PATH outside secure execution, then RUNPATH, cache and default locations subject to other options. A test that inherits a developer’s environment can therefore select a different provider from the image’s normal launch.

Keep a clean baseline and a deliberate override case separate. Record the actual loaded provider through a supported loader diagnostic on trusted test binaries or equivalent controlled observation. Static metadata predicts candidates; runtime evidence establishes which path the tested process selected.

Do not use LD_LIBRARY_PATH as an unexplained permanent repair. It can affect more objects than the missing dependency and make a service depend on its launcher. Secure-execution behavior also differs, so a shell test cannot qualify a privileged deployment by analogy.

Qualify the final image and relocation

Copy the owned fixture tree to another temporary location and rerun as a proposed relocation test. For the product, inspect the final filesystem, not only the staging directory, and include optional plugins that introduce another dependency level. Keep every object’s search metadata in the deployment manifest.

Negative cases should remove B, replace it with an incompatible candidate, omit A’s RUNPATH and introduce a known environment override. Require distinct diagnoses for absent file, wrong ABI and wrong selection. Do not change all search paths at once; a one-edge repair is easier to explain and retain in the build description.

The deliverable is a dependency graph annotated with the object responsible for each search. When that graph matches the final image and tested loader behavior, a transitive lookup failure becomes a precise packaging decision instead of a series of global directory additions.

Similar Posts