https://kerkour.com/bugs-rust-compiler-helps-prevent Sylvain Kerkour Home About My Book Subscribe (Ab)using technology for fun & profit. Creator of Bloom and author of Black Hat Rust. We're sorry but this website doesn't work properly without JavaScript enabled. Please enable it to continue. Bugs that the Rust compiler catches for you Wed, May 4, 2022 Over the decades, Humans have proved to be pretty bad at producing bug-free software. Trying to apply our approximative, fuzzy thoughts to perfectly logical computers seems doomed. While the practice of code reviews is increasing, especially with the culture of Open Source becoming dominant, the situation is still far from perfect: it costs a lot of time and thus money. What if, instead, we could have a companion, always available, never tired, and the icing on the cake, that doesn't cost the salaray of a developer that would help us avoid bugs in our software before they reach production? Let's see how a modern compiler and type system helps prevent many bugs and thus helps increase the security for everyone and reduces the costs of software production and maintenance. Resources leaks It's so easy to forget to close a file or a connection: resp, err := http.Get("http://kerkour.com") if err != nil { // ... } // defer resp.Body.Close() // DON'T forget this line On the other hand, Rust enforces RAII (Resource Acquisition Is Initialization) which makes it close to impossible to leak resources: they automatically close when they are dropped. let wordlist_file = File::open("wordlist.txt")?; // do something... // we don't need to close wordlist_file // it will be closed when the variable goes out of scope Unreleased mutexes Take a look at this Go code: type App struct { mutex sync.Mutex data map[string]string } func (app *App) DoSomething(input string) { app.mutex.Lock() defer app.mutex.Unlock() // do something with data and input } So far, so good. but when we want to process many items, things can go very bad fast func (app *App) DoManyThings(input []string) { for _, item := range input { app.mutex.Lock() defer app.mutex.Unlock() // do something with data and item } } We just created a deadlock because the mutex lock is not released when expected but at the end of the function. In the same way, RAII in Rust helps to prevent unreleased mutexes: for item in input { let _guard = mutex.lock().expect("locking mutex"); // do something // mutex is released here as _guard is dropped } Missing switch cases Let's imagine we are tracking the status of a product on an online shop: const ( StatusUnknown Status = 0 StatusDraft Status = 1 StatusPublished Status = 2 ) switch status { case StatusUnknown: // ... case StatusDraft: // ... case StatusPublished: // ... } But then, if we add the StatusArchived Status = 3 variant and forget to update this switch statement, the compiler still happily accepts the program and lets us introduce a bug. While in Rust, a non-exhaustive match produces a compile-time error: #[derive(Debug, Clone, Copy)] enum Platform { Linux, MacOS, Windows, Unknown, } impl fmt::Display for Platform { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Platform::Linux => write!(f, "Linux"), Platform::Macos => write!(f, "macOS"), // Compile time error! We forgot Windows and Unknown } } } Invalid pointer dereference As far as I know, it's not possible to create a reference to an invalid address in safe Rust. type User struct { // ... Foo *Bar // is it intended to be used a a pointer, or as an optional field? } And even better, because Rust has the Option enum, you don't have to use null pointer to represent the absence of something. struct User { // ... foor: Option, // it's clear that this field is optional } Uninitialized variables Let's say that we are processing users accounts: type User struct { ID uuid.UUID CreatedAt time.Time UpdatedAt time.Time Email string } func (app *App) CreateUser(email string) { // ... now := time.Now().UTC() user := User { ID: uuid.New(), CreatedAt: now, UpdatedAt: now, Email: email, } err = app.repository.CreateUser(app.db, user) // ... } Good, but now, we need to add the field AllowedStorage int64 to the User structure. If we forget to update the CreateUser function, the compiler will still happily accept the code without any changes and use the default value of and int64: 0, which may not be what we want. While the following Rust code struct User { id: uuid::Uuid, created_at: DateTime, updated_at: DateTime, email: String, allowed_storage: i64, } fn create_user(email: String) let user = User { id: uuid::new(), created_at: now, updated_at: now, email: email, // we forgot to update the function to initialize allowed_storage }; } produces a compile-time error, preventing us from shooting ourselves in the foot. Unhandled exceptions and errors It may sound stupid, but you can't have unhandled exceptions if you don't have exceptions... panic!() exists in Rust, but that's not how recoverable errors are handled. Thus, by imposing the programmers to handle each and every error (or the compiler refuses to compile the program), all while providing good tools to handle errors (the Result enum and the ? operator), the Rust compiler helps to prevent most (if not all) errors related to error handling. Data races Thanks to the Sync and Send traits, Rust's compiler can statically assert that no data race is going to happen. How does it work? You can learn more in the good write-up by Jason McCampbell. Hidden Streams In Go, data streams are hidden behind the io.Writer interface. On one hand, it allows to simplify its usage. On the other hand, it can reserve some surprise when used with types we don't expect to be a stream, bytes.Buffer for example. And that's exactly what happened to me a month ago: a bytes.Buffer was reused in a loop to render templates which leads the templates to be appended to the buffer instead of the buffer to be cleaned and reused. It would have never happened in Rust as Streams are a very specific type and would never have been used in this situation. Some Closing Thoughts Are smart compilers the end of bugs and code reviews? Of course not! But a strong type system, and the associated compiler are a weapon of choice for anyone who wants to drastically reduce the number of bugs in their software and make their users / customers happy. 1 email / week to learn how to (ab)use technology for fun & profit: Programming, Hacking & Entrepreneurship. We're sorry but this website doesn't work properly without JavaScript enabled. Please enable it to continue. [ ] Subscribe I hate spam even more than you do. I'll never share your email, and you can unsubscribe at any time. --------------------------------------------------------------------- Tags: hacking, programming, rust, tutorial [black_hat_] Want to learn Rust, Cryptography and Security? Get my book Black Hat Rust!