- PVSM.RU - https://www.pvsm.ru -
Во время прошлогодней Linux Plumbers Conference 2021 [1] один из мейнтейнеров, Мигель Охеда [2], задался вопросом: нужен ли сообществу Rust в коде ядра Linux и что нужно для того, чтобы соответствующие патчи были приняты в древе проекта? Комментарии от разработчиков были в основном доброжелательными, но без фанатизма. Лидер проекта Линус Торвальдс сказал, что не против т․ н․ пилотной серии патчей на Rust, с оговоркой, что и остальные разработчики должны рассматривать их в качестве опытной партии.
Тут уместно вспомнить, что ядро Linux вероятно один из самых масштабных проектов с открытым исходным кодом и самый успешный, учитывая пройденный путь за более, чем 30 лет после опубликования версии ядра 0.01. Всё это время разработка велась и ведётся поныне на языке программирования 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
Согласно последнему рабочему документу [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-проверки по разным проектам.
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 получил зелёный свет, пока что в ранге экспериментальной поддержки. Отправной точкой станет использование нового языка программирования при написании драйверов, если этот будет целесообразно. В частности, некоторые 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.
Figure 2. Rust в структуре каталогов ядра Linux
Так выглядит реализация драйверов Linux на Rust. Если идти справа налево, то в начале находится уже знакомый нам обработчик привязок C bindgen
, правее и за кадром уже чистый и без примесей C код Linux-ядра. Далее следует kernel crate
с требуемыми абстракциями, впрочем, это может быть какой-нибудь другой crate, или даже crates. Принципиальный момент заключается в том, что драйвер my_foo
может использовать только безопасные абстракции из kernel crate
. Драйвер не может напрямую обращаться к C-функциям. Благодаря такой двухступенчатой схеме подсистема обеспечивает безопасность кода Rust в Linux.
Figure 3. Принцип работы драйверов Rust
Поддержка реализована для следующих платформ.
В начале мая Мигель Охеда представил [11] коллегам уже седьмую серию патчей для разработки Rust-драйверов, из которых первая была опубликована без номера версии, в статусе RFC. Таким образом это считается Patch v6. Проект получает финансирование со стороны Internet Security Research Group и компании Гугл. Несмотря на экспериментальный статус поддержка Rust уже позволяет разработчикам создавать слои абстракций для различных подсистем, работать над новыми драйверами и модулями. Список [12] нестабильных функций и запросов все ещё внушительный, но работа над ним активно ведётся.
В этой серии патчей были следующие изменения.
alloc
обновлены до версии Rust 1.60.rustdoc
умеет их запускать, как обычный тест. Это очень удобно, так как можно показывать, как используется данная функция и одновременно тестировать её./// /// fn foo() {} /// println!("Hello, World!"); ///
До Patch v6 нельзя было запускать тестируемую документацию с использованием API ядра, с новым патчем это стало возможным. Документация из kernel crate во время компиляции преобразуется в KUnit тесты и выполняется при загрузке ядра.
rustc_codegen_gcc
добавлена новая функциональность по самозагрузке компилятора. Это означает, что его можно использовать для сборки самого компилятора rustc
. Кроме того, в GCC 12.1 включены исправления, необходимые для libgccjit
.net
.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
, предназначена для фрагментов кода, где приостановка абсолютно недопустима.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
Нажмите здесь для печати.