DT_RPATH vs DT_RUNPATH ELF Dynamic Tags and Library Search Precedence

 

DT_RPATH vs DT_RUNPATH
TLPI Chapter 41 ยท Section 41.10 โ€” ELF Dynamic Tags and Library Search Precedence
๐Ÿท๏ธ
ELF Tags
โš–๏ธ
Precedence
๐Ÿ”ง
–enable-new-dtags

Key Concepts
DT_RPATH DT_RUNPATH ELF dynamic section –enable-new-dtags LD_LIBRARY_PATH precedence glibc 2.2 Deprecated tag

Background: Why Two Different ELF Tags?

The original ELF specification defined only one way to embed a runtime library search path in a binary: the DT_RPATH tag. This worked well, but it had one significant drawback: it could NOT be overridden by LD_LIBRARY_PATH.

This became a problem when users needed to test programs with alternative library versions (e.g., debugging builds). You couldn’t override the baked-in rpath with LD_LIBRARY_PATH โ€” it was completely ignored.

Later ELF specifications introduced a new tag, DT_RUNPATH, which has lower priority than LD_LIBRARY_PATH. This allows users and sysadmins to override the rpath at runtime without recompiling the binary.

Summary: DT_RPATH = old, higher priority (can’t be overridden). DT_RUNPATH = new, lower priority (can be overridden by LD_LIBRARY_PATH).

Search Order: DT_RPATH vs DT_RUNPATH (Side-by-Side Diagram)

DT_RPATH (Old โ€” Deprecated)
1 DT_RPATH list in binary
2 LD_LIBRARY_PATH
3 /etc/ld.so.cache
4 Default dirs (/lib, /usr/lib)
โš  LD_LIBRARY_PATH cannot override DT_RPATH

DT_RUNPATH (New โ€” Recommended)
1 LD_LIBRARY_PATH
2 DT_RUNPATH list in binary
3 /etc/ld.so.cache
4 Default dirs (/lib, /usr/lib)
โœ“ LD_LIBRARY_PATH CAN override DT_RUNPATH

How to Create DT_RUNPATH Instead of DT_RPATH

By default, -rpath creates a DT_RPATH entry. To create DT_RUNPATH instead, add the --enable-new-dtags linker option:

# Default (creates DT_RPATH):
gcc -o prog prog.c \
    -Wl,-rpath,/home/mtk/pdir/d1 \
    -L/home/mtk/pdir/d1 -lx1

objdump -p prog | grep PATH
# RPATH   /home/mtk/pdir/d1        <-- only DT_RPATH
# With --enable-new-dtags (creates BOTH DT_RPATH and DT_RUNPATH):
gcc -o prog prog.c \
    -Wl,--enable-new-dtags \
    -Wl,-rpath,/home/mtk/pdir/d1 \
    -L/home/mtk/pdir/d1 -lx1

objdump -p prog | grep PATH
# RPATH    /home/mtk/pdir/d1       <-- for older dynamic linkers
# RUNPATH  /home/mtk/pdir/d1       <-- for modern dynamic linkers
Why both DT_RPATH and DT_RUNPATH appear: When --enable-new-dtags is used, the linker writes the path into BOTH tags for backward compatibility. Older dynamic linkers (before glibc 2.2) that don’t understand DT_RUNPATH will fall back to DT_RPATH. Modern dynamic linkers that support DT_RUNPATH will use it and ignore DT_RPATH.
Modern practice: Always use --enable-new-dtags so LD_LIBRARY_PATH can override your rpath for testing and debugging. This became the default in many Linux distributions.

Coding Example 1: DT_RPATH Cannot Be Overridden by LD_LIBRARY_PATH
#!/bin/bash
# dt_rpath_test.sh โ€” demonstrates that DT_RPATH ignores LD_LIBRARY_PATH

set -e
BASE=/tmp/dtag_demo
LIBV1=$BASE/lib_v1
LIBV2=$BASE/lib_v2

mkdir -p $BASE/bin $LIBV1 $LIBV2

# Build version 1 of the library
cat > $LIBV1/libfoo.c << 'EOF'
#include <stdio.h>
void foo(void) { printf("foo() from VERSION 1 of libfoo\n"); }
EOF
gcc -shared -fPIC -o $LIBV1/libfoo.so $LIBV1/libfoo.c

