Rust: Eliminating Use After Free Issues by Default
std::string s = “frayed knot”;
For those unfamiliar with C++ this is essentially assigning a string to the variable “s”. After the above line
executes a snapshot of memory is as follows:
(Image Credit to: “Programming Rust, Second Edition by Jim Blandy, Jason Orendorff, and Leonora F.S.
Tindall (O’Reilly). Copyright 2021 Jim Blandy, Leonora F.S. TIndall, and Jason Oredorff,
978-1-492-05259-3.”)
The actual variable “s” lives on the stack. It consists of 3 words: the pointer to its heap buffer, the capacity
of the string (its maximum size), and the length of the string (its current size). This is great, nothing wrong
with this at all.
The problem is when a temporary pointer to this string is created and this temporary pointer outlives the
variable “s”. In C++ it is valid to create a pointer to a character on the string’s heap buffer. So suppose we
have a variable “s_ptr” that points to the letter “f” on the heap buffer above:
We could get this using:
char* s_ptr = &s[0];
And the memory snapshot would become something like what’s shown below:
(Note s_ptr also lives on the stack, but points to the heap)
Again, still nothing bad. The issue is now with how C++ handles “s_ptr’s” lifetime. And by that I mean it
doesn’t. It’s entirely possible that the program destroys the variable “s” (e.g. “s” goes out of scope), which
means that its heap buffer will get freed. But C++ makes no promise to notify "s_ptr" of this destruction
leaving it to point to unowned memory. Because code like this compiles, the only way to guarantee that
the code is safe is if the programmer has considered every edge case and makes sure "s_ptr" is gone
before "s" frees up its heap buffer. While it’s not impossible to write safe code, it becomes much more
difficult when you have a large code base. And unfortunately it only takes one vulnerability to take a
system down.
Before diving into how Rust better handles such a situation, let’s consider why the above example could
lead to security issues. Below is a sample scenario:
Scenario 1: Being able to read Sensitive Data
Suppose that "s" gets destroyed by our program, but "s_ptr" is still alive and pointing to the heap buffer.
Let’s also suppose that "s_ptr" is a variable that the user can periodically ping via a button press to view
what it’s pointing to. Because “s” got destroyed, its buffer was freed. This means that if a new variable
allocates a string on the heap, the space “s” previously used is now available. Let’s dive into an example.
Suppose now the following line gets called:
std::string secret_password = “super secret”;
And it just so happens that the heap buffer for this string is at the exact same location that “s” was using,
that is:
And so because C++ didn’t check that “s_ptr” didn’t outlive the owner of the buffer it can now “snoop” on
any memory that it shouldn’t. Another possibility here is that rather than a password being stored an
attacker could have chosen malicious code to place on the heap which could potentially result in arbitrary
code execution.
Here is an example of the same type of issue described above in C++ code to help illustrate. The right
hand side of the screen shows the output of the code.
You can see that the string “s” is in scope only for the duration of the function “get_string_addr()” but C++
allows you to nonetheless return a pointer to the heap buffer of “s” and save it to “s_ptr” in the main
function. Initially printing “s_ptr” you can see that it contains the “frayed knot” string. But then after
randomly allocating a bunch of string buffers on the heap by calling the function “random_allocation()”
many times you can see that one of those allocations happened to occupy the same heap location pointed
to by “s_ptr” because when “s_ptr” is then printed again you can see that indeed it prints out
“PASSWORD” despite its address not being modified in anyway. This is an example of being able to
snoop on data you shouldn’t be able to.
Now let’s take a look at why this isn’t possible in Rust. When programming the equivalent of the above
C++ program in Rust there is one major difference: the Rust program wouldn’t compile. Before even
writing the whole program the Rust compiler recognizes that you are potentially creating insecure code:
Essentially Rust includes the concept of borrowing. Borrowing in simple terms is getting a reference to
some variable but a “non-owning” reference. So in the code above “&s” is a non-owning reference to the
variable “s”. But the Rust compiler detects that the value “s” won’t exist after this function returns and so
it is insecure to return a reference to “s” since its heap memory won’t be owned by “s” anymore.
While it is possible to get around the above issue by using unsafe rust, it makes more sense to trust
Rust’s compiler and have the reassurance that your code is safe of potential security issues.
A safer way to implement the C++ code in Rust would be to return the entire String object as follows:
The above code actually transfers ownership of the heap buffer to the variable “s_ptr” so no freeing of
memory occurred at all. And as seen above both times you get the same “frayed knot” string because
it’s impossible to write over the heap buffer since it was properly transferred to “s_ptr”
Now don’t get me wrong, you could easily write secure C++ code of this basic example. Probably just as
safe as Rust since this is really basic. But the main point is that C++ let’s you compile code that is
potentially dangerous, and Rust does not. In a large code base it’s probable that among many C++ files
there could be at least one of these bugs. Whereas if your Rust program compiles it simply guarantees
no such bugs exist anywhere.
In short while this example is trivial, use after free vulnerabilities like the C++ shown earlier are extremely
common. Rust eliminates this issue by only compiling code that won’t allow for such issues. There are
many ways to still achieve the same effect with Rust. You may need to be a little flexible, but to me it
seems this is a small price to pay for such a nice guarantee.
Comments
Post a Comment