900 гипотез об уязвимостях и 12 миллиардов токенов

—

от автора

«Просто напишите правильный промпт», говорили они

Сергей Гордейчик, СайберОК/SCADA StrangeLove

У меня появился проект, которому нужно перекладывать миллионы пакетов со скоростью канала. Для такой задачи я рассматривал C, C++ и Rust. На C я последний раз писал в 2009 году, и обратно не тянет. Слабак, согласен.
Выбрал Rust: современный язык, удобная сборка, большая библиотечная экосистема и обещание безопасности памяти. Когда в описании технологии встречается слово «безопасный», у меня по профессиональной привычке начинает чесаться в кончиках пальцев.

Что за проект? Пока не спрашивайте. Рассказ пойдёт не о нём, а о том, как я от него отвлёкся.

Язык для меня новый, поэтому я хотел разобраться, какие ошибки остаются в программах на Rust и чем их искать. В результате появились учебное приложение, сигнатуры для SAST, собственный исследовательский конвейер rust-in-peace, находки в библиотеках и приложениях, а затем и патч для ядра Linux и выступление на конференции. По дороге выяснилось, сколько убедительных ошибок способны произвести модели, агенты, проверяющие модели и я сам.

На ZeroNights я рассказывал об этом в докладе Rust in Peace: How to Raise Your Own Pet Mythos. Здесь можно позволить себе больше подробностей: откуда взялся каждый механизм, какую проблему он решал и что получилось при проверке в бою.

Харнесс, который смог

Сейчас rust-in-peace соединяет исследование исходников агентскими и традиционными методами с проверкой поведения программы. Несколько независимых проходов предлагают кандидатов. Другие агенты пытаются их опровергнуть. Для оставшихся утверждений подбирается эксперимент: последовательность сообщений протокола, вызов публичного API, фаззинг или инструментальная проверка. Затем воспроизводится сбой, готовится исправление и проверяется, выдерживает ли оно повторную атаку.

Сергей Гордейчик, сооснователь и генеральный директор СайберОК, SCADA StrangeLove

У меня появился проект, которому нужно перекладывать миллионы пакетов со скоростью канала. Для такой задачи я рассматривал C, C++ и Rust. На C я последний раз писал в 2009 году, и обратно не тянет. Слабак, согласен. Выбрал Rust: современный язык, удобная сборка, большая библиотечная экосистема и обещание безопасности памяти. Когда в описании технологии встречается слово «безопасный», у меня по профессиональной привычке начинает чесаться в кончиках пальцев.

Что за проект? Пока не спрашивайте. Рассказ пойдёт не о нём, а о том, как я от него отвлёкся.

Язык для меня новый, поэтому я хотел разобраться, какие ошибки остаются в программах на Rust и чем их искать. В результате появились учебное приложение, несколько способов анализа, собственный исследовательский конвейер rust-in-peace, находки в библиотеках и приложениях, а затем и патч для ядра Linux. По дороге выяснилось, сколько убедительных ошибок способны произвести модели, их проверяющие и я сам.

На ZeroNights я рассказывал об этом в докладе Rust in Peace: How to Raise Your Own Pet Mythos. Здесь можно позволить себе больше подробностей: откуда взялся каждый механизм, какую проблему он решал и что получилось при проверке в бою.

Харнесс, который смог

Сейчас rust-in-peace соединяет исследование исходников агентскими и традиционными методами с проверкой поведения программы. Несколько независимых проходов предлагают кандидатов. Другие агенты пытаются их опровергнуть. Для оставшихся утверждений подбирается эксперимент: последовательность сообщений протокола, вызов публичного API, фаззинг или инструментальная проверка. Затем воспроизводится сбой, готовится исправление и проверяется, выдерживает ли оно повторную атаку.

Система rust-in-peace: три независимых прохода и дополнительный SAST дают вопросы для проверки; API-тесты, фаззинг и исполнение в контейнерах дают доказательства.

Система rust-in-peace: три независимых прохода и дополнительный SAST дают вопросы для проверки; API-тесты, фаззинг и исполнение в контейнерах дают доказательства.

