Geschiedenis van de vraag
De UnwindSafe trait werd geïntroduceerd in Rust 1.9 samen met std::panic::catch_unwind om zorgen over uitzonderingveiligheid aan te pakken die zijn overgenomen uit C++ en andere talen met uitzonderingafhandeling. In Rust veroorzaken panieks die stack-ontwinding af, wat garandeert dat Drop-implementaties worden uitgevoerd, maar dit garandeert niet automatisch dat datastructuren in consistente staten blijven als een paniek een logische bewerking onderbreekt. De trait is ontworpen om types te markeren die het tolereren om in een actieve staat te zijn over een catch_unwind grens zonder het risico van ongedefinieerd gedrag of logische fouten.
Het probleem
Wanneer een mutabele referentie (&mut T) een catch_unwind grens overschrijdt en T interieure mutabiliteit bevat (zoals RefCell of Cell), kan een paniek T in een logisch inconsistente staat achterlaten. Bijvoorbeeld, als een paniek optreedt tussen RefCell::borrow_mut en het impliciete verval van de resulterende RefMut-bewaker, blijft de interne leen-teller van de RefCell verhoogd. Nadat catch_unwind de paniek heeft opgevangen en de uitvoering hervat, lijkt de RefCell mutabel geleend, terwijl de bewaker die de teller zou verlagen is weggevallen tijdens de ontwrichting. Deze "vervuilde" staat vormt een uitzondering-veilige schending omdat daaropvolgende bewerkingen op de RefCell zullen panikeren of onjuist gedrag vertonen, waardoor de programmatoestand op een manier wordt gecorrumpeerd die veilige code niet kan detecteren of herstellen.
De oplossing
UnwindSafe dient als een conservatieve marker trait: het wordt automatisch geïmplementeerd voor de meeste types, maar expliciet uitgesloten voor &mut T en elke verzameling die het bevat. Door &mut T te verbieden om UnwindSafe te implementeren, voorkomt het type-systeem dat mutabele referenties in catch_unwind worden doorgegeven, tenzij de programmeur ze expliciet wikkelt in AssertUnwindSafe. Deze wrapper is een onveilige overeenkomst waarbij de programmeur verklaart dat het gewikkelde type ofwel geen interieure mutabiliteit heeft of dat ze handmatig de uitzonderingveiligheid hebben geverifieerd. Deze architectonische keuze dwingt een expliciete opt-in af voor een potentieel gevaarlijk patroon, waardoor ervoor wordt gezorgd dat onopzettelijke blootstelling van mutabele, interieure mutabele staat over paniekgrenzen wordt opgevangen tijdens de compilatie.
use std::panic::{catch_unwind, AssertUnwindSafe}; use std::cell::RefCell; fn main() { let shared = RefCell::new(vec![1, 2, 3]); // Dit is niet compileren omdat &mut RefCell geen UnwindSafe is: // let _ = catch_unwind(|| { // let mut borrow = shared.borrow_mut(); // borrow.push(4); // panic!("onderbroken"); // }); // Expliciete opt-in met onveilige erkenning: let result = catch_unwind(AssertUnwindSafe(|| { let mut borrow = shared.borrow_mut(); borrow.push(4); panic!("onderbroken"); })); // Na de paniek kan 'shared' in een ongeldige leningtoestand zijn, // maar we hebben dit risico expliciet erkend met AssertUnwindSafe. println!("Hersteld: {:?}", result.is_err()); }
Probleembeschrijving
Een high-performance HTTP-server gebouwd met hyper moet panieks in door gebruikers gedefinieerde aanvraag handlers isoleren om te voorkomen dat een enkele misvormde aanvraag het hele proces beëindigt. De server onderhoudt een verbinding pool met behulp van RefCell (voor prestatie in een enkele thread) om actieve databaseverbindingen per thread bij te houden. De architectuur wikkelt elke aanvraag handler in catch_unwind om panieks op te vangen en deze op een nette manier te loggen. Tijdens belastingtests ondervindt de server een paniek in een handler die een mutabele leen van de verbinding pool's RefCell vasthoudt. Wanneer catch_unwind de paniek opvangt, blijft de interne leen-vlag van de pool ingesteld op "mutabel geleend" omdat de RefMut-bewaker is gevallen tijdens de ontwrichting zonder de decrement-logica uit te voeren. Opvolgende aanvragen op dezelfde thread proberen de pool te lenen en veroorzaken een runtime paniek vanwege de al geleende toestand, waardoor de thread effectief crasht en de pooltoestand verliest.
Oplossing 1: Verwijder catch_unwind en laat het proces beëindigen
Deze aanpak verwijdert het uitzonderingveiligheidsprobleem volledig door het proces te laten crashen bij elke paniek, en accepteert dat beschikbaarheid secundair is aan juistheid in deze specifieke context.
Voordelen: Volledig uitsluiten van uitzondering veiligheid zorgen; geen risico op staatcorruptie; eenvoudig te implementeren.
Nadelen: Onacceptabel voor productie beschikbaarheid; één kwaadwillige of verkeerde aanvraag beëindigt de hele dienst; schendt betrouwbaarheidseisen.
Oplossing 2: Vervang RefCell door Mutex en gebruik vergiftiging
Vervang de RefCell-gebaseerde pool door Mutex<Pool> en maak gebruik van Rust's mutex vergiftiging detectie.
Voordelen: Mutex detecteert panieks in vasthoudende threads en markeert zichzelf vergiftigd, waardoor daaropvolgende vergrendelpogingen corruptie kunnen detecteren via PoisonError; de standaardbibliotheek biedt ingebouwde veiligheid.
Nadelen: Mutex introduceert synchronisatie overhead die niet nodig is voor een enkele thread asynchrone executoren; vereist herstructurering van de verbinding pool om Send te zijn; vergiftiging vereist expliciete afhandelingslogica om de pool opnieuw te initialiseren.
Oplossing 3: Wikkel handlers in AssertUnwindSafe met toestand validatie
Houd RefCell voor prestaties, maar wikkel de handler in AssertUnwindSafe en implementeer een aangepaste drop-bewaker die de RefCell-toestand reset als er een paniek optreedt.
Voordelen: Behoudt de prestatievoordelen van RefCell; maakt paniekisolatie mogelijk; mogelijk om herstel logica te implementeren.
Nadelen: Vereist onveilige code om te interageren met AssertUnwindSafe; uiterst moeilijk om uitzondering veiligheid voor alle codepaden te garanderen; gemakkelijk om randgevallen te missen waar de staat gecorrumpeerd blijft.
Gekozen oplossing en redenatie
Het team selecteerde Oplossing 2 (Mutex met vergiftiging) voor de gedeelde verbinding pool, terwijl het Oplossing 3 alleen gebruikte voor aanvraag-specifieke tijdelijke buffers die triviaal kunnen worden opnieuw geïnitieerd. De expliciete vergiftigingsmechanisme van Mutex biedt een betrouwbare, gestandaardiseerde manier om corruptie te detecteren zonder dat elke mogelijke paniek punt onveilig hoeft te worden gecontroleerd. De kleine prestatie overhead werd geaccepteerd in ruil voor de veiligheidsgarantie.
Resultaat
De server is er met succes in geslaagd om panieks in aanvraag handlers te isoleren zonder het risico op staatcorruptie. Wanneer een handler panikeert terwijl hij de poolvergrendeling vasthoudt, wordt de mutex vergiftigd en detecteert de server dit bij de volgende toegang, waardoor de gecorrumpeerde thread-lokale pool wordt weggegooid en een nieuwe wordt gestart. Dit zorgt ervoor dat er geen ongedefinieerd gedrag optreedt en dat de dienst beschikbaar blijft, zelfs onder vijandige invoer.
Waarom vereist catch_unwind UnwindSafe, hoewel Rust destructors uitvoert tijdens panieks?
Veel kandidaten aannemen dat omdat Drop-implementaties worden uitgevoerd tijdens ontwrichting, uitzonderingveiligheid gegarandeerd is. Echter, UnwindSafe behandelt de logische staat van gegevens, niet alleen hulpbronlekken. Een paniek kan een reeks operaties onderbreken (zoals het bijwerken van een lengteveld voordat de bijbehorende gegevens worden bijgewerkt), waardoor een object tijdelijk in een inconsistente staat kan achterblijven. De destructor draait op deze gebroken staat, wat mogelijk corruptie kan verspreiden. UnwindSafe zorgt ervoor dat ofwel het type niet kan worden gebroken door onderbreking (onbeweeglijke gegevens) of dat de programmeur het risico erkent. Het voorkomt dat de uitvoering wordt hervat met objecten die hun eigen invarianties schenden.
Wat is het verschil tussen UnwindSafe en de Send/Sync auto-traits?
Terwijl Send en Sync ook auto-traits zijn, gebruiken zij positieve redenering: &T is Send als T Sync is, en &mut T is Send als T Send is. UnwindSafe gebruikt negatieve redenering: &mut T is nooit UnwindSafe, ongeacht T. Daarnaast fungeert AssertUnwindSafe als een waarde-niveau ontsnappingsventiel (vergelijkbaar met unsafe impl maar voor specifieke waarden), terwijl Send/Sync-schendingen doorgaans unsafe impl op type-niveau vereisen. UnwindSafe gaat ook samen met RefUnwindSafe voor gedeelde referenties, waardoor een duale trait-systeem ontstaat dat vergelijkbaar maar verschillend is van Send/Sync.
Hoe creëert de leen-vlag van RefCell onveiligheid met panieks en waarom heeft Mutex niet dezelfde UnwindSafe-problemen?
RefCell vertrouwt op een runtime leen-vlag. Als er een paniek optreedt tussen borrow_mut() en de Drop van de bewaker, blijft de vlag ingesteld, maar is de bewaker verdwenen. Wanneer de uitvoering wordt hervat, lijkt de RefCell geleend, maar bestaat er in werkelijkheid geen leen. Dit is een logische fout die toekomstige lenen onterecht laat panikeren. Mutex voorkomt dit door vergiftiging te implementeren: als er een paniek optreedt terwijl een vergrendeling wordt vastgehouden, markeert de Mutex zichzelf als vergiftigd. Opvolgende lock()-oproepen retourneren een fout die aangeeft dat de vorige thread panikeerde. Dit maakt de corruptie expliciet en detecteerbaar, terwijl de corruptie van RefCell stil is. Daarom is MutexGuard eigenlijk !UnwindSafe, maar het vergiftigingsmechanisme biedt een veilige herstelweg die RefCell mist.