RustProgrammazioneSviluppatore Rust

Come fa la varianza del tipo &mut T a prevenire l'assegnazione corretta di un &mut &'long str a un &mut &'short str, e quale problema di sicurezza della memoria permetterebbe se fosse consentito?

Supera i colloqui con l'assistente IA Hintsage

Risposta alla domanda

Storia della domanda

La varianza nei sistemi di tipi determina come le relazioni di sottotipizzazione tra parametri generici influenzano il tipo complessivo. L'approccio di Rust è stato fortemente influenzato dalla ricerca sulla gestione della memoria basata su regioni e dalla necessità di prevenire vulnerabilità di uso dopo la liberazione. Quando Rust ha introdotto riferimenti mutabili (&mut T), i progettisti hanno dovuto decidere se dovessero essere covarianti (come &T), contravarianti o invarianti. La scelta dell'invarianza per &mut T rispetto a T è stata fondamentale per mantenere la sicurezza della memoria senza richiedere controlli a tempo di esecuzione.

Il Problema

Se &mut T fosse covariante rispetto a T, potresti sostituire &mut U dove ci si aspetta &mut V se U è un sottotipo di V. In termini di vita, poiché 'long è un sottotipo di 'short (perché 'long vive più a lungo di 'short), ciò significherebbe che potresti assegnare &mut &'long str a &mut &'short str. Questo sembra innocuo ma crea un buco di solidità.

La Soluzione

&mut T è invariante rispetto a T. Ciò significa che &mut &'a str e &mut &'b str sono tipi non correlati a meno che 'a non sia esattamente uguale a 'b, indipendentemente dalla relazione di sottotipizzazione tra le durate. Il compilatore rifiuta il codice che tenta di costringere tra di essi, impedendo l'assegnazione di dati a vita breve a posizioni che si aspettano riferimenti a vita più lunga tramite un'indirezione mutabile.

Esempio di Codice:

fn demonstrate_invariance() { let mut long_lived: &'static str = "string statica"; // Questo compilerebbe se &mut T fosse covariante: // let short_ref: &mut &'short str = &mut long_lived; // Ma poiché &mut T è invariabile, questo fallisce: // errore: disallineamento di vita // let short_ref: &mut &'_ str = &mut long_lived; let local = String::from("temporaneo"); // Se quanto sopra fosse consentito, potremmo fare: // *short_ref = &local; // Ora long_lived punta a dati eliminati (UAF!) } // locale eliminato qui

Situazione dalla vita reale

Un team stava costruendo un gestore di configurazione per uno stack di rete ad alte prestazioni. La struttura principale doveva contenere un riferimento mutabile a una configurazione di protocollo che poteva essere scambiata a tempo di esecuzione senza acquisirne la proprietà.

Il Problema: Il design iniziale dell'API utilizzava &mut &'a Config dove 'a era la durata della sessione di rete. Gli sviluppatori hanno tentato di inizializzare questo con &mut &'static Config (per configurazioni predefinite globali) e poi passarci a funzioni che si aspettavano &mut &'session Config. Il compilatore ha rifiutato questo, causando confusione perché i riferimenti immutabili (& &'static Config) funzionavano bene.

Soluzioni Considerate:

1. Trasmutazione Non Sicura per Forzare la Conversione Il team ha considerato di usare std::mem::transmute per convertire &mut &'static Config in &mut &'session Config. Questo avrebbe eluso i controlli di varianza del compilatore. Tuttavia, questo avrebbe permesso di scrivere un riferimento a configurazione a vita breve in una posizione che potrebbe vivere oltre l'attuale ambito, portando a un comportamento indefinito immediato se la configurazione fosse stata accessibile dopo essere stata eliminata. Il rischio di uso dopo la liberazione nel codice di produzione ha reso questo inaccettabile.

2. Cambiare a Riferimenti Immutabili Hanno considerato di cambiare l'API per utilizzare & &'a Config invece di &mut &'a Config. Poiché i riferimenti condivisi sono covarianti, & &'static Config potrebbe coercire in & &'session Config. Tuttavia, questo ha rimosso la capacità di scambiare atomico le configurazioni durante gli aggiornamenti a tempo di esecuzione, che era un requisito fondamentale per il caricamento a caldo delle impostazioni senza riavviare le connessioni.

