Filesystems · Filesystem Fundamentals
Permission Checks at Open and the Bits That Change Meaning
The mode word is consulted once, at open, against the whole path; umask, the sticky bit, setgid inheritance, and default ACLs each bend the rule in a way that surprises the caller.
The permission check performed by open is not a check of the file. It is a check of the file, every directory on the path to it, the credentials of the calling process, and the flags passed to the call, evaluated once, at a moment when the objects involved are pinned. Understanding why the check happens there rather than per read explains the shape of the mode word, the purpose of each special bit, and the reason access is a weaker tool than it appears.
Three bits per class, and what each class may do
The low nine bits of the mode word are three triples: owner, group, other. Each triple grants read, write, and execute, and the meaning of each bit depends on the type of the object.
For a regular file, read permits read, write permits write and truncate, and execute permits execve. For a directory, read permits listing the names, write permits creating, removing, and renaming entries, and execute permits traversal, which is resolving a name inside it. The two directory permissions are independent in a way that file permissions are not: a directory with mode --x can be traversed but not listed, so a program that knows a name can open the file but cannot discover the name. A directory with mode r-- can be listed but not entered, which yields names with no way to reach them. The listing itself is produced by the cursor interface described in Reading a Directory as a Stream: readdir and Its Weak Guarantees, which is why the two permissions are not interchangeable.
Traversal requires execute on every directory in the path, and the requirement applies to the process’s own credentials at the time of the walk. A process that opens a file and then drops privileges still holds the descriptor, because the check happened at open and is not repeated. That property is the foundation of every privileged helper that opens a file as root and then hands the descriptor to an unprivileged worker: the worker’s reads are not re-checked.
The check itself picks exactly one class. If the process’s effective uid matches the file’s owner, the owner triple applies and the group and other triples are ignored, even if one of them would grant more. Otherwise, if the effective gid or any supplementary group matches the file’s group, the group triple applies. Otherwise the other triple applies. There is no union of permissions across classes, which is why adding a group to a file can reduce a user’s access: the user moves from the other class into the group class, where the bits may be stricter.
Root is the exception, with one narrow carve-out. A process with CAP_DAC_OVERRIDE bypasses the read, write, and directory-search checks entirely, so a directory with mode 000 is still traversable by such a process during ordinary path resolution. The carve-out is execution of a regular file: even a privileged process succeeds only when at least one of the three execute bits is set somewhere in the mode, which is what keeps an accidentally non-executable binary from being run by root. Read, write, and search have no such requirement.
Why the check is not repeated per read
A per-read check would require the kernel to know the credentials of the process at the time of each read, and to re-walk the path to the inode. Neither is available in the read path: read takes a descriptor, not a path, and the descriptor was created at a moment when the credentials were known.
The design follows from the interface’s own structure. A descriptor is a capability. Once granted, it confers the access it was opened with, and nothing about the file’s subsequent permission changes revokes it. A process that opens a file for writing and then has its write permission removed by another process continues to write, because the descriptor is the authority and the mode word was consulted only to create it.
This is also why changing a file’s mode to 000 does not stop an existing writer and does not stop an existing reader. It stops new opens. The distinction matters for any program that expects a permission change to take effect immediately on running processes.
The one case where the check is deferred is mmap with PROT_WRITE on a shared mapping. Write permission was checked at open, and the mapping inherits it; there is no per-page permission check on the fault path, only the protection bits in the page table.
Access control lists change the check, not the shape
The ACL model that Linux implements comes from the POSIX.1e draft, which was withdrawn before ratification and never became part of the base standard, so the persistence of ACLs is an extension rather than a conformance requirement; acl(5) documents how Linux stores and applies them. The model extends the mode word with named entries: a list of users and groups, each with its own permission triple, plus a mask entry that bounds the permissions granted to everything except the owner and the other class.
When a file has an ACL, the group triple in the mode word no longer describes the group class. It becomes the mask, and ls -l shows a + to indicate that an ACL is present. A chmod on such a file sets the mask rather than the group permissions, which is why changing the group bits on a file with an ACL can silently restrict every named entry: the mask caps them all.
The check order gains a step. The kernel first tests for an entry matching the process’s uid, then for entries matching any of its groups, and only then falls back to the other class. The owner entry is not subject to the mask; every named user, named group, and the group class are. A named entry that grants read but sits under a mask without read grants nothing, and there is no diagnostic at open time beyond EACCES.
Default ACLs on a directory are the creation-time analogue of the setgid rule: the directory passes its default entries to new files, the umask does not apply, and the created mode is derived from the requested mode and the defaults together. A program that assumes open(path, O_CREAT, 0660) produces a group-writable file is therefore wrong on a directory whose default ACL masks the group bits, and only fstat on the returned descriptor settles the question.
umask subtracts, and only at creation
open with O_CREAT and mkdir take a mode argument, and the kernel clears the bits set in the process’s umask before storing the mode. The umask is inherited across fork and exec and is changed only by the umask call, which returns the previous value.
The subtraction is applied to the requested mode, not to a default. A program that asks for 0666 with umask 022 gets 0644; a program that asks for 0600 gets 0600, since the umask’s bits are already clear. The umask never adds permission, so a program cannot exceed what it requested.
Two special bits are handled outside the umask arithmetic. POSIX leaves the setting of setuid and setgid at creation implementation-defined, and Linux clears the setgid bit when the file’s group does not match the creating process’s effective group and the process lacks the capability to set it. That rule exists so that a file created in a shared directory cannot be made group-executable with the wrong group’s privileges. The setgid bit on a directory is the case that matters most in practice: it can be set at creation, subject to the umask, and it changes the behaviour of files created inside, as described below.
The umask is a process-global setting and it is not thread-local. A library that creates a file with a restrictive mode by setting the umask and restoring it is racing every other thread in the process, and the window can leave an unrelated file with the wrong mode. The portable fix is to pass the mode explicitly and to fchmod the descriptor afterwards, which narrows the window but does not close it: between open and fchmod the file exists with the requested mode masked by the umask, which is why the requested mode should already be as restrictive as the final one.
The sticky bit on a directory
The sticky bit on a directory is not a permission in the nine-bit sense. It changes the rule for who may remove or rename an entry inside it: with the bit set, only the owner of the entry, the owner of the directory, or a process with CAP_FOWNER may unlink or rename that entry.
Without the bit, write permission on the directory is sufficient to remove any entry in it, regardless of who owns the file. That is why a shared directory such as /tmp is mode 1777: everyone may create entries, and the sticky bit prevents one user from deleting another’s.
The check is against the owner of the inode the entry names, not against the user who created the entry or the owner of the directory. A file with several hard links is owned by one user, and the sticky rule applies identically from every directory that contains it.
setgid on a directory is inheritance, not privilege
On a directory, the setgid bit changes the group assigned to new entries: a file or subdirectory created inside inherits the directory’s group rather than the creator’s effective gid. Subdirectories also inherit the setgid bit itself, so the property propagates down a tree created after the bit was set.
The mechanism exists to let shared project directories work without each member having to change group after every creation. A directory owned by group build with the setgid bit set produces files in group build no matter which member creates them, and the group write permission then applies uniformly.
On a regular file, the setgid bit is something else entirely: it makes the process that executes the file run with the file’s group, and the setuid bit makes it run with the file’s owner. Both are privilege elevation, both are ignored when the underlying filesystem is mounted with nosuid, and both are cleared by the kernel when a file is written by a process that does not own it, so that a modified binary does not retain elevation.
The two meanings share a bit and nothing else: on a directory it is inheritance and grants nothing, on a file it is elevation and grants the group’s access.
Checking before opening is a different question
access answers whether the process could open a path with a given mode, using the real uid and gid rather than the effective ones. The distinction exists for setuid programs: a setuid tool must decide whether the invoking user is allowed to do something, and the effective credentials belong to the tool’s owner, so checking with them would always succeed.
Using access to decide whether to attempt an open is a race, because the answer describes the tree at the moment of the call and the open resolves it again later. Between the two, a directory in the path can be replaced, a file’s mode can change, or the file can be deleted and recreated as a symlink to something else. The result is a check that passes and an open that either fails or succeeds on a different object than the one checked.
The safer order is to open first and then verify with fstat on the descriptor, checking the type, the owner, and the mode of the object actually opened. For the cases where the open must not happen at all unless the check passes, the openat2 resolution flags described in Path Resolution Component by Component constrain the walk itself, so the object opened is the object the path named when the walk began.
faccessat with AT_EACCESS uses the effective credentials and is the right call for a non-setuid program that wants to predict its own open. It is still a prediction, and the only reliable answer is the return value of the open.
What the mode word does not control
Several access decisions bypass the mode bits entirely. A file on a filesystem mounted read-only cannot be opened for writing regardless of its mode, and the failure is EROFS rather than EACCES. A file with the immutable attribute set cannot be written, renamed, or unlinked even by root, and the attribute lives in the inode’s flags rather than in the mode.
The mode also does not govern what happens to an open descriptor. Revoking access requires closing it, and a process that inherited a descriptor from a more privileged parent holds it with the parent’s authority. That is the mechanism behind a service that opens its listening socket, drops privileges, and continues to accept connections: the later drop does not touch the descriptor.
The link count and the mode interact in one more place worth noting. Removing the last name for a file does not free it while a descriptor is open, and the file continues to be readable and writable through that descriptor with the permissions it was opened with. What happens to the inode after the last descriptor closes, and how a filesystem tracks the interval, is the subject of Unlink While Open and the Life of an Orphaned Inode.
References
- open(2) — Linux manual page — man7.org
- path_resolution(7) — Linux manual page — man7.org
- acl(5) — Linux manual page — man7.org
- The Open Group Base Specifications Issue 7, 2018 edition — chmod() — The Open Group
- Information Technology — Portable Operating System Interface (POSIX) — Part 1: System Application Program Interface (API) — Amendment 2: Protection, Audit and Control Interfaces (POSIX.1e draft) — IEEE 1003.1e draft standard, withdrawn; the model Linux implements