# Build version 2 of the library
cat > $LIBV2/libfoo.c << 'EOF'
#include <stdio.h>
void foo(void) { printf("foo() from VERSION 2 of libfoo\n"); }
EOF
gcc -shared -fPIC -o $LIBV2/libfoo.so $LIBV2/libfoo.c

# Build main.c
cat > $BASE/main.c << 'EOF'
#include <stdio.h>
extern void foo(void);
int main(void) {
    foo();
    return 0;
}
EOF

echo "=== Build prog_rpath (DT_RPATH pointing to lib_v1) ==="
gcc -o $BASE/bin/prog_rpath $BASE/main.c \
    -Wl,-rpath,$LIBV1 \
    -L$LIBV1 -lfoo
# Note: NO --enable-new-dtags, so DT_RPATH is used

echo ""
echo "=== Build prog_runpath (DT_RUNPATH pointing to lib_v1) ==="
gcc -o $BASE/bin/prog_runpath $BASE/main.c \
    -Wl,--enable-new-dtags \
    -Wl,-rpath,$LIBV1 \
    -L$LIBV1 -lfoo

echo ""
echo "=== Verify ELF tags ==="
echo "prog_rpath:"
objdump -p $BASE/bin/prog_rpath | grep -E 'RPATH|RUNPATH'
echo "prog_runpath:"
objdump -p $BASE/bin/prog_runpath | grep -E 'RPATH|RUNPATH'

echo ""
echo "=== Run normally (both use lib_v1 from rpath) ==="
$BASE/bin/prog_rpath
$BASE/bin/prog_runpath

echo ""
echo "=== Try to override with LD_LIBRARY_PATH pointing to lib_v2 ==="
echo "prog_rpath (DT_RPATH โ€” LD_LIBRARY_PATH is IGNORED):"
LD_LIBRARY_PATH=$LIBV2 $BASE/bin/prog_rpath
# Still prints "VERSION 1" because DT_RPATH beats LD_LIBRARY_PATH

echo "prog_runpath (DT_RUNPATH โ€” LD_LIBRARY_PATH WORKS):"
LD_LIBRARY_PATH=$LIBV2 $BASE/bin/prog_runpath
# Prints "VERSION 2" because LD_LIBRARY_PATH beats DT_RUNPATH
This script clearly demonstrates the fundamental difference: with DT_RPATH, LD_LIBRARY_PATH has no effect. With DT_RUNPATH, LD_LIBRARY_PATH takes precedence.

Coding Example 2: Inspecting ELF Tags with readelf
#!/bin/bash
# inspect_elf_tags.sh โ€” use readelf to show DT_RPATH/DT_RUNPATH tags

BINARY=$1  # pass the binary as argument

echo "=== Dynamic section of: $BINARY ==="
readelf -d $BINARY | head -40

echo ""
echo "=== Filtered: RPATH/RUNPATH/NEEDED ==="
readelf -d $BINARY | grep -E 'RPATH|RUNPATH|NEEDED'

echo ""
echo "=== Same info via objdump ==="
objdump -p $BINARY | grep -E 'RPATH|RUNPATH|NEEDED'

# Example output for a binary built with --enable-new-dtags:
# (readelf -d)
#  0x000000000000001d (RUNPATH)   Library runpath: [/home/mtk/pdir/d1]
#  0x000000000000000f (RPATH)     Library rpath: [/home/mtk/pdir/d1]
#  0x0000000000000001 (NEEDED)    Shared library: [libx1.so]
#  0x0000000000000001 (NEEDED)    Shared library: [libc.so.6]
#
# Modern dynamic linkers seeing BOTH tags will use RUNPATH and ignore RPATH.

Coding Example 3: Practical Use Case โ€” Testing with Alternative Library
/* test_scenario.c โ€” scenario where DT_RUNPATH is useful for testing */
/*
 * Scenario:
 * - Production build of myapp has DT_RPATH pointing to /usr/lib/myapp/
 * - You want to test with a DEBUG version of libfoo.so in /tmp/debug/
 * - With DT_RPATH: you CANNOT override it. Must rebuild the binary.
 * - With DT_RUNPATH: just set LD_LIBRARY_PATH=/tmp/debug/ before running.
 */

