- PVSM.RU - https://www.pvsm.ru -

Rust и Linux

Rust и Linux - 1


Во время прошлогодней Linux Plumbers Conference 2021 [1] один из мейнтейнеров, Мигель Охеда [2], задался вопросом: нужен ли сообществу 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 архитектурно защищён от НП и связанных с ним проблем.

Согласно последнему рабочему документу [3] 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 [4]. Туда же следует отнести Stagefright [5] на ОС Android. Анализ 0-day дыр безопасности компании Гугл показал [6], что 80% из них вызваны нарушением безопасного доступа к памяти. Ниже на картинке ещё один результат fuzzing-проверки по разным проектам.

Rust и Linux - 2

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 [7].

В ядро Linux код попадает не сразу, а после тщательной проверки качества и соответствия внутренним стандартам. Исключения крайне редки, а это подразумевает необходимость часто тестировать программу на наличие возможных ошибок и дефектов. Сложность, или неудобство связанные с тестированием кода напрямую будут сказываться на результате работы. Так уж получилось, что язык С по сегодняшним меркам неважно приспособлен для всестороннего тестирования по ряду причин.

  • Немалые трудности представляет обработка сценариев с внутренними статическими функциями. Их можно вызвать лишь в самом файле, где они определены. Для того, чтобы до них добраться извне, нужно писать #include директивы, либо же использовать условия #ifdef.
  • Для того чтобы слинковать часть зависимостей с тестовой программой необходимо творчески редактировать Makefile, или CMakeLists.txt.
  • Нужно выбрать из множества фреймворков какой-то один, либо несколько самых популярных. Придётся их освоить, дабы уметь интегрировать свой проект и запускать автоматические проверки.

И всего этого можно избежать, написав в Rust:

#[test]
fn test_foo_prime() {
assert!(foo() == expected_result);
}

Вместе с тем, явная ошибка считать, что применение Rust в коде Linux лишено недостатков. Во-первых, одним дистиллированно безопасным кодом ядро написать не выйдет, во всяком случае, таковы реалии Linux. Иногда нужно переступить через порог безопасности, например, при статическом считывании и вычислении адресов регистров CPU.

Во-вторых, из-за дополнительных рантайм проверок кое-где могут возникнуть проблемы с производительностью и с высокой степенью вероятности это будут именно те редкие фрагменты кода, где сложно соответствовать принятым стандартам безопасности.

И наконец в третьих нельзя просто так взять и переписать код на другом ЯП из-за очевидных и неизбежных организационных проблем.

Rust в ядре, как это выглядит?

Так или иначе, Rust получил зелёный свет, пока что в ранге экспериментальной поддержки. Отправной точкой станет использование нового языка программирования при написании драйверов, если этот будет целесообразно. В частности, некоторые GPIO драйвера [8] уже пишут на Rust. Использование Rust в стеке WiFi и Bluetooth драйверов также может пойти на пользу делу по мнению мейнтейнера kernel.org [9] 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);

Документация проекта находится по адресу [10] на Гитхабе. Сейчас ссылки на заголовочные 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

Figure 2. Rust в структуре каталогов ядра Linux

Так выглядит реализация драйверов Linux на Rust. Если идти справа налево, то в начале находится уже знакомый нам обработчик привязок C bindgen, правее и за кадром уже чистый и без примесей C код Linux-ядра. Далее следует kernel crate с требуемыми абстракциями, впрочем, это может быть какой-нибудь другой crate, или даже crates. Принципиальный момент заключается в том, что драйвер my_foo может использовать только безопасные абстракции из kernel crate. Драйвер не может напрямую обращаться к C-функциям. Благодаря такой двухступенчатой схеме подсистема обеспечивает безопасность кода Rust в Linux.

Rust и Linux - 4

Figure 3. Принцип работы драйверов Rust

Поддержка реализована для следующих платформ.

  • arm (только armv6);
  • arm64;
  • powerpc (только ppc64le);
  • riscv (только riscv64);
  • x86_64.

7 патчей за 8 месяцев

В начале мая Мигель Охеда представил [11] коллегам уже седьмую серию патчей для разработки Rust-драйверов, из которых первая была опубликована без номера версии, в статусе RFC. Таким образом это считается Patch v6. Проект получает финансирование со стороны Internet Security Research Group и компании Гугл. Несмотря на экспериментальный статус поддержка Rust уже позволяет разработчикам создавать слои абстракций для различных подсистем, работать над новыми драйверами и модулями. Список [12] нестабильных функций и запросов все ещё внушительный, но работа над ним активно ведётся.

В этой серии патчей были следующие изменения.

▍ Инфраструктурные обновления

  • Инструментарий вместе с библиотекой alloc обновлены до версии Rust 1.60.
  • Rust имеет такую примечательную функциональность, как тестируемая документация [13]. Работает это следующим способом. Программист вставляет в комментарии примеры кода с помощью разметки 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-структур.

▍ Дополнительные материалы

Автор: Микаел Григорян

Источник [17]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/open-source/376001

Ссылки в тексте:

[1] Linux Plumbers Conference 2021: https://lpc.events/event/11/

[2] Мигель Охеда: https://ojeda.dev/

[3] последнему рабочему документу: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2596.pdf

[4] WannaCry: https://www.mandiant.com/resources/wannacry-malware-profile

[5] Stagefright: https://googleprojectzero.blogspot.com/2015/09/stagefrightened.html

[6] показал: https://twitter.com/LazyFishBarrel/status/1129000965741404160

[7] crates.io: https://crates.io/

[8] GPIO драйвера: https://lwn.net/Articles/863459/

[9] kernel.org: http://kernel.org/

[10] по адресу: https://rust-for-linux.github.io/docs/kernel/

[11] представил: https://lwn.net/Articles/894258/

[12] Список: https://github.com/Rust-for-Linux/linux/issues/2

[13] тестируемая документация: https://doc.rust-lang.org/rustdoc/documentation-tests.html

[14] Шестая версия патчей для ядра Linux с поддержкой языка Rust: https://www.opennet.ru/opennews/art.shtml?num=57153

[15] Rustaceans at the border: https://lwn.net/Articles/889924/

[16] Using Rust for kernel development: https://lwn.net/Articles/870555/

[17] Источник: https://habr.com/ru/post/670748/?utm_source=habrahabr&utm_medium=rss&utm_campaign=670748