OS Lab 6 - Users. Permissions

De la WikiLabs
Jump to navigationJump to search

In the last lab, we gave one process several threads. In this lab, we will look at how the kernel decides what a task is allowed to do. It compares the numbers of the task, its credentials, with the owner, group and mode of the file. We will read these numbers, turn them into names, and change the permissions of our own files. As an application, we will write our own ls -l, and a tool which finds and repairs dangerous permissions.

Note: Everything in this lab works as a normal user, and you will not need sudo. Some of the commands below fail with a permission error on purpose.

Learning objectives

By the end of this lab you should be able to:

  1. read the user and group IDs of a task, and turn them into names;
  2. read the owner, group and mode of a file with statx(2), and change the mode with chmod(2);
  3. say which class of permission bits applies when a task uses a file;
  4. explain what read, write and execute mean for files and for directories;
  5. recognize the set-user-ID, set-group-ID and sticky bits.

The credentials of a task

Every task has a set of numbers, which the kernel uses for every permission check. First, it has three user IDs:

ID Meaning
Real UID Who started the task
Effective UID Whose rights the task uses
Saved set-user-ID One more value the task can go back to

It also has a real GID, an effective GID and a saved set-group-ID, which work in the same way, and a list of supplementary groups. All of them are copied to the child by fork(2), and kept across exec, so everything started from a shell normally has the same credentials as the shell.

getresuid(2) and getresgid(2)

// In: <unistd.h>
int getresuid(uid_t *ruid, uid_t *euid, uid_t *suid);
int getresgid(gid_t *rgid, gid_t *egid, gid_t *sgid);

uid_t getuid(void);  // The real UID
uid_t geteuid(void); // The effective UID
gid_t getgid(void);  // The real GID
gid_t getegid(void); // The effective GID

getresuid(2) and getresgid(2) store the three IDs at the given addresses. The four shorter functions return a single ID, and cannot fail.

getgroups(2)

// In: <unistd.h>
int getgroups(int size, gid_t list[]);
Parameter Description
size How many elements list can hold
list Where to store the supplementary groups

It returns the number of groups, or -1 on error. If size is 0, list is not touched, and the call only returns the number of groups. This lets us first ask how many groups there are, and then declare an array of exactly that size: a variable-length array (VLA), whose size is only known at run time.

Note: A VLA must have at least one element. A task may have no supplementary groups at all, so the programs below make room for one more group than needed.

The following program prints all the credentials of the task running it:

 1#include <errno.h>
 2#include <stdio.h>
 3#include <stdlib.h>
 4#include <string.h>
 5#include <unistd.h>
 6
 7static void print_error(const char *what)
 8{
 9    char msg[256];
10    snprintf(msg, sizeof msg, "%s: %s\n", what, strerror(errno));
11    write(STDERR_FILENO, msg, strlen(msg));
12}
13
14int main(void)
15{
16    uid_t ruid, euid, suid;
17    gid_t rgid, egid, sgid;
18
19    getresuid(&ruid, &euid, &suid);
20    getresgid(&rgid, &egid, &sgid);
21
22    int n = getgroups(0, nullptr);
23    if (n == -1) {
24        print_error("getgroups");
25        return EXIT_FAILURE;
26    }
27    gid_t groups[n + 1]; // + 1: a VLA cannot be empty
28    n = getgroups(n, groups);
29    if (n == -1) {
30        print_error("getgroups");
31        return EXIT_FAILURE;
32    }
33
34    char msg[256];
35    snprintf(msg, sizeof msg, "UID: real %u, effective %u, saved %u\n", ruid, euid, suid);
36    write(STDOUT_FILENO, msg, strlen(msg));
37    snprintf(msg, sizeof msg, "GID: real %u, effective %u, saved %u\n", rgid, egid, sgid);
38    write(STDOUT_FILENO, msg, strlen(msg));
39
40    write(STDOUT_FILENO, "Groups:", 7);
41    for (int i = 0; i < n; i++) {
42        snprintf(msg, sizeof msg, " %u", groups[i]);
43        write(STDOUT_FILENO, msg, strlen(msg));
44    }
45    write(STDOUT_FILENO, "\n", 1);
46    return EXIT_SUCCESS;
47}
$ ./build/debug/app
UID: real 1000, effective 1000, saved 1000
GID: real 1000, effective 1000, saved 1000
Groups: 1000

