Overview
Filesystem Stack
Userland (syscalls 0x20–0x2E)
│
▼
fs/vfs — mount table, path dispatch
├── fs/fat12 — FAT12 (read/write) behind fs/fatdev::FatDev
│ ├── on the floppy (/mnt/fat)
│ └── on the RAM disk (/mnt/tmp)
├── fs/iso9660 — CD-ROM ISO9660 (read-only) ┐ behind fs/rofs::RoFs
└── fs/ustar — tar archive in RAM (read-only) ┘
│ │
fat12/block.rs memdisk/block.rs iso9660/block.rs
(Floppy/ISA DMA) (MemDisk/RAM) (Atapi/PIO)
│ │
BlockDevice trait (fs/block.rs)
Both filesystems implement the same BlockDevice trait. All higher-level logic (directory walks, FAT chains, ISO records) is built on top of that single abstraction.
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]);
}
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.
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 |
FAT12 volume in RAM at /mnt/tmp |
Mounts at Boot
Set up by init::fs::vfs_init():
| Path | FsType | Condition |
|---|---|---|
/ |
Root |
Always |
/mnt/fat |
Fat12 |
Always |
/mnt/tmp |
MemDisk |
Always (formatted empty at every boot) |
/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:
- Iterate all mounts; check if
pathstarts 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). - Require exact match or that the next character after the prefix is
/. - The mount with the longest matching prefix wins.
- 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 or MemDisk) |
try_readonly_absolute(path) |
Returns Some((fs_type, rel)) if path resolves under a read-only mount (Iso9660 or Tar) |
mount(path, fs_type) |
Add a mount entry |
umount(path) |
Remove a mount entry by path |
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.
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)
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 (rel, base_cluster, dev). A path under /mnt/fat or /mnt/tmp is stripped of its mount prefix and starts at that volume's root; anything else is resolved from the working directory's cluster, on the volume the working directory is on (FatDev::cwd()).
FatDev (fs/fatdev.rs)
The floppy and the RAM disk run the same fat12::Filesystem, which is generic over BlockDevice. FatDev is an enum (Floppy, Mem) that implements BlockDevice by forwarding to one or the other, so every FAT12 code path opens Filesystem::new(&dev) and does not care which disk it has.
| Function | Description |
|---|---|
FatDev::of(fs_type) |
The device behind a Fat12 or MemDisk mount |
FatDev::for_path(abs) |
The volume an absolute path lies on, and the path below the mount |
FatDev::for_dir(abs) |
Like for_path, but a path under no FAT mount is the floppy's (a bare path under / has always named the floppy) |
FatDev::cwd() |
The working directory's volume and cluster |
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) |
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
argis joined ontocur; 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
Nonewhen the result does not fit inPATH_MAX(32 bytes), which is also the size ofSysInfo.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 a &'static Mutex<[u8]>. Each sector is copied with interrupts off, so a task preempted mid-copy cannot leave the lock held. 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: a 512 KiB (TMP_SIZE) static buffer in the kernel's .bss. At boot, vfs_init() calls memdisk::format_tmp(), which lays an empty FAT12 volume onto it with fat12::format::format(), and then mounts it. From then on it 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.
fat12::format::format(dev, sectors, label) is a small, generic mkfs. It writes one reserved sector, two FATs sized to cover the data area, a 224-entry root directory, one sector per cluster, and media byte 0xF8. The extended boot record carries the FAT12 type string that Filesystem::new looks for. It refuses volumes too small for any data or too large for FAT12 (4085 clusters or more, about 2 MiB at one sector per cluster).
Size limit. The buffer counts toward the kernel image, and the whole image has to end below 0x600000, where every process's page table maps its private frame instead. With 512 KiB, the release kernel ends near 0x372000 and the debug one near 0x3D9000. The zeroed buffer also lands in the ELF file, because .gdt/.idt follow .bss in the same loaded segment.
Limits
| Resource | Value |
|---|---|
| Max VFS mounts | 8 |
| Max mount path length | 31 bytes |
| FAT12 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 |
512 KiB (1003 one-sector clusters) |
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 at0xC00000, user frames from0x1000000to0x2400000).module::adopt()runs first thing inkernel_mainand moves the archive to0x2800000(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 above0x2400000is used in place. The kernel's stack, heap and boot page tables live in the linker's.boot_reservedNOLOAD 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::lbais the index of an entry's 512-byte header (ROOT=u32::MAXfor 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.