1. Null pointer deref. Can be fixed by having optional types and requiring that possibly null pointers have to be wrapped in them.
2. Out of bounds references. Can be fixed by making the type system track how big all objects are, and having the compiler insert bounds checking.
3. Use after free. Can be fixed by the free function zero'ing heap objects smaller than a page (eg 4KB), and unmapping larger ones so that future accesses are a seg fault. The heap also needs to not create new objects at the same address as deleted ones, but we have 64-bit address spaces, so maybe that's fine.
These all have costs, but so do all solutions to these problems.
I can't think of any reasons that a language with manual memory management has to be less safe than one with a Garbage Collector / ARC.
This path leads you to something like Rust, which is safe without garbage collection or ARC, but I also wouldn't call it "manual memory management". The trade off they took for this is complexity in the language.
GP's proposal is to drop lifetimes and RAII (thereby simplifying the language semantics), make the programmer responsible for allocations and frees (as in C), and solve the temporal memory safety problem by doing additional work at runtime to ensure that use-after-frees reliably crash the process instead of overwriting return addresses or doing other arbitrarily bad things. Whether this is more fun to program in than RAII depends on whether you think it's better to suffer from too much abstraction or too little; people have sharply diverging intuitions on this and it's been a holy war since forever and it probably always will be.
The clearer-cut problem is that such a language would be slower than C or Rust, both because freeing memory involves extra work that C and Rust programs don't have to do, and because the requirement that use-after-frees must reliably behave a specific way inhibits optimization, since the compiler can't assume that use-after-frees don't occur. Also, using memory from a small allocation that's been zeroed out doesn't reliably crash the process unless a pointer in that allocation is dereferenced, and even then, this (contra point 1) would require the compiler to assume that null pointer dereferences can happen and must segfault, which, again, inhibits optimization.
Yes.
> are you suggesting that the compiler (or other static analysis) could catch all of the issues you listed?
Yes, the compiler for the first two and the standard library's heap implementation for the third.
> This path leads you to something like Rust
There are stops along this path before you get to Rust. If you just add the 3 things I mention above to a C like language, it would still be perfectly possible to leak memory. But that isn't a safety problem.
The 3 things don't include a borrow checker. You could still make doubly linked list and graph data-structures.
`Maybe(^T)` exists in Odin.
Bounds checking is on by default for all array-like access. Odin has fixed-length arrays, slices, dynamic arrays, maps, and #soa arrays, all of which support bounds checking. Odin does not have pointer arithmetic nor implicit array-to-pointer demotion which is pretty much removes most of the unsafety that languages like C have.
Odin also has built-in support for custom allocators which allows you do a lot more with extra safety features too beyond the default allocator. Use-after-free is usually also a symptom of an underlying value responsibility and lifetime problem rather than a problem in itself, of which is fundamentally an architectural issue. Ownership semantics in a language in Rust does deal with this issue BUT it does come at a huge cost in terms of architecting the code itself to accommodate this specific way of programming.
There is a common assumption amongst many of the comments that if you have manual memory management, you are defaulting to memory unsafety. This is untrue and memory management and memory safety are kind of unrelated in the grand scheme of things. You could have C with GC/ARC and still have all of its memory unsafety semantics.
One really good approach is to not use pointers in the first place and use handles. I highly recommend this post for more information: https://floooh.github.io/2018/06/17/handles-vs-pointers.html
Because use-after-free is a responsibility problem, handles are a way to make sure that a subsystem has responsibility over that memory directly rather than have it spread out across the program.
This is why Odin nor Zig "solve" this problem: solving it at the language level is not necessarily the best option.
See, having memory safety did not prevent the language from causing arbitrary code execution vulnerabilities. Having the log4j project be open source and popular did not prevent that either (so much for the "enough eye balls" theory).
Going back to Odin, when I think memory safety is not as big a concern as people make it out to be:
Your only source of concern is C.
This would be like judging SQL statements as fundamentally unsafe because websites written in PHP tended to (specially in the early 2000) be written in a very unsafe manner where user input was put directly into SQL strings.
The lesson that people took is not to throw SQL out the window, but to properly sanitize user input before passing to the queries, and to never use plain string concatenation when doing that.
So for manual memory management, the lesson to take from the vulnerabilities that C has caused is not that manual memory mangement is bad. It's that you need some facilities in the language to minimize the chance of them occurring by several orders of magnitude.
Odin does this by providing the slice type (and string type) that have their length known and providing several custom allocators out of the box.
The cool thing about the slice type is not just that the length is known: the language provides facilities for iterating over the slice that automatically never goes out of bound:
for item, index in slice {
// do something
}
This, and providing a "string builder" type into the core library that lets you dynamically construct a string in a safe way (you don't have to write the code to grow the string dynamically because it has already been done).These features make the "fear" of unsafe memory access largely unwarranted anymore.
What remains is a matter of what attracts you to programming: are you interested in having explicit control over a system to make it do what you want, or are you more interested in expressing some abstract ideas in an abstract mathematical virtual machine? If the latter, you might find Haskell or Lisp more appealing.
> This would be like judging SQL statements as fundamentally unsafe because websites written in PHP tended to (specially in the early 2000) be written in a very unsafe manner where user input was put directly into SQL strings.
This is indeed a good example, but supports my argument rather than yours. If we had devised ways to make it impossible (or incredibly hard and weird) to introduce sql injection vulnerabilities at the language level, then that would be excellent. One less thing to worry about!
I don't have a unique grudge against memory safety issues. It's just one type of issue that we've spent a lot of time devising solutions to that don't just boil down to "be very careful" and I'm generally supportive of any solution to any problem like that, if the tradeoffs are acceptable.
But I agree with you that it's a great thing to have language and library support that make memory safety issues significantly less common and problematic (to be clear, I only just heard of Odin from this article, but I think it looks pretty awesome on initial glance), and that that gets to the level of solution that we have for sql injection in practice. I think there are somewhat better alternative solutions available in the case of memory safety, but they have different tradeoffs.
> What remains is a matter of what attracts you to programming: are you interested in having explicit control over a system to make it do what you want, or are you more interested in expressing some abstract ideas in an abstract mathematical virtual machine?
I'm mostly interested in the first thing, but I think these are both false choices. There is a language that provides that explicit control with more memory safety (rust) with a different trade off (language complexity), and most other languages are memory safe without being focused on expressing abstract ideas in an abstract mathematical virtual machine (go, java, python, etc. etc.).
Σ exploits = Σ memory_corruption + Σ logic_errors
Having Σ memory_corruption ==> 0 is of course much welcomed outcome, even if Σ logic_errors > 0.