We have 50 years of experience of code telling us that, no, programmers are not consistently capable of avoiding memory safety just by being good about it. Saying that it's just a failing of lesser programmers is the height of extreme arrogance, since I guarantee you that you've written memory safety vulnerabilities if you've written any significant amount of C code.
The problem is not that the rules are hard to follow--they're actually quite easy! The problem is that, to follow the rules, you need to set up certain invariants, and in any appreciably large codebase, remembering all of the invariants is challenging. As an example, here's a recent memory safety vulnerability I accidentally created:
int map_value(map_t *map, void *key) {
// This returns a pointer to the internals of map, so it's invalidated
// any time map is changed. Therefore, don't change the map while this
// value is live.
int *value = add_to_map(map, key, default_value());
// ... 300 lines of code later...
// oops, need to recurse, this invalidated value...
int inner = map_value(value, f(key));
// ... 300 lines of code later...
// Hi, this is now a use-after-free!
*value = 5;
return value;
}
It's not that I'm too stupid to figure out how to avoid use-after-frees, it's that in the course of refactoring, I broke an invariant I forgot I needed. And the advantage of a language like Rust is that it bops me on the head when I do this.
1) If you're gonna return pointers, don't use an allocation scheme that invalidates pointers in the implementation of map_t.
2) If you want to reallocate and move memory, don't return pointers, return abstracted handles/indices.
Both of those are completely possible and not particularly unergonomic to do in C.
If you need to enforce invariants, then enforce them, C does have enough abstraction level for that.
I one worked on a commercial product, a mix of C++ calling our own C libraries, that had 4000+ LOC in a single case statement. This was one of my first jobs out of school so I was shocked.
My experience reading lower level languages suggests me that this is not only possible but it seems that the higher the quality of the project (in scope, authors, products) the more of those you find.
When people focus on the _right_ implementation for _their_ problem they often find out that best practices like DRY or abstractions don't scale well at all.
The problem is not that the rules are hard to follow--they're actually quite easy! The problem is that, to follow the rules, you need to set up certain invariants, and in any appreciably large codebase, remembering all of the invariants is challenging. As an example, here's a recent memory safety vulnerability I accidentally created:
It's not that I'm too stupid to figure out how to avoid use-after-frees, it's that in the course of refactoring, I broke an invariant I forgot I needed. And the advantage of a language like Rust is that it bops me on the head when I do this.