Когда RUST не защищает от багов
Знаешь, что меня реально зацепило? Вот Rust хвастается безопасностью памяти, отсутствием data races, все эти красивые абстракции. И тут — бац! — 44 уязвимости в одном проекте. Все мимо компилятора прошли.
Недавно Canonical раскрыл детали аудита uutils — это переписка GNU coreutils на Rust, которая должна была стать дефолтом в Ubuntu 26.04. Но не стала. И вот почему.
Toctou: когда проверка и использование живут раздельно
Самая большая группа багов — это классика Unix-безопасности под названием TOCTOU (Time Of Check To Time Of Use). Суть простая: ты проверяешь что-то про файл, а потом делаешь с ним действие. Между этими двумя моментами злоумышленник может подменить файл на симлинк.
В Rust это сделать до смешного легко. Стандартная библиотека даёт тебе удобные функции — fs::metadata, File::create, fs::remove_file — и все они принимают путь как строку. Каждый вызов заново резолвит этот путь в ядре. И каждый раз kernel видит новое имя.
Вот пример из install.rs — упрощённая версия уязвимости:
// Шаг 1: удаляем файл
fs::remove_file(to)?;
// Шаг 2: создаём заново — путь РЕЗОЛВИТСЯ СНОВА!
let mut dest = File::create(to)?;
copy(from, &mut dest)?;
Между шагами 1 и 2 кто-то с доступом к родительской директории может сделать to симлинком на /etc/shadow. И твой привилегированный процесс с удовольствием перезапишет теневые пароли.
Решение — использовать OpenOptions::create_new(true):
let mut dest = OpenOptions::new()
.write(true)
.create_new(true)
.open(to)?;
create_new гарантирует, что файл не существовал и нет симлинка. Но это работает только при создании. Для всего остального — открывай дескриптор родительской директории и работай относительно него.
Права доступа: рожай сразу, чини потом
Следующая проблема — установка прав после создания файла. Типичный антипаттерн:
// Создаём с дефолтными правами
fs::create_dir(&path)?;
// Потом меняем права
fs::set_permissions(&path, Permissions::from_mode(0o700))?;
Вот только между этими строками другой пользователь может открыть директорию. И никакой chmod потом не отберёт у него доступ.
Правило простое: выставляй права при рождении файла. OpenOptions::mode() и DirBuilderExt::mode() — твои друзья. Ядро применит твой umask поверх, так что не забудь его тоже.
Путь как строка — это не путь в файловой системе
Помнишь --preserve-root в chmod? Исходная проверка была в лоб:
if recursive && preserve_root && file == Path::new("/") {
return Err(PreserveRoot);
}
Проблема: /../, /./, /usr/.. или симлинк на / — всё это не равно строке /, но резолвится в корень. Запусти chmod -R 000 /../ и смотри, как система превращается в тыкву.
Исправление — использовать fs::canonicalize, который резолвит все .., . и симлинки в реальный абсолютный путь. Но есть нюанс: для сравнения двух произвольных путей на идентичность нужно открыть оба и сравнить их (device, inode) пары — так делает GNU coreutils.
А мой любимый баг из этой категории — CVE-2026-35363:
rm . # ❌ отклонено
rm .. # ❌ отклонено
rm ./ # ✅ удаляет текущую директорию
rm ./// # ✅ удаляет текущую директорию
Отказали . и .., но проглотили ./ и напечатали «Invalid input», пока директория испарялась. 😅
Граница unix: байты, а не строки
Rust гордится тем, что String и &str всегда в UTF-8. Это отлично работает в 99% случаев. Но Unix живёт в мире байтов: пути, переменные окружения, аргументы — всё это может быть невалидным UTF-8.
Мост между мирами предлагает три пути:
- from_utf8_lossy — молча заменяет невалидные байты на U+FFFD. Тихий data corruption.
- Strict mode с unwrap/expect — краш или отказ работать.
- OsStr или &[u8] — оставайся в байтах, это правильный путь.
Аудит нашёл баги в обоих первых вариантах. Особенно пострадал comm — классическая утилита для сравнения файлов.
Так что, RUST небезопасен?
Нет. Дело в другом.
Rust защищает от определённого класса проблем: утечки памяти, data races, use-after-free. Но файловые системы — это не Rust. Системные вызовы — это не Rust. Симлинки, права доступа, TOCTOU — это всё Unix, и ему плевать на твой borrow checker.
Когда пишешь привилегированный софт (sudo, rm, chmod), ты работаешь на границе двух миров. И эта граница требует другого мышления.
БЕЗОПАСНОСТЬ RUST
─────────────────
┌─────────────────┐
│ Borrow checker │ ← ловит это
│ Memory safety │
│ Data races │
└────────┬────────┘
│
▼ НЕ ловит
┌─────────────────┐
│ TOCTOU │
│ Permissions │
│ Path canonical │
│ Bytes vs UTF-8 │
└─────────────────┘
Главный вывод: если ты пишешь системный код на Rust — ты не в пузыре. TOCTOU, права, симлинки — это не те проблемы, которые компилятор решит за тебя. Но он хотя бы не даст выстрелить себе в ногу случайно.
Ссылки
- Статья «Bugs Rust Won’t Catch» — Matthias Endler
- uutils — Rust-реализация GNU coreutils
- Подробности CVE от Canonical
- Подкаст «Rust in Production» с Jon Seager
Дмитрий Полухин — продуктовый дизайнер. Пишу про разработку, AI и дизайн интерфейсов. Обо мне, контакты и профили.