惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
F
Fortinet All Blogs
B
Blog
GbyAI
GbyAI
P
Proofpoint News Feed
量子位
The Register - Security
The Register - Security
宝玉的分享
宝玉的分享
大猫的无限游戏
大猫的无限游戏
云风的 BLOG
云风的 BLOG
V
Visual Studio Blog
B
Blog RSS Feed
WordPress大学
WordPress大学
Recorded Future
Recorded Future
Recent Announcements
Recent Announcements
V
Vulnerabilities – Threatpost
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
S
Secure Thoughts
雷峰网
雷峰网
Stack Overflow Blog
Stack Overflow Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Webroot Blog
Webroot Blog
AWS News Blog
AWS News Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
The GitHub Blog
The GitHub Blog
爱范儿
爱范儿
O
OpenAI News
月光博客
月光博客
H
Hacker News: Front Page
S
Security Affairs
W
WeLiveSecurity
The Hacker News
The Hacker News
aimingoo的专栏
aimingoo的专栏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Help Net Security
Help Net Security
MongoDB | Blog
MongoDB | Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
D
Docker
T
The Blog of Author Tim Ferriss
Spread Privacy
Spread Privacy
Blog — PlanetScale
Blog — PlanetScale
J
Java Code Geeks
S
Securelist
Microsoft Azure Blog
Microsoft Azure Blog
TaoSecurity Blog
TaoSecurity Blog
T
Threat Research - Cisco Blogs
M
MIT News - Artificial intelligence
A
About on SuperTechFans

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
44 CVE в uutils: что Rust ловит, а что нет на границе с системой
arthurpro · 2026-05-06 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение10 мин

Охват и читатели810

Обзор

Перевод

В апреле 2026 года Canonical раскрыла 44 CVE в uutils. Это переписанная на Rust версия GNU coreutils, которая в Ubuntu идёт по умолчанию с 25.10. Раскрытие пришло из внешнего аудита, заказанного перед релизом 26.04 LTS. Большую часть уязвимостей нашли обычным ревью кода. Ни borrow checker, ни проверки clippy, ни cargo audit не поймали ни одной.

Этот аудит, пожалуй, самый чёткий из существующих примеров того, что Rust ловит, а что нет. Самый внятный разбор списка сделал Маттиас Эндлер в посте «Bugs Rust Won’t Catch» от 29 апреля. Эндлер ведёт консалтинг corrode и подкаст Rust in Production; недавно у него в гостях был Джон Сигер, вице-президент по инженерии в Canonical. Пост построен как разбор того самого раскрытия: 44 CVE распределены по восьми категориям; к большинству приложен git diff фикса.

Ниже разберу каркас Эндлера и добавлю два аргумента сверху. Первый: один из мейнтейнеров GNU coreutils в HN-треде показал бенчмарк, на котором рекомендованный Эндлером фикс не выживает. Второй: структурный аргумент про то, что 40 лет наслоённых POSIX-шрамов делают с любой переписью, независимо от языка.

Что Rust поймал, а что нет

Эндлер перечисляет восемь категорий. Форма везде одна: идиома Rust, одобренная системой типов, применяется в контексте, где система типов не видит, что не так.

TOCTOU на путях

Это самый большой кластер. Из-за него cp, mv и rm в 26.04 LTS по-прежнему из GNU, а не из uutils. Шаблон такой: один системный вызов проверяет, второй действует, оба принимают &Path. Между ними атакующий с правами записи в родительский каталог подменяет компонент пути на симлинк, ядро повторно резолвит путь во втором вызове, и привилегированное действие приземляется по адресу, выбранному атакующим.

Самый понятный пример — CVE-2026-35355 в install:

// 1. Удаляем целевой файл
fs::remove_file(to)?;
// ...
// 2. Резолвит путь заново. Идёт по симлинку, обрезает.
let mut dest = File::create(to)?;
copy(from, &mut dest)?;

Любой пользователь с правами записи в родительский каталог между шагами 1 и 2 может подменить to на симлинк, ведущий в /etc/shadow. Привилегированный процесс с радостью перезапишет его содержимым from. Лечится через OpenOptions::create_new(true). В документации этого вызова написано прямо: «никакого файла по целевому пути быть не должно, в том числе и (висячих) симлинков».

Permission-set-after-create

Близкий родственник TOCTOU. Шаблон fs::create_dir(&path)?; fs::set_permissions(&path, ...)?; оставляет окно, в котором каталог уже существует с правами по умолчанию, и любой локальный пользователь может вызвать open() и получить файловый дескриптор, который переживёт последующий chmod. Лечится через OpenOptions::mode() и DirBuilderExt::mode(), чтобы файл или каталог сразу рождался с нужными правами.

