Memory Safety Absolutists
44 points by ekuber
44 points by ekuber
Here is a quote by Andrew Kelly that the OP apparently found offensive:
Zig can offer its users an option to compile their projects, including entire tree of C/C++ dependencies, into an executable that is fully memory safe, with no escape hatch, at a ~1-6x performance penalty (depending on prevalence of pointer chasing).
Once this is implemented then, in a twist of fate, Zig will become one of the only toolchains capable of producing actually memory safe executables, especially when you consider that real world applications tend to have C/C++ dependencies and, in the case of Rust, additional use of unsafe beyond FFI.
The author seems to be treating this as an attack against Rust, and closes the article with an attack against Zig.
My own reaction is that Fil-C and the new Zig build mode are positive contributions to the ecosystem. Andrew's point about this build mode generating fully memory safe executables is a useful and valid point. The comparison to Rust is technically relevant. This new Zig build mode legitimately exists at a different point in the design space than Rust, and makes different tradeoffs. That is worth knowing about.
Andrew's parenthetical remark is also worth noting: “at a ~1-6x performance penalty (depending on prevalence of pointer chasing)”. I interpret this to mean that if you use the data-oriented programming style preferred by the Zig core team, then the performance penalty can be brought closer to 1×.
The author is replying to the title: "introduce an actually memory safe (unlike Rust) compilation mode inspired by Fil-C", (as well as Fil-C's author's twitter).
I don't think it's entirely unfair of people to read that as unnecessarily snipey. Andrew has since changed the title to something a little less provocative.
The old, provocative title is also just true though. Rust has much more memory safety than C or C++ or traditional Zig, but you rely on every line of unsafe and every line of C/C++ code you link against, to be bug free. Same with Go, or Java or Python (they don't have unsafe but depend on large swathes of C).
Being able to compile normal C and C++ code with strong memory safety guarantees is a very significant improvement in memory safety which should not be under estimated. This does not mean Rust is bad.
Same with Go, or Java or Python (they don't have unsafe but depend on large swathes of C).
Go has unsafe, and relying on a little bit of C is optional (you can disable cgo) (unless you mean the kernel it runs on).
Is the fil-c (and the zig version) fully safe? As in, you can't introduce a memory-related bugs by just being silly? Asking not because I doubt you, but because I don't know the details here and am curious. I'll have to read a bit more about these things.
AFAIK it's fully safe, unless you have data races. Just like in Go. And also like Go, there are no plans to change that.
Yes you can force pointer tearing by certain uses.
However if one properly makes use of the stdatomic ops, or even simply volatile or _Atomic pointers, then one of the 128 byte distinct atomic pointers slots is allocated under the covers, and so the tearing is avoided.
So it can be as simply as tagging certain pointers as volatile. Which ends up being compatible with the standard, even if not with most current de facto loose implementations, where any pointer ends up using atomic accesses (on ARM64 or AMD64).
Andrew's point about this build mode generating fully memory safe executables is a useful and valid point.
People keep saying this as if it's true, but is it actually true? As far as I know Fil-C only does its tracking at allocation granularity, so programs can corrupt state as long as it's within the same allocation. You can certainly define memory safety as only applying at the granularity of allocations, but this is a weaker definition of memory safety from Rust's. Not saying Fil-C is worthless, but this feels like a repeat of "Rust projects have so many CVEs because the Rust community's standards for correctness are much higher".
But not every software program is a simple utility. There are lots of popular projects, where GC and ABI incompatibility are an issue and there is no way they would ever start using Fil-C like technology, or at least not in a current form.
The author should watch the Fil-C presentation from the recent Software Should Work conference before dismissing Fil-C as only for "simple utilities". The author delivers the presentation using a laptop running a linux userland entirely on Fil-C, using OpenOffice Impress, also entirely on Fil-C. Sure, there are C and C++ programs for which Fil-C wouldn't present a good trade-off, but it sure doesn't look as much like a toy as the author seems to think.
As someone who came from Python, memory safety was the baseline. I came to Rust for three things:
Compile-time correctness via a strong type system, of which memory safety with the aid of #![forbid(unsafe_code)] and cargo-geiger-aided dependency auditing is the least interesting expression.
Not just an execution model, but also an ecosystem to make it as easy as possible for me to safety write code once and then share it between the various different languages and execution environments I work in. (If not for this, I could have switched to something like Haskell ages ago.)
Syntactic sugar like try!(x) (x? didn't exist yet) that makes it comfortable to write high-level code.
From what I've seen of it, Neither Zig nor Fil-C satisfy my interest in teaching the compiler to catch logic errors by encoding the invariants into the type system (eg. the typestate pattern, newtypes, etc.) and Fil-C's ABI incompatibility certainly is a problem for writing memory-safe compiled modules for stock runtimes like the CPython on my shared webhost.
Heck, from what I've seen that combination of "No GC and/or ABI-compatible with existing C", memory safety, and "feels surprisingly high-level" is common among reasons people give for what draws them to Rust.
You can certainly do typestate pattern in Zig, and also encode a lot of stuff in the typesystem. In some ways comptime is even more expressive than the tools Rust provides for this. There are tradeoffs of course: compilation time, verboseness, and most of the ecosystem not even attempting to go as far as this, but it is indeed possible. I always end up doing it and it's pretty fun.
True, but then you get into a few other points:
First, there's always a lot you can do. I have an on-hold DOS project where I implemented Rust-esque string slices in C, and it's on hold in the middle of migrating from Splint-checked "typedefs as newtypes" toward "single-element structs as newtypes". Do any of the code samples or dependencies I'm going to rely on do that? Hell no.
Second:
It's not comptime that breaks parametricity in Zig, it's that Zig does not have an equivalent to Rust traits or Haskell typeclasses that would express and limit what you're allowed to do with the comptime type that is passed in. Zig's type manipulation machinery is imperative rather than declarative, but that does not make it incompatible with parametricity. Zig has just chosen not put those restrictions on its comptime types.
-- dcreager
Third, that ties into one of the other reasons I don't use Haskell... Rust and its ecosystem believe in "fearless upgrades" a lot more and the declarative parametricity of Rust's traits enables that more than Zig's comptime.
I'm reminded of matklad's Zig and Rust from 2023:
I find myself often returning to the first Rust slide deck. A lot of core things are different (no longer Rust uses only the old ideas), but a lot is the same. To be a bit snarky, while Rust “is not for lone genius hackers”, Zig … kinda is. On more peaceable terms, while Rust is a language for building modular software, Zig is in some sense anti-modular.
Modularity, both within my own creations and between my creations and the rest of the ecosystem, is a core element of why Rust was the first contender to unseat Python as my preferred language.
I really care a lot about memory safety, so to me Fil-C is a no brainer, ditto Zig implementing Fil-C ABI. If your Rust project has C dependencies memory safety guarantee is weaker: it is possible to stay pure Rust but it is inconvenient. So to me, obviously, Rust also should implement Fil-C ABI so that C dependencies can be built memory safe and linked to memory safe Rust. It all seems obvious to me. I am not sure why this is controversial.
Rust already has a Miri interpreter that does it's own set of checks to make sure unsafe rust satisfy the guarantees safe rust implements. If rust implemented something like this I would hope that it is for debug builds only and only as an aid to making better FFI related code.
Having a garbage collector and a runtime checking every operation would not fit all use cases
As far as I know Miri can't run C dependencies today. I would also support C compiler that can target MIR. Sounds like a good idea anyway. C++ compiler, now that is a scary thought.
The whole point of Fil-C is to make it fast enough so that you can enable it in release builds, so I am against being debug builds only.
As for GC and runtime, obviously it will just be an option, and it won't be a default for a long time (although I eventually hope it becomes a default). Also Fil-C ABI and GC are orthogonal. You can implement Fil-C ABI without GC, and this is Zig's plan. See Zig's issue for details.
I think it deserves pointing out the scale of reaction elicited from rust devs when the merest hint of “your language isn’t memory safe” ribbing starts flying their direction for a change. I’m a rust guy and like the language quite a bit! But come on, fair is fair.
Indeed, "Rust is pretty memory safe, but Fil-C is more safe in terms of memory safety" doesn't seem to me that a controversial statement. Seems true, and it is a good thing? I mean, formally verified C in seL4 is more safe still.
I wish I had a succinct way to describe that Fil-C's safety is a NullPointerException-style safety ("well the program will crash instead of using a corrupted pointer").
This is good of course, in that you don't want busted memory accesss! But somehow when writing new code I'd ... rather just have a language ecosystem where I almost never need raw pointers in the first place.
Like at one point we have to talk about objectives of having something maintainable and that doesn't have to pick between crashing and memory corruption.
Funny enough, Rusts very early website had the claim "A language that doesn't segfault" until someone pointed out that there are cases where you still can run into segfaults.
It follows the definition of safety that everyone else uses. Rust is also happy to panic if you're doing dangerous stuff that it can't detect at compile time. Rust is better than Fil-C at protecting against application bugs which cause crashes thanks to its functional-language-inspired use of sum types and pattern matching, but nobody has ever claimed that this has anything to do with "safety"; in Rust's definition, unwrapping a None is a perfectly safe operation because it triggers a controlled application crash.
Fil-C-inspired Zig is probably somewhere in between. My understanding is that Zig has some features which make it easier to avoid bugs, such as differentiating between nullable pointers and non-nullable pointers and differentiating between pointers to single elements and pointers to a contiguous array of elements. But I can't really speak to that since I don't know Zig.
Zig uses proper sum types, like rust. And it’s idiomatic to pass in effects (allocators, io, etc), and to make use of defer instead of RAII.
I’d estimate both give you productivity and correctness advantages over less modern languages. On memory safety, rust prefers to prove your program is correct (unless you opt out with unsafe) and zig tends toward good debug allocators, tests, and now this fil-c-like approach.
I think what’s left is a classic soundness vs completeness debate, plus rust more usefully models race conditions on memory access via ownership semantics.
I feel there is a language lurking in the future that is better than either, though. Maybe it’s just a future version of one of these. I dunno.
The way you phase it is a true statement, but that’s not the formulation that the article is responding to. The article is responding to the formulation “introduce an actually memory safe (unlike Rust) compilation mode inspired by Fil-C”, where “memory safe (unlike Rust)” is inappropriately flamey.
AFAICT, there’s a sincere and insincere way of approaching Fil-C relative to Rust.
If you have pre-existing C code, and instead of rewriting in Rust, you recompile with Fil-C and actually deploy the Fil-C version, that’s great. That’s the sincere case.
However, there’s the insincere form of using Fil-C as a way to paint Rust not “actually memory safe” if one is in the C/C++/Rust/Zig space due to considering performance, ABI, target support, etc. characteristics as a reason not to use a language that’s been designed to be a garbage collected language, then turn around to suddenly accept the overhead and the current x86_64 Linux-only state of Fil-C for the moment of using Fil-C as a rhetorical device to flame about Rust, and then turning around again to proceed not to actually deploy as Fil-C.
I suppose in some sense I'm quite like Andrew Kelly ironically. But I really like "correct" programs, and largely refuse (To the best of my ability) to write anything that isn't "correct". The irony of course is that I prefer Rust over zig to that end.
There are some real upsides to picking Zig over Rust. It's easier to exit correctly on memory allocation in zig, compile times are better, etc.
I do not know if Fli-C is "more memory safe" than Rust. But the GC (for my applications) is a dealbreaker. So is it's apparent C ABI incompatibility.
One can always make any language immune to "race conditions" by single threading the task, or make it memory safe by introducing a garbage collector. The thing I like about Rust is that you get both with neither compromise.
What do you mean by refusing to write anything that isn't correct? More so than other devs? I'm curious about the methods people go through to ensure broader correctness. Like are you proving out the logic in a prover, or using DST, or 100% code (instruction?) coverage?
I know people really care about this stuff, but outside some blog posts and diving into individual techniques there doesn't seem to be a quick compilation of "here's all the various methods to ensure this is totally 100% correct in this critical system."
It's a bit clearer to me how you'd start with an embedded system on set hardware...but it's WAY less obvious to me what extremes of correctness are possible when talking about something like even a simple web service, game, or cli running ontop of an OS.
I appreciate the Zig community, Tigerbeetle, and matklad's and Hillel Wayne's writings for exposing me to what little I do know here. But I'm wildly curious what more is out there since surely safety critical/life critical systems have more assurances.
I don't think it's fair, because the comparisons are exploiting the imprecision of the term.
Rust defines its promises as "memory safety(1)", but you can say the true definition is "memory safety(2)", and then go around saying "Rust lacks memory safety(2)" (technically true) knowing it will be perceived as "Rust lacks memory safety(1)" (FUD).
The differences are subtle and deep in the "it depends" territory, so it's a linguistic hack that makes it very difficult to call bullshit on it.
When you point out that Fil-C is just a very high overhead way of adding memory sandboxing to C that WASM could do already, the author will tell you all about the difference between "memory safety(2)" of Fil-C and "memory safety(3)" of WASM.
I think that wrangling over the definition of "memory safety" is a sign that something exciting is happening! When a technology expands our capabilities it also expands or transmutes the definitions we use. This leads to a period of miscommunication and strife. Prior to this the term "memory safety" essentially meant "what all the GC'd VM languages like Java do (pay no attention to sun.misc.Unsafe) and what rust does when you don't use unsafe and what CHERI will eventually do when someone actually makes a chip that implements it". The meaning & ownership of the term becomes a point of contention.
The genuine innovation in Fil-C is precision of runtime UB detection. Arbitrary C code could already be safely contained in sandboxes that have a lower overhead, but Fil-C narrowed the containment from a block of address space (like WASM/RLBox) or best-effort approximations like Address Sanitizer to a runtime detection with tight guarantees. Fil-C is a Valgrind killer. That's the term it should be wrangling for, but that would demonstrate it's an incremental improvement, not a holy grail. That would be boring.
The fact that a high-overhead GC sandbox is even mentioned among nearly-zero-overhead memory-safety solutions is a clever marketing positioning. We've already had Emscripten that was compatible with C syntax and had more memory safety than unsafe Rust, and yet it wasn't presented as a miracle that fixes Rust's fatal flaw.
Just to be clear about how things work, I compiled the following code with fil-c:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
struct User {
char name[8];
int is_root;
};
int main(int argc, char*argv[]) {
struct User* user = malloc(sizeof(struct User));
strcpy(user->name, argv[1]);
if (user->is_root) {
printf("I am root!\n");
} else {
printf("I am not root :(\n");
}
return 0;
}
When I run it with "./fillo 012345678", it prints out "I am root!".
To me, this is surprising. I thought the claims about out-of-bounds checks included things like this. Especially since they are made in comparison to languages that would catch this case.
I, personally, would be very shy about claiming this is memory safe. It is, in the most technical sense/definition. And, it's an improvement when you're starting with existing C/C++ code. But, c'mon ...
Why didn't something like Fil-C appear earlier? E.g. 20 years ago?
It did. The mainframe guys have been screaming about security problems of sharing and tagging pointers and capabilities for DECADES.
But nobody wanted to pay for it in terms of CPU or memory (aka money).
I know tagged pointer hardware was tried. Why it was rejected is easy to understand (it is new hardware). I think the question is why quality implementation of software tagged pointer for commodity hardware was not tried much earlier.
People whined incessantly about garbage collection until basically Java/C# and then Javascript/Python became so dominant that you couldn't whine about it anymore. That was <20 years ago and really only less than 10 years if I'm being honest.
There was no way you were going to get anything slower and more invasive off the ground.
I would also argue that there is a lot of "Shoot the Messenger" in the C/C++ space. If your "memory safe" runtime that you recommended crashes on broken programs that just so happen to run right now, YOU are going to get the grief and not the person who wrote the garbagey C/C++ code. A lot of C/C++ improvements floundered on this because they would convert "working" C/C++ programs into "broken" ones.
It's unfortunately not a complete answer to your question, but for what it's worth I asked Filip something similar several months ago and this was his response:
I’ve been thinking about this problem since 2004.
Here’s a rough timeline:
2004-2018: I had ideas of how to do it but I thought the whole premise (memory safe C) was idiotic.
2018-2023: I no longer thought the premise was idiotic but I couldn’t find a way to do it that would result in fanatical compatibility.
2023-2024: early Fil-C versions that were much less compatible and much less performant
end of 2024: InvisiCaps breakthrough that gives current fanatical compatibility and “ok” performance.
It’s a hard problem. Lots of folks have tried to find a way to do it. I’ve tried many approaches before finding the current one.
I asked a follow-up question about what changed his mind about the idiotic-ness of the idea around 2018, and his response was:
The observation that the C variants used on GPUs are simplistic takes on memory safe C
I think 20 years ago computers were slower and people were much more skittish about overhead for safety.
"if Rust folks really cared about memory safety, they would promote Fil-C and ditch Rust as Fil-C is safer, otherwise they just care about their new shiny language, and not memory safety"
I think the most charitable version of this is that now that Fil-C exists, maybe we should abort the collective efforts to rewrite the world in Rust, and go back to using C and C++, so we can have an unfragmented ecosystem of libraries in those languages as we did before Rust came along. Of course, Rust libraries can be used from C and C++, but some C and C++ programmers don't want to use Rust libraries. So, the thinking goes, if we Rust users would just give up on Rust and acknowledge that there's a better solution now, this division wouldn't exist.
But of course, Fil-C has tradeoffs that Rust doesn't, mainly the garbage collector (edit to add: and the fact that Fil-C currently only supports Linux on x86-64). And there are other reasons to use Rust besides memory-safety; Cargo and the lack of a global namespace are big ones for me. It's just unfortunate that there's this division between language camps that is connected to the broader culture war. Like, I just want to write a library that everyone will be happy to use.
Wouldn’t C and C++ already constitute a fragmented ecosystem by themselves? I could imagine some C programmers who wouldn’t want to use C++ libraries existing.
And I would think with Zig and Odin entering the picture fragmentation is there even if Rust vanished tomorrow. Or does Rust introduce a different kind of fragmentation?
It's just unfortunate that there's this division between language camps
I don't see it as unfortunate. Even if a lot of rust libraries are "reimplementing" things that already exist in C, they do it differently because they're coming with different cultural assumptions and goals, and those different approaches add something to the space.
but some C and C++ programmers don't want to use Rust libraries.
Isn't this a tacit admission that there's something different beyond just implementation language? As someone who has written a lot of C and Rust, there are things I like and dislike about the approaches of both and the cultures they foster, and I think the world would be a poorer if we were to lose either. Culture war isn't the inevitable conclusion of cultural diversity.
I don't see it as unfortunate. Even if a lot of rust libraries are "reimplementing" things that already exist in C, they do it differently because they're coming with different cultural assumptions and goals, and those different approaches add something to the space.
And, sometimes, it's as simple as "zig cc hasn't taken over the world yet, so pure Rust dependencies are more convenient for a Rust project than C or C++ ones are".
Never underestimate the value of being able to trust that cargo build will Just Work™.
In that sense, cargo build sees success for the same reason Docker and Flatpak see success.
Like, I just want to write a library that everyone will be happy to use.
This is also the world I want, and I am working on a high-level transpiler from Rust to Zig that preserves generics as comptime. Rust is ideal source language because it encodes more constraints.
While it is true that it is currently Linux only on AMD64, there has been some recent work publicly discussed this last week to revive the ARM64 support. Have a look at the open PRs from 'pcc'.
As a general rule, the engineer who implements the code should get to decide how to implement the code. If you want to use Rust, use Rust. If you want to use C, you use C.
If there are two or more libraries - in Rust and in C - and they're functionally similar, they're in competition and the market will decide, and eventually one will "win".
As a general rule, performance will win, with memory safety only being an issue when exploitable via attacker-controlled input or the code being such a mess its regularly OOM'ing or segfaulting.
But this will always be bottom-up, hard for it to be top-down.
Never once had to use unsafe in rust. I use rust when i need something faster then php, don't want to reach for java because of the startup costs, and don't want to use C. Rust has all the memory safety of a garbage collected language with decent performance and actual machine code instead of vm bytecode.
Fil-C and Zig are very interesting, but i see no reason for adding another language stack to my repo (i have to take care of CI, deployment ect..) just because it's interesting or allows memory safety in use cases i don't need.
The whole memory safety language war is IMHO a combination of marketing, pissing context and huge egos. Reading about this topic does feel more like reading tabloids and less like engineers discussing technical tradeoffs.
Fil-C lets you run LibreOffice with memory safety. Rust can't do that, because Rust is not C++. Good luck rewriting LibreOffice in Rust.
Fil-C is an enormous advance. People seem to evaluate it on the level of "would I use it instead of Rust for new projects", but even if answer is no, it nearly does not diminish the value of Fil-C.
The thing I understand the least about Rust vs Fil-C comparisons is the implicit assumption that the entire point of memory safety is preventing UAF and such. But what about correctness and reliability?
If I write code in Fil-C, perhaps I'll have a guarantee that it won't lead to a security vulnerability, but I'll never know if my code actually works, or if there's a scenario where data doesn't quite have the right lifetimes and the program will crash in runtime.
If I write code in Rust, then sure, I'll have to trust some unsafe abstractions, but I also get the guarantee that if the foundations are correct, my code won't crash due to lifetime issues, ever. Any bugs will be caught in compile time and I can sleep safely.
Sure, Rust code can still crash on out-of-bounds accesses, but so can Fil-C, and generally speaking that kind of logic is much easier to audit than pointers. And if you really care, you can use branded indices to validate bounds in compile-time as well.
You build reliable software by designing a rigid structure with clear invariants and responsibilities. Sound compile-time checks give you that pretty much for free, the world's best runtime sanitizer can't.
but I'll never know if my code actually works, or if there's a scenario where data doesn't quite have the right lifetimes and the program will crash in runtime.
Something interesting I saw Filip say is that Fil-C lets him write C/C++ in a more "managed language style" that reduces/eliminates such worries. For example:
When I write new code in Fil-C, I just lean into the GC all the way, which makes programming in C and C++ so much nicer. I don’t ref count, I don’t use smart pointers, I don’t free and I don’t delete. It makes these languages so much nicer!
Also, Fil-C’s guarantee that it will panic on OOB or if a race goes badly means I spend basically zero time debugging memory safety issues. The reliability of the failures totally changes the dev experience for the better.
"Turnabout is fair play"
Or as the poster put it
When seeing the title of this post, I bet in some people's minds, the first thought was "Rust devs!". This connection is not unfounded.
Exactly. The Rust community has been very holier-than-thou on the memory safety front and quite absolutist ("how dare you code in non-Rust, don't you care about memory safety? You must be a bad person").
Now that it turns out that there is an alternative that is even safer, we suddenly get "well, it's a bit more complicated, memory safety isn't everything, you have to look at the broader context and requirements"
Excellent!
Glad you've come around to that point of view, dear Rust community. Now let's have civilized discussions about trade-offs.