3. Usare Cell<&'a Config> per la Mutabilità Interna Questa opzione consentirebbe la mutazione attraverso un riferimento condiviso. Tuttavia, Cell<T> è anch'esso invariabile rispetto a T per le stesse ragioni di sicurezza, quindi non ha risolto il problema della varianza. Inoltre, Cell non fornisce sincronizzazione per l'accesso multi-thread, e l'overhead del controllo degli prestiti a tempo di esecuzione con RefCell è stato ritenuto troppo costoso per il percorso critico.

4. Ridefinire con Tipi Proprietari e Indirezione La soluzione scelta ha eliminato completamente il modello riferimento-a-riferimento. Invece di memorizzare &mut &'a Config, la struttura ha memorizzato &'a mut ConfigHolder, dove ConfigHolder era un wrapper posseduto. Questo ha spostato la mutabilità al livello del holder piuttosto che al livello del riferimento, evitando il trabocchetto di varianza mantenendo la capacità di scambiare configurazioni. L'API è diventata più ergonomica perché gli utenti non dovevano più gestire doppi riferimenti.

Il Risultato: La ridefinizione ha prodotto un'API più sicura che si è compilata senza codice non sicuro. La natura invariabile di &mut T ha costretto il team a riconoscere un potenziale difetto architetturale in cui le assunzioni di durata potevano essere violate. Il sistema finale ha prevenuto una categoria di bug in cui puntatori di configurazione obsoleti potevano persistere oltre il loro periodo di validità.

Cosa spesso trascurano i candidati

Perché Cell<T> è invariabile rispetto a T, e come si relaziona questo con la varianza di &mut T?

Cell<T> fornisce mutabilità interna, consentendo la mutazione tramite riferimenti condivisi. Se Cell<T> fosse covariante rispetto a T, potresti fare un upcast di Cell<&'short str> in Cell<&'static str>. Poi, potresti memorizzare un riferimento a stringa di vita breve all'interno e successivamente leggerlo tramite il tipo Cell<&'static str>, trattando dati temporanei come statici. Questo rappresenterebbe una vulnerabilità di uso dopo la liberazione. Pertanto, come &mut T, Cell<T> (e UnsafeCell<T>) devono essere invariabili rispetto a T per prevenire la scrittura di dati a vita breve in uno slot che afferma di contenere dati a vita più lunga. Questa invarianza si propaga a RefCell, Mutex e altri tipi di mutabilità interna.

Come influisce PhantomData<T> sulla varianza di una struttura che non contiene realmente T, e perché utilizzeresti PhantomData<fn(T)> per ottenere contravarianza?

PhantomData<T> dice al compilatore di trattare la struttura come se possedesse un T ai fini della varianza e del controllo di eliminazione. Per impostazione predefinita, PhantomData<T> conferisce alla struttura la stessa varianza di T. Tuttavia, i puntatori a funzione hanno una varianza speciale: fn(A) -> B è contravariante in A (l'argomento) e covariante in B (il ritorno). Se hai bisogno che una struttura sia contravariante rispetto a una durata (significa che Struct<'long> è un sottotipo di Struct<'short> quando 'long vive più a lungo di 'short), devi usare PhantomData<fn(T)>. Questo è fondamentale per costruire callback o comparatori sicuri dal punto di vista dei tipi in cui la relazione tra le durate deve essere invertita.

In codice non sicuro, quando implementi una struttura auto-riferita usando puntatori raw, perché la struttura deve essere contrassegnata come invariabile rispetto ai suoi parametri di vita?

Quando una struttura contiene un puntatore raw che punta ad altri dati all'interno della stessa struttura (auto-riferita), la durata di quella struttura determina la validità del puntatore. Se la struttura fosse covariante rispetto alla sua durata 'a, potresti ridurre 'a a una durata più breve 'b, affermando effettivamente che la struttura vive solo per 'b. Tuttavia, il puntatore raw all'interno è stato creato quando la struttura viveva più a lungo e potrebbe puntare a dati che non sono più validi nello scope più breve. L'invarianza garantisce che la struttura non possa essere forzata a una durata più breve, preservando l'invarianza di sicurezza secondo cui l'auto-riferimento rimane valido per l'intera durata codificata nel sistema di tipi. Questo è il motivo per cui Pin è spesso combinato con marcatori di varianza espliciti nelle implementazioni auto-riferite non sicure.