Один сегфолт на каждые 800 тысяч запросов внутри C-библиотеки обнаружился спустя десять дней работы в продакшене. Код прошёл два code review и CI с clippy. Его написал Claude Sonnet — разработчик одобрил, второй ревьюер тоже. Расследование заняло три дня.

За шесть месяцев автор собрал около 180 unsafe-блоков в шести репозиториях: FFI-обёртки вокруг zstd, libsodium и кастомного аудиокодека, две lock-free структуры, два примитивных аллокатора, парсер бинарного формата с указательной арифметикой и немного no_std для встроенной железки. Каждый блок прогонялся через Miri, cargo-careful, ThreadSanitizer и Loom. Для сравнения: в безопасном Rust доля «сразу правильного» кода у тех же моделей составляла около 70%. В unsafe — вдвое меньше.

МодельКорректно с первого разаПадает под MiriПадает под Loom/TSan
Claude Sonnet38%44%18%
GPT-4 class33%49%18%
Gemini Pro29%51%20%
Qwen/DeepSeek (локально)22%58%20%
Среднее34%47%19%

Причина, по мнению автора, в структуре обучающих данных. Датасеты содержат много старого C и много safe Rust из туториалов. Unsafe Rust попадает туда преимущественно из учебных примеров уровня «вот как работает mem::transmute» и из нетривиальных фрагментов стандартной библиотеки — tokio, crossbeam, — где инварианты соблюдены, но не объяснены словами рядом с кодом. Комментарии вида // SAFETY: в открытом коде встречаются настолько редко, что модель не воспринимает их как жанр. Результат: unsafe пишется в стиле учебника, где инварианты подразумеваются, и именно они первыми ломаются на реальном коде.

Claude Sonnet показал лучший результат среди протестированных моделей — 38% корректного кода с первой попытки, Qwen/DeepSeek — 22%.

Самая частая категория ошибок — нарушение aliasing rules и модели Stacked Borrows. В Rust нельзя одновременно иметь &mut T и любой другой доступ к той же памяти, даже через сырой указатель, если он выведен из той же ссылки. Классический пример, встреченный в трёх разных проектах: наивная реализация split_at_mut, создающая два &mut [T] поверх одного исходного borrow. Компилируется, тесты зелёные, Miri немедленно выдаёт ошибку Undefined Behavior. Правильная версия использует split_at_mut из стандартной библиотеки или работает исключительно через сырые указатели, не возвращаясь к исходной ссылке внутри unsafe-блока. Когда автор указывал на нарушение aliasing rules, модели в половине случаев добавляли лишний слой unsafe { &mut *ptr }, что ничего не меняет.

Вторая по частоте проблема — потеря провенанса при арифметике через usize. Указатель кастуется в целое число, выполняется арифметика, результат кастуется обратно. На стабильном Rust это формально работает, но с приходом strict provenance такие указатели теряют связь с исходным объектом памяти. Под Miri с флагом -Zmiri-strict-provenance это уже UB. Правильный вариант — wrapping_byte_add непосредственно на указателе, без преобразования в число. Реальный случай из практики: модель закэшировала указатели в HashMap<usize,...> и переиспользовала их через несколько строк. На x86_64 код работал месяц, после миграции на arm64 начались segfault'ы.

Третья стабильная категория — несовпадение Layout при alloc и dealloc. В семи случаях из десяти при написании примитивного аллокатора dealloc вызывался с Layout, не совпадающим с тем, что передавался в alloc: layout считался для T, тогда как блок аллоцировался под Node<T>. Контракт аллокатора требует передать ровно тот же layout. Тесты проходят, потому что многие аллокаторы прощают несовпадение до определённого момента — затем segfault в продакшене. Отдельный подвид: arena-аллокатор, где layout для T посчитан аккуратно, но забыт padding при размещении нескольких объектов подряд. На arm64 с жёсткими требованиями к выравниванию это ломается стабильно.

Четвёртая категория — тихий double free при работе с ManuallyDrop. Когда unsafe-код перемещает значения через ptr::read, ptr::write или mem::transmute, Rust по-прежнему считает оригинал валидным и в какой-то момент вызывает его деструктор. Если внутри был String или Box, результат — двойное освобождение памяти. Miri ловит такое немедленно, обычные тесты — нет.

Общий вывод из шести месяцев эксперимента: LLM способны ускорить написание unsafe-кода, но не заменить понимание модели памяти Rust. Инструменты статического и динамического анализа — Miri, cargo-careful, ThreadSanitizer — остаются обязательными, а не опциональными, особенно когда код генерируется автоматически.