From: Ralf Jung Date: Sat, 15 Jul 2017 02:19:24 +0000 (-0700) Subject: post on UB X-Git-Url: https://git.ralfj.de/web.git/commitdiff_plain/c9758e7c5f610af60233bca358ee78ee0e45c08b?ds=inline post on UB --- diff --git a/personal/_posts/2017-07-14-undefined-behavior.md b/personal/_posts/2017-07-14-undefined-behavior.md new file mode 100644 index 0000000..d664512 --- /dev/null +++ b/personal/_posts/2017-07-14-undefined-behavior.md @@ -0,0 +1,86 @@ +--- +title: Undefined Behavior and Unsafe Code Guidelines +categories: internship rust +--- + +Last year, the [Rust unsafe code guidelines strike team](https://internals.rust-lang.org/t/next-steps-for-unsafe-code-guidelines/3864) was founded, and I am on it. +This is my first official Rust-related position. :-) +So, finally, with just less than one year of a delay, this is my take at what the purpose of that team is. + +Warning: This post may contain opinions. You have been warned. + +## When are optimizations legal? + +Currently, we have a pretty good understanding of what the intended behavior of *safe* Rust is. +That is, there is general agreement (modulo some [bugs](https://github.com/rust-lang/rust/issues/27868)) about the order in which operations are to be performed, and about what each individual operation does. + +For unsafe Rust, this is very different. +There are multiple reasons for this. +One particularly nasty one is related to compiler optimizations that rustc/LLVM either already perform today, or want to perform some day in the future. +Consider the following simple function: +{% highlight rust %} +fn simple(x: &mut i32, y: &mut f32) -> i32 { + *x = 3; + *y = 4.0; + *x +} +{% endhighlight %} +We would like the compiler to be able to *reorder* these two stores without changing program behavior. +After all, `x` and `y` are both mutable references, which the type system ensures are unique pointers, so they cannot possibly alias (i.e., the memory ranges they point to cannot overlap). +After this transformation, the code contains `*x = 3; *x`, which can be further optimized to `*x = 3; 3`, saving a memory access. +Compilers are able to get a lot of performance out of code by figuring out which operations are independent of each other, and then moving code around to either eliminate certain operations entirely (like the load of `x`), or making code faster to execute with clever scheduling that exploits the [parallelism](https://en.wikipedia.org/wiki/Instruction-level_parallelism) in modern CPU cores (this is per-core parallelism we are talking about, not the parallelism arising from having multiple cores). + +Optimizations like reordering stores are based on the compiler making *assumptions* about the code, and then using these assumptions to justify a program transformation. +In this case, the assumption is that the two stores never affect the same address. +Usually, if a compiler wants to make such an assumption, it has to do some static analysis to *prove* that this assumption actually holds in any possible program execution. +After all, if there is any execution for which the assumption does *not* hold, the optimization may be incorrect -- it could change what the program does! + +Now, it turns out that it is often really hard to obtain precise aliasing information. +This could be the end of the game: No alias information, no way to verify our assumptions, no optimizations. + +## But we want to optimize anyway! + +However, it turns out that compilers writers want these optimizations *so badly* that they came up with an alternative solution: +Instead of putting the burden for verifying such assumptions on the compiler, they put it on the programmer. + +To this end, the C standard says that memory accesses have to happen with the right "effective type": If data was stored with a `float` pointer, it must not be read with an `int` pointer. +If you violate this rule, your program has *undefined behavior* (UB) -- which is to say, the program may do *anything* when executed. +Now, if the compiler wants to make a transformation like reordering the two stores in our example, it can argue as follows: +In any particular execution of the given function, either `x` and `y` alias or they do not. +If they do not, reordering the two writes is just fine. +However, if they *do* alias, that would violate the effective type restriction, which would make the code UB -- so the compiler is permitted to do anything. +*In particular*, it is permitted to reorder the two writes. +As we have seen, in both of the possible cases, the reordering is correct; the compiler is thus free to perform the transformation. + +Undefined behavior moves the burden of proving the correctness of this optimization from the compiler to the programmer. +Unfortunately, it is often not easy to say whether a program has undefined behavior or not -- after all, such an analysis being difficult is the entire reason compilers rather rely on UB to perform their optimizations. +Furthermore, while C compilers are happy to exploit the fact that a particular program *has* UB, they do not provide a way to test that executing a program *does not* trigger UB. +In fact, it is pretty hard to perform such a test; the standard has not been written with such considerations in mind. +This has given UB a very bad reputation, and mostly rightly so, I would say. +[A lengthy blog post](https://blog.regehr.org/archives/1520) summarizes the situation quite well: +There is progress being made with various sanitizers, but e.g. for the effective type restriction we discussed above, the mitigation (i.e., the way to check or otherwise make sure your programs are not affected) is "turn off optimizations that rely on this". +That's not very satisfying. + +## Undefined Behavior in Rust + +Coming back to Rust, where are we at? +Safe Rust is [free from UB]({{ site.baseurl }}{% post_url 2017-07-08-rustbelt %}), but we still have to worry about unsafe Rust. +For example, what if unsafe code crafts two aliasing mutable references (something that is prevented in safe Rust) and passes them to our `simple` function? +This violates the assumptions we made when we reordered the two writes. +If we want to permit this optimization (which we do!), we have to argue why it cannot change program behavior. +It should be forbidden for unsafe Rust code to pass aliasing pointers to `simple`; doing so should result in UB. +So we have to come up with rules for when Rust code is UB. +This is what the unsafe code guidelines strike team set out to do. + +We could of course just copy what C does, but I hope I convinced you that this is not a great solution. +When defining UB for Rust, I hope we can do better than C. +I think we should strive for programmers' intuition agreeing with the standard and the compiler on what the rules are. +It turns out that is [not the case for C](https://www.cl.cam.ac.uk/~pes20/cerberus/notes50-survey-discussion.html), which leads to miscompilations and sometimes to security [vulerabilities](https://lwn.net/Articles/342330/). + +I also think that tooling to *detect* UB is of paramount importance, and that the specification should be written in a way that such tooling is feasible. +In fact, specifying a dynamic UB checker is a very good way to specify UB! +Such a specification would describe the additional state that is needed at run-time to then *check* at every operation whether we are running into UB. +It is with such considerations in my mind that I have previously written about [miri as an executable specification]({{ site.baseurl }}{% post_url 2017-06-06-MIR-semantics %}). + +Coming up next on this channel: During my [internship]({{ site.baseurl }}{% post_url 2017-05-23-internship-starting %}), I am working on such a specification. +I have a draft ready now, and I want to share it with the world to see what the world thinks about it.