Эта картинка сложилась постепенно; первые эксперименты были заметно менее развесистыми. Давайте посмотрим, как я к этому пришёл.

Что именно обещает Rust

Ownership и borrowing позволяют компилятору следить за владением объектами и заимствованием ссылок. Lifetimes связывают допустимое время жизни ссылок с объектами, на которые они указывают. Эти проверки помогают управлять памятью без сборщика мусора. Send и Sync участвуют в проверке передачи и совместного использования данных между потоками. Всё это существенно сокращает пространство ошибок, знакомых разработчикам C и C++.

У этих гарантий есть условия. Если библиотека использует unsafe, она сама отвечает за выполнение условий, на которых её безопасный интерфейс действительно безопасен. Ошибка в реализации такой абстракции может затронуть программу, в которой пользователь вообще не писал unsafe. Эта граница доверия подробно разобрана в Rustonomicon.

Одновременно с языком приходится изучать способ сборки приложения из готовых компонентов — crates. Библиотека получает часть гарантий от языка, часть реализует сама, часть ожидает от вызывающего кода. Приложение наследует все эти предположения вместе с удобными интерфейсами. Поэтому довольно быстро я начал исследовать отдельные библиотеки и места их соединения.

Безопасность ломается на границах, это общее место.

Дальше начинается обычное приложение. Пользователю одного клиента нельзя читать данные другого. Размер из сетевого заголовка нельзя бездумно превращать в объём выделяемой памяти. Повторное сообщение протокола должно приводить к допустимому переходу состояния. Проверку этих свойств разработчик организует сам. Rust тут не поможет.

Например, CVE-2023-26964 в h2 была связана с ростом очереди принимаемых потоков при определённом сочетании HTTP/2-кадров. Память оставалась доступной по правильным адресам. Её просто выделялось слишком много.

В экосистеме Rust много полезных инструментов. Cargo собирает проект и запускает тесты. Miri обнаруживает ряд нарушений правил работы с памятью при исполнении Rust-кода. cargo-fuzz помогает подключить фаззер. Чтобы всё это принесло пользу, надо выбрать вход в программу, описать проверяемое свойство и научиться отличать нарушение от допустимого отказа. Готового встроенного фаззинга всех свойств приложения вместе со стикером «Я пишу на Rust» не выдают.

Учебное приложение и бумажная безопасность

Для знакомства с этими границами я сделал Damn Vulnerable Rust Application. В нём были ошибки разграничения доступа, инъекция команд ОС, SSRF, выход из каталога при распаковке архива, утечка секрета в журнал, проблемы с Sync, двойным освобождением и разбором входных данных. Обычный набор неприятностей для приложения, написанного на безопасном языке.

К нему появилась модель угроз. Я не большой любитель бумажек вроде ГОСТ Р 58412-2019 или Common Criteria, хотя хлебнул этого прилично ещё во времена СТР-К. Здесь документ пригодился как список конкретных запретов: чужой объект нельзя вернуть по одному идентификатору; внешняя строка не должна становиться командой оболочки; нормализация буфера не должна оставлять действующими старые смещения.

Первые прогоны привычных инструментов безопасности заметного результата не дали. Ну ок, что ж.

Бахнем всё в LLM?

Я пересмотрел кандидатов из своего списка инструментов безопасности с ИИ и нашёл несколько «агентских SAST». После короткого теста основой стал Defending Code Reference Harness. В нём уже были модель угроз, анализ исходников, параллельный поиск, независимое воспроизведение и проверка исправлений. Исполняемая часть была ориентирована на C/C++ и детектор ошибок памяти AddressSanitizer. Мне предстояло научить её работать с особенностями Rust и с вопросами, для которых нужны другие способы проверки.

Сначала всё выглядело обнадёживающе. Просто запустил, и ОНО ЧТО-ТО НАШЛО! Затем обнаружилось, что DVRA довольно щедро объясняет собственные уязвимости: названия функций, соседние исправленные варианты, демонстрационные тесты. Агент умел читать. Вопрос, умел ли он находить ошибку без подсказки автора кода, оставался открытым.

