The Linux file system: names, inodes, permission bits and ownership

Three people share one results directory. Alice owns it and runs the jobs. Bob is in her group and must read and write her files, and she must read and write his. Carol is on the same cluster, outside the group, and must read nothing. A file has nine permission bits, so there are \(2^{9}=512\) possible modes. Exactly 4 of them give the owner and the group read and write access and give everyone else no access: 660, 670, 760 and 770. The default configuration of the lecture's machine produces mode 664 for a new file and puts the file in the creator's personal group, so a file that Bob creates is read-only for Alice. One more bit, set once on the directory, makes Alice and Bob read and write every new file and keeps Carol out: mode 2770 for the directory with a creation mask of 002 in every member's shell. This chapter defines the file-system objects that these numbers describe, derives the rule that decides each access, and checks the rule against the kernel of the verification machine for 1536 requests on files it owns, for files in all three classes, and for three numeric identities in a scratch directory.

Grounded in two recordings of the NPTEL course High Performance Scientific Computing (Prof. Shivasubramanian Gopalakrishnan, IIT Bombay): W1L3, Mastering Linux Directory and File Management, and W1L4, Understanding File Permissions and Ownership in Linux. Lecture material: the commands whoami, uname, lscpu, free; files and directories, pwd, ls -la and its columns, hidden files, cd with no argument, with - and with ..; mkdir, rmdir, mv, touch, text editors, rm and rm -r, cat with > and >>, cp and cp -r; owner, group and others; read, write and execute; the ten-character mode string; chmod in symbolic and octal notation; chown and groups. Research extensions: Ritchie and Thompson (1974, the i-node, links and the protection bits of the first UNIX file system); POSIX.1-2024 (file access permissions, directory protection, the chmod grammar, the umask utility); the Filesystem Hierarchy Standard 3.0; the Linux man-pages for inode, path_resolution, credentials, open, mkdir, link, unlink, rename, rmdir, symlink, chmod, chown, free, login.defs and pam_umask; the GNU Coreutils 9.4 manual. Original teaching constructions: the formal model of inodes, credentials and modes; the access predicate and its check against the kernel; the creation law and the inference, under stated conditions, that the lecture's shell used a creation mask of 002; the model of symbolic chmod and its check against GNU chmod on 24,576 cases; the decoder of mode strings; the shared-directory case with three numeric identities; all programs; the six exercises.

The objects of the chapter and the running case

The chapter describes the part of a Linux system that stores named data and decides who may use it. The description has three parts: the objects that are stored, the processes that act on them, and the operations that connect the two.

Stored objects. An inode is a record that describes one file. It holds a type (regular file, directory, symbolic link, device, FIFO, socket), a mode \(m\), an owner user ID \(u_f\), a group ID \(g_f\), a link count \(n\), a size, timestamps, and a reference to the content (inode(7)). The mode is a number from 0 to 4095, so it has 12 bits. The low 9 bits are the permission bits. The 3 high bits are the special bits: set-user-ID (octal 4000), set-group-ID (2000) and sticky (1000). A regular file has as content a sequence of bytes. A directory has as content a table that maps names to inodes; each pair of a name and an inode is a directory entry. A symbolic link has as content a path. A name is not an attribute of an inode: the same inode can have several names, in the same directory or in different ones. Ritchie and Thompson (1974, p. 366) state it as a design rule, "a file does not exist within a particular directory; the directory entry for a file consists merely of its name and a pointer to the information actually describing the file", and POSIX.1-2024 (XBD 4.8) calls the whole structure a directed graph because several entries may refer to one file. A path is a string of names separated by /; resolving a path is following the entries from a starting directory to an inode.

Processes. A process \(q\) carries credentials: an effective user ID \(u_q\), an effective group ID \(g_q\), and a set \(\Sigma_q\) of supplementary group IDs. Write \(G_q=\{g_q\}\cup \Sigma_q\). The kernel compares these numbers, not names, with the numbers in the inode (credentials(7), path_resolution(7)). A process also has a working directory and a creation mask \(\mu\) (the umask), a 9-bit number defined in the section on defaults. A privileged process is one that the system exempts from the permission bits (except that execution needs at least one execute bit; see the section on permissions); on Linux this is a process with the appropriate capability, normally one that runs as user 0 (root). All other processes are unprivileged, and this chapter's rules describe them.

Operations. The operations are the system calls behind the commands of the lectures: mkdir, rmdir, open with creation, link, symlink, unlink, rename, chmod, chown, and path resolution. Each is a function of the inode table, the credentials of the caller and the arguments. The chapter derives, for the operations that the lectures show, the permission that each needs and the change that each makes to the table.

Notation. The three classes of the lecture are the owner (u), the group (g) and the other class (o; the lecture's "others"). The octal digit of class \(c\) in a mode is \(m_c=\lfloor m/8^{e_c}\rfloor \bmod 8\) with \(e_{\mathrm u}=2\), \(e_{\mathrm g}=1\), \(e_{\mathrm o}=0\); the three digits are the three groups of three permission bits. The weights of the access rights are read \(=4\), write \(=2\), execute \(=1\). The symbols \(\wedge\), \(\vee\) and \(\lnot\) denote bitwise AND, OR and complement on 9-bit numbers, so \(\lnot\mu=511-\mu\). Octal numbers in the text have a leading zero when they are modes (0644) and none when they are digits.

The running case is the shared directory of the opening. A group named hpsc has the numeric ID 2100. Alice (user 2001, primary group 2001) and Bob (user 2002, primary group 2002) are members of hpsc. Carol (user 2003, primary group 2003) is not. The directory proj belongs to Alice and to the group hpsc. The requirement has three parts. R1: Alice and Bob create files in proj and read and write each other's files. R2: Carol can list nothing, read nothing and traverse nothing in proj. R3: R1 and R2 hold for files created later, without anyone running chmod or chgrp on them. The IDs are numbers chosen for this chapter; no account with these names exists on the verification machine, and the kernel experiments use the numbers directly.

All commands run in a WSL 2 Ubuntu 24.04 guest on an Intel Core i5-12450H laptop under Windows 11, as the account az9713 (user 1000), with GNU coreutils 9.4 and Python 3.12. Every runnable listing creates its own directory under /tmp, which is an ext4 file system. None works under /mnt/c. On the Windows drive, which the guest mounts through the 9p protocol, a test on this machine created a file, ran chmod 000 on it, and stat then reported mode 555: that file system does not store Linux mode bits, so the chapter's results do not apply there.

Who runs the commands and on what: whoami, uname, lscpu, free W1L3 3:04

The lecture opens with four commands that identify the user and the machine. whoami prints the name of the user of the session. uname -a prints the kernel name, release and machine architecture. lscpu prints the processor model, the number of processors, and the cache and thread layout. free prints memory in kibibytes, and free -h scales each number to a unit (Ki, Mi, Gi) based on powers of 1024 and prints the unit letter. The first command matters for this chapter, because every permission decision starts from the identity that it reports. The other three describe the machine and belong to later chapters: free to Chapter 4 (memory and networking commands) and lscpu to Chapter 6 (node architecture).

A user has a name and a number. The name is for people. The kernel compares the number (credentials(7)), so the command id, which prints the number, the primary group and the supplementary groups, is the one that predicts access. A session on the verification machine shows both:

$ whoami
az9713
$ id
uid=1000(az9713) gid=1000(az9713) groups=1000(az9713),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),1001(docker)
$ uname -sr
Linux 6.6.87.2-microsoft-standard-WSL2
$ lscpu | grep -E 'Model name|^CPU\(s\)|Thread|Hypervisor'
CPU(s):                               12
Model name:                           12th Gen Intel(R) Core(TM) i5-12450H
Thread(s) per core:                   2
Hypervisor vendor:                    Microsoft
$ free -k
               total        used        free      shared  buff/cache   available
Mem:        16209100      605592    14853796        3536      966756    15603508
Swap:        4194304           0     4194304

The set \(G_q\) of this process has eight members: the primary group 1000 and seven supplementary groups. The lscpu slide (W1L3 7:15) prints Model name: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz, CPU(s): 1 and Hypervisor vendor: VMware. The generation is 11th Gen, and i5 is the product tier. The slide also shows that the lecture's machine is a virtual machine with one processor, which is why its numbers cannot be compared with the 12 logical processors above. The free -h slide (W1L3 9:15) shows 3.7G total, 1.1G used and 888M free.

The free slide of the lecture (W1L3 8:15) is a table in kibibytes (KiB, units of 1024 bytes), and the used column has had two different definitions in two versions of the program. The next listing does the arithmetic. Inputs: the six numbers of the slide. Output: both definitions applied to them, and each number converted to GiB and MiB. The check is that exactly one definition reproduces the printed used column.

# Columns of the lecture's `free` screenshot (KiB), W1L3, 8:15 into the recording.
total, used, free, shared, cache, available = 3861044, 1170740, 910584, 63084, 1779720, 2347100
print('older procps:  total - free - buff/cache =', total - free - cache, ' printed used =', used)
print('procps 4:      total - available         =', total - available)
for name, kib in (('total', total), ('used', used), ('free', free), ('available', available)):
    print(f'{name:<9}{kib / 2**20:6.3f} GiB {kib / 2**10:8.1f} MiB')
older procps:  total - free - buff/cache = 1170740  printed used = 1170740
procps 4:      total - available         = 1513944
total     3.682 GiB   3770.6 MiB
used      1.117 GiB   1143.3 MiB
free      0.868 GiB    889.2 MiB
available 2.238 GiB   2292.1 MiB

The slide's used of 1 170 740 KiB equals total minus free minus buffers and cache, so the lecture's free used the older definition. The manual of the current procps-ng program (free(1)) defines used as total minus available, and on the verification machine this definition holds: \(16\,209\,100-15\,603\,508=605\,592\), the value of the session above, while total minus free minus buffers and cache is \(388\,548\). The two definitions differ by 217 044 KiB on this machine and by 343 204 KiB on the lecture's slide, so a script that reads the used column gets numbers that depend on the version of the program. The version of procps on the lecture's machine is not known; the statement above is inferred from the numbers.

The directory tree, paths and the working directory W1L3 10:53

The lecture defines a file as a collection of data stored on a disk or in memory and a directory as a special file that contains references to other files and directories; the slide adds "It's like a folder that organizes files into a hierarchy" (W1L3 11:15). The model of this chapter makes the second definition exact: a directory is an inode whose content is a table of names. The next section develops it. This section treats the tree that the tables form and the commands that move through it: pwd, ls, cd.

Where things are. The Filesystem Hierarchy Standard (FHS 3.0, 19 March 2015) fixes the top of the tree by two distinctions. A file is shareable if one host can store it and others can use it, and static if it changes only by administrator action (FHS §2). Files that differ in either respect belong in different directories, so that they can sit on different file systems. This yields /usr for static shareable programs, /etc for static host-specific configuration, and /var for variable data (FHS §2 and §3). User files live under /home, which the standard calls "clearly a site-specific filesystem": "no program should assume any specific location for a home directory, rather it should query for it" (FHS §3.8.1). The lecture's home directory is /home/master. Temporary files go to /tmp, whose contents a program "must not assume" survive between runs (FHS §3.18.1), or to /var/tmp, whose contents "must not be deleted when the system is booted" (FHS §5.15.1). This chapter's listings use /tmp for that reason.

Resolution. A path that begins with / is absolute and starts at the root directory. Any other path is relative and starts at the working directory of the process (path_resolution(7)). The kernel takes the path one name at a time. For each name it needs search permission on the current directory, otherwise it fails with EACCES ("Permission denied"); the name must exist, otherwise ENOENT; and every name except the last must be a directory, otherwise ENOTDIR. Two names are special: . is the current directory and .. its parent. At the root, .. is the root itself; the listing below confirms it. The working directory belongs to the process and is looked up at each resolution. It follows that a separate program cannot change the working directory of the shell that started it, which is why cd is built into the shell.

The lecture shows three uses of cd: with a name, with no argument (the user goes to the home directory), with - (to the previous directory) and with .. (to the parent). The listing runs all of them in a scratch tree whose "home" is set by assigning the shell variable HOME. Inputs: none. Output: the working directory after each command, relative to the scratch root, which the program does not print. The last lines run ls in a directory that holds one hidden file. The check is that each result matches the lecture's description.

#!/bin/bash
d=$(mktemp -d /tmp/hpsc-ch03.XXXXXX); cd "$d" || exit 1
trap 'cd /; rm -rf "$d"' EXIT
mkdir -p home/proj/sub; export HOME=$d/home
where() { echo "$1 -> ${PWD#$d}"; }

