Ich arbeite an einer Bibliothek, mit deren Hilfe Typen abgewickelt werden können, die über FFI-Grenzen in eine Zeigergröße passen. Angenommen, ich habe eine Struktur wie diese:
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>());
Diese Struktur hat 1 Datenbyte und 7 Füllbytes. Ich möchte eine Instanz dieser Struktur in eine packen usize
und sie dann auf der anderen Seite einer FFI-Grenze entpacken. Da diese Bibliothek generisch ist, verwende ich MaybeUninit
und 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) };
Löst dieser assume_init
Aufruf undefiniertes Verhalten aus? Mit anderen Worten, wenn die ptr::write
Struktur in den Puffer kopiert wird , kopiert sie dann die Nichtinitialisierung der Auffüllbytes und überschreibt den initialisierten Zustand als Nullbytes?
Wenn dieser oder ein ähnlicher Code in Miri ausgeführt wird, erkennt er derzeit kein undefiniertes Verhalten. Doch je die Diskussion über dieses Thema auf Github , ptr::write
ist angeblich erlaubt , dieses Füllbytes zu kopieren, und darüber hinaus ihre uninitialized-ness zu kopieren. Ist das wahr? Die Dokumente für ptr::write
sprechen darüber überhaupt nicht, ebenso wenig wie der Nomicon-Abschnitt über nicht initialisierten Speicher .
quelle
Antworten:
Ja. "Nicht initialisiert" ist nur ein weiterer Wert, den ein Byte in der Rust Abstract Machine neben den üblichen 0x00 - 0xFF haben kann. Schreiben wir dieses spezielle Byte als 0xUU. (Weitere Informationen zu diesem Thema finden Sie in diesem Blog-Beitrag .) 0xUU wird von Kopien beibehalten, genau wie jeder andere mögliche Wert, den ein Byte haben kann, von Kopien beibehalten wird.
Die Details sind jedoch etwas komplizierter. Es gibt zwei Möglichkeiten, Daten in Rust im Speicher zu kopieren. Leider werden die Details hierfür auch vom Rust-Sprachteam nicht explizit angegeben. Was folgt, ist meine persönliche Interpretation. Ich denke, was ich sage, ist unumstritten, sofern nicht anders angegeben, aber das könnte natürlich ein falscher Eindruck sein.
Untypisierte / byteweise Kopie
Wenn ein Bereich von Bytes kopiert wird, überschreibt der Quellbereich im Allgemeinen nur den Zielbereich. Wenn der Quellbereich also "0x00 0xUU 0xUU 0xUU" war, enthält der Zielbereich nach dem Kopieren genau diese Liste von Bytes.
So verhält sich
memcpy
/memmove
in C (in meiner Interpretation des Standards, was hier leider nicht sehr klar ist). Führt in Rustptr::copy{,_nonoverlapping}
wahrscheinlich eine byteweise Kopie durch, die derzeit jedoch nicht genau spezifiziert ist, und einige Leute möchten möglicherweise sagen, dass sie ebenfalls eingegeben wurde. Dies wurde in dieser Ausgabe etwas diskutiert .Typisierte Kopie
Die Alternative ist eine "typisierte Kopie", die bei jeder normalen Zuweisung (
=
) und bei der Übergabe von Werten an / von einer Funktion auftritt. Eine typisierte Kopie interpretiert den Quellspeicher eines bestimmten TypsT
und "serialisiert" diesen Wert des Typs dann erneutT
in den Zielspeicher.Der Hauptunterschied zu einer byteweisen Kopie besteht darin, dass Informationen verloren gehen, die für den Typ nicht relevant
T
sind. Dies ist im Grunde eine komplizierte Art zu sagen, dass eine getippte Kopie das Auffüllen "vergisst" und es effektiv auf nicht initialisiert zurücksetzt. Im Vergleich zu einer untypisierten Kopie verliert eine getippte Kopie mehr Informationen. Nicht typisierte Kopien behalten die zugrunde liegende Darstellung bei, typisierte Kopien behalten nur den dargestellten Wert bei.Selbst wenn Sie auf umwandeln
0usize
,PaddingDemo
kann eine getippte Kopie dieses Werts diesen Wert auf "0x00 0xUU 0xUU 0xUU" (oder andere mögliche Bytes für das Auffüllen) zurücksetzen - vorausgesetzt,data
der Offset 0 befindet sich, was nicht garantiert ist (fügen#[repr(C)]
Sie hinzu, wenn Sie möchten diese Garantie).In Ihrem Fall wird
ptr::write
ein Argument vom Typ verwendetPaddingDemo
, und das Argument wird über eine typisierte Kopie übergeben. Bereits zu diesem Zeitpunkt können sich die Füllbytes beliebig ändern, insbesondere können sie 0xUU werden.Nicht initialisiert
usize
Ob Ihr Code UB hat, hängt dann von einem weiteren Faktor ab, nämlich ob ein nicht initialisiertes Byte in a
usize
UB ist. Die Frage ist, ob ein (teilweise) nicht initialisierten Speicherbereich repräsentiert eine ganze Zahl? Derzeit ist dies nicht der Fall und somit gibt es UB . Ob dies der Fall sein sollte, wird jedoch heftig diskutiert, und es ist wahrscheinlich, dass wir dies irgendwann zulassen werden.Viele andere Details sind jedoch noch unklar - zum Beispiel kann die Umwandlung von "0x00 0xUU 0xUU 0xUU" in eine Ganzzahl durchaus zu einer vollständig nicht initialisierten Ganzzahl führen, dh Ganzzahlen können möglicherweise die "teilweise Initialisierung" nicht beibehalten. Um teilweise initialisierte Bytes in Ganzzahlen beizubehalten, müssten wir grundsätzlich sagen, dass eine Ganzzahl keinen abstrakten "Wert" hat, sondern nur eine Folge von (möglicherweise nicht initialisierten) Bytes. Dies spiegelt nicht wider, wie Ganzzahlen in Operationen wie verwendet werden
/
. (Ein Teil davon hängt auch von LLVM-Entscheidungen abpoison
undfreeze
; LLVM kann entscheiden, dass beim Laden eines Integer-Typs das Ergebnis vollständig ist,poison
wenn ein Eingabebyte vorhanden istpoison
.) Selbst wenn der Code nicht UB ist, weil wir nicht initialisierte Ganzzahlen zulassen, verhält er sich möglicherweise nicht wie erwartet, da die Daten, die Sie übertragen möchten, verloren gehen.Wenn Sie Rohbytes übertragen möchten, empfehle ich, einen dafür geeigneten Typ zu verwenden, z
MaybeUninit
. Wenn Sie einen Integer-Typ verwenden, sollte das Ziel darin bestehen, Integer-Werte zu übertragen, dh Zahlen.quelle
usize
Taschen von Bytes (und nicht Ganzzahlen) dargestellt werden, dann ja,usize
undMaybeUninit<usize>
wäre äquivalent und beide würden die zugrunde liegende Darstellung auf Byte-Ebene (und dies schließt "undefinierte Bytes" ein) perfekt beibehalten.ptr::write
intelligent genug ist, um die nicht initialisierten Tailing-Bytes nicht zu kopieren.