Передает ли std :: ptr :: write "неинициализированная" байтов, которые он записывает? - PullRequest
8 голосов
/ 09 апреля 2020

Я работаю над библиотекой, которая помогает проводить операции с типами указателей типа int за пределами FFI. Предположим, у меня есть такая структура:

use std::mem::{size_of, align_of};

struct PaddingDemo {
    data: u8,
    force_pad: [usize; 0]
}

assert_eq!(size_of::<PaddingDemo>(), size_of::<usize>());
assert_eq!(align_of::<PaddingDemo>(), align_of::<usize>());

Эта структура имеет 1 байт данных и 7 байтов заполнения. Я хочу упаковать экземпляр этой структуры в usize, а затем распаковать его на другой стороне границы FFI. Поскольку эта библиотека является обобщенной c, я использую MaybeUninit и ptr::write:

use std::ptr;
use std::mem::MaybeUninit;

let data = PaddingDemo { data: 12, force_pad: [] };

// In order to ensure all the bytes are initialized,
// zero-initialize the buffer
let mut packed: MaybeUninit<usize> = MaybeUninit::zeroed();
let ptr = packed.as_mut_ptr() as *mut PaddingDemo;

let packed_int = unsafe {
    std::ptr::write(ptr, data);
    packed.assume_init()
};

// Attempt to trigger UB in Miri by reading the
// possibly uninitialized bytes
let copied = unsafe { ptr::read(&packed_int) };

Вызывает ли это assume_init вызванное неопределенное поведение? Другими словами, когда ptr::write копирует структуру в буфер, копирует ли он неинициализированный байт заполнения, перезаписывая инициализированное состояние как нулевые байты?

В настоящее время, когда этот или подобный код запустить в Мири, он не обнаруживает неопределенного поведения. Тем не менее, согласно обсуждению этой проблемы на github , ptr::write предположительно разрешено копировать эти байты заполнения и, кроме того, копировать их неинициализированную сущность. Это правда? Документы по ptr::write вообще не говорят об этом, а также раздел nomicon по неинициализированной памяти .

1 Ответ

3 голосов
/ 11 апреля 2020

Вызывает ли этот вызов accept_init неопределенное поведение?

Да. «Uninitialized» - это просто еще одно значение, которое может иметь байт в Rust Abstract Machine, помимо обычных 0x00 - 0xFF. Давайте запишем этот специальный байт как 0xUU. (См. этот пост в блоге, чтобы узнать больше об этой теме .) 0xUU сохраняется копиями, как и любое другое возможное значение, которое может иметь байт, сохраняется копиями.

Но детали немного сложнее. В Rust есть два способа скопировать данные в память. К сожалению, детали для этого также не указаны явно языковой командой Rust, так что ниже следует моя личная интерпретация. Я думаю то, что я говорю, не вызывает сомнений, если не указано иное, но, конечно, это может быть неправильным впечатлением.

Нетипизированная / побайтовая копия

В общем, когда копируется диапазон байтов, диапазон источника просто перезаписывает целевой диапазон - поэтому, если исходный диапазон был «0x00 0xUU 0xUU 0xUU», то после копии целевой диапазон будет иметь этот точный список байтов.

Вот как ведут себя memcpy / memmove в C (в моей интерпретации стандарта, которая, к сожалению, здесь не очень понятна). В Rust ptr::copy{,_nonoverlapping} , вероятно, выполняет побайтовое копирование, но на самом деле оно точно не указано прямо сейчас, и некоторые люди могут захотеть сказать, что оно также напечатано. Это было обсуждено немного в этом выпуске .

Типизированная копия

Альтернативой является "типизированная копия", что происходит при каждом обычном назначении (= ) и при передаче значений в / из функции. Типизированная копия интерпретирует исходную память некоторого типа T, а затем «повторно сериализует» это значение типа T в целевую память.

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

Так что даже когда вы преобразуете 0usize в PaddingDemo, типизированная копия этого значения может сбросить это значение до «0x00 0xUU 0xUU 0xUU» (или любые другие возможные байты для заполнения) - при условии, что data находится со смещением 0, что не гарантируется (добавьте #[repr(C)], если вы хотите эту гарантию).

In В вашем случае ptr::write принимает аргумент типа PaddingDemo, а аргумент передается через типизированную копию. Таким образом, уже в этот момент байты заполнения могут изменяться произвольно, в частности они могут становиться 0xUU.

Неинициализирован usize

То, будет ли в вашем коде UB, зависит от еще одного фактора, а именно: наличие неинициализированного байта в usize означает UB. Вопрос в том, представляет ли (частично) неинициализированный диапазон памяти какое-то целое число? В настоящее время его нет и, таким образом, существует UB . Однако, должно ли это иметь место, широко обсуждается , и кажется вероятным, что мы в конечном итоге допустим это.

Многие другие детали все еще неясны, например - например, преобразование 0x00 0xUU 0xUU 0xUU "к целому числу вполне может привести к неинициализированному целому числу полностью , т. Е. Целые числа могут быть не в состоянии сохранить" частичную инициализацию ". Чтобы сохранить частично инициализированные байты в целых числах, мы должны в основном сказать, что целое число не имеет абстрактного «значения», это просто последовательность (возможно, неинициализированных) байтов. Это не отражает, как целые числа используются в таких операциях, как /. (Отчасти это также зависит от решений LLVM, касающихся poison и freeze; LLVM может решить, что при загрузке целочисленного типа результат будет полностью poison, если любой входной байт равен poison. ) Так что даже если код не является UB, потому что мы разрешаем неинициализированные целые числа, он может работать не так, как ожидалось, потому что данные, которые вы хотите передать, теряются.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...