A FILESYSTEM, FROM FIRST PRINCIPLES

Every byte.
Accounted for.

A complete filesystem inside a single disk image. Explore how C turns raw storage into names, directories, and persistent data.

1 GiBdisk image4 KiBper block15public APIs
01 /

Watch the bytes move

INTERACTIVE MODEL
APPLICATION LAYER01 / 09

Begin with a blank image

Formatting writes the superblock, bitmap, and FAT. The root directory owns block 265; block 266 is the first free block.

userfs / consoleAPI TRACE
DIRECTORY TREEname → metadata
DISK IMAGEuserfs.img
One image, four regions. Select a region to inspect its role. Diagram not to scale.
DATA BLOCKS 265–296
DirectoryFile dataFree
1allocated data blocks
261,878free data blocks
ENTRY INSPECTORDIRECTORY

/

FAT CHAIN

A JavaScript teaching model of the C design. Commands wrap open/write/close operations; this page does not run C or create a real disk image. Lab files are limited to 64 KiB. The actual implementation and persistence tests are in the repository.

02 /

A small system. A complete story.

Read the design

No inode table.
No hidden machinery.

A name and its metadata live together in the parent directory. The bitmap tracks ownership. The FAT links blocks into a stream. Every operation eventually reaches an explicit byte offset in one host file.

THE PUBLIC BOUNDARY

A familiar interface, in user space.

Applications include userfs.h and link the C library. Format and mount an image, create a hierarchy, then open files for offset-based I/O. Up to 32 descriptors hold independent offsets while mounted.

ufs_mount("userfs.img");
ufs_create("/hello.txt");
int fd = ufs_open("/hello.txt", UFS_O_RDWR);
ufs_write(fd, "Hello!", 6);
ufs_close(fd);
ufs_unmount();
Explore userfs.h ↗

THE 64-BYTE DIRECTORY ENTRY

A name is also the metadata.

64 records fit in a 4 KiB directory block. A full directory grows through the same FAT mechanism as a file.

filename48 Bsize4 Bfirst block4 Bflags2 Breserved6 B

Flags: is_directory (1 byte) + is_used (1 byte). File names use at most 47 bytes plus a null terminator. Widths are illustrative.

03 /

From an idea to an API call.

C11 / LINUX
01—03

Lifecycle

ufs_format · ufs_mount · ufs_unmount

Initialize the fixed image, validate and load allocation metadata, then flush and release runtime state.

04—06

Directories

ufs_mkdir · ufs_rmdir · ufs_listdir

Create nested directories, list active entries, and remove empty directories. Each directory owns at least one block.

07—10

Files & handles

ufs_create · ufs_unlink
ufs_open · ufs_close

Empty files allocate no data block. Open handles remember the parent entry location, flags, and current offset.

11—15

Data & metadata

ufs_read · ufs_write · ufs_seek
ufs_truncate · ufs_stat

Follow FAT chains for byte I/O. Seeking alone allocates nothing; later writes and truncate growth zero-fill new ranges.

Designed to make the fundamentals visible.

UserFS is a single-process library with one mounted image. It is not a kernel mount or FUSE driver. There is no journal, thread synchronization, permissions model, or hard-link support. Successful mutations flush through fsync; arbitrary power-loss recovery is outside its contract.

Implementation limits ↗

GO ONE LEVEL DEEPER

Read it. Build it. Take it apart.

Created by Marwan Yasser Negm · Sudo Team · STM training 2026