|
|
Log in / Subscribe / Register

Rust for embedded Linux kernels

By Jonathan Corbet
April 23, 2024

OSSNA
The Rust programming language, it is hoped, will bring a new level of safety to the Linux kernel. At the moment, though, there are still a number of impediments to getting useful Rust code into the kernel. In the Embedded Open Source Summit track of the Open Source Summit North America, Fabien Parent provided an overview of his work aimed at improving the infrastructure needed to write the device drivers needed by embedded systems in Rust; there is still some work to be done.

Parent started with the case for using Rust in the kernel; it may not be a proper justification, he said, but it is true that Rust is one of the most admired languages in use. C is about 50 years old and has not changed much since the C89 standard came out. It has the advantage of a simple syntax that is easy to learn, and it is efficient for writing low-level code. But C also makes it easy to write code containing undefined behavior and lacks memory-management features.

[Fabien Parent] Rust, instead, is about ten years old and has a new release every six weeks. It is harder to learn and forces developers to come up to speed on concepts like ownership and borrowing. But the code produced is efficient; Rust's abstractions are meant to be zero-cost, with the verification work done at compile time. Rust forces developers to handle errors, eliminating another frequent cause of bugs.

Thus, he said, it makes sense to use Rust in the kernel, hopefully leading to safer code overall. There is basic Rust support in the kernel now, but it is focused on driver code. There is currently no plan to support core-kernel code written in Rust, partly because the LLVM-based rustc compiler, which is the only viable compiler for Rust code currently, does not support all of the architectures that the kernel does. Rust support in the kernel is still considered to be experimental.

There are some drawbacks to using Rust in the kernel, starting with the current drivers-only policy. Most kernel vulnerabilities, he said, are not actually in driver code; instead, they appear in core code like networking and filesystems. As long as Rust is not usable there, it cannot help address these problems. Adding Rust, of course, will complicate the maintenance of the kernel, forcing maintainers to learn another language. The abstractions needed to interface Rust to the rest of the kernel are all new code, some of which may well contain bugs of its own.

Parent became interested in Rust after stumbling across a sample GPIO driver in Rust on LWN. He immediately started trying to write some kernel code in Rust, but failed soon thereafter. At this point, there simply is not a lot of kernel code that a new developer can use to learn from. So, instead, he went and rewrote all of his custom tools in Rust; after that, he was better prepared to work on the kernel.

There are, he said, a lot of people trying to contribute to the Rust-for-Linux effort; there is an online registry containing much of that work. But many of the basic abstractions needed for useful Rust code still are not in the mainline, and that is preventing others from making progress. The work that is seemingly advancing, including support for graphics drivers, Android's binder, and filesystems like PuzzleFS are not useful for the embedded work that Parent is interested in. Most of this work has been done on x86 systems, with the exception of the Apple M1 GPU driver. Many of the key abstractions needed for embedded work are missing from the kernel; many of those exist, but they are often unmaintained.

Parent had a long list of requirements for embedded systems, starting with support for the Rust language on 64-bit Arm systems; that, at least, has been merged for the upcoming 6.9 kernel release. Many abstractions for subsystems like clocks, pin control, run-time power management, regulators, and so on are not yet there. The abstractions have proved to be a challenge; maintainers will not merge code that is not used elsewhere in the kernel, but drivers cannot be merged until the abstractions are there. That leads to a situation where a lot of people are involved, each of whom are waiting on pieces from the others. That makes it hard to get the pieces upstream.

Parent's objective is to write simple drivers with minimal dependencies, each of which can be used to get a small number of abstractions upstream. He gave as an example a regulator driver that needs a relatively small set of abstractions, including those for platform drivers, regulators, regmap, I2C drivers, and Open Firmware for probing. He will be trying to get that set upstream; from there, work can proceed to more complex drivers.

The (conspicuously undocumented) regmap interface was called out for how it can showcase the advantages of Rust. Regmap eases access to devices that export an array of registers for configuration and operation. The Rust regmap abstraction allows the provision of a type-safe interface, built on top of the regmap_field API, that is generated with some "macro magic". The type checking allows the interface to ensure that register operations use the correct data types with each register, catching a number of common errors.

Parent's next step is to upstream a lot of this work, a task that, he acknowledges, will be difficult. But, if nothing else, he has learned a few lessons, starting with the fact that abstractions are more complex than one might expect, and they will have bugs. One problematic area is in ownership of resources; that is going to be hard to nail down for as long as there are extensive interfaces between the Rust and C sides. He advised other Rust developers to not try to write complete abstractions at the outset; instead, only the parts that are actually needed should be implemented.

Linked lists, a famous point of difficulty for Rust in general, present a special hazard in kernel code. The Rust compiler likes to move data around as a program runs; if that data happens to be a structure containing linked-list pointers, moving it will break the list and create hard-to-find bugs. Adding a list_head structure to an existing C structure can, as a result, break a Rust abstraction built on that structure in ways that are hard to detect automatically. The way he talked about this problem suggested a certain amount of hard-earned experience.

