Have you ever mounted a filesystem and wondered why a perfectly executable script suddenly refuses to run?
For example:
$ ls -l /data/test.sh
-rwxr-xr-x 1 user user 32 Sep 20 10:20 /data/test.sh
$ /data/test.sh
-bash: /data/test.sh: Permission denied
The file has the execute bit.
The owner is correct.
The script itself looks fine.
So why does Linux refuse to run it?
One possible reason is the filesystem was mounted with:
noexec
You may have seen it in /etc/fstab:
/dev/sdb1 /data xfs defaults,noexec 0 0
At first glance, noexec sounds simple: “don’t execute files from this filesystem.”
That’s roughly correct, but there is more to it.
See also: Mastering the Linux Command Line — Your Complete Free Training Guide
Understanding what noexec actually does—and what it doesn’t do—is important when you’re hardening Linux servers, mounting shared storage, working with temporary directories, or troubleshooting a mysterious Permission denied.
Let’s look at it with real examples.
Table of Contents
What Does noexec Mean?
noexec is a filesystem mount option.
When a filesystem is mounted with:
noexec
Linux prevents programs located on that filesystem from being directly executed through the normal executable-loading mechanism.
For example:
mount -o noexec /dev/sdb1 /data
Now /data is mounted with the noexec option.
If you have:
/data/hello
and it is an executable binary:
$ ls -l /data/hello
-rwxr-xr-x 1 user user 16784 Sep 20 10:30 /data/hello
you may still get:
$ /data/hello
-bash: /data/hello: Permission denied
The important point is:
The file’s execute permission is not enough.
The filesystem itself says:
Don’t execute programs directly from here.
A Simple Example
Let’s create a small shell script.
mkdir /tmp/noexec-test
Create the script:
cat > /tmp/noexec-test/hello.sh <<'EOF'
#!/bin/bash
echo "Hello from the script"
EOF
Make it executable:
chmod +x /tmp/noexec-test/hello.sh
Normally:
/tmp/noexec-test/hello.sh
prints:
Hello from the script
Now let’s put the directory on a noexec filesystem.
For example, if /data is mounted with noexec:
cp /tmp/noexec-test/hello.sh /data/
Then:
chmod +x /data/hello.sh
Check the permissions:
ls -l /data/hello.sh
You might see:
-rwxr-xr-x 1 user user 35 Sep 20 10:30 /data/hello.sh
Try to run it:
/data/hello.sh
You may get:
-bash: /data/hello.sh: Permission denied
This is the part that often confuses people.
The file has:
x
permission.
But the filesystem has:
noexec
The filesystem-level restriction prevents direct execution.
Check Whether a Filesystem Uses noexec
Before troubleshooting, check the mount options.
The easiest command is:
findmnt /data
For example:
TARGET SOURCE FSTYPE OPTIONS
/data /dev/sdb1 xfs rw,relatime,noexec
There it is:
noexec
You can also use:
mount | grep ' /data '
Example:
/dev/sdb1 on /data type xfs (rw,relatime,noexec)
Another useful command is:
findmnt -no OPTIONS /data
which might return:
rw,relatime,noexec
If you’re troubleshooting a particular file, findmnt is often the fastest way to determine which filesystem contains it:
findmnt /data/hello.sh
noexec in /etc/fstab
On a server, noexec is often configured in /etc/fstab.
For example:
/dev/sdb1 /data xfs defaults,noexec 0 0
The important part is:
defaults,noexec
The next time the system mounts /data, the filesystem will use the noexec option.
You can see the current configuration with:
grep -v '^#' /etc/fstab
For example:
/dev/mapper/vg_data-lv_data /data xfs defaults,noexec 0 0
After changing /etc/fstab, don’t immediately reboot just to test it.
You can usually remount the filesystem:
mount -o remount /data
Then verify:
findmnt /data
You should see:
noexec
in the mount options.
What Happens When Linux Executes a File?
To understand noexec, it’s useful to look at what happens when you type:
/data/hello
Your shell eventually asks the Linux kernel to execute the file.
At the system-call level, this involves the execve() family of system calls.
A simplified view looks like this:
Shell
|
| execute /data/hello
v
execve()
|
v
Linux kernel
|
+--> Check file permissions
|
+--> Check filesystem mount flags
|
+--> Load executable
|
v
Program starts
With noexec, the filesystem’s mount flags tell the kernel that direct execution isn’t allowed from that filesystem.
So even if:
-rwxr-xr-x
says the file is executable, the mount policy can still prevent execution.
noexec Does Not Remove the Execute Bit
This distinction is important.
Suppose:
chmod +x /data/test.sh
Then:
ls -l /data/test.sh
shows:
-rwxr-xr-x
Mounting the filesystem with:
noexec
doesn’t change the file’s permissions.
You can still see:
x
in the permission string.
The restriction exists at the filesystem mount level.
Think about it like this:
File permissions
+
Filesystem mount policy
=
Whether direct execution is allowed
Both layers matter.
noexec Is Not the Same as chmod -x
These two approaches look similar but work differently.
Remove execute permission:
chmod -x /data/test.sh
Now:
ls -l /data/test.sh
might show:
-rw-r--r--
The file itself no longer has execute permission.
With noexec, however:
ls -l /data/test.sh
can still show:
-rwxr-xr-x
while:
/data/test.sh
fails.
The difference is:
chmod -x
|
+--> File-level permission
noexec
|
+--> Filesystem-level execution restriction
What About Shell Scripts?
This is where things get interesting.
Suppose /data is mounted with noexec.
You have:
/data/test.sh
with:
#!/bin/bash
echo "Hello"
Direct execution:
/data/test.sh
may fail:
Permission denied
But try:
bash /data/test.sh
You may get:
Hello
Why?
Because you’re not asking the kernel to execute /data/test.sh as the program.
You’re executing:
/bin/bash
and telling Bash to read /data/test.sh as input.
The flow is different:
/data/test.sh
|
X
direct execution blocked by noexec
versus:
/bin/bash
|
+--> read /data/test.sh
|
+--> interpret commands
This is one of the most important things to understand about noexec.
noexec does not mean “files on this filesystem cannot be read or interpreted.”
It primarily restricts direct execution.
Example: noexec Does Not Stop Bash From Reading a Script
Suppose:
findmnt /data
returns:
/data /dev/sdb1 xfs rw,relatime,noexec
And:
cat /data/test.sh
works:
#!/bin/bash
echo "Hello"
Then:
/data/test.sh
fails:
Permission denied
But:
bash /data/test.sh
works.
This is expected.
The filesystem is not saying:
“You cannot read this file.”
It’s saying:
“Don’t directly execute programs from this filesystem.”
What About Python?
The same concept applies to interpreted languages.
Suppose you have:
/data/test.py
with:
#!/usr/bin/python3
print("Hello")
Direct execution:
/data/test.py
may fail when /data is mounted with noexec.
But:
python3 /data/test.py
may work.
Again:
python3
|
+--> reads /data/test.py
The Python interpreter itself is being executed from another filesystem.
The script is simply being read as data.
Does noexec Stop ELF Binaries?
Yes, this is one of the primary purposes of noexec.
Suppose:
/data/myprogram
is an ELF executable:
file /data/myprogram
might return:
/data/myprogram: ELF 64-bit LSB pie executable, x86-64, ...
And:
ls -l /data/myprogram
shows:
-rwxr-xr-x
If /data is mounted with noexec:
/data/myprogram
can fail with:
Permission denied
even though the binary itself is executable.
Copy the Binary Somewhere Else
Here’s a simple demonstration.
Suppose:
/data/hello
doesn’t run:
/data/hello
returns:
Permission denied
Copy it to /tmp:
cp /data/hello /tmp/hello
Make sure /tmp isn’t also mounted with noexec:
findmnt /tmp
Then:
/tmp/hello
may run successfully.
Nothing changed about the binary.
You changed the filesystem from which you’re executing it.
That’s a very useful troubleshooting technique.
noexec and /tmp
One common place where administrators use noexec is /tmp.
You may see something like:
tmpfs /tmp tmpfs defaults,nodev,nosuid,noexec 0 0
The three options are often discussed together:
nodev
nosuid
noexec
They solve different problems.
nodev
Prevents device files from being interpreted as devices on the filesystem.
nosuid
Disables the effect of set-user-ID and set-group-ID bits.
noexec
Prevents direct execution of programs from the filesystem.
So:
nodev
-> device files
nosuid
-> SUID/SGID behavior
noexec
-> direct program execution
They are separate controls.
A Practical /tmp Example
Suppose /tmp is mounted as:
tmpfs on /tmp type tmpfs (rw,nosuid,nodev,noexec,relatime)
Someone downloads a binary:
curl -o /tmp/tool https://example.com/tool
Then:
chmod +x /tmp/tool
and:
/tmp/tool
fails:
Permission denied
But if they copy it to a filesystem mounted without noexec:
cp /tmp/tool ~/tool
chmod +x ~/tool
~/tool
it may run.
Again, the important variable is the filesystem mount option.
noexec Does Not Mean “No Code Can Run”
This is perhaps the biggest misconception.
Suppose:
/home/user
is mounted with noexec.
It does not necessarily mean the user cannot run programs.
For example:
/usr/bin/bash
can still run if /usr is mounted with normal execution permissions.
Then Bash can read:
/home/user/test.sh
as a script.
Similarly:
python3 /home/user/test.py
can still work.
The same applies to programs that load libraries or data from other filesystems, although there are additional details depending on how the files are used.
How to Find Which Mount Contains a File
When you see:
Permission denied
for an executable, don’t immediately run:
chmod +x
First check the filesystem:
findmnt /path/to/file
For example:
findmnt /opt/myapp/bin/server
You might get:
TARGET SOURCE FSTYPE OPTIONS
/opt /dev/sdc1 xfs rw,relatime,noexec
Now you know why the binary cannot execute.
You can also check the mount point with:
df -T /opt/myapp/bin/server
and:
mount | grep ' /opt '
A Realistic Troubleshooting Example
Imagine an application deployment fails.
The deployment system reports:
Permission denied
You check:
ls -l /opt/app/bin/start.sh
and see:
-rwxr-xr-x 1 app app 1024 Sep 20 10:20 /opt/app/bin/start.sh
At first glance, everything looks correct.
You try:
/opt/app/bin/start.sh
and get:
Permission denied
Don’t immediately change permissions.
Check:
findmnt /opt
Suppose the result is:
TARGET SOURCE FSTYPE OPTIONS
/opt /dev/sdc1 xfs rw,relatime,noexec
Now the problem is obvious.
The file has execute permission.
But /opt is mounted with noexec.
How to Temporarily Remove noexec
If you have the appropriate administrative access and need to test whether noexec is the cause, you can remount the filesystem.
For example:
sudo mount -o remount,exec /opt
Then verify:
findmnt /opt
Try the program again:
/opt/app/bin/start.sh
If it now works, you’ve confirmed that noexec was involved.
However, don’t treat this as a permanent fix without understanding why noexec was configured.
On production systems, mount options are often part of the security baseline.
Changing /etc/fstab
If you have:
/dev/sdc1 /opt xfs defaults,noexec 0 0
and you remove noexec:
/dev/sdc1 /opt xfs defaults 0 0
you can apply the change with:
sudo mount -o remount /opt
Then check:
findmnt /opt
But be careful.
There is a difference between:
mount -o remount,exec /opt
and permanently changing:
/etc/fstab
The first changes the current mount.
The second changes what happens after the next mount or reboot.
noexec and Container Environments
noexec can also become relevant when working with containers.
For example, an application may mount a host directory into a container:
/data:/app/data
If the underlying filesystem has noexec, applications may encounter execution problems depending on where binaries are stored and how they are launched.
This can be especially confusing because the container configuration may look correct.
If an application says:
permission denied
while trying to execute a binary, inspect the host filesystem and mount options as well as the container configuration.
The important lesson is:
A container does not make filesystem mount flags disappear.
The exact behavior depends on the container runtime, mount namespace, and how the filesystem is exposed to the container.
noexec and Network Filesystems
You may also encounter noexec with NFS or other shared filesystems.
For example:
server:/export/tools /tools nfs defaults,noexec 0 0
This can be useful when /tools is intended primarily as shared data storage and users shouldn’t directly execute arbitrary programs from it.
But it can also break applications that expect to execute binaries or scripts directly from the mount.
If an application works on local storage but fails after moving files to NFS, check:
findmnt /tools
and:
mount | grep /tools
before assuming the application itself is broken.
noexec vs nosuid vs nodev
These three options often appear together:
nodev,nosuid,noexec
But they should not be treated as interchangeable.
| Option | Main purpose |
|---|---|
noexec | Prevent direct execution of programs |
nosuid | Ignore SUID/SGID privilege changes |
nodev | Don’t interpret device files as devices |
For example:
/dev/sdb1 /data xfs defaults,nodev,nosuid,noexec 0 0
creates a more restrictive mount.
This can make sense for filesystems used for uploads, temporary data, shared files, or other content that should not normally contain executable programs.
How to Test noexec Yourself
If you want to understand noexec rather than just memorize it, build a small test environment.
Create a filesystem image:
dd if=/dev/zero of=/tmp/test.img bs=1M count=100
Create a filesystem:
mkfs.ext4 /tmp/test.img
Create a mount point:
sudo mkdir -p /mnt/noexec-test
Mount it:
sudo mount -o loop,noexec /tmp/test.img /mnt/noexec-test
Verify:
findmnt /mnt/noexec-test
You should see something similar to:
TARGET SOURCE FSTYPE OPTIONS
/mnt/noexec-test /dev/loop0 ext4 rw,relatime,noexec
Now create a script:
cat > /mnt/noexec-test/test.sh <<'EOF'
#!/bin/bash
echo "noexec test"
EOF
Make it executable:
chmod +x /mnt/noexec-test/test.sh
Try:
/mnt/noexec-test/test.sh
You should see:
Permission denied
Now try:
bash /mnt/noexec-test/test.sh
You should see:
noexec test
This small experiment demonstrates the difference better than a long definition.
When Should You Use noexec?
noexec makes sense when a filesystem is primarily intended for data rather than executable programs.
Common examples include:
- Temporary storage
- User-uploaded files
- Shared data directories
- Certain backup locations
- Application data directories
- Some removable storage
- Specific network filesystems
For example:
/dev/sdb1 /data xfs defaults,nodev,nosuid,noexec 0 0
can make sense if /data is strictly intended for application data.
But don’t blindly add noexec to every filesystem.
Some applications legitimately need to execute files from particular locations.
The Bottom Line
The easiest way to remember noexec is:
noexecis a filesystem mount restriction, not a file permission.
A file can have:
-rwxr-xr-x
and still fail to execute when its filesystem is mounted with:
noexec
Check it with:
findmnt /path/to/file
For example:
findmnt /data/test.sh
If you see:
noexec
you’ve found an important clue.
And remember the difference between:
/data/test.sh
and:
bash /data/test.sh
The first asks Linux to execute the file directly.
The second executes Bash and asks Bash to read the file.
That’s why noexec can block the first command while allowing the second.
Once you understand that distinction, many of those mysterious:
Permission denied
errors become much easier to troubleshoot.


