Special Permissions: setuid, setgid, Sticky Bit

Beyond the nine standard read/write/execute bits you already know from chmod, Linux defines three special permission bits: setuid, setgid, and the sticky bit. These bits change how the kernel behaves when a file is executed or when files are created inside a directory. They are what let an ordinary user run passwd and update a system file they don’t own, what lets a whole team share a project folder without constantly running chgrp, and what keeps one user from deleting another user’s files in a shared, world-writable folder like /tmp.

Overview: How Special Permissions Work

Recall that a file’s permissions are three sets of three bits: owner, group, and other, each covering read (4), write (2), and execute (1). Special permissions add a fourth set of bits layered on top of that, which is why you sometimes see a 4-digit octal mode like 4755 instead of the usual 3-digit 755. That leading digit is a bitmask: 4 for setuid, 2 for setgid, 1 for sticky, and they can be added together (a directory with both setgid and sticky is 3).

setuid (Set User ID)

When the setuid bit is set on an executable file, the kernel does something special during execve(): instead of running the new process with the effective UID of the user who launched it, it runs the process with the effective UID of the file’s owner. This is how /usr/bin/passwd, owned by root, lets a regular user edit /etc/shadow — a file only root can normally write — for the single, narrow purpose of changing their own password. The process still remembers who really invoked it (the real UID), but its effective UID — the one the kernel checks for permission decisions — is temporarily the file owner’s.

setgid (Set Group ID)

On an executable file, setgid works the same way as setuid but for the group: the process’s effective GID becomes the file’s group owner instead of the invoking user’s primary group. On a directory, setgid means something different and, in practice, more useful day to day: any file or subdirectory created inside inherits the directory’s group instead of the creating user’s default group, and new subdirectories also inherit the setgid bit itself, so the behavior propagates. This is the standard trick for shared team folders — everyone’s files automatically end up owned by the team group.

Sticky Bit

On a directory, the sticky bit restricts deletion: normally, if you have write permission on a directory, you can delete or rename any file inside it, even one you don’t own, because the write check happens against the directory, not the file. With the sticky bit set, the kernel adds an extra rule: a user may only delete or rename a file inside that directory if they own the file, own the directory, or are root. This is exactly why /tmp is world-writable but users still can’t delete each other’s temp files — it’s mode 1777. (Historically, the sticky bit had a different meaning on executable files — keeping the program’s memory image on swap for faster reloading — but that behavior is obsolete on modern Linux and the bit only matters on directories today.)

Syntax

Special permissions are set with the same chmod command used for ordinary permissions, either symbolically or with a 4-digit octal mode.

chmod  
Symbolic form Octal digit Meaning
u+s 4000 setuid — run as the file’s owner
g+s 2000 setgid — run as the file’s group, or new files in a directory inherit its group
+t 1000 sticky bit — only the owner can delete their own files in this directory

In an ls -l listing, these bits replace the executable-position character: s in the owner’s execute slot means setuid, s in the group’s execute slot means setgid, and t in the other’s execute slot means sticky. If the underlying execute bit isn’t also set, you’ll see a capital S or T instead — more on why that matters below.

Examples

Example 1: Spotting setuid on a real system binary

ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 68208 Jan  6  2022 /usr/bin/passwd

The lowercase s where owner-execute normally goes tells you setuid is on, and the file is owned by root. Any user who runs passwd gets a process whose effective UID is 0 for the duration of that run, which is the only way an unprivileged user can modify /etc/shadow.

Example 2: A shared team directory with setgid

sudo mkdir /srv/projects
sudo groupadd devteam
sudo chown root:devteam /srv/projects
sudo chmod 2775 /srv/projects
ls -ld /srv/projects
drwxrwsr-x 2 root devteam 4096 Aug  4 10:15 /srv/projects

chmod 2775 sets rwxrwxr-x plus the setgid bit (the leading 2). Now watch what happens when a team member, alice, whose primary group is alice and not devteam, creates a file inside:

touch /srv/projects/notes.txt
ls -l /srv/projects/notes.txt
-rw-r--r-- 1 alice devteam 0 Aug  4 10:16 /srv/projects/notes.txt

Even though alice’s own default group is alice, the new file’s group is devteam, inherited from the directory. Without setgid, the file would have landed in alice’s personal group and other team members with only group-read access to devteam wouldn’t be able to read it.

Example 3: A public upload folder with the sticky bit

sudo mkdir /srv/public-uploads
sudo chmod 1777 /srv/public-uploads
ls -ld /srv/public-uploads
drwxrwxrwt 2 root root 4096 Aug  4 10:20 /srv/public-uploads

Everyone can write into this directory (the trailing rwt), but the t means only the owner of a given file — or root — can remove it. This is the exact mode /tmp uses on every mainstream distribution.

Auditing setuid binaries on a system

find / -perm -4000 -type f 2>/dev/null
/usr/bin/passwd
/usr/bin/sudo
/usr/bin/su
/usr/bin/mount

find‘s -perm -4000 matches any file with the setuid bit set, regardless of its other permission bits; the 2>/dev/null discards “permission denied” noise from directories you can’t traverse.