Your numbers may be different. The three IDs are the same for almost every task, and only differ while special programs run, as we will see below.

From numbers to names

The kernel does not know what users are called. Names are looked up in user space, by the C library:

// In: <pwd.h>
struct passwd *getpwuid(uid_t uid);

// In: <grp.h>
struct group *getgrgid(gid_t gid);

They return a structure describing the user or group, or nullptr if nothing is known about that number. For example, a file can still belong to a user who has been deleted. A program must be ready to print the number instead.

// In: <pwd.h> (simplified)
struct passwd {
    char *pw_name;  // The name of the user
    uid_t pw_uid;   // Its UID
    gid_t pw_gid;   // Its primary group
    char *pw_dir;   // Its home directory
    char *pw_shell; // Its shell
};

// In: <grp.h> (simplified)
struct group {
    char *gr_name; // The name of the group
    gid_t gr_gid;  // Its GID
};
Note: The returned structure is kept in a static buffer, which the next call to the same function overwrites. If you need a name after another lookup, copy it first.

The following program prints the same line as the id command:

 1#include <errno.h>
 2#include <grp.h>
 3#include <pwd.h>
 4#include <stdio.h>
 5#include <stdlib.h>
 6#include <string.h>
 7#include <unistd.h>
 8
 9static void print_error(const char *what)
10{
11    char msg[256];
12    snprintf(msg, sizeof msg, "%s: %s\n", what, strerror(errno));
13    write(STDERR_FILENO, msg, strlen(msg));
14}
15
16static void print_group(gid_t gid)
17{
18    const struct group *gr = getgrgid(gid);
19    char msg[256];
20    snprintf(msg, sizeof msg, "%u(%s)", gid, gr != nullptr ? gr->gr_name : "?");
21    write(STDOUT_FILENO, msg, strlen(msg));
22}
23
24int main(void)
25{
26    int n = getgroups(0, nullptr);
27    if (n == -1) {
28        print_error("getgroups");
29        return EXIT_FAILURE;
30    }
31    gid_t groups[n + 1]; // + 1: a VLA cannot be empty
32    n = getgroups(n, groups);
33    if (n == -1) {
34        print_error("getgroups");
35        return EXIT_FAILURE;
36    }
37
38    uid_t uid = getuid();
39    const struct passwd *pw = getpwuid(uid);
40
41    char msg[256];
42    snprintf(msg, sizeof msg, "uid=%u(%s) gid=", uid, pw != nullptr ? pw->pw_name : "?");
43    write(STDOUT_FILENO, msg, strlen(msg));
44    print_group(getgid());
45
46    write(STDOUT_FILENO, " groups=", 8);
47    for (int i = 0; i < n; i++) {
48        if (i > 0) {
49            write(STDOUT_FILENO, ",", 1);
50        }
51        print_group(groups[i]);
52    }
53    write(STDOUT_FILENO, "\n", 1);
54    return EXIT_SUCCESS;
55}
$ ./build/debug/app
uid=1000(john) gid=1000(john) groups=1000(john)
$ id
uid=1000(john) gid=1000(john) groups=1000(john)

The permissions of a file

Every file has an owner (a UID), a group (a GID) and a mode of 16 bits. The mode holds the type of the file, 3 special bits, and 9 permission bits: read, write and execute, for each of the three classes.

The owner, group and mode of a file. The mode is usually written in octal: each digit is one group of 3 bits. The file here is rwxr-x---, or 0750.

When a task uses a file, the kernel picks exactly one class, and only checks that one:

The first match decides. An owner with --- cannot read the file, even if the others can.

The checks use the effective IDs. A task with effective UID 0, the superuser, skips them. In addition, every directory on the path needs the execute permission.

The mode bits have names, defined in <sys/stat.h>:

Class Read Write Execute
Owner S_IRUSR (0400) S_IWUSR (0200) S_IXUSR (0100)
Group S_IRGRP (0040) S_IWGRP (0020) S_IXGRP (0010)
Others S_IROTH (0004) S_IWOTH (0002) S_IXOTH (0001)