Even so, he summarized, writing kernel code in Rust makes a lot of things easier. Error handling is much more straightforward, and the compiler can ensure that developers have handled all possible values. Driver code tends to be a lot shorter and, he said, if the code compiles, it is likely to work.

[Thanks to the Linux Foundation, LWN's travel sponsor, for supporting our travel to this event.]

Index entries for this article
KernelDevelopment tools/Rust
ConferenceOpen Source Summit North America/2024


to post comments

Rust for embedded Linux kernels

Posted Apr 23, 2024 18:12 UTC (Tue) by AClwn (subscriber, #131323) [Link] (4 responses)

The Rust compiler likes to move data around as a program runs; if that data happens to be a structure containing linked-list pointers, moving it will break the list and create hard-to-find bugs. Adding a list_head structure to an existing C structure can, as a result, break a Rust abstraction built on that structure in ways that are hard to detect automatically.

I would be interested in reading more detail about this. My high-level understanding of Rust is that it will refuse to compile when presented with code that could cause memory errors, but this text makes it sound as though Rust will actually introduce additional bugs by unsafely moving memory around because it doesn't understand pointers. What exactly happens when you add the aforementioned list_head structure? Do you get a compile failure or runtime bugs?

Rust for embedded Linux kernels

Posted Apr 23, 2024 18:47 UTC (Tue) by mb (subscriber, #50428) [Link]

>My high-level understanding of Rust is that it will refuse to compile when presented with code that could cause memory errors

Yes. But that is only true for Rust code outside of unsafe {} blocks. (All C code essentially is inside of an unsafe block)
Inside of unsafe blocks the unsafe code has to ensure that the requirements of Rust's safe code are upheld.

Therefore, the Rust<->C bindings have to ensure that the requirements on both sides are upheld.
If the C side struct cannot be moved, because for example it contains list_head, the Rust-C-binding needs to pin that structure so the safe Rust code can't move it. If it tries to, it will *then* get a compile error.

But if the C code didn't have list_head before, it was movable and there was no need to pin it. Adding a list_head can therefore break safe Rust code, if the wrapper was not prepared due to the missing pinning.

Rust for embedded Linux kernels

Posted Apr 23, 2024 19:30 UTC (Tue) by atnot (guest, #124910) [Link] (2 responses)

The first thing to understand is that this isn't really a Rust problem itself, it also happens under C. For example if you pass or assign a struct that contains a list_head by value, that may introduce a copy and make those list_heads invalid, which is likely to lead to some sort of undesired behavior like use-after-free down the line.

The differences are that while in C, passing larger structs by value is generally frowned upon, rusts rough equivalent, move (which like passing by value in C may or may not actually copy memory), is used very commonly as it is the only way to transfer full ownership of memory to other code. Also, while in C the API only needs to be safe when used right, in Rust it needs to be safe under all circumstances. And finally while in C passing by value leaves the original copy available, in rust it becomes invalidated, making any reference to it immediate UB.

In pure Rust, this isn't a problem, because data may only be moved if it has not been borrowed, i.e. there cannot be any references to it elsewhere. Any API that lets you get ownership access to data that has pointers to it is immediately unsound.

The problem is that these rules don't apply to C code. When bindgen runs on your struct, it will just faithfully map all of those C pointers into an equivalent Rust struct. And Rust will say, well, these are just pointers, we can just move them around like in C. But it can't know that this struct is not safe to pass by value, because that is implicit. You have no idea what the other code relies upon. This is kind of the inherent danger that comes with FFI, and the reason why it's unsafe.

Rust for embedded Linux kernels

Posted Apr 24, 2024 12:13 UTC (Wed) by josh (subscriber, #17465) [Link]

> rusts rough equivalent, move (which like passing by value in C may or may not actually copy memory), is used very commonly as it is the only way to transfer full ownership of memory to other code

It's the only way when you have data on the stack. Data in the heap can have ownership passed around by pointer, and Rust does this all the time (e.g. Box and Vec and many other data structures). But if you have data on the *stack* and you want to pass full ownership around, you have to do it by copying.

You can safely pass around *references* to data on the stack, and Rust will guarantee that that data doesn't outlive the stack frame it came from.

Rust for embedded Linux kernels

Posted Apr 25, 2024 13:38 UTC (Thu) by khim (subscriber, #9252) [Link]

> When bindgen runs on your struct, it will just faithfully map all of those C pointers into an equivalent Rust struct.

Sounds like a problem of tooling to me. Why couldn't bindgen notice that there are list_head structure inside, add PhantomPinned marker and make sure that C ABI uses Pin<Foo> everywhere and not straight Pin<Foo>?

Then addition of list_head would still be ABI breakage, but it would be a compile-time ABI breakage and these are relatively easy to handle.


Copyright © 2024, Eklektix, Inc.
This article may be redistributed under the terms of the Creative Commons CC BY-SA 4.0 license
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds