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

推荐订阅源

罗磊的独立博客
Google DeepMind News
Google DeepMind News
MyScale Blog
MyScale Blog
A
About on SuperTechFans
Martin Fowler
Martin Fowler
M
MIT News - Artificial intelligence
Recent Announcements
Recent Announcements
D
DataBreaches.Net
B
Blog
博客园 - 【当耐特】
爱范儿
爱范儿
有赞技术团队
有赞技术团队
P
Proofpoint News Feed
WordPress大学
WordPress大学
小众软件
小众软件
Apple Machine Learning Research
Apple Machine Learning Research
I
InfoQ
Engineering at Meta
Engineering at Meta
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Last Week in AI
Last Week in AI
Microsoft Azure Blog
Microsoft Azure Blog
雷峰网
雷峰网
量子位
G
Google Developers Blog

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет 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 за минуты Опыт разработчика как экономика внимания
Твой код на Rust компилируется, проходит тесты и является...
vibecodingai · 2026-05-09 · via Все публикации подряд на Хабре

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

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

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

Мнение

Если взять случайный крейт с crates.io, поставить на него Miri и подождать минут пять, шанс увидеть красное сообщение про undefined behavior где-то в зависимостях стремится к единице. Чаще всего виноват не автор хитрого unsafe-блока ради скорости, а вполне обычная библиотека, которой пять лет, у которой звёзд на гитхабе больше, чем у твоего пет-проекта строчек кода, и которая всё это время спокойно лежит в продакшене.

Самое неприятное в этой истории то, что компилятор ничего не скажет. Тесты пройдут. Бенчмарки покажут красивые наносекунды. А потом LLVM 19 обновится до LLVM 20, поменяет один проход оптимизации, и твой сервис начнёт ронять прод по понедельникам. Чтобы понять, почему так происходит, придётся залезть в три темы, которые в обычной жизни Rust-разработчика не встречаются: pointer provenance, Stacked Borrows и пришедшую им на смену Tree Borrows.

Указатель это не адрес

Самое вредное заблуждение, которое тащится в Rust из C, звучит так: указатель это число, представляющее адрес в памяти. С точки зрения процессора так и есть. С точки зрения компилятора уже лет двадцать как нет, просто в C про это стесняются говорить вслух, а в Rust решили оформить явно.

У каждого указателя в модели Rust помимо адреса есть невидимый ярлык, называется provenance. Это по сути идентификатор аллокации, из которой указатель родился, плюс набор разрешений на доступ. Когда ты делаешь Box::new(42), рантайм возвращает тебе не просто адрес, а адрес плюс свежий ярлык, привязанный именно к этому боксу. Когда ты кастуешь &mut T в *mut T, ярлык наследуется. Когда ты складываешь два числа и пытаешься объявить результат указателем через transmute, ярлыка у получившейся штуки нет, и любое разыменование такого указателя является UB, даже если по битам адрес совпадает с тем, что вернул аллокатор пять строк назад.

Проверить это руками просто:

fn main() {
    let a = [1u8, 2, 3, 4];
    let b = [5u8, 6, 7, 8];
    let pa = a.as_ptr();
    let pb = b.as_ptr();
    let offset = pb as isize - pa as isize;
    let forged = unsafe { pa.offset(offset) };
    unsafe { println!("{}", *forged); }
}

На любом нормальном процессоре forged будет указывать ровно туда же, куда pb. Программа что-то напечатает. Скорее всего пятёрку. Запусти под Miri, и увидишь сообщение про trying to retag from a pointer that was not derived from this allocation. С точки зрения железа всё корректно, с точки зрения модели памяти Rust ты использовал ярлык от массива a, чтобы залезть в массив b, а это запрещено независимо от того, что числа сошлись.

Смысл провенанса в том, чтобы компилятор мог делать алиасинг-анализ без ограничений языка C. В C спецификация говорит: если два указателя на разные объекты, они не алиасят, кроме случаев. Случаев много, они громоздкие, и restrict помогает редко. В Rust спецификация говорит: ярлыки разные, значит указатели не алиасят, точка. Это позволяет помечать почти все ссылки атрибутом noalias на уровне LLVM, и оптимизатор имеет право переставлять загрузки и записи как ему удобно.

Stacked Borrows как формальная игра в реборроу

Долгое время не существовало вообще никакой формальной модели, описывающей, какой код Rust считается корректным на уровне unsafe. Команда rustc писала компилятор, опираясь на интуицию, LLVM делал оптимизации по своим правилам, а пользователи писали unsafe-блоки, надеясь, что оба этих мира между собой договорятся. В 2018 году Ральф Юнг предложил Stacked Borrows, и с этого момента у Rust появилась хотя бы рабочая теория, которую можно проверять на машине.

Идея устроена так. К каждому байту памяти мысленно прицеплен стек тегов. Когда ты создаёшь новую ссылку на этот байт, на стек кладётся новый тег. Когда ты используешь ссылку, проверяется, что её тег где-то в стеке есть, и все теги выше неё снимаются. Если тега в стеке не оказалось, это UB.

Из этой простой механики вытекает почти вся семантика заимствований, включая ту, которую ты привык получать от borrow checker, плюс куча правил для unsafe, которых borrow checker не видит. Например, классика:

fn main() {
    let mut x = 42;
    let r1 = &mut x;
    let raw = r1 as *mut i32;
    let r2 = &mut *r1;
    *r2 = 7;
    unsafe { *raw = 13; }
    println!("{}", x);
}

Borrow checker этот код пропускает: raw это сырой указатель, его время жизни никого не волнует, r2 валидный реборроу от r1. Программа компилируется, запускается, печатает 13. Под Miri ловится UB. Объяснение в терминах стека: создание r2 положило новый тег поверх тега raw, поэтому, когда ты пишешь через raw, его тег уже не на вершине, а правила Stacked Borrows требуют для записи, чтобы тег был в стеке и выше него ничего не лежало.

