Is mem::forget(mem::uninitialized()) defined behavior?
Asked Answered
A

1

13

In mutagen, I'm injecting various mutations in the code. One thing I'd like to mutate is the pattern if let Ok(x) = y { .. }. However, this poses quite the challenge, as I cannot know the type of y – the user could have built their own enum with an unary Ok variant. I can still opportunistically mutate it for cases where we actually have a Result whose error type implements Default using a trait that looks like the following simplified:

#![feature(specialization)]

pub trait Errorer {
    fn err(self, mutate: bool) -> Self;
}

impl<X> Errorer for X {
    default fn err(self, _mutate: bool) -> Self {
        self
    }
}

impl<T, E> Errorer for Result<T, E>
where
    E: Default,
{
    fn err(self, mutate: bool) -> Self {
        if mutate {
            Err(Default::default())
        } else {
            self
        }
    }
}

Alas, there aren't that many errors which implement Default, so this is not too useful. Even an implementation for Result<T, Box<Error>> would give us more bang for the buck (and be completely possible). However, given that I don't care much about code actually inspecting the error, I wonder if I could do a general implementation by extending the mutation of the above code to

match Errorer::err(y, mutation) {
    Ok(x) => { .. }
    Err(x) => { mem::forget(x); }
}

and have err return Err(mem::uninitialized()) when mutating – so is this behavior safe? Note: I'm returning Err(mem::uninitialized()) from a method, only to mem::forget it later. I see no way this could panic, so we should assume that the value will be indeed forgotten.

Is this defined behavior or should I expect nasal demons?

Armagnac answered 4/5, 2018 at 22:52 Comment(3)
Something that comes to mind: Rust is free to manipulate how it represents enums based on the representation of its variants' payloads (i.e. the trick that makes Option<Box<T>> the same size as Box<T>). I'm not aware of any layout optimisations that affect Result, but I also see no reason they couldn't be added in future. If that were the case, mem::uninitialized could leave the E value in an invalid state that changes how the Result is interpreted.Helbona
Yeah, I figured as much. So there is no guarantee of defined behavior when it comes to putting invalid states into enums.Armagnac
Not sure whether it's the case yet, but Result<T, !> could and (someday) should have the same layout as T.Lofty
T
10

No, this is not defined behavior, at least not for all types. (I can't tell how your code would be called as part of mutation, so I don't know if you have control over the types here, but the generic impl sure makes it look like you do not.) That's demonstrated by the following piece of code:

#![feature(never_type)]
use std::mem;

fn main() {
    unsafe { mem::forget(mem::uninitialized::<!>()) }
}

If you run this on the playground, you will see the program die with a SIGILL. The ASM output shows that LLVM just optimized the entire program to immediate SIGILL because of the way it uses a value of the uninhabited type !:

playground::main:
    ud2

Generally speaking, it is near impossible to correctly use mem::uninitialized in generic code, see e.g. this issue of rc::Weak. For this reason, that function is in the process of being deprecated and replaced. But that won't help you here; what you want to do is just outright illegal for Result<T, !>.

Tannenberg answered 7/5, 2018 at 15:47 Comment(1)
LLVM just optimized the entire program to immediate SIGILL — now that's efficient code generation.Bullis

© 2022 - 2024 — McMap. All rights reserved.