Permission bits
Check algorithm
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:
How ls -l displays permissions:
| 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) |
When you access a file, the kernel checks in this order and stops at the first match:
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;
}
umask bits are always turned OFF in newly created files. Think of umask as a “permission denial mask”.
| 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) |
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
