Estoy trabajando en una biblioteca que ayuda a realizar transacciones de tipos que se ajustan a un tamaño de puntero int sobre los límites de FFI. Supongamos que tengo una estructura 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>());
Esta estructura tiene 1 byte de datos y 7 bytes de relleno. Quiero empaquetar una instancia de esta estructura en ay usize
luego descomprimirla en el otro lado de un límite de FFI. Debido a que esta biblioteca es genérica, estoy usando MaybeUninit
y 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
Esa llamada desencadenó un comportamiento indefinido? En otras palabras, cuando ptr::write
copia la estructura en el búfer, ¿copia el no inicializado de los bytes de relleno, sobrescribiendo el estado inicializado como cero bytes?
Actualmente, cuando este o un código similar se ejecuta en Miri, no detecta ningún comportamiento indefinido. Sin embargo, según la discusión sobre este tema en github , ptr::write
supuestamente se permite copiar esos bytes de relleno y, además, copiar su no inicializado. ¿Es eso cierto? Los documentos para ptr::write
no hablan sobre esto en absoluto, ni la sección de nomicon sobre memoria no inicializada .
fuente
Respuestas:
Si. "Sin inicializar" es solo otro valor que puede tener un byte en Rust Abstract Machine, junto al habitual 0x00 - 0xFF. Escribamos este byte especial como 0xUU. (Consulte esta publicación de blog para obtener un poco más de información sobre este tema ). 0xUU se conserva mediante copias al igual que cualquier otro valor posible que un byte pueda tener se conserva mediante copias.
Pero los detalles son un poco más complicados. Hay dos formas de copiar datos en la memoria en Rust. Desafortunadamente, los detalles para esto tampoco están especificados explícitamente por el equipo de lenguaje Rust, por lo que lo que sigue es mi interpretación personal. Creo que lo que digo no es controvertido a menos que se indique lo contrario, pero, por supuesto, podría ser una impresión equivocada.
Copia sin tipo / byte-wise
En general, cuando se copia un rango de bytes, el rango de origen simplemente sobrescribe el rango de destino, por lo que si el rango de origen era "0x00 0xUU 0xUU 0xUU", entonces, después de la copia, el rango de destino tendrá esa lista exacta de bytes.
Así es como se comporta
memcpy
/memmove
en C (en mi interpretación del estándar, que desafortunadamente no está muy claro aquí). En Rust,ptr::copy{,_nonoverlapping}
probablemente realiza una copia en bytes, pero en realidad no se especifica con precisión en este momento y algunas personas pueden querer decir que también está escrita. Esto se discutió un poco en este tema .Copia mecanografiada
La alternativa es una "copia escrita", que es lo que sucede en cada asignación normal (
=
) y al pasar valores a / desde una función. Una copia mecanografiada interpreta la memoria de origen en algún tipoT
y luego "vuelve a serializar" ese valor de tipoT
en la memoria de destino.La diferencia clave para una copia en bytes es que
T
se pierde información que no es relevante en el tipo . Básicamente, esta es una forma complicada de decir que una copia escrita "olvida" el relleno y, efectivamente, la restablece como no inicializada. En comparación con una copia sin tipo, una copia con tipo pierde más información. Las copias sin tipo conservan la representación subyacente, las copias con tipo solo conservan el valor representado.Entonces, incluso cuando transmuta
0usize
aPaddingDemo
, una copia escrita de ese valor puede restablecer esto a "0x00 0xUU 0xUU 0xUU" (o cualquier otro byte posible para el relleno) - suponiendo que sedata
encuentre en el desplazamiento 0, lo que no está garantizado (agregue#[repr(C)]
si lo desea esa garantía)En su caso,
ptr::write
toma un argumento de tipoPaddingDemo
, y el argumento se pasa a través de una copia escrita. Entonces, en ese punto, los bytes de relleno pueden cambiar arbitrariamente, en particular pueden convertirse en 0xUU.Sin inicializar
usize
Si su código tiene UB, entonces depende de otro factor, es decir, si tener un byte no inicializado en a
usize
es UB. La pregunta es, ¿un rango de memoria (parcialmente) no inicializado representa algún número entero? Actualmente, no lo hace y, por lo tanto, hay UB . Sin embargo, si ese debería ser el caso, se debate mucho y parece probable que eventualmente lo permitamos.Muchos otros detalles aún no están claros, sin embargo - por ejemplo, la transmutación de "0x00 0xUU 0xUU 0xUU" a un entero bien puede resultar en una completamente número entero no inicializado, es decir, números enteros pueden no ser capaces de preservar "inicialización parcial". Para preservar bytes parcialmente inicializados en números enteros, tendríamos que decir básicamente que un número entero no tiene un "valor" abstracto, es solo una secuencia de bytes (posiblemente no inicializados). Esto no refleja cómo se usan los enteros en operaciones como
/
. (Algo de esto también depende de las decisiones de LLVMpoison
yfreeze
; LLVM podría decidir que al realizar una carga en el tipo entero, el resultado es completopoison
si algún byte de entrada espoison
.) Entonces, incluso si el código no es UB porque permitimos enteros no inicializados, puede que no se comporte como se esperaba porque se están perdiendo los datos que desea transferir.Si desea transferir bytes sin procesar, sugiero usar un tipo adecuado para eso, como
MaybeUninit
. Si usa un tipo entero, el objetivo debe ser transferir valores enteros, es decir, números.fuente
usize
representa bolsas de bytes (y no enteros), entonces sí,usize
yMaybeUninit<usize>
sería equivalente y ambos preservarían perfectamente la representación de nivel de bytes subyacente (y esto incluye "bytes indefinidos").ptr::write
es lo suficientemente inteligente como para no copiar los bytes de cola no inicializados.