После удаления подсказок ранний результат ухудшился. Я добавил Rust-специфичные вопросы и модель угроз в харнесс, и стало лучше. В более позднем эксперименте DVRA-3 результат составил 9 из 9 на трёх вариантах: исходном, с нейтральными именами и без соседних исправленных функций. Среди девяти был недостижимый из API старый путь с командной инъекцией. Предусмотренная ложная «закладка» во всех трёх случаях была отвергнута на этапе триажа. Неплохо.

Достаточно хороший повод выйти за пределы собственного полигона. В качестве первых целей я выбрал популярные библиотеки разбора сложных форматов. Они были близки к моей исходной задаче, а знакомство с C/C++ подсказывало, где обычно водятся баги.

Очень убедительная ошибка

На настоящем коде система тоже начала приносить результаты. Один из самых многообещающих оказался ложным.

В x509-parser нашёлся убедительно описанный сценарий с пустым RSA-значением и последующим обращением к данным. Объяснение хорошо читалось, триаж поддерживал вывод, опасная операция в коде действительно существовала. Для такого материала уже хочется придумывать заголовок и сабмитить.

При попытке собрать PoC выяснилось, что значение до этой операции проходит через asn1-rs, где неподходящий вход отвергается. Цепочка, на которой держалось утверждение, обрывалась раньше. Более того, один из независимых статических проходов уже нашёл нужную проверку в зависимости. Подробности этого разбора остались в журнале уроков проекта.

Чтобы получить боевую уязвимость в библиотеке, нужно провести управляемые атакующим данные через её настоящий интерфейс. Это уже существенно более требовательная задача.

После x509 я стал требовать от проверяющего ссылку на конкретное место в зависимости — dep_citation — либо объяснение, почему зависимость к сценарию не относится. Исходники зависимостей должны были быть доступны агенту. Проверка начала явно отвечать на три вопроса: существует ли описанный механизм, может ли атакующий до него добраться и какие последствия получит реальный потребитель.

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

Спираль и память об ошибках

О таком способе работы я уже писал в статье «Я созидатель, а ты ССД #1», когда разбирал метод Spiral. При развитии rust-in-peace мы пользовались скиллом detection-engineering-playbook — приложением этой дисциплины к разработке детекторов. Сначала изучаем данные, затем формулируем одну опровержимую гипотезу и заранее решаем, каким экспериментом её проверить. Измерение определяет следующий шаг; если данных недостаточно, это тоже результат.

Для rust-in-peace виток выглядел так: гипотеза → эксперимент → результат → урок → изменение инструмента → следующая гипотеза. В истории с x509 результатом стало требование dep_citation. Ошибка исследователя превратилась в обязательный вопрос к следующему проверяющему.

В LESSONS.md у уроков есть привязка к исследованию и конкретное изменение, которое из него следует. Эта память передаётся следующим агентам через инструкции, правила, тесты и требования к доказательствам. Развивался и сам способ проверки: неудачный PoC мог показать, что мы выбрали неправильный вход, забыли про зависимость или проверяли другую сборку. Такой результат менял стенд следующего эксперимента.

Выйди и зайди нормально

Первый конвейер rust-in-peace дублировал идеи Anthropic и опирался на модель угроз. Это помогало задавать точные вопросы: когда проверяется размер, какой объект выделяется до проверки, есть ли бюджет рекурсии у каждого обхода? Заодно агент получал предположения о том, где искать.

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

Так появился blind-проход. Ему выдавался тот же код и общая задача исследования, но не моя модель угроз, не список известных CVE и не ответы соседних агентов. Результаты объединялись уже после поиска.

Следующий механизм вырос из другой привычки. Если ошибка нашлась в одном месте, стоит проверить остальные места с похожими паттернами реализации. Старое исправление могло закрыть один маршрут, сохранив соседний. Для отдельного прохода харнесс собирает историю CVE, RustSec, изменений и собственных подтверждённых или опровергнутых гипотез.