The three special bits are:

Bit Value Shown as Meaning
Set-user-ID, S_ISUID 04000 s instead of the owner's x The program runs with the effective UID of the owner of the file
Set-group-ID, S_ISGID 02000 s instead of the group's x The program runs with the effective GID of the group of the file. On a directory: new files get the group of the directory
Sticky, S_ISVTX 01000 t instead of the others' x On a directory: an entry can only be deleted or renamed by its owner, the owner of the directory, or root

If the x underneath is not set, the letter is uppercase: S or T.

statx(2)

// In: <fcntl.h> and <sys/stat.h>
int statx(int dirfd, const char *pathname, int flags, unsigned int mask, struct statx *statxbuf);
Parameter Description
dirfd A directory to which a relative pathname is relative, or AT_FDCWD for the current directory
pathname The file
flags 0 to follow a symbolic link and describe its target, or AT_SYMLINK_NOFOLLOW to describe the link itself
mask Which fields we want. STATX_BASIC_STATS asks for the usual ones
statxbuf Where to store the result
// In: <sys/stat.h> (simplified)
struct statx {
    uint16_t stx_mode;  // The type, special bits and permission bits
    uint32_t stx_uid;   // The owner
    uint32_t stx_gid;   // The group
    uint32_t stx_nlink; // The number of hard links
    uint64_t stx_size;  // The size in bytes
    struct statx_timestamp stx_atime; // Last access
    struct statx_timestamp stx_mtime; // Last modification
};

struct statx_timestamp {
   int64_t tv_sec;    /* Seconds since the Epoch (00:00 1st Jan 1970 UTC) */
   uint32_t tv_nsec;  /* Nanoseconds since tv_sec */
};

The type is tested with macros: S_ISREG(mode) (regular file), S_ISDIR (directory), S_ISLNK (symbolic link), S_ISCHR (character device), S_ISBLK (block device), S_ISFIFO (pipe) and S_ISSOCK (socket).

statx(2) only needs the execute permission on the directories of the path, and no permission on the file itself. The following program describes any file we can reach:

 1#include <errno.h>
 2#include <fcntl.h>
 3#include <grp.h>
 4#include <pwd.h>
 5#include <stdio.h>
 6#include <stdlib.h>
 7#include <string.h>
 8#include <sys/stat.h>
 9#include <unistd.h>
10
11static void print_error(const char *what)
12{
13    char msg[256];
14    snprintf(msg, sizeof msg, "%s: %s\n", what, strerror(errno));
15    write(STDERR_FILENO, msg, strlen(msg));
16}
17
18int main(int argc, char *argv[])
19{
20    if (argc != 2) {
21        const char *usage = "usage: app <path>\n";
22        write(STDERR_FILENO, usage, strlen(usage));
23        return EXIT_FAILURE;
24    }
25
26    struct statx st;
27    if (statx(AT_FDCWD, argv[1], 0, STATX_BASIC_STATS, &st) == -1) {
28        print_error(argv[1]);
29        return EXIT_FAILURE;
30    }
31
32    const char *type = "other";
33    if (S_ISREG(st.stx_mode)) {
34        type = "regular file";
35    } else if (S_ISDIR(st.stx_mode)) {
36        type = "directory";
37    }
38
39    const struct passwd *pw = getpwuid(st.stx_uid);
40    const struct group *gr = getgrgid(st.stx_gid);
41
42    char msg[512];
43    snprintf(msg, sizeof msg, "%s, owner %u (%s), group %u (%s), mode %04o\n", type,
44             st.stx_uid, pw != nullptr ? pw->pw_name : "?",
45             st.stx_gid, gr != nullptr ? gr->gr_name : "?",
46             st.stx_mode & 07777);
47    write(STDOUT_FILENO, msg, strlen(msg));
48    return EXIT_SUCCESS;
49}
$ ./build/debug/app /etc/shadow
regular file, owner 0 (root), group 42 (shadow), mode 0640
$ ./build/debug/app /tmp
directory, owner 0 (root), group 0 (root), mode 1777
$ ./build/debug/app /usr/bin/passwd
regular file, owner 0 (root), group 0 (root), mode 4755
$ ./build/debug/app /nope
/nope: No such file or directory

