Skip to main content

Phase 2: File Permissions

Motivation

After Phase 1 establishes who the user is, Phase 2 answers what they are allowed to touch. Without file permissions, a logged-in patient can open /device/config, overwrite /dosage/insulin.log, or read the audit log: defeating the entire point of authentication.

Phase 2 adds Unix-style DAC (Discretionary Access Control) to every inode.

Original xv6 vs Phase 2 Code Delta

Stock xv6 has inode ownership fields in memory but not full Unix-style per-inode permission enforcement across all relevant syscall paths. This phase makes permissions first-class and enforced end-to-end.

ComponentStock xv6-riscvModified xv6-security
On-disk inode metadataNo mode/uid/gid persisted in this project baselineAdded mode, uid, gid to struct dinode
In-memory inode/stat propagationLimited metadata for DAC decisionsmode, uid, gid loaded/stored via ilock, iupdate, stati
Permission helperNoneperm_check(ip, access) in kernel/perms.c
Enforcement pointsMostly open-time semanticsExplicit checks in sys_open, fileread, filewrite, sys_exec
File ownership initializationGeneric file creation defaultsOwnership defaults set from caller identity during inode allocation

Files touched for this phase: kernel/fs.h, kernel/file.h, kernel/stat.h, kernel/fs.c, kernel/file.c, kernel/sysfile.c, kernel/perms.h, kernel/perms.c, mkfs/mkfs.c, plus user tools user/chmod.c and user/chown.c.

The Permission Bit Model

Unix permission bits pack into a 16-bit mode word:

15–12 11–9 8–6 5–3 2–0
filetype (unused) owner group other
rwx rwx rwx

Each three-bit group:

Bit positionMeaning
r (bit 2)Read access
w (bit 1)Write access
x (bit 0)Execute access

Example: 0644 = rw-r--r--:

  • Owner: read + write
  • Group: read only
  • Other: read only

What Changed in struct dinode

/* kernel/fs.h: new fields on the on-disk inode */
struct dinode {
short type; // existing: T_FILE, T_DIR, T_DEVICE
ushort mode; // NEW: octal permission bits, e.g. 0644
ushort uid; // NEW: owner user ID
ushort gid; // NEW: owner group ID
// ... rest unchanged ...
};

The in-memory struct inode mirrors these fields. ialloc, iupdate, ilock, and stati all read and write the new fields.

This is the key difference from original xv6 behavior in this codebase: permission decisions are now based on persistent inode metadata rather than ad hoc assumptions in user space.

Medical Files and Their Permissions

mkfs/mkfs.c creates these files at image build time:

PathModeOwnerAccessible by
/patient/records0400uid=1, gid=1Patient reads, admin override
/dosage/insulin.log0640uid=2, gid=1Doctor (owner) writes, patient (group) reads, admin override
/device/config0600uid=0, gid=0Admin only
/audit/syscall.log0400uid=0, gid=0Admin read-only

The insulin log is the one file two roles share. The doctor owns it and writes new doses. The patient is put in the file's group (gid=1) with read-only group bits, so they can see their dose history but never change it. Nobody else gets any access.

perm_check() Flowchart

Walkthrough: a Patient Opens /device/config

Say the patient is logged in (uid=1, gid=1) and runs cat /device/config. The file is owned by admin (uid=0, gid=0) with mode 0600. Here is what the kernel does, step by step:

  1. sys_open calls namei("/device/config") and finds the inode.
  2. It calls perm_check(ip, 'r') before handing back a file descriptor.
  3. perm_check first asks: is the caller admin (uid 0)? No, the patient is uid 1. No bypass.
  4. Is the caller the owner? ip->uid is 0, the patient is uid 1. Not the owner.
  5. Is the caller in the file's group? ip->gid is 0, the patient's gid is 1. Not the group.
  6. So the patient falls into the "other" class. The "other" read bit in 0600 is not set.
  7. perm_check returns 0. sys_open returns -1. The cat fails with a permission error.
  8. The audit log records this: a SYS_open with result -1 from uid 1. See Phase 3.

The patient never sees the device config, and the attempt leaves a trace. If the same patient opened /patient/records instead, step 4 would match (they own it) and the open would succeed.

The Four Enforcement Points

Every file access in xv6 passes through one of these four kernel locations:

HookFileWhen it runs
sys_open()kernel/sysfile.cBefore any file descriptor is created
fileread()kernel/file.cOn every read() syscall
filewrite()kernel/file.cOn every write() syscall
sys_exec()kernel/sysfile.cBefore loading a program image

Checking only at open is insufficient: an attacker with an already-open fd could still read/write after their permission is revoked.

Compared with stock xv6, this closes a common teaching-kernel gap by validating access both when descriptors are opened and when I/O actually occurs.

chmod and chown Syscalls

# Inside xv6 shell (admin logged in)

# Restrict config to admin-only
chmod /device/config 0600

# Transfer ownership of a log file
chown /dosage/insulin.log 1 1

chmod allows admin or file owner to change mode bits. chown is stricter and requires admin (uid == 0) in the current implementation.

These syscall entry points are additions in this repository and are wired through the expanded syscall table, so permissions are controlled in kernel space rather than only by convention in user programs.

Compliance Coverage

QEMU terminal showing patient denied on /device/config and allowed on /patient/records

TestWhat it checks
T07Patient cannot open /device/config (EACCES)
T08Patient can read /patient/records
T09Patient cannot write /patient/records (EACCES)
T10Clinician can write /dosage/insulin.log
T11Clinician cannot read /device/config (EACCES)
T12Admin can open all protected files

Known Implementation Detail: DIRSIZ

The filename compliance_test is 16 characters. The original xv6 DIRSIZ is 14. We increased it to 16:

/* kernel/fs.h */
#define DIRSIZ 16

This required updating mkfs.c to pad directory entries to the new size so that ls and directory reads remain aligned to struct dirent.