Virtual Filesystem

A filesystem provides a hierarchical naming scheme for files. A virtual filesystem permits each program to employ its own particular naming scheme, potentially completely different from schemes used by other programs.

  1. Backend Abstractions
  2. Access to Virtual Filesystem Operations
  3. File and Filesystem Registration
  4. Mounting Filesystems
  5. Access to Files
  6. Namespaces and Files
  7. Access to Directories
  8. Namespaces and Directories
  9. The ROM Filesystem
  10. Populating a Namespace Filesystem

Since virtual filesystem functionality is defined for each program, it therefore makes sense to provide access to it using a library that is loaded by each program. The libl4re-vfs library provides such access:

Several different implementation files are mentioned in the following file:

These implementation files reside in the following directory:

They define various abstractions as follows:

Name Abstractions
ns_fs Namespace-based filesystem
ro_file Read-only file
vcon_stream Virtual console stream

Backend Abstractions

Definitions of basic file and filesystem abstractions can be found in the following file:

The Be_ prefix refers to "backend", evidently.

Access to Virtual Filesystem Operations

Each program contains a singleton called __rtld_l4re_env_posix_vfs_ops with an apparent alias l4re_env_posix_vfs_ops, also exposed via L4Re::Vfs::vfs_ops. It appears to be declared here:

It appears to be defined here (along with file factory registration) in Vfs_init:

File and Filesystem Registration

Filesystems appeared to be registered using the register_file_system method on the vfs_ops singleton. Similarly file factories, producing appropriate file abstractions, are registered using the register_file_factory method.

File factories are registered in Vfs_init whereas filesystems appear to be registered automatically, at least if they derive from Be_file_system. The base class for file factories is File_factory_t:

It wraps capabilities referencing objects having the indicated "interface" (IFACE) with objects providing the indicated "implementation" (IMPL).

Employing the Vfs_init definitions, the following correspondence between "interfaces" and "implementations" is established:

Object Type File Type
Dataspace Ro_file
Namespace Ns_dir
Vcon Vcon_stream

Thus, for a capability providing access to a namespace, a Ns_dir object is created, for access to a dataspace, a Ro_file is created. And so on.

Mounting Filesystems

The mount method in the Vfs class is able to assign mountpoints to the virtual filesystem:

This is performed within the Fs::mount method:

First, the filesystem concerned is acquired using the get_file_system method. Then, the mount method is called on the filesystem to obtain a directory. Finally, the directory is assigned to the mountpoint.

The get_file_system method in the Vfs class searches the filesystem registry before attempting to dynamically load a filesystem using the Vfs_config::load_module function. This function dynamically loads a filesystem library:

However, another definition is found here, presumably for when dynamic loading is not available:

Access to Files

Programs configured to use shared libraries need linking to a number of libraries. A program using fopen needs to employ the C library libuc_c.so:

Within this library the fopen function in turn employs _stdio_fopen:

This invokes the open function which is provided by the backend library libc_be_l4refile.so:

The open function employs the __internal_open function which obtains a L4Re::Vfs::File from the __internal_resolve function, presenting it to the alloc_fd method of the vfs_ops singleton (L4Re::Vfs::vfs_ops) to allocate a file descriptor:

This accesses a store of file descriptors, invoking the alloc method:

It also associates the file with the allocated descriptor using the store's set method:

fopen__internal_open...__internal_resolvealloc_fdFileallocsetfopen_stdio_fopenopen

How the __internal_resolve function obtains the file is as follows. First, it calls the __internal_get_dir function to obtain a file for the path. This invokes a method via the vfs_ops singleton to obtain a file (or, more precisely, a directory) reference.

__internal_resolve__internal_resolve...__internal_get_dir...openat...get_rootget_cwdget_fileDir...File

Then, it calls the openat method on the directory:

The openat function first calls the get_mount method and then calls the get_entry method:

The get_mount method employs the _mount_tree member of the file instance to call the lookup method on the mount tree. This appears to search for a path and to return a mountpoint path, modifying the supplied Mount_tree reference for the mountpoint. Ultimately, a directory is returned for the mountpoint.

openatopenat...get_mount...get_entry...get_dscap_to_vfs_objectDataspace...lookup...mount...findMount_treeDirFile

The get_entry method is potentially provided by different classes, Env_dir and Ns_dir. Both of these perform exactly the same operations but delegate work to separate get_ds methods which appear to allocate a dataspace for the file and obtain the capability for that dataspace.

Then, the cap_to_vfs_object function appears to wrap the dataspace capability with a L4Re::Vfs::File object. The nature of the file is determined by a query of the object referenced by the capability, with the object metadata indicating protocol or name information that can then be used to make a file factory and thus a file.

Namespaces and Files

The Ns_dir class employs a Namespace object that is queried in its get_ds method using the namespace's query method. This should obtain a capability for the path that refers to a dataspace.

Interestingly, the getdents method (dent meaning directory entry) employs a special path (.dirinfo) whose dataspace is mapped and scanned directly for directory details.

So, it appears that namespaces can provide directory information, and the method of delivering file information is using dataspaces, with the file itself being accessed using a Ro_file abstraction.

Access to Directories

As noted above, getdents provides directory content information. Starting from the C library level, to obtain this information, the readdir function is invoked:

This in turn invokes __getdents:

Which may call __getdents64 and then __syscall_getdents64:

Or it may call __syscall_getdents. Either way, the __getdents and apparently equivalent __getdents64 functions provided by the C library backend are likely to be called:

The __getdents64 function obtains a File object for the file descriptor involved using the get_file method on the vfs_ops singleton, and it then calls the getdents method on the object.

Namespaces and Directories

Namespaces may act as directories, established above, but they can also provide directories. Presumably, the query operation in get_ds permits a namespace hierarchy to be navigated in order to obtain a dataspace for a file.

The ROM Filesystem

The rom filesystem is populated in the init_stage2 method of Moe:

This method registers rom and rwfs namespaces within the root namespace, and it then iterates over each of the modules in the payload, obtaining a dataspace for each of them and registering entries for them either in the rom namespace (for non-writable dataspaces) or the rwfs namespace (for writable dataspaces).

Populating a Namespace Filesystem

One way of doing this is to define a namespace in the configuration:

local subdir = l:create_namespace({
    file = "this gets replaced by a file dataspace"
    });

local ns = l:create_namespace({
    dir = l:create_namespace({
        subdir = subdir
        })
    });

Here, the directory containing the file, subdir, is exposed directly for convenience. Then, a program replaces the placeholder entry for the file in this directory.

l:start({
        caps = { myfs = subdir:m("rw") },
        log = { "server", "r" },
    },
    "rom/ex_testfile_server");

This is done by using get_cap on the program's environment to obtain the myfs capability mapping to subdir. Then, a capability for a dataspace is allocated, memory is allocated for the dataspace, and the memory is mapped into the running program, with some content copied into the memory for exposure via the file. Note that this program cannot terminate or the file entry will not persist in a way that permits its use.

Meanwhile, another program may access the file by accessing the namespace tree:

l:start({
        caps = { myfs = ns:m("r") },
        log = { "client", "g" },
    },
    "rom/ex_testfile");

Here, the myfs capability refers to the root of the tree, not the namespace corresponding to the directory containing the file. This means that the file is not accessible at the top level, and thus a suitable path must be used to access it:

FILE *fp = fopen("myfs/dir/subdir/file", "r");

The Ns_dir instance will employ the first component of the path to identify a capability corresponding to a namespace. Then, each component of the path is used to navigate the tree, with the final component yielding a dataspace that can be accessed using the Ro_file implementation.