stx_mode also holds the type of the file, and & 07777 keeps only the special and permission bits. In the examples above:

  • /etc/shadow holds the password hashes, and the others class has no permissions at all.
  • /tmp can be written by anyone, but the sticky bit (the leading 1) stops users from deleting each other's files.
  • /usr/bin/passwd is set-user-ID (the leading 4) and belongs to root. This is how a normal user can change their own password: the program runs with effective UID 0, and it knows who started it from the real UID.

chmod(2)

// In: <sys/stat.h>
int chmod(const char *pathname, mode_t mode);

chmod(2) replaces the permission and special bits of a file. Only the owner of the file, or root, may call it. The permission bits themselves do not matter: an owner with --- can still give itself rw-. Since it replaces all the bits, changing just some of them means starting from the old mode, which statx(2) gives us. For example, to remove the write permission of the group and of the others:

mode_t new_mode = (st.stx_mode & 07777) & ~(S_IWGRP | S_IWOTH);
chmod(path, new_mode);
Note: chmod(2) follows symbolic links: called on a link, it changes the file the link points to.

Trying it out

The chmod command takes the same octal numbers. Try the following on your own files:

$ echo hello > notes.txt
$ chmod 044 notes.txt
$ cat notes.txt
cat: notes.txt: Permission denied
$ chmod 644 notes.txt
$ cat notes.txt
hello

With mode 044, everybody else can read the file, but its owner cannot: only the owner class is checked. On a directory, the three permissions mean something else:

Permission For a directory
Read List the names
Write Create, delete and rename entries
Execute Go through it, to use the names inside
$ mkdir box
$ echo hi > box/a.txt
$ chmod 600 box
$ ls box
a.txt
$ cat box/a.txt
cat: box/a.txt: Permission denied
$ chmod 300 box
$ ls box
ls: cannot open directory 'box': Permission denied
$ cat box/a.txt
hi
$ chmod 500 box
$ touch box/b.txt
touch: cannot touch 'box/b.txt': Permission denied
$ rm box/a.txt
rm: cannot remove 'box/a.txt': Permission denied
$ chmod 700 box

Without execute, we can see the names, but we cannot use them. Without read, we can use a name, if we already know it. Deleting a file needs write permission on its directory. The permissions of the file itself are not checked. Depending on its version, ls box may also complain about each name in the first case.

Exercises

Write each program in its own copy of the project template. As a reminder from the first lab:

mkdir -p ~/oslab/lab06 && cd ~/oslab/lab06
curl -OL https://wiki.dcae.pub.ro/images/9/93/Os_lab_skeleton.zip
unzip -d ls Os_lab_skeleton.zip
unzip -d doctor Os_lab_skeleton.zip

For this lab:

  • As in the last labs, do not use the standard I/O library for output: build the text with snprintf(3) and write(2) it.
  • Do not run other programs: make the system calls yourself.
  • If a file cannot be reached, print the error on standard error, and continue with the next one.

A. Your own ls -l

Write a command which lists a directory like ls -la. It takes the directory as an optional parameter, and uses the current directory if there is none. Build it in these steps:

  1. Names and sizes. Read the directory with readdir(3), as in Lab 3, and call statx(2) for each entry. Print the size and the name.
  2. The mode. Print the mode as 10 characters: the type (-, d, l, c, b, p or s), then the 9 permission bits. Write a function which fills in a string.
  3. The other columns. Add the number of hard links, the owner, the group and the date. If a number has no name, print the number. For a symbolic link, also print -> target.
  4. The special bits. Show s and S for the set-user-ID and set-group-ID bits, as described above. You can ignore the sticky bit.
  5. What can I do? Add a first column with your own rights over the file, such as r-x, by picking the class as the kernel does: compare with your effective UID, then with your effective GID and supplementary groups, or else use the others class.