Равенство строк пути не равно идентичности в файловой системе

Изначально проверка --preserve-root в chmod выглядела буквально так: if recursive && preserve_root && file == Path::new("/") { return Err(PreserveRoot); }. Эту проверку обходит всё, что резолвится в /, но пишется иначе: /../, /./, симлинк, что угодно, что бы canonicalize свёл к корню. Запустите chmod -R 000 /../ и наблюдайте, как блокируется вся система.

Самый абсурдный случай в этой категории — CVE-2026-35363:

rm .    # ❌ отклоняется
rm ..   # ❌ отклоняется
rm ./   # ✅ принимается
rm ./// # ✅ принимается

rm отклонял голые . и .., но спокойно принимал ./, удалял текущий каталог, а потом печатал «Invalid input». Сравнение строк споткнулось о завершающий слеш.

UTF-8 против сырых байтов на границах Unix

В Rust типы String и &str всегда UTF-8. Unix-пути, переменные окружения и байтовые потоки, идущие через cut, comm, tr, UTF-8 не гарантируют. Везде, где программа на Rust сталкивается с этим, у неё три варианта: преобразование с потерями (молча подменяет невалидные байты на U+FFFD; Эндлер называет это «нарядной порчей данных»), строгое преобразование (падает на первом же не-UTF-8 байте) или остаться в OsStr / &[u8]. Аудит нашёл баги в обоих первых вариантах.

CVE-2026-35346 в comm использовал String::from_utf8_lossy, и пропуск бинарного файла через comm молча искажал вывод. Фикс заменил print! на BufWriter::write_all и оставил данные в байтах.

Panic как DoS

Любой unwrap, любой expect, любая индексация слайса, любая арифметика без проверок в коде, обрабатывающем входные данные, способны превратиться в отказ в обслуживании, если атакующий управляет формой входных данных.

CVE-2026-35348 в sort --files0-from вызывал expect() при UTF-8-преобразовании каждого имени файла:

$ python3 -c "open('list0','wb').write(b'weird\xffname\0')"
$ coreutils sort --files0-from=list0
thread 'main' panicked at uu_sort-0.2.2/src/sort.rs:1076:18:
Could not parse string from zero terminated input.

GNU sort обращается с именами файлов как с сырыми байтами, потому что имена файлов и есть байты. Версия uutils падает на первом же не-UTF-8 пути. По выражению Эндлера: «ваша ночная cron-задача мертва, прощай выходные».

Проигнорированные ошибки

