First Rust PR: Making AWS Firecracker Valgrind-Friendly

I decided to jump into the deep end of the pool for seeing what Rust's impact on a real security-focused project would be. So as my first Rust code, I put my mind toward attacking the oldest open GitHub Issue in the Firecracker virtual machine manager (open-sourced by Amazon Web Services). The issue's objective was to get the project analyzed with Valgrind:

--cleanup-level switch for Valgrind-friendly exit mode by AE1020 · Pull Request #2535 · firecracker-microvm/firecracker (github.com)

I was able to pare down the amount of reachable memory left after running an Alpine Linux VM from 111 allocations down to just 10. If anyone can make sense of the backtraces for the ten extant allocations--and if they are the kinds of things that are avoidable or not in Rust--that would be really helpful:

Backtraces of the still reachable allocations.

And generally, if anyone wants to read over the changes for feedback, the diff is a skimmable amount of code.

Language Impressions

My background is that I know a depressingly huge amount of C++ and C, and an embarrassingly small amount of Haskell. But it was enough that with the help of the rust-analyzer extension in VSCode, I was able to hack through things without having done any prior programming in Rust.

Tooling and debugging "just worked". (Though I had a bit of pain in the beginning because I didn't realize Firecracker's seccomp filtering was incompatible with the debugger, so breakpoints weren't being hit until I turned it off.) Being able to jump-to-declaration for Rust standard library functions and having coherent code with documentation right there is a major win.

The borrow checker is great, and I don't find it troublesome at all. But I'm the sort of person where it's preaching to the choir in terms of the need for the rules, so that's not surprising.

My first unhappy realization was that I could not add a constructor to an existing type, when I wanted to see at runtime when instances of that type were created. If your code is not already stylized to always use a ::new(...) function, you can't come in after the fact and hook reference sites that are using MyType {...} initialization.

C++ originated from the idea of being able to incrementally take advantage of it in C codebases. That incremental perspective means that the simple/naive code you write to start with has a smooth slope toward being able to ramp up your definitions with new abilities while keeping your callsites the same. This helps even if you're just doing those changes for instrumentation purposes.

Rust seems unfriendly to this...seemingly on general principle. In the case of function-like macros, you have to name them specially by ending them with !. So you can't swap in a macro for what you have 1,000 calls to under a name which doesn't end in !, even in a debug build. I'd really like a way to turn off this naming rule.

The "can't twist definitions without rewriting callsites" gripe is probably the biggest area of concern I've found. But overall I'd say it was a positive experience. I can imagine picking Rust to use for things in the future--though I don't have anything at the moment in mind. The next thing I'll probably look at will be learning about the limits of what macros can and can't do.