2.11.1. File Access Control

Contemporary operating systems permit more than one person to use a computer by providing a separate account for each person. The operating system protects the files associated with each account and allows or disallows other users to view or modify files that belong to other accounts. POSIX-compliant systems (like Unix, Linux, and macOS) define three kinds of file access: by the file owner (called the user), by a group, and by others (everyone else). A group consists of one or more accounts and has an intermediate level of access, which can allow more access than others while at the same time allowing less access than the user. Windows maintains this information in an Access Control List or ACL, but some file information is still available using the following technique. POSIX systems compress or encode file type and access information into a 16-bit integer to save disk space:

  1. 7-bits for the mode: one of the seven bits distinguishes between a regular file and a directory (1 for a directory and 0 for a regular file). The other six bits indicate other file attributes that we ignore for simplicity.
  2. 9-bits for file access control: controlled by three sets of three bits; in each set, 1 grants permission, and 0 disallows it. The three sets are named:
    1. User
    2. Group
    3. Others
  Binary Decimal Hexadecimal
Read 100 4 0x4
Write 010 2 0x2
Execute 001 1 0x1
Binary Decimal Permissions
000 0 none
001 1 execute
010 2 write
011 3 write + execute
100 4 read
101 5 read + execute
110 6 read + write
111 7 read + write + execute
(a)(b)
File access control encoded with bits.
  1. File permissions. For each kind of owner, the operating system encodes file permissions as a 3-bit pattern.
  2. File access permission combinations. With three bits, file systems can represent eight unique permission combinations, including no access.

Problem

Our task is to write a program that uncompresses or decodes the file type and the nine bits representing the read, write, and execute permissions for the user, group members, and others. Although there are many kinds of files, our solution must only distinguish between directories and "regular" files. For now, our program only needs to print the permissions as a decimal value whose meaning matches Figure 1(a).

Program Solution

For now, we must be content with just a few program fragments. We need to study structures and control statements before we can understand a complete version of the program. Until then, we focus on uncompressing the tightly encoded file access information. We begin with the encoded data already stored in the unsigned short integer named st_mode. The first part of the solution uses an If-statement, which we study in the next chapter, but operates as it would in a Java program.

unsigned short st_mode;
	. . .
//if (file_info.st_mode >> 15)          // alternate
if (S_IFREG & st_mode != 0)
	cout << "file"\n;
else
	cout << "directory\n";
cout << "Permissions:" << endl;

cout << "user " << ((st_mode >> 6) & 0x7) << endl;
cout << "group " << ((st_mode >> 3) & 0x7) << endl;
cout << "other " << (st_mode & 0x7 <<) endl;
 
 
(a)(b)
Bitwise operator examples.
  1. Determining if the file is a regular file or a directory.
    • The 16-bit unsigned integer st_mode contains information about a file. That information was previously recovered from the computer's file system by a system call not illustrated here.
    • S_IFREG is a symbolic constant, denoting a regular file, defined in a system header file. It consists of a 16-bit bit-mask with fifteen 0-bits and one 1-bit.
    • The bitwise-AND between the mask and the value stored in st_mode will produce a non-zero value only if the file described by st_mode is a directory.
  2. Displaying file permissions. The file permissions are displayed as decimal (i.e., base-10) numbers, which we interpret as illustrated in Figure 1.
    1. (st_mode >> n) shifts the bits in st_mode n places to the right. Recall that st_mode consists of three 3-bit groups. The left-most three bits encode the user's permissions, the three middle bits encode the group's permissions, and the right-most three bits encode the other's permissions (and are not shifted). The parentheses cause the bit-shift operation to take place first.
    2. (...) & 0x7 "masks out" all but the right-most three bits. 716 = 0...01112, so the bitwise-AND will always yield 0s in the first thirteen bit positions, and will "pass through" the last three bits of st_modes.

Caution

A short integer usually spans two bytes of memory, and the byte order depends on the underlying computer hardware. The solution presented here works on a little endian computer (e.g., Intel or AMD). It does not work on big endian hardware.

Bitwise Operations Illustrated

The concepts and the operators appearing in Figure 2 are likely unfamiliar to most readers, so we attempt to gain some insight by looking at a graphical representation of what occurs when each bit-shift statement runs. Notice that the figures below present the three output statements in the opposite order in which they appear in the program fragment.

An illustration of a 16-bit integer; the top 7-bits represent the file's mode bits (i.e., the file type), and the bottom nine bits represent the read, write, and execute bits for three different users. The 16-bit integer is shifted to the right by six bits and then is bitwise ANDed with 0000000000000111 (base 2). Together, the shift and logical-AND ensure that the top thirteen bits are 0 and the bottom three bits are the user's access permissions.
Decoding user's file permissions. Decoding the user's permissions is challenging, requiring two steps. First, we shift the bits in st_mode right 6-bit positions, which discards the bottom six bits and shifts in six zeros at the highest positions. Next, we must discard any remaining bits left in the file's mode, which we do with a bitwise-AND operation and a mask of 0x7 or 111 in binary. The result of both bitwise operations is in the range of 0-7.
An illustration of a 16-bit integer; the top 7 bits represent the file mode bits (i.e., the file type), and the bottom nine bits represent the read, write, and execute bits for three different users. The 16-bit integer is shifted to the right by three bits and then is bitwise ANDed with 0000000000000111 (base 2), which again makes the top thirteen bits 0 and leaves the bottom 3 bits unmodified.
Decoding the group's file permissions. Decoding the group's permissions is the same complexity as decoding the user's permissions: the only difference is that we shift right 3-bit positions instead of six. We shift right 3-bit positions, which discards the bottom three bits and shifts in three zeros at the highest positions. We follow this operation with a bitwise-AND with 0x7 to discard any bits from the modes, which leaves us with just the group's permissions.
The 16-bit integer is bitwise ANDed with 0000000000000111 (base 2), which makes the top thirteen bits 0 and leaves the bottom 3 bits unmodified.
Decoding other's file permissions. Decoding the permissions for everyone else on the computer is easy because the three bits are already the least significant (i.e., at the lowest bit position). All that is required is masking out the top thirteen bits. The resulting value, consisting of three bits, must be in the range of 0-7.

Please see stat.cpp for the complete program.