First of all, very quick history lesson: memory-safe languages have been around for decades. Java, for example, is aggressively memory-safe (aside from the risk of null reference exceptions, which can cause crashes but not memory corruption); it exposes no pointers (addresses) and allows no manual memory management. Yet the mountains of code written in Java still have vulnerabilities all the time.
Why?
Well, some of it is that, in the end, the features of the language that make it memory-safe have to be implemented in actual machine logic, which is decidedly not memory-safe. It doesn't do any good to have a language where all java.util.ArrayList
accesses are guaranteed bounds-checked if ArrayList
is implemented using a native memory buffer which, due to some pointer arithmetic resulting in integer overflows on certain platforms, occasionally thinks an index is safe when it isn't. Any language implemented on an actual, real-world, widely-used instruction set will face this issue, because all that the CPU understands are addresses and values.
But even aside from bugs in the compiler or runtime, there's no lack of logic bugs. Shell injections, like SQL injections and XSS (which really ought to be termed "script injection" or "html injection"), lead to arbitrary code execution without any memory corruption. Missing authorization checks, where some object that should only allow certain users access allows everybody instead or where something is R/W for a group where it should be RO, are common and can give access to all sorts of things. Cryptographic mistakes, like reusing the same IV/nonce and key for multiple operations or failing to include integrity checks on encrypted data, can break any system that depends on them. Spoofing attacks, where some message is assumed to be trustworthy but is attacker-controlled (often due to one of the errors above) can also lead to code execution.
It is impossible to design a language, even in theory, that is immune to such issues while still being Turing-complete.
There are attempts at doing this with specific chunks of code. "Provable correctness", where you attempt to exhaustively specify the input space and map it all to the correct outputs, and then verify (typically through static analysis) that the inputs all produce their correct outputs, is one attempt at this. However, even where the program isn't too complex for such proofs to be feasible, the idea breaks down because "inputs" include the entire environment in which the program runs, and "outputs" include all the detectable effects of the code (not just the actual value it returns). An algorithm that takes a 128-byte string as an input and returns true iff it exactly matches some secret value without ever exposing the secret itself is very easy to prove correct. However, if it uses an early-exit algorithm (where the first byte that doesn't match causes the function to immediately return false) then a timing attack reduces the difficulty of finding the secret from "brute-force it until the heat death of the universe without getting close" to "this might take a few hours, depending on how noisy the timing info is". That's the problem with provable correctness: if you didn't think to consider things like "the time the function takes must be constant", you won't consider "the time the function takes" as an output and therefore won't notice the way wrong inputs with longer matching substrings take longer to produce an output; you'll just see that they still return false and call the code Correct.