chmod, umask, access(), set-UID/GID and Sticky Bit in Linux Explained

 

chmod, umask, access(), set-UID/GID and Sticky Bit in Linux Explained
chmod, umask, access(), set-UID/GID and Sticky Bit in Linux Explained
12
Permission bits
4-step
Check algorithm
umask
Creation mask

The Linux Permission System

Linux uses a 9-bit permission model dividing file access into three categories (owner, group, other) and three operations (read, write, execute). Plus three special bits (setUID, setGID, sticky) — 12 bits total, stored in the low 12 bits of st_mode.

Understanding this system is fundamental to Linux system programming. Every time you call open(), read(), or exec(), the kernel runs a permission check algorithm.

Key Terms:

chmod() fchmod() umask() access() S_IRUSR/S_IWUSR/S_IXUSR set-UID bit set-GID bit sticky bit S_ISUID/S_ISGID/S_ISVTX effective UID

1. The 12 Permission Bits — Full Reference

How ls -l displays permissions:

rw-r-x–x ← type owner group other
Constant Octal Bit Meaning
S_ISUID 04000 set-UID On exec: process gets file owner’s UID
S_ISGID 02000 set-GID On exec: process gets file’s GID
S_ISVTX 01000 sticky Restricted delete on directory
S_IRUSR 0400 owner-r Owner can read
S_IWUSR 0200 owner-w Owner can write
S_IXUSR 0100 owner-x Owner can execute
S_IRGRP 040 group-r Group can read
S_IWGRP 020 group-w Group can write
S_IXGRP 010 group-x Group can execute
S_IROTH 04 other-r Others can read
S_IWOTH 02 other-w Others can write
S_IXOTH 01 other-x Others can execute

Directory permissions mean something different:

Permission On regular file On directory
Read (r) Can read file contents Can list filenames (ls)
Write (w) Can modify file contents Can create/delete files in directory
Execute (x) Can run as a program Can access files inside (search permission)
Key insight: To delete a file you only need write+execute on the parent directory — NOT any permission on the file itself. You can delete a file you can’t even read.

2. Permission-Checking Algorithm — 4 Steps

When you access a file, the kernel checks in this order and stops at the first match:

1
Privileged process? If yes → full access granted regardless of permissions.
2
Effective UID == file UID? If yes → apply owner permission bits.
3
Effective GID or supplementary GID == file GID? If yes → apply group permission bits.
4
Otherwise → apply other permission bits.
Tricky edge case: Checking stops at the first matching rule. If you are the file owner but the owner has NO permissions (—-r–r–), you are still denied even though group/other CAN access it. Being the owner is checked first, but the owner bits say “no”.

3. Code Examples

Example 1: chmod() — set file permissions

#include <stdio.h>
#include <sys/stat.h>
#include <stdlib.h>

int main(void) {
    /* Set read-only for everyone (0444) */
    if (chmod("myfile", S_IRUSR | S_IRGRP | S_IROTH) == -1) {
        perror("chmod");
        exit(1);
    }
    printf("Set to r--r--r-- (0444)\n");

    /* Set rwxr-xr-x (0755) — typical for executables */
    if (chmod("myfile", 0755) == -1) {
        perror("chmod");
        exit(1);
    }
    printf("Set to rwxr-xr-x (0755)\n");

    /* Set rw-r--r-- (0644) — typical for regular files */
    chmod("myfile", 0644);
    printf("Set to rw-r--r-- (0644)\n");

    return 0;
}

Example 2: fchmod() — change permissions via file descriptor

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

int main(void) {
    int fd = open("myfile", O_RDWR);
    if (fd == -1) { perror("open"); return 1; }

    /* fchmod avoids TOCTOU race — file identified by fd, not name */
    if (fchmod(fd, 0600) == -1)  /* rw------- */
        perror("fchmod");
    else
        printf("Permissions changed to 0600\n");

    close(fd);
    return 0;
}

Example 3: Modify specific bits (add or remove permissions)

#include <stdio.h>
#include <sys/stat.h>

int add_permission(const char *path, mode_t bit) {
    struct stat sb;
    if (stat(path, &sb) == -1) return -1;
    /* OR in the new bit — adds to existing perms */
    return chmod(path, sb.st_mode | bit);
}

