Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

exFAT Read-Only Filesystem

Overview

The kernel includes a read-only exFAT filesystem driver that sits on top of the virtio-blk block device. It auto-detects bare exFAT volumes, MBR-partitioned disks, and GPT-partitioned disks, then exposes simple directory-listing and file-read operations through the shell.

The driver is implemented entirely in devices/src/virtio/exfat.rs with no external dependencies.


Architecture

Shell (ls / cat / cd / pwd)
        │
        │  open_exfat / list_dir / read_file
        ▼
  ExfatVol  ──── async sector reads ────▶  BlkInbox
        │                                     │
  Partition detection                   VirtioBlkActor
  Boot sector parse                     (virtio-blk driver)
  FAT traversal
  Dir entry parse
  Path walk

All filesystem I/O is done one 512-byte sector at a time via the ask pattern on the virtio-blk actor’s mailbox (VirtioBlkMsg::Read).


Partition Auto-Detection

open_exfat reads sector 0 and applies the following decision tree:

sector0[3..11] == "EXFAT   "
  → bare exFAT (no partition table); volume starts at LBA 0

sector0[510..512] == [0x55, 0xAA]
  read sector 1
  sector1[0..8] == "EFI PART"
    → GPT: scan partition entries starting at the LBA stored in the header
      look for type GUID = EBD0A0A2-B9E5-4433-87C0-68B6B72699C7
      (on-disk mixed-endian: A2 A0 D0 EB E5 B9 33 44 87 C0 68 B6 B7 26 99 C7)
      read StartingLBA of the matching entry, verify "EXFAT   " there
  else
    → MBR: scan partition table at sector0[446..510]
      look for entry with type byte 0x07
      read LBA start (bytes 8–11 LE u32), verify "EXFAT   " there

else
  → ExfatError::UnknownPartitionLayout

Type 0x07 is shared by exFAT and NTFS. The driver always verifies the OEM name at the candidate partition’s first sector before accepting it as exFAT.


On-Disk Layout

Boot Sector

OffsetSizeFieldNotes
38FileSystemNameMust equal "EXFAT " (with trailing space)
804FatOffsetSectors from volume start to FAT
884ClusterHeapOffsetSectors from volume start to data region
964FirstClusterOfRootDirectoryCluster number of root dir
1091SectorsPerClusterShiftsectors_per_cluster = 1 << shift
5102BootSignatureMust equal [0x55, 0xAA]

FAT (File Allocation Table)

An array of u32 little-endian values. Entry N holds the next cluster in the chain for cluster N, or 0xFFFFFFFF for end-of-chain.

fat_lba          = volume_lba + FatOffset
sector_of_entry  = fat_lba + (N * 4) / 512
byte_in_sector   = (N * 4) % 512

Cluster Heap

cluster_lba(N) = cluster_heap_lba + (N − 2) * sectors_per_cluster

Cluster numbers start at 2; clusters 0 and 1 are reserved.

Directory Entry Sets

Each entry is 32 bytes. A file or directory is represented by a consecutive set of three or more entries:

Type byteNameKey fields
0x85File[1] SecondaryCount; [4..6] FileAttributes (bit 4 = directory)
0xC0Stream Extension[8..16] DataLength (u64 LE); [20..24] FirstCluster (u32 LE)
0xC1+File Name[2..32] up to 15 UTF-16LE code units per entry

Type 0x00 marks the end of directory; scanning stops immediately. Any type byte with bit 7 clear (< 0x80) is an unused or deleted entry and is skipped.


ExfatVol State

#![allow(unused)]
fn main() {
pub struct ExfatVol {
    lba_base:            u64,  // absolute LBA of the exFAT boot sector
    sectors_per_cluster: u64,
    fat_lba:             u64,  // absolute LBA of the FAT
    cluster_heap_lba:    u64,  // absolute LBA of the cluster heap
    root_cluster:        u32,
}
}

This is returned by open_exfat and passed to every subsequent call. The shell calls open_exfat fresh on each command (stateless).


Public API

#![allow(unused)]
fn main() {
/// Auto-detect layout and open the exFAT volume.
pub async fn open_exfat(inbox: &BlkInbox) -> Result<ExfatVol, ExfatError>;

/// List directory at `path` (e.g. "/" or "/docs").
pub async fn list_dir(vol: &ExfatVol, inbox: &BlkInbox, path: &str)
    -> Result<Vec<DirEntry>, ExfatError>;

/// Read a file into memory.  Capped at 16 KiB.
pub async fn read_file(vol: &ExfatVol, inbox: &BlkInbox, path: &str)
    -> Result<Vec<u8>, ExfatError>;
}
#![allow(unused)]
fn main() {
pub struct DirEntry {
    pub name:   String,
    pub is_dir: bool,
    pub size:   u64,
}

pub enum ExfatError {
    NoDevice, IoError, NotExfat, UnknownPartitionLayout,
    PathNotFound, NotAFile, NotADirectory, FileTooLarge,
}
}

