14

Can anyone explain in details what is going on with the following. Let's imagine I am mounting a directory with noexec option as follows:

mount -o noexec /dev/mapper/fedora-data /data

So to verify this I ran mount | grep data:

/dev/mapper/fedora-data on /data type ext4 (rw,noexec,relatime,seclabel,data=ordered)

Now within /data I'm creating a simple script called hello_world as follows:

#!/bin/bash

echo "Hello World"
whoami

So I made the script executable by chmod u+x hello_world (this will however have no effect on a file system with noexec options) and I tried running it:

# ./hello_world
-bash: ./hello_world: Permission denied

However, prepanding bash to the file yields to:

# bash hello_world
Hello World
root

So then I created a simple hello_world.c with the following contents:

#include <stdio.h>

int main()
{
    printf("Hello World\n");
    return 0;
}

Compiled it using cc -o hello_world hello_world.c

Now running:

# ./hello_world
-bash: ./hello_world: Permission denied

So I tried to run it using

/lib64/ld-linux-x86-64.so.2 hello_world

The error:

./hello_world: error while loading shared libraries: ./hello_world: failed to map segment from shared object: Operation not permitted

So this is of course true since ldd returns the following:

ldd hello_world
ldd: warning: you do not have execution permission for `./hello_world'
    not a dynamic executable

On another system where noexec mount option doesn't apply I see:

ldd hello_world
    linux-vdso.so.1 (0x00007ffc1c127000)
    libc.so.6 => /lib64/libc.so.6 (0x00007facd9d5a000)
    /lib64/ld-linux-x86-64.so.2 (0x00007facd9f3e000)

Now my question is this: Why does running a bash script on a file system with noexec option work but not a c compiled program? What is happening under the hood?

2
  • note: chmod o+x «filename» will not give execute permission to file owning user, or group. Use chmod ugo+x «filename» or chmod +x «filename» Dec 22, 2018 at 11:54
  • You are right. I used chmod u+x hello_world but forgot to change that in the question. Thanks for pointing that out. Dec 22, 2018 at 12:13

5 Answers 5

29

What's happening in both cases is the same: to execute a file directly, the execute bit needs to be set, and the filesystem can't be mounted noexec. But these things don't stop anything from reading those files.

When the bash script is run as ./hello_world and the file isn't executable (either no exec permission bit, or noexec on the filesystem), the #! line isn't even checked, because the system doesn't even load the file. The script is never "executed" in the relevant sense.

In the case of bash ./hello_world, well, The noexec filesystem option just plain isn't as smart as you'd like it to be. The bash command that's run is /bin/bash, and /bin isn't on a filesystem with noexec. So, it runs no problem. The system doesn't care that bash (or python or perl or whatever) is an interpreter. It just runs the command you gave (/bin/bash) with the argument which happens to be a file. In the case of bash or another shell, that file contains a list of commands to execute, but now we're "past" anything that's going to check file execute bits. That check isn't responsible for what happens later.

Consider this case:

$ cat hello_world | /bin/bash

… or for those who do not like Pointless Use of Cat:

$ /bin/bash < hello_world

The "shbang" #! sequence at the beginning of a file is just some nice magic for doing effectively the same thing when you try to execute the file as a command. You might find this LWN.net article helpful: How programs get run.

3
  • Thank you for your detailed explenation and the link to LWN.net article is very helfull! Dec 22, 2018 at 8:13
  • 2
    If my guess is correct, what also plays a role is that /bin/bash is stored on filesystem without noexec, whereas OP's script is on noexec filesystem. Dec 22, 2018 at 23:37
  • @SergiyKolodyazhnyy Yes.
    – mattdm
    Dec 23, 2018 at 2:25
15

Previous answers explain why the noexec setting doesn't prevent a script from being run when the interpreter (in your case /bin/bash) is explicitly called from the command line. But if that was all there was to it, this command would have worked as well:

/lib64/ld-linux-x86-64.so.2 hello_world

And as you noted that doesn't work. That's because noexec has another effect as well. The kernel will not allow memory mapped files from that file system with PROT_EXEC enabled.

Memory mapped files are used in multiple scenarios. The two most common scenarios are for executables and libraries. When a program is started using the execve system call, the kernel will internally create memory mappings for the linker and executable. Any other libraries needed are memory mapped by the linker through the mmap system call with PROT_EXEC enabled. If you tried to use a library from a filesystem with noexec the kernel would refuse to do the mmap call.

When you invoked /lib64/ld-linux-x86-64.so.2 hello_world the execve system call will only create a memory mapping for the linker and the linker will open the hello_world executable and attempt to create a memory mapping in pretty much the same way it would have done for a library. And this is the point at which the kernel refuse to perform the mmap call and you get the error:

./hello_world: error while loading shared libraries: ./hello_world: failed to map segment from shared object: Operation not permitted

The noexec setting still allows memory mappings without execute permission (as is sometimes used for data files) and it also allows normal reading of files which is why bash hello_world worked for you.

1
  • Fascinating! I learned a lot here. Thank you. Feb 27, 2020 at 23:09
2

Executing command on this way:

bash hello_world

you make bash read from file hello_world (which is not forbidden).

In other cases OS try to run this file hello_world and fail because of noexec flag

2
  • Well the kernel will check the shebang to decide what instructions to expect/run. So running ./hello_world would mean the same right or am I missing the point here? Dec 21, 2018 at 21:55
  • @ValentinBajrami, no, they execute on different way. Think about no execution flag on the file. Dec 21, 2018 at 21:57
2

When you run the script via bash It just reads in the file and interprets it.

However when you pass the name to the kernel -- it really inspects the file for "#!" and loads the interpreter specified according to the kernel option "CONFIG_BINFMT_SCRIPT". It says:

Say Y here if you want to execute interpreted scripts starting with "#!" followed by the path to an interpreter.

You can build this support as a module; however, until that module gets loaded, you cannot run scripts. Thus, if you want to load this module from an initramfs, the portion of the initramfs before loading this module must consist of compiled binaries only.

Most systems will not boot if you say M or N here. If unsure, say Y.

The above is the help text associated with this option. For another interesting difference. I wrote my script:

> cat myprog.sh
#!/bin/cat

echo "Hello World"
> chmod +x myprog.sh

Running it with bash still runs bash's interpreter:

> bash myprog.sh
Hello World

However the kernel now does:

> myprog.sh
#!/bin/cat

echo "Hello World"

The kernel printed out the script including the 1st line because it called 'cat'.

In the case of the C program , you aren't calling an interpreter to run the binary. The kernel tries to run it directly. Still if you loaded all of your executable into memory using some debuggers, you could still "run" your program as it's being loaded via the debugger.

The 'noexec' option is like turning off the execute bit on your binary and disables the kernel from running the binary "natively".

This does make a difference, BTW, if your program had a SetUID bit set on the program -- loading it with an interpreter won't set your UID, only when the kernel loads it can that privilege be enabled.

FWIW -- windows has the same type of mechanism.

If you add ".sh" as a "executable suffix" like ".exe" or ".vbs", windows will automatically run your file according to how you have setup ".sh" files to be executed. Theoretically, you could setup ".txt" files to automatically be typed out if you entered their name on the command line.

Similarly you could put some short call to a program to print out text files to the screen. That's a reason not to leave your self logged in at a public location.

-2

Because the bash executable doesn't reside on said filesystem.

3
  • 2
    That is true but running ./hello_world would also us /bin/bash which doesn't reside on a file system with noexec option mounted. If that is true then the c program would run as well corerct? Dec 21, 2018 at 21:56
  • 1
    Nope. That's not the way it works.
    – tink
    Dec 21, 2018 at 21:58
  • 3
    @ValentinBajrami The shebang line in the script isn't read until after the noexec and execute-bit hurdles have been cleared.
    – Blrfl
    Dec 22, 2018 at 12:38

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .