haampie / libtree

ldd as a tree
MIT License
2.67k stars 60 forks source link

Handle symlinks to executables using $ORIGIN in rpath/runpath better #49

Open ekacoei opened 2 years ago

ekacoei commented 2 years ago

What is the expected output if a dependency cannot be found? I cannot resolve my Java dependencies:

$ ./libtree /usr/bin/java 
java
└── libjli.so not found: RPATH (empty) LD_LIBRARY_PATH (empty) RUNPATH "/usr/bin//../lib/jli":"/usr/bin//../lib": /etc/ld.so.conf "/usr/lib/x86_64-linux-gnu/libfakeroot":"/usr/local/lib/i386-linux-gnu":"/lib/i386-linux-gnu":"/usr/lib/i386-linux-gnu":"/usr/local/lib/i686-linux-gnu":"/lib/i686-linux-gnu":"/usr/lib/i686-linux-gnu":"/usr/local/lib":"/usr/local/lib/x86_64-linux-gnu":"/lib/x86_64-linux-gnu":"/usr/lib/x86_64-linux-gnu":"/lib32":"/usr/lib32":"/libx32":"/usr/libx32":

$ ldd /usr/bin/java
    linux-vdso.so.1 (0x00007ffc30d7b000)
    libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f541bf8f000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f541bf6e000)
    libjli.so => not found
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f541bf69000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f541bda8000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f541c1f0000)

$ /usr/bin/java --version
openjdk 11.0.9.1 2020-11-04
OpenJDK Runtime Environment (build 11.0.9.1+1-post-Debian-1deb10u2)
OpenJDK 64-Bit Server VM (build 11.0.9.1+1-post-Debian-1deb10u2, mixed mode, sharing)
haampie commented 2 years ago

See also https://github.com/haampie/libtree/issues/46, libtree skips some glibc libraries because output tends to be very verbose otherwise. With libtree -v you get the other libs too.

haampie commented 2 years ago

Okay, Java is very odd, I can't explain it yet. Using the C rewrite:

$ ./libtree /usr/bin/java 
lib.so 
└── libjli.so not found
    ┊ Paths considered in this order:
    ┊ 1. rpath is skipped because runpath was set
    ┊ 2. LD_LIBRARY_PATH was not set
    ┊ 3. runpath:
    ┊    /usr/bin//../lib/amd64/jli
    ┊    /usr/bin//../lib/amd64
    ┊ 4. ld.so.conf:
...

So, you're reading this right, java has a soname lib.so :sweat_smile:, and a needed library libjli.so, and also runpaths, but the library can't be found from there. Also ldd chokes:

$ ldd /usr/bin/java 
    linux-vdso.so.1 (0x00007ffef8fac000)
    libjli.so => not found
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa54d940000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fa54db55000)

It also has a default interpreter:

$ readelf -l /usr/bin/java | grep interpret
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

If I pass it to the interpreter:

$ /lib64/ld-linux-x86-64.so.2 /usr/bin/java 
/usr/bin/java: error while loading shared libraries: libjli.so: cannot open shared object file: No such file or directory

So, apparently that's not how java executes.

haampie commented 2 years ago

Okay, it turns out that ldd output is completely unreliable for symlinks.

Note that this works:

$ ./libtree -v $(realpath /usr/bin/java)
lib.so 
├── libjli.so [runpath]
│   ├── libz.so.1 [ld.so.conf]
│   │   └── libc.so.6 [ld.so.conf]
│   ├── libpthread.so.0 [ld.so.conf]
│   ├── libc.so.6 [ld.so.conf]
│   └── libdl.so.2 [ld.so.conf]
└── libc.so.6 [ld.so.conf]

$ ldd $(realpath /usr/bin/java)
    linux-vdso.so.1 (0x00007ffd4b90b000)
    libjli.so => /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/../lib/amd64/jli/libjli.so (0x00007f3db0e03000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3db0bf5000)
    libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f3db0bd9000)
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f3db0bd3000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f3db0bb0000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f3db0e1c000)

If you execute /usr/bin/java the executable path is resolved before ld.so is invoked (from strace output), and then $ORIGIN runpaths are relative to the real path. If you invoke ld.so by hand and pass the symlink, $ORIGIN is relative to the symlink and it fails.