В lopdf это помогло увидеть разницу между ограничением глубины при разборе файла и последующими рекурсивными обходами уже загруженного документа. Каждый обход имеет собственный путь до стека и собственные условия остановки. Эксперимент с поиском вариантов сохранил как успешные воспроизведения, так и убедительный отрицательный результат для object::CrelIterator: гипотеза о гигантской аллокации через collect() не выдержала проверки порядка вызовов.

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

В одной исследовательской выборке из 50 различных кандидатов три прохода дали 80 хитов: одна и та же гипотеза могла встретиться несколько раз. После проверки осталось 20 находок — 15 с динамическим воспроизведением, или PoC, и пять с проверенной цепочкой по исходникам. Пересечение выглядело так:

Матрица пересечений: 9 находок появились во всех трёх проходах, 3 — в двух и 8 — только в одном. Модель угроз сохранила 14, blind — 13, CVE/history — 14.

Матрица пересечений: 9 находок появились во всех трёх проходах, 3 — в двух и 8 — только в одном. Модель угроз сохранила 14, blind — 13, CVE/history — 14.

Каждый столбец обозначает одну находку. У модели угроз и blind оказалось по три одиночных находки, у прохода по истории — две. Числа в строках пересекаются, поэтому их сумма больше двадцати.

Эти данные помогли принять архитектурное решение: сохранять одиночные находки и проверять их. Голосование большинством потеряло бы восемь из двадцати.

Для независимости недостаточно написать агенту «не читай чужие ответы». В одном сравнении агенты добрались до старого плана исследования и результатов PoC от другого анализа. Такой прогон перестаёт измерять пользу независимого взгляда. Пришлось изолировать выдаваемое дерево файлов и проверять журналы чтения. Знать правильный ответ оказалось удобнее, чем его искать; агенты освоили это без отдельного обучения. Благодаря разбору Hugging Face теперь все об этом знают.

О пользе SAST

Отдельную ветку работы я посвятил статическому анализу — SAST. Ожидание было простым: пусть специализированные инструменты перечислят подозрительные места, а модели разберутся с достижимостью и последствиями.

Проверенные тогда наборы правил для Rust довольно слабо покрывали интересовавшие меня ошибки парсеров. Сначала я посмотрел, с чем вообще предстоит работать:

Набор

Правил для Rust

semgrep/semgrep-rules, каталог rust/lang/security

10

trailofbits/semgrep-rules, каталог rs

1

Всего в проверенных пакетах OpenGrep

11

CodeQL, набор rust-security-extended

18

Это срез эксперимента с правилами «из коробки»: OpenGrep 1.26.0, CodeQL 2.26.1 и rust-queries 0.1.38. Само количество правил ещё ничего не говорит о способности найти нужный класс ошибок. Следующая проверка была практической: прогнать готовый набор CodeQL по семи библиотекам. Перед запуском проверили извлечение исходников — всего 560 файлов. Результат:

Библиотека

Срабатывания готовых запросов CodeQL

image-png

0

miniz_oxide

0

msgpack-rust

0

object

0

quick-xml

0

ttf-parser

0

lopdf

109

У последней строки есть объяснение: 76 срабатываний приходились на пароли в тестах и примерах, ещё 33 — на RC4/MD5 в реализации алгоритмов формата PDF. В общем, та самая знакомая многим пользователям SAST ситуация, когда 109 = 0.

Инвентаризация правил и первые прогоны описаны в материалах SAST-направления.

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

Здесь Спираль получила совсем прикладную форму. Запись эксперимента включала гипотезу, набор примеров, проверку, способную её опровергнуть, и фактический результат. Для правила мало сработать на уязвимом коде: проверяли также исправленный вариант и код-приманки. Опровергнутые гипотезы оставались в журнале и объясняли, почему следующий вариант правила устроен иначе.

После группировки срабатываний получилось 305 элементов для проверки. В семи случаях правило указало на подтверждённый дефект. Во время проверки агент предложил ещё 24 кандидата: он изучал окружающий код, пытался опровергнуть исходное утверждение и находил другую проблему. В первоначальном разборе подтвердились одиннадцать; позднее один случай признали предусмотренным поведением, осталось десять.

Разветвление исследования SAST: проверка 305 групп сигналов дала 7 прямых подтверждений; при чтении контекста возникли ещё 24 кандидата, из которых сначала подтвердились 11, после пересмотра — 10.

Разветвление исследования SAST: проверка 305 групп сигналов дала 7 прямых подтверждений; при чтении контекста возникли ещё 24 кандидата, из которых сначала подтвердились 11, после пересмотра — 10.

Нижняя ветка появилась во время попыток опровергнуть верхнюю. Один из одиннадцати первоначально подтверждённых случаев при повторном разборе оказался предусмотренным поведением.

Так у результатов появились две отдельные графы: «указано правилом» и «найдено при проверке срабатывания». Хорошо видна полезная функция SAST: багов он особо не находил, но организовал чтение кода вокруг конкретного вопроса. Публичный разбор атрибуции.

Я рассчитывал повысить полноту поиска более содержательными правилами. Польза нашлась в работе, которую агент проделывал, чтобы объяснить ошибочность срабатываний. SAST создан, чтобы фолзить.

От гипотезы к работающему стенду

Утверждение «здесь возможен выход за границы» не указывает, каким инструментом его лучше проверять. Иногда достаточно входного файла и AddressSanitizer. Иногда нужны Miri, объект с необычной реализацией трейта или определённая последовательность вызовов API. Для состояния сетевого протокола потребуется настоящий диалог между сторонами.

В Rust-профиле появился выбор способа проверки по классу ошибки и возможностям цели. Он определяет подходящий шаблон стенда и детектор. Агент связывает шаблон с реальной библиотекой: находит публичный вход, создаёт нужные объекты, учитывает параметры сборки, готовит начальные данные. Компилятор быстро возвращает часть фантазий об API в исходное состояние.

Направляемый фаззинг, guided fuzzing, в этой системе имеет два уровня управления. Агент использует гипотезу, чтобы выбрать вход, начальные примеры и при необходимости структуру данных. Затем libFuzzer через cargo-fuzz меняет входы, ориентируясь на покрытие. Для формата со сложными заголовками и взаимозависимыми полями полезны словарь или генератор структуры. Полное описание перехода от находки к стенду есть в find-to-fuzz.

Переход от гипотезы к фаззингу: агент привязывает стенд к реальному API и готовит входы; libFuzzer изменяет их по покрытию, а сохранённый сбой воспроизводится повторно.

Переход от гипотезы к фаззингу: агент привязывает стенд к реальному API и готовит входы; libFuzzer изменяет их по покрытию, а сохранённый сбой воспроизводится повторно.

Свойство определяет способ проверки: для ошибок памяти полезны Miri или ASan, для состояния протокола нужен тест последовательности действий. Сам по себе рост покрытия ещё не доказывает нарушение.

На DVRA-003 этот путь можно проследить целиком. Парсер сохранял смещения в исходном буфере, затем нормализация укорачивала буфер, а чтение продолжало использовать старые диапазоны. Два из трёх поисковых агентов получили падение. Независимая проверка его воспроизвела; два запуска моста к фаззингу также воспроизвели тот же дефект.

Для исполняемых целей сохранили важную границу исходного проекта: поиск и проверка проходят в разных контейнерах. Проверяющий получает входные данные, а исходную программу берёт из подготовленного образа. Изменённое поисковым агентом окружение вместе с PoC к нему не переезжает.

После исправления нужно снова собрать программу, повторить исходный вход, запустить её тесты и дать свежему агенту возможность атаковать изменённый участок. Такой порядок реализован в проверке патчей. Если перестал работать один воспроизводящий пример, ещё стоит выяснить, что стало с самим механизмом ошибки.

h2 и клиент, которому не требовался server push

