Estou trabalhando em uma biblioteca que ajuda a transacionar tipos que se encaixam em um tamanho de ponteiro int sobre os limites da FFI. Suponha que eu tenha uma estrutura como esta:
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>());
Essa estrutura possui 1 byte de dados e 7 bytes de preenchimento. Quero compactar uma instância dessa estrutura em um usize
e descompactá-lo do outro lado de um limite de FFI. Como esta biblioteca é genérica, estou usando MaybeUninit
e 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) };
Essa assume_init
chamada desencadeou um comportamento indefinido? Em outras palavras, quando ptr::write
copia a estrutura no buffer, ele copia a não inicialização dos bytes de preenchimento, substituindo o estado inicializado como zero bytes?
Atualmente, quando esse código ou semelhante é executado no Miri, ele não detecta nenhum comportamento indefinido. No entanto, de acordo com a discussão sobre esse problema no github , ptr::write
supostamente é permitido copiar esses bytes de preenchimento e, além disso, copiar seu estado não inicializado. Isso é verdade? Os documentos para ptr::write
não falam sobre isso, nem a seção nomicon sobre memória não inicializada .
fonte
Respostas:
Sim. "Não inicializado" é apenas outro valor que um byte na Rust Abstract Machine pode ter, próximo ao usual 0x00 - 0xFF. Vamos escrever este byte especial como 0xUU. (Consulte esta postagem do blog para obter mais informações sobre esse assunto .) 0xUU é preservado por cópias, assim como qualquer outro valor possível que um byte possa ter é preservado por cópias.
Mas os detalhes são um pouco mais complicados. Existem duas maneiras de copiar dados na memória no Rust. Infelizmente, os detalhes para isso também não são explicitamente especificados pela equipe de idiomas da Rust; portanto, a seguir, é minha interpretação pessoal. Eu acho que o que estou dizendo é incontroverso, a menos que seja indicado de outra forma, mas é claro que isso poderia ser uma impressão errada.
Cópia sem tipo / byte
Em geral, quando um intervalo de bytes está sendo copiado, o intervalo de origem apenas substitui o intervalo de destino - portanto, se o intervalo de origem era "0x00 0xUU 0xUU 0xUU", depois da cópia, o intervalo de destino terá a lista exata de bytes.
É assim que se comporta
memcpy
/memmove
em C (na minha interpretação do padrão, que não está muito claro aqui, infelizmente). No Rust,ptr::copy{,_nonoverlapping}
provavelmente executa uma cópia em bytes, mas na verdade não está especificada com precisão no momento e algumas pessoas podem querer dizer que foi digitada também. Isso foi discutido um pouco nesta edição .Cópia digitada
A alternativa é uma "cópia digitada", que é o que acontece em todas as atribuições normais (
=
) e ao passar valores para / de uma função. Uma cópia digitada interpreta a memória de origem em algum tipoT
e, em seguida, "re-serializa" esse valor do tipoT
na memória de destino.A principal diferença para uma cópia em bytes é que as informações que não são relevantes para o tipo
T
são perdidas. Essa é basicamente uma maneira complicada de dizer que uma cópia digitada "esquece" o preenchimento e a redefine efetivamente para não inicializada. Comparado a uma cópia não digitada, uma cópia digitada perde mais informações. Cópias sem tipo preservam a representação subjacente, cópias digitadas apenas preservam o valor representado.Portanto, mesmo quando você transmuta
0usize
paraPaddingDemo
, uma cópia digitada desse valor pode redefinir para "0x00 0xUU 0xUU 0xUU" (ou quaisquer outros bytes possíveis para o preenchimento) - supondo-data
se que esteja no deslocamento 0, que não é garantido (adicione#[repr(C)]
se desejar essa garantia).No seu caso,
ptr::write
aceita um argumento do tipoPaddingDemo
e o argumento é passado por uma cópia digitada. Portanto, já nesse ponto, os bytes de preenchimento podem mudar arbitrariamente, em particular eles podem se tornar 0xUU.Não inicializado
usize
Se o seu código possui UB depende de outro fator, a saber, se um byte não inicializado em a
usize
é UB. A questão é: um intervalo de memória (parcialmente) não inicializado representa algum número inteiro? Atualmente, não existe e, portanto, existe UB . No entanto, se esse deve ser o caso é muito debatido e parece provável que eventualmente o permitiremos.Muitos outros detalhes ainda não são claras, embora - por exemplo, transmutando "0x00 0xUU 0xUU 0xUU" para um inteiro pode resultar em um totalmente inteiro não inicializado, ou seja, inteiros pode não ser capaz de preservar a "inicialização parcial". Para preservar bytes parcialmente inicializados em números inteiros, teríamos que dizer basicamente que um número inteiro não tem um "valor" abstrato, é apenas uma sequência de bytes (possivelmente não inicializados). Isso não reflete como os inteiros são usados em operações como
/
. (Parte disso também depende das decisões do LLVMpoison
efreeze
; o LLVM pode decidir que, ao fazer uma carga no tipo inteiro, o resultado será totalmentepoison
se houver algum byte de entrada.poison
.) Portanto, mesmo que o código não seja UB porque permitimos números inteiros não inicializados, ele pode não se comportar conforme o esperado, porque os dados que você deseja transferir estão sendo perdidos.Se você deseja transferir bytes não processados, sugiro usar um tipo adequado para isso, como
MaybeUninit
. Se você usar um tipo inteiro, o objetivo deve ser transferir valores inteiros - ou seja, números.fonte
usize
representa pacotes de bytes (e não números inteiros), então sim,usize
eMaybeUninit<usize>
seria equivalente e ambos preservariam perfeitamente a representação no nível de bytes subjacente (e isso inclui "bytes indefinidos").ptr::write
é inteligente o suficiente para não copiar os bytes não inicializados restantes.