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.
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 |
Definitions of basic file and filesystem abstractions can be found in the following file:
The Be_ prefix refers to "backend", evidently.
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:
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.
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:
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:
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.
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.
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.
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.
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 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 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).
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.