Executing a bash script or a c binary on a file system with noexec option

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.


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.


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