cd ~/proj/sub;  where 'cd ~/proj/sub'
cd ..;          where 'cd ..'
cd - > /dev/null; where 'cd -'
cd;             where 'cd'
cd proj/./sub/../sub; where 'cd proj/./sub/../sub'

mkdir ls_demo; cd ls_demo; touch .hidden visible
echo "ls:    $(ls | paste -sd" ")"
echo "ls -A: $(ls -A | paste -sd" ")"
echo "ls -a: $(ls -a | paste -sd" ")"
cd /
echo "at /: . and .. are the same directory: $([ . -ef .. ] && echo yes || echo no)"
cd ~/proj/sub -> /home/proj/sub
cd .. -> /home/proj
cd - -> /home/proj/sub
cd -> /home
cd proj/./sub/../sub -> /home/proj/sub
ls:    visible
ls -A: .hidden visible
ls -a: . .. .hidden visible
at /: . and .. are the same directory: yes

Each line matches the lecture. cd with no argument goes to HOME, cd - returns to the directory left by the previous cd, and cd .. goes up one level. The path proj/./sub/../sub resolves to proj/sub: . and .. are names in each directory table, so resolution treats them like any other name. The last three lines test the lecture's rule that a name that starts with a dot makes a hidden file. The rule belongs to ls, not to the file system: the kernel stores .hidden like any name, plain ls skips every name that starts with a dot, ls -A shows all of them except . and .., and ls -a shows all. The last line shows that at the root, . and .. are the same directory.

The long listing. The lecture's ls -la screen (W1L3 14:15 to 17:15) is the first view of the model's attributes. One line reads drwxrwxr-x. 28 master master 4096 Dec 14 2023 anaconda3. The ten characters drwxrwxr-x are the type and the mode (decoded in the section on permissions). The dot after them is not part of the mode. GNU ls prints it for "a file with a security context, but no other alternate access method" and prints + when another alternate method such as an access control list applies (Coreutils 9.4 manual, §10.1.2 "What information is listed", the description of -l); a security context is the label of the SELinux security module. The lecture does not discuss it. Its presence on the file lines of the lecture's screens, and the group named wheel in the later groups output, indicate a Red Hat-family system with SELinux; the verification machine prints no dot. The number 28 is the link count; the next section derives it. master master are the owner and the group. The number 4096 is the size, in bytes, of the directory object itself, that is, of its table of names, not of the files in it. The same screen shows that this size follows the table and not a fixed block. In the one home directory, anaconda3, .cache and .config report 4096 bytes, while Documents, .dbus, Desktop, .conda, Anaconda and Downloads report 6, 25, 27, 30, 47 and 141 bytes, and /home reports 35. On the verification machine's /tmp (ext4), a new empty directory reports 4096 bytes, one block. The small sizes on the lecture's screen come from a file system that stores short tables in less space than a block; the screens do not name that file system. The date is a modification time, not a creation time (the same coreutils description: "normally the modification timestamp"), and the section on commands shows the two times apart. The line above the entries, total 60, is the file-system space of the listed entries in 1024-byte blocks, not a count of files.

A directory is a table of names: link counts, hard links and symbolic links W1L3 15:53

The lecture reads the second column of ls -l as the number of links associated with the file or directory and does not develop it. The column is the link count \(n\) of the model, and it answers a question that the lecture's commands raise: what exactly does rm remove, and what does mv change? Ritchie and Thompson describe the original structure (1974, pp. 366 to 369). A directory entry holds a name and an i-number, an index into a table of i-nodes. The i-node holds the owner, the protection bits, the addresses of the contents, the size, the time of last modification, the number of links ("the number of times it appears in a directory") and the type. Making a link "involves creating a directory entry with the new name, copying the i-number from the original file entry, and incrementing the link-count field of the i-node". Deleting a file "is done by decrementing the link-count of the i-node specified by its directory entry and erasing the directory entry. If the link-count drops to 0, any disk blocks in the file are freed and the i-node is deallocated". The Linux system calls keep this design: link(2) creates a new name for an existing file, and the two names have "the same permissions and ownership" and are indistinguishable ("it is impossible to tell which name was the original"); unlink(2) deletes one name, and the file itself is deleted only when that name was the last link and "no processes have the file open" (the file then remains until the last descriptor is closed).

Directory tables and inodes A directory table with three names. Two names, a.txt and b.txt, point to inode 57 with link count 2. The name s.txt points to inode 91, a symbolic link whose content is the text b.txt. directory proj (inode 20) nameinode a.txt57 b.txt57 s.txt91 inode 57: regular file mode, owner, group, size, times link count n = 2 content: the bytes of the file inode 91: symbolic link mode 0777, link count n = 1 content: the text "b.txt" resolved again by name
Figure 3.1. One directory table, two inodes. The names a.txt and b.txt are two entries for inode 57, whose link count is 2. The name s.txt is an entry for inode 91, a separate file whose content is a path; the dashed arrow shows that the kernel resolves that path by name each time the link is used. The figure shows structure only; the numbers 20, 57 and 91 are illustrative.

The link count of a directory. Which names refer to a directory \(\delta\) that has \(s(\delta)\) subdirectories? The entry for \(\delta\) in its parent; the entry . inside \(\delta\); and the entry .. inside each of the \(s(\delta)\) subdirectories. Each such name adds one to the link count, so on a file system that follows the original convention \[ n(\delta) = 2 + s(\delta). \tag{3.1} \] The 1974 paper states the first part (p. 366): "each directory always has at least two entries". The lecture's screen gives three checks. The home directory (. in the first line of the ls -la screen) has link count 22, so it has 20 subdirectories, hidden ones included; the parent /home has count 4, so 2 subdirectories; and anaconda3 has count 28, so 26 subdirectories. The ls -la screen at W1L4 4:45 adds a fourth: the entry . there is Desktop, with link count 4 and exactly two subdirectories, simpleproject and TestDir2. The program below verifies (3.1) directly and also tests the rules of links. Inputs: none. Output: link counts, results of the calls, and the error that each forbidden call returns, by the name of its error code. The check is that each result agrees with the man pages quoted above.

import errno, os, shutil, tempfile

def attempt(label, call, *args):
    try:
        call(*args)
        print(f'{label}: ok')
    except OSError as e:
        print(f'{label}: {errno.errorcode[e.errno]}')

root = tempfile.mkdtemp(dir='/tmp')
os.chdir(root)

# A directory is a table of names; its link count is 2 + number of subdirectories.
os.makedirs('proj/data/raw')
os.makedirs('proj/src')
os.mkdir('proj/docs')
for d in ('proj', 'proj/data', 'proj/src', 'proj/docs'):
    subdirs = sum(os.path.isdir(os.path.join(d, n)) for n in os.listdir(d))
    print(f'{d:<10} subdirectories {subdirs}  link count {os.stat(d).st_nlink}')

# A hard link is a second name for one inode.
with open('proj/a.txt', 'w') as f:
    f.write('residuals\n')
os.link('proj/a.txt', 'proj/b.txt')
a, b = os.stat('proj/a.txt'), os.stat('proj/b.txt')
print('a and b same inode:', a.st_ino == b.st_ino, ' link count:', a.st_nlink)
os.unlink('proj/a.txt')
print('after unlink(a): b reads', repr(open('proj/b.txt').read()), ' link count:', os.stat('proj/b.txt').st_nlink)

# A symbolic link is a file whose content is a path.
os.symlink('b.txt', 'proj/s.txt')
print('symlink: lstat is link:', os.path.islink('proj/s.txt'), ' mode:', oct(os.lstat('proj/s.txt').st_mode & 0o777),
      ' target text:', os.readlink('proj/s.txt'), ' reads:', repr(open('proj/s.txt').read()))
os.unlink('proj/b.txt')
print('after unlink(b): symlink still present:', os.path.lexists('proj/s.txt'), ' resolves:', os.path.exists('proj/s.txt'))
attempt('open dangling symlink', open, 'proj/s.txt')

# Limits of links and renames.
attempt('hard link to a directory', os.link, 'proj/src', 'proj/src2')
attempt('rmdir of a non-empty directory', os.rmdir, 'proj/data')
attempt('unlink of a directory', os.unlink, 'proj/data')
other = tempfile.mkdtemp(dir='/dev/shm')
open('proj/c.txt', 'w').close()
print('/tmp and /dev/shm on different file systems:', os.stat(root).st_dev != os.stat(other).st_dev)
attempt('rename across file systems', os.rename, 'proj/c.txt', other + '/c.txt')
attempt('hard link across file systems', os.link, 'proj/c.txt', other + '/c.txt')
before = os.stat('proj/c.txt').st_ino
os.rename('proj/c.txt', 'proj/docs/c.txt')
print('rename inside one file system keeps the inode:', os.stat('proj/docs/c.txt').st_ino == before)

shutil.rmtree(root)
shutil.rmtree(other)
proj       subdirectories 3  link count 5
proj/data  subdirectories 1  link count 3
proj/src   subdirectories 0  link count 2
proj/docs  subdirectories 0  link count 2
a and b same inode: True  link count: 2
after unlink(a): b reads 'residuals\n'  link count: 1
symlink: lstat is link: True  mode: 0o777  target text: b.txt  reads: 'residuals\n'
after unlink(b): symlink still present: True  resolves: False
open dangling symlink: ENOENT
hard link to a directory: EPERM
rmdir of a non-empty directory: ENOTEMPTY
unlink of a directory: EISDIR
/tmp and /dev/shm on different file systems: True
rename across file systems: EXDEV
hard link across file systems: EXDEV
rename inside one file system keeps the inode: True

The first four lines confirm (3.1): proj has three subdirectories and link count 5, proj/data one subdirectory and count 3, and the two empty directories count 2. The hard link b.txt and the original a.txt have the same inode, and the count is 2. After unlink of a.txt the file is still readable through b.txt and the count is 1: only a name was removed. The symbolic link is its own file. Its mode is 0777, which symlink(7) states is always so on Linux because the permissions of a symbolic link "are not used in any operations"; its content is the text b.txt; and when the target name is unlinked the link stays and fails to resolve, so opening it returns ENOENT. The next five lines show the three limits of links. A hard link to a directory fails with EPERM (link(2)); the 1974 paper gives the reason, that with arbitrary links to directories "it would be quite difficult to detect when the last connection from the root to a directory was severed". rmdir of a non-empty directory fails with ENOTEMPTY (rmdir(2)), and Linux reports unlink of a directory as EISDIR (unlink(2), which notes that POSIX prescribes EPERM). The program found /tmp and /dev/shm on different file systems, and both rename and link across them fail with EXDEV, "not on the same mounted filesystem" (rename(2), link(2)). Ritchie and Thompson (p. 367) give the same rule for the first UNIX file system: "no link may exist between one file system hierarchy and another". Inside one file system, rename keeps the inode, so the last line prints True.

These results give the exact meaning of two commands of the lecture. mv inside one file system changes directory entries and no data. When the destination is on another file system, rename cannot work, and GNU mv "falls back on copying as if by cp -a, then (assuming the copy succeeded) it removes the original" (Coreutils manual, §11.4); the cost then grows with the size of the data. A script that moves a result file from a fast scratch file system to a home file system therefore moves bytes, and its time grows with the size of the file. If the copy fails, mv removes the partial copy in the destination. If several directories are moved and the copy of a later one fails, the ones already copied stay at the destination and the rest stay at the source (Coreutils manual, §11.4), so one command can leave the data split across two file systems. And rm does not erase a file. It removes a name, and the file disappears when the last name and the last open descriptor are gone.

Creating, renaming, moving, copying and deleting W1L3 23:51 W1L4 0:37

The lectures present these commands that change the inode table: mkdir and rmdir for directories, touch and the editors vim and gedit for new files, mv for renaming and moving, rm and rm -r for deletion, and cp and cp -r for copies. Each command is a small program around one or two system calls, and the model of the previous section says what each call changes. Table 3.1 lists the calls and the documented conditions under which they fail.

Table 3.1. Commands of the lectures, the system calls behind them, and the documented conditions that make the call fail.
CommandSystem callChange to the modelDocumented failure conditions
mkdir dmkdirNew directory inode with the entries . and ..; the parent's link count rises by 1 (3.1); mode \(=\) requested mode \(\wedge\lnot\mu\wedge\) 0777EEXIST if the name exists, even as a dangling symbolic link; EACCES without write permission on the parent or search permission on a directory of the path (mkdir(2))
rmdir drmdirRemoves the entry of an empty directoryENOTEMPTY if d has entries other than . and ..; EACCES without write permission on the containing directory (rmdir(2))
touch f, a new file from an editor or from >open with O_CREATNew inode, new entry; mode \(=\) requested mode \(\wedge\lnot\mu\)EACCES if the file does not exist and write access to the parent directory is not allowed (open(2))
mv a brenameMoves or renames an entry; the inode and its other links are unchanged; an existing \(b\) is replaced atomicallyEXDEV if the two names are on different mounted file systems (rename(2))
rm funlinkRemoves one entry; the count \(n\) falls by 1EACCES without write permission on the containing directory or search permission on a directory of the path (unlink(2))
cp a bopen, read, writeIf \(b\) does not exist: a new inode with the creator's ownership and a new modification timeAs for creation
ln a blinkNew entry for the same inode; count \(+1\)EPERM for a directory; EXDEV across file systems (link(2))
chmod, chownchmod, chownChanges the mode, or the owner and group, in the inodeSee the sections on chmod and ownership