chmod -R и chown -R возвращали exit code последнего обработанного файла, а не худшего. chmod -R 600 /etc/secrets/* мог упасть на половине файлов и выйти с кодом 0. dd сбрасывал ошибку своего set_len() через Result::ok(), чтобы воспроизвести поведение GNU для /dev/null. Но тот же код срабатывал и на обычных файлах, и забитый диск молча обрывал запись на середине файла-получателя.

Несовместимость поведения с GNU

Эндлер замечает: «Заметная часть этих CVE не про то, что код делает что-то небезопасное, а про то, что код делает что-то отличное от GNU, и какой-то shell-скрипт где-то полагался именно на поведение GNU».

Самый яркий случай: CVE-2026-35369 в kill. GNU читает kill -1 <PID> как «послать сигнал номер 1 указанному PID». uutils читает то же самое как «послать сигнал по умолчанию процессу с PID -1», что на Linux означает каждый процесс, который ты видишь. Опечатка превращается в общесистемный рубильник.

Резолви до того, как пересечёшь границу

CVE-2026-35368 — самый серьёзный отдельный баг в аудите. Это локальный root через chroot. Упрощённый шаблон:

chroot(new_root)?;
// Всё ещё uid 0, но уже внутри файловой системы атакующего.
let user = get_user_by_name(name)?;
setgid(user.gid())?;
setuid(user.uid())?;
exec(cmd)?;

get_user_by_name идёт через NSS, а NSS вызывает dlopen для модулей libnss_* в рантайме. После chroot эти модули загружаются уже из нового корневого каталога. Атакующий, у которого получилось положить файл в chroot, исполняет свой код с uid 0. GNU chroot резолвит пользователя до chroot. Фикс точно такой же. Статическая линковка не помогает: NSS динамичен в любом случае.

Это восемь классов и 44 CVE. Ни одного из них borrow checker не ловит.

Что ответили мейнтейнеры GNU coreutils

Тред на HN под постом Эндлера растянулся на 361 комментарий. Самый полезный комментарий стоит первым. Один из мейнтейнеров GNU coreutils оспаривает одно из правил Эндлера и показывает, во что оно обходится.

Эндлер рекомендует фиксить баги с равенством строк пути через fs::canonicalize: разворачивать .., . и симлинки в реальный абсолютный путь до сравнения. У мейнтейнера в возражении есть конкретный бенчмарк на глубоко вложенном каталоге:

$ mkdir -p $(yes a/ | head -n $((32 * 1024)) | tr -d '\n')
$ while cd $(yes a/ | head -n 1024 | tr -d '\n'); do :; done 2>/dev/null
$ echo a > file
$ time cp file copy
real 0m0.010s
$ time uu_cp file copy
real 0m12.857s

GNU cp отрабатывает за 10 миллисекунд. Версия uutils с логикой canonicalize справляется с тем же почти за тринадцать секунд. В 1200 раз медленнее на искусственном, но правдоподобном пути. Общая мысль мейнтейнера: GNU «работает изо всех сил, чтобы избегать произвольных лимитов». Именно на патологические на вид входы и натыкаются продакшн-скрипты, а 12-секундный cp в глубоком дереве сборки уже сам по себе превращается в инцидент.

Тот же комментарий поправляет и одно из самых сильных утверждений Эндлера. В посте сказано: «Перепись на Rust не отгрузила ни одного из этих [memory-safety] багов за сопоставимый период активности». Мейнтейнер указывает на GHSA-w9vv-q986-vj7x, реальный memory-safety advisory в Rust uutils. Уязвимость пофиксили до релиза LTS, но «ноль» теперь под вопросом, и формулировка Эндлера от этого ослабевает.

Тред не закрывает аргумент Эндлера. Категории граничных багов, которые он перечисляет, реальны. А вот категоричное заявление о победе над memory-safety съезжает с «ноль, точка» на «близко к нулю, с оговорками». Разница меньше, чем то, на чём держится спор «переписывать или оставить»; но это уже не лозунг, а измерение.

Структурный аргумент: сорок лет POSIX-шрамов

Вторая ветка HN-треда утверждает, что ось «Rust против C» вообще не та. Один из участников треда сформулировал так: «читают они сигнатуру функции, а нужны им шрамы». Линия GNU coreutils тянется от пакетов fileutils / textutils / shellutils конца 80-х (объединились в coreutils в 2002). За ней десятилетия точечных фиксов: переполнение буфера в pwd на путях длиннее 2 * PATH_MAX, запись за пределы кучи в od --strings -N, чтение неаллоцированной памяти в b2sum --check. Каждый из них хранит большой пласт знания вида «вот так не делай, вот почему», и живёт это знание в кодовой базе шрамами, а не документацией. Перепись с нуля заново зарабатывает часть этих шрамов уже на машинах пользователей.

У другого комментатора формулировка жёстче: «Они умели писать на Rust, но явно недостаточно знали Unix API, его семантику и подводные камни. Большинство этих ошибок — крайне любительские с точки зрения тех, кто давно работает над coreutils». Третий разворачивает это в претензию к процессу: «У переписи неизбежно на порядки больше багов и уязвимостей, чем у кода, который поддерживают десятилетиями. Аргумент про безопасность работал только для долгосрочного перехода, а не для торопливого».

Контраргумент тоже есть, и пост Эндлера — его сильнейшая версия. Перепись на Rust не отгрузила класс memory-safety багов. Список CVE из аудита не содержит ни переполнений буфера, ни use-after-free, ни double-free, ни гонок данных по разделяемому изменяемому состоянию, ни разыменований нулевого указателя, ни чтений неинициализированной памяти. А у GNU coreutils в каждой из этих категорий за последние пару лет находились CVE:

Класс уязвимости (memory-safety)

GNU coreutils CVE

Переполнение буфера (pwd, глубокие пути > 2×PATH_MAX)

9.11, 2026

OOB read (numfmt, trailing blanks)

9.9, 2025

Heap buffer overflow (unexpand --tabs)

9.9, 2025

Запись за буфер в куче (od --strings -N)

9.8, 2025

Чтение одного байта до буфера в куче (sort с offset ключа SIZE_MAX)

9.8, 2025

Heap overwrite (split --line-bytes)

CVE-2024-0684, 9.5, 2024

Stack buffer overrun (tail -f с большим ulimit -n)

9.0, 2021

Что бы вы ни думали о решении Canonical поставить uutils по умолчанию в Ubuntu, сравнение, которое предлагает Эндлер, честное. Список CVE из переписи на Rust читается как иной класс провала по сравнению со списком GNU. Поверхность атак та же, привилегированные системные утилиты, а класс багов уже другой.

Где сместилась граница безопасности

Если положить все 44 CVE рядом, картина не «Rust не поймал баги». Картина другая: «Rust поймал тот класс багов, который ловит, а аудит нашёл следующий по соседству».

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

Стандартная библиотека Rust в нескольких местах сделана так, что путь наименьшего сопротивления оказывается не на той стороне этой границы. Параметры &Path везде; String там, где на самом деле байтовая последовательность; вызвать unwrap или expect ничего не стоит. API на дескрипторах (openat, fstatat, unlinkat) есть на каждом Unix; std::fs не выводит их на передний план.[^1]

Это не критика языка Rust. Это наблюдение про дизайн std::fs, и само это решение тянется к кросс-платформенным ограничениям стандартной библиотеки Rust 1.0. Один из комментаторов на HN сформулировал так: «std::fs страдает от того, что это наименьший общий знаменатель. Языку нужно было что-то к 1.0, и, к сожалению, это что-то так и осталось». Та же форма повторяется почти в каждом языке с переносимой абстракцией над файловой системой; это не особенность одного только Rust. Но довод в пользу переписи на Rust в духе «мы переписали на Rust, поэтому это безопаснее» зависит от того, делает ли язык больше, чем std::fs сейчас умеет на границе с системными вызовами.

Сама история про безопасность Rust остаётся реальной и полезной. Она просто заканчивается на системном вызове.

Категории из аудита (TOCTOU, выставление прав после создания, равенство строк пути, UTF-8 против байтов, panic как DoS, проигнорированные ошибки, совместимость с GNU, резолв до пересечения границы) и есть новый чек-лист для любой переписи привилегированной утилиты на Rust. Ни одну из них компилятор не ловит. Все ловятся при ревью кода человеком, который читал этот список.

Что Rust ловит, а что — нет: чек-лист

Если уместить выводы аудита на одну страницу:

Класс багов

Что искать в коде

Что использовать вместо

TOCTOU на путях

два сисколла подряд по &Path

OpenOptions::create_new, *at-семейство сисколлов

Permission-set-after-create

create_dir + set_permissions

OpenOptions::mode(), DirBuilderExt::mode()

Равенство строк пути

path == Path::new("/"), прямые ==

fs::canonicalize (с оговоркой про производительность)

UTF-8 vs сырые байты

from_utf8_lossy, print! на пользовательских данных

OsStr, &[u8], BufWriter::write_all

Panic как DoS

unwrap, expect, индексация слайса на входе

Result-возврат, проверки границ

Проигнорированные ошибки

Result::ok() без обработки, exit code последнего

агрегация ошибок, экстремум вместо последнего

Несовместимость с GNU

поведение, отличающееся от GNU на пограничных случаях

прогон тест-сьюта GNU coreutils

Резолв после пересечения границы

chroot / setuid до резолва ресурсов

резолви всё до пересечения

Список не каверзный, короткий, и его легко грепать.

Что значит «мы переписали это на Rust»

Один и тот же вывод следует и из поста Эндлера, и из обмена мнениями в HN-треде. Фраза «мы переписали это на Rust» — это связное утверждение об одном конкретном классе багов. Сама по себе она не является утверждением о безопасности переписанного инструмента. Это утверждение про отсутствие классов багов, которые ловит система типов Rust. Классы, которые система типов не ловит (восемь штук, что видно по 44 CVE), должны ловиться чем-то ещё.

Чек-лист Эндлера и есть правильная форма того «чего-то ещё». Грепаем кодовую базу на from_utf8_lossy, неаккуратные unwrap(), проигнорированные Result, File::create, сравнения строк с "/", операции «создать каталог и потом выставить права». Искать всё это не сложнее, чем баг с нарушением memory safety. На это нужен другой тип аудита и другой ревьюер: тот, у кого 40 лет POSIX-шрамов служат документацией, которую upstream не написал.

44 CVE — это не вердикт по Rust, а измерение того, куда смотреть на следующем аудите. Фразу «мы переписали на Rust» читай так же, как фразу «у нас есть юнит-тесты»: полезное утверждение об одном конкретном классе провалов, без претензии на остальные.

Важная сноска от Эндлера: класс багов «путь против дескриптора» в каком-то смысле проще избегать в C, чем в Rust. Код на C естественным образом тяготеет к открытому файловому дескриптору и *at-семейству системных вызовов (openat, fstatat, unlinkat, mkdirat), а большинство сисколлов создания принимают mode напрямую. Высокоуровневые API в std::fs абстрагируются от файлового дескриптора и работают со значениями &Path, поэтому повторно-резолвящий вызов через путь становится самым лёгким вариантом. API на дескрипторах есть на каждой Unix-платформе; Rust просто не выставляет их на передний план.