Special Permissions: setuid, setgid, and the Sticky Bit
The ordinary rwx bits decide whether a process can read, write, or execute a file. Two further questions come up in practice: which identity does an executable run with once it starts, and who is allowed to delete a file in a shared directory? Linux answers both with three special mode bits: set-user-ID (setuid), set-group-ID (setgid), and the sticky bit.
File Permissions covered how a process's identity is matched against rwx, and Ownership covered where that identity's UID/GID actually come from. Special bits are stored in the same mode field but change how the kernel interprets execution and directory operations, and they introduce their own attack surface at the same time.
Warning
Placeholders such as <path> and <group> below are not literal values — replace them with a verified real value before running anything. Do not copy a command containing < or > straight into a shell.
This article covers how these three bits sit alongside rwx and how to read their octal values; the s, S, t, and T characters in ls -l; how setuid and setgid change a process's effective UID/GID on an executable; how setgid on a directory makes new files inherit its group; what the sticky bit protects and what it does not; and how to audit special bits with stat, find, findmnt, and package tools, backed by a backup, a verification step, and a rollback.
Where Special Bits Sit Inside the Mode
An ordinary mode is written as three octal digits: owner, group, and other. The special bits occupy a fourth digit in front of those three:
| Special bit | Octal value | Symbolic change | Primary purpose |
|---|---|---|---|
| setuid | 4 |
u+s, u-s |
run an executable with its owner's effective UID |
| setgid | 2 |
g+s, g-s |
run an executable with its group, or make a directory inherit its group |
| sticky | 1 |
+t, -t |
restrict deletion and renaming in a shared directory |
For example:
These numbers add together: 4 + 2 = 6, 2 + 1 = 3, and all three combined make 7. A leading zero, as in 04755, is sometimes used to make it visually clear that the number is octal.
Check a mode in more than one form:
stat %a gives the raw bits, while %A and ls -l give a symbolic view. A + at the end of an ls -l line can indicate an ACL; special bits do not replace ACLs.
What s, S, t, and T Mean
A special bit does not get its own column — it is shown in place of the corresponding execute character:
sin the owner execute slot: setuid is set, and owner execute is also set.Sin the owner execute slot: setuid is set, but owner execute is not.sin the group execute slot: setgid is set, and group execute is also set.Sin the group execute slot: setgid is set, but group execute is not.tin the other execute slot: sticky is set, and other execute is also set.Tin the other execute slot: sticky is set, but other execute is not.
For example:
-rwsr-xr-x 4755 root:root /usr/bin/passwd
drwxrws--- 2770 root:catalog-editors /srv/catalog/shared
drwxrwxrwt 1777 root:root /tmp
-rwS------ 4600 ali:ali mode-sample
The capital S in the last line does not mean "a stronger setuid." It means the opposite: without owner execute, this regular file will not run as an executable at all, and this mode combination is usually a mistake or an unfinished setting.
Note
On a regular file, setgid combined with a cleared group execute bit — the group S shown above — was historically also used to request mandatory file locking. That mechanism is considered unreliable and has not been supported since Linux 5.15. Do not design new locking around this mode combination on a current system.
How setuid Works on an Executable
A process's real UID reflects who started it; its effective UID is the identity used in most access checks. In an ordinary executable, both belong to the caller. When a native executable with the setuid bit runs through execve(2), the kernel replaces the effective UID with the file's owner; the real UID is unchanged.
Simplified flow:
ali runs the executable
│
▼
the kernel checks mode and mount/process restrictions
│
▼
if setuid is honored: real UID=ali, effective UID=the file's owner
│
▼
the program performs only whatever privileged action its own code allows
A common example on Ubuntu:
A representative result:
When an ordinary user changes their own password, passwd needs to update protected credential data. Setuid root does not hand the user an arbitrary root shell — the program itself is responsible for strictly checking its arguments, the calling user's identity, and which operations are actually allowed.
Note
The exact mode depends on the distribution, package version, and hardening policy. Some historical programs use Linux capabilities, a privileged service, or another mechanism instead of setuid. Do not treat a list found online as ground truth for your system — check the local file.
Setuid Does Not Always Apply
Linux will not grant new authority through setuid/setgid in these cases:
- the file's file system is mounted with
nosuid; - the calling process has
no_new_privsset; - the process is in a relevant
ptrace-monitored state; - the executable is an interpreted script — Linux ignores setuid/setgid bits on scripts.
Checking the mount context, read-only:
If nosuid appears, the privilege transition does not happen even though the bit is stored on disk. Depending on the kernel version, the executable may then run with ordinary privileges, or fail outright.
Danger
Do not try to build a privilege mechanism by running chmod u+s on a shell script. Linux ignores the bit for scripts; behavior on other platforms may differ. For a genuinely privileged task, have a narrow sudoers rule, a reviewed native helper, a system service, or, where appropriate, a minimal capability model go through a security review instead.
Why a setuid Executable Is Risky
A bug in argument parsing, environment handling, temporary file creation, a race condition, a library, or an external command called from a setuid root program can all lead an ordinary user to root authority. This is especially true if an unprivileged account can write to the file or one of its parent directories, letting an attacker replace the executable outright.
Before adding a setuid bit, at minimum:
- check whether the task can be expressed more narrowly with
sudo, a service, or a capability; - audit the ownership and mode of the executable and every parent directory;
- establish where the binary came from and which package owns it;
- check the mount, MAC policy, and
no_new_privscontext; - record the change in a change record with a monitoring and rollback plan.
setgid on an Executable versus on a Directory
setgid on an executable tells the kernel to replace the process's effective GID with the file's group. The real GID and supplementary groups are not replaced on their own. In symbolic form, the s appears in the group execute slot:
But setgid on a directory does not elevate a process's identity at all. It makes new files and subdirectories inherit the parent directory's group instead of the creating process's own group. New subdirectories usually inherit the setgid bit too.
This is exactly how the shared-directory problem from Ownership gets solved:
stat -c '%A %a %U:%G %n' -- <team_directory>
chmod g+s -- <team_directory>
stat -c '%A %a %U:%G %n' -- <team_directory>
For example, if a 2770 directory owned by root:catalog-editors gets a new file from a group member, that file's group is normally catalog-editors — not the user's own primary group.
Important limits:
- setgid only affects objects created afterward — it does not change existing files' groups;
- it does not grant the new file's
rwbits — those come from the requested mode,umask, and any default ACL; - a new subdirectory inherits setgid, but the access bits inside it still depend on
umaskor a default ACL; - if a user is not a member of the directory's group, or an ACL does not allow it, setgid alone does not create access for them.
In short, setgid handles "keep the group consistent." Granting write access to that group is a separate matter of mode, ACL, and the umask policy covered in umask.
Note
GNU chmod can preserve a directory's existing setuid/setgid bits through certain three- or four-digit numeric mode changes. When the intent is to remove a bit, use an explicit symbolic form such as chmod g-s -- <directory> and confirm with stat. Directory setuid semantics are not consistent across platforms; Linux uses setgid for the shared-directory case.
The Sticky Bit Protects Directory Entries
A user with w and x on a directory can normally delete or rename any entry in it, even one owned by someone else. The sticky bit narrows that rule: removing or renaming a file requires being the file's owner, the directory's owner, or an appropriately privileged process.
The classic example:
A typical result on Ubuntu Server:
1777 lets every user use the directory; the trailing t restricts one user from deleting or renaming another user's directory entry there.
The sticky bit does not:
- restrict reading a file's content;
- stop a user with
won the file itself from modifying that content; - automatically fix a program that guesses filenames, follows symlinks carelessly, has race conditions, or creates temp files unsafely;
- affect copying to another directory or a file's own access mode.
So 1777 is not a secure vault. A program should create a temporary file through a safe API, with an unpredictable name and an appropriate mode such as 0600.
A Safe Lab
This lab works only inside ~/lab/special-permissions, does not need sudo, and does not create a privileged executable. The setuid sample belongs to the current user rather than granting any new identity — it exists only to study how the mode indicator looks.
First confirm the target does not already exist:
If it returns 0, continue:
mkdir -m 0700 -p ~/lab/special-permissions
mkdir ~/lab/special-permissions/shared
mkdir ~/lab/special-permissions/dropbox
chmod 2770 ~/lab/special-permissions/shared
chmod 1777 ~/lab/special-permissions/dropbox
touch ~/lab/special-permissions/shared/report.txt
mkdir ~/lab/special-permissions/shared/reports
touch ~/lab/special-permissions/mode-sample
chmod 4600 ~/lab/special-permissions/mode-sample
See the result in one consistent format:
A representative, relevant excerpt:
d drwxrws--- 2770 ali:ali /home/ali/lab/special-permissions/shared
f -rw-r--r-- 644 ali:ali /home/ali/lab/special-permissions/shared/report.txt
d drwxr-sr-x 2755 ali:ali /home/ali/lab/special-permissions/shared/reports
d drwxrwxrwt 1777 ali:ali /home/ali/lab/special-permissions/dropbox
f -rwS------ 4600 ali:ali /home/ali/lab/special-permissions/mode-sample
Hostname, user, and the exact ordinary bits will differ depending on your umask. The important observations:
sharedshows ansin the group execute slot, marking it as a setgid directory;reports, a new subdirectory, inherited the setgid bit;report.txtinherited the group, but its own mode was not turned into0660by setgid alone;dropbox's trailingtshows both sticky and other execute are present;mode-sample'sSshows owner execute is missing.
Give mode-sample its owner execute bit and watch the indicator change:
chmod u+x ~/lab/special-permissions/mode-sample
stat -c '%A %a %U:%G %n' \
~/lab/special-permissions/mode-sample
Expected mode:
The file is empty and does not belong to another UID — do not try to run it. This exercise demonstrated the mode notation, not a real privilege transition.
Auditing Special Bits with find
Finding any of the three special bits in the lab:
In GNU find, /7000 means at least one of setuid, setgid, or sticky is present. More targeted searches:
find ~/lab/special-permissions -xdev -type f -perm -4000 \
-printf 'setuid %m %u:%g %p\n'
find ~/lab/special-permissions -xdev -perm -2000 \
-printf 'setgid %m %u:%g %p\n'
find ~/lab/special-permissions -xdev -type d -perm -1000 \
-printf 'sticky %m %u:%g %p\n'
-perm -4000 requires every listed bit to be present; since only one bit is being asked for here, it finds setuid objects. /6000 matches either setuid or setgid:
This is a read-only audit suited to production, though it can add noticeable disk I/O and access-log traffic on a large tree. sudo is needed to see into restricted directories. Scope the search to a specific mount or application root; do not casually walk / and its virtual file systems.
Warning
Do not attach -exec chmod, -delete, or another fix to an audit's output. Removing the bit from a legitimate setuid/setgid executable can break login, password changes, scheduling, or another system function. The output is only a list of candidates to verify.
How to Check Each Finding
For example, for <found_file>:
target='<found_file>'
stat -c '%A %a %u:%g %U:%G %n' -- "$target"
namei -l -- "$target"
findmnt -no TARGET,VFS-OPTIONS,FS-OPTIONS --target "$target"
file -- "$target"
Then identify the package origin with the tool that matches the distribution.
Debian/Ubuntu:
RHEL family:
If rpm -Vf produces no output, the checked attributes match the package database's recorded state. If it prints markers, interpret them against rpm(8). dpkg-query -S only shows which package delivered the path — it does not prove the current mode is what was expected. In either family, compare against your configuration management baseline, vendor security advisories, and change history.
The following are strong warning signs:
- a setuid/setgid executable is writable by an account or group other than its owner;
- one of the parent directories is writable by an untrusted user;
- the binary cannot be traced back to a package or an approved deployment source;
- the bit appeared recently, with no matching change record;
- the executable loads a shell, an interpreter, or an arbitrary plugin/hook;
- a privileged program relies on user-controlled configuration, environment, or a relative
PATH.
Practical Scenario: Group Fragmentation in a Shared Directory
Problem: /srv/catalog/shared is used by the catalog-editors group. New files created by different users end up in each user's own primary group, breaking collaboration. The goal is to enable group inheritance for new objects without blindly running a recursive change across the existing tree.
This changes mode and ownership in production. Prepare a test host or snapshot, a second administrator session, a verified group membership list, and a rollback file first.
1. Check the Current State Without Changing Anything
getent group catalog-editors
stat -c '%A %a %u:%g %U:%G %n' -- /srv/catalog/shared
namei -l -- /srv/catalog/shared
findmnt -no TARGET,VFS-OPTIONS,FS-OPTIONS --target /srv/catalog/shared
Then look at the existing objects one level deep:
Confirm the group exists, the intended users are members of it, and the directory is not another file system or a symlink.
2. Save Rollback Data
If ACL tools are installed, take a metadata backup:
( umask 077
sudo getfacl -p /srv/catalog/shared \
> ~/catalog-shared.before.acl
)
stat -c '%A %a %U:%G %n' ~/catalog-shared.before.acl
umask 077 only affects the backup created inside this subshell and keeps it closed to other local users — covered in more depth in the next article. Also check the backup's content:
Do not place the backup inside the shared directory itself. It restores ACLs, ordinary mode, and ownership — it is not a backup of file content.
3. The Smallest Change
Owner stays the same; only the group and the directory's setgid bit are adjusted:
These commands are not recursive. Existing files' group and mode remain unchanged.
4. Verify in a New Session
stat -c '%A %a %U:%G %n' -- /srv/catalog/shared
sudo -u <group_member> -- touch /srv/catalog/shared/.inheritance-test
stat -c '%A %a %U:%G %n' \
/srv/catalog/shared/.inheritance-test
The test file's group should be catalog-editors. Whether it is group-writable depends on the creating user's umask and any default ACL — setgid alone does not guarantee that.
Check the test file explicitly, then remove it:
stat -c '%A %a %U:%G %n' \
/srv/catalog/shared/.inheritance-test
sudo rm -i -- /srv/catalog/shared/.inheritance-test
Warning
rm does not move a file to any kind of trash. This command targets only the specific .inheritance-test file created above, and -i asks for confirmation. Do not use a different name or a wildcard here.
5. Rollback
If the result does not match policy:
sudo setfacl --restore="$HOME/catalog-shared.before.acl"
stat -c '%A %a %U:%G %n' -- /srv/catalog/shared
setfacl --restore restores the owner, group, mode, and ACL from the backup — do not apply it blindly to a different host or path. If no backup exists, write the exact rollback chgrp and chmod g-s commands from the earlier stat values into your change plan in advance.
Fixing the group of existing objects already inside the directory is a separate migration: do not run chgrp -R without checking who owns each file, hard links, ACLs, and any files currently open.
Common Mistakes
Expecting a setuid Script to Run as Root
Linux ignores setuid/setgid bits on interpreted scripts. Instead of writing a workaround wrapper, design the task around a narrow policy or a verified service interface.
Reading a Capital S or T as Stronger Permission
The capital letter means the matching execute/search bit is missing. S on an executable, or T on a directory expected to be traversable, is usually a sign of a broken or incomplete mode, not extra protection.
Assuming a setgid Directory Makes Every File Group-Writable
setgid inherits group ownership. Access bits are decided by the requested mode, umask, and any default ACL.
Assuming the Sticky Bit Protects File Content
The sticky bit governs deletion and renaming decisions inside a directory. If the file itself allows group or other write, its content can still be changed.
Trusting an Internet List of "All setuid Files"
Packages, versions, and hardening policy vary. A local stat, the package owner, the mount state, and a verified baseline are the actual source of truth.
Removing Every Bit an Audit Finds at Once
Legitimate privileged programs can stop working. Check each finding's purpose, origin, writable paths, and possible alternative mechanism individually.
Expecting chmod 0755 to Always Clear a Directory's setgid
GNU chmod can preserve a directory's special bits through certain numeric mode changes. State the intent explicitly with chmod g-s, then confirm with stat.
Treating nosuid as the Only Protection Needed
nosuid restricts privilege transitions through setuid/setgid and file capabilities, but it does not by itself solve problems from an ordinary executable, a readable secret, writable code, or a script run through an interpreter.
Exercises
- Record the
statoutput for/usr/bin/passwd,/tmp, and/var/tmp. Explain which ordinary execute bit eachsortshares its position with. - Build the safe lab. Turn
mode-sample'sSintos, then back into a plainxwithchmod u-s, recording the numeric and symbolic mode after each step. - Create a file and a subdirectory inside
shared. Compare their group, setgid bit, and access mode, and determine which part came from inheritance and which fromumask. - Compare
find ... -perm -2000againstfind ... -perm /6000. Explain the "all bits" versus "any bit" semantics using each finding. - Write a read-only setuid/setgid audit plan for
/usror a separate test mount. Include-xdev, package ownership, parent directories,nosuid, a baseline, and a rollback decision — do not attach an automaticchmodto the findings.
Verification criterion: for every bit, state the object type, the numeric and symbolic mode, the kernel effect, its limitations, the audit method, and a safer alternative where relevant.
When you finish the lab, check the target first:
If only lab objects appear:
Warning
rm -rI deletes the lab tree with no undo mechanism. The absolute target used here is a specific path inside your home directory; do not run the command without checking the read-only find output first.
References
- Linux man-pages:
execve(2): https://man7.org/linux/man-pages/man2/execve.2.html - Linux man-pages:
inode(7): https://man7.org/linux/man-pages/man7/inode.7.html - Linux man-pages:
fcntl_locking(2): https://man7.org/linux/man-pages/man2/fcntl_locking.2.html - GNU Coreutils manual: mode structure: https://www.gnu.org/software/coreutils/manual/html_node/Mode-Structure.html
- GNU Coreutils manual: changing special mode bits: https://www.gnu.org/software/coreutils/manual/html_node/Changing-Special-Mode-Bits.html
- GNU Coreutils manual: directory setuid and setgid: https://www.gnu.org/software/coreutils/manual/html_node/Directory-Setuid-and-Setgid.html
- GNU Coreutils manual:
chmodinvocation: https://www.gnu.org/software/coreutils/manual/html_node/chmod-invocation.html - GNU Findutils manual: numeric modes: https://www.gnu.org/software/findutils/manual/html_node/find_html/Numeric-Modes.html
- util-linux:
findmnt(8): https://man7.org/linux/man-pages/man8/findmnt.8.html