This section gathers the table edits of both lectures, including the cp demonstration of W1L4 (21:52), ahead of the section on redirection, which follows the order of W1L4. The listing reproduces the lecture's directory and file examples on /tmp with a creation mask of 002 (the value that a later section infers for the lecture's machine), and adds the checks that the table implies. Inputs: none. Output: one block of lines per command, each printing the fact that the command's documentation predicts. The checks are: mv keeps the inode; rmdir refuses a non-empty directory with the message of the lecture's slide (W1L4 10:45); cp -r run twice nests the copy; cp creates a new inode while cp -p preserves the modification time; rm removes a name and does not consult the mode of the file.

#!/bin/bash
d=$(mktemp -d /tmp/hpsc-ch03.XXXXXX); cd "$d" || exit 1
trap 'cd /; rm -rf "$d"' EXIT
umask 002

echo '$ mv inside one file system'
mkdir TestDir; i=$(stat -c %i TestDir)
mv TestDir TestDir2
[ "$(stat -c %i TestDir2)" = "$i" ] && echo 'same inode after mv'

echo '$ rmdir, then rm -r'
mkdir TestDir2/child; touch TestDir2/file4
rmdir TestDir2 2>&1
rm -r TestDir2 && echo 'rm -r removed the tree'

echo '$ cp -r, twice'
mkdir -p simpleproject/src; touch simpleproject/README.md
cp -r simpleproject project2
cp -r simpleproject project2
find project2 | sort

echo '$ cp and cp -p'
touch -d '2020-01-01 00:00:00' old
cp old copy; cp -p old kept
[ copy -nt old ] && echo 'cp: the copy is newer than the original'
[ kept -nt old ] || [ kept -ot old ] || echo 'cp -p: same modification time'
[ old -ef copy ] || echo 'cp: a new inode'

echo '$ rm removes a name, and consults the directory, not the file'
echo data > keep; ln keep alias; rm keep
echo "alias still reads '$(cat alias)', link count $(stat -c %h alias)"
touch locked; chmod 000 locked
rm locked && echo 'rm of a mode 000 file: removed'
mkdir ro; touch ro/f; chmod 666 ro/f; chmod a-w ro
rm ro/f 2>&1
chmod u+w ro

echo '$ the time in ls -l'
birth=$(stat -c %W old); mtime=$(stat -c %Y old)
[ "$birth" -gt "$mtime" ] && echo 'birth time is later than the modification time of 2020-01-01'
$ mv inside one file system
same inode after mv
$ rmdir, then rm -r
rmdir: failed to remove 'TestDir2': Directory not empty
rm -r removed the tree
$ cp -r, twice
project2
project2/README.md
project2/simpleproject
project2/simpleproject/README.md
project2/simpleproject/src
project2/src
$ cp and cp -p
cp: the copy is newer than the original
cp -p: same modification time
cp: a new inode
$ rm removes a name, and consults the directory, not the file
alias still reads 'data', link count 1
rm of a mode 000 file: removed
rm: cannot remove 'ro/f': Permission denied
$ the time in ls -l
birth time is later than the modification time of 2020-01-01

The first block shows that mv TestDir TestDir2 keeps the inode number, so the lecture's rename moved no data. The second block reproduces the lecture's failure: rmdir prints Directory not empty, and rm -r removes the directory and everything under it. Because rmdir needs an empty directory, the recursion must remove the entries of a directory before it removes the directory. In the third block the second cp -r simpleproject project2 found that project2 already existed, and so it created project2/simpleproject inside it; the lecture's single run was the first of these. The fourth block shows the three differences between a copy and the original: the copy is a new inode, it belongs to the user who ran cp (the listing does not print the owner; by inode(7) the owner of a new file is the effective user of the creating process), and it has a new modification time unless cp -p asks for the old one.

The fifth block corrects the lecture's description of rm. The lecture, and its slide (W1L4 12:45), say that rm deletes the data permanently and should be used with caution. The caution is justified, because rm calls unlink and keeps no copy of the name, but the statement is too strong in two ways. First, rm removes a name. The file survives while another name or an open descriptor exists: after ln keep alias and rm keep, the content is still readable through alias and the link count is 1. Second, rm does not overwrite the content. The Coreutils manual (§11.5) warns that "it is usually possible to recover the contents of that file" and points to shred for those who need more assurance. The same block shows that the file's own mode is not consulted: rm removed a file of mode 000, and refused to remove a file of mode 666 in a directory without write permission, with Permission denied. The mode that decides is the one of the containing directory, as unlink(2) states. When stdin is a terminal and a file is unwritable, rm asks for confirmation before it removes it (Coreutils §11.5); in the listing stdin is not a terminal, so no question appears. The last block separates two times. The date column of ls -l is the modification time, not the creation time; the birth time is stored separately, and for a file that touch -d gave a modification time in 2020, the birth time is later than that.

The lecture creates files in three ways. touch makes an empty file. An editor, vim on the command line or gedit in a window, makes a file with content. The ls -la screen of the vim demonstration (W1L4 4:45) shows file1 made by touch with size 0 and file2 made by vim with size 33, and both with mode -rw-rw-r--. Both programs therefore request the same mode when they create a file, which the section on defaults explains. The lecture's remark that touch creates "an empty file" is correct for a name that does not exist. For a name that exists, touch changes timestamps and keeps the content; the listing uses touch -d for that purpose.

Concatenation and redirection W1L4 13:14

The lecture introduces cat as the command that prints a file on the terminal, concatenates several files, and, with > or >>, creates a file from them. The slide marks > with the word Redirect (W1L4 17:45) and shows it inside the cat command line, which hides who does the work. The shell reads the characters > file4, removes them from the command line, opens file4 itself, and starts cat with its standard output attached to the open file. For > the shell opens the file for writing with O_TRUNC, which truncates an existing regular file to length 0 (open(2)). For >> it opens with O_APPEND, so that "before each write" the offset moves to the end of the file. The program cat never sees the operator. Chapter 5 treats the shell's parsing in full; here only the order of events matters, because it decides what a command like cat file2 file3 > file2 does. The listing runs the lecture's four examples and two commands that name an input file as the output. Inputs: two small files with the lecture's content. Output: the line counts and byte counts that each command leaves, and the diagnostics. The check is that the counts match the order of events just described.

#!/bin/bash
d=$(mktemp -d /tmp/hpsc-ch03.XXXXXX); cd "$d" || exit 1
trap 'cd /; rm -rf "$d"' EXIT
printf 'My name is Linux\nI am the best !\n' > file2
printf 'Hello from GNU\nI am Open Source !\n' > file3

echo '$ cat file2 file3'
cat file2 file3
echo '$ cat file2 file3 > file4'
cat file2 file3 > file4
echo "file4 has $(wc -l < file4) lines"
echo '$ cat file2 file3 > file4   (second time)'
cat file2 file3 > file4
echo "file4 has $(wc -l < file4) lines"
echo '$ cat file2 file3 >> file4'
cat file2 file3 >> file4
echo "file4 has $(wc -l < file4) lines"
echo '$ cat file3 >> file3'
{ cat file3 >> file3; } 2>&1; echo "exit status $?, file3 has $(wc -c < file3) bytes"
echo '$ cat file2 file3 > file2'
echo "file2 has $(wc -c < file2) bytes before"
cat file2 file3 > file2; echo "exit status $?"
echo "file2 has $(wc -c < file2) bytes after; file3 has $(wc -c < file3)"
cmp -s file2 file3 && echo 'file2 now equals file3: the old content of file2 is gone'
$ cat file2 file3
My name is Linux
I am the best !
Hello from GNU
I am Open Source !
$ cat file2 file3 > file4
file4 has 4 lines
$ cat file2 file3 > file4   (second time)
file4 has 4 lines
$ cat file2 file3 >> file4
file4 has 8 lines
$ cat file3 >> file3
cat: file3: input file is output file
exit status 1, file3 has 34 bytes
$ cat file2 file3 > file2
file2 has 33 bytes before
exit status 0
file2 has 34 bytes after; file3 has 34
file2 now equals file3: the old content of file2 is gone

The first > creates file4 with the 4 lines of the two inputs. The second > leaves 4 lines: the old content was replaced, as the lecture says. >> appends 4 more lines and gives 8. The last example loses data. The shell truncated file2 to 0 bytes before cat started, so cat read an empty file2 and the 34 bytes of file3 and wrote 34 bytes into file2. The command exits with status 0 and prints nothing, and the original 33 bytes of file2 are gone; cmp shows that file2 now equals file3. GNU cat refuses the append form: cat file3 >> file3 prints input file is output file, exits with status 1 and leaves file3 at 34 bytes. It does not refuse the truncating form, because the input is already empty when cat starts, so a script must not rely on the check. The lecture's demonstration used a new file as the target and did not meet the case.

Owner, group and other: the access rule and the ten characters W1L4 25:50

The lecture states that Linux is a multi-user system, so a file needs an owner and rules for others, and divides users into three categories: the owner (slide: "The person who owns the file"), the group ("A group of users who share access to the file") and others ("Everyone else"). It divides the access into read, write and execute, and says that for a directory, execute permission lets a user navigate or traverse it (slide: "traverse a directory"). These statements are correct. They leave one question open: when a user fits more than one category, which rights apply?

The access rule. The question for a process \(q\) and a file \(f\) is whether the kernel grants the access \(a\), where \(a\) is 4 for read, 2 for write or 1 for execute (search, for a directory). Linux selects one class and looks at that class only (path_resolution(7), section "Permissions"): the owner class when the effective user ID of \(q\) equals the owner of \(f\); otherwise the group class when the group of \(f\) is the effective group or one of the supplementary groups of \(q\); otherwise the other class. In symbols, with \(u_q\) and \(G_q\) from the model, \[ c(q,f)=\begin{cases}\mathrm{u} & u_q=u_f,\\ \mathrm{g} & u_q\ne u_f \text{ and } g_f\in G_q,\\ \mathrm{o} & \text{otherwise,}\end{cases}\tag{3.2} \] and an unprivileged process is granted the access \(a\) exactly when \[ m_{c(q,f)}\wedge a\neq 0 .\tag{3.3} \] Linux compares the file-system user ID, which equals the effective user ID unless a program changes it with setfsuid (path_resolution(7)). POSIX.1-2024 (XBD 4.7) states the same: access is granted "if the requested access permission bit is set for the class (file owner class, file group class, or file other class) to which the process belongs"; a privileged process is granted read, write and search always, and execute if at least one execute bit is set. The same section lets an implementation add mechanisms: an additional mechanism "shall only further restrict" access, and an alternate mechanism must be "enabled only by explicit user action, on a per-file basis". For a file reached by a path, the kernel applies (3.3) to the file and also requires search permission, (3.3) with \(a=1\), on every directory of the path before the last name.

The consequence of the word "otherwise" in (3.2) is that the classes are not cumulative. A user who owns a file is judged by the owner digit alone, even if the group digit is larger. Table 3.2 gives the meaning of the three rights. In the directory column, read and execute are independent rights, and creation and removal need write and execute together.

