Skip to content

Filesystem Overview

Filesystem Stack

VFS dispatches writable mounts through FatDev and FAT, and read-only mounts through RoFs; floppy, RAM disk and CD use BlockDevice, while tar is read directly from RAM.

Filesystem dispatch and storage backends. Mounts appear only when their backing device or archive is available; / and /mnt are synthesized from the mount table.

The floppy, RAM disk, RNTP network disk and ATAPI device implement BlockDevice. FAT directory and chain logic runs over that abstraction; ISO9660 uses ATAPI block reads, while tar walks a GRUB-loaded memory archive through the shared read-only RoFs interface.


BlockDevice Trait (fs/block.rs)

pub trait BlockDevice {
    fn read_sector(&self, lba: u64, buffer: &mut [u8]);
    fn write_sector(&self, lba: u64, buffer: &[u8; 512]);
    fn try_read_sector(&self, lba: u64, buffer: &mut [u8]) -> Result<(), &'static str>;
    fn try_write_sector(&self, lba: u64, buffer: &[u8; 512]) -> Result<(), &'static str>;
    fn begin_io(&self) -> Result<(), &'static str>;
    fn end_io(&self);
    fn sector_count(&self) -> Option<u64>;
}

One sector = 512 bytes for FAT12/floppy. ISO9660 uses 2048-byte logical blocks but maps them to the same interface internally. Implementors: Floppy, Atapi (write is a no-op), MemDisk, NetDisk (RNTP/TCP).


VFS (fs/vfs/mod.rs)

The VFS is a simple mount table — it does not provide a unified file descriptor layer or inode abstraction. All it does is map path prefixes to filesystem types, and dispatch resolves which filesystem to use for a given absolute path.

Mount Table

pub static VFS: Mutex<VfsTable>

pub struct VfsTable {
    mounts: [VfsMount; MAX_MOUNTS],   // MAX_MOUNTS = 8
    count:  usize,
}

pub struct VfsMount {
    path:     [u8; 32],
    path_len: usize,
    fs_type:  FsType,
}

FsType enum:

Variant Meaning
None Empty / unused slot
Root Root mountpoint (/)
Fat12 FAT12 floppy at /mnt/fat
Iso9660 ISO9660 CD-ROM at /mnt/iso
Tar ustar archive loaded by GRUB, at /mnt/tar
MemDisk The RAM disk at /mnt/tmp: FAT16 (FAT12 when it is only 2 MiB)

Mounts at Boot

Set up by init::fs::vfs_init():

Path FsType Condition
/ Root Always
/mnt/fat Fat12 Only if floppy_check_init() found a FAT12 volume; an empty mount would answer everything below it with a disk error
/mnt/tmp MemDisk When memdisk::place_tmp() finds it memory, which takes 2 MiB of free RAM; formatted empty at every boot
/mnt/net NetDisk While an RNTP client is connected to a FAT12/16 RAM export
/mnt/iso Iso9660 Only if Iso9660::probe() succeeds
/mnt/tar Tar Only if GRUB loaded a tar archive as a module

Path Resolution

VfsTable::resolve(path) returns (FsType, relative_sub_path) using longest-prefix matching:

  1. Iterate all mounts; check if path starts with the mount path, compared case-insensitively (FAT folds names to upper case, so a program handing back a path in another case must still hit the mount).
  2. Require exact match or that the next character after the prefix is /.
  3. The mount with the longest matching prefix wins.
  4. Returns the sub-path after stripping the mount prefix (and a leading /).

Example: path b"/mnt/fat/SUBDIR/FILE.TXT" → (Fat12, b"SUBDIR/FILE.TXT").

Helpers

Function Description
try_fat12_absolute(path) Returns Some(rel) if path resolves under the Fat12 mount
try_iso9660_absolute(path) Returns Some(rel) if path resolves under the Iso9660 mount
try_fat_absolute(path) Returns Some((fs_type, rel)) if path resolves under a writable FAT12 mount (Fat12, MemDisk or NetDisk)
try_readonly_absolute(path) Returns Some((fs_type, rel)) if path resolves under a read-only mount (Iso9660 or Tar)
mount_dir(dir) Returns the names directly below dir when it is / or on the way to a mount (see below)
mount(path, fs_type) Add a mount entry
umount(path) Remove a mount entry by path

type_of(path) answers the type of the mount a path lies on.

These are the primary VFS entry points used by syscall handlers in abi/syscall.rs. Both wait (bounded spin) for the mount table lock instead of giving up at the first contention: a missed lock used to make an absolute path look relative and be resolved in the wrong directory.

FsType::name() gives the name mount and dir print for a type (rootfs, fat12, iso9660, tar, memdisk, netdisk).

Directories Made of Mounts

