chmod u+s my-script.sh runs without error, ls -l correctly shows the s bit on the file, and yet running the script never executes it with the expected owner privileges. This isn’t a misunderstood permissions bug: the Linux kernel deliberately ignores the setuid bit on any interpreted script, a security protection, not an oversight.

What setuid actually does on a binary

On a compiled executable, the setuid bit runs the process with the file owner’s privileges rather than those of the user launching it, the classic example being passwd (owned by root, letting any user modify their own password stored in a file only root can write).

# Setuid bit visible: the "s" replaces the owner's "x"
ls -l /usr/bin/passwd
# -rwsr-xr-x 1 root root ... /usr/bin/passwd

Why the kernel ignores it on a script

A script always starts with a shebang line (#!/bin/bash) that the kernel interprets by launching the designated interpreter with the script as an argument. Between the moment the kernel opens the setuid file and the moment the interpreter actually executes it, a window of time exists that an attacker can exploit (swapping the script for a symlink to a different file right after it’s opened, an exploit class called a setuid script race condition): rather than closing that window case by case, the Linux kernel simply disables setuid entirely for any file starting with a shebang, and has for decades.

# The bit stays visible in ls -l, but the kernel
# silently ignores it at execution time: no error,
# the script just runs with the caller's own privileges
chmod u+s my-script.sh
./my-script.sh

The fix: a compiled wrapper, not the script itself

The standard solution is a small C wrapper, compiled to a binary, that carries the setuid bit itself and simply calls the target script: the kernel applies setuid normally to this compiled binary, without the race condition window specific to interpreted files. This indirection isn’t a workaround hack, it’s the intended mechanism for exactly this use case.

The connection to sudo

This protection is part of why sudo remains the recommended path for controlled privilege elevation rather than setuid on a script: sudo is itself a compiled binary carrying setuid, purpose-built to handle this elevation in an audited way, with a sudoers file that precisely defines who can do what, rather than working around the kernel protection with a poorly thought-out homemade wrapper.

Takeaway

The setuid bit stays visible in ls -l on a script, but the Linux kernel silently ignores it at execution time, a deliberate protection against an exploitable race condition class on any file interpreted via a shebang. The standard fix is a C-compiled wrapper carrying the setuid bit, never the script itself. This same protection is one of the reasons sudo, itself a setuid binary built for exactly this purpose, remains the recommended path for controlled privilege elevation rather than a homemade workaround.