Table 3.2. The meaning of the three access rights for a regular file and for a directory.
RightRegular fileDirectory
read (4)Read the contentList the names in the table
write (2)Change the contentCreate, remove and rename entries (with execute also granted; the file's own mode is not consulted)
execute (1)Run the file as a programSearch: use the directory in a path, that is, look up a name in it

The listing tests (3.2) and Table 3.2 on a scratch file and a scratch directory. Inputs: none. Output: what cat, ls and cd do under each mode, with the diagnostics of the shell. The checks are: with mode 070 or 007 the owner is refused although the group or other digit is 7; with mode 400 the owner reads; a directory with mode 400 lists its names but allows neither cat dir/f nor cd dir; a directory with mode 100 refuses ls but allows cat dir/f and cd dir.

#!/bin/bash
d=$(mktemp -d /tmp/hpsc-ch03.XXXXXX); cd "$d" || exit 1
trap 'cd /; chmod -R u+rwx "$d"; rm -rf "$d"' EXIT
umask 022

echo '--- the owner class is chosen first'
echo secret > f; chmod 070 f
echo "mode 070:"; cat f 2>&1
chmod 007 f
echo "mode 007:"; cat f 2>&1
chmod 400 f
echo "mode 400:"; cat f 2>&1

echo '--- read and search on a directory are different rights'
mkdir dir; echo x > dir/f
chmod 400 dir
echo "mode 400 on dir:"; ls dir; cat dir/f 2>&1; cd dir 2>&1
chmod 100 dir
echo "mode 100 on dir:"; ls dir 2>&1; cat dir/f; cd dir && echo 'cd works'
--- the owner class is chosen first
mode 070:
cat: f: Permission denied
mode 007:
cat: f: Permission denied
mode 400:
secret
--- read and search on a directory are different rights
mode 400 on dir:
f
cat: dir/f: Permission denied
owner_class.sh: line 17: cd: dir: Permission denied
mode 100 on dir:
ls: cannot open directory 'dir': Permission denied
x
cd works

The first two blocks prove the selection of one class. The test user owns f and is in its group (the group of a new file is the user's primary group). Under mode 070 the group digit is 7, and the access is refused: \(c(q,f)=\mathrm{u}\), and \(m_{\mathrm u}=0\). The same happens under 007. Under 400 the access is granted. The directory blocks show that the two rights are independent. With read only, ls dir prints the name f, because listing needs only the table of names; cat dir/f and cd dir fail, because both need search permission. With search only, ls dir is refused, but cat dir/f prints the content (the user must know the name), and cd dir succeeds.

The ten characters. The mode string that ls -l prints is a code for the type and the 12-bit mode. The first character is the type. The lecture names two values, - for a regular file and d for a directory; the Coreutils manual (§10.1.2) lists the others: l symbolic link, c and b character and block devices, p FIFO, s socket. The next nine characters are three groups of three, for the owner, the group and the other class, and each position holds r, w or x or the character - for a clear bit. A special bit is shown in the position of the execute bit of its class: s when the set-user-ID (set-group-ID) bit and the execute bit are both set, S when only the special bit is set, t when the sticky bit and other-execute are both set, and T when only the sticky bit is set (Coreutils §10.1.2). Figure 3.2 shows the layout of the 12 bits.

The 12 bits of a mode Twelve boxes in four groups of three. The first group holds the special bits set-user-ID 4, set-group-ID 2 and sticky 1. The next three groups hold read 4, write 2 and execute 1 for owner, group and other. Each group is one octal digit. suidsgidsticky rwx rwx rwx 421 421 421 421 special digit owner digit m_u group digit m_g other digit m_o ls shows s, S inthe owner or group x,t, T in the other x octal 0100, 0200, 0400 octal 010, 020, 040 octal 01, 02, 04
Figure 3.2. The 12 bits of a mode, as four octal digits. The weight of each bit inside its digit is written under the box. The mode 4755 has the digits 4, 7, 5, 5: set-user-ID, then rwx, r-x and r-x. The figure shows the bit layout; it does not show which operations each bit controls.

The lecture decodes three strings on its screens (W1L4 29:45 to 32:45). -rw-rw-r-- is a regular file: owner rw- (4+2), group rw- (4+2), other r-- (4), so mode 664. drwxrwxr-x is a directory with the digits 7, 7, 5: mode 775. -rwxrwxr-x, the executable exe.sh on the same screen, is a regular file with the digits 7, 7, 5. The other class of the directory holds read and execute, as the screen at W1L4 31:45 shows.

The decoder below turns a mode into its ten characters by the rules above. Inputs: the mode of 4096 files, one for each 12-bit value, created in a scratch directory and set by chmod; a directory, a symbolic link and a FIFO; and the file /usr/bin/passwd. Output: the number of files for which the function agrees with GNU stat -c %A and with Python's stat.filemode, then examples. The check is agreement on all 4096 values.

import os, stat, subprocess, tempfile

def mode_string(mode):
    """The ten characters of ls -l for st_mode, from the bit layout of stat(2)."""
    kind = {0o140000: 's', 0o120000: 'l', 0o100000: '-', 0o060000: 'b',
            0o040000: 'd', 0o020000: 'c', 0o010000: 'p'}[mode & 0o170000]
    out = [kind]
    for shift, special, low, high in ((6, 0o4000, 's', 'S'), (3, 0o2000, 's', 'S'), (0, 0o1000, 't', 'T')):
        bits = (mode >> shift) & 7
        out.append('r' if bits & 4 else '-')
        out.append('w' if bits & 2 else '-')
        if mode & special:
            out.append(low if bits & 1 else high)
        else:
            out.append('x' if bits & 1 else '-')
    return ''.join(out)

def main():
    with tempfile.TemporaryDirectory(dir='/tmp') as d:
        for m in range(4096):
            path = os.path.join(d, f'{m:04o}')
            open(path, 'w').close()
            os.chmod(path, m)
        names = sorted(os.listdir(d))
        text = subprocess.run(['stat', '-c', '%n %A', *names], cwd=d, capture_output=True, text=True).stdout
        gnu = dict(line.split() for line in text.splitlines())
        agree_gnu = sum(mode_string(os.stat(os.path.join(d, n)).st_mode) == gnu[n] for n in names)
        agree_py = sum(mode_string(os.stat(os.path.join(d, n)).st_mode) == stat.filemode(os.stat(os.path.join(d, n)).st_mode) for n in names)
        print(f'{len(names)} regular files, one per 12-bit mode')
        print(f'mode_string equals GNU stat %A for {agree_gnu}, equals stat.filemode for {agree_py}')
        os.mkdir(os.path.join(d, 'dir'))
        os.symlink('0644', os.path.join(d, 'link'))
        os.mkfifo(os.path.join(d, 'fifo'))
        for n in ('dir', 'link', 'fifo', '4755', '2770', '1777'):
            st = os.lstat(os.path.join(d, n))
            print(f'{n:>5} {oct(st.st_mode & 0o7777)[2:].zfill(4)} {mode_string(st.st_mode)}')
    print('/usr/bin/passwd', mode_string(os.stat('/usr/bin/passwd').st_mode))

main()
4096 regular files, one per 12-bit mode
mode_string equals GNU stat %A for 4096, equals stat.filemode for 4096
  dir 0755 drwxr-xr-x
 link 0777 lrwxrwxrwx
 fifo 0644 prw-r--r--
 4755 4755 -rwsr-xr-x
 2770 2770 -rwxrws---
 1777 1777 -rwxrwxrwt
/usr/bin/passwd -rwsr-xr-x

The function agrees with both references on all 4096 values, so the four octal digits and the ten characters carry the same information (for the type character, the examples cover four of the seven types: regular file, directory, symbolic link and FIFO). The examples show the special bits: mode 4755 prints s in the owner execute position, 2770 prints s in the group position, 1777 prints t, and the setuid program /usr/bin/passwd of this machine prints -rwsr-xr-x.

Finally, the access rule (3.2) and (3.3) is a prediction about the kernel, so the next program tests it. Inputs: 512 files that the test user owns, one for each permission mode; then up to 3000 entries under each of /etc, /var/log, /usr/bin and /home. The program computes the predicate from the inode and compares it with os.access, which asks the kernel with the effective user, for read, write and execute. Assumptions: the process is unprivileged; the file system is the root ext4 file system; path-prefix search permission is not modelled, which restricts the second test to entries that the directory walk reached. Output: agreement counts, and whether each class of system entries had at least 20 entries.

import os, tempfile

BIT = {'r': 4, 'w': 2, 'x': 1}

def may(mode, owner, group, user, want):
    """Access predicate for an unprivileged process. user = (uid, set of group ids)."""
    uid, gids = user
    shift = 6 if uid == owner else 3 if group in gids else 0   # first matching class only
    return bool((mode >> shift) & BIT[want])

# 1. The kernel agrees with the predicate for the owner class: 512 modes x 3 requests.
me = (os.geteuid(), set(os.getgroups()) | {os.getegid()})
agree = total = 0
with tempfile.TemporaryDirectory(dir='/tmp') as d:
    for m in range(512):
        p = os.path.join(d, f'{m:03o}')
        open(p, 'w').close()
        os.chmod(p, m)
        for want, flag in (('r', os.R_OK), ('w', os.W_OK), ('x', os.X_OK)):
            total += 1
            agree += may(m, os.geteuid(), os.getegid(), me, want) == os.access(p, flag, effective_ids=True)
print(f'owner class, own files: kernel and predicate agree on {agree} of {total} requests')

# 2. ... and for the group and other classes, on files this user does not own.
seen = {'owner': 0, 'group': 0, 'other': 0}
bad = bad_acl = 0
dev = os.stat('/').st_dev
for top in ('/etc', '/var/log', '/usr/bin', '/home'):
    budget = 3000                                  # entries examined under each top directory
    for dirpath, dirs, files in os.walk(top):
        if budget == 0:
            break                                  # stop the walk, not only the inner loop
        for name in files + dirs:
            if budget == 0:
                break
            budget -= 1
            p = os.path.join(dirpath, name)
            try:
                st = os.lstat(p)
            except OSError:
                continue
            if st.st_dev != dev or os.path.islink(p):
                continue
            cls = 'owner' if st.st_uid == me[0] else 'group' if st.st_gid in me[1] else 'other'
            seen[cls] += 1
            try:
                has_acl = 'system.posix_acl_access' in os.listxattr(p)
            except OSError:
                has_acl = False
            for want, flag in (('r', os.R_OK), ('w', os.W_OK), ('x', os.X_OK)):
                if may(st.st_mode & 0o777, st.st_uid, st.st_gid, me, want) != os.access(p, flag, effective_ids=True):
                    bad_acl += has_acl
                    bad += not has_acl
print('system entries examined per class, at least 20 each:', {k: v >= 20 for k, v in seen.items()})
print('disagreements on files without an ACL:', bad)
print('disagreements on files with an ACL (the mode bits are not the whole rule):', 'some' if bad_acl else 'none')
owner class, own files: kernel and predicate agree on 1536 of 1536 requests
system entries examined per class, at least 20 each: {'owner': True, 'group': True, 'other': True}
disagreements on files without an ACL: 0
disagreements on files with an ACL (the mode bits are not the whole rule): some

For files owned by the test user the kernel agrees with the predicate on all 1536 requests (512 modes, three accesses). This test is exhaustive for the owner class only: for its own files the predicate always selects the owner digit, so the order of the group class and the other class is not tested by it. Under /etc, /var/log and /usr/bin the walk ended before the budget (1516, 206 and 1093 entries examined); only under /home did it stop at 3000. Of the entries that were not symbolic links and lay on the root file system, 2997 were in the owner class (all under /home, the test user's own files), 42 in the group class (all under /var/log) and 1946 in the other class, in the run that produced the sample. The printed line says that each class had at least 20 entries, and the number of disagreements on files without an access control list was 0. The counts depend on the machine and change as logs rotate. The order between the group class and the other class (for example, a group member refused under mode 0604) was not tested against the kernel. On the files that carry an ACL attribute the predicate disagrees with the kernel for some requests: examples are the systemd journal files under /var/log/journal, which have mode 0640, owner root and a group that the test user does not belong to, and which the test user can nevertheless read. The program does not decode the ACL, and getfacl is not installed, so the listing does not say which entry grants the access; it shows only that the nine mode bits are not the whole rule on a file that has the attribute. This is the extra mechanism that POSIX allows, and the boundary of the model of this chapter.

Changing the mode: symbolic and octal notation W1L4 33:07

Only the owner of a file, or a privileged process, may change its mode (POSIX chmod, DESCRIPTION; chmod(2)). The lecture presents the two notations of chmod. This section gives each a precise meaning and tests the symbolic one against the program on the machine.

Octal notation. The lecture assigns the weights 4, 2 and 1 to read, write and execute and adds the weights of the rights to be granted: 7 is 4+2+1, 5 is 4+1, 6 is 4+2. The assignment is a bijection between the eight subsets of \(\{r,w,x\}\) and the numbers 0 to 7: \[ d = 4\,[r] + 2\,[w] + 1\,[x] , \tag{3.4} \] where \([r]\) is 1 if read is granted and 0 otherwise, and likewise for \([w]\) and \([x]\). Equation (3.4) is a binary numeral whose leading bit is \([r]\). Every integer from 0 to 7 has exactly one such numeral, so each digit names exactly one subset. Three classes give \(8^3=512=2^9\) permission modes, and a fourth digit for the special bits gives the 4096 values of the decoder. The lecture's examples follow: 755 is owner 7 (rwx), group 5 (r-x), other 5 (r-x); 644 is rw-, r--, r--; 777 is rwx for everyone.

Symbolic notation. A symbolic mode is a list of clauses separated by commas. Each clause is a who (any of u, g, o, or a for all three, or nothing), an operator (+, - or =), and a permission (any of r, w, x, or one of u, g, o to copy the current rights of that class); a clause may repeat the operator and permission part (POSIX chmod, EXTENDED DESCRIPTION; the letters X, s and t are left out of this chapter's model). Let \(K\) be the set of classes of the who, with all three when the who is a or empty, and let \(b\) be the 3-bit value of the permission. Let \(\sigma_K(b)\) be \(b\) placed in each class of \(K\): \(\sigma_K(b)=\sum_{c\in K}b\cdot 8^{e_c}\). Let \(A\) be the mask of bits that the clause may change: \(A=\lnot\mu\) when the who is empty and \(A=511\) otherwise, where \(\mu\) is the creation mask of the process that runs chmod. Then the clause changes the current mode \(m\) as \[ m'=\begin{cases} m\vee(\sigma_K(b)\wedge A) & \text{for } +,\\ m\wedge\lnot(\sigma_K(b)\wedge A) & \text{for } -,\\ \bigl(m\wedge\lnot\sigma_K(7)\bigr)\vee(\sigma_K(b)\wedge A) & \text{for } =. \end{cases}\tag{3.5} \] For the permission copied from a class, \(b\) is the digit of that class in \(m\) before the operation. The mask \(A\) is the point that the lecture does not make. POSIX.1-2024 states: when the who is not specified, the bits are set or cleared "except for those with corresponding bits in the file mode creation mask of the invoking process". The Coreutils manual (§27.2.6) gives the example that with a mask of 2 the mode +w gives write permission to the owner and the group "but not to other users", while a+w "ignores umask".

The first listing repeats the lecture's chmod session, with the modes printed by stat instead of ls -l, and then shows what the mask changes. Inputs: new files made under the masks 002 and 027. Output: octal and symbolic mode after each command. The checks: the lecture's results (chmod +x file2 gives -rwxrwxr-x, chmod 777 gives -rwxrwxrwx; W1L4 39:45 to 41:45) must reproduce; and +x must give a different result under mask 027.

#!/bin/bash
d=$(mktemp -d /tmp/hpsc-ch03.XXXXXX); cd "$d" || exit 1
trap 'cd /; rm -rf "$d"' EXIT
show() { stat -c '%a %A %n' "$@"; }

echo '--- the lecture session, umask 002'
umask 002
touch file1 file2
show file1 file2
chmod +x file2; show file2
chmod 777 file1; show file1
chmod 755 file1; show file1
chmod 644 file1; show file1
chmod u+x file1; chmod g-w file1; chmod a+r file1; show file1

echo '--- the same chmod +x under umask 027'
umask 027
touch g1 g2
chmod +x g1; chmod a+x g2
show g1 g2

echo '--- chmod -w under umask 002 on a file that others may write'
umask 002
touch h; chmod 646 h
chmod -w h 2>&1; echo "exit status $?"
show h
--- the lecture session, umask 002
664 -rw-rw-r-- file1
664 -rw-rw-r-- file2
775 -rwxrwxr-x file2
777 -rwxrwxrwx file1
755 -rwxr-xr-x file1
644 -rw-r--r-- file1
744 -rwxr--r-- file1
--- the same chmod +x under umask 027
750 -rwxr-x--- g1
751 -rwxr-x--x g2
--- chmod -w under umask 002 on a file that others may write
chmod: h: new permissions are r--r--rw-, not r--r--r--
exit status 1
446 -r--r--rw- h

The first block reproduces the lecture's screens. A file created by touch has mode 664 (the lecture's -rw-rw-r--), chmod +x turns 664 into 775, and chmod 777, 755 and 644 set the modes absolutely. The last line chains three clauses: u+x, g-w and a+r take 644 to 744, which the lecture's three symbolic examples predict. The lecture's demonstration gives execute to all three classes (664 becomes 775, W1L4 39:45), and the demonstration suggests that a clause without a who acts on all classes. The second block shows that this statement is not general. Under mask 027, chmod +x gives 750, with execute for the owner and the group only, while chmod a+x gives 751. By (3.5), \(A=\lnot 027=0750\), and the others' execute bit is outside \(A\). The lecture's result, execute for all three classes, holds for the lecture's machine, whose mask is 002, and does not hold for a user whose mask is 027. The third block shows the mask on the minus operator. For mode 646 and mask 002, chmod -w clears write for the owner and the group and leaves it for the other class: the result is 446, the mode that (3.5) predicts. GNU chmod prints a warning that the new permissions differ from the plain removal of write, and exits with status 1.

The second listing turns (3.5) into a program and tests it against chmod. Inputs: all 512 permission modes as starting modes; 16 clauses that include the lecture's examples (+x, a+w, u+x, g-w), absolute settings (=rw, a=r, o=), copies (go=u, g=u,o=g, o+g), and a list (u=rwx,g=rx,o=r); three masks, 002, 022 and 027. Output: five worked examples from the model, then the number of cases and the number on which the model and GNU chmod differ. The program runs the real chmod once per clause and mask on all 512 files. Assumptions: nine permission bits only; files, not directories.

import os, re, subprocess, tempfile

R, W, X = 4, 2, 1
SHIFT = {'u': 6, 'g': 3, 'o': 0}

def apply_symbolic(mode, spec, umask):
    """POSIX symbolic mode on the nine permission bits (no s, t, X)."""
    for clause in spec.split(','):
        who = re.match(r'[ugoa]*', clause).group()
        classes = 'ugo' if who in ('', 'a') or 'a' in who else who
        masked = who == ''
        for op, perm in re.findall(r'([-+=])([ugo]|[rwx]*)', clause[len(who):]):
            if perm in ('u', 'g', 'o'):
                bits = (mode >> SHIFT[perm]) & 7
            else:
                bits = sum({'r': R, 'w': W, 'x': X}[c] for c in perm)
            field = 0
            for c in classes:
                field |= bits << SHIFT[c]
            allowed = 0o777 & ~umask if masked else 0o777
            span = sum(7 << SHIFT[c] for c in classes)
            if op == '+':
                mode |= field & allowed
            elif op == '-':
                mode &= ~(field & allowed)
            else:
                mode = (mode & ~span) | (field & allowed)
    return mode

CLAUSES = ['+x', '-x', '+w', '-w', 'a+w', 'a+x', 'u+x', 'g-w', 'o=', 'go=u', 'g=u,o=g',
           'u=rwx,g=rx,o=r', '=rw', 'a=r', 'g+w,o-r', 'o+g']
UMASKS = [0o002, 0o022, 0o027]

def main():
    cases = bad = 0
    with tempfile.TemporaryDirectory(dir='/tmp') as d:
        names = []
        for m in range(512):
            p = os.path.join(d, f'{m:03o}')
            open(p, 'w').close()
            os.chmod(p, m)
            names.append(p)
        for spec in CLAUSES:
            for u in UMASKS:
                for m in range(512):
                    os.chmod(names[m], m)
                subprocess.run(['chmod', spec, *names], stderr=subprocess.DEVNULL, preexec_fn=lambda: os.umask(u))
                for m in range(512):
                    got = os.stat(names[m]).st_mode & 0o777
                    cases += 1
                    if got != apply_symbolic(m, spec, u):
                        bad += 1
                        if bad <= 5:
                            print(f'MISMATCH {spec} umask {u:03o} from {m:03o}: chmod {got:03o} model {apply_symbolic(m, spec, u):03o}')
    for m, spec, u in [(0o664, '+x', 0o002), (0o664, '+x', 0o027), (0o664, 'a+x', 0o027),
                       (0o444, '+w', 0o022), (0o444, 'a+w', 0o022)]:
        assert spec in CLAUSES and u in UMASKS
        print(f'{m:03o} {spec:<4} umask {u:03o} -> {apply_symbolic(m, spec, u):03o}')
    print(f'{cases} cases: {len(CLAUSES)} clauses x {len(UMASKS)} umasks x 512 initial modes')
    print(f'disagreements between the model and GNU chmod: {bad}')

main()
664 +x   umask 002 -> 775
664 +x   umask 027 -> 774
664 a+x  umask 027 -> 775
444 +w   umask 022 -> 644
444 a+w  umask 022 -> 666
24576 cases: 16 clauses x 3 umasks x 512 initial modes
disagreements between the model and GNU chmod: 0

The model and chmod agree on all \(16\times3\times512=24\,576\) cases. The model's examples give the lecture's case (664 with +x under 002 gives 775), the same command under 027 (774, where the others' bit is withheld and, in this example, the owner and group execute bits are granted), and a+x (775). They also give the case that explains a common failure: chmod +w on a read-only file (444) under mask 022 gives 644, with write for the owner only, while a+w gives 666. The agreement establishes (3.5) for permission bits, files and the 16 clauses; it does not establish the behaviour for the letters X, s and t, for directories, or for clauses outside the list, and the warning of GNU chmod in the third block of the previous listing is not part of the model.

Ownership: chown, chgrp and the group of a new file W1L4 42:39

The lecture states that the user of the session is by default the owner of a new file, shows chown owner:group file with the syntax of the slide (W1L4 43:45), lists the groups of the session with the command groups (output master wheel shadow), and gives a file to the user minion1 and the group shadow (W1L4 44:45). The first statement is correct and can be made more precise. By inode(7), the owner of a new file is the effective user ID of the creating process. The group is, by open(2), either the effective group ID of the process (System V semantics) or the group of the parent directory (BSD semantics); "on Linux, the behavior depends on whether the set-group-ID mode bit is set on the parent directory: if that bit is set, then BSD semantics apply". The lecture's files have the group master, the personal group of the user master, so its directories did not have that bit. The section on defaults returns to this rule, because it solves the running case.

The lecture presents chown without stating that changing the owner needs privilege, and it does. The prompt on the lecture's screen (W1L4 44:45) is [root@localhost Desktop]#: the command ran as root. The session user is in the groups wheel and shadow (W1L4 43:45), so the :shadow half of chown minion1:shadow file3 needed no privilege; only the change of owner to minion1 did. By chown(2), "only a privileged process (Linux: one with the CAP_CHOWN capability) may change the owner of a file", and the owner of a file "may change the group of the file to any group of which that owner is a member". The same page adds that when an unprivileged user changes the owner or group of an executable file, the set-user-ID and set-group-ID bits are cleared. The next transcript runs the three cases on numeric identities. The transcript was produced by the root account of the verification guest with the program setpriv, which runs a command with chosen numeric user, group and supplementary group IDs; no account was created, and the scratch directory was deleted afterwards. The code lab does not run it, because it needs root. Standard input was not a terminal; with a terminal, rm asks before it removes a write-protected file, and the removals of write-protected files in these transcripts would wait for an answer. The script lists the identities of the running case: alice 2001 with groups 2001 and 2100, bob 2002 with groups 2002 and 2100, and carol 2003 with group 2003 only. The sticky-bit lines of the output belong to the next section.

exec 2>&1
# Run as root. Identities are numeric, so no account is created.
# alice 2001:2001, bob 2002:2002, carol 2003:2003, group hpsc 2100.
as() { u=$1; shift; case $u in
  alice) setpriv --reuid=2001 --regid=2001 --groups=2001,2100 -- "$@";;
  bob)   setpriv --reuid=2002 --regid=2002 --groups=2002,2100 -- "$@";;
  carol) setpriv --reuid=2003 --regid=2003 --groups=2003      -- "$@";;
esac; }
d=$(mktemp -d /tmp/hpsc-root-demo.XXXX); chmod 755 $d; cd $d
echo "--- sticky bit"
mkdir pub; chmod 777 pub; as alice touch pub/a.dat; as bob rm pub/a.dat; echo "bob rm in 0777 dir exit $?"
as alice touch pub/b.dat; chmod 1777 pub; as bob rm pub/b.dat; echo "bob rm in 1777 dir exit $?"
as alice rm pub/b.dat; echo "alice rm in 1777 dir exit $?"
echo "--- chown and chgrp by an unprivileged owner"
mkdir own; chown 2001:2001 own; cd own
as alice touch mine.dat; as alice chown 2002 mine.dat; echo "alice chown to bob exit $?"
as alice chgrp 2100 mine.dat; echo "alice chgrp to hpsc (a group of alice) exit $?"
as alice chgrp 2003 mine.dat; echo "alice chgrp to carol's group exit $?"
stat -c '%A %u:%g %n' mine.dat
cd /; rm -rf $d
--- sticky bit
bob rm in 0777 dir exit 0
rm: cannot remove 'pub/b.dat': Operation not permitted
bob rm in 1777 dir exit 1
alice rm in 1777 dir exit 0
--- chown and chgrp by an unprivileged owner
chown: changing ownership of 'mine.dat': Operation not permitted
alice chown to bob exit 1
alice chgrp to hpsc (a group of alice) exit 0
chgrp: changing group of 'mine.dat': Operation not permitted
alice chgrp to carol's group exit 1
-rw-r--r-- 2001:2100 mine.dat

