FlawOpen

flawopen.com/Incidents/rust-unsafe-smallvec-overflow-january-2021

● servo/rust-smallvec · CVE-2021-25900 · January 2021 · CVSS 9.8 · Критический
Исследования · FlawOpen

CVE-2021-25900: переполнение буфера в smallvec

SmallVec::insert_many доверял нижней границе size_hint; если итератор выдавал больше, происходило переполнение буфера. Исправлено в 0.6.14 и 1.6.1.

💡 Простыми словами (ELI5)

Склад славится безопасностью: робот проверяет каждую полку, прежде чем на неё что-то поставят. Для некоторых задач робота выключают, и человек работает вручную, обещая быть аккуратным. Один такой работник спросил водителя: «Сколько примерно коробок везёте?», собрал полку такого размера и начал ставить. Оценка была догадкой, а не гарантией, и когда коробок оказалось больше обещанного, стопка вылезла за край полки.

Ключевые понятия и термины

SmallVec
Коллекция Rust, хранящая несколько элементов прямо внутри себя и переходящая на буфер в куче при росте. Её скорость обеспечивает unsafe-код, управляющий буфером вручную.
size_hint
Метод Iterator, сообщающий оценочные нижнюю и верхнюю границы числа оставшихся элементов. Это лишь подсказка: безопасный код может вернуть любое значение, даже неверное.
Unsafe Rust
Код в блоке unsafe может делать то, что компилятор не проверяет, например писать через сырые указатели; автор обязан обеспечить безопасность памяти для любого безопасного вызывающего кода.
Корректность (soundness)
Безопасный API корректен, если никакой безопасный код, сколь угодно необычный, не может вызвать через него неопределённое поведение. insert_many таким не был: допустимый size_hint мог повредить память.

Связи со стандартами

  • Ссылка CWE для слабого места, описанного в этом руководстве: CWE-122 CWE-787

Хронология инцидента

2021-01-08

Юйчан Пэ (Yechan Bae) и Rust-группа SSLab Технологического института Джорджии сообщают об ошибке; RustSec публикует RUSTSEC-2021-0003, исправление выходит в smallvec 0.6.14 и 1.6.1.

2021-01

Присваивается CVE-2021-25900; cargo audit начинает помечать уязвимые версии в деревьях зависимостей.

Пошаговый сценарий атаки

Шаг 1

Итератор занижает свою длину

insert_many вызывается с итератором, у которого нижняя граница size_hint меньше числа реально выдаваемых элементов. Безопасному коду это разрешено.

Шаг 2

Место резервируется по подсказке

insert_many резервирует место только под нижнюю границу и сдвигает существующие элементы на эту величину.

Шаг 3

Все элементы всё равно записываются

Внутри блока unsafe цикл записывает каждый элемент через сырой указатель, и лишние попадают за конец выделенной памяти.

Шаг 4

Куча повреждается

Переполнение перезаписывает соседнюю память кучи. Уязвимы программы, передающие в 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.

Чек-лист по защите системы для инженеров

Источники