BlkInbox is a type alias for the virtio-blk actor’s mailbox:

#![allow(unused)]
fn main() {
pub type BlkInbox = Arc<Mailbox<ActorMsg<VirtioBlkMsg, VirtioBlkInfo>>>;
}

Path Resolution

The shell maintains a current working directory (CWD) in Shell::cwd (spin::Mutex<String>, default "/").

resolve_path(cwd, path) in kernel/src/shell.rs handles relative and absolute paths, then normalize_path collapses . and .. components:

cwd = "/a/b"
resolve("../c")  →  normalize("/a/b/../c")  →  "/a/c"
resolve("/foo")  →  "/foo"
resolve("")      →  "/a/b"   (defaults to CWD)

Path component matching in the driver is case-insensitive ASCII (str::eq_ignore_ascii_case). Non-ASCII filename characters are replaced with ? in the decoded string.


Shell Commands

CommandDescription
ls [path]List directory; defaults to CWD
cat <path>Print file as text; non-printable bytes shown as .
pwdPrint current working directory
cd [path]Change CWD; verifies the target exists; defaults to /
blk ls [path]Alias for ls
blk cat <path>Alias for cat

cd calls list_dir on the target path before updating the CWD, so invalid paths are rejected with an error rather than silently accepted.


Memory Budget

Peak heap usage during ls:

ItemSize
Boot sector512 B
FAT sector (per entry lookup)512 B
Cluster data (typical 4 KiB cluster)4 KiB
Vec<DirEntry>small
Total~5 KiB

read_file caps output at 16 KiB. The kernel heap is 100 KiB; both operations are well within budget.


Limitations

Read-only

Write support is not implemented. VirtioBlkMsg::Write exists in the block driver but the exFAT layer has no write path.

Entry sets crossing cluster boundaries

scan_dir_cluster collects all sectors of a cluster into a flat buffer before parsing entries. An entry set whose 0x85 primary entry is in one cluster and whose secondary entries start in the next cluster will be silently skipped. This situation does not arise on normally-formatted volumes where directories start empty.

ASCII-only filenames

UTF-16LE code points above U+007F are replaced with ?. Files can still be opened by name if the shell command uses the same replacement — but in practice, test images should use ASCII filenames.

Fresh volume open per command

open_exfat reads the boot sector (and up to ~32 GPT entry sectors) on every shell command. A cached ExfatVol stored in the shell actor would reduce overhead, but is unnecessary given the current workload.

16 KiB file cap

read_file returns ExfatError::FileTooLarge for files exceeding 16 KiB. The limit exists to protect the 100 KiB heap; it can be raised if the heap is grown.


Key Files

FileRole
devices/src/virtio/exfat.rsPartition detection, boot parse, FAT traversal, dir scan, path walk, public API
devices/src/virtio/mod.rsRe-exports BlkInbox, DirEntry, ExfatError, ExfatVol, public functions
kernel/src/shell.rscmd_blk_ls, cmd_blk_cat, cmd_cd, cmd_pwd, resolve_path, normalize_path

Creating Test Images

GPT (macOS default)

hdiutil create -size 32m -fs ExFAT -volname TEST test-gpt.dmg
hdiutil attach test-gpt.dmg
cp hello.txt /Volumes/TEST/
mkdir /Volumes/TEST/subdir
cp nested.txt /Volumes/TEST/subdir/
hdiutil detach /Volumes/TEST
hdiutil convert test-gpt.dmg -format UDRO -o test-gpt-ro.dmg

MBR-partitioned

diskutil eraseDisk ExFAT TEST MBRFormat /dev/diskN

Bare exFAT (no partition table)

diskutil eraseVolume ExFAT TEST /dev/diskN

Running in QEMU

qemu-system-x86_64 ... \
  -drive file=test-gpt.img,format=raw,if=none,id=hd0 \
  -device virtio-blk-pci,drive=hd0

Then in the shell:

ostoo:/> ls
  [DIR]        subdir
  [FILE    13] hello.txt
ostoo:/> cat /hello.txt
Hello, kernel!
ostoo:/> cd subdir
ostoo:/subdir> ls
  [FILE    11] nested.txt
ostoo:/subdir> cat nested.txt
Hello again!
ostoo:/subdir> cd /
ostoo:/> pwd
/