Alice cannot give her file to Bob: chown returns Operation not permitted and exits with status 1. She can change its group to 2100, because she is a member of that group (exit status 0), and cannot change it to Carol's group 2003, which she is not in. The final stat shows the result: owner 2001, group 2100. In the lecture, chown minion1:shadow file3 needed root for the same reason, and on a shared system a user who wants another owner must ask the administrator, while changing the group to a group of which one is a member needs no help. That second operation is what the running case uses to place a project directory in the group hpsc.

Defaults and special bits: the creation mask, set-group-ID, sticky and set-user-ID

This section covers what the lectures leave out and the running case needs. It has no recording behind it. The lecture shows that a new file has mode -rw-rw-r-- and a new directory drwxrwxr-x and the slides do not say where these modes come from.

The creation mask. A program that creates a file passes the mode that it requests to the system call, and the kernel removes the bits that the process's creation mask \(\mu\) names. For open with O_CREAT, "in the absence of a default ACL, the mode of the created file is (mode & ~umask)" (open(2)); for mkdir it is "(mode & ~umask & 0777)" (mkdir(2)). In the notation of the model, \[ m_{\text{new}} = m_{\text{req}}\wedge\lnot\mu . \tag{3.6} \] The mask belongs to the process and is set by the shell command umask (POSIX umask), so the same command can create files with different modes in two sessions. The tools of the lectures behave as if they request 0666 for a file (touch, the editors, redirection) and 0777 for a directory (mkdir): no execute bit is requested for a file, and a directory needs the execute bit to be usable. (The requests of the lecture's tools were not observed; the statement is the assumption that the next program makes.) The lecture's machine produced 664 and 775. The next program tests (3.6) for all 512 masks and then solves for the mask of the lecture. Inputs: each mask from 000 to 777, a file created with requested mode 0666 and a directory created with 0777 under it. Output: the number of masks for which the result equals (3.6), and the masks that reproduce a file mode of 664 and a directory mode of 775 together.