$ ./build/debug/app
rwx drwxr-xr-x   1 john     john           44 2026-11-09 15:54 .
rwx drwxr-xr-x   1 john     john           50 2026-11-09 15:54 ..
rw- -rw-r-S---   1 john     john            6 2026-11-09 15:54 notes.txt
rwx lrwxrwxrwx   1 john     john            9 2026-11-09 15:54 link -> notes.txt
rwx drwxr-x--x   1 john     john            0 2026-11-09 15:54 sub
rwx -rwsr-xr-x   1 john     john        68064 2026-11-09 15:54 tool
$ ./build/debug/app /etc | grep shadow
--- -rw-r-----   1 root     shadow        840 2026-11-09 15:54 shadow

The names do not have to be sorted. The widths of the columns are up to you.

  • You do not need to build a path for each entry: give statx(2) the directory through dirfd(dir), which returns the file descriptor of an open DIR, and the name of the entry as pathname.
  • Use AT_SYMLINK_NOFOLLOW, so that links are listed as links.
  • readlinkat(dirfd(dir), name, buffer, size) reads the target of a link. It returns the length, and does not add a '\0'.
  • For the date: time_t when = st.stx_mtime.tv_sec;, then localtime_r(&when, &tm), and strftime(date, sizeof date, "%Y-%m-%d %H:%M", &tm), from <time.h>.
  • getpwuid(3) and getgrgid(3) use different buffers, but copy the names anyway if you want to look up anything else before printing them.

After step 4, compare your output with the real ls.

B. Permission doctor

Write a command which walks a directory and everything below it, and reports dangerous permissions. With --fix, it also repairs them.

./build/debug/app [--fix] <directory>

It checks only regular files and directories, including the starting directory, against three rules:

Applies to Problem Repair
Regular files and directories Writable by others, except for directories with the sticky bit Remove the write permission of the others
Regular files whose name ends in .key Any permission for the group or the others Remove all of them
Regular files Set-user-ID or set-group-ID Remove those bits

Create a test directory inside the project:

mkdir -p demo/sub demo/locked
echo hello > demo/notes.txt
echo hello > demo/shared.txt && chmod 666 demo/shared.txt
echo secret > demo/sub/server.key && chmod 644 demo/sub/server.key
cp /bin/true demo/sub/tool && chmod 4755 demo/sub/tool
chmod 777 demo/sub
ln -s /etc/shadow demo/link
chmod 000 demo/locked
$ ./build/debug/app demo
demo/sub: writable by others (0777)
demo/sub/server.key: private key open to group or others (0644)
demo/sub/tool: runs with the rights of its owner or group (4755)
demo/locked: Permission denied
demo/shared.txt: writable by others (0666)
problems found: 4, fixed: 0
$ ./build/debug/app --fix demo
demo/sub: writable by others (0777)
  fixed: 0777 -> 0775
demo/sub/server.key: private key open to group or others (0644)
  fixed: 0644 -> 0600
demo/sub/tool: runs with the rights of its owner or group (4755)
  fixed: 4755 -> 0755
demo/locked: Permission denied
demo/shared.txt: writable by others (0666)
  fixed: 0666 -> 0664
problems found: 4, fixed: 4
$ ./build/debug/app demo
demo/locked: Permission denied
problems found: 0, fixed: 0

The lines may come in a different order. The exit status is 0 if every problem found was fixed, and 1 otherwise.

  • Write a recursive function which takes the path of a directory. Build the path of each entry with snprintf(3), into a buffer of PATH_MAX bytes (from <limits.h>).
  • Skip . and .., or the walk will never end.
  • Use AT_SYMLINK_NOFOLLOW, and leave symbolic links alone.
  • Set errno to 0 before each call to readdir(3). The recursive calls change it.
  • A file can break several rules. Work out the new mode from all of them, and call chmod(2) once.
Note: The set-user-ID bit on demo/sub/tool is harmless here, because that program is a copy of true. Never set it on a shell or on anything which runs commands: whoever starts the program would get your rights.

Some things to look at:

  • Run ./build/debug/app --fix /usr/bin. Why does it fail for every file?
  • Why is /tmp not reported, even though everybody can write to it?
  • demo/link has mode 0777. What would --fix try to do if the program treated it like a regular file? Why would that fail here, and what would happen if the link pointed to a file of yours?