Over the last decade or so, there has been a strong push towards languages that are considered "memory safe", citing security as the primary reason. However, most security benefits attributed to memory safety languages can be very easily achieved by applying certain mitigations to unsafe languages:
-
Bounds checking: Eliminates all buffer overflows/underflows. Requires language support.
-
Control-flow Integrity (CFI): Checks whether indirect jumps (function pointers/vtable) are valid (at runtime). This mitigation when combined with hardware mitigations like Intel CET would prevent all remote-code execution (RCE) attacks.
-
Type-stable allocators: Traditional memory allocators reuse the set of same memory regions for objects of approximately same size (called "size classes"). The problem that would arise here is that when a use-after-free occurs and the two successive objects are of different data types, this could enable RCE attacks (this is known as type confusion). For example, a function pointer could overlap with data obtained from external source. The obvious solution is to reserve the same set of memory regions for objects of similar types instead of size classes.
One might ask, "Why are type-stable allocators are needed when there is CFI?". The reason is there are certain object-reuse based attacks that can leak sensitive information. For example, consider the following structs:
struct User { char secret[16]; }; ... struct Button { char label[16]; };
Because these structs belong to the same size class, type confusions arising from UAF between these two types can potentially lead to leakage of sensitive data.
-
W^X: Ensures whether a memory page is either executable or writable (but not both) using hardware memory protection capabilities. This could prevent execution of attacker's code, because data from untrusted source would mostly reside in a writeable but not executable page.
-
Stack cookies: Ensures that a stack buffer overflow can not overwrite the return address. This mitigation is especially useful for languages like C where arrays do not retain bounds information.
-
CHERI/ARM MTE: Hardware memory safety. Most bullet-proof of all mitigations. But currently not mainstream and it will take a while until hardware-level memory safety becomes mainstream.
Coming to the relevant part: how much less secure is an unsafe language with mitigations than safe Rust or a GC'd language? Not much, I'd say. For the sake of the argument let us assume that an "unsafe language w/ mitigations" is slightly less secure than safe Rust. What does that mean for the statisic that 70% of CVEs are memory-related? Obviously, almost all of those 70% bugs would not be exploitable with these mitigations. I do not understand how the "70% of CVEs" stat makes any sense when there are known and widely deployed mitigations that would thwart most of those. This is where people who have been advocating for memory safety are deliberately conflating two different things:
- Memory safety for security reasons.
- Memory safety as a language-level feature, for correctness reasons (using either GC or a borrow checker).
Now, security is very hard to argue against. But when it is possible to attain similar levels of security with certain mitigations, what benefits does a excessively complicated language like Rust or any other language with a borrow checker bring? Furthermore, any safe language with arrays and aliasing can have bugs that resemble use-after-frees (cf. "index confusion" bugs in Rust).
Looking back at history, only GC'd languages where considered memory safe. Memory safety while being a PLT concept, shot to fame because the term was used to market GC'd languages like Java (and Java became really popular, incidentally). Right now, memory safety is just a "label" used to promote certain languages.
Fil-C is an excellent example of the mitigations. (Mitigations, for some reasons, are looked down in security circles...).