1

I am trying to symlink executable files to a bin directory within a docker script. I need a way to identify executables and return boolean status. I've been trying this:

for i in ../src/u-boot/tools/*; do if ! [[ "readelf -h $i | grep -q DYN" ]]; then ln -s $i .; fi; done

It doesn't work and I do not understand why.

If I look at the actual return values I see the values I expect:

root@0f1bca692c90:/home/work/bin# readelf -h ../src/u-boot/tools/mkimage |grep -q DYN 
root@0f1bca692c90:/home/work/bin# echo $?
0
root@0f1bca692c90:/home/work/bin# readelf -h ../src/u-boot/tools/mkimage.c |grep -q DYN
readelf: Error: Not an ELF file - it has the wrong magic bytes at the start
root@0f1bca692c90:/home/work/bin# echo $?
1

I'm assuming that I misunderstand how Bash interprets return values. I've tried with single and double brackets, and I get the same behavior.

Update

I was able to get it working like this:

RUN for i in ../src/u-boot/tools/*; do if /usr/bin/readelf -h $i | grep -q DYN; then ln -s $i .; fi; done

2 Answers 2

2

The problem has several aspects, but the primary one is that you have chosen the Bash [[ ... ]] test construct and placed the test condition completely inside double-quotes. This means that the entire condition is instead interpreted as a string, which - as it is non-empty - always evaluates to "true".

The second point is that you actually don't need the test construct. Since you base your test on the presence of the DYN property in the output of readelf, which you test via grep, you can simply state

if readelf -h "$i" | grep -q "DYN"
then
   ...
fi

Since the exit status of a pipeline of commands is that of the last command in the pipeline, you will get "true" if DYN was found in the output, and "false" if DYN was not found.

The last point may be a misconception on the value of $?. For shell tests, an exit code of 0 means true while non-zero means false, so the negation (if ! ....) you had is (if I understand your intention correctly) wrong at this point.

0
$ elf_files=( =uname =busybox /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libncurses.so.6.5
    core file.o )
$ for f ($elf_files) readelf -h $f | grep -H --label=$f Type
/usr/bin/uname:  Type:                              DYN (Position-Independent Executable file)
/usr/bin/busybox:  Type:                              EXEC (Executable file)
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2:  Type:                              DYN (Shared object file)
/lib/x86_64-linux-gnu/libc.so.6:  Type:                              DYN (Shared object file)
/lib/x86_64-linux-gnu/libncurses.so.6.5:  Type:                              DYN (Shared object file)
core:  Type:                              CORE (Core file)
file.o:  Type:                              REL (Relocatable file)

So if you're looking for ELF executables but are matching on DYN in the output of readelf -h, you'll have false positives such as /lib/x86_64-linux-gnu/libncurses.so.6.5 which is not meant to be executed and false negatives such as busybox which here is statically linked but is definitely an ELF executable.

Technically, some shared libraries can be executed.

For instance the GNU libc, when executed, reports its version on stdout:

$ /lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Debian GLIBC 2.40-3) stable release version 2.40.
Copyright (C) 2024 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 14.2.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
Minimum supported kernel: 3.2.0
For bug reporting instructions, please see:
<http://www.debian.org/Bugs/>.

You'll see that it has a .interp section. And its entry point in the ELF header is not 0.

ld-linux-x86-64.so.2 is also executable, and can also be seen as a shared lib, but has no .interp section, it is actually the interpreter of dynamically linked executables, but can also be executed directly.

$ file --mime-type $elf_files
/usr/bin/uname:                             application/x-pie-executable
/usr/bin/busybox:                           application/x-executable
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2: application/x-sharedlib
/lib/x86_64-linux-gnu/libc.so.6:            application/x-sharedlib
/lib/x86_64-linux-gnu/libncurses.so.6.5:    application/x-sharedlib
core:                                       application/x-coredump
file.o:                                     application/x-object

Still doesn't identify ld-linux.so and libc as executable, and doesn't show they are ELF files.

$ file $elf_files
/usr/bin/uname:                             ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=5fe9376fe34784ee78ef01f4d4d5b8c8d51fc12d, for GNU/Linux 3.2.0, stripped
/usr/bin/busybox:                           ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=8d6280f3f338cc3c3aa826b100ddf71e5deef2b6, for GNU/Linux 3.2.0, stripped
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), static-pie linked, BuildID[sha1]=9e7570f03ee32b31ce7ab60df72e19390657a7e2, stripped
/lib/x86_64-linux-gnu/libc.so.6:            ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=522aec690df2798fac29fc4608b2c28fd5968271, for GNU/Linux 3.2.0, stripped
/lib/x86_64-linux-gnu/libncurses.so.6.5:    ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=a29e3368f1d02204459225efde34fbb1fdf4b795, stripped
core:                                       ELF 64-bit LSB core file, x86-64, version 1 (SYSV), SVR4-style, from 'sleep 1', real uid: 1000, effective uid: 1000, real gid: 1000, effective gid: 1000, execfn: '/usr/bin/sleep', platform: 'x86_64'
file.o:                                     ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

Better, as you see they're ELF files and that libc has an interpreter, but ld-linux.so is still not clearly identified as executable.

$ for f ($elf_files) readelf -h $f | grep -H --label=$f Entry
/usr/bin/uname:  Entry point address:               0x2730
/usr/bin/busybox:  Entry point address:               0x4a3080
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2:  Entry point address:               0x1bb00
/lib/x86_64-linux-gnu/libc.so.6:  Entry point address:               0x29f30
/lib/x86_64-linux-gnu/libncurses.so.6.5:  Entry point address:               0x0
core:  Entry point address:               0x0
file.o:  Entry point address:               0x0

There, the executable files and only them have an entry point other than 0x0, so it looks like a more reliable way to find ELF executables.

But readelf may not be the best tool for that. For instance, readelf -h will report the ELF header in every file in a ar archive:

$ ar r binaries.a =uname =busybox
ar: creating binaries.a
$ readelf -h binaries.a | grep -e Type -e Entry
  Type:                              DYN (Position-Independent Executable file)
  Entry point address:               0x2730
  Type:                              EXEC (Executable file)
  Entry point address:               0x4a3080

So will objdump -f:

$ objdump -f binaries.a
In archive binaries.a:

uname:     file format elf64-x86-64
architecture: i386:x86-64, flags 0x00000150:
HAS_SYMS, DYNAMIC, D_PAGED
start address 0x0000000000002730


busybox:     file format elf64-x86-64
architecture: i386:x86-64, flags 0x00000102:
EXEC_P, D_PAGED
start address 0x00000000004a3080

But at least the format makes it easier to detect archives. So you can do:

is_ELF_executable() (
  set -o pipefail
  LC_ALL=C objdump -f -- "${1-$REPLY}" 2> /dev/null | awk '
    NR == 1 && /^In archive/ {exit}
    /^start address 0x.*[^0]/ {found = 1; exit}
    END {exit (!found)}'
)

Then:

if is_ELF_executable /bin/ls; then
  echo Yep.
fi

By using ${1-$REPLY}, I can also use it as a zsh qualifier function. For instance, to find the ELF executables with some execute permissions in /lib/x86_64-linux-gnu:

$ print -rl /lib/x86_64-linux-gnu/*(*+is_ELF_executable)
/lib/x86_64-linux-gnu/cups-pk-helper-mechanism
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
/lib/x86_64-linux-gnu/libc.so.6

If I don't limit to the ones with execute permissions, I find more:

$ print -rl /lib/x86_64-linux-gnu/*(.+is_ELF_executable)
/lib/x86_64-linux-gnu/cups-pk-helper-mechanism
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
/lib/x86_64-linux-gnu/libcap.so.2.66
/lib/x86_64-linux-gnu/libc.so.6
/lib/x86_64-linux-gnu/libfreehand-0.1.so.1.0.2
/lib/x86_64-linux-gnu/libiptcdata.so.0.3.3
/lib/x86_64-linux-gnu/libpsx.so.2.66
/lib/x86_64-linux-gnu/libptytty.so.0
/lib/x86_64-linux-gnu/libQt5Core.so.5.15.13
/lib/x86_64-linux-gnu/libQt6Core.so.6.7.2
/lib/x86_64-linux-gnu/libvidstab.so.1.1
/lib/x86_64-linux-gnu/libx86.so.1
/lib/x86_64-linux-gnu/libxcb-xrm.so.0.0.0

Interestingly, not all have a .interp section. Except for ld-linux.so, all those that don't segfault when executed (here tested by making an executable copy).

Taking libptytty.so.0 as an example, if I build it from source with dpkg-buildpackage, I get a 0x0 entry point, so I don't know why it's not 0 in the system's one, but in any case, that comes to show that the heuristic can still be refined any improved.

Edit. Looks like those shared libraries with an entry point that is not 0 are those that were built by older versions of GNU ld (2.37 for that libptytty.so.0). On Debian 11, I find that all shared libraries had a non-zero entry point, so while that method appears to work on FreeBSD or recent GNU/Linux systems, it doesn't in older systems.

A method checking:

  • "executable" mentioned in the ELF type
  • presence of a .interp section

as heuristics would be more reliably, but give a false negative for the dynamic linker.

is_ELF_executable() {
  LC_ALL=C readelf -hS -- "${1-$REPLY}" 2> /dev/null | perl -lne '
    if ($. == 1) {
      last unless /^ELF/; # rule out non-ELF including archives
    } elsif (/^\s*\[ *\d+\]\s*\.interp\s/ || /^\s*Type:.*(?i:exec)/) {
      $ok = 1; last;
    }
    END {exit !$ok}'
}

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.