Re: The Case for Rust (in any system)

From: Alan Somers <asomers_at_freebsd.org>
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.