Введение в статью | Rudra: поиск ошибок безопасности памяти в экосистеме Rust

задняя часть Rust

Автор: Чжан Ханьдун


Введение

Лаборатория безопасности системного программного обеспечения в Технологическом институте ДжорджииОткрытый исходный кодRudra, за анализ и сообщение о потенциальной безопасности памяти и уязвимостях в коде Unsafe Rust, для которого они также опубликуют соответствующий документ в материалах 28-го семинара ACM по принципам операционной системы в 2021 году, который в настоящее время доступен в репозитории исходного кода Rudra.скачать.

Примечание. Эта статья не является переводом статьи, а является моим обзором и кратким изложением статьи.

резюме

Язык Rust фокусируется на безопасности памяти и производительности.Rust широко используется в традиционном системном программном обеспечении, таком как операционные системы, встроенные системы, сетевые фреймворки, браузеры и т. д. В этих областях безопасность и производительность незаменимы.

Идея безопасности памяти Rust заключается в проверке владения памятью во время компиляции, в частности доступа и времени жизни выделенных в памяти объектов. Общие и эксклюзивные ссылки компилятора Rust на значения обеспечивают две гарантии посредством проверки заимствования:

  1. Время жизни ссылки не может быть больше, чем время жизни ее переменной-владельца. Чтобы избежать использования после освобождения (UAF).
  2. Общие и эксклюзивные ссылки не могут существовать одновременно, что устраняет риск одновременного чтения и записи одного и того же значения.

К сожалению, эти правила безопасности слишком строги. Когда кому-то нужно вызвать базовую аппаратную систему или получить лучшую производительность, необходимо временно обойти правила безопасности. Эти требования не могут быть решены с помощью Safe Rust, но необходимы для разработки системы, поэтому вводится Unsafe Rust. Небезопасный Rust означает, что ответственность за проверку безопасности компилятором временно делегирована программисту.

Надежность кода Unsafe Rust имеет решающее значение для безопасности памяти всей программы, потому что большая часть системного программного обеспечения, такого как операционная система или стандартная библиотека, неотделима от него.

Некоторые люди могут наивно полагать, что Unsafe Rust может устранить свои риски, просто просмотрев исходный код. Суть дела, однако, в том, что рассуждения о правильности очень деликатны и подвержены ошибкам по трем причинам:

  1. Ошибки работоспособности нарушают границы безопасности Rust, а это означает, что весь внешний код, включая стандартную библиотеку, должен быть вменяемым.
  2. Безопасный и небезопасный код взаимозависимы.
  3. Все невидимые пути кода, вставленные компилятором, должны быть должным образом обоснованы программистом.

Чтобы дать Rust прочную основу, было проведено множество исследовательских проектов, таких как формализация системы типов и операционной семантики, проверка их правильности и построение моделей для проверки. Они очень важны, но недостаточно практичны, поскольку не охватывают всю экосистему. Существуют также некоторые динамические методы, такие как фаззинг Miri и Fuzz, но эти методы нелегко использовать в больших масштабах, поскольку они требуют больших вычислительных ресурсов.

В настоящее время язык Rust становится популярным, и пакет Unsafe Rust постепенно увеличивается. Поэтому важно разработать практический алгоритм для определения безопасности памяти.

В этом документе представлены три важных шаблона ошибок, представлен небезопасный код и предоставлены такие инструменты, как Rudra. Есть три вклада в работу авторов этой статьи:

  1. Были выявлены три шаблона ошибок в Unsafe Rust и разработаны два новых алгоритма для их поиска.
  2. 263 новые уязвимости безопасности памяти обнаружены в экосистеме Rust с помощью Rudra. Это составляет 41,4% всех ошибок в RustSec с 2016 года.
  3. Открытый исходный код. Rudra имеет открытый исходный код, и мы планируем внести его основные алгоритмы в официальный линтер Rust.

Rudra

RudraИспользуется для анализа и сообщения о потенциальных уязвимостях безопасности памяти в коде Unsafe Rust. Поскольку ошибки в небезопасном коде угрожают основам гарантий безопасности Rust,RudraОсновное внимание уделяется распространению нашего анализа на репозитории реестра пакетов Rust (такие какcrates.io) для всех программ и библиотек, размещенных в .RudraВесь реестр можно просканировать за 6,5 часов (43kpackage) и выявил 263 ранее неизвестные уязвимости безопасности памяти, 98 отправленныхRustSecОбъявления и 74 CVE, что составляет общее количество зарегистрированных с 2016 г.RustSec41,4% всех уязвимостей.

image.png

RudraНовые обнаруженные уязвимости незаметны, и они существуют в библиотеках экспертов Rust: две вstdбиблиотека, одна в официальнойfuturesбиблиотека, одна в компиляторе Rustrustcсередина.RudraИмеет открытый исходный код и планирует интегрировать свои алгоритмы в официальный линтер Rust.

Рудра, имя происходит от санскрита, переводится как Рутро (или Рудра), бог бурь, охоты, смерти и природы в индуистской мифологии. Когда он в гневе, он без разбора причиняет боль людям и животным, также он хорош в лечении людей травами. Его название означает «рев» или «рев» (вероятно, ураган или буря).

Rudraа такжеMiriРазница:

RudraЭто статический анализ, который анализирует исходный код без выполнения.Miriявляется интерпретатором и должен выполнять код.

Оба могут использоваться в комбинации.

О небезопасной ржавчине

потому чтоunsafeНаличие ключевых слов открывает интересную область дизайна API: как сообщать о безопасности API.

Обычно есть два пути:

  1. Внутренний небезопасный API напрямую доступен пользователям API, но использование ключевого слова unsafe для объявления API небезопасным также требует добавления аннотации границы безопасности.
  2. Безопасно инкапсулируйте 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:

  1. Panic Safety: Ошибки безопасности памяти, вызванные паникой.
  2. Инвариант безопасности более высокого порядка: ошибки, вызванные типами более высокого порядка без данных гарантий безопасности.
  3. Распространение отправки/синхронизации в универсальных типах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 предоставляет для типов более высокого порядка, — это правильность подписи типа. Распространенной ошибкой, однако, является неверное предположение о функции, предоставляемой вызывающей стороной:

  1. Логическая непротиворечивость: например, функция сортировки следует отношению общего порядка.
  2. Чистота: всегда возвращайте один и тот же вывод для одного и того же ввода.
  3. Семантические ограничения: только для параметров, так как они могут содержать неинициализированные байты.

Для небезопасного кода вы должны проверить эти свойства самостоятельно или указать правильные ограничения (например, с чертой 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Правила станут очень сложными, как показано на рисунке:

image.pngкак правило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.

Дизайн Рудры

Общий дизайн выглядит следующим образом:

image.png

Рудра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, но в китайском есть только слово «безопасность». Итак, здесь необходимо пояснить:

  1. Безопасность, как правило, относится к информационной безопасности, сетевой безопасности и тому подобному.
  2. Безопасность, как правило, относится к функциональной безопасности.

Обычно проблемы информационной безопасности вызваны функциональными уязвимостями.

резюме

Заключительная глава статьи также содержит много данных для демонстрации эффектов Rudra, а также результаты тестов Rudra и Fuzz, Miri и других инструментов статического анализа Rust.

image.pngПриведенное выше изображение является результатом использования Rudra авторами статьи для проверки нескольких операционных систем, реализованных на Rust.Подробности см. в статье.

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