import os, tempfile

ok_file = ok_dir = 0
fits = []
with tempfile.TemporaryDirectory(dir='/tmp') as d:
    for u in range(512):
        os.umask(u)
        f, g = os.path.join(d, f'f{u:03o}'), os.path.join(d, f'd{u:03o}')
        os.close(os.open(f, os.O_CREAT | os.O_WRONLY, 0o666))
        os.mkdir(g, 0o777)
        file_mode, dir_mode = os.stat(f).st_mode & 0o777, os.stat(g).st_mode & 0o777
        ok_file += file_mode == 0o666 & ~u
        ok_dir += dir_mode == 0o777 & ~u
        if file_mode == 0o664 and dir_mode == 0o775:
            fits.append(f'{u:03o}')
print(f'umask values tried: 512; file mode == 0666 & ~umask: {ok_file}; directory mode == 0777 & ~umask: {ok_dir}')
print('umask values that give a file mode 664 and a directory mode 775:', fits)
umask values tried: 512; file mode == 0666 & ~umask: 512; directory mode == 0777 & ~umask: 512
umask values that give a file mode 664 and a directory mode 775: ['002']

The kernel obeys (3.6) for all 512 masks, for files and for directories. Exactly one mask produces the lecture's pair: 002. The reasoning also follows from (3.6) by hand. A file request of 0666 has no execute bits, so the file cannot reveal whether the mask has execute bits; it shows that \(\mu\wedge0666=0002\). The directory request of 0777 reveals them all: the result 775 means that the only bit removed is the 002 bit, so \(\mu=0002\). The inference rests on three conditions. The tools requested 0666 for a file and 0777 for a directory, as umask_check.py assumes. The directories had no default ACL, because open(2) applies the mask only "in the absence of a default ACL". And the files and TestDir2 were made under the same mask. Under these conditions the lecture's screens fit the inference: the files made in the demonstrations have -rw-rw-r-- (W1L4 4:45, 6:45 and 39:45), and TestDir2 has drwxrwxr-x (W1L4 4:45). The same screen shows directories that do not fit the pair: the user's Desktop (the entry . at W1L4 4:45) has drwxr-xr-x and the home directory (..) has drwx------. Other programs created them, and these programs may request other modes or run under other masks, so these directories do not test the shell's mask. The root-owned simpleproject, drwxr-xr-x, was created by root, whose mask is separate. A mask of 002 for a user with a private group is one setting of the shadow and PAM configuration of Ubuntu 24.04: pam_umask(8), option usergroups, states "If the user is not root and the username is the same as primary group name, the umask group bits are set to be the same as owner bits (examples: 022 -> 002, 077 -> 007)", and the comment in /etc/login.defs of the verification machine states that with USERGROUPS_ENAB yes a default UMASK of 022 becomes 002 for a user whose user ID equals the group ID and whose name equals the primary group name, and login.defs(5) documents UMASK as the initial value of the mask for pam_umask. The lecture's files have the group master, the user's name; the screens do not show which mechanism set the mask on the lecture's machine. The verification machine's shell prints the mask 022 although its /etc/login.defs has USERGROUPS_ENAB yes and the user ID equals the group ID, so a file made there by touch has mode 644. The two defaults differ in one bit, the group's write permission, and that bit decides the running case.

The set-user-ID bit. The first UNIX file system had seven protection bits: six gave read, write and execute permission independently to the owner and to all other users, and the seventh worked as follows; "if the seventh bit is on, the system will temporarily change the user identification of the current user to that of the creator of the file whenever the file is executed as a program" (Ritchie and Thompson 1974, p. 367). That bit is set-user-ID (octal 4000, S_ISUID; inode(7)). A process started from such a file has the file's owner as effective user, and (3.2) then judges it as that user. The decoder's example, /usr/bin/passwd with mode -rwsr-xr-x and owner root on this machine, is an instance: a user who runs it gets a process whose effective user is root, the owner of the file. The first system had no group class at all, and the nine-bit mode with a group digit of this chapter is a later extension; the 1974 paper does not describe it. This chapter does not run a set-user-ID program, so it does not test the effect, and the description above rests on the paper and on inode(7).

The set-group-ID bit on a directory. On a regular file, set-group-ID (2000) is the group counterpart of the bit above. On a directory it has a different, documented meaning: "newly created subfiles inherit the same group as the directory, and newly created subdirectories inherit the set-group-ID bit of the parent directory" (Coreutils §27.5; open(2) and mkdir(2) say the same). Without the bit, a new file takes the creator's effective group, as the section on ownership showed. The bit solves the problem of the running case, because it makes the group of every new file the group of the project directory with no action by the creator. The listing tests it with a group that the test account belongs to and that is not its primary group. Inputs: a directory whose group is such a group. Output: the answers to four questions (is the group of a new file the directory's group, with and without the bit; is the bit inherited by a subdirectory; is the owner the creator), and the effect of three chmod commands on the bit. The Coreutils manual states that chmod 770 leaves the bit of a directory unchanged and that a numeric mode of five or more digits clears it; the last block tests that statement.

#!/bin/bash
d=$(mktemp -d /tmp/hpsc-ch03.XXXXXX); cd "$d" || exit 1
trap 'cd /; rm -rf "$d"' EXIT
umask 002
g=$(id -Gn | tr ' ' '\n' | grep -vx "$(id -gn)" | head -n 1)   # a group of mine other than my primary group
[ -n "$g" ] || { echo 'this account has no supplementary group'; exit 1; }
mkdir proj; chgrp "$g" proj; chmod 770 proj
same() { [ "$(stat -c %G "$1")" = "$2" ] && echo yes || echo no; }

echo "directory group differs from my primary group: $([ "$(stat -c %G proj)" != "$(id -gn)" ] && echo yes || echo no)"
touch proj/a
echo "without setgid, group of a new file is the directory group: $(same proj/a "$g")"
chmod g+s proj
stat -c '%A %n' proj
touch proj/b; mkdir proj/sub
echo "with setgid, group of a new file is the directory group: $(same proj/b "$g")"
stat -c '%A %n' proj/sub
echo "owner of the new file is me: $([ "$(stat -c %u proj/b)" = "$(id -u)" ] && echo yes || echo no)"
echo '--- does chmod keep the setgid bit of a directory?'
chmod 770 proj; stat -c '%a %A %n' proj
chmod 00770 proj; stat -c '%a %A %n' proj
chmod 2770 proj; chmod g-s proj; stat -c '%a %A %n' proj
directory group differs from my primary group: yes
without setgid, group of a new file is the directory group: no
drwxrws--- proj
with setgid, group of a new file is the directory group: yes
drwxrwsr-x proj/sub
owner of the new file is me: yes
--- does chmod keep the setgid bit of a directory?
2770 drwxrws--- proj
770 drwxrwx--- proj
770 drwxrwx--- proj

Without the bit, the new file proj/a is not in the directory's group (no). After chmod g+s proj the directory shows drwxrws---, the new file proj/b is in the directory's group (yes), the new subdirectory shows s in its group position (drwxrwsr-x) so the rule propagates down the tree, and the owner of the new file is still the creator. In the last block, chmod 770 proj kept the bit (mode 2770 in the first line), chmod 00770 proj cleared it (second line), and chmod g-s cleared it (third line). The consequence for administrators is that the form of the chmod command decides whether the shared directory stays shared. A mode written with four digits or fewer does not remove the bit from a directory, and a mode written with five digits does.

The sticky bit. On a directory the bit 1000 restricts deletion: a file in the directory "can be renamed or deleted only by the owner of the file, by the owner of the directory, and by a privileged process" (inode(7); POSIX XBD 4.5 gives the same three conditions and calls the writability of the file by the process optional and implementation-defined). It is the answer to the section on commands, where write permission on a directory allowed removal of any entry. The shared directory /tmp has mode drwxrwxrwt on this machine: every user may create entries, and only the owners may remove their own. The transcript of the section on ownership has the lines that test it. Bob removed Alice's file from a directory of mode 0777 (exit status 0), could not remove the same kind of file from a directory of mode 1777 (Operation not permitted, exit status 1), and Alice could remove her own file from the 1777 directory (exit status 0).

The shared project directory

The requirements R1 to R3 of the model section can now be met. The administrator creates the group hpsc and adds Alice and Bob. Alice creates proj, gives it to the group (chgrp hpsc proj, which she may do because she is a member), and sets mode 2770. Every member sets the mask 002 in the shell's start-up file. The design has three parts, and each one answers one requirement. The other digit of the directory is 0, so by (3.2) and (3.3) with \(a=1\), Carol has no search permission on proj and cannot reach anything inside it (R2). The set-group-ID bit gives every new file the group hpsc (R3, group). The mask 002 gives every new file mode 664, so the group digit is 6 and has write permission (R3, mode); and (3.2) then judges Alice and Bob by the group digit for files that the other one created.

The program below applies the rules to Bob's file in three configurations: the mask 022 without the bit, the mask 002 without the bit, and the mask 002 with the bit. It evaluates the access predicate of the previous sections for read and write, for each of the three users, on the file that Bob creates, and requires search permission on proj as well. Inputs: the identities of the model section and the three configurations. Output: for each configuration, the mode and group of the new file and the access of the three users. The check against the kernel follows in the next transcript.

# The shared project directory: who can do what to a file that bob creates.
BIT = {'r': 4, 'w': 2, 'x': 1}

def may(mode, owner, group, user, want):
    """Access predicate for an unprivileged process. user = (uid, set of group ids)."""
    uid, gids = user
    shift = 6 if uid == owner else 3 if group in gids else 0   # first matching class only
    return bool((mode >> shift) & BIT[want])

# Numeric identities of the running case; group 2100 is hpsc.
USERS = {'alice': (2001, {2001, 2100}), 'bob': (2002, {2002, 2100}), 'carol': (2003, {2003})}
EGID = {'alice': 2001, 'bob': 2002, 'carol': 2003}
HPSC = 2100

def create(directory, who, umask):
    """New file: owner = creator, mode = 0666 & ~umask, group = directory's if setgid else creator's."""
    dmode, dgroup = directory
    group = dgroup if dmode & 0o2000 else EGID[who]
    return (0o666 & ~umask, USERS[who][0], group)