В h2 нашлась ошибка учёта потоков HTTP/2. Сервер мог создать обещанный поток через PUSH_PROMISE, затем прислать на нём несколько информационных ответов 1xx. Состояние потока ещё оставалось ReservedRemote, а код повторно заходил в ветку первоначального учёта. Проверка assert!(!stream.is_counted) обнаруживала, что поток уже посчитан, и вызывала панику.

Ошибка h2: после PUSH_PROMISE первый ответ 1xx учитывает поток, следующий пытается учесть его повторно. Паника завершает процесс в сборке Deno с panic=abort.

Ошибка h2: после PUSH_PROMISE первый ответ 1xx учитывает поток, следующий пытается учесть его повторно. Паника завершает процесс в сборке Deno с panic=abort.

В схеме показаны и механизм ошибки, и два места для исправления: учёт потока в h2 и настройка server push в Deno. Путь до потребителя разберём ниже.

В исправлении h2 условие стало учитывать этот признак:

if is_initial && !stream.is_counted {    // Первоначальный учёт потока выполняется один раз.}

Для оценки последствий пришлось пройти путь в обратную сторону: от библиотеки к программе, которая её использует. Если клиент отключает server push, описанная последовательность недоступна. Версия h2 в списке зависимостей сама по себе ответа не даёт.

В Deno обнаружился подходящий потребитель. При неудачном обычном WebSocket Upgrade клиент мог перейти к WebSocket поверх HTTP/2. Этот путь создавал h2::client::Builder с настройками по умолчанию, где push разрешён. Злонамеренный сервер мог добиться перехода к этому варианту соединения и прислать нужную последовательность кадров. В выпускной сборке Deno с panic = "abort" паника завершала весь процесс.

Server push этому клиенту вообще не требовался. Исправление Deno отключило его через enable_push(false), а отдельный тест проверил запрет. В библиотеке исправили учёт состояния, в потребителе закрыли ненужную возможность протокола. Два небольших изменения появились из одного исследования, но проверяли разные свойства.

RustDesk и привычная ошибка в непривычном месте

В RustDesk обнаружилось повторное использование потока шифрования. Для обоих направлений соединения применялся общий ключ secretbox, а счётчики nonce начинались с одинакового значения. Поэтому одинаковые номера сообщений в разных направлениях использовали одинаковый ключ и nonce.

Для соответствующих частей шифротекста получается знакомое соотношение:

C₁ = P₁ XOR KC₂ = P₂ XOR KC₁ XOR C₂ = P₁ XOR P₂

Здесь K обозначает поток шифрования. Наблюдатель, которому доступны оба направления, может связать открытые тексты; знание одного фрагмента позволяет восстановить соответствующий фрагмент другого. Для этого не нужно получать сам секретный ключ. В исправлении hbb_common #614 новая версия обмена выводит отдельный подключ для каждого направления; изменение подключено в RustDesk #16326. Совместимость со старым вариантом обмена в коде сохранена: со старыми клиентами используется прежняя схема.

Здравствуй Downgrade. Сила привычки и совместимости. У компилятора Rust здесь не было повода возражать. Все объекты могли иметь правильного владельца и допустимое время жизни. Неправильным было использование криптографического примитива в протоколе.

На RustDesk мы также сравнивали два независимых blind-прохода с разными моделями:

Проход

Кандидаты

Осталось после проверки

Отвергнуто

Opus

4

2

2

Sonnet

6

4

2

Объединение

10

6

4

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

Когда я говорю о банде агентов, я имею в виду независимые попытки с последующим объединением и проверкой. Дополнительные агенты на разных моделях полезны. Иногда короткий контекст может быть преимуществом и помочь агенту попасть в интересную точку.

Четыре строки для Linux

Rust в ядре Linux был одним из аргументов в пользу выбора языка. Через некоторое время исследование вернулось к ядру с вполне конкретной научной задачей. А мы уже настолько круты, чтобы «крашнуть» ядро Android?

В Rust Binder число файловых дескрипторов уже ограничивалось размером буфера транзакции. Однако массив четырёхбайтовых значений разворачивался в более крупные внутренние структуры. На 64-битной системе FileEntry занимал 24 байта, а Reservation — 16.

Для примерно 900 тысяч элементов получались запросы приблизительно на 20,6 и 13,7 MiB физически непрерывной памяти. Такой запрос вызывал предупреждение аллокатора. Самим векторам физическая непрерывность не требовалась: подходил тип KVVec, допускающий переход к vmalloc для больших объёмов.

Замена двух KVec на KVVec выглядит скромно по сравнению с объяснением. Но у Линуса есть известная фраза: «I personally consider security bugs to be just “normal bugs”». Об этом мне напомнили, пока я вспоминал, как пользоваться git send-email, чтобы отправить патч.

Патч получил Reviewed-by: Alice Ryhl и был принят в интеграционные ветки ядра; тот же коммит доступен в зеркале linux-next. Теперь я настоящий ржавый пингвиноид.

Заметки агентов о безопасности агентов

Было бы бесчеловечно не натравить rust-in-peace на агентов. На пути попался Codex.

В проверенном варианте macOS-песочницы запись в верхний .git рабочей области запрещалась. Во вложенном репозитории каталог .git оказывался доступен. Сохранённый тест показывал оба действия: отказ для верхнего каталога и успешную запись во вложенный. Так можно подготовить Git hook, который запустится при последующей подходящей операции Git в этом репозитории вне песочницы.

Другой публичный отчёт касался OAuth-адреса, полученного от MCP-сервера: путь подготовки авторизации допускал схемы, которые приложение могло передать системному обработчику URL.

Забавно, но на 1 октября все шесть моих отчётов об ошибках в Codex всё ещё открыты.

Это всё, что нужно знать о безопасности агентской разработки и разработки агентов в 2026 году.

После таких находок было естественно проверить собственную систему. В self-review rust-in-peace выяснилось, что исследуемый код и агент с API-учётными данными выполняются в одном контейнере. Ограничение сети защищало часть внешней границы, но не отделяло секрет внутри контейнера от запущенной там программы. А вдруг она пойдет в немецкую wiki?

Сколько стоил правильный промпт

Подбиваем результаты: 900 предложений от агентов, после объединения повторов внутри запусков — 870 записей кандидатов. Положительную оценку проверяющих получили 447, спорными остались 62, отвергнуты были 308. Ещё 53 зависели от условий сборки, не были проверены или не получили полного результата.

В отдельном реестре было 69 находок с динамическим PoC. По всему журналу отправок — 103 группы находок, для 46 из которых на 29 сентября были известны принятые исправления.

В сохранившихся журналах Claude и Codex, включая два использовавшихся профиля Claude, получилась такая цена «правильного промпта»:

Что считали

Результат

Человеческие сообщения

1 173

Субагенты

615

Обращения к моделям

75 701

Входные и выходные токены

12,16 млрд

Из них — чтение кешированного контекста

11,66 млрд

Часы активности

≈137 ч

Сумма времени отдельных агентов

≈210 агенто-часов

Двенадцать миллиардов токенов текста я, к счастью, не читал.

Сейчас у меня есть работающая система, перекладывающая пакетики со скоростью канала, и инструмент для проверки её безопасности. Напомню: rust-in-peace — побочный продукт любопытства: «А какие баги я посажу в своё Rust-приложение?»

Модель угроз помогает задавать вопросы, независимый поиск добавляет другие, история исправлений направляет к пропущенным вариантам. SAST организует чтение кода. Компилятор, фаззер, протокольный стенд и независимый проверяющий сверяют гипотезы с наблюдаемым поведением программы.

Лучшие изменения в rust-in-peace появились после случаев, когда очередное утверждение не выдержало проверки практикой. Так замыкается очередной виток Спирали: результат эксперимента меняет правила, тесты и инструкции для следующего запуска. Следующему агенту достаётся немного меньше свободы повторить мою ошибку.

После этого можно написать разработчику, приложить воспроизведение и предложить исправление. Что происходит дальше, я расскажу во второй статье — об инфраструктуре безопасности Rust.

ссылка на оригинал статьи https://habr.com/ru/articles/1089374/