Lifetime-vereisten van structs kunnen worden omzeild via covariante veldtoewijzing (Issue #164250)

Introductie en Context

Deze specifieke bug is geclassificeerd als P-high (hoge prioriteit). Hoewel een dergelijk lek normaal gesproken als 'kritiek' zou worden beschouwd, is deze prioriteit bepaald door het feit dat de bug al 11 jaar bestaat zonder dat deze in de praktijk breed is aangetroffen. Het is onwaarschijnlijk dat dit lek per ongeluk tot geheugenonveiligheid heeft geleid, aangezien exploitatie een opzettelijk kwaadaardige Drop-implementatie vereist.

De Soundness-bug

Het probleem draait om het feit dat lifetime-vereisten van een struct kunnen worden omzeild via covariante veldtoewijzing.

Voorbeeld van het lek

In de volgende code wordt een referentie die niet 'static is, toegewezen aan een veld dat een 'static lifetime vereist.

use std::sync::Mutex;

static KEPT: Mutex<Vec<Box<dyn std::any::Any + Send>>> = Mutex::new(Vec::new());

struct Q<T: 'static + Send>(Option<T>);

impl<T: 'static + Send> Drop for Q<T> {
    fn drop(&mut self) {
        if let Some(t) = self.0.take() {
            KEPT.lock().unwrap().push(Box::new(t));
        }
    }
}

fn main() {
    {
        let l = String::from("hello world, this is long enough");
        let mut q = Q(None);
        q.0 = Some(&l); // Hier wordt de lifetime-eis omzeild
    }
    let _z = String::from("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
    let k = KEPT.lock().unwrap();
    println!("{}", k[0].downcast_ref::<&'static String>().unwrap());
}

Verwacht resultaat: Het programma zou niet geaccepteerd moeten worden door de compiler. Actueel resultaat: Het programma compileert en print XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX. Bij gebruik van AddressSanitizer (asan) crasht het programma met een AddressSanitizer: stack-use-after-scope.

Analyse van de onveiligheid

De kern van de unsoundness kan als volgt worden vereenvoudigd:

struct Q<T: 'static>(Option<T>);

impl<T: 'static> Drop for Q<T> {
    fn drop(&mut self) {
        // De code hier heeft toegang tot een &String die niet 'static is,
        // maar gaat ervan uit dat het een &'static String is.
    }
}

fn main() {
    let l = String::new();
    let mut q = Q(None);
    q.0 = Some(&l);
}

Alternatieve varianten en bewijzen

Verschillende versies van deze bug zijn geïdentificeerd die aantonen dat het probleem breed aanwezig is in verschillende versies van Rust.

Variant zonder gebruik van static

Het is mogelijk om een vergelijkbaar effect te bereiken zonder direct op een static te vertrouwen:

struct MyType<'a, 'b: 'a, 'c>(&'c mut &'a str, &'b str);

impl<'a, 'b: 'a, 'c> Drop for MyType<'a, 'b, 'c> {
    fn drop(&mut self) {
        *self.0 = self.1;
    }
}

fn main() {
    let mut me_out = "rarw";
    {
        let l = String::from("some cool string");
        let mut q = MyType(&mut me_out, "");
        q.1 = &l;
    }
    println!("me_out: {me_out}");
}

Variant met Segfault (sinds versie 1.4)

Een versie die resulteert in een segfault of het printen van vrijgegeven geheugen:

struct Q<T, F: FnMut(&T)>(Option<T>, F);

impl<T, F: FnMut(&T)> Drop for Q<T, F> {
    fn drop(&mut self) {
        self.1(self.0.as_ref().unwrap())
    }
}

fn main() {
    let mut out: Option<&'static _> = None;
    {
        let l = vec![vec![1]];
        let mut q = Q(None, |s| {
            out = Some(*s);
        });
        q.0 = Some(&l);
    }
    println!("{:?}", out);
}

Variant met Threads (eenvoudigste weergave)

Deze versie illustreert duidelijk hoe een referentie die niet 'static is, wordt behandeld als een 'static referentie in een nieuwe thread:

use std::time::Duration;
use std::thread;

struct Q<'a: 'static>(Option<&'a String>);

impl<'a: 'static> Drop for Q<'a> {
    fn drop(&mut self) {
        // De code hier gaat ervan uit dat de referentie 'static is
        let wrong: &'static String = self.0.unwrap();
        thread::spawn(move || {
            thread::sleep(Duration::from_secs(1));
            println!("{}", wrong);
        });
    }
}

fn main() {
    {
        let l = String::from("abc");
        let mut q = Q(None);
        q.0 = Some(&l); // q.0 zou een &'static String moeten zijn, maar krijgt een lokale referentie
    }
    thread::sleep(Duration::from_secs(2));
}

Variant via Unification Coercion

Er is ook aangetoond dat unification in het algemeen de lifetime-bounds van het type niet controleert, zelfs zonder directe veldtoewijzing:

struct Q<T: Copy, F: FnMut(T)>(T, F);

impl<T: Copy, F: FnMut(T)> Drop for Q<T, F> {
    fn drop(&mut self) {
        self.1(self.0)
    }
}

type T = Vec<Vec<i32>>;

fn main() {
    let long = Box::leak(Box::new(T::new())) as &'static T;
    {
        let local: T = vec![vec![1]];
        // Het type is Q<&'local T, fn(&'static T)>, wat onjuist is
        let mut unified = [
            Q(
                long,
                (|s: &'static T| {
                    std::thread::spawn(move || {
                        println!("{s:?}");
                    });
                }) as fn(&'static T),
            ),
            Q(&local, (|_: &T| {}) as fn(&T)),
        ];
        let [l, r] = &mut unified;
        std::mem::swap(&mut l.0, &mut r.0);
    }
    std::thread::sleep_ms(1000);
}

Classificatie en Labels

  • Categorie: C-bug (Bug)
  • Impact: I-unsound (Soundness hole)
  • Prioriteit: P-high (Hoge prioriteit)
  • Teams: T-types (Types team)
  • Gebieden:
  • A-borrow-checker
  • A-destructors (Drop)
  • A-lifetimes
  • A-variance (Covariantie)