def rights(directory, fileinfo, who):
    dmode, dgroup = directory
    fmode, fowner, fgroup = fileinfo
    reach = may(dmode, USERS['alice'][0], dgroup, USERS[who], 'x')       # search permission on proj
    return ''.join(c if reach and may(fmode, fowner, fgroup, USERS[who], c) else '-' for c in 'rw')

print('file created by bob in proj (alice:hpsc, others none); access of alice / bob / carol')
for label, dmode, umask in (('mode 0770, umask 022', 0o770, 0o022),
                            ('mode 0770, umask 002', 0o770, 0o002),
                            ('mode 2770, umask 002', 0o2770, 0o002)):
    directory = (dmode, HPSC)
    f = create(directory, 'bob', umask)
    row = '  '.join(f'{u} {rights(directory, f, u)}' for u in USERS)
    print(f'{label}: file {f[0]:o} group {f[2]}   {row}')
file created by bob in proj (alice:hpsc, others none); access of alice / bob / carol
mode 0770, umask 022: file 644 group 2002   alice r-  bob rw  carol --
mode 0770, umask 002: file 664 group 2002   alice r-  bob rw  carol --
mode 2770, umask 002: file 664 group 2100   alice rw  bob rw  carol --

In the first two configurations Alice has read access only. The file is in Bob's personal group 2002, so Alice is judged as part of the other class, whose digit is 4 in both cases; the mask 002 gives the group digit write permission, but for Bob's group, and Alice is not in it. In the third configuration the file is in group 2100, Alice is a member, and the group digit 6 gives her read and write access. In all three configurations Carol has no access, because the directory's other digit is 0. Only the third configuration meets R1, R2 and R3.

The model is a prediction about the kernel, so the next transcript runs the same three configurations on the kernel with the numeric identities. As in the section on ownership, it was run as root with setpriv and is not part of the code lab. Standard input was not a terminal; with a terminal, rm asks before it removes a write-protected file, and the removals of write-protected files in this transcript would wait for an answer. The program probe asks the kernel through test -r and test -w as each identity, and stat prints the mode, the owner and the group of the new files as owner:group numbers.

exec 2>&1
# Run as root. Identities are numeric, so no account is created.
# alice 2001:2001, bob 2002:2002, carol 2003:2003, group hpsc 2100.
as() { u=$1; shift; case $u in
  alice) setpriv --reuid=2001 --regid=2001 --groups=2001,2100 -- "$@";;
  bob)   setpriv --reuid=2002 --regid=2002 --groups=2002,2100 -- "$@";;
  carol) setpriv --reuid=2003 --regid=2003 --groups=2003      -- "$@";;
esac; }
probe() { # file: report r and w for each user, as the kernel decides
  out=
  for u in alice bob carol; do
    r=-; w=-
    as $u test -r "$1" && r=r
    as $u test -w "$1" && w=w
    out="$out$u $r$w  "
  done; echo "${out%  }"; }
d=$(mktemp -d /tmp/hpsc-root-demo.XXXX); chmod 755 $d; cd $d
mkdir proj; chown 2001:2100 proj; chmod 770 proj
stat -c '%A %u:%g %n' proj
echo "--- umask 022, directory 0770"
as bob sh -c 'umask 022; echo run1 > proj/r1.dat'
stat -c '%A %u:%g %n' proj/r1.dat; probe proj/r1.dat
echo "--- umask 002, directory 0770"
as bob sh -c 'umask 002; echo run2 > proj/r2.dat'
stat -c '%A %u:%g %n' proj/r2.dat; probe proj/r2.dat
echo "--- umask 002, directory 2770 (setgid)"
chmod 2770 proj
stat -c '%A %u:%g %n' proj
as bob sh -c 'umask 002; echo run3 > proj/r3.dat; mkdir proj/sub'
stat -c '%A %u:%g %n' proj/r3.dat proj/sub; probe proj/r3.dat
echo "--- who may remove bob's file r3.dat? (needs w and x on proj, not on the file)"
chmod 000 proj/r3.dat
as alice rm proj/r3.dat; echo "alice rm exit status $?"; ls proj
echo "--- carol and the directory"
as carol ls proj; as carol test -x proj; echo "carol test -x proj exit status $?"
cd /; rm -rf $d
drwxrwx--- 2001:2100 proj
--- umask 022, directory 0770
-rw-r--r-- 2002:2002 proj/r1.dat
alice r-  bob rw  carol --
--- umask 002, directory 0770
-rw-rw-r-- 2002:2002 proj/r2.dat
alice r-  bob rw  carol --
--- umask 002, directory 2770 (setgid)
drwxrws--- 2001:2100 proj
-rw-rw-r-- 2002:2100 proj/r3.dat
drwxrwsr-x 2002:2100 proj/sub
alice rw  bob rw  carol --
--- who may remove bob's file r3.dat? (needs w and x on proj, not on the file)
alice rm exit status 0
r1.dat
r2.dat
sub
--- carol and the directory
ls: cannot open directory 'proj': Permission denied
carol test -x proj exit status 1

The three result lines of the kernel, alice r- bob rw carol --, alice r- bob rw carol -- and alice rw bob rw carol --, are the three lines of the model, so all 18 yes-or-no answers (three configurations, three users, two rights) agree, and the modes and groups of the files agree with the model's (-rw-r--r-- with group 2002, -rw-rw-r-- with group 2002, -rw-rw-r-- with group 2100). The subdirectory that Bob made in the last configuration has the mode drwxrwsr-x and the group 2100: the bit propagates, as the previous listing showed for the test account. Carol's attempt to list the directory ends with Permission denied, and the test of search permission exits with status 1. The line alice rm exit status 0 shows a consequence that the requirement did not exclude. Alice removed Bob's file, although its mode was 000 and she was not its owner, because removal needs write and search permission on proj, which her group digit gives her. Every member can therefore remove any other member's files. The sticky bit (mode 3770 for the directory) would allow only the owner of a file and Alice, as owner of the directory, to remove it. The exercises return to this choice.

The result is established for the model of nine permission bits, three classes selected first-match, and the setgid and mask rules, on one local file system, for three numeric identities. It is not established for a cluster. On a cluster the account and group tables come from a directory service, the home and project directories may sit on a network or parallel file system whose server also checks identities, and an ACL or a security label may allow or deny access beyond the mode bits. The chapter did not test any of these.

What the model establishes and what it does not

Table 3.3 lists the statements of the lectures that this chapter tested, with the status that the tests gave them.

Table 3.3. Statements of the lectures and their status in this chapter.
Statement of the lectureStatusEvidence in this chapter
whoami prints the user of the session.Confirmed; the number is what the kernel usesid prints name and number (session in the section on identity).
The lscpu screen identifies the processor.Read exactlyThe lscpu slide shows 11th Gen Intel(R) Core(TM) i5-1135G7, 1 CPU, hypervisor VMware (W1L3 7:15).
free -h reports 3.7G total, 1.1G used and 888M free.Confirmed from the slide (W1L3 9:15); the used column of the KiB table follows the older definitionfree_used.py.
A directory is a special file that holds references to files and directories.Confirmed; made exactA name-to-inode table; the name is not part of the file (Ritchie and Thompson 1974; names_inodes.py).
A name that starts with a dot is a hidden file.True of ls onlyThe kernel stores the name; ls, ls -A, ls -a differ (nav_session.sh).
The second column of ls -l is the number of links.Confirmed and derived(3.1); counts 28, 22 and 4 on the lecture's screen mean 26, 20 and 2 subdirectories; four counts checked by program.
The date column of ls -l.Made exact: modification timeCoreutils §10.1.2; the birth time is separate (files_session.sh).
cd, cd - and cd .. go to the home directory, the previous directory and the parent.Confirmednav_session.sh.
rmdir removes only an empty directory; rm -r removes a tree.ConfirmedENOTEMPTY, message of the W1L4 10:45 screen reproduced.
rm deletes permanently.Refinedrm removes a name; content survives other links and is usually recoverable (Coreutils §11.5); the mode of the directory decides, not the mode of the file.
mv renames and moves.Confirmed; two casesInside one file system the inode is kept; across file systems rename fails with EXDEV and mv copies and removes (Coreutils §11.4).
The slide labels > as redirection within the cat command.Made exactThe shell truncates the target before cat starts; cat file2 file3 > file2 loses the data of file2 (redirect_session.sh).
> overwrites and >> appends.Confirmed4, 4 and 8 lines.
cp -r copies a directory recursively.Confirmed; second run nestsproject2/simpleproject (files_session.sh).
Execute permission on a directory lets a user traverse it.Confirmed; read and search are independentowner_class.sh.
Users fall into three categories: owner, group, others.Confirmed; the rule is first match, not union(3.2); mode 070 and 007 lock the owner out; kernel agrees on 1536 of 1536 owner-class requests; the group-before-other order is not tested on the kernel.
The mode string has ten characters.Confirmed4096 of 4096 values decoded as GNU stat does; the dot after the ten characters is a security-context marker, + an ACL.
Weights 4, 2, 1; modes 755, 644, 777.Confirmed(3.4), a bijection onto 0 to 7.
chmod +x without a who gives execute to all classes.ConditionalAll under the mask 002 (W1L4 39:45); owner and group only under 027 (24,576 cases agree with (3.5)).
The user who creates a file is its owner by default.Confirmed; the group depends on the directoryEffective user; effective group unless the directory has set-group-ID (group_session.sh).
chown owner:group file changes ownership.RefinedOnly a privileged process may change the owner; the lecture's prompt is # (W1L4 44:45); an owner may change the group to one of the owner's own groups (transcript).
New files have rw-rw-r-- and directories rwxrwxr-x; the slides give no cause.ExplainedFollows, under the conditions stated in the section on defaults, from the creation mask 002, the only mask that gives both (umask_check.py).

The model establishes six things. The files of a system are inodes, and a directory is a table of names, so removal, renaming and linking are edits of tables, and the link count of a directory is two plus its subdirectories (3.1). Access is decided by one class, selected first-match, and not by the union of the classes (3.2, 3.3); the kernel agreed on every request tested: exhaustively for the owner class, and on a sample of system files without an ACL. The order between the group class and the other class (for example, a group member refused under mode 0604) was not tested against the kernel. The mode string and the octal digits are two codings of the same 12 bits (3.4). The symbolic chmod is a set of bit operations whose mask depends on the creation mask (3.5). The creation law (3.6) explains the default modes of new files, and, under the conditions stated in the section on defaults, the lecture's shell used the mask 002. The running case needs a directory of mode 2770 and the mask 002.

The model leaves out most of what a production system adds. It has no access control lists, and the journal files of this machine show that the nine bits are not the whole rule there. It has no security labels (the dot after the mode string), no file attributes such as the immutable flag (named in unlink(2)), and no capabilities beyond the single notion of a privileged process. It has no network or parallel file system, where a server decides and identities must agree between machines. It has no time dimension: a program that tests test -w file and then writes can find the answer changed between the two steps. It gives no performance information, although the cost of mv across file systems, which scales with the data, matters for large results. Three limits of the evidence are listed again for the reader who wants to repeat the chapter. The transcripts with three numeric identities need root. The checks against the system files depend on the files of the machine that ran them. The lecture's screens come from a virtual machine that is not available, so statements about its umask, its security module and its free are inferences from the screens.

Exercises

Exercise 1 (derive). (a) Prove that (3.4) maps the 8 subsets of \(\{r,w,x\}\) one-to-one onto \(\{0,\dots,7\}\). (b) Count the nine-bit modes in which the owner class and the group class both hold read and write and the other class holds nothing, and write them in octal. (c) Count the nine-bit modes in which at least one class holds write permission without read permission.

Solution

(a) Suppose two different subsets have the same sum. Remove the elements they share. The remaining sets are disjoint and not both empty, with the same sum. Let the larger of all the remaining weights be \(a\), in one of the sets. The other set holds only weights smaller than \(a\), and the sum of all weights smaller than \(a\) is less than \(a\) (4 is greater than 2+1, and 2 is greater than 1). The sums differ, which contradicts the assumption, so the map is injective. It maps 8 subsets into the 8 values 0 to 7, so it is also onto. (b) The owner digit must be 6 or 7 (read and write granted, execute free), the group digit likewise, and the other digit must be 0, so there are \(2\times2\times1=4\) modes: 0660, 0670, 0760 and 0770. (c) A class has write without read when its digit is 2 or 3, which is 2 of the 8 digits. Each class has 6 digits that are not of that kind, so \(6^3=216\) modes have no such class, and \(512-216=296\) have at least one.

