First, the context.

For those who are not aware, Rust for Linux is a project within the Linux developer community to add kernel support for the Rust programming language with the objective of developing drivers that can sit alongside regular C drivers in tree. The project is still somewhat young, but it's already reached the significant milestone of a first driver being merged.

Two years ago i came across Rust for Linux thank to Asahi Lina's M1 GPU driver development streams. i went through a quick tutorial, and voilĂ , there was a driver: rust_foxes.

Two Years Later

Now, two years later almost, what has changed?

First, the Rust for Linux project deprecated the rust branch everyone used to experiment. It was full of abstractions for many, many things (including miscellaneous devices, user IO, etc), but its development had not followed the regular process of (painstakingly) posting things on the LKMLs, and people reviewing them.

So almost everything was basically thrown into the trash. What is coming now is definitely an opportunity to do better (eg. properly handling multiple memory allocators, properly defining memory allocation flags, a better structure for the alloc crate, etc).

That also means that the driver no longer really works unless you build against the old rust branch from back in the days of 6.0... Well it would have, if i had rebased my code. Even the API for simple modules changed between me writing the module, and when rust was abandoned.

For example, consider Module::init, the entrypoint of a module. In my code it is:

fn init(_name: &'static CStr, _module: &'static ThisModule) -> Result<Self> {

The current definition used in both rust and rust-next (the up-to-date branch used to push RFL's updates to Linus):

fn init(_module: &'static ThisModule) -> Result<Self> {

Thankfully, during the last two years, i have spent a big part of my time working on drivers using RFL's tree, including making abstractions.

Update Time

Going through the code, as someone who now has 2 more years of experience not only writing drivers for RFL but also, crucially, abstractions, i listed several things in need of update:

  • The Module trait
  • Missing miscdev abstractions
  • Missing io_buffer abstractions

For the missing abstractions, i dove in the code of the old rust branch and pulled out the big chunks of code my driver had previously relied on. Then, i brushed up very little details to update the code.

As an example, consider code that turns a Result into an errno code. RFL's rust had a macro for that purpose, which is now replaced by kernel::error::from_result.

In a similar fashion, everything that allocates now needs specific memory allocation flags to be passed so that the proper call is made and gfp is not always assumed.

i trimmed the code down to the minimum needed to open and read the device:

  • Abstractions for struct file_operations
  • Abstractions to copy and write from userland
  • Abstractions for IO buffers
  • Abstractions for simple devices
  • Abstractions to register a miscdevice

The set of changes totals +1240 lines.

Now, onto the driver

After fixing imports and the problem of Module trait implementation, i decided to flex a little by adding a new feature to the driver.

Previously, the driver would mindlessly write foxes one after the other, ad infinitum. That was funny, but i can do better.

The trait for file operations has two associated types. One of these types represents a data type passed to the handler of the open() event. The precise instance is stored in memory when we request the registration:

let reg = miscdev::Options::new()
    .mode(0o444)
    .register_new(kernel::fmt!("foxes"), ())?;

In this example, and in my current version of the driver, that type is ().

A second associated type tells the file operations what type is passed for all callbacks within the same open/release session. That is, the instance is created during the open callback, and destroyed at release. A borrowed version of the data is then passed for all callbacks... including read.

The read callback is the only one i implement, and the only one i need. Ironically, i use it to write in the buffer the user provided. Previously, that writing filled the buffer given by the user as much as possible, including with partial foxes (first few bytes of an incomplete UTF-8 emoji). Now, with the session data type set to Box<AtomicUsize>, i obtained a &AtomicUsize on all callbacks, and could now play with it.

Importantly, all core atomic scalar types can be modified through a non-mutable reference. That was all i was going to get anyways:

#[vtable]
impl Operations for FoxDev {
    type Data = Box<AtomicUsize>;

    fn open(_: &(), _file: &File) -> Result<Self::Data> {
        Ok(Box::new(AtomicUsize::new(200), GFP_KERNEL)?)
    }

    fn read(
        data: &AtomicUsize,
        _file: &File,
        writer: &mut impl IoBufferWriter,
        offset: u64,
    ) -> Result<usize> {
        //...
    }
}

The really simple part was changing the writing code so that it checks the current count remaining, limits itself to that value (or not if the buffer is small), and then subtracts the number of foxes fully written. Once the count is exhausted, and a round of read returns 0 bytes read, your program will likely call close().

With that code in place, all calls to cat /dev/foxes, for example, will show the same number of foxes before exiting.

In a subsequent modification (one i have not pushed to the repository), i even made it so the count became global to all different reading attempts. In order to do that, i wrapped a AtomicUsize in an Arc (Atomic Referenced Counter), and used that as a data type for the open callback, at which point i cloned the Arc and used it for all callbacks as well. The count was decreased in a similar way, except this time it was global to multiple parallel calls.

Conclusion

i underwent this little restoration project as an attempt to not lose my mind writing a more complex, fully-fledged driver for RFL. The cute little character device that prints foxes is a cute example and the first example of code i used in the project, so i would like it to remain up-to-date, and useful to people who want to get into the project.

Reflecting quickly on my (almost) two years of practice, i realized that while i gained a certain amount of skepticism regarding the capacity of the project to onboard newcomers to the kernel (myself included), i gained a lot of knowledge of how Linux works internally, but also how FFI in Rust interacts with it, and how deep primitives like cells, tools like Arc or Box, and regular code can work together. It's been a journey, and while i hope to take a break from more serious endeavors around RFL, i will definitely keep tinkering.