flawopen.com/Incidents/rust-unsafe-smallvec-overflow-january-2021
CVE-2021-25900: переполнение буфера в smallvec
SmallVec::insert_many доверял нижней границе size_hint; если итератор выдавал больше, происходило переполнение буфера. Исправлено в 0.6.14 и 1.6.1.
Склад славится безопасностью: робот проверяет каждую полку, прежде чем на неё что-то поставят. Для некоторых задач робота выключают, и человек работает вручную, обещая быть аккуратным. Один такой работник спросил водителя: «Сколько примерно коробок везёте?», собрал полку такого размера и начал ставить. Оценка была догадкой, а не гарантией, и когда коробок оказалось больше обещанного, стопка вылезла за край полки.
Ключевые понятия и термины
SmallVec- Коллекция Rust, хранящая несколько элементов прямо внутри себя и переходящая на буфер в куче при росте. Её скорость обеспечивает unsafe-код, управляющий буфером вручную.
size_hint- Метод Iterator, сообщающий оценочные нижнюю и верхнюю границы числа оставшихся элементов. Это лишь подсказка: безопасный код может вернуть любое значение, даже неверное.
Unsafe Rust- Код в блоке unsafe может делать то, что компилятор не проверяет, например писать через сырые указатели; автор обязан обеспечить безопасность памяти для любого безопасного вызывающего кода.
Корректность (soundness)- Безопасный API корректен, если никакой безопасный код, сколь угодно необычный, не может вызвать через него неопределённое поведение. insert_many таким не был: допустимый size_hint мог повредить память.
Связи со стандартами
Хронология инцидента
Юйчан Пэ (Yechan Bae) и Rust-группа SSLab Технологического института Джорджии сообщают об ошибке; RustSec публикует RUSTSEC-2021-0003, исправление выходит в smallvec 0.6.14 и 1.6.1.
Присваивается CVE-2021-25900; cargo audit начинает помечать уязвимые версии в деревьях зависимостей.
Пошаговый сценарий атаки
Итератор занижает свою длину
insert_many вызывается с итератором, у которого нижняя граница size_hint меньше числа реально выдаваемых элементов. Безопасному коду это разрешено.
Место резервируется по подсказке
insert_many резервирует место только под нижнюю границу и сдвигает существующие элементы на эту величину.
Все элементы всё равно записываются
Внутри блока unsafe цикл записывает каждый элемент через сырой указатель, и лишние попадают за конец выделенной памяти.
Куча повреждается
Переполнение перезаписывает соседнюю память кучи. Уязвимы программы, передающие в insert_many итераторы, на которые влияет недоверенный ввод.
Исходный код: Уязвимый vs Защищённый вариант
// Выделять память исходя из *подсказки*...
let (lower, _) = iter.size_hint();
self.reserve(lower);
unsafe {
let ptr = self.as_mut_ptr().add(index);
for (i, item) in iter.enumerate() {
// ...но записывать столько
// элементов, сколько поступит фактически.
ptr::write(ptr.add(i), item);
}
}// Повторно проверять ёмкость для каждого элемента.
// Не позволять значению из безопасного кода
// задавать границу, используемую кодом unsafe.
for (i, item) in iter.enumerate() {
self.reserve(1);
unsafe {
let ptr = self.as_mut_ptr()
.add(index + i);
ptr::write(ptr, item);
}
}
// Реальное исправление также значительно сократило
// объём кода unsafe.Чек-лист по защите системы для инженеров
- ✓Никогда не определяйте размер буфера для unsafe-кода по методу безопасного трейта — size_hint, len у трейт-объекта или компаратору; перепроверяйте ёмкость перед каждой записью.
- ✓Пишите для каждого блока unsafe комментарий // SAFETY: с инвариантом, на который он опирается, и отклоняйте ревью, где этот инвариант зависит от недоверенного безопасного кода.
- ✓Запускайте Miri в CI на тестах с враждебными реализациями: итератор с ложным size_hint, Drop с паникой, компаратор без полного порядка.
- ✓Держите блоки unsafe как можно меньше; исправление smallvec заодно сократило unsafe-код в insert_many, чтобы его можно было проверить.
- ✓Запускайте cargo audit (или cargo deny) в CI, чтобы зависимости с предупреждениями RustSec, например smallvec до 0.6.14 или 1.6.1, проваливали сборку.
Источники
- RustSec — RUSTSEC-2021-0003: переполнение буфера в SmallVec::insert_many
- servo/rust-smallvec — задача 252
- CVE-2021-25900 — запись CVE
- MITRE CWE-787
- Heartbleed: одна пропущенная проверка границ — полинтернета
- Baron Samedit: десять лет root в обратной косой черте
- Citrix Bleed: как неверно прочитанное возвращаемое значение обошло MFA