In an earlier post , Google announced that Android now supports the Rust programming language used in the development of the OS itself. In this regard, the authors of this publication also decided to evaluate how much the Rust language is in demand in the development of the Linux kernel. This post covers the technical aspects of this work with a few simple examples.
For nearly half a century, C has remained the primary language for kernel development, because C provides the degree of control and predictable performance required for such a critical component. The density of memory security bugs in the Linux kernel is usually very low because the code is very high quality, the code reviews are to strict standards, and safeguards are carefully implemented. However, bugs related to memory safety still crop up regularly . In Android, kernel vulnerabilities are generally considered a serious flaw, as they sometimes allow the security model to be bypassed due to the kernel running in privileged mode.
Rust is supposed to have matured enough to co-operate with C as the language for the practical implementation of the kernel. Rust helps to reduce potential bugs and security vulnerabilities in privileged code, while integrating well with the core core and maintaining good performance.
Rust support
A primary prototype of the Binder driver was developed to adequately compare the security and performance characteristics of the existing C version and its Rust counterpart. There are over 30 million lines of code in the Linux kernel, so we do not set ourselves the goal of rewriting it entirely in Rust, but to provide the ability to add the required code in Rust. We believe this incremental approach helps leverage the high-performance implementation in the kernel, while providing kernel developers with new tools to improve memory safety and maintain performance at runtime.
We joined the Rust for Linux organization where the community has done and continues to do a lot to add Rust support to the Linux kernel build system. We also need to design systems so that code fragments written in two languages ββcan interact with each other: we are especially interested in safe abstractions without overhead that allow Rust code to use core functionality written in C, as well as the ability to implement functionality in idiomatic Rust, which can be called smoothly from parts of the kernel written in C.
Since Rust is a new language in the core, we also have the ability to oblige developers to adhere to best practices for documentation and consistency. For example, we have specific machine-verifiable requirements regarding the use of unsafe code: for each unsafe function, the developer must document the requirements that the caller must satisfy - thus ensuring that it is safe to use. In addition, each time an unsafe function is called, it is the developer's responsibility to document the requirements that must be met by the caller as a guarantee that use will be safe. In addition, for each call to unsafe functions (or use of unsafe constructs, for example,when dereferencing a raw pointer), the developer should document the rationale why doing so is safe.
Rust is famous not only for its security, but also for how useful and user-friendly it is for developers. Next, let's look at a few examples that demonstrate how Rust can be useful for kernel developers when writing safe and correct drivers.
Driver example
Consider the implementation of a semaphore symbolic device. Each device has an actual value; when writing n bytes, the device value is increased by n ; with each read, this value is decreased by 1 until the value reaches 0, in which case this device is blocked until such a decrement operation can be performed on it without going below 0.
Let's say
semaphore
this is a file representing our device. We can interact with it from the shell like this:
> cat semaphore
When
semaphore
is the device that has just been initialized, the command shown above is locked because the current device value is 0. It will be unlocked if we run the next command from another shell, as it will increment the value by 1, thereby allowing the original operation readout to complete:
> echo -n a > semaphore
We can also increase the counter by more than 1 if we write more data, for example:
> echo -n abc > semaphore
increases the counter by 3, so the next 3 readings will not block.
To demonstrate some more aspects of Rust, let's add the following features to our driver: remember what the maximum value reached during the entire life cycle, and also remember how many reads each file performed on the device.
Now let's show how such a driver would be implemented in Rust , comparing this option with the implementation in C.... However, we note that the development of this topic at Google is just beginning, and in the future everything may change. We would like to highlight how Rust can be useful to a developer in every aspect. For example, at compile time, it allows us to eliminate or greatly reduce the likelihood of entire classes of bugs creeping into the code, while keeping the code flexible and running at minimal overhead.
Character devices
The developer needs to do the following to implement the driver for the new character device in Rust:
- Implement a trait
FileOperations
: All functions associated with it are optional, so the developer only needs to implement those that are relevant for the given scenario. They correspond to fields in the C structurestruct file_operations
. - Implement a trait
FileOpener
is the type-safe equivalent of a C fieldopen
from a structstruct file_operations
. - Register a new device type for the kernel: this will tell the kernel what functions will need to be called in response to operations on files of a new type.
The following is a comparison of the first two steps in our first example in Rust and C:
Character devices in Rust have a number of security benefits:
- File-by-file lifecycle state management:
FileOpener::open
Returns an object whose lifetime since then is owned by the caller. Any object that implements the trait can be returnedPointerWrapper
, and we provide implementations forBox <T>
andArc <T>
, , Rust, , .
FileOperations
self
( ),release
, ( ).release
, - , . «» ( , ).
, Rust , C. C , Rust, , Rust, , , . , C, Rust , Rust. , open , , , ,ioctl
/read
/write
( ) ,filp->private_data
, ..
- : ,
open
release
self
, , Rust , .
( ), :Mutex
SpinLock
( atomics) .
, ( ), ( ).
: , , Rust . , , ,FileOperation::open
.Arc, .
, FileOperation
( , ,open
,FileOperations
) β .
, , . , Cmiscdevice
,filp->private_data
;cdev
,inode->i_cdev
. , ,container_of
, . Rust .
: , Rust , . . C , , (void
*) : , , . Rust .
: ,FileOperations
, . ,impl FileOperations for Device
,Device
β , (FileState
). , , , . (neovim
LSP-rust-analyzer
.)
Rust, , C,struct file_operations
. (declare_file_operations
): , , ,const
, , .
Ioctl
ioctl,ioctl
,FileOperations
, .
Ioctl , , , , , (, , , , ) . Rust (cmd.dispatch
), .
. , , ioctl, Rust :cmd.raw
ioctl ( , ).
, , , - , :
- , .
- , , , TOCTOU ( ). , , . , Rust .
- : , , ioctl.
IOCTL_GET_READ_COUNT
UserSlicePtrWriter
,sizeof(u64)
, ioctl. - : ioctl , , . ,
UserSlicePtrWriter
UserSlicePtrReader
.
C, ( , ) ; Rustunsafe
, . Rust:
. ; , C Rust , , , , :
, C, , «» , unix , , .
Rust:
-
Semaphore::inner
, ,lock
. , . C, ,count
max_seen
semaphore_state
, . there is no enforcement that the lock is held while they're accessed. - (RAII): , (
inner
) . , : , , , , ; : , , ,drop
. - ,
Lock
, , ,Mutex
SpinLock
, , C, . , , , . - Rust , . , , . C
semaphore_consume
Linux: , ,mutex_unlock
prepare_to_wait
, . - : , , , , , . , ioctl , , . Rust , . , C,
atomic64_t
, , .
,open
,read
write
:
Rust:
-
?
operator:open
read
Rust ; , , , . C , - . - : Rust , , - . C .
open
, , Ckref_get
( ); Rustclone
( ), . - RAII: Rust , ,
inner
, , . - : Rust , .
write
, , . C , , .
.
10% !