int remove_permission(const char *path, mode_t bit) {
    struct stat sb;
    if (stat(path, &sb) == -1) return -1;
    /* AND with complement — removes the bit */
    return chmod(path, sb.st_mode & ~bit);
}

int main(void) {
    /* Add owner execute permission */
    add_permission("myscript.sh", S_IXUSR);
    printf("Added owner execute\n");

    /* Remove other read permission */
    remove_permission("private.txt", S_IROTH);
    printf("Removed other-read\n");

    /* Add write, remove other-read simultaneously */
    struct stat sb;
    stat("myfile", &sb);
    chmod("myfile", (sb.st_mode | S_IWUSR) & ~S_IROTH);

    return 0;
}

Example 4: umask() — the creation mask

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

int main(void) {
    mode_t old_mask;
    int fd;
    struct stat sb;

    /* Get current umask WITHOUT changing it: set to 0 and save old */
    old_mask = umask(0);
    umask(old_mask);  /* restore it */
    printf("Current umask: %04o\n", old_mask);

    /* Typical umask is 022 (----w--w-)
       This means: always turn OFF group-write and other-write
       So open("f", ..., 0666) gives 0644 (0666 & ~022 = 0644) */

    /* Demo: set a custom umask, create file, see result */
    umask(0033);  /* turn off group-write, other-write+execute */

    fd = open("testperm", O_CREAT | O_WRONLY, 0777);
    if (fd == -1) { perror("open"); return 1; }
    close(fd);

    stat("testperm", &sb);
    printf("Requested: 0777, umask: 0033, got: 0%03o\n",
           (unsigned int)(sb.st_mode & 0777));
    /* Result: 0777 & ~0033 = 0744 (rwxr--r--) */

    umask(old_mask);  /* restore original umask */
    unlink("testperm");
    return 0;
}

Example 5: access() — check permissions using real UID/GID

#include <stdio.h>
#include <unistd.h>

int main(void) {
    const char *path = "/etc/shadow";

    /* access() uses REAL UID/GID (not effective) */
    /* Useful in setUID programs to check if the real user has access */

    if (access(path, F_OK) == 0)
        printf("File exists\n");
    else
        printf("File does not exist\n");

    if (access(path, R_OK) == 0)
        printf("Real user can read\n");
    else
        printf("Real user cannot read\n");

    if (access(path, W_OK) == 0)
        printf("Real user can write\n");
    else
        printf("Real user cannot write\n");

    if (access(path, R_OK | W_OK) == 0)
        printf("Real user can read AND write\n");

    return 0;
}
/* WARNING: access() has a TOCTOU race condition.
   Between the access() check and the actual open(),
   the file could be replaced by an attacker.
   Avoid access() in security-sensitive code. */

Example 6: set-UID program — how it works

/* A setUID-root program runs with EFFECTIVE UID = root,
   even if the real user is unprivileged.
   Classic example: /usr/bin/passwd (lets users change their own password)

   To create a setUID program:
   gcc -o mysetuid mysetuid.c
   chown root mysetuid     (root must own it)
   chmod u+s mysetuid      (set the setUID bit)
   chmod 4755 mysetuid     (same as above: 4=setUID, 7=rwx, 5=r-x, 5=r-x) */

#include <stdio.h>
#include <unistd.h>

int main(void) {
    printf("Real UID:      %d\n", getuid());   /* who ran this */
    printf("Effective UID: %d\n", geteuid());  /* what permissions we have */
    /* If setUID-root: Real UID = normal user, Effective UID = 0 */
    return 0;
}

Example 7: sticky bit on directory — restricted delete

/* The sticky bit on a directory means:
   A user can only unlink()/rename() a file if:
   - They have write+execute on the directory AND
   - They own the file OR they own the directory

   /tmp is the classic example: chmod +t /tmp
   Anyone can create files there, but can't delete others' files. */

#include <stdio.h>
#include <sys/stat.h>

int main(void) {
    struct stat sb;

    stat("/tmp", &sb);

    if (sb.st_mode & S_ISVTX)
        printf("/tmp has the sticky bit set\n");

    /* Set sticky bit on a directory you own */
    if (chmod("/home/ravi/shared", S_IRWXU | S_IRWXG | S_IRWXO | S_ISVTX) == -1)
        perror("chmod");
    /* ls -ld /home/ravi/shared shows: drwxrwxrwt (lowercase t = sticky+exec)
                                         or: drwxrwxrwT (uppercase T = sticky, no exec) */
    return 0;
}

Example 8: Check if a file has setUID or setGID bit set

#include <stdio.h>
#include <sys/stat.h>

void check_special_bits(const char *path) {
    struct stat sb;
    if (stat(path, &sb) == -1) { perror("stat"); return; }

    printf("%s: ", path);
    if (sb.st_mode & S_ISUID) printf("[SETUID] ");
    if (sb.st_mode & S_ISGID) printf("[SETGID] ");
    if (sb.st_mode & S_ISVTX) printf("[STICKY] ");
    printf("\n");
}

int main(void) {
    check_special_bits("/usr/bin/passwd");   /* setUID-root */
    check_special_bits("/usr/bin/wall");     /* setGID */
    check_special_bits("/tmp");              /* sticky */
    return 0;
}

4. How umask() Works — Visual Explanation

umask bits are always turned OFF in newly created files. Think of umask as a “permission denial mask”.

/* How new file permissions are calculated: */
mode_requested = 0666 (rw-rw-rw-)
umask = 0022 (—-w–w-)
/* actual = mode_requested & ~umask */
actual = 0644 (rw-r–r–)
Common umask Bits turned off open(0666) → file gets mkdir(0777) → dir gets
022 group-write, other-write rw-r–r– (0644) rwxr-xr-x (0755)
027 group-write, other all rw-r—– (0640) rwxr-x— (0750)
077 group all, other all rw——- (0600) rwx—— (0700)
000 Nothing turned off rw-rw-rw- (0666) rwxrwxrwx (0777)

5. Interview Questions

Q1 What is the difference between chmod() and umask()?

A chmod() changes permissions on an existing file. umask() sets a process-level mask that specifies which permission bits are always turned OFF when creating new files. umask doesn’t affect existing files.

Q2 A file has permissions —-r–r–. The owner tries to read it. What happens?

A The owner is denied. The permission check stops after finding the owner match (step 2), and the owner bits say “no read”. The fact that group/other have read permission is irrelevant because the owner was matched first.

Q3 What is the sticky bit on a directory?

A The sticky bit on a directory is a “restricted delete” flag. Even if a user has write permission on the directory, they can only delete or rename files they own. Classic example: /tmp has the sticky bit so users can’t delete each other’s temp files.

Q4 What is the difference between set-UID and set-GID bits on an executable?

A When a set-UID executable runs, the process’s effective UID is set to the file’s owner UID. When a set-GID executable runs, the effective GID is set to the file’s group GID. Both allow privilege elevation during execution.

Q5 If open() is called with mode 0777 and umask is 022, what permissions does the new file get?

A The actual mode = 0777 & ~022 = 0755 (rwxr-xr-x). But note: open() for a regular file would use 0666 as the mode, not 0777. mkdir() uses 0777 typically.

Q6 What is the TOCTOU vulnerability with access() and how do you avoid it?

A TOCTOU = Time-Of-Check, Time-Of-Use. Between calling access() and the actual open(), an attacker might swap the file with a symlink to a privileged file. The check result is stale. Avoid by dropping to the real user’s identity (setfsuid), attempting the operation directly, and checking errno instead of using access().

Q7 You need read permission on a directory to list files in it. What if you have execute but no read?

A Without read permission, ls (or any directory listing) fails. But with execute (search) permission only, you can still access files in the directory if you know their exact names. This is a useful access-control technique for public directories.

Q8 Write code to make a file readable by everyone but writable only by owner.

A

chmod("myfile", S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
/* Equivalent: chmod("myfile", 0644); */

Next: I-node Flags — ext2 extended file attributes, immutable, append-only and more

← Previous Next Topic →

Leave a Reply

Your email address will not be published. Required fields are marked *