Любимый сценарий, в котором это превращается в баг прода, это передача сырого указателя в C-библиотеку. Ты честно создаёшь &mut, кастуешь, передаёшь. Где-то рядом другой &mut от того же объекта живёт в Rust-коде, и компилятор имеет полное право предположить, что C через сырой указатель туда не полезет, потому что ярлык не тот. Ассемблер, сгенерированный из таких предположений, начинает кешировать значения в регистрах, и привет.

Почему пришлось выключать noalias и причём тут LLVM

В районе 2014 года rustc впервые включил noalias для &mut ссылок. Через несколько недель выяснилось, что в LLVM есть баги, которые срабатывают только на коде с большим количеством noalias-параметров, потому что C-компиляторы такой код почти не генерируют. Атрибут пришлось выключить. Включили обратно через пару лет, поймали следующий слой багов, выключили снова. Эта история с переменным успехом продолжалась примерно до 2021 года, и каждый раз rustc находил в LLVM что-то новое, что Clang за двадцать лет не нашёл, потому что человек на C просто не пишет в стиле «двадцать ссылок без алиасинга подряд».

Побочный эффект для всего сишного мира получился приятный: те же оптимизации заработали корректнее в Clang, и restrict стало можно использовать без страха, что компилятор споткнётся. Rust здесь сыграл роль адского стенда нагрузочного тестирования для алиасинг-анализа в LLVM, и до сих пор играет.

Tree Borrows и почему Stacked Borrows оказалось слишком строгим

Stacked Borrows красивая модель, но у неё есть проблема. Она запрещает паттерны, которые широко встречаются в реальном unsafe-коде и при этом интуитивно выглядят корректными. Самый болезненный пример это interior mutability через сырые указатели и работа со стандартными коллекциями.

use std::cell::Cell;

fn main() {
    let c = Cell::new(0i32);
    let p = &c as *const Cell<i32> as *mut i32;
    unsafe { *p = 1; }
    println!("{}", c.get());
}

По интуиции код корректен: Cell для того и существует, чтобы внутрь можно было писать через общую ссылку. По Stacked Borrows здесь нюансы с тем, как именно был получен p, и разные версии Miri разные версии этого кода ругают по-разному. Накопилась библиотека таких пограничных случаев, и стало понятно, что модель надо чинить.

Tree Borrows это работа Невена Вильмена, защищённая в 2023 году. Вместо стека тегов на каждый байт строится дерево, корнем которого является исходная аллокация, а детьми идут все производные ссылки и указатели. У каждого узла есть состояние: Reserved, Active, Frozen, Disabled. Переходы между состояниями описывают, что разрешено делать с памятью через данный тег, и что происходит с потомками, когда родителя используют для чтения или записи.

Эта модель строго слабее Stacked Borrows на куче существующих unsafe-паттернов и одновременно строго сильнее на тех местах, где Stacked Borrows был дырявым. Самое практичное следствие: код вокруг UnsafeCell, который раньше ругался в Miri без явной вины автора, под Tree Borrows проходит. Включить в Miri можно через MIRIFLAGS=-Zmiri-tree-borrows.

Что с этим делать сегодня

Если ты пишешь только safe Rust и не используешь unsafe, можно условно расслабиться. Условно потому, что любая твоя зависимость, у которой внутри есть unsafe impl Send или ручная работа с указателями, может протащить UB к тебе. Поэтому простой совет звучит так: раз в спринт прогоняй тесты под Miri и Tree Borrows.

cargo +nightly miri test
MIRIFLAGS="-Zmiri-tree-borrows" cargo +nightly miri test

Если тесты под Miri идут на порядок дольше обычных, это нормально, Miri это интерпретатор, а не нативный код. Но они находят такое, что не находит ни санитайзер, ни fuzz, ни ручной ревью.

Для авторов библиотек есть пара более жёстких правил, которые стоит держать в голове. Никогда не получай ссылку и сырой указатель из одного и того же &mut так, чтобы они оба пережили друг друга. Если нужен сырой указатель, который будет жить долго, бери его до создания всех ссылок и не делай реборроу. Используй UnsafeCell для всего, во что планируешь писать через общий доступ, даже если кажется, что рядом нет ни одной ссылки. И не пытайся восстановить указатель из числа через as, если у тебя нет провенанса под рукой, для этого с недавних пор есть with_exposed_provenance, и это не косметика, а единственный задокументированный способ сделать такое без UB.

Финальная мысль

Главное, что меняется в голове после знакомства с провенансом и Tree Borrows, это отношение к unsafe. Unsafe в Rust часто подаётся как «здесь компилятор тебе верит». На самом деле всё наоборот: в unsafe компилятор тебе верит ровно по тем правилам, которые описаны в модели памяти, и эти правила строже, чем в C. Цена этой строгости агрессивные оптимизации, которых нет нигде больше. Каждый раз, когда твой бенчмарк показывает, что Rust на десять процентов быстрее аналогичного C, где-то под капотом сработал noalias, который смог сработать только потому, что Stacked или Tree Borrows гарантировал отсутствие алиасинга.

Так что когда видишь в выводе Miri незнакомое слово retag, не закрывай терминал. Это компилятор показывает, что между твоим представлением о памяти и его представлением начали расходиться чертежи, и через пару релизов LLVM это разойдение станет видно невооружённым глазом, прямо в проде.


Люблю Rust, пишу на нём и разбираю код так, чтобы сложные вещи становились понятнее — подписывайтесь: t.me/rust_code

Спасибо за внимание, пишите в комментах, плиз ваши замечания!