It seems better (but note the man ldd security warning) to do

$ LD_TRACE_LOADED_OBJECTS=1 /usr/bin/java
    linux-vdso.so.1 (0x00007fffb7764000)
    libjli.so => /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/../lib/amd64/jli/libjli.so (0x00007f1353f70000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1353d62000)
    libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f1353d46000)
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f1353d40000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f1353d1d000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f1353f84000)

Not sure how to handle this best in libtree... warn if the user passes a symlink? Resolve the symlink?

ekacoei commented 2 years ago

Thank you for your detailed writedown, that provided me a lot of insigths. Concerning whether you shall resolve symlinks - as far as I know it is very common that libraries symlink one another to cover version compatibility

$  ls -l /usr/lib/libgimp-2.0.so.0
lrwxrwxrwx 1 root root 23 Dec 24  2018 /usr/lib/libgimp-2.0.so.0 -> libgimp-2.0.so.0.1000.8

but I could not find libgimp being loaded

$ ./libtree $(realpath $(which gimp))
gimp-2.10
├── libgimpwidgets-2.0.so.0 [default paths]
│   ├── libgimpbase-2.0.so.0 [default paths]
│   │   └── libgexiv2.so.2 [ld.so.conf]
│   │       └── libexiv2.so.14 [ld.so.conf]
│   ├── libgimpcolor-2.0.so.0 [default paths]
│   │   ├── libgimpbase-2.0.so.0 (collapsed) [default paths]
│   │   ├── libgegl-0.4.so.0 [ld.so.conf]
│   │   │   ├── libgmodule-2.0.so.0 [ld.so.conf]
│   │   │   └── libbabl-0.1.so.0 [ld.so.conf]
│   │   │       └── liblcms2.so.2 [ld.so.conf]
│   │   ├── libbabl-0.1.so.0 (collapsed) [ld.so.conf]
│   │   ├── libcairo.so.2 [ld.so.conf]
│   │   │   ├── libpixman-1.so.0 [ld.so.conf]
│   │   │   ├── libpng16.so.16 [ld.so.conf]
│   │   │   ├── libxcb-shm.so.0 [ld.so.conf]
│   │   │   ├── libxcb-render.so.0 [ld.so.conf]
│   │   │   ├── libXrender.so.1 [ld.so.conf]
│   │   │   └── libXext.so.6 [ld.so.conf]
│   │   └── liblcms2.so.2 (collapsed) [ld.so.conf]
│   ├── libgimpconfig-2.0.so.0 [default paths]
│   │   ├── libgimpbase-2.0.so.0 (collapsed) [default paths]
│   │   ├── libgimpcolor-2.0.so.0 (collapsed) [default paths]
│   │   └── libgimpmath-2.0.so.0 [default paths]
│   ├── libgegl-0.4.so.0 (collapsed) [ld.so.conf]
│   ├── libbabl-0.1.so.0 (collapsed) [ld.so.conf]
│   ├── libgtk-x11-2.0.so.0 [ld.so.conf]
│   │   ├── libgdk-x11-2.0.so.0 [ld.so.conf]
│   │   │   ├── libXrender.so.1 (collapsed) [ld.so.conf]
│   │   │   ├── libXinerama.so.1 [ld.so.conf]
│   │   │   │   └── libXext.so.6 (collapsed) [ld.so.conf]
│   │   │   ├── libXi.so.6 [ld.so.conf]
│   │   │   │   └── libXext.so.6 (collapsed) [ld.so.conf]
│   │   │   ├── libXrandr.so.2 [ld.so.conf]
│   │   │   │   ├── libXext.so.6 (collapsed) [ld.so.conf]
│   │   │   │   └── libXrender.so.1 (collapsed) [ld.so.conf]
│   │   │   ├── libXcursor.so.1 [ld.so.conf]
│   │   │   │   ├── libXrender.so.1 (collapsed) [ld.so.conf]
│   │   │   │   └── libXfixes.so.3 [ld.so.conf]
│   │   │   ├── libXcomposite.so.1 [ld.so.conf]
│   │   │   ├── libXdamage.so.1 [ld.so.conf]
│   │   │   │   └── libXfixes.so.3 (collapsed) [ld.so.conf]
│   │   │   ├── libXfixes.so.3 (collapsed) [ld.so.conf]
│   │   │   ├── libcairo.so.2 (collapsed) [ld.so.conf]
│   │   │   └── libXext.so.6 (collapsed) [ld.so.conf]
│   │   ├── libgmodule-2.0.so.0 (collapsed) [ld.so.conf]
│   │   ├── libXcomposite.so.1 (collapsed) [ld.so.conf]
│   │   ├── libXdamage.so.1 (collapsed) [ld.so.conf]
│   │   ├── libXfixes.so.3 (collapsed) [ld.so.conf]
│   │   ├── libatk-1.0.so.0 [ld.so.conf]
│   │   └── libcairo.so.2 (collapsed) [ld.so.conf]
│   ├── libgdk-x11-2.0.so.0 (collapsed) [ld.so.conf]
│   ├── libcairo.so.2 (collapsed) [ld.so.conf]
│   └── liblcms2.so.2 (collapsed) [ld.so.conf]
├── libgtk-x11-2.0.so.0 (collapsed) [ld.so.conf]
├── libgdk-x11-2.0.so.0 (collapsed) [ld.so.conf]
├── libgimpconfig-2.0.so.0 (collapsed) [default paths]
├── libgimpmath-2.0.so.0 (collapsed) [default paths]
├── libgimpthumb-2.0.so.0 [default paths]
│   └── libgimpbase-2.0.so.0 (collapsed) [default paths]
├── libgimpcolor-2.0.so.0 (collapsed) [default paths]
├── libgimpmodule-2.0.so.0 [default paths]
│   ├── libgimpbase-2.0.so.0 (collapsed) [default paths]
│   ├── libgimpconfig-2.0.so.0 (collapsed) [default paths]
│   └── libgmodule-2.0.so.0 (collapsed) [ld.so.conf]
├── libgimpbase-2.0.so.0 (collapsed) [default paths]
├── libcairo.so.2 (collapsed) [ld.so.conf]
├── libgegl-0.4.so.0 (collapsed) [ld.so.conf]
├── libgegl-npd-0.4.so [ld.so.conf]
│   ├── libgegl-0.4.so.0 (collapsed) [ld.so.conf]
│   └── libbabl-0.1.so.0 (collapsed) [ld.so.conf]
├── libbabl-0.1.so.0 (collapsed) [ld.so.conf]
├── liblcms2.so.2 (collapsed) [ld.so.conf]
├── libgexiv2.so.2 (collapsed) [ld.so.conf]
└── libmypaint-1.3.so.0 [ld.so.conf]
    └── libjson-c.so.3 [ld.so.conf]

