Voor Rust 1.36 vertrouwden ontwikkelaars op std::mem::uninitialized om stapelgeheugen toe te wijzen voor waarden die later zouden worden geïnitialiseerd. Deze functie was fundamenteel onveilig omdat het de compiler vertelde dat er een geldig T op die geheugensplaats bestond, terwijl de bits willekeurig waren. Voor types met veiligheidsinvarianties—zoals bool, char of referenties—leidde dit onmiddellijk tot ongedefinieerd gedrag, omdat de compiler optimaliseerde op basis van de veronderstelling dat de waarde geldig was (bijvoorbeeld, een bool die 0 of 1 is). RFC 1892 introduceerde MaybeUninit<T> als een union-achtige abstractie om expliciet geheugen aan te geven dat nog geen geldig T bevat, waardoor dit onveiligheidsprobleem werd opgelost.
Het kernprobleem komt voort uit de behandeling van niet-geïnitialiseerd geheugen door LLVM als undef of poison, in combinatie met de automatische generatie van drop glue in Rust. Wanneer de compiler gelooft dat een variabele van type T actief is, kan het destructor-aanroepen of niche-optimalisaties uitvoeren. Als T een bool is, kan een niet-geïnitialiseerd byte de waarde 2 bevatten, wat de bitvaliditeitsinvariant schendt. Dit lezen tijdens het controleren van de drop of discriminator inspectie vormt ongedefinieerd gedrag. Bovendien, als de initialisatie halverwege een array mislukt, zou de drop glue voor het array-type proberen alle elementen te droppen, waarbij niet-geïnitialiseerde stapelbytes als pointers worden geïnterpreteerd, wat leidt tot use-after-free of double-free fouten.
MaybeUninit<T> fungeert als een getypeerde container die wellicht een geldig T bevat, of niet. Het voorkomt dat de compiler veronderstelt dat initialisatie heeft plaatsgevonden, waardoor de emissie van drop glue en ongeldige bit-patroonoptimalisaties wordt beperkt. De programmeur moet handmatig bijhouden welke instanties zijn geïnitialiseerd, meestal via een aparte index of booleaanse array. Om een waarde te extraheren, gebruikt men assume_init, assume_init_ref, of std::ptr::read, maar alleen nadat er aantoonbaar een geldig T is geschreven via write of pointermanipulatie. De kritische invariant is dat assume_init nooit mag worden aangeroepen op geheugen dat niet volledig is geïnitialiseerd, en wanneer men een gedeeltelijk geïnitialiseerde structuur verlaat, moet de programmeur handmatig alleen de geïnitialiseerde elementen droppen met ptr::drop_in_place om hulpbronlekken te voorkomen.
use std::mem::{self, MaybeUninit}; use std::ptr; fn init_array_fallible<T, E, const N: usize>( mut f: impl FnMut(usize) -> Result<T, E>, ) -> Result<[T; N], E> { let mut array: [MaybeUninit<T>; N] = unsafe { MaybeUninit::uninit().assume_init() }; let mut i = 0; while i < N { match f(i) { Ok(val) => { array[i].write(val); i += 1; } Err(e) => { for j in 0..i { unsafe { ptr::drop_in_place(array[j].as_mut_ptr()); } } return Err(e); } } } Ok(unsafe { mem::transmute::<[MaybeUninit<T>; N], [T; N]>(array) }) }
Je ontwikkelt een no_std kernel driver voor een netwerkinterfacekaart waarbij heap-toewijzing is verboden en latentie voorspelbaar moet zijn. Je moet een vaste tabel van 1024 Connection objecten op de stapel toewijzen. Elke Connection initialisatie omvat een hardware registerwrite die kan falen als de NIC-buffer vol is. De uitdaging is ervoor te zorgen dat als de 500e verbinding faalt, de voorgaande 499 correct worden gesloten (het droppen van bestandsdescriptoren en het vrijgeven van DMA-mappingen) terwijl de resterende 524 slots onveranderd blijven, zonder ongedefinieerd gedrag van het droppen van niet-geïnitialiseerd geheugen.
Een mogelijke benadering is om Default::default() te gebruiken om de array vooraf in te stellen met sentinelwaarden. Dit vereist dat Connection Default implementeert, wat problematisch is omdat een "standaard" verbinding nog steeds kernelbronnen zou verwerven die expliciet moeten worden vrijgegeven, wat het foutpad compliceert. Bovendien verspilt het construeren van 1024 dummy verbindingen alleen maar om ze te overschrijven initialisatiecycli en schendt het de strikte timingvereisten van de driver voor het online brengen van de interface.
Een tweede strategie maakt gebruik van Vec<Connection> met with_capacity en dynamisch toevoegen, gevolgd door conversie naar een vaste array. Dit is veilig en idiomatisch in gebruikersruimtecode. Echter, Vec vereist een globale allocator, die in deze kernelcontext niet beschikbaar is. Het introduceert ook mogelijke paniekpaden en geheugenfragmentatie die onaanvaardbaar zijn in de kernelruimte, en de conversie naar een array met vaste grootte vereist runtime-controles die de foutafhandelingslogica compliceren.
De derde benadering benut MaybeUninit<[Connection; 1024]> om de opslag zonder initialisatie toe te wijzen. Succesvol geïnitialiseerde verbindingen worden geschreven via MaybeUninit::write, en als er een fout optreedt op index i, itereren we handmatig van 0 tot i-1 en roepen ptr::drop_in_place aan op elk geïnitialiseerd slot voordat we de fout retourneren. Bij succes transmute we de hele array naar het geïnitialiseerde type. We hebben deze oplossing gekozen omdat deze kosteloze stapeltoewijzing biedt met voorspelbare prestaties, voldoet aan de no_std beperking, en ervoor zorgt dat hulpbroncleanup alleen plaatsvindt voor werkelijk geïnitialiseerde objecten. Het resultaat was een robuuste driver die nooit ongedefinieerd gedrag aanriep tijdens gedeeltelijke herstel van fouten en consistente microseconde-initiatie latentie handhaafde.
Waarom is het aanroepen van assume_init op een niet-geïnitialiseerde MaybeUninit<T> ongedefinieerd gedrag, zelfs als de waarde daarna nooit expliciet wordt gelezen?
Veel kandidaten geloven dat ongedefinieerd gedrag alleen optreedt wanneer je daadwerkelijk de gegevens benadert, zoals ze afdrukken of er op vertakken. Echter, het typesysteem van Rust informeert de compiler dat er een geldig T onmiddellijk bestaat bij het aanroepen van assume_init. Voor types met niche-optimalisaties (zoals bool, char, Option<&T>, of NonNull<T>) kan de compiler code genereren die het bitpatroon inspecteert om enumvarianten of geldigheid te bepalen. Als het geheugen willekeurige bits bevat (bijv. 0xFF voor een bool), triggert deze inspectie ongedefinieerd gedrag in LLVM (het laden van poison of undef). Daarnaast, wanneer de scope eindigt, voegt de compiler drop glue in voor de T, die zal proberen destructors uit te voeren op garbage data, wat leidt tot crashes of beveiligingsproblemen. Dus, assume_init is een contract waarbij de programmeur geldige initialisatie garandeert; het schenden ervan vergiftigt de status van de compiler, ongeacht expliciete reads.
Wat is het verschil tussen het gebruik van MaybeUninit::write versus std::ptr::write op de pointer die is teruggegeven door MaybeUninit::as_mut_ptr(), en wanneer is elke geschikt?
MaybeUninit::write is een veilige methode die het eigendom van een T overneemt en deze in de niet-geïnitialiseerde plek schrijft, en retourneert een mutabele referentie naar de nu-geïnitialiseerde data. Het heeft de voorkeur wanneer je de waarde gereed hebt en directe veilige toegang wilt. In tegenstelling tot dat, is std::ptr::write een onveilige functie die een waarde naar een rauwe pointer schrijft zonder de oude waarde te lezen of te droppen (wat cruciaal is omdat het geheugen niet-geïnitialiseerd is). Je moet ptr::write gebruiken wanneer je schrijft via een rauwe pointer verkregen vanuit as_mut_ptr() en de beperkingen van de borrow checker van write wilt vermijden, of wanneer je lage-abstrahies implementeert waarbij je alleen rauwe pointers hebt. Het belangrijkste verschil is dat write veiligheidsgaranties en levensduurtracking biedt, terwijl ptr::write handmatige verificatie vereist dat de bestemming geldig, goed uitgelijnd en niet-geïnitialiseerd is om aliasing-violation of voortijdige drops te voorkomen.
Hoe verwijder je correct een gedeeltelijk geïnitialiseerde array van MaybeUninit<T> zonder middelen te lekken of ongedefinieerd gedrag aan te roepen, en waarom is de volgorde van operaties kritiek?
Wanneer de initialisatie mislukt op index i, moet je alleen elementen 0..i droppen. De juiste procedure is om te itereren van 0 tot i-1 en std::ptr::drop_in_place(array[j].as_mut_ptr()) aan te roepen. Dit runt de destructor voor T zonder de waarde uit de MaybeUninit wrapper te verplaatsen (wat de plek in een moved-from staat zou laten, hoewel technisch nog steeds niet-geïnitialiseerd). Het is cruciaal om deze opschoningsactie onmiddellijk bij falen uit te voeren, voordat je de fout retourneert, om ervoor te zorgen dat het stackframe netjes wordt afgewikkeld. Als je in plaats daarvan zou proberen mem::forget op de array te gebruiken of gewoon zou retourneren, zou de MaybeUninit wrapper worden gedropt (een nop), maar de levende T instanties binnen zouden hun bronnen (zoals bestandsdescriptors of heapgeheugen) lekken. Omgekeerd, als je per ongeluk elementen i..N dropt, zou je ongedefinieerd gedrag aanroepen door garbage geheugen als geldige T instanties te behandelen.