Программирование RustUnsafe и памятьСистемный разработчик на Rust

Достаточно ли совпадения числового адреса для разыменования указателя, восстановленного из usize?

Достаточно ли совпадения числового адреса для разыменования указателя, восстановленного из usize?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Нет. Совпадение числового адреса само по себе не доказывает, что восстановленный raw pointer связан с действующей аллокацией, имеет подходящую provenance и указывает на объект, доступный для чтения или записи. Преобразование адреса обратно в указатель не отменяет требований безопасности: разыменование допустимо только при доказанном времени жизни объекта, корректном типе, выравнивании и границах.

Исторический контекст

Raw pointers и преобразования указателей в целые числа нужны для низкоуровневого кода: взаимодействия с операционными системами, аппаратными адресами, C-библиотеками и API, которые хранят непрозрачный контекст в машинном слове. В традиционной модели казалось естественным считать указатель просто числом, которое можно сохранить, передать и восстановить.

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

Постановка проблемы

Предположим, указатель на объект преобразовали в usize, сохранили, а позднее восстановили. Между этими операциями исходный объект мог быть уничтожен, память могла быть освобождена и повторно выдана другому объекту, а целочисленное значение могло быть изменено арифметикой.

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

Важно различать две вещи: сам cast между указателем и usize обычно не является разыменованием и не читает память, но последующее использование результата как указателя требует полного доказательства инвариантов. Целое число не становится безопасным дескриптором объекта автоматически.

Подробное решение

У указателя есть не только числовая часть, то есть адрес, но и семантическая связь с областью памяти, из которой он был получен. В терминологии модели памяти это обычно описывают как provenance. Она помогает определить, к какой аллокации указатель относится и какие операции над ней могут быть корректными.

Поэтому для безопасного доступа нужно доказать как минимум следующее:

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

Преобразование указателя в usize особенно опасно, если число используется как полноценный идентификатор памяти. Арифметика над числом не является заменой типизированного вычисления смещения указателя: она может вывести значение за пределы объекта, потерять связь с аллокацией или создать адрес, который численно выглядит допустимым, но семантически не является указателем на нужный объект.

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

Нужно учитывать и платформу. Размер указателя может отличаться от размера usize в экзотических архитектурах и специальных моделях выполнения, а преобразования могут зависеть от ABI. Для обычного Rust-кода usize предназначен для адресоподобных значений текущей платформы, но это не превращает хранение указателя в целое число в переносимый способ управления памятью.

Практическое правило: если требуется сохранить ссылку на Rust-объект, храните сам raw pointer в подходящем типе и явно управляйте временем жизни объекта. Если через FFI нужен непрозрачный идентификатор, лучше передавать указатель как *mut c_void по контракту API либо использовать отдельный целочисленный handle, связанный с таблицей живых объектов. В последнем случае число является ключом, а не адресом, и доступ выполняется после проверки записи в таблице.

Конкретные API и гарантии для работы с адресами и provenance зависят от версии Rust и используемой модели strict provenance. Поэтому нельзя формулировать универсальное правило вида «любое преобразование usize обратно в указатель всегда запрещено» или, наоборот, «совпадения адреса всегда достаточно». На собеседовании ожидается более точный вывод: числовое равенство — лишь одно наблюдаемое свойство, а не доказательство допустимости разыменования.

Ситуация из практики

Библиотека на Rust регистрирует callback в C-библиотеке. C API предоставляет только поле uintptr_t user_data, которое возвращается без изменения при каждом вызове callback. Разработчик сохраняет там адрес состояния Rust, а затем восстанавливает его из числа.

Рассматривались три варианта. Первый — оставить адрес в uintptr_t: это просто и совместимо с конкретным C ABI, но требует строгого контроля времени жизни, платформенных преобразований и правил provenance; ошибка в C может превратить число в произвольный адрес. Второй — передавать указатель через предусмотренное API поле void*: это лучше отражает семантику указателя, но возможно только если C-интерфейс действительно предоставляет такой параметр и не обещает хранить значение как самостоятельное число.

Третий вариант — выдавать C целочисленный handle, а состояние хранить в защищённой таблице Rust. Он требует синхронизации и обработки недействительных handles, зато число больше не интерпретируется как адрес, а освобождение объекта можно связать с удалением записи.

Для нового интерфейса выбран третий вариант, поскольку callback может приходить после обычного пути завершения операции. Таблица позволила явно отслеживать состояние регистрации, отклонять устаревший handle и не полагаться на совпадение адресов. Для уже заданного ABI, где изменение uintptr_t невозможно, допустим первый вариант только при документированном контракте: C не изменяет значение, Rust гарантирует жизнь объекта до последнего callback, а все преобразования проверены для целевых платформ.

Что кандидаты часто упускают

1. Сохраняет ли преобразование указателя в usize время жизни объекта?

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

Это особенно важно для Box, Vec и других владельцев: перемещение самого владельца обычно не уничтожает объект, но освобождение владельца завершает время жизни выделенной памяти. Для FFI нужно отдельно договориться, кто удерживает объект живым и какое событие прекращает возможность callback.

2. Делает ли неизменённый round trip через usize указатель полностью безопасным?

Нет. Неизменённое число устраняет один риск — арифметическое повреждение адреса, — но не доказывает остальные инварианты. Нужно по-прежнему установить, что объект жив, память принадлежит нужной аллокации, указатель правильно выровнен, тип соответствует объекту, а доступ разрешён правилами Rust.

В конкретном ABI и на конкретных платформах такой round trip может быть частью рабочего FFI-контракта. Но это практическая договорённость, а не универсальная замена проверке безопасности; при переносе кода или изменении модели памяти предположение должно быть пересмотрено.

3. Если после освобождения тот же адрес выдан новому объекту, можно ли использовать старый указатель?

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

Нужно получить новый указатель из нового живого объекта и доказать его свойства заново. Именно поэтому таблица handles безопаснее хранения адресов: после удаления записи старый handle можно отклонить, даже если аллокатор позже использует тот же числовой адрес для другой памяти.