Когда RUST не защищает от багов

01.05.2026 · 5 мин

Знаешь, что меня реально зацепило? Вот 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.

Мост между мирами предлагает три пути:

Аудит нашёл баги в обоих первых вариантах. Особенно пострадал 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, права, симлинки — это не те проблемы, которые компилятор решит за тебя. Но он хотя бы не даст выстрелить себе в ногу случайно.

Ссылки

Дмитрий Полухин — продуктовый дизайнер. Пишу про разработку, AI и дизайн интерфейсов. Обо мне, контакты и профили.