Computing History · Unix Lineage

Nine Permission Bits, Three Classes, and No Way to Deny

How the mode word is checked at open time, why umask subtracts rather than assigns, and what setuid, setgid and the sticky bit change about the check.

The mode word is twelve bits of which nine do the ordinary work: read, write and execute for owner, group and other. The remaining three are setuid, setgid and sticky, and each of them changes the check rather than the bits being checked. The whole scheme is a permission lattice with no negative entries. There is no way to say that a particular user is forbidden a file that their class would otherwise be allowed, and every complication in the model follows from that absence.

The check happens once, at open

Permission is evaluated when a descriptor is created, not when it is used. open resolves the path, walks each directory component checking execute permission, arrives at the target inode, and compares the process’s credentials against the mode. If the check passes, the descriptor is created with the requested access mode and no further authorisation occurs. A read on that descriptor succeeds regardless of any later chmod, and a process that holds a descriptor to a file it can no longer open by name can still use the descriptor.

The class selection is by identity, not by membership list. If the process’s effective user ID equals the file’s owner, the owner bits apply and the group and other bits are ignored, even if the process is also in the file’s group and the group bits would be more permissive. Otherwise, if the effective group ID or any supplementary group ID matches the file’s group, the group bits apply. Otherwise the other bits apply. Exactly one class is consulted, and the answer is the permission for that class. This is why an owner with mode 0600 cannot read a file that their own group can: the owner class wins, and the group bits are never examined.

The three bits within a class are not independent in the way they look. Read on a regular file permits reading; write permits writing; execute permits execution if the file is a program. On a directory the meanings shift entirely: read permits listing the names, write permits creating and removing entries, and execute permits traversing, which means looking up a name inside it. A directory with write but no execute lets a process create a file whose name it cannot then resolve. A directory with execute but no read lets a process reach a known path but not discover names. The pair is why chmod 711 on a shared directory is a common configuration: traverse for everyone, list only for the owner.

umask subtracts, and that is the whole design

umask is a mask of bits to clear from the mode requested at creation. It is not a default mode and not an assignment. A process with umask 022 that calls open with mode 0666 gets 0644, because the group-write and other-write bits are cleared. The same umask applied to a request for 0777 yields 0755.

The subtraction form is what makes the mask composable with the request. A program states the permissions it wants in the most permissive form it could accept, and the mask removes what the environment forbids. A library that creates a file with mode 0600 is unaffected by any umask, because the bits the mask clears are already clear. A program that creates a file with mode 0666 gets the umask applied, which is the intended behaviour: the program says “readable and writable”, and the user’s policy decides whether the group and the world get those permissions.

The default umask of 022 on many systems is a policy, not a requirement. A umask of 077 makes new files private by default. umask is inherited across fork and exec, so a shell’s setting applies to every program it starts, and a program that changes it must restore it before returning to a caller that did not expect the change. The mask is process state with no scoping mechanism.

There is a subtlety in the creation call: open with O_CREAT takes the mode and the umask is applied by the kernel, so the file appears with the reduced mode. mkdir and mkfifo do the same. But chmod does not apply the umask, because it is setting an existing file’s mode rather than creating one, and a program that creates a file and then calls chmod to widen it defeats the mask entirely. That sequence is a bug rather than a policy: it produces a file whose permissions ignore the user’s mask, and the window between creation and chmod is also a window in which the file exists with the creation mode.

#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

int make_private(const char *path) {
    mode_t old = umask(077);          /* clear group and other entirely */
    int fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0600);
    umask(old);                       /* restore for the rest of the process */
    return fd;
}

O_EXCL matters here. Without it, open on an existing file succeeds and the mode argument is ignored, so a program that believes it created a private file may have opened one whose permissions were set by someone else. With O_EXCL, creation fails if the name exists, and the check-and-create is atomic with respect to other processes using the same call.

The three special bits change the check

Setuid on an executable means that when the program is executed, the process’s effective user ID becomes the file owner’s rather than the caller’s. The real user ID stays the caller’s, so the process can still tell who invoked it, and the saved set-user-ID is set to the new effective ID so the program can drop and regain the privilege. The classic use is a program that needs to read a file the invoking user cannot read: the binary is owned by root, mode 4755, and any user who executes it runs with effective ID zero for the duration.

The bit is checked at exec, and only for the execute permission. A setuid file that is not executable has no effect. The kernel ignores the bit on scripts in most implementations, because a script is executed by an interpreter which would then be running with the elevated identity and could be induced to run arbitrary commands; the check for the interpreter’s own setuid bit is a separate matter. This is why setuid shell scripts are a well-known hazard rather than a supported feature.

Setgid on an executable does the same for the effective group ID. On a directory, setgid has an entirely different meaning: new files and subdirectories created inside inherit the directory’s group rather than the creating process’s effective group, and new subdirectories also inherit the setgid bit. This is the mechanism behind shared project directories, where members of a group can create files that the rest of the group can modify without a chgrp step. The two meanings share a bit and nothing else.

The sticky bit on a directory restricts deletion. Without it, write permission on a directory permits removing any entry in it, regardless of the permissions on the entry’s inode. With it set, a process may remove or rename an entry only if it owns the entry, owns the directory, or is privileged. The historical use is /tmp, mode 1777: world-writable so any process can create a file, sticky so no process can remove another’s. The bit’s original meaning on a regular file, “keep the program text in swap”, is obsolete and ignored.

The model’s blind spots

The absence of deny rules is the first. A file mode can grant to a class but cannot subtract from a broader grant. If a file is mode 0644 and a specific user should not read it, there is no mode that expresses “everyone except this user”; the choices are to change the group, to use an ACL, or to move the file. POSIX ACLs extend the model with named entries and a mask, and the mask interacts with the group bits in a way that makes ls output and stat disagree about what the group can do; the ACL mask is stored in the group bits position, so a file with an ACL shows a group mode that is the mask rather than the group entry’s permission.

The second blind spot is that the check uses effective credentials at the moment of open, and those credentials can change. A setuid program runs with elevated identity, and any file it opens is opened with that identity’s permissions. If it opens a file whose path is supplied by the caller and does not drop privilege first, the caller can read any file the elevated identity can read. This is the entire class of setuid vulnerabilities, and it is a consequence of the check being on identity rather than on intent.

The third is that the directory permission check applies to every component of the path, and the check is done during resolution, not once. A path with twenty components performs twenty execute checks, and a change to any ancestor’s mode between the resolution and the final open is not atomic. A process can be induced to resolve through a directory that is being renamed or replaced, which is why the *at calls with directory descriptors exist: they fix the starting point so that the resolution cannot be redirected by a concurrent change in an ancestor. That mechanism is described in the namespace article.

Finally, the mode word says nothing about what happens after the descriptor is open. A file opened for writing can be truncated, appended, or written at an arbitrary offset by any process holding a descriptor, and the mode does not distinguish these. The distinction between “may write” and “may only append” is O_APPEND on the description, not a bit on the inode, and the two live at different levels of the model described in the descriptor article.

References

  1. The Open Group Base Specifications Issue 7, System Interfaces: chmod — POSIX.1-2017, IEEE Std 1003.1
  2. The Open Group Base Specifications Issue 7, System Interfaces: umask — POSIX.1-2017, IEEE Std 1003.1
  3. Linux man-pages: path_resolution(7) — Michael Kerrisk, man-pages project
  4. The UNIX Time-Sharing System — Ritchie & Thompson, Communications of the ACM, 1974