A user-space filesystem in C, built around one persistent disk image.
UserFS turns a raw 1 GiB disk image into a hierarchical store of files and directories. It combines a free-space bitmap, a global File Allocation Table (FAT), and fixed-size directory entries with a small, POSIX-inspired API.
The project makes filesystem internals inspectable: a name resolves to a directory record, the record points to a chain of blocks, and each block maps to an explicit offset in the image. All persistent state lives inside that one host file.
Created by Marwan Yasser Negm, as part of Sudo Team / STM training 2026.
The interactive website includes:
- A nine-step walkthrough of format, directory creation, file writes, growth, shrink, block reuse, remount, and deletion.
- A command lab with a live directory tree, allocation map, entry inspector, and FAT chains.
- Selectable architecture layers, disk-region explanations, and a guide to all 15 public functions.
- Direct access to the original 77-slide presentation, architecture diagram, and C source.
The website is a JavaScript teaching model, not a C runtime, FUSE mount, or disk-image editor. Its files are limited to 64 KiB, and its state resets when the page reloads. Real persistence is exercised by the C integration suites.
- Nested directories and regular files, with absolute-path traversal.
- A fixed 4 KiB block size and 64-byte directory records; no inode table.
- Next-fit allocation, FAT-linked file and directory growth, and freed-block reuse.
- Independent file offsets, append mode, seek, read/write, and truncate.
- Zero-filled file growth, zeroed reused blocks, and cleared truncated tails.
- Metadata flushing and
fsyncat successful mutation boundaries. - Allocation validation, bounded FAT traversal, and cycle detection.
- Linux, including Debian under WSL on Windows.
- A C11 compiler, GNU Make, and
ar(provided bybuild-essentialon Debian/Ubuntu). - AddressSanitizer and UndefinedBehaviorSanitizer support for
make sanitize.
The library uses pread, pwrite, ftruncate, and fsync; native Windows/MinGW is not the supported build environment. Tests create temporary images with a logical size of 1 GiB. Actual host allocation may be sparse.
git clone https://github.com/Marwan-Negm/userfs.git
cd userfs
make # Build libuserfs.a
make test # Run both C integration suites
make sanitize # Run both suites under ASan + UBSan
make demo # Write, unmount, remount, and verify a messageThe example uses a unique temporary image and removes it afterward. Expected output:
Read after remount: Hello from UserFS!
Persistence verified: the same bytes survived unmount and mount.
See examples/quickstart.c for the complete error-checked program. Link your own application with:
cc -std=c11 -D_FILE_OFFSET_BITS=64 -I. your_program.c libuserfs.a -o your_programFormatting erases the target image. Call ufs_format only when creating or intentionally resetting an image; use ufs_mount to reopen an existing filesystem.
make tools # Build the nine small utilities in build/
make walkthrough # Run tests/demo.sh using build/disk.imgmake walkthrough reformats build/disk.img on every run. The utilities mount that image, perform their operation, and unmount. For example, after the walkthrough has initialized it:
cd build
./make_dir /notes
./create_file /notes/hello.txt
./write_file /notes/hello.txt "Hello, UserFS!"
./read_file /notes/hello.txt
./file_stat /notes/hello.txt
./list_dir /notesflowchart TD
A[Application / tests] --> B[userfs.h — public API]
B --> C[Path resolver and directory entries]
B --> D[Open descriptor table — runtime only]
C --> E[Bitmap allocator and FAT chains]
D --> E
E --> F[pread / pwrite / fsync]
F --> G[One persistent 1 GiB disk image]
| Region | Absolute blocks | Size | Responsibility |
|---|---|---|---|
| Superblock | 0 |
4 KiB | Magic, version, geometry, region pointers |
| Allocation bitmap | 1–8 |
32 KiB | One allocation bit per absolute disk block |
| Global FAT | 9–264 |
1 MiB | One 32-bit next-block value per block |
| Data region | 265–262143 |
1,072,656,384 bytes | Directory entries and file payloads |
There are 262,144 blocks. The root directory occupies block 265, leaving 261,878 initially free data blocks. FAT values are absolute block indexes: 0 means free, and 0xFFFFFFFF ends a chain.
Each object stores its metadata directly in its parent directory:
struct disk_dir_entry {
char filename[48]; // Up to 47 bytes + terminator
uint32_t size; // Exact regular-file size
uint32_t first_block_index; // First absolute block in the FAT chain
uint8_t is_directory;
uint8_t is_used;
uint8_t reserved[6];
}; // 64 bytes; 64 entries per blockAn empty file uses size = 0 and first_block_index = 0. Every directory owns at least one block. When its entries fill a block, the directory grows through its FAT chain; removed slots are reused before further growth.
Read the full design · View the original architecture diagram
Full declarations, flags, and public structures are in userfs.h.
| Group | Functions |
|---|---|
| Image lifecycle | ufs_format, ufs_mount, ufs_unmount |
| Directories | ufs_mkdir, ufs_rmdir, ufs_listdir |
| File lifecycle | ufs_create, ufs_unlink |
| Descriptors | ufs_open, ufs_close |
| Data access | ufs_read, ufs_write, ufs_seek, ufs_truncate |
| Metadata | ufs_stat |
Operations return -1 on failure and set errno. Successful reads and writes return a byte count; seek returns the new offset; listdir returns the number of entries copied, capped by the provided capacity. Open returns a library descriptor. Other successful operations return 0.
Open flags are UFS_O_RDONLY, UFS_O_WRONLY, or UFS_O_RDWR, optionally combined with UFS_O_APPEND. Opening does not create or truncate a file. Append re-reads the file size for every write.
| Property | Contract |
|---|---|
| Execution | Single process, one mounted image; no thread synchronization or multi-process locking |
| Image size | Exactly 1 GiB |
| Path length | At most 255 bytes; absolute paths only |
| Component length | At most 47 bytes; . and .. are rejected |
| Open descriptors | At most 32, with independent offsets |
| File size | Stored in 32 bits; growth is also limited by available image blocks |
| Directory size | Reported as 0; directory storage is tracked through its chain |
| Delete semantics | An open file cannot be unlinked (EBUSY); directories must be empty |
| Disk representation | Native struct representation; intended for little-endian Linux/x86 |
| Recovery | No journal or full filesystem-repair tool |
This is an educational library, not a kernel filesystem or FUSE driver. It does not implement permissions, owners, timestamps, links, rename, or arbitrary crash recovery. Seeking past EOF does not allocate blocks; a later write allocates and zero-fills the gap, so sparse gaps are not stored as holes inside UserFS.
Successful mutations flush allocation metadata and call fsync. This is distinct from transaction atomicity during power loss. Mount validates the geometry and allocation structures, including the root chain; it is not a complete reachability/ownership fsck.
The two C suites inspect raw disk geometry and exercise persistence, nested paths, multi-block I/O, zero-filled gaps, truncate growth/shrink, append behavior, directory expansion and slot reuse, descriptor limits, reclamation, ENOSPC rollback, and selected corruption cases.
GitHub Actions builds the library and utilities, runs both suites normally and with sanitizers, verifies the persistence example, and runs the command-line walkthrough. A separate job checks the browser model.
make test
make sanitizeThe website consists of static HTML, CSS, and JavaScript in docs/. No application dependencies or build step are required. Use Node.js 20 or newer for the local server and model tests:
npm start # http://127.0.0.1:4173
npm run check # JavaScript syntax
npm test # Browser-model scenariosThe layout uses Google Fonts when available and falls back to system fonts. The interactive model runs locally in the visitor's browser.
GitHub Pages publishes the /docs folder on main. Its URL is configured in the repository's About section and linked at the top of this README. Changes pushed to that folder are published automatically by GitHub Pages.
userfs.c C implementation
userfs.h Public API and limits
Makefile Library, tests, utilities, and demos
examples/quickstart.c Complete persistence example
tests/ Integration suites and small CLI utilities
readme/DESIGN.md Detailed format and behavior specification
readme/*.png Original architecture diagram
presentation/*.pptx Original 77-slide walkthrough
docs/ Interactive GitHub Pages website
scripts/serve.mjs Dependency-free local website server
.github/workflows/ci.yml Automated checks
See CONTRIBUTING.md for the development workflow. Keep changes traceable from the public API to the disk format, and accompany bug fixes with focused regression tests.
The original presentation and diagram are preserved as supporting material. The C source and executable tests are the authority for implemented behavior. No license has been selected for this repository.