Автор: Чжан Ханьдун
Введение
Лаборатория безопасности системного программного обеспечения в Технологическом институте ДжорджииОткрытый исходный кодRudra
, за анализ и сообщение о потенциальной безопасности памяти и уязвимостях в коде Unsafe Rust, для которого они также опубликуют соответствующий документ в материалах 28-го семинара ACM по принципам операционной системы в 2021 году, который в настоящее время доступен в репозитории исходного кода Rudra.скачать.
Примечание. Эта статья не является переводом статьи, а является моим обзором и кратким изложением статьи.
резюме
Язык Rust фокусируется на безопасности памяти и производительности.Rust широко используется в традиционном системном программном обеспечении, таком как операционные системы, встроенные системы, сетевые фреймворки, браузеры и т. д. В этих областях безопасность и производительность незаменимы.
Идея безопасности памяти Rust заключается в проверке владения памятью во время компиляции, в частности доступа и времени жизни выделенных в памяти объектов. Общие и эксклюзивные ссылки компилятора Rust на значения обеспечивают две гарантии посредством проверки заимствования:
- Время жизни ссылки не может быть больше, чем время жизни ее переменной-владельца. Чтобы избежать использования после освобождения (UAF).
- Общие и эксклюзивные ссылки не могут существовать одновременно, что устраняет риск одновременного чтения и записи одного и того же значения.
К сожалению, эти правила безопасности слишком строги. Когда кому-то нужно вызвать базовую аппаратную систему или получить лучшую производительность, необходимо временно обойти правила безопасности. Эти требования не могут быть решены с помощью Safe Rust, но необходимы для разработки системы, поэтому вводится Unsafe Rust. Небезопасный Rust означает, что ответственность за проверку безопасности компилятором временно делегирована программисту.
Надежность кода Unsafe Rust имеет решающее значение для безопасности памяти всей программы, потому что большая часть системного программного обеспечения, такого как операционная система или стандартная библиотека, неотделима от него.
Некоторые люди могут наивно полагать, что Unsafe Rust может устранить свои риски, просто просмотрев исходный код. Суть дела, однако, в том, что рассуждения о правильности очень деликатны и подвержены ошибкам по трем причинам:
- Ошибки работоспособности нарушают границы безопасности Rust, а это означает, что весь внешний код, включая стандартную библиотеку, должен быть вменяемым.
- Безопасный и небезопасный код взаимозависимы.
- Все невидимые пути кода, вставленные компилятором, должны быть должным образом обоснованы программистом.
Чтобы дать Rust прочную основу, было проведено множество исследовательских проектов, таких как формализация системы типов и операционной семантики, проверка их правильности и построение моделей для проверки. Они очень важны, но недостаточно практичны, поскольку не охватывают всю экосистему. Существуют также некоторые динамические методы, такие как фаззинг Miri и Fuzz, но эти методы нелегко использовать в больших масштабах, поскольку они требуют больших вычислительных ресурсов.
В настоящее время язык Rust становится популярным, и пакет Unsafe Rust постепенно увеличивается. Поэтому важно разработать практический алгоритм для определения безопасности памяти.
В этом документе представлены три важных шаблона ошибок, представлен небезопасный код и предоставлены такие инструменты, как Rudra. Есть три вклада в работу авторов этой статьи:
- Были выявлены три шаблона ошибок в Unsafe Rust и разработаны два новых алгоритма для их поиска.
- 263 новые уязвимости безопасности памяти обнаружены в экосистеме Rust с помощью Rudra. Это составляет 41,4% всех ошибок в RustSec с 2016 года.
- Открытый исходный код. Rudra имеет открытый исходный код, и мы планируем внести его основные алгоритмы в официальный линтер Rust.
Rudra
Rudra
Используется для анализа и сообщения о потенциальных уязвимостях безопасности памяти в коде Unsafe Rust. Поскольку ошибки в небезопасном коде угрожают основам гарантий безопасности Rust,Rudra
Основное внимание уделяется распространению нашего анализа на репозитории реестра пакетов Rust (такие какcrates.io
) для всех программ и библиотек, размещенных в .Rudra
Весь реестр можно просканировать за 6,5 часов (43k
package) и выявил 263 ранее неизвестные уязвимости безопасности памяти, 98 отправленныхRustSec
Объявления и 74 CVE
, что составляет общее количество зарегистрированных с 2016 г.RustSec
41,4% всех уязвимостей.
Rudra
Новые обнаруженные уязвимости незаметны, и они существуют в библиотеках экспертов Rust: две вstd
библиотека, одна в официальнойfutures
библиотека, одна в компиляторе Rustrustc
середина.Rudra
Имеет открытый исходный код и планирует интегрировать свои алгоритмы в официальный линтер Rust.
Рудра, имя происходит от санскрита, переводится как Рутро (или Рудра), бог бурь, охоты, смерти и природы в индуистской мифологии. Когда он в гневе, он без разбора причиняет боль людям и животным, также он хорош в лечении людей травами. Его название означает «рев» или «рев» (вероятно, ураган или буря).
Rudra
а такжеMiri
Разница:
Rudra
Это статический анализ, который анализирует исходный код без выполнения.Miri
является интерпретатором и должен выполнять код.Оба могут использоваться в комбинации.
О небезопасной ржавчине
потому чтоunsafe
Наличие ключевых слов открывает интересную область дизайна API: как сообщать о безопасности API.
Обычно есть два пути:
- Внутренний небезопасный API напрямую доступен пользователям API, но использование ключевого слова unsafe для объявления API небезопасным также требует добавления аннотации границы безопасности.
- Безопасно инкапсулируйте API (абстракция безопасности), то есть используйте внутренние утверждения, чтобы гарантировать доступность Panic при пересечении границы безопасности, тем самым избегая генерации UB.
Второй подход, безопасная абстракция, которая скрывает фактор небезопасности за безопасным API, стал общепринятым в сообществе Rust.
Разделение безопасных и небезопасных позволяет нам различать, кто несет ответственность за нарушения безопасности. Safe Rust означает, что неопределенное поведение не может быть вызвано никакими средствами. Другими словами, задача Safe API состоит в том, чтобы гарантировать, что любой допустимый ввод не нарушит ожидаемое поведение небезопасного кода, инкапсулированного внутри.
Это резко контрастирует с C или C++, где ответственность за правильное использование API лежит на пользователе.
Например, вlibc
серединаprintf()
, когда он ошибается, вызывая неправильный указатель, никто не будет его винить. Однако эта проблема привела к ряду проблем с безопасностью памяти: уязвимость строки формата. Помните ошибку, из-за которой телефоны Apple блокировались добавлением специального имени Wifi?
В то время как в Rustprintln!()
Это не должно и не может вызывать segfault. Кроме того, если ввод вызывает segfault, то это считается ошибкой разработчика API.
Определение ошибок безопасности памяти в Rust
В Rust есть два типа определений Unsafe: небезопасные функции и небезопасные трейты.
Небезопасная функция предполагает, что вызывающий объект будет в безопасности при вызове функции.
Черта Unsafe предполагает предоставление дополнительных семантических гарантий при реализации черты. например стандартная библиотекаpub unsafe trait TrustedLen: Iterator { }
, требование признака должно проверитьIterator::size_hint()
Верхняя граница , может гарантироватьTrustedLen
Выраженная семантика "доверенной длины".
В документе дается четкое и последовательное определение ошибок безопасности памяти, а не операционной семантики Rust:
Определение 1: Тип и значение определяются обычным образом. Тип — это набор значений.
Определение 2: для типа Т,safe-value(T)
Определяется как ценность, которую можно безопасно создать. Например, строка в Rust внутренне представлена как массив байтов, но при создании через безопасный API она может содержать толькоUTF-8
Закодированное значение.
Определение 3: Функция F является приемником типаarg(F)
значение и возвращает типret(F)
значение . Для нескольких аргументов мы рассматриваем его как кортеж.
Определение 4: если вsafe-value(arg(F))
есть в коллекцииv
(записывается как:∃𝑣 ∈ safe-value(𝑎𝑟𝑔(𝐹))
), чтобы при вызовеF(v)
вызывает нарушение безопасности памяти или возвращает значение, не принадлежащее safe-value(𝑟𝑒𝑡(𝐹))
возвращаемое значение в коллекции𝑣𝑟𝑒𝑡
time (обозначается как: 𝑣𝑟𝑒𝑡 ∉ safe-value(𝑟𝑒𝑡(𝐹))), то функция F имеет дефект безопасности памяти.
Определение 5: для универсальной функцииΛ
,pred(Λ)
определяется как удовлетворяющаяΛ
Набор типов предикатов типов (относится к квалификации признаков). учитывая тип𝑇∈pred(Λ)
,resolve(Λ,𝑇)
Создайте универсальную функцию как конкретную функцию 𝐹.
Определение 6: если общая функцияΛ
может быть реализована как функция с недостатками безопасности памяти, т.е.∃𝑇 ∈ pred(Λ)
, так что𝐹=resolve(Λ,𝑇)
имеет дефект безопасности памяти, универсальная функция имеет дефект безопасности памяти.
Определение 7: если типSend
Реализации не могут передаваться через границы потоков, тогда тип имеет проблемы с безопасностью памяти.
Определение 8: если типSync
Реализация не может одновременно обращаться к типу через указатели с псевдонимами, тогда у нее возникают проблемы с безопасностью памяти. То есть определите не потокобезопасный метод, который получает&self
.
Три важных паттерна ошибок в Unsafe Rust
Посредством качественного анализа известных уязвимостей в статье обобщены три важных типа паттернов ошибок в Unsafe Rust:
- Panic Safety: Ошибки безопасности памяти, вызванные паникой.
- Инвариант безопасности более высокого порядка: ошибки, вызванные типами более высокого порядка без данных гарантий безопасности.
- Распространение отправки/синхронизации в универсальных типах
Send/Sync
Распространение): неверное руководство по универсальному внутреннему типуSend/Sync
Реализация вызывает дженерикиSend/Sync
Ошибки, вызванные неправильными ограничениями.
Panic Safety
Это похоже на концепцию безопасности исключений в других языках программирования, таких как C++. Концепция в Rust, похожая на Exception в других языках программирования, называется Panic. Паника обычно используется, когда программа достигает невосстановимого состояния, конечно, ее можно реализовать и на Rust.UnwindSafe
Тип черты ловит панику.
Когда возникает паника, запускается раскрутка стека, вызывается деструктор объекта, размещенного в стеке, и поток управления передается обработчику паники. Таким образом, когда происходит паника, будет вызван деструктор текущей выживающей переменной, что вызовет некоторые проблемы с безопасностью памяти, такие как освобождение памяти, которая уже была освобождена.
Но очень сложно и подвержено ошибкам правильно рассуждать о панической безопасности в небезопасном коде. Часто завернутый небезопасный код может временно обходить проверки владения, а безопасный завернутый API гарантирует, что он не нарушает правила безопасности, основанные на граничных условиях безопасности, прежде чем будет возвращено значение внутреннего небезопасного кода. Однако, если обернутый небезопасный код вызывает панику, его внешние проверки безопасности могут не выполняться. Это может вызвать проблемы с безопасностью памяти, такие как Uninitialized или Double Free в C/C++.
В документе это определяется как:
если функция𝐹
Drop
тип𝑇
значение𝑣
, так что𝑣
Во время расслабления𝑣 ∉ safe-value(𝑇)
, и приводит к нарушению безопасности памяти, указывая на то, что функция имеет уязвимость системы безопасности Panic.
// 标准库 `String::retain()` 曝出的 CVE-2020-36317 Panic safety bug
pub fn retain<F>(&mut self, mut f: F)
where
F: FnMut(char) -> bool
{
let len = self.len();
let mut del_bytes = 0;
let mut idx = 0;
unsafe { self.vec.set_len(0); } // + 修复bug 的代码
while idx < len {
let ch = unsafe {
self.get_unchecked(idx..len).chars().next().unwrap()
};
let ch_len = ch.len_utf8();
// self is left in an inconsistent state if f() panics
// 此处如果 f() 发生了恐慌,self 的长度就会不一致
if !f(ch) {
del_bytes += ch_len;
} else if del_bytes > 0 {
unsafe {
ptr::copy(self.vec.as_ptr().add(idx),
self.vec.as_mut_ptr().add(idx - del_bytes),
ch_len);
}
}
idx += ch_len; // point idx to the next char
}
unsafe { self.vec.set_len(len - del_bytes); } // + 修复bug 的代码 ,如果 while 里发生panic,则将返回长度设置为 0
}
fn main(){
// PoC: creates a non-utf-8 string in the unwinding path
// 此处传入一个 非 UTF-8 编码字符串引发恐慌
"0è0".to_string().retain(|_| {
match the_number_of_invocation() {
1 => false,
2 => true,
_ => panic!(),
}
});
}
Higher-order Safety Invariant
Функция должна безопасно выполнять все безопасные входные данные, включая типы данных параметров, параметры универсального типа и замыкания, переданные извне.
Другими словами, безопасная функция не должна предоставлять больше, чем безопасные инварианты, предоставляемые компилятором Rust. Так называемый безопасный инвариант относится к безопасной функции в Rust, в случае любого действительного ввода, не должно иметь какого-либо неопределенного поведения.
Например, функция сортировки в Rust не должна вызывать какое-либо неопределенное поведение, даже если предоставленные пользователем компараторы не следуют общему упорядочиванию и не будут давать сбои. А вот функция сортировки в Cpp, когда пользователь предоставляет компаратор, не совместимый с текущим, будет segfault.
Единственный безопасный инвариант, который Rust предоставляет для типов более высокого порядка, — это правильность подписи типа. Распространенной ошибкой, однако, является неверное предположение о функции, предоставляемой вызывающей стороной:
- Логическая непротиворечивость: например, функция сортировки следует отношению общего порядка.
- Чистота: всегда возвращайте один и тот же вывод для одного и того же ввода.
- Семантические ограничения: только для параметров, так как они могут содержать неинициализированные байты.
Для небезопасного кода вы должны проверить эти свойства самостоятельно или указать правильные ограничения (например, с чертой Unafe), чтобы вызывающая сторона была обязана проверять эти свойства.
Применение безопасных инвариантов для типов более высокого порядка затруднено в системе типов Rust. Например, передача неинициализированного буфера предоставленной вызывающей стороной реализации Read.
К сожалению, многие программисты на Rust оптимизируют производительность, предоставляя вызывающим функциям неинициализированный буфер, не подозревая о присущей ему ненадежности. Из-за своей вездесущности и тонкости стандартная библиотека Rust теперьконкретно определенный, вызываемый с неинициализированным буферомread()
Это нездоровое поведение само по себе.
В документе это определяется как:
Ошибки инвариантности более высокого порядка — это ошибки безопасности памяти в функциях, вызванные предположениями о том, что инвариантность более высокого порядка гарантирована, а система типов Rust не дает никаких гарантий относительно кода, предоставленного вызывающим кодом.
1 // CVE-2020-36323: a higher-order invariant bug in join()
2 fn join_generic_copy<B, T, S>(slice: &[S], sep: &[T]) -> Vec<T>
3 where T: Copy, B: AsRef<[T]> + ?Sized, S: Borrow<B>
4 {
5 let mut iter = slice.iter();
6
7 // `slice`is converted for the first time
8 // during the buffer size calculation.
9 let len = ...; // `slice` 在这里第一次被转换
10 let mut result = Vec::with_capacity(len);
11 ...
12 unsafe {
13 let pos = result.len();
14 let target = result.get_unchecked_mut(pos..len);
15
16 // `slice`is converted for the second time in macro
17 // while copying the rest of the components.
18 spezialize_for_lengths!(sep, target, iter; // `slice` 第二次被转换
19 0, 1, 2, 3, 4);
20
21 // Indicate that the vector is initialized
22 result.set_len(len);
23 }
24 result
25 }
26
27 // PoC: a benign join() can trigger a memory safety issue
28 impl Borrow<str> for InconsistentBorrow {
29 fn borrow(&self) -> &str {
30 if self.is_first_time() {
31 "123456"
32 } else {
33 "0"
34 }
35 }
36 }
37
38 let arr: [InconsistentBorrow; 3] = Default::default();
39 arr.join("-");
Этот кодBorrow<str>
Реализуйте функцию, вызываемую внутри метода соединения.join_generic_copy
дисплей. существуетjoin_generic_copy
Внутренне будетslice
сделать два преобразования, в то время как вspezialize_for_lengths!
Внутри макроса вызов.borrow()
метод, который возвращает строку неинициализированных байтов, если второе преобразование не совпадает с первым.
здесь, Borrow<B>
Является типом высокого порядка, находится внутриborrow
Непротиворечивость не гарантируется, он может вернуть другой фрагмент, если он не обработан, он, вероятно, предоставит вызывающей стороне неинициализированные байты.
Propagating Send/Sync in Generic Types
Что касается дженериков,Send/Sync
Правила станут очень сложными, как показано на рисунке:
как правилоSend/Sync
Он автоматически реализуется компилятором, но когда разработчики работают с Unsafe, им может потребоваться реализовать эти два свойства вручную. Ручная реализацияSend/Sync
Трудно получить право. один не понимаетSend/Sync
Разработчики, реализующие его вручную, могут легко внести ошибки в код.
В документе это определяется как:
Если дженерики реализуютсяSend/Sync
тип, если он указывает неверныйSend/Sync
ограничения, то общийSend/Sync
Ограничения становятся неверными. это в дженерикахSend/Sync
Распространение небезопасных ошибок.
1 // CVE-2020-35905: incorrect uses of Send/Sync on Rust's futures
2 pub struct MappedMutexGuard<'a, T: ?Sized, U: ?Sized> {
3 mutex: &'a Mutex<T>,
4 value: *mut U,
5 _marker: PhantomData<&'a mut U>, // + 修复代码
6 }
7
8 impl<'a, T: ?Sized> MutexGuard<'a, T> {
9 pub fn map<U: ?Sized, F>(this: Self, f: F)
10 -> MappedMutexGuard<'a, T, U>
11 where F: FnOnce(&mut T) -> &mut U {
12 let mutex = this.mutex;
13 let value = f(unsafe { &mut *this.mutex.value.get() });
14 mem::forget(this);
15 // MappedMutexGuard { mutex, value }
16 MappedMutexGuard { mutex, value, _marker: PhantomData } // + 修复代码
17 }
18 }
19
20 // unsafe impl<T: ?Sized + Send, U: ?Sized> Send
21 unsafe impl<T: ?Sized + Send, U: ?Sized + Send> Send // + 修复代码
22 for MappedMutexGuard<'_, T, U> {}
23 //unsafe impl<T: ?Sized + Sync, U: ?Sized> Sync
24 unsafe impl<T: ?Sized + Sync, U: ?Sized + Sync> Sync // + 修复代码
25 for MappedMutexGuard<'_, T, U> {}
26
27 // PoC: this safe Rust code allows race on reference counter
28 * MutexGuard::map(guard, |_| Box::leak(Box::new(Rc::new(true))));
Проблемы, обнаруженные в библиотеке фьючерсов Rust, неправильная ручная работаSend/Sync
Реализация нарушает гарантии безопасности потоков.
Затронутые версии,MappedMutexGuard
изSend/Sync
Реализация рассматривает толькоT
разница вMappedMutexGuard
отмененU
цитаты.
когдаMutexGuard::map()
Закрытие, используемое в, возвращает то же самое, что иT
несвязанныйU
, это может привести к гонкам данных в безопасном коде Rust.
Эта проблема исправленаSend/Sync
реализации, и вMappedMutexGuard
добавить типPhantomData<&'a mut U>
Отметьте, чтобы сообщить компилятору, что эта защита также находится поверх U.
Дизайн Рудры
Общий дизайн выглядит следующим образом:
РудраHIR
чтобы получить структуру кода ящика (включая определения трейтов, сигнатуры функций, небезопасные блоки и т. д.) с помощьюMIR
получить семантику кода (поток данных, граф потока управления, зависимости вызовов и т. д.). Почему бы не использовать LLVM IR? Потому что на этом уровне абстракции Rust уже нет.
потом через внутр.Unsafe Dataflow Checker (UD)
ПроверятьPanic Safety Bug
а такжеHigher-order Invariant Bug
,пройти черезSend/Sync Variance Checker(SV)
ПроверятьSend/Sync Variance Bug
. Наконец, результаты суммируются в выходном отчете по приоритету.
Unsafe Dataflow Checker (UD)
а такжеSend/Sync Variance Checker(SV)
Соответствующие двум наборам алгоритмов, пожалуйста, обратитесь к документу и коду для получения подробной информации.
Объяснение английских терминов, связанных с безопасностью
В английском языке есть несколько слов о безопасности, например Security and Safety, но в китайском есть только слово «безопасность». Итак, здесь необходимо пояснить:
- Безопасность, как правило, относится к информационной безопасности, сетевой безопасности и тому подобному.
- Безопасность, как правило, относится к функциональной безопасности.
Обычно проблемы информационной безопасности вызваны функциональными уязвимостями.
резюме
Заключительная глава статьи также содержит много данных для демонстрации эффектов Rudra, а также результаты тестов Rudra и Fuzz, Miri и других инструментов статического анализа Rust.
Приведенное выше изображение является результатом использования Rudra авторами статьи для проверки нескольких операционных систем, реализованных на Rust.Подробности см. в статье.
Эта статья достойна внимания и помогает нам по-настоящему понять философию безопасности Rust. В документе также представлен новый взгляд на состояние безопасности языка Rust, а также инструмент статической проверки, заслуживающий нашего внимания.