So I am lost at the moment if libtree has to handle symlinks on real world examples, including libraries as symlinks.

haampie commented 2 years ago

It only applies to executables, not to libraries, because ld.so gets the resolved executable path (I think).

$ORIGIN is relative to the path in which the library was found, even if it is a symlink. For example if you have exe depends on libb.so depends on liba.so and the libs are in the same dir with $ORIGIN rpath, but you symlink libb.so from another dir, and set exe's rpath to that other dir, liba.so is not found.

 $ mkdir -p a b
 $ echo 'int f(){return 3;}' | gcc -shared -o a/liba.so -Wl,-soname,liba.so -nostdlib -x c -
 $ echo 'extern int f(); int g(){return f();}' | gcc -shared -o a/libb.so -Wl,-soname,libb.so '-Wl,-rpath,$ORIGIN' -nostdlib -x c - -La -la 
 $ ln -s ../a/libb.so b/libb.so
 $ echo 'extern int g(); int main(){return g();}' | gcc -o exe '-Wl,-rpath,$ORIGIN/b' -x c - -La -lb 
 $ ./exe 
./exe: error while loading shared libraries: liba.so: cannot open shared object file: No such file or directory

 $ echo 'extern int g(); int main(){return g();}' | gcc -o exe '-Wl,-rpath,$ORIGIN/a' -x c - -La -lb 
 $ ./exe
[works]
haampie commented 2 years ago

Another thing to note is that the linker likes to copy the soname into DT_NEEDED instead of the filename you provide in -l<libname>. So if linking works & rpaths are set correctly, it may still fail at runtime if there is no file/symlink matching the soname.