Rust - популярный язык программирования благодаря тому, что в нем особое внимание уделяется безопасности и надежности, а также тому, что он по умолчанию применяет лучшие практики. Несмотря на все эти удивительные особенности, он не идеален - как и любой другой язык, - и логика вашего кода иногда может вызывать неожиданное поведение.
Можно написать синтаксически корректный, но по своей сути баговый код. Например, может произойти ничего не подозревающее арифметическое переполнение. Вот тут-то и пригодятся инструменты автоматической проверки кода.
Автоматическая верификация - это техника, которая помогает доказать, что программа удовлетворяет определенным свойствам, таким как безопасность памяти и отсутствие ошибок во время выполнения. Кроме того, инструменты автоматической верификации позволяют проверять корректность параллельного кода, который бывает сложно протестировать вручную.
Автоматическая верификация особенно важна для Rust, поскольку она может помочь гарантировать, что небезопасный код используется правильно. В этом руководстве мы рассмотрим пять лучших инструментов проверки Rust - без особого порядка - и то, как они могут помочь вам создать более надежное программное обеспечение.
Помните, что, хотя сообщество Rust активно работает над развитием и улучшением инструментов автоматической верификации Rust, область инструментов автоматической верификации в целом все еще находится в процессе развития.
Ознакомьтесь со сравнительной таблицей в конце статьи, чтобы узнать, какие из этих инструментов готовы к производству на момент написания статьи.
cargo-fuzz
Первый инструмент, который мы рассмотрим, cargo-fuzz, использует технику, называемую fuzzing, для автоматизированного тестирования программного обеспечения. Предоставляя программе множество правильных, почти правильных или неправильных входных данных, fuzzing может помочь разработчикам найти нежелательное поведение или уязвимости.
Когда мы пишем тесты, мы обычно учитываем только несколько входов и пишем тесты, основываясь на том, как, по нашему мнению, будет реагировать система. Такой подход может привести к пропуску ошибок, особенно тех, которые вызваны неожиданными или неправильно сформированными входными данными.
Фаззинг может помочь вам найти эти пропущенные ошибки, предоставляя программе широкий спектр входных данных, включая недопустимые и неожиданные. Если программа дает сбой или ведет себя необычно в ответ на один из этих входов, это сигнализирует о наличии ошибки.
Крейт Rust cargo-fuzz может помочь вам в фазз-тестировании кода Rust. Он работает, генерируя случайные входные данные и подавая их в функцию, которую вы хотите протестировать. Если функция паникует или терпит крах, cargo-fuzz сохранит входные данные, которые вызвали сбой.
Вот пример того, как использовать cargo-fuzz для фазз-тестирования функции Rust:
#[macro_use]
extern crate libfuzzer_sys;
fuzz_target!(|data: &[u8]| {
let json_string = std::str::from_utf8(data).unwrap();
let _ = serde_json::from_str::<serde_json::Value>(&json_string).unwrap();
});
Приведенный выше код тестирует парсер JSON, подавая ему случайные входные данные. Цель fuzz_target будет вызываться до тех пор, пока не встретит входной сигнал, который вызовет панику и приведет к краху.
В отчете о сбое сообщается, где и почему произошла паника. Например:
Из приведенного отчета видно, что паника произошла, когда программа попыталась вызвать функцию Result::unwrap() на значении Error. Она запаниковала со следующей ошибкой:
Error("EOF while parsing a value", line: 1, column: 0);
Эта информация поможет вам провести дальнейшее расследование, чтобы определить, не вызвана ли ошибка плохим вводом, который ваш код не смог обработать. Благодаря cargo-fuzz у вас будет возможность исправить это.
Не забывайте, что некоторые из ошибок, которые вы обнаружите с помощью фаззинга, могут оказаться непрактичными или неприменимыми в реальной жизни, а значит, фаззинг может генерировать ложные срабатывания. Кроме того, фаззинг может быть ресурсоемким, особенно если речь идет о большой или сложной кодовой базе.
Кани
Kani - это современный инструмент автоматической проверки кода, который поможет вам проверить корректность вашего Rust-кода за считанные секунды. Он использует технику, называемую проверкой модели, которая позволяет исследовать все возможные и невозможные состояния программы, включая состояния, недостижимые при обычном выполнении.
Проверка моделей позволяет Kani обнаружить проблемы в вашем коде, которые могут быть вызваны логикой с непредвиденными последствиями. Вы также можете использовать Kani для выявления других проблем, которые трудно или невозможно обнаружить с помощью других методов модульного тестирования, интеграционного тестирования или даже ручного тестирования.
Рассмотрим приведенный ниже код:
fn product(a: i32, b: i32) -> i32 { a * b }
Вы согласитесь со мной, что приведенный выше код - это правильный код на языке Rust, верно? Посмотрите на него еще раз - можете ли вы найти что-нибудь, что может пойти не так в этом коде?
Давайте воспользуемся Kani, чтобы выяснить это:
fn product(num1: i32, num2: i32) -> i32 {
num1 * num2
}
#[kani::proof]
fn main() {
let num1 = kani::any();
let num2 = kani::any();
let result = product(num1, num2);
}
В приведенном выше коде мы использовали Kani для доказательствачто код корректен и в нем нет ошибок. Кани даст нам следующий ответ:
Все вроде бы правильно, за исключением того, что Kani обнаруживает возможность переполнения в процессе умножения.
Это происходит потому, что функция product не гарантирует, что мы не превысим максимальное значение i32, которое составляет 2,147,483,647 - все, что больше этого значения, приведет к ошибке. По сути, все, для чего будет использоваться эта функция, не сможет работать с числами, превышающими два миллиарда.
В этом случае использование Kani для выявления этой потенциальной проблемы позволяет либо сразу изменить тип данных, либо оставить все как есть и правильно обработать ошибку, если это ожидаемое поведение.
Как и любой другой инструмент, Kani имеет свои ограничения. К счастью, эти ограничения достаточно хорошо задокументированы. Они варьируются от неопределенного поведения до некоторых неподдерживаемых функций в Rust.
Proptest
Proptest позволяет проверить свойства функции с помощью множества допустимых и недопустимых входных данных, чтобы найти ошибки. Это отличается от классических методов тестирования, таких как модульное тестирование, где вы указываете некоторые входные данные и добавляете утверждения, основанные на ожидаемом поведении.
Тестирование свойств - это форма fuzz-тестирования, которая более контролируема и сосредоточена на проверке конкретных свойств. Это делает его хорошим выбором для тестирования сложных систем, где традиционное фазз-тестирование может быть слишком медленным или неэффективным.
Давайте рассмотрим, как можно использовать крейт Rust Proptest:
use proptest::prelude::{any, proptest};
fn add_two_numbers(first_number: i32, second_number: i32) -> i32 {
first_number + second_number
}
proptest! {
#[test]
fn test_add_two_numbers(first_number in any::<i32>(), second_number in any::<i32>()) {
let expected = first_number + second_number;
let actual = add_two_numbers(first_number, second_number);
assert_eq!(actual, expected);
}
}
В приведенном выше коде мы тестируем простую функцию, которая складывает два числа. Что может пойти не так с такой простой функцией?
Давайте посмотрим на сигнатуру функции test_add_two_numbers:
fn test_add_two_numbers(first_number in any::<i32>(), second_number in any::<i32>())
Стратегия any::<i32>() - это стратегия Proptest, которая генерирует случайные значения i32, как допустимые, так и недопустимые. Это позволяет нам тестировать функцию add_two_numbers() с широким диапазоном входных данных, включая крайние случаи и аномалии.
Тестовая функция Proptest будет генерировать большое количество случайных входов для параметров первое_число и второе_число.
Для каждого входа тест утверждает, что фактический вывод функции add_two_numbers() равен ожидаемому. Если какой-либо из тестов не сработает, Proptest выведет в консоль неудачные входы.
Вот пример того, как Proptest может генерировать входные данные для функции test_add_two_numbers():
(10, 20) (-100, 100) (1234567890, 9876543210) (0, 0) (-1, -1) (std::i32::MIN, std::i32::MAX)
Если тест не работает с определенными входными данными, Proptest также уменьшит входные данные. Например, если тест провалился с входом (10, 20), Proptest может повторить попытку с входом (5, 10) или даже (1, 2). Это помогает определить наименьший возможный вход, который приводит к неудаче теста.
Когда мы запускаем тест, мы получаем отчет о неудачном тестировании, как показано ниже:
Отчет показывает, что существует вероятность переполнения. Он также показывает минимальный воспроизводимый ввод. С этой информацией мы можем приступить к исправлению ошибки.
Хотя тестирование свойств может работать очень хорошо для определенного диапазона входных данных, иногда оно может пропустить некоторые крайние случаи и дать вам ложноположительный или ложноотрицательный результат. Другими словами, он может галлюцинировать ошибки там, где их на самом деле нет, или не найти ошибку за пределами заданного покрытия.
Rust Klee
KLEE - это механизм символьного выполнения, который позволяет интеллектуально исследовать все пути кода в вашей программе для обнаружения уязвимостей или ошибок. Он построен на основе инфраструктуры компилятора LLVM, который написан на языках C и C++.
Как следствие, большинство реализаций KLEE также написаны на C и C++. Однако фундаментальные концепции KLEE могут быть реализованы на любом языке программирования.
Rust Klee - это реализация KLEE на языке Rust с открытым исходным кодом. Как и другие инструменты верификации, о которых мы рассказывали в этом руководстве, Rust Klee был разработан для проверки определенных свойств. В частности, согласно сообщению в блоге автора пилотного проекта, он был разработан для:
Проверки безопасности Инварианты Параметризованные проверки Проверка функциональной корректности программ на Rust
Это делает его более целенаправленным и эффективным, чем некоторые другие инструменты верификации, которые могут быть более универсальными.универсального назначения.
Однако в той же записи в блоге пилота автор предупреждает, что целью его проекта никогда не было ”создание системы верификации, готовой к интеграции в любой проект”. Фактически, репозиторий не обновлялся уже четыре года, и, похоже, дальнейшее развитие не планируется.
Скорее, целью автора Rust Klee было исследовать, можно ли применить инструменты верификации Rust, основанные на KLEE, к коду Rust-for-Linux для проверки на наличие ошибок. Цель проекта - дать представление о потенциальных проблемах и о том, что нужно сделать, чтобы другие могли использовать инструменты формальной верификации для проверки аналогичного кода.
В итоге, хотя Rust Klee еще не готов к использованию в производстве, он все же заслуживает внимания как классный инструмент, который может помочь сформировать ландшафт формальной верификации в экосистеме Rust.
Haybale
Haybale - это также движок символьного исполнения, имеющий схожие с Rust Klee возможности, за исключением того, что Haybale полностью написан на Rust и основан на Rust LLVM IR под капотом.
Хотя он не пытается стать еще одним Rust Klee, он все же обладает схожими функциями. Как механизм символьного исполнения, он фокусируется на преобразовании всех переменных вашей программы в математические выражения и анализирует пути выполнения для обнаружения ошибок или уязвимостей.
Самое лучшее в Haybale - это то, что он тестирует ваш Rust-код как есть, без добавления дополнительного тестового кода. Конечно, даже если мы все согласны с тем, что написание тестов - это лучшее, что можно сделать, это все равно считается скучной и утомительной задачей.
Вот пример из документации, который проверяет, вернет ли функция foo на выходе ноль:
match find_zero_of_function("foo", &project, Config::default(), None) {
Ok(None) => {
println!("foo никогда не может вернуть 0");
}
Ok(Some(inputs)) => {
println!("Inputs for which foo returns 0: {:?}", inputs);
}
Err(e) => {
panic!("{}", e);
}
}
Haybale может рассуждать обо всем коде, находить ошибки и выдавать отчет, который поможет вам доказать, есть ли в вашем коде ошибки, а затем воспроизвести и исправить их. Хотя Haybale может отловить не все ошибки, он, скорее всего, обнаружит критические ошибки, которые могут привести к сбоям во время выполнения, и даст вам шанс исправить их.
Klee и Haybale имеют свои ограничения, как и любой другой инструмент автоматической проверки. Например, Klee может не найти ошибок в исполняемых файлах, использующих сложные абстракции.
Как уже говорилось, механизмы символьного исполнения работают, исследуя все возможные пути через код программы, чтобы найти входные данные или условия, которые могут привести к неожиданному поведению или ошибкам. Однако программа, использующая сложные абстракции, такие как сложные структуры данных или сложные шаблоны проектирования, может создавать множество возможных путей выполнения.
Эта сложность может перегрузить такие инструменты, как Klee или Haybale, и затруднить эффективный анализ всех возможных сценариев. В результате они могут сдаться в процессе работы. В этом случае лучше всего использовать ручные или традиционные методы тестирования для проверки этих участков кода.
Давайте рассмотрим пять инструментов проверки Rust, которые мы обсудили в этом руководстве, в следующем сравнительном обзоре:
Инструмент Популярность (звезды GitHub) Поддержка сообщества Лучше всего использовать для… Совместимость версий Готов ли инструмент к производству?
cargo-fuzz 1.3k Активен Поиск ошибок, вызванных неожиданными или неправильно сформированными входными данными Rust 1.45 и выше ✅
Kani 1.4k Активно Находит ошибки, вызванные логикой с непредвиденными последствиями, и другие проблемы, которые трудно или невозможно обнаружить другими методами тестирования Rust 1.60 и выше ✅
Proptest 1.4k Активное тестирование свойств функции с большим количеством допустимых и недопустимых входов для поиска ошибок Rust 1.47 и выше ✅
Rust Klee 3 Неактивен Проверка специфических свойств программ на Rust, таких как инварианты безопасности и функциональная корректность Rust 1.47 и выше ❌
Haybale 455 Не очень активно Тестирование поведения программы с использованием символьных значений, а не известных входных данных Rust 1.47 и выше Haybale все еще находится в разработке, но уже используется некоторыми проектами для проверки своего кода.
Помните, что инструменты для проверки Rust находятся на ранней стадии развития, и со временем их будет становиться все больше.
Заключение
Инструменты автоматической верификации очень важны для обнаружения ошибок при разработке программного обеспечения, хотя, возможно, они еще не получили широкого распространения среди разработчиков. Эти инструменты могут помочь вам обнаружить ошибки, которые вы не смогли бы найти с помощью классических методов тестирования, и повысить надежность вашего кода.
Счастливого хакинга!