Exercise 2 (compute). (a) Write the four-digit octal mode of each string: -rwsr-xr-x, drwxrws---, -rw-r-Sr--, drwxrwxrwt. (b) A user has the mask 037. Compute the mode of a new file that a tool creates with the request 0666 and of a new directory created with the request 0777. (c) On the lecture's ls -la screen, the entry . has link count 22, the entry .. (the directory /home) has 4, and anaconda3 has 28. How many subdirectories does each of these directories have?

Solution

(a) -rwsr-xr-x: the set-user-ID bit is set and the owner execute bit is set (lowercase s), so the special digit is 4; the digits are 7 (rwx), 5 (r-x), 5 (r-x): 4755. drwxrws---: set-group-ID with group execute, special digit 2; owner 7, group 7, other 0: 2770. -rw-r-Sr--: the capital S in the group position means set-group-ID without group execute; special digit 2; owner rw- is 6, group r-S is 4 (read only), other r-- is 4: 2644. drwxrwxrwt: sticky with other execute, special digit 1; 777: 1777. (b) \(\lnot037=511-31=480=0740\) in octal. The file: \(0666\wedge0740=0640\). The directory: \(0777\wedge0740=0740\). (c) By (3.1), \(s=n-2\): the home directory has 20 subdirectories, /home has 2, and anaconda3 has 26. The first count includes hidden directories such as .cache and .config, which the screen shows.

Exercise 3 (counterexample). A student claims: "(i) A user who is a member of a file's group can always use the group's rights on it. (ii) To remove a file, a user needs write permission on the file." Give one counterexample to each claim, using the results of this chapter.

Solution

(i) Let the user own a file of mode 070, and let the file's group be the user's primary group. The user is a member of the group, and the group digit is 7. By (3.2), the owner class is selected first, because \(u_q=u_f\); the owner digit is 0, so (3.3) refuses every access. The listing owner_class.sh shows cat failing with Permission denied for the modes 070 and 007. The claim holds only for a user who is not the owner. (ii) Removal changes the directory's table, not the file. The listing files_session.sh removed a file of mode 000 with rm and refused to remove a file of mode 666 in a directory without write permission. With the sticky bit on the directory, even write permission on the directory is not enough: the transcript shows Operation not permitted for Bob in a 1777 directory.

Exercise 4 (diagnose an assumption). A student wants the members of her group to edit the result files in a directory. The files have mode 444. Her shell has the mask 022. She runs chmod +w *.dat, and the group still cannot write. (a) Predict the mode of the files after the command. (b) State the assumption that fails. (c) Give a command that gives write permission to the group and to nobody else, and compute the mode it produces; then say why chmod a+w is a wrong repair.

Solution

(a) The clause +w has no who, so \(A=\lnot022=0755\) and \(\sigma_{\{u,g,o\}}(2)=0222\). The bits that change are \(0222\wedge0755=0200\), the owner's write bit only. The mode is \(0444\vee0200=0644\); the model listing prints this case: 444 +w umask 022 -> 644. (b) The assumption that fails is that a clause without who acts on all classes. By the POSIX rule, bits of the creation mask are excluded, and the mask 022 contains the write bits of the group and the other class. (c) chmod g+w *.dat has a who, so \(A=511\) and the change is \(0020\): \(0444\vee0020=0464\). The command chmod ug+w would also let the owner write and give 0664. The repair chmod a+w gives \(0444\vee0222=0666\) (the model listing prints 444 a+w umask 022 -> 666): the other class also gets write permission, which violates R2 when the directory's other digit is not 0 and is excessive in any case.

Exercise 5 (design judgment). A principal investigator (PI) owns a directory for the results of a project. The students of the project, in the group lab, must create and edit files. External collaborators, in a group ext, must read the files and must not change them. Nobody else may read them. (a) Show that no assignment of owner, group and nine mode bits to one file meets all four requirements. (b) Propose a design that meets them for the students and the PI with mode bits only, and say what is given up. (c) Say what the sticky bit would add to the design and what it would cost.

Solution

(a) The rights over one file are given by three digits that apply to three sets of users: the owner, the members of one group (the file's group), and everybody else. The requirement gives three different rights to three sets that each contain users other than the owner: the students (read and write), the collaborators (read only) and the rest (none). Apart from the single owner, a file distinguishes only two sets: the members of its one group, and everyone else. So at most two of these three sets can receive different rights. If the file's group is lab, the collaborators fall into the other class, which must then be 0 to keep out the rest, so they get nothing. If the file's group is ext, the students fall into the other class. The owner class can hold the PI only, because it applies to one user ID. Hence the four requirements cannot all hold with mode bits alone. (b) Make the PI the owner of the directory, the group lab, mode 2770 (or 3770), make the PI a member of lab (otherwise the PI falls into the other class for files that students create, and the other digit is 0), and have every member work with the mask 002; then the PI and the students meet R1 to R3 of the running case, as the case section showed, and the collaborators have no access. What is given up is the collaborators' read access. Two ways to restore it lie outside the model of this chapter and were not tested here: an access control list entry for the group ext, which POSIX XBD 4.7 allows as an alternate mechanism and which the journal files of this machine suggest the kernel honours (the listing did not decode the ACL), or a second directory, owned by the PI, with the group ext and mode 2750, filled with copies that the PI makes. The copies are new inodes (the listing files_session.sh), so they fall out of date, and the ACL is not visible in the mode string except as a +. (c) With the bit (mode 3770), only the owner of a file, the PI (as owner of the directory) and a privileged process can remove or rename a file, so one student cannot remove another's files; the case-section transcript showed the opposite without the bit. The cost is that a student cannot remove or replace a file that a colleague created, even one that the student needs to rebuild, and the PI becomes the only person who can clear space.

Exercise 6 (implementation and measurement). Write a C function mode_string(mode_t m, char out[11]) that returns the ten characters of ls -l for a st_mode value, including the special bits. Write a test that creates seven files, sets the modes 0644, 0755, 04755, 02770, 01777, 03000 and 0070 with chmod, and compares the function's result with stat -c %A. Run it. Explain the output for 03000.

Solution

The function reads the three class digits with a shift and replaces the execute character by s, S, t or T when the matching special bit is set, as the Coreutils manual specifies. The program calls stat -c %A on each file through popen and compares the two strings.

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

/* The ten characters of ls -l for a st_mode value. */
static void mode_string(mode_t m, char out[11])
{
    const char *kind = "?";
    switch (m & S_IFMT) {
    case S_IFREG: kind = "-"; break;
    case S_IFDIR: kind = "d"; break;
    case S_IFLNK: kind = "l"; break;
    case S_IFIFO: kind = "p"; break;
    case S_IFCHR: kind = "c"; break;
    case S_IFBLK: kind = "b"; break;
    case S_IFSOCK: kind = "s"; break;
    }
    out[0] = kind[0];
    static const mode_t special[3] = { S_ISUID, S_ISGID, S_ISVTX };
    static const char on[3] = { 's', 's', 't' }, off[3] = { 'S', 'S', 'T' };
    for (int c = 0; c < 3; c++) {
        int bits = (m >> (6 - 3 * c)) & 7;
        out[1 + 3 * c] = bits & 4 ? 'r' : '-';
        out[2 + 3 * c] = bits & 2 ? 'w' : '-';
        out[3 + 3 * c] = (m & special[c]) ? (bits & 1 ? on[c] : off[c]) : (bits & 1 ? 'x' : '-');
    }
    out[10] = '\0';
}

int main(void)
{
    char dir[] = "/tmp/hpsc-modestr-XXXXXX";
    if (!mkdtemp(dir)) return 1;
    if (chdir(dir)) return 1;
    const mode_t modes[] = { 0644, 0755, 04755, 02770, 01777, 03000, 0070 };
    int bad = 0;
    for (size_t i = 0; i < sizeof modes / sizeof modes[0]; i++) {
        char name[16], text[11], cmd[64], line[64];
        snprintf(name, sizeof name, "f%04o", (unsigned)modes[i]);
        close(creat(name, 0600));
        chmod(name, modes[i]);
        struct stat st;
        stat(name, &st);
        mode_string(st.st_mode, text);
        snprintf(cmd, sizeof cmd, "stat -c %%A %s", name);
        FILE *p = popen(cmd, "r");
        if (!p || !fgets(line, sizeof line, p)) return 1;
        pclose(p);
        line[strcspn(line, "\n")] = '\0';
        bad += strcmp(text, line) != 0;
        printf("%04o %s %s\n", (unsigned)(st.st_mode & 07777), text, strcmp(text, line) ? "DIFFERS" : "same as stat -c %A");
    }
    printf("disagreements: %d\n", bad);
    char cmd[64];
    snprintf(cmd, sizeof cmd, "rm -rf %s", dir);
    return system(cmd) != 0 || bad;
}
0644 -rw-r--r-- same as stat -c %A
0755 -rwxr-xr-x same as stat -c %A
4755 -rwsr-xr-x same as stat -c %A
2770 -rwxrws--- same as stat -c %A
1777 -rwxrwxrwt same as stat -c %A
3000 ------S--T same as stat -c %A
0070 ----rwx--- same as stat -c %A
disagreements: 0

All seven strings equal GNU stat's, so the disagreement count is 0. The mode 03000 has the special bits for set-group-ID and sticky and no permission bits. The group position has no execute bit, so the set-group-ID bit appears as capital S, and the other position has no execute bit, so the sticky bit appears as capital T: ------S--T. The mode 0070 shows that the group digit alone is set and the owner digit is 0, the situation of the section on permissions in which the owner is locked out. The agreement of seven strings is a smaller test than the 4096 values of the Python decoder, and it does not test the file types other than regular files.

Primary sources

  • Recordings: W1L3, Mastering Linux Directory and File Management (slides: contents, whoami, uname -a, lscpu, free, files and directories, pwd, ls -la, cd and its variants, mkdir, mv); W1L4, Understanding File Permissions and Ownership in Linux (slides: touch, text editors, rm, cat, cp, authorization, the ten-character structure, chmod symbolic and numeric, chown), NPTEL High Performance Scientific Computing, IIT Bombay, 2025.
  • D. M. Ritchie and K. Thompson, "The UNIX Time-Sharing System", Communications of the ACM 17(7), July 1974, pp. 365 to 375 (electronic version recreated by E. A. Brewer). §3.2, p. 366: a file does not exist within a directory; each directory has at least two entries; §3.2, p. 367: no arbitrary links to directories; §3.4, p. 367: no link between file system hierarchies; §3.5, p. 367: seven protection bits and the set-user-ID bit; §4, pp. 368 to 369: the i-node fields, link creation and removal, link count.
  • The Open Group, POSIX.1-2024 (Issue 8), Base Definitions, chapter 4 "General Concepts": §4.5 Directory Protection (sticky-bit deletion conditions); §4.7 File Access Permissions (class selection, privileged processes, additional and alternate mechanisms); §4.8 File Hierarchy (the hierarchy is a directed graph).
  • The Open Group, POSIX.1-2024 chmod: who may change a mode; the symbolic-mode grammar and the exception for bits of the creation mask when the who is omitted. POSIX.1-2024 umask: the file mode creation mask. sys/stat.h (read for the names of the mode bits).
  • Linux man-pages project (pages as fetched on 2026-10-26 from man7.org): inode(7) (fields, mode bit values, owner of a new file, sticky-bit meaning); path_resolution(7) (resolution steps, the three-class selection rule); credentials(7) (effective and supplementary IDs); open(2) (O_TRUNC, O_APPEND, group of a new file, mode and umask); mkdir(2); rmdir(2); link(2); unlink(2); rename(2); symlink(7) (permissions of a symbolic link); chmod(2); chown(2) (who may change owner and group); free(1) (the definition of used); login.defs(5) (UMASK, the initial file mode creation mask used by pam_umask); the comment on USERGROUPS_ENAB in /etc/login.defs of Ubuntu 24.04 (shadow 4.13) on the verification machine (022 becomes 002 for private user groups); pam_umask(8) (option usergroups: for a user who is not root and whose name equals the primary group name, the group bits of the mask are set equal to the owner bits).
  • Filesystem Hierarchy Standard, Version 3.0, The Linux Foundation, 19 March 2015 (read from the PDF; the HTML edition was not served to the fetching client). Chapter 2: shareable and static files; §3.8 /home; §3.18 /tmp; §5.15 /var/tmp.
  • GNU Coreutils 9.4 manual (read from the copy installed with the verification system, info coreutils; the web edition at gnu.org refused the fetching client): §10.1.2 What information is listed (file types, s, S, t, T, the . and + markers, modification time); §11.4 mv (copy and remove across file systems); §11.5 rm (prompting, recovery warning); §27.1 Structure of File Mode Bits; §27.2.6 The Umask and Protection; §27.5 Directories and the Set-User-ID and Set-Group-ID Bits.