Skip to content

preallocated_gen_new is unsound #543

@Kixunil

Description

@Kixunil

Behold, use-after-free!

use secp256k1::Secp256k1;

fn make_bad_secp() -> Secp256k1::<secp256k1::AllPreallocated<'static>> {
    // in principle `Box` is not needed but if it's not here it won't show up as a crash.
    let mut array = Box::new([secp256k1::ffi::types::AlignedType::ZERO; 1024]);
    secp256k1::Secp256k1::<secp256k1::AllPreallocated<'static>>::preallocated_gen_new(&mut *array).unwrap()
}

fn main() {
    let secp = make_bad_secp();
    let secret = secp256k1::SecretKey::from_slice(b"release the nasal daemons!!!!!!!").unwrap();
    let pubkey = secp256k1::PublicKey::from_secret_key(&secp, &secret);
    println!("Dear compiler, do not optimize this computation {}", pubkey);
}

Yep, no unsafe. Curiously, this seems to corrupt the heap on my machine or something because it crashes after printing the key. Anyway, it's still UB so I'm lucky I didn't get nasal daemons. 🤷‍♂️

But, but, but there's C: 'buf bound!

The problem is C: 'buf means C outlives 'buf, so in our case 'static 'buf which is trivially true for all lifetimes 'buf.
In theory the bound should've been inverted: 'buf: C. But sadly, that's not a valid Rust syntax.

The bad news is I see no way of avoiding putting lifetimes everywhere with the current design.

Possibilities I can think of:

  • Make Secp256k1 unsized type (to prevent mem::swap which is curiously forbidden by secp). This will needlessly add size back as slice metadata but we can't do anything about it until extern types are stabilized and in our MSRV. I think Box<Secp256k1> could be valid, if not we need to have our own BoxedSecp256k1.
  • Add a lifetime parameter to Context, Signing, and Verification traits contaminating everything.
  • GAT can probably help but I can't think of how from top off my head and this would unreasonably bump MSRV so I don't want to spending much time on thinking about this one.

Maybe there's some ridiculous hack I didn't think about (my mind is not fresh rn), so if anyone knows that'd be great.

@apoelstra found another problem with the API: it's possible to cause invalid deallocation. Demo for reference:

fn main() {
    let mut array = [secp256k1::ffi::types::AlignedType::ZERO; 1024];
    let secp = secp256k1::Secp256k1::<secp256k1::All>::preallocated_gen_new(&mut array).unwrap();
    let secret = secp256k1::SecretKey::from_slice(b"release the nasal daemons!!!!!!!").unwrap();
    let pubkey = secp256k1::PublicKey::from_secret_key(&secp, &secret);
    println!("Dear compiler, do not optimize this computation {}", pubkey);
}

Notice that the array lives for the duration of the whole function so wrong lifetimes do not cause UB but invalid deallocation does.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions