Rust in the Linux
Ramfs in Rust
The purpose of this article is to describe the state of Rust in Linux, expand what can easily be written in Rust and demonstrate code overview of ramfs implementation in it (of course, with adding some screenshots of the listed issues). You will also become familiar with key things valuable to know such as:
- pointers on where to start your own Rust kernel module
- more details about the Linux file system implementation than you need to know
- a newfound appreciation of the years of work that happened in the topic of the Linux
The requirements for your knowledge in order to understand all these things above is knowledge of some compiled programming language (preferably Rust) and vague idea of what a kernel is and what it does. So to do!
Rust for Linux | History
Rust 1.0 was released on May 15, 2015. The effort was officially announced on April 14, 2021 by Miguel Ojeda.
[PATCH 00/13] [RFC] Rust support
From: Miguel Ojeda firstname.lastname@example.org
“Some of you have noticed the past few weeks and months that a serious attempt to bring a second language to the kernel was being forged. We are finally here, with an RFC that adds support for Rust to the Linux kernel. […]”
Since then, work has been ongoing in the Rust-for-Linux Github repository: GitHub — Rust-for-Linux/linux: Adding support for the Rust language to the Linux kernel.
Scheduled for upstreaming in 6.1 (currently the latest preview kernel release is 6.0-rc6).
”Unless something odd happens, it [Rust] will make it into 6.1.” — Linus Torvalds
Rust for Linux | Practice
The Linux kernel is written in C. Rust itself has some FFI capabilities, so it’s possible to call this code directly from Rust. The preferred method of using Rust in Linux is to use kernel crate. Not the entire kernel is exposed this way, it is still a work in progress, but you get the benefit of having a nice rustian API.
Rust code is called in C using bindgen. This is a helpful little crate that generates Rust declarations for existing C and C++ code. Although you can directly call these bindings from your module, this is dicouraged.
Since file system support is not yet available (or wasn’t at the time of writing), the Rust file system implementation will directly call some C kernel code.
Rust for Linux | Beginnings
One good place to start tinkering with your own module is the character device. Most guides I’ve encountered deal with this very device, which makes sense because it’s quick to implement and self-contained.
Character devices are some of the files you see in `/dev` — stuff like `/dev/random`; they are the opposite of block devices, which also live in `/dev`, like `/dev/sda` (which is usually the first disk on your device). From the user’s perspective, they tend to behave like normal files that can be read and written from, providing a convenient interface for use from user space as well as for implementation in the kernel.
Another simple example is the TCP server, if for some reason running the HTTP server as root wasn’t enough for you.
The Rust for Linux project is quite dynamic, so it’s a good idea to take a look at the samples/rust folder, which contains simple examples of existing functionality and may contain something new that is of interest.
Other places to start — bindgen is there to help you…
Since most of the kernel functionality is exposed by bindgen, you can write something else, pretty much anything — just be prepared that you will have to write more boilerplate and deal with mode C API than Rust.
Filesystems in Linux | We’ve finally made it
Crash course concepts
- dcache —the kernel caches the directory structure in RAM to speed up access
This cache is mostly filesystem-independent (except for network filesystems, because we need to make sure that our cached results are not invalidated by other clients, which is irrelevant to our example). In our rustfs implementation, we will be using it strongly, and in fact it will be our main place to store the directory structure — since we’ll be implementing a RAM Disk, there’s no supporting device to which we can write the directory structure.
- inode — contains metadata about the file (file mode, UID,GID, size, file times, …, but not file name)
- dentry —directory entry containing file name and refers to particular inode. Multiple dentries can refer to the same inode, which usually results in the same file being available in two locations, perphaps under different names
Filesystems in Linux | Declaring and registering the file system module
Module 1. This screenshot contains a simple macro to declare a new kernel module, as well as the associated type, so we know where to start.
Module 2. This is a continuation of the previous screenshot, and contains the State type, which we will use to store module parameters (mainly as a demonstration).
Next we have an implementation of the Context type, which includes an example of how to actually pass module parameters to the `State` structure.
Module 3. Final sreenshot of the module setup.
We declare that our superblock (the data structure used to describe the file system) is independent, which means that there can be many independent superblocks (since everything happens only in RAM and is lost on unmounting).
After that, we give our file system a name and set flags.
The only flag we set is `USERNS_MOUNT` and it has to do with Linux namespaces. Namespaces are a method of separating resources and, in conjunction with CGroups, are what makes, for example, Docker tick. Since an unpriviliged user can run a namespace, it’s important to keep it secure, and mounting a file system can easily lead to a security hole, so it forbidden by default. It’s forbidden unless the file system is declared as namespace safe, and ours is, because all user activity is contained in virtual files in RAM, which, again, are lost on unmounting. After that, we fill in the rest of the superblocks and initialize the root directory.
RamFS “in” Rust | Here be dragons
This section shows the implementation of the Rust function that creates an inode. At this point, we’ve left kernel crate and are interacting directly with Linux.
First we call `new_inode`, to get the inode from the kernel, we give it the inode number. Then there are 3 code blocks that take care of memory mapping. Since, again, we’re writing RamFS, we need to make sure our data doesn’t get evicted from RAM (since it has nowhere else to go) and we set up address space operations, which, fortunately have already been implemented.
The screenshot below shows the implementation of the `init_root` function from 2 images ago. First we create a`INode`, and then make it behave like a directory by setting up the appropriate file and inode operations.
File operations are struct pointers to functions that implement the various functionalities you would expect from a file — read, write, etc.; while inode operations are analogous to directory functionality — file creation, renaming, etc.
We stub file operations with the default implementation from the kernel (since we’re creating a root directory and don’t care about file operations), and then put `RUST_RAMFS_DIR_INODE_OPERATIONS` (described later) as the struct for inode operations, which will make the root inode behave like a normal directory.
After that we have a call to `inode_init_owner`. It makes sure that the file mode is set correctly. By reusing existing kernel code, we not only save time and typing, but also make our file system behave more like any other Linux file system, which is a good thing. After that, we need to create a dentry for the new inode, and since there is no parent directory for the root directory, we use a special `d_make_root` function. Then we can register this dentry as root in the superblock.
Implementing a file system doing stuff (super important)
First we have the definition of the `set_operations` function (mainly for completeness).
More important is `RUST_RAMFS_DIR_INODE_OPERATIONS` struct. We put pointers to various functions that implement directory operations. What is notable there is that we can still get away with using some kernel provided implementations for operations such as renaming, lookup, etc; because we know that the entire directory structure is cached in memory. The only functionality we need to implement is the creation of new files, directories or other special files.
Same as before, but for files operations
In this section we have definition of `RUST_RAMFS_FILE_OPERATIONS` and `RUST_RAMFS_INODE_OPERATIONS`. These are similar to the already discussed file/inode operations for directories, but for files. For this reason, this time, the file operations are filled out, while the inode operations remain unimplemented (what does it mean to create a file inside a file?).
As for file operations, we can again take advantage of the fact that our file system exists only in RAM and rely on existing kernel implementations to access the contents of the file.
Creating a file (just kidding… not yet)
When filling in the above structs, we need to match the function signatures to what the kernel expects, so we declare the `rust_ramfs_create` function as `unsafe extern “C”`, which will cause the kernel to emit a function that can be called from C.
Function `rust_create_inode` does most of the heavy lifting here. All that’s left is error handling, and then calling `d_instantiate`, which fills the dentry we got as a parameter with inode information for the inode we just created. We also take care of incrementing the number of inodes, so the kernel knows if the dentry is in use.
So we made it — we have reached the point of creating a file
Here we have`rust_create_inode` called above. It creates an inode, sets the ownership (all the same as before).
Finally, for files (S_IFREG, which is a regular file) it sets the appropriate file and inode operations, to RUST_RAMFS_FILE_OPERATIONS` and `RUST_RAMFS_INODE_OPERATIONS, so that the newly created inode looks, walks and quacks like a normal file.
This concludes the code review. The code snippets provided are a bit truncated, but should guide you through the kernel module declaration in Rust, as well as the file system implementation, so that you have an idea of how kernel knows how to interact with our FS.
Elaboration created by Kinga Kuśnierz (Content Writer at Altimetrik Poland) based on materials from the webinar, authored by Mikołaj Florkiewicz (Senior Engineer at Altimetrik Poland).
Rust for Linux repo: https://github.com/Rust-for-Linux
Repo with my changes: https://github.com/fl0rek/linux
Great guide for writing first module: Rust Kernel Module: Getting Started :: WayNew — Yu-Wei Wu’s blog (wusyong.github.io)