/* production_build.sh */
/*
gcc -o myapp main.c \
    -Wl,-rpath,/usr/lib/myapp \     <-- DT_RPATH (can't override later)
    -L/usr/lib/myapp -lfoo
*/

/* better_build.sh */
/*
gcc -o myapp main.c \
    -Wl,--enable-new-dtags \
    -Wl,-rpath,/usr/lib/myapp \     <-- DT_RUNPATH (can override)
    -L/usr/lib/myapp -lfoo

# Now to test with debug library:
LD_LIBRARY_PATH=/tmp/debug_libs ./myapp    <-- uses debug libfoo.so
./myapp                                    <-- uses production libfoo.so
*/

#include <stdio.h>
extern void foo_operation(void);  // from libfoo

int main(void) {
    printf("Running myapp...\n");
    foo_operation();
    return 0;
}

Compatibility and Usage Reference
Feature DT_RPATH DT_RUNPATH
ELF standard status Deprecated (old) Current standard
glibc support since Original glibc 2.2
LD_LIBRARY_PATH override โŒ Cannot override โœ… Can override
gcc flag to create -Wl,-rpath,... (default) -Wl,--enable-new-dtags -Wl,-rpath,...
Transitive rpath following Yes No (RUNPATH is not followed for deps)
Recommended for new code No Yes
Important difference in transitive behavior: DT_RPATH in a shared library is used when searching for THAT library’s dependencies. DT_RUNPATH is NOT propagated to dependency searches of dependent libraries. This is a subtle but important difference for chained library scenarios.

Interview Questions
Q1. What is the key difference between DT_RPATH and DT_RUNPATH?
The key difference is their precedence relative to LD_LIBRARY_PATH. DT_RPATH has HIGHER priority โ€” it is checked BEFORE LD_LIBRARY_PATH, so LD_LIBRARY_PATH cannot override it. DT_RUNPATH has LOWER priority โ€” it is checked AFTER LD_LIBRARY_PATH, so LD_LIBRARY_PATH can override the embedded path. DT_RUNPATH is the newer standard and is preferred for flexibility.
Q2. What linker option do you use to create DT_RUNPATH instead of DT_RPATH?
You add -Wl,--enable-new-dtags to the gcc command line along with -Wl,-rpath,.... This causes the linker to write the path into BOTH the DT_RPATH and DT_RUNPATH fields. Modern dynamic linkers that understand DT_RUNPATH will use it (and ignore DT_RPATH). Older linkers (pre-glibc 2.2) that don’t know about DT_RUNPATH will fall back to DT_RPATH.
Q3. Why does an ELF binary sometimes have BOTH DT_RPATH and DT_RUNPATH entries?
When you build with --enable-new-dtags, the linker writes the rpath into both DT_RPATH and DT_RUNPATH fields for backward compatibility. Older dynamic linkers that don’t support DT_RUNPATH will use DT_RPATH. Newer dynamic linkers that support DT_RUNPATH will use it and completely ignore DT_RPATH.
Q4. When would you prefer DT_RPATH over DT_RUNPATH?
DT_RPATH might be preferred in high-security environments where you want to guarantee that a specific library is always used regardless of what LD_LIBRARY_PATH is set to. This prevents a malicious user from substituting a different library by manipulating LD_LIBRARY_PATH. However, for most cases DT_RUNPATH is preferred because it allows legitimate overrides for testing and debugging.
Q5. How do you verify whether a binary uses DT_RPATH or DT_RUNPATH?
Use readelf -d binary | grep -E 'RPATH|RUNPATH' or objdump -p binary | grep -E 'RPATH|RUNPATH'. readelf uses the tag names from the ELF dynamic section; you’ll see lines like (RPATH) or (RUNPATH) indicating which tag is present.
Q6. What happens at runtime if a binary has both DT_RPATH and DT_RUNPATH and the dynamic linker supports both?
The dynamic linker will use DT_RUNPATH and completely ignore DT_RPATH. Support for DT_RUNPATH was added in glibc 2.2, and any dynamic linker supporting it will take DT_RUNPATH as the authoritative entry and discard DT_RPATH. This means LD_LIBRARY_PATH can still override the path.

Leave a Reply

Your email address will not be published. Required fields are marked *