![Rust и Linux - 1 Rust и Linux - 1](https://www.pvsm.ru/images/2022/06/14/Rust-i-Linux.png)
Во время прошлогодней Linux Plumbers Conference 2021 один из мейнтейнеров, Мигель Охеда, задался вопросом: нужен ли сообществу Rust в коде ядра Linux и что нужно для того, чтобы соответствующие патчи были приняты в древе проекта? Комментарии от разработчиков были в основном доброжелательными, но без фанатизма. Лидер проекта Линус Торвальдс сказал, что не против т․ н․ пилотной серии патчей на Rust, с оговоркой, что и остальные разработчики должны рассматривать их в качестве опытной партии.
Тут уместно вспомнить, что ядро Linux вероятно один из самых масштабных проектов с открытым исходным кодом и самый успешный, учитывая пройденный путь за более, чем 30 лет после опубликования версии ядра 0.01. Всё это время разработка велась и ведётся поныне на языке программирования C. Линус Торвальдс без ума от C и не раз высказывался в том духе, что от добра добра не ищут, и все остальные ЯП непригодны для разработки ядра.
Мне нравится разбираться с железом и для этой цели C нет равных.
I like interacting with hardware from a software perspective. And I have yet to see a language that comes even close to C…When I read C, I know what the assembly language will look like.
Линус Торвальдс 2012
Быстрый, низкоуровневый и традиционно один из самых востребованных языков программирования, разве C нужны дополнительные подпорки в коде ядра? Что может предложить Rust такого, чтобы вся затея в итоге оправдала себя? Если в двух словах, то всё дело в НП, то есть в неопределённом поведении, характерном для некоторых типичных сценариев в C. Такое поведение зачастую оборачивается ошибками в коде и скрытыми уязвимостями, в то время как Rust архитектурно защищён от НП и связанных с ним проблем.
Согласно последнему рабочему документу C, неопределённое поведение возникает при использовании ошибочной программной конструкции, или данных, и данный документ для таких сценариев не предъявляет никаких требований. Примером такого рода является поведение при разыменовании нулевого указателя. Такого же поведения можно добиться, если значение первого оператора равно INT_MIN
, или второго оператора — равно 0.
int f(int a, int b) {
return a / b;
}
Для того чтобы исправить НП, нужно задать условия выхода.
int f(int a, int b) {
if (b == 0)
abort();
if (a == INT_MIN && b == -1)
abort();
return a / b;
}
Неопределенное поведение проявляется в нарушениях безопасного использования памяти, например, к ошибкам связанным с переполнением буфера, чтением, или записи за пределами буфера, использование освобождённой памяти (use-after-free) и др. Из недавних примеров можно вспомнить уязвимость записи за пределами буфера WannaCry. Туда же следует отнести Stagefright на ОС Android. Анализ 0-day дыр безопасности компании Гугл показал, что 80% из них вызваны нарушением безопасного доступа к памяти. Ниже на картинке ещё один результат fuzzing-проверки по разным проектам.
![Rust и Linux - 2 Rust и Linux - 2](https://www.pvsm.ru/images/2022/06/14/Rust-i-Linux-2.png)
Figure 1. Соотношение по дырам безопасности в проектах на разных языках программирования.
Чтобы не быть голословными, рассмотрим на примере:
#include <stdlib.h>
int main(void)
{
int * const a = malloc(sizeof(int));
if (a == NULL)
abort();
*a = 42;
free(a);
free(a);
}
Несмотря на явную оплошность с двойным освобождением памяти, компилятор не прерывает работу. Таким образом, программа может быть запущена, хоть и завершится с ошибкой.
|13:59:54|adm@redeye:[~]> gcc -g -Wall -std=c99 -o test test.c
|13:59:59|adm@redeye:[~]> echo $?
0
|14:00:05|adm@redeye:[~]> ./test
free(): double free detected in tcache 2
Aborted
Проверим теперь поведение точно такого же кода на Rust.
pub fn main() {
let a = Box::new(42);
drop(a);
println!("{}", *a);
}
На этапе компиляции программа выдаст ошибку из-за того, что память в переменной была высвобождена. Текст содержит описание и ссылку с кодом ошибки.
rustc app.rs
error[E0382]: borrow of moved value: a
--> app.rs:4:17
|
2 | let a = Box::new(42);
| - move occurs because a has type std::boxed::Box<i32>, which does not implement the Copy trait
3 | drop(a);
| - value moved here
4 | println!("{}", *a);
| ^^ value borrowed here after move
error: aborting due to previous error
For more information about this error, try rustc --explain E0382.rustc app.rs
Преимущества Rust не ограничиваются безопасным доступом к памяти, есть ряд других полезных свойств, которые могли бы облегчить труд разработчиков ядра Linux. Взять хотя бы инструментарий для управления зависимостями. Много-ли тех, кому по душе сражаться с include путями в заголовочных файлах, раз за разом запускать pkg-config
вручную, либо через макросы Autotools, полагаться на то, что пользователь установит нужные версии библиотек? Разве не проще записать всё необходимое в файл Cargo.toml
, перечислив в нём названия и версии всех зависимостей? При запуске cargo build
они автоматически подтянутся из реестра пакетов crates.io.
В ядро Linux код попадает не сразу, а после тщательной проверки качества и соответствия внутренним стандартам. Исключения крайне редки, а это подразумевает необходимость часто тестировать программу на наличие возможных ошибок и дефектов. Сложность, или неудобство связанные с тестированием кода напрямую будут сказываться на результате работы. Так уж получилось, что язык С по сегодняшним меркам неважно приспособлен для всестороннего тестирования по ряду причин.
- Немалые трудности представляет обработка сценариев с внутренними статическими функциями. Их можно вызвать лишь в самом файле, где они определены. Для того, чтобы до них добраться извне, нужно писать
#include
директивы, либо же использовать условия#ifdef
. - Для того чтобы слинковать часть зависимостей с тестовой программой необходимо творчески редактировать
Makefile
, илиCMakeLists.txt
. - Нужно выбрать из множества фреймворков какой-то один, либо несколько самых популярных. Придётся их освоить, дабы уметь интегрировать свой проект и запускать автоматические проверки.
И всего этого можно избежать, написав в Rust:
#[test]
fn test_foo_prime() {
assert!(foo() == expected_result);
}
Вместе с тем, явная ошибка считать, что применение Rust в коде Linux лишено недостатков. Во-первых, одним дистиллированно безопасным кодом ядро написать не выйдет, во всяком случае, таковы реалии Linux. Иногда нужно переступить через порог безопасности, например, при статическом считывании и вычислении адресов регистров CPU.
Во-вторых, из-за дополнительных рантайм проверок кое-где могут возникнуть проблемы с производительностью и с высокой степенью вероятности это будут именно те редкие фрагменты кода, где сложно соответствовать принятым стандартам безопасности.
И наконец в третьих нельзя просто так взять и переписать код на другом ЯП из-за очевидных и неизбежных организационных проблем.
Rust в ядре, как это выглядит?
Так или иначе, Rust получил зелёный свет, пока что в ранге экспериментальной поддержки. Отправной точкой станет использование нового языка программирования при написании драйверов, если этот будет целесообразно. В частности, некоторые GPIO драйвера уже пишут на Rust. Использование Rust в стеке WiFi и Bluetooth драйверов также может пойти на пользу делу по мнению мейнтейнера kernel.org Kees Cook.
Если пройти по ссылке, то можно заметить, что код на Rust несколько компактней, но возможно тут немного срезаны углы и принятый стиль разработки Linux нарушен в плане игнорирования длин строк, несоблюдения конвенций наименования переменных и пр. Однако, если приглядеться поближе, есть и более существенные отличия.
writeb(pl061->csave_regs.gpio_is, pl061->base + GPIOIS);
writeb(pl061->csave_regs.gpio_ibe, pl061->base + GPIOIBE);
writeb(pl061->csave_regs.gpio_iev, pl061->base + GPIOIEV);
writeb(pl061->csave_regs.gpio_ie, pl061->base + GPIOIE);
В этом фрагменте C кода происходит расчёт вручную некоего адреса внутри функции writeb
и если сравнить с аналогичным фрагментом на Rust, то можно заметить, что там нет лазейки для произвольной записи в память за рамками смещения.
pl061.base.writeb(inner.csave_regs.gpio_is, GPIOIS);
pl061.base.writeb(inner.csave_regs.gpio_ibe, GPIOIBE);
pl061.base.writeb(inner.csave_regs.gpio_iev, GPIOIEV);
pl061.base.writeb(inner.csave_regs.gpio_ie, GPIOIE);
Документация проекта находится по адресу на Гитхабе. Сейчас ссылки на заголовочные include
файлы C не работают. Rust имеет доступ к условной компиляции на основе конфигурации ядра.
#[cfg(CONFIG_X)] // CONFIG_X активен (y or m)
#[cfg(CONFIG_X="y")] // CONFIG_X активен и является встроенным (y)
#[cfg(CONFIG_X="m")] // CONFIG_X активен является модулем (m)
#[cfg(not(CONFIG_X))] // CONFIG_X не активен
На данный момент интеграция нового языка программирования выглядит так. Название kernel crate
не должно пугать, это не реализация ядра на Rust, а всего лишь реализация необходимых абстракций. Прикладное средство bindgen
является по сути парсером, который автоматически создаёт привязки для заголовочных файлов C. Bindgen
считывает заголовки C и из них пишет соответствующие функции на Rust.
![Rust и Linux - 3 Rust и Linux - 3](https://www.pvsm.ru/images/2022/06/14/Rust-i-Linux-3.png)
Figure 2. Rust в структуре каталогов ядра Linux
Так выглядит реализация драйверов Linux на Rust. Если идти справа налево, то в начале находится уже знакомый нам обработчик привязок C bindgen
, правее и за кадром уже чистый и без примесей C код Linux-ядра. Далее следует kernel crate
с требуемыми абстракциями, впрочем, это может быть какой-нибудь другой crate, или даже crates. Принципиальный момент заключается в том, что драйвер my_foo
может использовать только безопасные абстракции из kernel crate
. Драйвер не может напрямую обращаться к C-функциям. Благодаря такой двухступенчатой схеме подсистема обеспечивает безопасность кода Rust в Linux.
![Rust и Linux - 4 Rust и Linux - 4](https://www.pvsm.ru/images/2022/06/14/Rust-i-Linux-4.png)
Figure 3. Принцип работы драйверов Rust
Поддержка реализована для следующих платформ.
- arm (только armv6);
- arm64;
- powerpc (только ppc64le);
- riscv (только riscv64);
- x86_64.
7 патчей за 8 месяцев
В начале мая Мигель Охеда представил коллегам уже седьмую серию патчей для разработки Rust-драйверов, из которых первая была опубликована без номера версии, в статусе RFC. Таким образом это считается Patch v6. Проект получает финансирование со стороны Internet Security Research Group и компании Гугл. Несмотря на экспериментальный статус поддержка Rust уже позволяет разработчикам создавать слои абстракций для различных подсистем, работать над новыми драйверами и модулями. Список нестабильных функций и запросов все ещё внушительный, но работа над ним активно ведётся.
В этой серии патчей были следующие изменения.
▍ Инфраструктурные обновления
- Инструментарий вместе с библиотекой
alloc
обновлены до версии Rust 1.60. - Rust имеет такую примечательную функциональность, как тестируемая документация. Работает это следующим способом. Программист вставляет в комментарии примеры кода с помощью разметки Markdown, а
rustdoc
умеет их запускать, как обычный тест. Это очень удобно, так как можно показывать, как используется данная функция и одновременно тестировать её.
/// /// fn foo() {} /// println!("Hello, World!"); ///
До Patch v6 нельзя было запускать тестируемую документацию с использованием API ядра, с новым патчем это стало возможным. Документация из kernel crate во время компиляции преобразуется в KUnit тесты и выполняется при загрузке ядра.
- В соответствии с новыми требованиями в тесты не должны завершаться предупреждениями линтера Clippy.
- В Rust подсистеме GCC
rustc_codegen_gcc
добавлена новая функциональность по самозагрузке компилятора. Это означает, что его можно использовать для сборки самого компилятораrustc
. Кроме того, в GCC 12.1 включены исправления, необходимые дляlibgccjit
.
▍ Абстракции и драйвера
- Начальная поддержка сетевого стека в рамках модуля
net
. - Методы асинхронного программирования Rust можно использовать в ограниченных средах, включая ядро. В последнем патче появилась поддержка async в коде модуля
kasync
. Благодаря этому можно, например, написать асинхронный TCP сокет для ядра.
async fn echo_server(stream: TcpStream) -> Result {
let mut buf = [0u8; 1024];
loop {
let n = stream.read(&mut buf).await?;
if n == 0 {
return Ok(());
}
stream.write_all(&buf[..n]).await?;
}
}
- Реализована поддержка фильтра сетевых пакетов
net::filter
и связанного с ним образцаrust_netfilter.rs
. - Добавлен простой мютекс
mutex::Mutex
, не требующий привязки. Это довольно удобно, не смотря на то, что по функционалу мютекс уступает своему аналогу на C. - Новый механизм блокировки
NoWaitLock
, который в соответствии с названием, никогда не приводит к ситуации ожидания ресурса. Если ресурс занят другим потоком, ядром CPU, то попытка блокировки завершится ошибкой, а не остановкой вызывающего. - Ещё одна блокировка
RawSpiLock
, на основе C-эквивалентаraw_spinlock_t
, предназначена для фрагментов кода, где приостановка абсолютно недопустима. - Для тех объектов, по отношению к которым всегда подсчитывается количество ссылок (a. k. a.
always-refcounted
), создан новый типARef
. Его область применения — облегчить определение надстроек существующих C-структур.
▍ Дополнительные материалы
Автор: Микаел Григорян