A process’s umask removes permission bits when creating files and directories under the ordinary mode-based creation rule. The application first supplies a requested mode; the mask restricts that request. It is neither a complete access policy nor an instruction to rewrite existing files. A directory’s default access control list changes the creation calculation, so inspecting only the process mask can give an incomplete prediction.

When it breaks

Without a default ACL, the ordinary permission result is the requested mode bitwise AND the complement of umask. For requested 0666 and mask 0022, that gives 0644. This is not arithmetic subtraction. A request for 0600 remains 0600 under the same mask, and a request that omits execute permission does not acquire it because the mask is permissive.

A restrictive umask does not repair an existing world-writable file. Opening and truncating that file changes content while preserving its existing mode. A later chmod can also widen permissions regardless of the current mask. Treating umask as a permanent upper bound on permissions therefore leaves both existing-file and subsequent-permission-change cases unaccounted for.

When the parent directory supplies a default ACL, Linux derives the new access ACL from that default and restricts it by the application’s requested mode; the process umask is not applied in the ordinary way. A shared directory can consequently produce group-accessible files even when the creator’s mask is 0077. The inherited ACL must be part of the diagnosis.

Check it quickly

Save this as mask.py and run python3 mask.py on Linux with Python 3 and setfacl installed. The temporary-file directory must support default ACLs. The recorded run used Python 3.11.2, acl 2.3.1, and a separate ext4 volume selected through TMPDIR. The initial tmpfs environment rejected default ACLs, so the complete experiment was rerun on ext4:

from pathlib import Path
from tempfile import TemporaryDirectory
import os,stat,subprocess

def create(path,requested):
    fd=os.open(path,os.O_WRONLY|os.O_CREAT|os.O_EXCL,requested);os.close(fd)
def mode(path):return stat.S_IMODE(path.stat().st_mode)
previous=os.umask(0o022)
try:
    with TemporaryDirectory(prefix='xbcatm-umask-') as directory:
        root=Path(directory)
        data=root/'data';create(data,0o666);assert mode(data)==0o644
        executable=root/'executable';create(executable,0o777);assert mode(executable)==0o755
        private=root/'private';create(private,0o600);assert mode(private)==0o600
        folder=root/'folder';folder.mkdir(mode=0o777);assert mode(folder)==0o755
        assert 0o600&~0o022==0o600 and 0o600-0o022!=0o600
        print('without default ACL: requested mode AND NOT umask; 0666 becomes 0644, 0777 becomes 0755')
        os.umask(0o077)
        strict=root/'strict';create(strict,0o666);assert mode(strict)==0o600
        existing=root/'existing';create(existing,0o666);existing.chmod(0o666)
        fd=os.open(existing,os.O_WRONLY|os.O_TRUNC);os.close(fd)
        assert mode(existing)==0o666
        print('restrictive mask leaves an existing 0666 file unchanged; chmod can widen permissions later')
        acl_folder=root/'acl';acl_folder.mkdir()
        subprocess.run(['setfacl','-d','-m','u::rwx,g::rwx,o::---',str(acl_folder)],check=True)
        inherited=acl_folder/'inherited';create(inherited,0o666)
        restricted=acl_folder/'restricted';create(restricted,0o600)
        assert mode(inherited)==0o660 and mode(restricted)==0o600
        print('with default ACL and umask 0077: request 0666 yields 0660; request 0600 still yields 0600')
        status=dict(line.split(':',1) for line in Path('/proc/self/status').read_text().splitlines() if ':' in line)
        assert int(status['Umask'].strip(),8)==0o077
        print('/proc/self/status observes this process mask without temporarily changing it')
finally:os.umask(previous)

With mask 0022 and no inherited default ACL, requesting 0666 produces a 0644 file. Requesting 0777 produces 0755 for both a regular file and a directory, while requesting 0600 still produces 0600. The fixture also checks that arithmetic subtraction would give a different result for the restrictive request, exposing that common calculation error.

After switching to mask 0077, a newly created file requested as 0666 becomes 0600. Another file is explicitly changed to 0666, then opened with truncation but without creation. Its mode stays 0666. This comparison distinguishes initial creation from later operations on an existing inode, rather than assuming every open reapplies creation policy.

Next, the fixture installs a default ACL granting the owning user and group full permissions and granting others none. Under the still-active 0077 mask, a new file requested as 0666 receives mode 0660. The inherited ACL preserves group read/write permissions despite the restrictive mask, but the application’s request excludes execute permission.

A neighboring file requested as 0600 remains 0600 under that same default ACL. The default does not invent permissions absent from the requested mode. These two requests establish both the default-ACL exception to ordinary masking and the continued role of the application’s explicit creation request.

The script reads Umask from /proc/self/status and confirms that the process mask is still 0077. It restores the original mask in finally and removes the temporary tree. If the selected filesystem cannot install the ACL, the command fails; skipping that step would leave the central exception untested.

Apply creation policy at the right layer

Umask is inherited through normal process creation and preserved across execution of another program. Set it in the actual launch path for a service or command. An unrelated terminal’s mask does not retroactively change the state of an already running process, and shell startup files matter only when the launch path reads them.

Because umask is process-wide for threads sharing the relevant filesystem state, temporarily changing it to discover its previous value can interfere with concurrent file creation. On Linux, reading the proc status field avoids that mutation. Applications needing a specific private file should request suitably restrictive permissions and verify their actual storage context.

Inspect parent-directory access, ownership, ACLs, and the resulting file permissions together. A 0660 file inside an inaccessible parent is not automatically reachable by its group, and mode bits alone do not describe all named ACL entries. The experiment proves creation results; end-to-end access requires testing the intended identity through the complete path.

Sources

  • umask(2), creation masks, inheritance, default ACL interaction, and proc inspection, checked 2026-09-12.
  • acl(5), default ACL inheritance and requested-mode permission restrictions, checked 2026-09-12.
  • open(2), creation modes and opening or truncating an existing file, checked 2026-09-12.