How It Works Step by Step

When you run a setuid binary, several things happen in sequence: (1) your shell forks a child process, which at that point still carries your real and effective UID; (2) the child calls execve() on the target file; (3) before handing control to the new program, the kernel checks the file’s mode bits, sees setuid is set, and copies the file owner’s UID into the new process’s effective UID (the real UID stays yours, which is how the program can still tell who invoked it, via getuid()); (4) for the rest of that process’s life, every permission check the kernel performs — opening files, sending signals, and so on — uses the effective UID, not your real one. For setgid directories, the mechanism is different: it’s not execve() at all, but the file-creation syscalls (open() with O_CREAT, mkdir()) — when the kernel creates a new inode inside a setgid directory, it copies the parent directory’s group ownership onto the new inode instead of using the creating process’s primary group. For the sticky bit, the extra check happens inside unlink() and rename(): normally passing the directory’s write-permission check is sufficient to remove any entry in it, but if the sticky bit is set, the kernel adds a second condition — the caller’s effective UID must match either the file’s owner, the directory’s owner, or be 0 (root) — and only then does it proceed.

Common Mistakes

Mistake 1: Expecting setuid to work on a shell script

#!/usr/bin/env bash
echo "Effective UID: $(id -u)"
sudo chown root:root backup.sh
sudo chmod 4755 backup.sh
./backup.sh

You might expect this to print Effective UID: 0. It won’t — Linux’s kernel deliberately ignores the setuid and setgid bits on scripts (anything starting with #!). The reason is a well-known race condition: between the kernel reading the shebang line and the interpreter actually opening the script, an attacker could swap the file out from under it and get an arbitrary script run as root. If you need controlled privilege elevation for a script, use sudo with a tightly scoped /etc/sudoers entry instead of setuid.

Mistake 2: Recursive chmod silently stripping the setgid bit

sudo chmod -R 777 /srv/projects

This is doubly wrong: it destroys the setgid bit you carefully set (a numeric mode like 777 has no special-bit digit, so it resets it to off) and it makes every file world-writable, defeating the point of a group-restricted shared folder. Fix it with a symbolic mode that only touches the bits you mean to change, and re-apply setgid explicitly:

sudo chmod -R u=rwX,g=rwX,o=rX /srv/projects
sudo chmod g+s /srv/projects

The capital X here sets execute only on directories and files that already have execute set on some bit, which avoids accidentally making plain data files executable.

Mistake 3: Misreading a capital S or T

-rwSr--r-- 1 root root 12040 Aug  4 09:02 /usr/local/bin/oldtool

A capital S (or T for sticky) means the special bit is set but the matching execute bit is not. Here setuid is on, but the owner has no execute permission, so the file can’t actually run as anyone — it’s a broken, likely accidental, configuration worth investigating rather than a stronger form of setuid.

Best Practices

  • Prefer sudo with a narrow sudoers rule over writing your own setuid programs or scripts — setuid on custom code is a classic path to privilege-escalation bugs.
  • Periodically audit setuid/setgid binaries with find / -perm -4000 -o -perm -2000 -type f 2>/dev/null and remove the bit from anything that doesn’t need it.
  • Use setgid directories (chmod 2775) for team-shared project folders so new files automatically get the right group.
  • Use the sticky bit (mode 1777) on any world-writable directory to stop users from deleting each other’s files.
  • Never combine setuid with world-writable (777) — an attacker who can overwrite a setuid binary’s contents inherits its privilege.
  • On modern systems, consider Linux capabilities (setcap) instead of full setuid-root when a program only needs one specific privilege, such as binding to a low port.
  • Always verify the result with ls -l or ls -ld after using octal modes — a missing leading digit silently drops the special bit.

Practice Exercises

  • Run a system-wide search for every setuid and setgid binary on your machine and list which ones you recognize; for each unfamiliar one, look up what it does before deciding whether it’s expected.
  • Create /srv/team-docs, make a new group called writers, set the directory’s group to writers, and set mode 2775. Create a file inside as yourself and confirm with ls -l that it inherited the group instead of your personal one.
  • Create /srv/shared-drop with mode 1777. Explain (you don’t need two separate user accounts to reason about it) why a second user could create a file there but not delete a file owned by the first user, referencing the extra check the kernel performs on unlink().

Summary

  • setuid (4000, shown as s in the owner-execute slot) makes an executable run with the file owner’s effective UID instead of the caller’s.
  • setgid (2000, shown as s in the group-execute slot) does the same for group on executables, and on directories makes new files inherit the directory’s group.
  • The sticky bit (1000, shown as t in the other-execute slot) restricts file deletion inside a directory to the file’s owner, the directory’s owner, or root — the mode /tmp uses.
  • A capital S or T in ls -l output means the special bit is set without the matching execute bit — usually a misconfiguration.
  • The Linux kernel ignores setuid/setgid on shell scripts by design, to prevent a shebang-swap race condition; use sudo instead.
  • Recursive numeric chmod commands can silently strip special bits — use symbolic modes or re-apply the bit explicitly afterward.