/ is no filesystem of its own, and neither is /mnt: they hold nothing but the way to the mount points. VfsTable::mount_dir(dir) lists such a directory from the mount table alone. For each mount below dir it takes the name one level down, once (/mnt/fat and /mnt/tmp both give mnt below /), and returns them in a MountDir, each with the type of the mount it is, or Root for a directory that only leads to mounts further down:

dir Result
/ mnt (Root); always a MountDir, even with nothing mounted
/mnt fat (Fat12), tmp (MemDisk), iso (Iso9660), tar (Tar), whichever are mounted
/mnt/tmp, /mnt/iso/bin None: inside a mounted filesystem, which lists itself
/nope, /mn None: leads to no mount

dir is absolute and normalized, and is matched without regard to case, as resolve matches. The kernel shell's cd accepts such a directory (storing cluster 0), and its dir prints the listing; see The Root Directory.

Syscall Dispatch Pattern

Every filesystem syscall uses the same two-step dispatch:

path → try_readonly_absolute(path)
         Some((fs_type, rel)) → RoFs::probe(fs_type)?.resolve(rel)  [read-only]
         None                 → vfs_resolve_fat12(path) → Filesystem::new(&dev)
                                  None → FileNotFound            [`/`, `/mnt`, outside every mount]

RoFs (fs/rofs.rs) is an enum over Iso9660 and Tar; both hand out IsoEntry, so a handler treats the two alike. Writes to either are refused.

vfs_resolve_fat12(path) answers (dev, rel): the name is made absolute first (a relative one joined onto the working directory, . and .. collapsed), then a path under /mnt/fat or /mnt/tmp is stripped of its mount prefix and walked from that volume's root. Anything else answers None and the syscall reports the file missing: / and /mnt only lead to the mounts and hold no files, and nothing lies outside them. A name means the same to every syscall, to the kernel shell and to the program loader; / is never the floppy, whatever the working directory.

Syscall 0x2D lists / and /mnt from the mount table (vfs::mount_dir), syscall 0x2E accepts them as working directories, and syscall 0x40 answers for /mnt as for /.

FatDev (fs/fatdev.rs)

The floppy, RAM disk and RNTP network disk run the same fat12::Filesystem, which is generic over BlockDevice. FatDev is an enum (Floppy, Mem, Net) that forwards I/O to the selected device. /mnt/net appears after an RNTP/TCP handshake and disappears on disconnect. FAT12/16 reads and writes use fallible device calls; a failed sector invalidates the operation and stops further writes. A network filesystem operation holds an ownership guard while scheduling remains active for its client daemon. Shared RNTP sessions also acquire a server operation lock, invalidate metadata on foreign revision changes and commit their writes before releasing the lock. Multiple R2 instances and Linux FUSE mounts can access the same export. Sequential RNTP reads use a separate 32-sector buffer inside the current FAT operation, with batched TCP fetches and kernel completions. This speeds up fmimage and other range readers without evicting cached FAT metadata.

Memento uses background processes for file copies (fmcopy.elf) and image previews (fmimage.elf). The image loader performs file lookup, reads and decoding while the desktop paints progress and handles input. A slow or stalled RNTP read therefore leaves the Image window closable and the desktop responsive.

Function Description
FatDev::of(fs_type) The device behind a Fat12, MemDisk or NetDisk mount
FatDev::for_path(abs) The volume an absolute path lies on, and the path below the mount
FatDev::cwd_mounted() The working directory's volume and cluster, or None when it is on no FAT volume (/, /mnt, the ISO, the archive)

A FAT cluster number alone no longer identifies a directory: cluster 5 on the floppy and cluster 5 on the RAM disk are different places. The volume is always taken from the path next to it; for the working directory that is SYSTEM_CONFIG.path, which is why chdir (syscall 0x2E) stores the normalized absolute path.


Working Directory

The current working directory is stored in SYSTEM_CONFIG (init/config.rs) as two fields:

Field Type Description
path [u8; 32] String representation (e.g. /mnt/fat/SUBDIR)
path_cluster u16 FAT12 cluster for the directory on the volume path lies on (0 = root; 0 for ISO9660, the archive, / and /mnt)

Changed by syscall 0x2E (chdir) and the shell's cd, which validate that the target exists as a directory before updating.

Path Normalisation (fs/vfs/path.rs)

