Re: The Case for Rust (in any system)
Date: Thu, 05 Sep 2024 22:37:27 UTC
On Thu, Sep 5, 2024 at 3:13 PM <ske-89@pkmab.se> wrote: > > Alan Somers <asomers@freebsd.org> wrote: > > In fact, of all the C bug fixes that I've been involved with (as > > either author or reviewer) since May, about three quarters could've > > been avoided just by using a better language. > ... > > To summarize, here's the list of this week's security advisories, and > > also some other recent C bug fixes of my own involvement: > > After checking several of these examples, I'm wondering what the code > would have looked like in some "better language", where those bugs would > have been avoided? > > E.g for the "use after free" or "unitialized memory" examples. > > To me, several of those bugs seem fairly complex, and not just a > question of having bounds checking for arrays or a borrow checker > for pointers, or something simple like that. > > But maybe the bugs could have been detected and prevented if the > code would have been forced to be expressed in a completely > different manner by some other language? Or what is your vision > of how that would be accomplished? > > You seem to be saying that certain examples would be solved by > a better language, and certain ones would not, so I suppose you > do have some vision of how that would work. > > I'm just curious to learn more, since it is not obvious to me, > and thus all the more interresting. > > /Kristoffer Eriksson Excellent question. Here's why a selected sample of those bugs would've been prevented had the programs been written in Rust. 2909ddd17cb4d750852dc04128e584f93f8c5058 Rust uses RAII wherever possible. Variables are automatically deallocated when they leave scope. Circular references are almost impossible to create due to the lifetime borrow checker. So bugs like this really just can't happen in idiomatic Rust code. CVE-2024-45063 Written in idiomatic Rust, the lun->write_buffer would've had a type like Option<Box<Vec<u8>>>. The only way to free that would be to remove the contents of the Option, leaving None in its place. So a subsequent use-after-free would be impossible. The bug would still be present, but instead of a use-after-free the READ BUFFER command would have to create and zero-initialize a new buffer. The bug would be immediately obvious to the user since READ BUFFER would return the wrong data (all zeroes). CVE-2024-8178 Rust abhors uninitialized data. LLVM doesn't even guarantee that a program will run correctly when accessing it. So written in idiomatic Rust, lun->write_buffer would either be zero initialized, or it would be allocated like `Vec::with_capacity(262144)`. In the latter case, it would be partially initialized during the WRITE BUFFER command. But given the semantics of SCSI's READ BUFFER and WRITE BUFFER commands, I think zero-initialization is more appropriate. CVE-2024-6119 In Rust, initializing a union via one member and then accessing it via another is actually considered to be the same thing as reading uninitialized memory, and LLVM abhors it. The idiomatic solution is to use a enum (which is similar to a Java enum) instead of a union. The enum is basically a tagged union, so the programs knows at runtime which member is initialized. That makes bugs like CVE-2024-6119 impossible in idiomatic Rust code. CVE-2024-41928 This bug involved zero-initialing a structure, but with the wrong size. Idiomatic Rust code never uses anything like bzero. In fact, zero-initializing a structure is considered unsafe, because an all-zero pattern isn't valid for all structures. To initialize a structure in Rust, you either need to provide the value of every member or else use an initializer function. The simplest intializer is often STRUCT_NAME::default(), which can be automatically derived and is often equivalent to bzero. But all of those methods know the size of the structure, so bugs like this aren't possible in idiomatic Rust code. CVE-2024-45287 In debug builds of Rust, integer math operations are by default bounds-checked at runtime. That catches many bugs like this. For release builds, integer math operations are wrapping by default, but the programmer can also select bounds-checking. In this particular case, however, a Rust programmer wouldn't have attempted to multiply those two integers together. Instead, `value` would've been a Vec of some type, and it would be initialized like `Vec::with_capacity(nvp->nvp_nitems)`. 1f5bf91a85e93afa17bc9c03fe7fade0852da046 Rust's borrow checker will ensure that a single variable cannot be modified from two locations at the same time, or modified in one and read from another. This check happens at compile-time, with 0 runtime cost. For cases whether the compiler cannot determine whether the access is safe, various runtime options are available, like Mutex. In this case, the function's author actually performed a cast to remove "const" from the variable. Rust makes such casts harder, and it's better type system makes them far less necessary. 35f4984343229545881a324a00cdbb3980d675ce and eced2e2f1e56b54753702da52a88fccbe73b3dcb In idiomatic Rust, a falliable function returns a `Result` type, and the compiler is smart enough to know when a programmer ignores the Result. It will generate a warning, and most projects are configured to treat that type of warning as an error. So bugs like this don't usually happen. They can, however. A programmer can deliberately ignore the error, as in eced2e2f1e5 , or he can "unwrap" it. That means "panic on error", which is not terribly different from the bug fixed by 35f49843432. So it's possible but far from certain that a Rust implementation would've prevented these bugs. That's why in my summary I said "about three quarters" could've been avoided.