Skip to content

Virtual file system ​

Xila has several things that can look like files: persistent storage, character devices, block devices, pipes, and control endpoints such as network interfaces. The Virtual File System (VFS) gives all of them one path namespace and one place to apply access policy.

The VFS does not define how LittleFS stores blocks or how a device implements IO. Its job is to decide what owns this path?, is this task allowed to access it?, and where should the operation go next?

Mental model ​

Think of the virtual_file_system::VirtualFileSystem as a router for paths:

The VFS owns the namespace and policy, while the mounted backend owns the concrete storage/device behavior. That separation is what allows a caller to use the same file-facing abstractions regardless of whether /data/example lives in LittleFS or /devices/network/wlan0 represents a control device.

Following one path through the system ​

Suppose an executable opens /data/config.json through File::open. It does not talk directly to the root file system.

The VFS first associates the request with the supplied Xila task, derives user/group and time context from its Task, Users, and Time dependencies, and resolves which mount owns the path. It then checks the relevant permissions before translating the absolute path into the backend-relative path understood by that mount.

Only after those namespace and policy decisions does the request reach the mounted backend through the file_system::FileSystemOperations contract.

The same routing idea applies to directories and devices. The difference is the kind of mounted endpoint and the operations supported after resolution, not a separate namespace implementation.

Why Task, Users, and Time are dependencies ​

The namespace alone is not enough to behave like a system file layer.

Task tells the VFS which logical task initiated an operation and therefore which user/group context applies. Users answers membership questions used during permission checks. Time provides the timestamps written during create/access/modify operations.

This is why a VirtualFileSystem is explicitly constructed with references to those managers. The VFS centralizes filesystem policy without reimplementing identity or time itself.

Backends share contracts, not implementations ​

Regular file-system backends implement file_system::FileSystemOperations. Character and block devices use the corresponding device contracts. Pipes are provided as in-memory endpoints by the VFS itself.

Because the VFS dispatches through these contracts, mounting a new backend does not require callers to learn a new path API. The backend can still have completely different on-disk or device semantics underneath that contract.

This division also defines a boundary: authentication/session policy lives above the VFS, while concrete block layout and wear-level/storage algorithms live below it.

Mounts and handles have lifetimes ​

Resolving a path is not the end of the VFS's job. Open files/directories and mounted devices keep references into backend state, so the VFS tracks live use to prevent a normal unmount from invalidating active handles.

An unmount can therefore report a busy resource while references are alive. A forced unmount deliberately overrides that protection. This is an architectural consequence of exposing long-lived typed handles rather than copying every operation into a stateless path request.

For async file reads, writes, flushes and controls, an operation-specific poll_fn waits for the backend to signal progress; cancellation unregisters pending operations. The separate VFS poll(...) helper for synchronous metadata is a one-shot call and does not retry a busy backend. A file opened with the non-blocking state surfaces the busy condition instead of registering an asynchronous waiter.

Under the hood ​

The current VFS is an explicitly owned virtual_file_system::VirtualFileSystem. initialize(...) constructs it from Task, Users, Time, and the root FileSystemOperations implementation.

Its mutable routing state is divided across RwLock<CriticalSectionRawMutex, ...> collections:

  • file_systems: Vec<InternalFileSystem> stores mount points, backend pointers, and reference counts;
  • a character-device BTreeMap maps inodes to mounted character endpoints;
  • a block-device BTreeMap does the same for block endpoints;
  • a pipe BTreeMap tracks in-memory pipe endpoints.

Mount resolution selects the file system whose mount point owns the requested path and forwards a relative path to that backend. Typed File and Directory wrappers retain the VFS/backend context needed for later operations rather than resolving the absolute path again for every byte transferred.

Those collection choices, lock boundaries, inode maps, and reference-count strategy are current implementation details. The stable architectural idea is the shared namespace, policy gate, mount dispatch, and typed handle semantics.

Default namespace shape ​

create_default_hierarchy(...) gives a newly assembled runtime a conventional set of locations. The exact tree is useful because these paths communicate intent across modules, but it should be read as a namespace convention rather than a statement about one physical disk:

  • /binaries — executable files;
  • /configuration — user-space configuration, including /configuration/shared;
  • /data — application data, including /data/shared;
  • /devices — mounted device endpoints, including /devices/network;
  • /logs — system/application logs;
  • /system — system-owned data such as /system/users and /system/groups;
  • /temporary — temporary data expected to be cleared across reboot/runtime recreation.

The root backend provides the base tree, while additional file systems and devices can be mounted into that namespace later. create_default_hierarchy creates directories and applies baseline permissions; it does not itself implement automatic temporary-directory cleanup or log rotation.

Failure and policy boundaries ​

A path can fail before the backend is called: the path may be invalid, its mount may not resolve, required metadata may be absent, or permissions may reject the operation. Backend errors then enter the VFS error domain after dispatch.

This distinction is useful when debugging: PermissionDenied is a policy decision made before backend execution, while a backend NotFound/IO failure describes what happened after routing to the selected implementation.

Device cleanup helpers intentionally tolerate stale device identifiers in expected cleanup cases, making repeated startup cleanup more idempotent. By contrast, live-reference protection on unmount is deliberately strict unless force is requested.

Async operation semantics and cancellation ​

The current VFS explicitly distinguishes operations that can be polled from synchronous metadata calls:

  • File::read, File::write, File::flush and typed File::control use core::future::poll_fn through the operation layer in blocking mode. A device can return Pending and arrange wake-up.
  • Dropping one of those futures drops an operation guard that calls cancel_operation, detaching its device registration. This is important when a guest exits, cancels work or abandons an async file call.
  • Files opened with StateFlags::NonBlocking instead issue synchronous attempts and return available data or busy/error directly.
  • virtual_file_system::poll(operation) is currently a one-shot call, not a busy-retry loop. It runs the supplied closure exactly once and returns its result. Metadata operations using it can surface RessourceBusy immediately.

Example: read a named file ​

rust
let mut file = virtual_file_system::File::open(
    vfs,
    task_id,
    file_system::Path::from_str("/data/config.json"),
    file_system::Flags::new(file_system::AccessFlags::Read, None, None),
)
.await?;
let mut bytes = [0u8; 256];
let count = file.read(&mut bytes).await?;
file.close(vfs).await?;

This is an integration example: vfs and task_id come from the assembled runtime, and errors must be handled for absent files, permissions or unsupported operations. Like other reads, the result may be shorter than the supplied buffer.

Limitations and trade-offs ​

Centralizing namespace and permission policy gives all callers consistent semantics, but it also means shared VFS metadata locks can become contention points under heavy parallel access.

Symbolic-link and advisory file-locking behavior is not implemented in the current surface, and the C/ABI filesystem layer still has incomplete entry points such as some symlink, timestamp, and truncate operations. That ABI incompleteness should not be confused with the full set of internal Rust VFS capabilities.

New file systems can be integrated by implementing the file-system contracts and mounting them; new devices can implement the direct character/block contracts and be placed at namespace paths. Callers should depend on those semantics rather than on internal collection types or initialization helpers.

Explore further ​