Search code examples
linuxpermissionsshared-librariesfile-permissionsglibc

Why I can execute files without execution permission if I use ld-2.31.so?


I am a bit shocked about this discover. If I don't have execution permission on a file, say, foo... I can still execute it with:

/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 ./foo

(ld-linux-x86-64.so.2 is a symlink to ld-2.31.so).

Since I don't think I know what's really happening in that command, since I don't really know what ld-2.31.so is, can you explain why can that execute files? Is this what happens in background that we don't see, when we just do ./foo ? What is ld-2.31.so ? What's its role? Are there any other "tricks" and libraries(I don't know their purpose, I am a newbie actually) that do similar jobs?


Solution

  • Is this what happens in background that we don't see, when we just do ./foo ?

    Kind of. What happens is that the kernel

    1. checks that ./foo has execute permissions
    2. mmaps the file according to the program headers (readelf -Wl foo) in it
    3. notices that the file has PT_INTERP segment, reads its contents (usually /lib64/ld-linux-x86-64.so.2 on Linux x86_64 systems)
    4. mmaps the interpreter according to its own program headers
    5. transfers control to the interpreter in user space. It is then up to the interpreter to finish loading any needed shared libraries and initialize them, and finally transfer control to foos _start.

    Because of this startup sequence, for a dynamically linked ./foo, 10s of thousands of instructions execute before _start.

    This is also how it's possible to have ./foo executable, but for attempts to run it produce "No such file or directory". You can try this for yourself:

    gcc main.c -Wl,--dynamic-linker=/foo/bar/baz
    ls -l a.out
    ./a.out
    

    I can still execute it with: /path/to/ld.so ./foo

    In this case, you are skipping a few steps from the "normal" execution path.

    From the kernel perspective, ld.so is a statically linked binary, and it is executable, so the kernel is happy to mmap its segments and transfer control to it.

    And ld.so itself recognizes that it's been invoked in "unusual" way with command line arguments, so it proceeds to perform additional mappings of ./foos segments (which would have already been performed by the kernel otherwise). In order to perform the mappings, ld.so just needs a file; it doesn't care about permissions on that file.

    This mode of operation proved very useful for e.g. testing new versions of ld.so itself.

    It also allows you to run a binary with missing interpreter (the a.out above): /lib64/ld-linux-x86-64.so.2 ./a.out would work when ./a.out doesn't.