vfs::normalize_path(cur, arg, out) builds the absolute path the shell's cd and dir work with:

  • A relative arg is joined onto cur; an absolute one replaces it.
  • Empty components (//, trailing /) and . are dropped; .. pops a component, and .. at the root stays at the root.
  • The walk is purely textual. Neither filesystem has symlinks, and it is the only way to honour .. on ISO9660, whose directory lookups skip the ./.. records.
  • It returns None when the result does not fit in PATH_MAX (32 bytes), which is also the size of SysInfo.system_path; callers report the error instead of storing a truncated path.

The module depends on nothing else in the kernel so the host unit tests (tests/unit/path.rs) can include it directly.


RAM Disk (/mnt/tmp, fs/memdisk/)

MemDisk (memdisk/block.rs) is a writable BlockDevice over memory given to it at boot (attach(base, len); before that it is a disk of no sectors). Each sector is copied with interrupts off and the disk's lock held, so a task preempted mid-copy cannot leave the lock held and a sector is never read half written. A sector number past the end reads back as zeros and is dropped on write, so a bad cluster number read from a damaged directory cannot index out of bounds.

memdisk::TMP is the disk behind /mnt/tmp. At boot, vfs_init() calls:

  1. memdisk::place_tmp(), which gives it a sixteenth of the usable RAM, in whole 2 MiB pages (126 MiB under QEMU's 2 GiB, 16 MiB under 256 MiB), at the top of the highest usable region of the Multiboot2 memory map below 4 GiB (boot::usable_top). It keeps clear of everything below the last process frame (0x5000000: the kernel, the user windows and heap, the 32 frames) and of the boot archive. With no room for that much it halves the size down to 2 MiB, and with none for 2 MiB there is no /mnt/tmp.
  2. memdisk::format_tmp(), which lays an empty volume onto it with fat12::format::format(): FAT16, with 2 KiB clusters at 128 MiB (FAT12 at 2 MiB; see Formatting).

It is then mounted, and behaves exactly like the floppy: files, subdirectories, rename, delete, write_file_at and ELF loading from the working directory all work there. Nothing on it survives a reboot.

Why there. The identity map boot.asm sets up covers 0–4 GiB in every process: the first GiB is copied into each process's page table and the rest is shared by them, all of it for the kernel only, outside the few user windows. So a syscall reaches the disk whatever process is running, and no program can touch it. Top-down, it stays out of the way of what is placed from below: the archive, and the user heap's extension, which also checks that it does not run into the disk (on a machine of a few hundred MiB the two come close). The disk used to be 512 KiB of the kernel's .bss, and could not grow there: the image has to end below 0x400000, which every process maps as user memory.

The first file on it is KERNDBG.LOG, the kernel's debug log as it stood at the end of init; the shell's debug command rewrites it with the log as it is now. See Debug Log.


Mount Sizes (fs::usage, syscall 0x40)

fs::usage(fs_type) answers how big a mounted filesystem is, how much of it is free, and its format on the medium (fs::Format: none, fat12, fat16, iso9660, tar), which says more than the mount type for the RAM disk:

Mount Size Free
/ 0: no filesystem, the way to the mounts 0
/mnt/fat, /mnt/tmp the whole volume, from the boot sector the free clusters, counted in one pass over the FAT
/mnt/iso the volume space size in the primary volume descriptor, read once with the root 0
/mnt/tar the archive's length 0

The kernel shell's mount, syscall 0x40 and through it bsh's mount and Memento's Files window show them. Each mount's medium is read with the mount table already let go: the floppy can take a while, and every path lookup waits on that lock.


Limits

Resource Value
Max VFS mounts 8
Max mount path length 31 bytes
FAT sector size 512 bytes
ISO9660 block size 2048 bytes
Max directory entries returned (syscall 0x28) 32
Max directory entries returned (syscall 0x2D) 64
Max VFS mounts listed (syscall 0x2C) 8 × 34-byte entries
/mnt/tmp RAM disk size a sixteenth of the usable RAM in 2 MiB pages, at least 2 MiB
FAT16 volume fewer than 65525 clusters of up to 32 KiB: about 2 GiB

Boot Medium Archive (/mnt/tar)

Booted from a USB stick, the kernel has no driver to read the medium it came from. Instead, make build_iso packs everything in iso/ except boot/ into iso/boot/usb.tar (ustar format), and grub.cfg loads it next to the kernel with module2 /boot/usb.tar usb. GRUB reads it through the firmware, so it works on any medium GRUB can boot from.

  • Relocation (fs/ustar/module.rs): GRUB usually places a module just past the kernel image, where the kernel keeps things at fixed physical addresses (process stacks, the userland heap at 0xC00000, user frames from 0x1000000 to 0x5000000). module::adopt() runs first thing in kernel_main and moves the archive to 0x5400000 (or past the boot information, if that is in the way), after checking the memory map says the target is usable RAM. A module already above 0x5000000 is used in place. Both addresses follow from MAX_PROCESSES. The kernel's stack, heap and boot page tables live in the linker's .boot_reserved NOLOAD section, so the loaded segment covers them and GRUB will not put the module there.
  • Lookup (fs/ustar/mod.rs): a linear scan over the headers, in memory. IsoEntry::lba is the index of an entry's 512-byte header (ROOT = u32::MAX for the root). Names compare case-insensitively. Directories must have their own entries in the archive, which GNU tar writes.
  • Limits: read-only, a snapshot taken at boot, and the archive stays in RAM for the life of the system.

Related documentation: Kernel Internals, FAT12 and FAT16, ISO9660, Filesystem Syscalls.