Skip to content

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:

4755 = setuid + 0755
2770 = setgid + 0770
1777 = sticky + 0777
6755 = setuid + setgid + 0755

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 -c 'symbolic=%A octal=%a owner=%U group=%G path=%n' -- <path>
ls -ld -- <path>

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:

  • s in the owner execute slot: setuid is set, and owner execute is also set.
  • S in the owner execute slot: setuid is set, but owner execute is not.
  • s in the group execute slot: setgid is set, and group execute is also set.
  • S in the group execute slot: setgid is set, but group execute is not.
  • t in the other execute slot: sticky is set, and other execute is also set.
  • T in 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:

command -v passwd
stat -c '%A %a %U:%G %n' -- /usr/bin/passwd

A representative result:

/usr/bin/passwd
-rwsr-xr-x 4755 root:root /usr/bin/passwd

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_privs set;
  • 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:

findmnt -no TARGET,VFS-OPTIONS,FS-OPTIONS --target /usr/bin/passwd

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:

  1. check whether the task can be expressed more narrowly with sudo, a service, or a capability;
  2. audit the ownership and mode of the executable and every parent directory;
  3. establish where the binary came from and which package owns it;
  4. check the mount, MAC policy, and no_new_privs context;
  5. 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:

-rwxr-sr-x 2755 root:tty <executable>

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 rw bits — 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 umask or 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:

stat -c '%A %a %U:%G %n' -- /tmp /var/tmp

A typical result on Ubuntu Server:

drwxrwxrwt 1777 root:root /tmp
drwxrwxrwt 1777 root:root /var/tmp

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 w on 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:

test ! -e ~/lab/special-permissions
echo $?

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:

find ~/lab/special-permissions -xdev -maxdepth 2 \
    -printf '%y %M %m %u:%g %p\n'

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:

  • shared shows an s in the group execute slot, marking it as a setgid directory;
  • reports, a new subdirectory, inherited the setgid bit;
  • report.txt inherited the group, but its own mode was not turned into 0660 by setgid alone;
  • dropbox's trailing t shows both sticky and other execute are present;
  • mode-sample's S shows 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:

-rws------ 4700 ali:ali /home/ali/lab/special-permissions/mode-sample

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:

find ~/lab/special-permissions -xdev -perm /7000 \
    -printf '%m %u:%g %p\n'

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:

sudo find /usr /opt -xdev -type f -perm /6000 \
    -printf '%m %u:%g %p\n'

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:

dpkg-query -S -- "$target"

RHEL family:

rpm -qf -- "$target"
rpm -Vf -- "$target"

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:

find /srv/catalog/shared -xdev -mindepth 1 -maxdepth 1 \
    -printf '%y %m %u:%g %p\n'

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:

sed -n '1,20p' ~/catalog-shared.before.acl

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:

sudo chgrp catalog-editors /srv/catalog/shared
sudo chmod g+rwx,o-rwx,g+s /srv/catalog/shared

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

  1. Record the stat output for /usr/bin/passwd, /tmp, and /var/tmp. Explain which ordinary execute bit each s or t shares its position with.
  2. Build the safe lab. Turn mode-sample's S into s, then back into a plain x with chmod u-s, recording the numeric and symbolic mode after each step.
  3. 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 from umask.
  4. Compare find ... -perm -2000 against find ... -perm /6000. Explain the "all bits" versus "any bit" semantics using each finding.
  5. Write a read-only setuid/setgid audit plan for /usr or a separate test mount. Include -xdev, package ownership, parent directories, nosuid, a baseline, and a rollback decision — do not attach an automatic chmod to 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:

find ~/lab/special-permissions -xdev -maxdepth 2 \
    -printf '%y %m %p\n'

If only lab objects appear:

rm -rI -- ~/lab/special-permissions

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: chmod invocation: 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